美文网首页Flutter圈子
Flutter - 从0到完成一个App(一)

Flutter - 从0到完成一个App(一)

作者: YW_Drenched | 来源:发表于2020-03-05 14:36 被阅读0次

本文主要针对有Dart和Flutter基础的小伙伴,Dart和Flutter基础,废话不多说,开撸。

文件配置

1.打开终端,cd到你想创建的根目录,然后执行flutter create xxxx(xxx为项目名),如显示如下图,即项目创建成功。

图1

2.这里我使用的VSCode,用VSCode打开创建的项目,在lib目录下创建config、pages、tools文件夹

  • config 用来配置网络请求以及路由文件
  • pages 根据需求自己定义的页面以及组件
  • tools 存放自己封装的工具类
图2
路由配置

在config文件夹下创建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 在这个Map对象中配置创建好的页面
  • onGenerateRoute这个方法是谷歌官方为了配置路由为我们提供的,所以直接复制就可以了。有兴趣的小伙伴可以研究下。
工具代码封装

Flutter为我们提供了很多优秀的第三方库,这里我们使用的第三方网络库是Dio,当然也有很多其他的库如http,小伙伴可以https://pub.flutter-io.cn/查找

网络工具

1.在pubspec.yaml的依赖中导入Dio

图3
2.在config文件夹中创建service_methon.dart,在service_methon.dart中对Dio按我们自己的需要做一点小处理
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 网络请求类型
  • url 网络请求地址
  • {Map dataDic} 网络请求参数(可选参数)
  • Future 是在未来某个时间获得想要对象的一种手段。简单来说,就是我们能够通过它在某个时间点获得异步任务中返回的值。实际上,就是给 Future 设置回调函数,当异步任务执行完成后,会调用回调函数。

cyw_getNetworkData此方法是一个异步的网络请求方法,在Dart中方法名后要加上async关键字

3.config文件夹中创建service_url.dart,在service_url.dart配置URL

图4
md5加密类

tools文件中创建encrypt.dart,Dart中直接为我们提供了MD5加密,导入相关库就可以直接使用了

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);
  // 这里其实就是 digest.toString()
  return hex.encode(digest.bytes);
}
本地图片配置

1.准备本地图片,在根目录下创建images文件夹,并准备2x,3x文件夹(高清图准备),在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

个人觉得Flutter本地图片配置相对于iOS有点憨,不知道以后会不会有改进。

以上基本准备工作就完成了,接下来步入正题。

登录注册界面
登录首页界面
效果图1

1.在tools文件夹下创建iosTypeButton.dart,并封装此按钮组件,个人喜欢iOS风格,所以就创建了cupertino风格的,代码如下:

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.进入main.dart,把Flutter自动生成的代码删除,按照如下方式配置路由和界面

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固定的函数
    );
  }
}

3.创建登录页面的首页界面,即loginMain页面。在pages文件夹中创建loginMianPage.dart,并导入之前封装好的按钮组件,代码如下:

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, '用户注册', () {
              // Navigator.of(context).pushNamed('/registPage');
              print('用户注册');
            },
                textColor: Color.fromRGBO(102, 102, 102, 1),
                bgColor: Color.fromRGBO(243, 243, 243, 1),
                fontSize: 16,
                radius: 0),
          ],
        ),
      ),
    );
  }
}

4.点击事件路由配置。在pages文件夹下面创建loginPage.dartregistPage.dart,然后在config-route.dart配置需要跳转的路由:

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')打开就可以实现页面的跳转了。
至此,此界面功能基本完成。

登录界面
效果图2

1.此界面会用到2个第三库:
shared_preferences用来存储用户登录信息,类似iOS中NSUserDefaults
fluttertoast 一个轻量化的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();
        },
      ),
    );
  }

// 账号登录
  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(
            '账号登录',
            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: '请输入手机号码',
                keyboardType: TextInputType.phone,
                controller: mobileVC),
            Divider(
              color: Colors.black26,
            ),
            SizedBox(
              height: 20.0,
            ),
            _creatMyText(
                placeholder: '请输入密码',
                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.界面写好后,就需要处理点击登录按钮后的网络请求了

  TextEditingController mobileVC = TextEditingController();
  TextEditingController pwdVC = TextEditingController();
  String mobile = '';//用来存储页面textFiled的值
  String pwd = '';//用来存储页面textFiled的值

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

    // 监听mobleTextField
    mobileVC.addListener(() {
      setState(() {
        this.mobile = mobileVC.text;
      });
    });
    // 监听pwdTextField
    pwdVC.addListener(() {
      setState(() {
        this.pwd = pwdVC.text;
      });
    });
  }
// 登录按钮点击事件
  void _loginBtnClick() {
    print('${this.mobile} - ${this.pwd}');
    if (this.mobile.length != 11) {
      Fluttertoast.showToast(msg: '手机号码格式错误', 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);
      }
    });
  }
// 存储用户信息
  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用来监听mobileTextField文本改变
  • Navigator.pushNamedAndRemoveUntil push到下一个页面,并将前面路由清空。这样push导航栏上就不会有默认的返回按钮。
