从 0 开始手把手带你搭建一套规范的 Flutter-mvp 项目工程环境

前言

随着Flutter2.0的发布,Flutter对桌面和Web的支持也正式宣布进入stable渠道。相信过不了多久,Flutter必会成为另一主流。前一阵子,基于个人的需要以及想真正的体验一下Flutter开发,本人就使用Flutter开发了一款记账类APP。对于这次开发的体验总结一下:就是爽!开发体验非常棒!还没尝试过的同学可以从本文开始学习,从0开始搭建一套规范的Flutter项目工程环境。

本文篇幅较长,会从以下几个方面展开:

  • 环境安装
  • 架构搭建
  • Flutter MVP规范
  • 常用插件
  • 代码规范
  • 提交规范(待定)
  • 单元测试
  • 打包发布

本项目完整的代码托管在 Gitee 仓库,欢迎点亮小星星。

文章目录

技术栈

  • 编程语言:Dart + Flutter
  • 路由工具:fluro: ^2.0.3
  • 网络请求库:dio: ^3.0.10
  • 接口服务封装工具:retrofit: 1.3.4+1
  • toast插件:fluttertoast: ^7.1.5
  • 状态管理:provider: ^4.3.3
  • 事件总线:^2.0.0

环境安装

配置与工具要求

  • 操作系统: Windows 7 或更高版本 (64-bit)
  • 磁盘空间: 2G.
  • 工具 : Flutter 依赖下面这些命令行工具.

获取Flutter SDK

去flutter官网下载其最新可用的安装包,点击下载

这里使用版本

解压如下:

image-20210421112227644

image-20210421112257965

配置环境变量

  • 我的电脑->右键属性->高级系统设置->环境设置

    • 系统变量找到Path追加flutter/bin

      image-20210421112547766

      image-20210421112737141

    • 用户环境变量添加PUB_HOSTED_URL和FLUTTER_STORAGE_BASE_URL

      PUB_HOSTED_URL=https://pub.flutter-io.cn
      FLUTTER_STORAGE_BASE_URL=https://storage.flutter-io.cn
      

      image-20210421113201288

运行flutter doctor

使用管理员身份运行git bash或PowerShell

image-20210421113358718

flutter doctor

效果如下:

image-20210421113615638

获取Android SDK

image-20210421141240356

这里使用版本

image-20210421141431738

安装相关工具

image-20210421154334350

image-20210421154358739

再次配置环境变量

  • 我的电脑->右键属性->高级系统设置->环境设置

    • 用户环境变量添加ANDROID_HOME

      image-20210421154913329

    • 系统变量找到Path追加platform-tools和tools

      主要是platform-tools/adb.exe

      image-20210421155108836

安装夜神模拟器

  1. 替换夜神模拟器adb.exe

将上面的platform-tools/adb.exe覆盖夜神模拟器的Nox/bin/nox_adb.exe

覆盖前先备份

image-20210421160525723

  1. 打开夜神模拟器

    image-20210421161556573

  2. 使用flutter devices查看设备情况

    flutter devices
    

    修改环境变量后,要重新打开git bash。

    image-20210421161318674

    这里会看到有三个设备,VOG AL10就是夜神模拟器,另外两个为浏览器。

    到此,环境安装完成。

安装VSCode

架构搭建

使用flutter create命令初始化项目雏形

使用flutter create命令创建一个project

# 默认为Kotlin语言,如果使用java语言,则需要-a参数
flutter create -a java fluttermvp
cd fluttermvp

默认工程目录如下图:

image-20210421152201316

运行应用程序
  • 检查Android设备是否在运行。如果没有显示

    flutter devices
    
  • 运行flutter run命令来运行应用程序

    flutter run
    

    因刚才安装的Android SDK build-tools工具版本不对,会报如下错

    image-20210421162308353

    打开SDK Manager.exe安装对应版本即可

    image-20210421162401874

    Android SDK Build-tools安装完后,还会报错,因为还有一个问题未解决。

    image-20210421165453458

    目前最高版本只有29,所以要只能选下载29的,然后再修改fluttermvp/android/app/gradle.bulid文件

    compileSdkVersion 30 ==> compileSdkVersion 29
    targetSdkVersion 30 ==> targetSdkVersion 29
    

    安装android SDK Platform

    image-20210421163329930

    image-20210421163347122

  • 如果 一切正常,在应用程序建成功后,您应该在您的设备或模拟器上看到应用程序:

    image-20210421170009511

