preface

With the release of Flutter2.0, Flutter’s desktop and Web support was officially announced into the Stable channel. I believe that Flutter will soon become another mainstream. Some time ago, BASED on my personal needs and desire to have a real experience of Flutter development, I developed an accounting APP using Flutter. To summarize the experience of this development: it is cool! The development experience is great! For those of you who have not yet tried Flutter, you can start with this article to build a normative Flutter project engineering environment from zero.

This article is a long one and will be developed from the following aspects:

  • Environmental installation
  • Architectural structures,
  • Flutter MVP specification
  • Common plug-in
  • Code specification
  • Submission specification (pending)
  • Unit testing
  • Packaging releases

The complete code of this project is hosted in the Gitee warehouse, welcome to light up the little star.

Technology stack

  • Programming language: Dart + Flutter
  • Routing tool: Fluro: ^2.0.3
  • Network Request library: dio: ^3.0.10
  • Interface service encapsulation tool: Retrofit: 1.3.4+1
  • Toast plugin: Fluttertoast: ^7.1.5
  • Provider: ^4.3.3
  • Event bus: ^2.0.0

Environmental installation

Configuration and tool requirements

  • Operating system: Windows 7 or later (64-bit)
  • Disk space: 2 GB.
  • toolFlutter relies on the following command-line tools.
    • Git for Windows

Access to Flutter the SDK

Go to the Flutter website to download the latest available installation package and click Download.

I’m using version here

Decompression is as follows:

Configuring environment Variables

  • My computer -> Right-click Properties -> Advanced System Settings -> Environment Settings

    • System variable find Path append flutter/bin

    • Add PUB_HOSTED_URL and FLUTTER_STORAGE_BASE_URL to the user environment variables

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

Run the flutter doctor

Run Git bash or PowerShell as an administrator

flutter doctor
Copy the code

The effect is as follows:

Access to the Android SDK

I’m using version here

Install related tools

Configure the environment variables again

  • My computer -> Right-click Properties -> Advanced System Settings -> Environment Settings

    • Add ANDROID_HOME to the user environment variable

    • System variables find Path to append platform-tools and tools

      Mainly is the platform – the tools/adb. Exe

Install the Nighthian simulator

  1. Replace The Nighthian emulator adb.exe

Overwrite Nox/bin/nox_adb.exe in the Nox/bin/nox_adb.exe simulator with platform-tools/adb

Make a backup before overwriting

  1. Open the Nighthian simulator

  2. Check device status using flutter Devices

    flutter devices
    Copy the code

    After modifying the environment variables, re-open Git bash.

    Here you can see that there are three devices, VOG AL10 is the Garth emulator and the other two are browsers.

    At this point, the environment installation is complete.

Install VSCode

slightly

Architectural structures,

Initialize the project prototype using the flutter create command

Create a project using the flutter create command

#The default is Kotlin, and the -a parameter is required if the Java language is used
flutter create -a java fluttermvp
cd fluttermvp
Copy the code

The default project directory is shown below:

Run the application

  • Check whether the Android device is running. If it doesn’t show

    flutter devices
    Copy the code
  • Run the flutter run command to run the application

    flutter run
    Copy the code

    The following error message is displayed because the version of the Android SDK build-tools is incorrect

    Open SDK Manager.exe and install the corresponding version

    After the Android SDK build-Tools is installed, an error will be reported because there is still a problem that has not been resolved.

    Currently the highest version only 29, 29, so to can only choose to download and then modify the fluttermvp/android/app/gradle. Bulid file

    compileSdkVersion 30 ==> compileSdkVersion 29
    targetSdkVersion 30 ==> targetSdkVersion 29
    Copy the code

    Install the Android SDK Platform

  • If all goes well, you should see the application on your device or emulator after it has been built:

Use VSCode to open the project

Temporarily install three commonly used plug-ins

Experience a wave of thermal overload

Flutter can achieve a fast development cycle through hot reload, which means that the modified code can be loaded in real time without reloading the application without losing state. Simply make a change to the code, then tell the IDE or command-line tool that you need to reload (click the Reload button) and you’ll see the change on your device or emulator.

  1. Open the filelib/main.dart
  2. The string'You have pushed the button this many times:'Change to'You have clicked the button this many times:'
  3. Do not press the “Stop” button; Keep your application running.
  4. To view your changes, callSave (cmd-s / ctrl-s), or clickHot overload button(Button with lightning icon).