注册界面
效果图3

注册界面跟登录界面很相似,复用的代码也很多,下面就直接粘代码了:

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;//倒计时初始时间
  String _getCodeString = '获取验证码';
  bool _hasTime = true;//用来判断是否在倒计时中

  @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() { 
    // 页面销毁时销毁定时器
    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();
        },
      ),
    );
  }

// 账号登录
  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(
            '注册',
            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,
    );
  }

// 验证码
  Widget _codeTextFiled() {
    return Stack(
      children: <Widget>[
        _creatMyText(
            placeholder: '请输入手机号码',
            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,
        )
      ],
    );
  }

// 点击获取验证码
  void _getCode() {
    if (_mobile.length != 11) {
      Fluttertoast.showToast(msg: '手机号格式错误');
      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('验证码已经发送');
        this._reGetCountdown();
      } else {
        print('发送失败');
      }
    });
  }

// 倒计时
  void _reGetCountdown() {
    setState(() {
      if (_countdownTimer != null) {
        return;
      }
      _hasTime = false;
      // Timer的第一秒倒计时是有一点延迟的,为了立刻显示效果可以添加下一行。
      _getCodeString = '${_allTime--}重新获取';
      _countdownTimer = Timer.periodic(new Duration(seconds: 1), (timer) {
        setState(() {
          if (_allTime > 0) {
            _getCodeString = '${_allTime--}S重新获取';
          } else {
            _getCodeString = '获取验证码';
            _allTime = 59;
            _countdownTimer.cancel();
            _countdownTimer = null;
            _hasTime = true;
          }
        });
      });
    });
  }

// 点击注册按钮
  _registBtnClick(){
    if(_pwd.length == 0 || _code.length == 0){
      Fluttertoast.showToast(msg: '填写信息不能为空');
      return;
    }
    if (_mobile.length != 11) {
      Fluttertoast.showToast(msg: '手机号格式错误');
      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: '注册成功');
        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: '请输入验证码',
                  keyboardType: TextInputType.number,
                  controller: _codeVC),
              Divider(
                color: Colors.black26,
              ),
              SizedBox(height: 20),
              _creatMyText(
                  placeholder: '请输入密码',
                  keyboardType: TextInputType.number,
                  obscureText: true,
                  controller: _pwdVC),
              Divider(
                color: Colors.black26,
              ),
              SizedBox(height: 50),
              iosTypeBtn(360, 60, '注册', _registBtnClick,
                  textColor: Colors.white,
                  bgColor: Color.fromRGBO(58, 184, 228, 1),
                  fontSize: 16,
                  radius: 0),
            ],
          )),
    );
  }
}

这里值得一提的是在Flutter定时器的使用,在periodic方法中一定要记住再次调用setState刷新页面UI。

      _countdownTimer = Timer.periodic(new Duration(seconds: 1), (timer) {
        setState(() {
          if (_allTime > 0) {
            _getCodeString = '${_allTime--}S重新获取';
          } else {
            _getCodeString = '获取验证码';
            _allTime = 59;
            _countdownTimer.cancel();
            _countdownTimer = null;
            _hasTime = true;
          }
        });
      });

至此,从文件配置到登录注册界面已完成,如有造成了错误、误解的代码望大家谅解,最后希望能留下你宝贵的建议。

相关文章

  • Flutter - 从0到完成一个App(一)

    本文主要针对有Dart和Flutter基础的小伙伴,Dart和Flutter基础,废话不多说,开撸。 文件配置 1...

  • 零基础三分钟写一个Flutter App

    这个教程是面向完全没有接触过Flutter开发,从0开始搭建Flutter开发环境到写第一个Flutter app...

  • Flutter App 从0 到 1

    指定ios 和 android 的语言创建项目 flutter1.9 以后,默认 ios 项目的语言为 swift...

  • 如何从0到1完成 app 设计

    作为互联网行业的设计师,不仅仅需要完成用户界面(User Interface,简称 UI)的视觉设计,还应该具备产...

  • flutter 从0到1

    flutter 是什么 flutter是Google基于Dart语言开发的移动应用开发框架,在保持原生性能的条件下...

  • Flutter从0到1

    01: Mac环境配置[https://www.jianshu.com/writer#/notebooks/504...

  • Flutter设置APP版本与构建版本

    当一个纯Flutter APP开发完成,我们要打包发布到App Store和各大安卓市场,这时候我们需要设置APP...

  • Mac配置fluter开发环境

    配置flutter开发环境我主要参考的是这篇文章mac flutter 开发环境配置 从0到1 流程,对于一个初学...

  • Flutter: 完成一个图片 APP

    Flutter: 完成一个图片APP 自从 Flutter 推出之后, 一直是备受关注, 有看好的也有不看好的, ...

  • Flutter--从0到1,实战Flutter

    作为一位刚入门Flutter,实战完成了一个项目的iOS开发者,今天在这里和大家来聊聊如何从0到1,实战Flutt...

网友评论

    本文标题:Flutter - 从0到完成一个App(一)

    本文链接:https://www.haomeiwen.com/subject/fsfmoctx.html