使用VSCode打开工程

image-20210421170258717

暂时安装3个常用插件

image-20210421170658755

体验一波热重载

Flutter 可以通过 热重载(hot reload) 实现快速的开发周期,热重载就是无需重启应用程序就能实时加载修改后的代码,并且不会丢失状态(译者语:如果是一个web开发者,那么可以认为这和webpack的热重载是一样的)。简单的对代码进行更改,然后告诉IDE或命令行工具你需要重新加载(点击reload按钮),你就会在你的设备或模拟器上看到更改。

  1. 打开文件lib/main.dart
  2. 将字符串
    'You have pushed the button this many times:' 更改为
    'You have clicked the button this many times:'
  3. 不要按“停止”按钮; 让您的应用继续运行.
  4. 要查看您的更改,请调用 Save (cmd-s / ctrl-s), 或者点击 热重载按钮 (带有闪电图标的按钮).

你会立即在运行的应用程序中看到更新的字符串

将上前面运行的命令行关闭。使用VSCode启动调试

image-20210421171621618

image-20210421171940553

image-20210421172033459

Flutter配置文件

后续使用到再依次说明

name: fluttermvp
description: A new Flutter project.
publish_to: 'none' 
version: 1.0.0+1

environment:
  sdk: ">=2.7.0 <3.0.0"

dependencies:
  flutter:
    sdk: flutter
  cupertino_icons: ^1.0.2
dev_dependencies:
  flutter_test:
    sdk: flutter
flutter:
  uses-material-design: true

规范目录结构

├── android/                 # 安卓工程
├── ios/                     # ios工程
├── lib/                     # flutter&dart代码
	├── api/                 # 接口层
    ├── base/                # 基类
    ├── event/               # eventbus相关
    ├── http/                # http请求工具
    ├── iconfont/            # 阿里云矢量图标
    ├── model/               # 实体层
	├── modules/             # 功能模块
   	├── router/              # 路由
    └── tool/				 # 工具库
├── test/					 # 测试
├── web/                     # web工程
└── pubspec.yaml			 # flutter配置文件

为了后续导包统一,这里建议修改一下pubspec.yaml的name为app。修改后需要重启一下vscode,这样导包功能才生效。

name: app                       # 这里由之前的fluttermvp->app
description: A new Flutter project.
publish_to: 'none' 
version: 1.0.0+1

environment:
  sdk: ">=2.7.0 <3.0.0"

dependencies:
  flutter:
    sdk: flutter
  cupertino_icons: ^1.0.2
dev_dependencies:
  flutter_test:
    sdk: flutter
flutter:
  uses-material-design: true

集成路由工具fluro

fluro: ^2.0.3

image-20210421175321399

  1. 获取插件

image-20210421180028102

  1. 新建两个页面moudules/example/route_a.dartmoudules/example/RouterBPage.dart

image-20210510085428532

  1. stateful与stateless这里暂时不说区别,选stateful,输入名称

image-20210510085514493

  1. 快速修复,导包

image-20210510085539615

image-20210422093539323

image-20210510085614793

  1. 最后代码修改成如下:
import 'package:flutter/material.dart';

class RouterAPage extends StatefulWidget {
  @override
  _RouterAPageState createState() => _RouterAPageState();
}