You will immediately see the updated string in the running application

Close the previous command line. Use VSCode to start debugging

Flutter configuration file

The subsequent use to explain in turn

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
Copy the code

Canonical directory structure

├─ Android / # Android Engineering ├─ ios/ # Ios Engineering ├─ API / # API Layer ├─ API / # API Layer ├─ Android / # Android Engineering ├─ ios/ # Ios Engineering ├─ API / # API Layer ├─ HTTP / # HTTP request tool ├ ─ ─ iconfont / # ali cloud vector icon ├ ─ ─ the model # / physical layer ├ ─ ─ modules / # function module ├ ─ ─ the router / # routing └ ─ ─ tool / # tool library ├ ─ ─ └ / # ├─ web/ # web engineering ├─ ├─ pubspecCopy the code

It is suggested to change the name of pubspec.yaml to app in order to unify the following guide packages. After modification, you need to restart vscode so that the package guide function can take effect.

name: app                       # here from the previous 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
Copy the code

Integrated routing tool Fluro

Fluro: ^ 2.0.3

  1. To get the plugin

  1. Create two new pagesmoudules/example/route_a.dartwithmoudules/example/RouterBPage.dart

  1. Stateful was set to stateless, and the name was stateful

  1. Quick fix, guide package

  1. Finally, the code was modified as follows:
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"),),); }}Copy the code
  1. Route_b. Dart Repeat the preceding steps.

  2. Create a routing file 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';
    
    / / root page
    var rootHandler = new Handler(
        handlerFunc: (BuildContext context, Map<String.List<String>> params) {
      return MyApp();
    });
    / / page
    var emptyHandler = new Handler(
        handlerFunc: (BuildContext context, Map<String.List<String>> params) {
      return ErrorPage();
    });
    / / RouterPageA page
    var routerAHandler = new Handler(
        handlerFunc: (BuildContext context, Map<String.List<String>> params) {
      return RouterAPage();
    });
    / / RouterPageB page
    var routerBHandler = new Handler(
        handlerFunc: (BuildContext context, Map<String.List<String>> params) {
      return RouterBPage();
    });
    
    
    Copy the code

    Create the empty page by referring to RouterPageA or RouterPageB.

    The thing to notice here is the guide package here

    import 'package:app/main.dart';

    The app corresponds to the name of pubSpec. yaml before it is modified

    import 'package:fluttermvp/main.dart';

  3. Create the routing configuration file router/routes.dart

    import 'package:app/router/router_handlers.dart';
    import 'package:fluro/fluro.dart';
    
    class Routes {
      static void configureRoutes(FluroRouter router) {
        / / page
        router.notFoundHandler = emptyHandler;
        // The root page
        router.define("/".handler: rootHandler);
        // RouterPageA
        router.define("/routerA".handler: routerAHandler);
        // RouterPageB
        router.define("/routerB".handler: routerBHandler);}}Copy the code
  4. Create the routing tool class 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 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); } // jump to the specified address, 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 (" pass parameter $paramStr"); return "$registerPath? $paramStr"; }}Copy the code
  5. Added route configuration on the portal page

    
    void main() {
      /// Configuring a Route Start
      FluroRouter router = FluroRouter();
      Routes.configureRoutes(router);
      NavTool.router = router;
    
      /// The entrance
      runApp(MyApp());
    }
    Copy the code
  6. Layout snippets

    new RaisedButton(
        child: new Text("RouterA"),
        onPressed: () {
            NavTool.push(context, "/routerA");
        }),
    new RaisedButton(
        child: new Text("RouterB"),
        onPressed: () {
            NavTool.push(context, "/routerB");
        })
    Copy the code
  7. Effect of screenshots

    At this point, the route is integrated, and the further learning of the route will not be expanded here.

Integrate common Flutter utility classes

Flustars: ^ 2.0.1

Flustars relies on the common Dart utility library Common_utils and wraps other third-party libraries to share easy-to-use utility classes. If you have a good utility class welcome PR. Currently includes SharedPreferences Util, Screen Util, Directory Util, Widget Util, Image Util.

Integrated network request library Dio + Retrofit + JSON

