Flutter - 從0到完成一個(gè)App(一)

本文主要針對(duì)有Dart和Flutter基礎(chǔ)的小伙伴,Dart和Flutter基礎(chǔ),廢話不多說,開擼。

文件配置

1.打開終端,cd到你想創(chuàng)建的根目錄,然后執(zhí)行flutter create xxxx(xxx為項(xiàng)目名),如顯示如下圖,即項(xiàng)目創(chuàng)建成功。

圖1

2.這里我使用的VSCode,用VSCode打開創(chuàng)建的項(xiàng)目,在lib目錄下創(chuàng)建config、pages、tools文件夾

  • config 用來配置網(wǎng)絡(luò)請(qǐng)求以及路由文件
  • pages 根據(jù)需求自己定義的頁(yè)面以及組件
  • tools 存放自己封裝的工具類
圖2
路由配置

在config文件夾下創(chuàng)建route.dart ,在route.dart中配置路由

import 'package:flutter/material.dart';

final routes = {
  
};
// ignore: strong_mode_top_level_function_literal_block
var onGenerateRoute = (RouteSettings settings) {
  final String name = settings.name;
  final Function pageContentBuilder = routes[name];
  // print(name);
  if (pageContentBuilder != null) {
    if (settings.arguments != null) {
      final Route route = MaterialPageRoute(
          builder: (context) =>
              pageContentBuilder(context, arguments: settings.arguments));
      return route;
    } else {
      final Route route =
          MaterialPageRoute(builder: (context) => pageContentBuilder(context));
      return route;
    }
  }
};
  • routes 在這個(gè)Map對(duì)象中配置創(chuàng)建好的頁(yè)面
  • onGenerateRoute這個(gè)方法是谷歌官方為了配置路由為我們提供的,所以直接復(fù)制就可以了。有興趣的小伙伴可以研究下。
工具代碼封裝

Flutter為我們提供了很多優(yōu)秀的第三方庫(kù),這里我們使用的第三方網(wǎng)絡(luò)庫(kù)是Dio,當(dāng)然也有很多其他的庫(kù)如http,小伙伴可以https://pub.flutter-io.cn/查找

網(wǎng)絡(luò)工具

1.在pubspec.yaml的依賴中導(dǎo)入Dio

圖3

2.在config文件夾中創(chuàng)建service_methon.dart,在service_methon.dart中對(duì)Dio按我們自己的需要做一點(diǎn)小處理

import 'package:dio/dio.dart';
import 'dart:async';
import 'dart:io';

Future cyw_getNetworkData(String type, String url, {Map dataDic}) async {
  Response res;
  if (type == 'POST') {
    try {
      res = await Dio().post(url, data: dataDic);
      return res;
    } catch (e) {
      print(e);
    }
  } else if (type == 'GET') {
    try {
      res = await Dio().get(url);
      return res;
    } catch (e) {
      print(e);
    }
  }
}
  • type 網(wǎng)絡(luò)請(qǐng)求類型
  • url 網(wǎng)絡(luò)請(qǐng)求地址
  • {Map dataDic} 網(wǎng)絡(luò)請(qǐng)求參數(shù)(可選參數(shù))
  • Future 是在未來某個(gè)時(shí)間獲得想要對(duì)象的一種手段。簡(jiǎn)單來說,就是我們能夠通過它在某個(gè)時(shí)間點(diǎn)獲得異步任務(wù)中返回的值。實(shí)際上,就是給 Future 設(shè)置回調(diào)函數(shù),當(dāng)異步任務(wù)執(zhí)行完成后,會(huì)調(diào)用回調(diào)函數(shù)。

cyw_getNetworkData此方法是一個(gè)異步的網(wǎng)絡(luò)請(qǐng)求方法,在Dart中方法名后要加上async關(guān)鍵字

3.config文件夾中創(chuàng)建service_url.dart,在service_url.dart配置URL

圖4

md5加密類

tools文件中創(chuàng)建encrypt.dart,Dart中直接為我們提供了MD5加密,導(dǎo)入相關(guān)庫(kù)就可以直接使用了

import 'dart:convert';
import 'package:convert/convert.dart';
import 'package:crypto/crypto.dart';


// md5 加密
String generateMd5(String data) {
  var content = new Utf8Encoder().convert(data);
  var digest = md5.convert(content);
  // 這里其實(shí)就是 digest.toString()
  return hex.encode(digest.bytes);
}
本地圖片配置