class _RouterAPageState extends State<RouterAPage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("RouterA"),
      ),
      body: new Center(
        child: new Text("RouterA"),
      ),
    );
  }
}


  1. route_b.dart重复上述操作。

  2. 新建路由处理文件router/route_handles.dart

    import 'package:app/modules/example/route_b.dart';
    import 'package:fluro/fluro.dart';
    import 'package:flutter/material.dart';
    import 'package:app/main.dart';
    import 'package:app/modules/common/error.dart';
    import 'package:app/modules/example/route_a.dart';
    
    // 根页面
    var rootHandler = new Handler(
        handlerFunc: (BuildContext context, Map<String, List<String>> params) {
      return MyApp();
    });
    // 空页面
    var emptyHandler = new Handler(
        handlerFunc: (BuildContext context, Map<String, List<String>> params) {
      return ErrorPage();
    });
    // RouterPageA页面
    var routerAHandler = new Handler(
        handlerFunc: (BuildContext context, Map<String, List<String>> params) {
      return RouterAPage();
    });
    // RouterPageB页面
    var routerBHandler = new Handler(
        handlerFunc: (BuildContext context, Map<String, List<String>> params) {
      return RouterBPage();
    });
    
    
    

    上述的空页面请参考RouterPageA或RouterPageB的方式自行创建。

    这里要注意的是这里的导包

    import 'package:app/main.dart';

    app对应的就是pubspec.yaml的name,在没有修改之前就是

    import 'package:fluttermvp/main.dart';

  3. 新建路由配置文件router/routes.dart

    import 'package:app/router/router_handlers.dart';
    import 'package:fluro/fluro.dart';
    
    class Routes {
         
      static void configureRoutes(FluroRouter router) {
         
        //空页面
        router.notFoundHandler = emptyHandler;
        // 根页面
        router.define("/", handler: rootHandler);
        // RouterPageA
        router.define("/routerA", handler: routerAHandler);
        // RouterPageB
        router.define("/routerB", handler: routerBHandler);
      }
    }
    
    
    
  4. 新建路由工具类tool/NavTool.dart

    import 'package:fluro/fluro.dart';
    import 'package:flutter/material.dart';
    
    class NavTool {
         
      static FluroRouter router;
    
      /// 设置路由对象
      static void setRouter(FluroRouter router) {
         
        router = router;
      }
    
      /// 跳转到首页
      static void goRoot(BuildContext context) {
         
        router.navigateTo(context, "/", replace: true, clearStack: true);
      }
    
      /// 跳转到指定地址
      static void push(BuildContext context, String path,
          {
         bool replace = false, bool clearStack = false}) {
         
        FocusScope.of(context).unfocus();
        router.navigateTo(context, path,
            replace: replace,
            clearStack: clearStack,
            transition: TransitionType.native);
      }
    
      /// 跳转到指定地址,有回调
      static void pushResult(
          BuildContext context, String path, Function(Object) function,
          {
         bool replace = false, bool clearStack = false}) {
         
        FocusScope.of(context).unfocus();
        router
            .navigateTo(context, path,
                replace: replace,
                clearStack: clearStack,
                transition: TransitionType.native)
            .then((value) {
         
          if (value == null) {
         
            return;
          }
          function(value);
        }).catchError((onError) {
         
          print("$onError");
        });
      }
    
      /// 跳转到指定地址-传参
      static void pushArgumentResult(BuildContext context, String path,
          Object argument, Function(Object) function,
          {
         bool replace = false, bool clearStack = false}) {
         
        router
            .navigateTo(context, path,
                routeSettings: RouteSettings(arguments: argument),
                replace: replace,
                clearStack: clearStack)
            .then((value) {
         
          if (value == null) {
         
            return;
          }
          function(value);
        }).catchError((onError) {
         
          print("$onError");
        });
      }
    
      /// 跳转到指定地址-传参
      static void pushArgument(BuildContext context, String path, Object argument,
          {
         bool replace = false, bool clearStack = false}) {
         
        router.navigateTo(context, path,
            routeSettings: RouteSettings(arguments: argument),
            replace: replace,
            clearStack: clearStack);
      }
    
      /// 回退
      static void goBack(BuildContext context) {
         
        FocusScope.of(context).unfocus();
        Navigator.pop(context);
      }
    
      static void goBackWithParams(BuildContext context, result) {
         
        FocusScope.of(context).unfocus();
        Navigator.pop(context, result);
      }
    
      /// 替换当前地址
      static String changeToNavigatorPath(String registerPath,
          {
         Map<String, Object> params}) {
         
        if (params == null || params.isEmpty) {
         
          return registerPath;
        }
        StringBuffer bufferStr = StringBuffer();
        params.forEach((key, value) {
         
          bufferStr
            ..write(key)
            ..write("=")
            ..write(Uri.encodeComponent(value))
            ..write("&");
        });
        String paramStr = bufferStr.toString();
        paramStr = paramStr.substring(0, paramStr.length - 1);
        print("传递的参数  $paramStr");
        return "$registerPath?$paramStr";
      }
    }
    
  5. 入口页新增路由配置

    
    void main() {
      /// 配置路由开始
      FluroRouter router = FluroRouter();
      Routes.configureRoutes(router);
      NavTool.router = router;
    
      /// 入口
      runApp(MyApp());
    }
    
  6. 布局代码片段

    new RaisedButton(
        child: new Text("RouterA"),
        onPressed: () {
            NavTool.push(context, "/routerA");
        }),
    new RaisedButton(
        child: new Text("RouterB"),
        onPressed: () {
            NavTool.push(context, "/routerB");
        })
    
  7. 效果截图

    image-20210422102127010

    image-20210422102143909

    image-20210422102203330

    至此,路由算是集成完毕,路由的进一步学习这里就先不展开。