Dart does not support reflection, but dart is disabled: Mirror cannot use reflection, so processing on JSON to bean is not very friendly. However, we can use tools that trigger commands at compile time to restore the comfort of native development processing as much as possible.

Dependencies Environment dependencies package:

Dio: ^ 3.0.10

Retrofit: 1.3.4 + 1

Json_annotation: ^ 3.0.1

Dev_dependencies Environment dependencies package:

Retrofit_generator: 1.4.1 + 3

Build_runner: ^ 1.7.3

Json_serializable: ^ 3.1.1

Install the Json To Dart plug-in

The plug-in converts JSON into Dart beans

Plug-in demo:

{
    "userId": 1."userName": "Zhang"."avatar": ""
}
Copy the code

Copy the JSON string -> Right-click the directory where you want to create the Dart file ->Covert JSON from Clipboard Here

Type the class name and press Enter

Say Yes enter

Say Yes enter

The following user_vo.dart is generated


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;
    returndata; }}Copy the code

In DART, json to Map is supported by default, so there are two steps to json to bean. The first step is JSON to Map, and the second step is map to bean.

As you can see from the structure of the UserVo class, to define map to bean or bean to Map, you need to define four parts:

  • Property field
  • A constructor
  • Map to bean method
  • Bean to map method

Because dart has no reflection, field by field conversion is required, which can be done by the plug-in.

Complete sample interface request

  1. Find the sample data returned by the interface request

    Here I take the system classification interface of my personal bookkeeping APP as an example.

    {
      "code": 0."msg": "Query classification succeeded"."data": [{"id": 94."name": "Occupational income"."sort": 10."icon": "m_zhiyeshouru"."selected": false."children": [{"id": 95."name": "Salary"."sort": 10.65."icon": "m_xinzi"."selected": false
            },
            {
              "id": 97."name": "Bonus"."sort": 10.67."icon": "m_jiangjin"."selected": false}]}]}Copy the code
  2. Convert to entity classes using the Json to Dart plug-in

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();
    returndata; }}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();
    returndata; }}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;
    returndata; }}Copy the code
  1. Create the interface class 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(a)abstract class CateService {
      factory CateService(Dio dio, {String baseUrl}) = _CateService;
      @POST("/bill/category/listCategory")
      Future<SysCateResp> listSysCate(@Field(a)String tallyType);
    }
    Copy the code

    Note two places, the initial generated file, will report an error.

    • part 'cate_service.g.dart';
    • factory CateService(Dio dio) = _CateService;
  2. Vscode open a new terminal and run the following command

    flutter pub run build_runner build
    Copy the code

  3. Viewing the build file

    Dart will generate the cate_service.g.art file, which is the file specified by part.

    // GENERATED CODE - DO NOT MODIFY BY HAND
    
    part of 'cate_service.dart';
    
    / / * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
    // RetrofitGenerator
    / / * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
    
    class _CateService implements CateService {
      _CateService(this._dio, {this.baseUrl}) {
        ArgumentError.checkNotNull(_dio, '_dio');
      }
    
      final Dio _dio;
    
      String baseUrl;
    
      @override
      Future<SysCateResp> listSysCate(tallyType) async {
        ArgumentError.checkNotNull(tallyType, 'tallyType');
        const _extra = <String.dynamic> {};final queryParameters = <String.dynamic> {};final _data = {'tallyType': tallyType};
        _data.removeWhere((k, v) => v == null);
        final _result = await _dio.request<Map<String.dynamic> > ('/bill/category/listCategory',
            queryParameters: queryParameters,
            options: RequestOptions(
                method: 'POST',
                headers: <String.dynamic>{},
                extra: _extra,
                baseUrl: baseUrl),
            data: _data);
        final value = SysCateResp.fromJson(_result.data);
        returnvalue; }}Copy the code
  4. Create a new unit test class

    test/main_test.dart

    import 'package:app/api/cate_service.dart';
    import 'package:app/model/sys_cate_resp.dart';
    import 'package:dio/dio.dart';
    
    Future<void> main() async {
      CateService cateService =
          new CateService(new Dio(), baseUrl: "http://bill-app.mldong.com");
      SysCateResp cateResp = await cateService.listSysCate("10");
      print(cateResp.toJson());
    }
    Copy the code
  5. Open the file Ctrl+F5 to Run, or mouse click Run

    Console output:

Dio Global Configuration