1.準(zhǔn)備本地圖片,在根目錄下創(chuàng)建images文件夾,并準(zhǔn)備2x,3x文件夾(高清圖準(zhǔn)備),在images文件中存放圖片

圖5

2.在pubspec.yaml -assets中配置本地圖片

  assets:
    - images/logo.png
    - images/back.png
    - images/personal.png
    - images/home_bg.png
    - images/ai-i.png
    - images/bangzhu.png
    - images/chakanmingxi.png
    - images/jilu.png
    - images/qianbao.png
    - images/yijianfankui.png
    - images/pic_head.png
    - images/ic_right.png

個(gè)人覺得Flutter本地圖片配置相對(duì)于iOS有點(diǎn)憨,不知道以后會(huì)不會(huì)有改進(jìn)。

以上基本準(zhǔn)備工作就完成了,接下來步入正題。

登錄注冊(cè)界面
登錄首頁(yè)界面
效果圖1

1.在tools文件夾下創(chuàng)建iosTypeButton.dart,并封裝此按鈕組件,個(gè)人喜歡iOS風(fēng)格,所以就創(chuàng)建了cupertino風(fēng)格的,代碼如下:

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';


class iosTypeBtn extends StatelessWidget {
  double height;
  double width;
  String name;
  Color bgColor;
  Color textColor;
  double fontSize = 14;
  double radius = 0;
  Function onPressed;

  iosTypeBtn(this.width, this.height, this.name, this.onPressed,
      {this.bgColor, this.radius, this.fontSize, this.textColor});
  @override
  Widget build(BuildContext context) {
    // TODO: implement build
    return Container(
      width: this.width,
      height: this.height,
      child: CupertinoButton(
        child: Text(
          this.name,
          style: TextStyle(color: this.textColor, fontSize: this.fontSize),
        ),
        color: this.bgColor,
        onPressed: this.onPressed,
        borderRadius: BorderRadius.all(Radius.circular(this.radius)),
      ),
    );
  }
}

2.進(jìn)入main.dart,把Flutter自動(dòng)生成的代碼刪除,按照如下方式配置路由和界面

import 'package:flutter/material.dart';
import 'config/route.dart';

void main(){
  return runApp(MyApp());
}


class MyApp extends StatelessWidget {
  const MyApp({Key key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,//是否顯示debug banner
      initialRoute: '/loginMain',//第一次加載顯示的路由
      routes:routes,//配置的路由
      onGenerateRoute: onGenerateRoute, //傳入google固定的函數(shù)
    );
  }
}

3.創(chuàng)建登錄頁(yè)面的首頁(yè)界面,即loginMain頁(yè)面。在pages文件夾中創(chuàng)建loginMianPage.dart,并導(dǎo)入之前封裝好的按鈕組件,代碼如下:

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import '../tools/iosTypeButton.dart';

class LoginMainPage extends StatelessWidget {
  const LoginMainPage({Key key}) : super(key: key);


// logo展示 自定義方法google建議帶下劃線
  Widget _logo(context) {
    return Container(
      width: MediaQuery.of(context).size.width,
      child: Image.asset('images/logo.png'),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Container(
        color: Colors.white,
        child: Column(
          children: <Widget>[
            _logo(context),
            iosTypeBtn(360, 60, '用戶登錄', () {
              // Navigator.of(context).pushNamed('/loginPage');
              print('用戶登錄');
            },
                textColor: Colors.white,
                bgColor: Color.fromRGBO(58, 184, 228, 1),
                fontSize: 16,
                radius: 0),
            SizedBox(
              height: 20,
            ),
            iosTypeBtn(360, 60, '用戶注冊(cè)', () {
              // Navigator.of(context).pushNamed('/registPage');
              print('用戶注冊(cè)');
            },
                textColor: Color.fromRGBO(102, 102, 102, 1),
                bgColor: Color.fromRGBO(243, 243, 243, 1),
                fontSize: 16,
                radius: 0),
          ],
        ),
      ),
    );
  }
}

4.點(diǎn)擊事件路由配置。在pages文件夾下面創(chuàng)建loginPage.dartregistPage.dart,然后在config-route.dart配置需要跳轉(zhuǎn)的路由:

import 'package:flutter/material.dart';
import '../pages/loginMainPage.dart';
import '../pages/loginPage.dart';
import '../pages/registPage.dart';

final routes = {
  '/loginMain':(context) => LoginMainPage(),
  '/loginPage':(context) => LoginPage(),
  '/registPage':(context) => RegistPage(),
};