集成Flutter常用工具类

flustars: ^2.0.1

flustars依赖于Dart常用工具类库common_utils,以及对其他第三方库封装,致力于为大家分享简单易用工具类。如果你有好的工具类欢迎PR.
目前包含SharedPreferences Util, Screen Util, Directory Util, Widget Util, Image Util。

集成网络请求库dio+retrofit+json

因为dart不支持反射,确切说是Flutter 禁用了dart:mirror无法使用反射,所以在json to bean上处理并不是很友好,不过我们可以借助一些工具,通过命令在编译期触发,能尽可能的还原原生开发处理的舒适度。

dependencies环境依赖包:

dio: ^3.0.10

retrofit: 1.3.4+1

json_annotation: ^3.0.1

dev_dependencies环境依赖包:

retrofit_generator: 1.4.1+3

build_runner: ^1.7.3

json_serializable: ^3.1.1

安装Json To Dart插件

该插件可以将json转成Dart 的bean

image-20210508103514032

插件小试:

{
   
    "userId": 1,
    "userName": "张三",
    "avatar": ""
}

复制上述json字符串->选中要创建dart文件的目录右键->Covert Json from Clipboard Here

image-20210508103636907

输入类名回车

image-20210508141932065

选择yes回车

image-20210508142012380

选择yes回车

image-20210508141640768

最终生成如下user_vo.dart


class UserVo {
  int userId;
  String userName;
  String avatar;

  UserVo({this.userId, this.userName, this.avatar});

  UserVo.fromJson(Map<String, dynamic> json) {
    if(json["userId"] is int)
      this.userId = json["userId"];
    if(json["userName"] is String)
      this.userName = json["userName"];
    if(json["avatar"] is String)
      this.avatar = json["avatar"];
  }

  Map<String, dynamic> toJson() {
    final Map<String, dynamic> data = new Map<String, dynamic>();
    data["userId"] = this.userId;
    data["userName"] = this.userName;
    data["avatar"] = this.avatar;
    return data;
  }
}

在dart中,json to map是默认支持的,这里就不再说明,由json to bean共有两步,第一步是json to map,第二步是map to bean。

从UserVo类的结构可以看出,要想将map to bean 或 bean to map,需要定义四个部分内容:

  • 属性字段
  • 构造方法
  • map to bean方法
  • bean to map 方法

因为dart中没有反射,所以需要一个个字段去转换,该工作可以由上述插件帮转换。