The new Dio() above uses the default configuration, but in most cases we need to do some global request interceptor, such as printing the request log, appending tokens to the request, etc.

Create a new class HTTP /dio_manager.dart

The Dio object is processed as follows:

  • Singleton Dio
  • The request header appends the version number
  • Set the request root address
  • Request timeout
  • Response timeout
  • Request Log Printing
/* * Network request management */
import 'package:app/config/config.dart';
import 'package:dio/dio.dart';

class DioManager {
  // write a singleton
  // In Dart, variables that begin with an underscore are private variables
  static DioManager _instance;

  Dio dio = new Dio();
  DioManager() {
    // Set default configs
    dio.options.headers = {
      "version": GlobalConfig.API_VERSION,
    };
    dio.options.baseUrl = GlobalConfig.BASE_URL;
    dio.options.connectTimeout = 5000;
    dio.options.receiveTimeout = 3000;
  }
  static DioManager getInstance() {
    if (_instance == null) {
      _instance = DioManager();
    }
    Enable request log printing in debug mode
    if (GlobalConfig.isDebug) {
      _instance.dio.interceptors.add(LogInterceptor(
          request: false.// Do not print the request
          requestBody: true.// Prints the request body
          responseHeader: false.// The response header is not printed
          responseBody: true)); // Prints the response body
    }
    return_instance; }}Copy the code

Call the sample

import 'package:app/api/cate_service.dart';
import 'package:app/http/dio_manager.dart';
import 'package:app/model/sys_cate_resp.dart';

Future<void> main() async {
  CateService cateService = new CateService(DioManager.getInstance().dio);
  SysCateResp cateResp = await cateService.listSysCate("10");
  print(cateResp.toJson());
}
Copy the code

Ignore the *.g.art file

Since the *.g.art file is tool-generated, it is not recommended to add it to version control, and you need to append a line to the.gitignore file

*.g.dart
Copy the code

Integrated Ali vector icon library

Website: www.iconfont.cn/

The introduction of SVG library

Flutter_svg: ^ 0.22.0

Install the flutter-iconfont- CLI plugin

Flutter -iconfont-cli is a Nodejs plugin used as a tool class to generate dart icon dependent classes based on aliyun JS files.

npm install flutter-iconfont-cli -g
Copy the code

Ali vector icon flow sample

  • The login

    slightly

  • Create a project

  • Add ICONS to projects

    slightly

  • Hit Generate code

  • Initialize using a plug-in

    npx iconfont-init
    Copy the code

  • Open iconfont. Json and replace the js address with the following:

    {
        "symbol_url": "Please refer to readme. md and copy the JS link provided by the official website."."save_dir": "./lib/iconfont"."trim_icon_prefix": "icon"."default_icon_size": 18."null_safety": true
    }
    Copy the code

    = = >

    {
        "symbol_url": "//at.alicdn.com/t/font_2534875_d4lkc1mlsxk.js"."save_dir": "./lib/iconfont"."trim_icon_prefix": ""."default_icon_size": 18."null_safety": false
    }
    Copy the code
    • Symbol_url js links

      Please copy the project link directly from the iconfont website. Make sure it’s a.js suffix and not a.css suffix. If you haven’t create iconfont warehouse, then you can fill in this link to test: http://at.alicdn.com/t/font_1373348_ghk94ooopqr.js

    • save_dir

      The location of the component generated from the iconfont icon. This folder is cleared each time a component is generated.

    • trim_icon_prefix

      If your icon has a common prefix that you don’t want to repeat, you can use this option to remove the prefix altogether.

    • default_icon_size

      We will add a default font size for each generated icon component, but you can also change the size by passing props

    • null_safety

      Dart 2.12.0 supports the null security feature. After this parameter is enabled, the generated syntax will change. Therefore, you need to change the SDK to ensure that the syntax can be recognized.

      Environment: - SDK: "> = 2.7.0 (3.0.0" + SDK: "> = 2.12.0 < 3.0.0"Copy the code

      Null_safety is not supported in the current version, so change it to false.

  • Using command generation

    npx iconfont-flutter
    Copy the code

If the vector icon changes, you can repeat the process again.

Example icon usage

/// IconFont(IconNames.xxx);
/// IconFont(IconNames.xxx, color: '#f00');
/// IconFont(IconNames.xxx, colors: ['#f00', 'blue']);
/// IconFont(IconNames.xxx, size: 30, color: '#000');