最后將loginMianPage中注釋的 Navigator.of(context).pushNamed('/registPage')Navigator.of(context).pushNamed('/loginPage')打開就可以實(shí)現(xiàn)頁(yè)面的跳轉(zhuǎn)了。
至此,此界面功能基本完成。

登錄界面
效果圖2

1.此界面會(huì)用到2個(gè)第三庫(kù):
shared_preferences用來存儲(chǔ)用戶登錄信息,類似iOS中NSUserDefaults
fluttertoast 一個(gè)輕量化的Toast彈窗
按官方文檔集成即可。

2.界面代碼如下:

 // 返回按鈕
  Widget _backBtn(context) {
    return Container(
      height: 24.0,
      width: 24.0,
      child: InkWell(
        child: Image.asset('images/back.png'),
        onTap: () {
          Navigator.of(context).pop();
        },
      ),
    );
  }

// 賬號(hào)登錄
  Widget _topWidget() {
    return Container(
      child: Stack(
        children: <Widget>[
          Positioned(
            child: Container(
              width: 130.0,
              height: 8.0,
              color: Color.fromRGBO(58, 184, 228, 1),
            ),
            bottom: 5,
          ),
          Text(
            '賬號(hào)登錄',
            style: TextStyle(
              fontSize: 34,
              fontWeight: FontWeight.w600,
            ),
          ),
        ],
      ),
    );
  }

// textFiled
  Widget _creatMyText(
      {String placeholder,
      bool obscureText = false,
      TextEditingController controller,
      keyboardType}) {
    return TextField(
      decoration: InputDecoration(
        hintText: placeholder,
        border: InputBorder.none,
      ),
      obscureText: obscureText,
      controller: controller,
      keyboardType: keyboardType,
    );
  }
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Container(
        margin: EdgeInsets.fromLTRB(30, 80, 30, 0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: <Widget>[
            _backBtn(context),
            SizedBox(
              height: 50.0,
            ),
            _topWidget(),
            SizedBox(
              height: 60.0,
            ),
            _creatMyText(
                placeholder: '請(qǐng)輸入手機(jī)號(hào)碼',
                keyboardType: TextInputType.phone,
                controller: mobileVC),
            Divider(
              color: Colors.black26,
            ),
            SizedBox(
              height: 20.0,
            ),
            _creatMyText(
                placeholder: '請(qǐng)輸入密碼',
                keyboardType: TextInputType.phone,
                obscureText: true,
                controller: pwdVC),
            Divider(
              color: Colors.black26,
            ),
            SizedBox(
              height: 50,
            ),
            iosTypeBtn(360, 60, '登錄', _loginBtnClick,
                textColor: Colors.white,
                bgColor: Color.fromRGBO(58, 184, 228, 1),
                fontSize: 16,
                radius: 0),
          ],
        ),
      ),
    );
  }
}

3.界面寫好后,就需要處理點(diǎn)擊登錄按鈕后的網(wǎng)絡(luò)請(qǐng)求了

  TextEditingController mobileVC = TextEditingController();
  TextEditingController pwdVC = TextEditingController();
  String mobile = '';//用來存儲(chǔ)頁(yè)面textFiled的值
  String pwd = '';//用來存儲(chǔ)頁(yè)面textFiled的值

  @override
  void initState() {
    super.initState();

    // 監(jiān)聽mobleTextField
    mobileVC.addListener(() {
      setState(() {
        this.mobile = mobileVC.text;
      });
    });
    // 監(jiān)聽pwdTextField
    pwdVC.addListener(() {
      setState(() {
        this.pwd = pwdVC.text;
      });
    });
  }
// 登錄按鈕點(diǎn)擊事件
  void _loginBtnClick() {
    print('${this.mobile} - ${this.pwd}');
    if (this.mobile.length != 11) {
      Fluttertoast.showToast(msg: '手機(jī)號(hào)碼格式錯(cuò)誤', gravity: ToastGravity.CENTER);
      return;
    }
    if (this.pwd.length == 0) {
      Fluttertoast.showToast(msg: '密碼不能為空', gravity: ToastGravity.CENTER);
      return;
    }

    Map dataMap = {'mobile': this.mobile, 'password': generateMd5(this.pwd)};
    cyw_getNetworkData('POST', servicePath['loginPath'], dataDic: dataMap)
        .then((val) {
      var result = json.decode(val.toString());
      print(result);
      if (result['returnCode'] == '0000') {
        Fluttertoast.showToast(msg: '登錄成功', gravity: ToastGravity.CENTER);
        _saveUserInfo(result);
        // push到homePage,并將前面路由清空
        Navigator.pushNamedAndRemoveUntil(context, '/homePage', null);
      } else {
        Fluttertoast.showToast(msg: '登錄失敗', gravity: ToastGravity.CENTER);
      }
    });
  }