完整的接口请求样例
  1. 找到接口请求返回的样例数据

    这里以我个人记账app的系统分类接口做为举例。

    {
         
      "code": 0,
      "msg": "查询分类成功",
      "data": [
        {
         
          "id": 94,
          "name": "职业收入",
          "sort": 10,
          "icon": "m_zhiyeshouru",
          "selected": false,
          "children": [
            {
         
              "id": 95,
              "name": "薪资",
              "sort": 10.65,
              "icon": "m_xinzi",
              "selected": false
            },
            {
         
              "id": 97,
              "name": "奖金",
              "sort": 10.67,
              "icon": "m_jiangjin",
              "selected": false
            }
          ]
        }
      ]
    }
    
  2. 使用Json to Dart插件转成实体类

sys_cate_resp.dart


class SysCateResp {
  int code;
  String msg;
  List<Data> data;

  SysCateResp({this.code, this.msg, this.data});

  SysCateResp.fromJson(Map<String, dynamic> json) {
    if(json["code"] is int)
      this.code = json["code"];
    if(json["msg"] is String)
      this.msg = json["msg"];
    if(json["data"] is List)
      this.data = json["data"]==null?[]:(json["data"] as List).map((e)=>Data.fromJson(e)).toList();
  }

  Map<String, dynamic> toJson() {
    final Map<String, dynamic> data = new Map<String, dynamic>();
    data["code"] = this.code;
    data["msg"] = this.msg;
    if(this.data != null)
      data["data"] = this.data.map((e)=>e.toJson()).toList();
    return data;
  }
}

class Data {
  int id;
  String name;
  int sort;
  String icon;
  bool selected;
  List<Children> children;

  Data({this.id, this.name, this.sort, this.icon, this.selected, this.children});

  Data.fromJson(Map<String, dynamic> json) {
    if(json["id"] is int)
      this.id = json["id"];
    if(json["name"] is String)
      this.name = json["name"];
    if(json["sort"] is int)
      this.sort = json["sort"];
    if(json["icon"] is String)
      this.icon = json["icon"];
    if(json["selected"] is bool)
      this.selected = json["selected"];
    if(json["children"] is List)
      this.children = json["children"]==null?[]:(json["children"] as List).map((e)=>Children.fromJson(e)).toList();
  }

  Map<String, dynamic> toJson() {
    final Map<String, dynamic> data = new Map<String, dynamic>();
    data["id"] = this.id;
    data["name"] = this.name;
    data["sort"] = this.sort;
    data["icon"] = this.icon;
    data["selected"] = this.selected;
    if(this.children != null)
      data["children"] = this.children.map((e)=>e.toJson()).toList();
    return data;
  }
}

class Children {
  int id;
  String name;
  double sort;
  String icon;
  bool selected;

  Children({this.id, this.name, this.sort, this.icon, this.selected});

  Children.fromJson(Map<String, dynamic> json) {
    if(json["id"] is int)
      this.id = json["id"];
    if(json["name"] is String)
      this.name = json["name"];
    if(json["sort"] is double)
      this.sort = json["sort"];
    if(json["icon"] is String)
      this.icon = json["icon"];
    if(json["selected"] is bool)
      this.selected = json["selected"];
  }

  Map<String, dynamic> toJson() {
    final Map<String, dynamic> data = new Map<String, dynamic>();
    data["id"] = this.id;
    data["name"] = this.name;
    data["sort"] = this.sort;
    data["icon"] = this.icon;
    data["selected"] = this.selected;
    return data;
  }
}
  1. 新建接口类cate_service.dart

    import 'package:app/model/sys_cate_resp.dart';
    import 'package:dio/dio.dart';
    import 'package:retrofit/retrofit.dart';
    part 'cate_service.g.dart';
    
    @RestApi()
    abstract class CateService {
      factory CateService(Dio dio, {String baseUrl}) = _CateService;
      @POST("/bill/category/listCategory")
      Future<SysCateResp> listSysCate(@Field() String tallyType);
    }
    

    注意两个地方,开始没有生成相关文件,会报错。

    • part 'cate_service.g.dart';
    • factory CateService(Dio dio) = _CateService;
  2. vscode打开新终端执行如下命令

    flutter pub run build_runner build
    

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值