import 'package:app/iconfont/icon_font.dart';
import 'package:flutter/material.dart';

class IconPage extends StatefulWidget {
  @override
  _IconPageState createState() => _IconPageState();
}

class _IconPageState extends State<IconPage> {
  List<IconNames> iconList = new List(a);@override
  void initState() {
    super.initState();
    IconNames.values.forEach((element) {
      iconList.add(element);
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text("Icon"),
        ),
        body: new GridView.builder(
            scrollDirection: Axis.vertical,
            gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
              maxCrossAxisExtent: 80.// The maximum width of the child control is 100
              childAspectRatio: 1.0.// Width/height ratio is 1:1
              crossAxisSpacing: 5,
              mainAxisSpacing: 10,
            ),
            padding: EdgeInsets.all(10),
            itemCount: iconList.length,
            itemBuilder: (BuildContext context, int position) {
              IconNames icon = this.iconList[position];
              return new GestureDetector(
                child: new Container(
                    alignment: Alignment.center,
                    decoration: new BoxDecoration(color: Colors.white),
                    child: new Column(
                      mainAxisAlignment: MainAxisAlignment.center,
                      children: [
                        new Container(
                          decoration: new BoxDecoration(
                              color: new Color(0xfff0f0f0),
                              borderRadius:
                                  BorderRadius.all(new Radius.circular(24))),
                          width: 48,
                          height: 48.//child: IconTool.getIcon("${tag.icon}"),
                          child: newCenter( child: IconFont(icon), ), ) ], )), ); })); }}Copy the code

Integrate loading components

component/common_components.dart

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

class LoadingDialog extends Dialog {
  /// Display loading
  /// @param Current context
  static void show(BuildContext context, {bool mateStyle}) {
    Navigator.of(context).push(DialogRouter(LoadingDialog()));
  }

  /// Hiding loading
  /// @param Current context
  static void hide(BuildContext context) {
    Navigator.of(context).pop();
  }

  @override
  Widget build(BuildContext context) {
    return WillPopScope(
        child: Material(
          // Create a transparent layer
          type: MaterialType.transparency, // Transparent type
          child: Center(
            // Keep the control centered
            child: CupertinoActivityIndicator(
              radius: 18,
            ),
          ),
        ),
        onWillPop: () async {
          return Future.value(false); }); }}class DialogRouter extends PageRouteBuilder {
  final Widget page;

  DialogRouter(this.page)
      : super(
          opaque: false,
          barrierColor: Color(0x00000001),
          pageBuilder: (context, animation, secondaryAnimation) => page,
          transitionsBuilder: (context, animation, secondaryAnimation, child) =>
              child,
        );
}

Copy the code

Integration of toast

Fluttertoast: ^ 8.0.6

Fluttertoast.showToast("Login successful!");
Copy the code

Integrated State Management

The provider: ^ 4.3.3

Here’s an example of using the Provider document:

lib/modules/example/provider_test.dart

Define the objects to share

class Counter with ChangeNotifier.DiagnosticableTreeMixin {
  int _count = 0;

  int get count => _count;

  void increment() {
    _count++;
    notifyListeners();
  }

  /// Makes `Counter` readable inside the devtools by listing all of its properties
  @override
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
    properties.add(IntProperty('count', count)); }}Copy the code

Defining the provider

For testing purposes, the provider is defined at the top level.

void main() {
  runApp(
    // Multiple providers can be defined
    MultiProvider(
      providers: [
        ChangeNotifierProvider(create: (_) => Counter()),
      ],
      child: const MyApp(),
    ),
  );
}
Copy the code

Defining consumers

The main use of context. Watch is to listen for data changes

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

  @override
  Widget build(BuildContext context) {
    return Text(

        /// Calls `context.watch` to make [Count] rebuild when [Counter] changes.
        '${context.watch<Counter>().count}',
        key: const Key('counterState'), style: Theme.of(context).textTheme.headline4); }}Copy the code