// 存儲(chǔ)用戶信息
  void _saveUserInfo(userInfo) async{
    // SharedPreferences 類似iOS中NSUserDefaults
    SharedPreferences prefs = await SharedPreferences.getInstance();
        prefs.setString('userId', userInfo['retnrnJson']['id']);
        prefs.setBool('isLogin', true);
        prefs.setString('userName', userInfo['retnrnJson']['userName']);
        prefs.setString('password', userInfo['retnrnJson']['password']);
        prefs.setString('mobile', userInfo['retnrnJson']['mobile']);
  }
  • mobileVC.addListener用來監(jiān)聽mobileTextField文本改變
  • Navigator.pushNamedAndRemoveUntil push到下一個(gè)頁(yè)面,并將前面路由清空。這樣push導(dǎo)航欄上就不會(huì)有默認(rèn)的返回按鈕。
注冊(cè)界面
效果圖3

注冊(cè)界面跟登錄界面很相似,復(fù)用的代碼也很多,下面就直接粘代碼了:

import 'package:flutter/material.dart';
import 'package:flutter_app05/config/service_methon.dart';
import 'dart:async';
import 'package:fluttertoast/fluttertoast.dart';
import '../tools/encrypt.dart';
import '../config/service_url.dart';
import 'dart:convert';
import '../tools/iosTypeButton.dart';
class RegistPage extends StatefulWidget {
  RegistPage({Key key}) : super(key: key);

  @override
  _RegistPageState createState() => _RegistPageState();
}

class _RegistPageState extends State<RegistPage> {
  TextEditingController _mobileVC = TextEditingController();
  TextEditingController _codeVC = TextEditingController();
  TextEditingController _pwdVC = TextEditingController();
  String _mobile = '';
  String _code = '';
  String _pwd = '';
  Timer _countdownTimer;
  int _allTime = 59;//倒計(jì)時(shí)初始時(shí)間
  String _getCodeString = '獲取驗(yàn)證碼';
  bool _hasTime = true;//用來判斷是否在倒計(jì)時(shí)中

  @override
  void initState() {
    super.initState();
    _mobileVC.addListener(() {
      setState(() {
        _mobile = _mobileVC.text;
      });
    });
    _codeVC.addListener(() {
      setState(() {
        _code = _codeVC.text;
      });
    });
    _pwdVC.addListener(() {
      setState(() {
        _pwd = _pwdVC.text;
      });
    });
  }

  @override
  void dispose() { 
    // 頁(yè)面銷毀時(shí)銷毀定時(shí)器
    if (_countdownTimer != null) {
      _countdownTimer = null;
      _countdownTimer.cancel();
    }
    super.dispose();
  }