MyApp related

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

  @override
  Widget build(BuildContext context) {
    return constMaterialApp( home: MyHomePage(), ); }}class MyHomePage extends StatelessWidget {
  const MyHomePage({Key key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Example'),
      ),
      body: Center(
        child: Column(
          mainAxisSize: MainAxisSize.min,
          mainAxisAlignment: MainAxisAlignment.center,
          children: const <Widget>[
            Text('You have pushed the button this many times:'),

            Count(),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        key: const Key('increment_floatingActionButton'),
        onPressed: () => context.read<Counter>().increment(),
        tooltip: 'Increment',
        child: constIcon(Icons.add), ), ); }}Copy the code

The result is: click the float button on the MyHomePage page component and the value of the Count page component changes. Demo, skip.

Theme color management

To be determined.

Integrated event bus

Event_bus: ^ 2.0.0

Flutter MVP specification

Create the MVP layer base class

base/mvp.dart

import 'package:app/component/common_components.dart';
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:fluttertoast/fluttertoast.dart';

/// V layer of the base class
abstract class BaseView {
  BuildContext getContext();

  // Display loading loading
  void showLoading();

  / / hide loading
  void hideLoading();

  // Display toast
  void showToast(String msg);
  /支那* *Set button index - used to control disabled enabled* /
  void setCurrentBtnName(String btnName);
  String getCurrentBtnName();
  // Set the loading state
  void setLoading(bool loading);
  bool getLoading();
}

/// P layer abstract class
abstract class IPresenter {
  void deactivatePresenter();
  void disposePresenter();
  void initPresenter();
}

/// P layer of the base class
class BasePresenter<V extends BaseView> extends IPresenter {
  V view;
  CancelToken _cancelToken;

  @override
  void deactivatePresenter() {}

  @override
  void disposePresenter() {
    // Request cancellation
    if(_cancelToken ! =null) {
      if(_cancelToken.isCancelled) { _cancelToken.cancel(); }}}@override
  void initPresenter() {}
}

/// State the base class
abstract class BaseState<T extends StatefulWidget.V extends BasePresenter>
    extends State<T> implements BaseView {
  V presenter;
  String currentBtnName = "";
  bool loading = false;
  V createPresenter();
  BaseState() {
    presenter = createPresenter();
    presenter.view = this;
  }
  @override
  BuildContext getContext() {
    return context;
  }

  bool _isShowDialog = false;
  @override
  void hideLoading() {
    if (mounted && _isShowDialog) {
      _isShowDialog = false;
      LoadingDialog.hide(context); }}@override
  void showLoading() {
    /// Avoid repeated pop-ups
    if(mounted && ! _isShowDialog) { _isShowDialog =true;
      Future.delayed(Duration.zero, () {
        LoadingDialog.show(context); }); }}@override
  void showToast(String msg) {
    Fluttertoast.showToast(msg: msg);
  }

  @override
  void dispose() {
    super.dispose(); presenter? .disposePresenter(); }@override
  void deactivate() {
    super.deactivate(); presenter? .deactivatePresenter(); }@override
  void initState() {
    super.initState(); presenter? .initPresenter(); }@override
  void setCurrentBtnName(String btnName) {
    setState(() {
      this.currentBtnName = btnName;
    });
  }

  @override
  String getCurrentBtnName() {
    return this.currentBtnName;
  }

  @override
  void setLoading(bool loading) {
    setState(() {
      this.loading = loading;
    });
  }

  @override
  bool getLoading() {
    return this.loading; }}Copy the code

MVP Structure description

The MVP structure in this framework consists of three files

  • reg_contact.dart

    The abstract class used to define the interface between V and P

  • reg_presenter_impl.dart

    P interface concrete implementation class – write business logic

  • reg.dart

    The UI layer

MVP skeleton code generation tool

generate/index.js

  • Install dependencies

    You need to install dependencies before using them for the first time. Run the following command in the current project:

    npm install
    Copy the code
  • See the help

    node generate/index.js -h
    Copy the code

  • Generate a new module

    node ./generate/index.js -f reg -co 1
    Copy the code
  • Generate a new module – overlay type

    node ./generate/index.js -f reg -co 1
    Copy the code

The resulting module is stored in lib/modules/reg

Common plug-in

Mainly VsCode plug-ins

  • Dart

    The Dart code extends the VS code and supports the Dart programming language, as well as providing tools for efficiently editing, refactoring, running, and reloading Flall mobile applications and AngularDart Web applications.

  • Flutter

    This VS code extension adds support for efficient editing, refactoring, running, and reloading of Flitter mobile applications, as well as support for the Dart programming language.

  • Flutter Widget Snippts

    Dart and Flutter syntax fragment hint

  • Json To Dart

    Convert JSON into the Dart entity class tool

Code specification

The file name

All file names are named with underscores.

router_handlers.dart
icon.dart
tools.dart
Copy the code

The name of the class

Refer to Java naming conventions, big hump.

class UserService {}class UserVo {}Copy the code

Method name Attribute name

Refer to Java naming conventions, small hump.

String userName="";
int age = 0;
void loginByUserName(String userName,String password){
    
}
Copy the code

Private method name and property name

Follow the Java naming conventions, small hump, but start with an underscore

String _userName = "";
int _age = 0;
Copy the code

Submit specifications

slightly

Unit testing

The scaffolding you start building has integrated the unit test dependencies by default

dev_dependencies:
  flutter_test:
    sdk: flutter
Copy the code

Simple to use

lib/test/main_test.dart

import 'dart:math';

import 'package:flutter_test/flutter_test.dart';
void main() {
  test("Simple judgment", () {
    expect(new Random().nextInt(3), 1);
  });
}
Copy the code

Click on the Run

The actual value is inconsistent with the expected value

The actual value is consistent with the expected value

Grouping test

Combine multiple tests using groups to test multiple associated tests.

import 'dart:math';

import 'package:flutter_test/flutter_test.dart';

void main() {
  group("Group test", () {
    test(1 "test", () {
      expect(new Random().nextInt(3), 1);
    });
    test("The test 2", () {
      expect(new Random().nextInt(3), 1);
    });
    test("Test 3", () {
      expect(new Random().nextInt(3), 1);
    });
  });
}
Copy the code

Network interface test

import 'package:app/api/cate_service.dart';
import 'package:app/http/http.dart';
import 'package:app/model/sys_cate_resp.dart';
import 'package:flutter_test/flutter_test.dart';

void main() {
  test("Interface request Test:", () async {
    CateService cateService = new CateService(DioManager.getInstance().dio);
    SysCateResp cateResp = await cateService.listSysCate("20");
    // Verify that cateresp. code is 0
    expect(cateResp.code, 0);
  });
}
Copy the code

The Widget test

  1. Create a new pagelib/modules/example/unit_test.dart
import 'package:flutter/material.dart';

class UnitPage extends StatefulWidget {
  @override
  _UnitPageState createState() => _UnitPageState();
}

class _UnitPageState extends State<UnitPage> {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: "Unit Test",
      home: Scaffold(
        appBar: AppBar(
          title: Text("Unit Test"),
        ),
        body: new Center(
            child: new RaisedButton(
                key: new Key("btnClickMe"),
                child: new Text("点我"),
                onPressed: () {
                  print("Hello World!"); })),),); }}Copy the code
  1. Unit testing process

Get the RaisedButton object by Key -> execute the click event for that object

import 'package:app/modules/example/unit_test.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter_test/flutter_test.dart';

void main() {
  testWidgets('This is a Widget test', (WidgetTester tester) async {
    await tester.pumpWidget(new UnitPage());
    // Get the RaisedButton object
    final btnClickMe = find.byKey(new Key("btnClickMe"));
    // Verify that the object exists
    expect(btnClickMe, findsWidgets);
    // Execute the button click event
    tester.tap(btnClickMe);
  });
}
Copy the code
  1. Run Run

  1. The results of

Note: The widget to be tested needs to be wrapped with the MaterialApp();

Of course, it is also possible to test components that are not MaterialApp() wrapped by a StatefulBuilder construct.

Case 1:

import 'package:app/main.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';

void main() {
  testWidgets('This is a Widget test', (WidgetTester tester) async {
    await tester.pumpWidget(new StatefulBuilder(
        builder: (BuildContext context, StateSetter setState) {
      return new MaterialApp(
        home: new MyHomePage(title: 'Flutter Demo Home Page')); }));// Get the FloatingActionButton object
    final btn = find.byType(FloatingActionButton);
    // Verify that the object exists
    expect(btn, findsWidgets);
    // Execute the button click event
    tester.tap(btn);
  });
}

Copy the code

Example 2:

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