  // 返回按鈕
  Widget _backBtn(context) {
    return Container(
      height: 24.0,
      width: 24.0,
      child: InkWell(
        child: Image.asset('images/back.png'),
        onTap: () {
          Navigator.of(context).pop();
        },
      ),
    );
  }

// 賬號(hào)登錄
  Widget _topWidget() {
    return Container(
      child: Stack(
        children: <Widget>[
          Positioned(
            child: Container(
              width: 80.0,
              height: 8.0,
              color: Color.fromRGBO(58, 184, 228, 1),
            ),
            bottom: 5,
          ),
          Text(
            '注冊(cè)',
            style: TextStyle(
              fontSize: 34,
              fontWeight: FontWeight.w600,
            ),
          ),
        ],
      ),
    );
  }

// textFiled
  Widget _creatMyText(
      {String placeholder,
      bool obscureText = false,
      TextEditingController controller,
      keyboardType}) {
    return TextField(
      decoration: InputDecoration(
        hintText: placeholder,
        border: InputBorder.none,
      ),
      obscureText: obscureText,
      controller: controller,
      keyboardType: keyboardType,
    );
  }

// 驗(yàn)證碼
  Widget _codeTextFiled() {
    return Stack(
      children: <Widget>[
        _creatMyText(
            placeholder: '請(qǐng)輸入手機(jī)號(hào)碼',
            keyboardType: TextInputType.number,
            controller: _mobileVC),
        Positioned(
          child: InkWell(
            child: Text(
              _getCodeString,
              style: TextStyle(
                  color: Color.fromRGBO(58, 184, 228, 1), fontSize: 15),
            ),
            onTap: _hasTime ? this._getCode : () {},
          ),
          right: 20,
          top: 10,
        )
      ],
    );
  }

// 點(diǎn)擊獲取驗(yàn)證碼
  void _getCode() {
    if (_mobile.length != 11) {
      Fluttertoast.showToast(msg: '手機(jī)號(hào)格式錯(cuò)誤');
      return;
    }
    _reGetCountdown();
    cyw_getNetworkData("POST", servicePath['sendCode'],
        dataDic: {'mobile': this._mobile, 'type': '2'}).then((result) {
      var data = json.decode(result.toString());
      print(data);
      if (data['returnCode'] == '0000') {
        print('驗(yàn)證碼已經(jīng)發(fā)送');
        this._reGetCountdown();
      } else {
        print('發(fā)送失敗');
      }
    });
  }

// 倒計(jì)時(shí)
  void _reGetCountdown() {
    setState(() {
      if (_countdownTimer != null) {
        return;
      }
      _hasTime = false;
      // Timer的第一秒倒計(jì)時(shí)是有一點(diǎn)延遲的,為了立刻顯示效果可以添加下一行。
      _getCodeString = '${_allTime--}重新獲取';
      _countdownTimer = Timer.periodic(new Duration(seconds: 1), (timer) {
        setState(() {
          if (_allTime > 0) {
            _getCodeString = '${_allTime--}S重新獲取';
          } else {
            _getCodeString = '獲取驗(yàn)證碼';
            _allTime = 59;
            _countdownTimer.cancel();
            _countdownTimer = null;
            _hasTime = true;
          }
        });
      });
    });
  }

// 點(diǎn)擊注冊(cè)按鈕
  _registBtnClick(){
    if(_pwd.length == 0 || _code.length == 0){
      Fluttertoast.showToast(msg: '填寫信息不能為空');
      return;
    }
    if (_mobile.length != 11) {
      Fluttertoast.showToast(msg: '手機(jī)號(hào)格式錯(cuò)誤');
      return;
    }
    Map dataDic = {'mobile':_mobile,'userName':'','password':generateMd5(_pwd),'code':_code};
    cyw_getNetworkData("POST", servicePath['signUpPath'],dataDic: dataDic).then((val){
      var result = json.decode(val.toString());
      print(result);
      if (result['returnCode'] == '0000'){
        Fluttertoast.showToast(msg: '注冊(cè)成功');
        Navigator.of(context).pop();

      }else{
        Fluttertoast.showToast(msg: result['returnMsg:']);
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Container(
          margin: EdgeInsets.fromLTRB(30, 80, 30, 0),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: <Widget>[
              _backBtn(context),
              SizedBox(
                height: 50.0,
              ),
              _topWidget(),
              SizedBox(height: 60),
              _codeTextFiled(),
              Divider(
                color: Colors.black26,
              ),
              SizedBox(height: 20),
              _creatMyText(
                  placeholder: '請(qǐng)輸入驗(yàn)證碼',
                  keyboardType: TextInputType.number,
                  controller: _codeVC),
              Divider(
                color: Colors.black26,
              ),
              SizedBox(height: 20),
              _creatMyText(
                  placeholder: '請(qǐng)輸入密碼',
                  keyboardType: TextInputType.number,
                  obscureText: true,
                  controller: _pwdVC),
              Divider(
                color: Colors.black26,
              ),
              SizedBox(height: 50),
              iosTypeBtn(360, 60, '注冊(cè)', _registBtnClick,
                  textColor: Colors.white,
                  bgColor: Color.fromRGBO(58, 184, 228, 1),
                  fontSize: 16,
                  radius: 0),
            ],
          )),
    );
  }
}

這里值得一提的是在Flutter定時(shí)器的使用,在periodic方法中一定要記住再次調(diào)用setState刷新頁(yè)面UI。

      _countdownTimer = Timer.periodic(new Duration(seconds: 1), (timer) {
        setState(() {
          if (_allTime > 0) {
            _getCodeString = '${_allTime--}S重新獲取';
          } else {
            _getCodeString = '獲取驗(yàn)證碼';
            _allTime = 59;
            _countdownTimer.cancel();
            _countdownTimer = null;
            _hasTime = true;
          }
        });
      });

至此,從文件配置到登錄注冊(cè)界面已完成,如有造成了錯(cuò)誤、誤解的代碼望大家諒解,最后希望能留下你寶貴的建議。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容