void main() {
  testWidgets('This is a Widget test', (WidgetTester tester) async {
    await tester.pumpWidget(new StatefulBuilder(
        builder: (BuildContext context, StateSetter setState) {
      return new MaterialApp(
        home: new Text("T")); }));// Get the Text object
    final t = find.byType(Text);
    // Verify that the object exists
    expect(t, findsWidgets);
  });
}
Copy the code

Other complex interactions are not demonstrated here, but more in-depth

Packaging releases

Due to the limited conditions, here only introduces the Android version of the package.

Example Change the name of an application package

Suppose com.example is changed to com.mldong

  1. Modify the directory

    android/app/src/main/java/com/example/==>android/app/src/main/java/com/mldong/

  2. Modify the mainactivity. Java file

    android/app/src/main/java/com/example/MainActivity.java

    com.example==>com.mldong

    package com.mldong.fluttermvp;
    
    import io.flutter.embedding.android.FlutterActivity;
    
    public class MainActivity extends FlutterActivity {}Copy the code
  3. Modify the androidmanifest.xml file

    Production configuration:

    android/app/src/main/java/AndroidManifest.xml

    Development configuration:

    android/app/src/debug/AndroidManifest.xml

    Line 2

    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
        package="com.mldong.fluttermvp">
    Copy the code
  4. Modify the build.gradle file

    android/app/build.gradle

    About 32 lines or so, nodes

    android->defaultConfig->applicationId

    applicationId "com.mldong.fluttermvp"
    Copy the code

Modify the icon

You can use the icon workshop to generate ICONS

icon.wuruihong.com/

The generated file is copied to the android/app/SRC/res/mipmap – * directory.

The file name is ic_launcher.png

Generating signature Files

  1. Generating signature Files

    keytool -genkey -v -keystore key.jks -keyalg RSA -keysize 2048 -validity 36500 -alias fluttermvp
    Copy the code

  2. Copy the signature file to the Android root directory

    fluttermvp/android/key.jks

  3. Viewing signature File Information (On demand)

    keytool -list -v -keystore android/key.jks -storepass 123456
    Copy the code

  4. Create the key.properties configuration file

    storeFile=../key.jks
    storePassword=123456
    keyAlias=fluttermvp
    keyPassword=123456
    Copy the code
  5. Modify the build.gradle file

    android/app/build.gradle

    Add the following code at the same level as android node

    def keystoreProperties = new Properties()
    def keystorePropertiesFile = rootProject.file('key.properties')
    if (keystorePropertiesFile.exists()) {
        keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
    }
    Copy the code

    The signingConfigs node is added to the Android node

    android {
    	signingConfigs {
            release {
                keyAlias keystoreProperties['keyAlias']
                keyPassword keystoreProperties['keyPassword']
                storeFile file(keystoreProperties['storeFile'])
                storePassword keystoreProperties['storePassword']}}}Copy the code

    Android ->buildTypes->release

    SigningConfig SigningConfigs. debug changed to signingConfig SigningConfigs.release

    android {
        buildTypes {
            release {
                signingConfig signingConfigs.release
            }
        }
    }
    Copy the code

Note: For security, the key.properties file is not added to the repository.

For compatibility where key.properties does not exist, you can change it to:

android {
   
    if(keystoreProperties['keyAlias'] &&
        keystoreProperties['keyPassword'] &&
        keystoreProperties['storeFile'] &&
        keystoreProperties['storePassword']){
        signingConfigs {
            release {
                keyAlias keystoreProperties['keyAlias']
                keyPassword keystoreProperties['keyPassword']
                storeFile file(keystoreProperties['storeFile'])
                storePassword keystoreProperties['storePassword']
            }
        }
        buildTypes {
            release {
                signingConfig signingConfigs.release
            }
        }
    } else {
        buildTypes {
            release {
            // TODO: Add your own signing config for the release build.
            // Signing with the debug keys for now, so `flutter run --release` works.
                signingConfig signingConfigs.debug
            }
        }
    }
    
}
Copy the code

Generate APK files

flutter build apk
Copy the code

The last

From technology selection to architecture construction, from unit test to package release, this paper guides you step by step on how to transform a simple framework of Flutter project into a standardized Flutter MVP engineering environment. It basically covers the whole process of Flutter project development, especially suitable for students who are new to Flutter engineering.

Because the length is longer, involves the technical point more, unavoidably will appear the mistake, hopes everybody many corrects, thanks everybody!