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
- 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
-
Open the Nighthian simulator
-
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.
- Open the file
lib/main.dart
- The string
'You have pushed the button this many times:'
Change to'You have clicked the button this many times:'
- Do not press the “Stop” button; Keep your application running.
- 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
- To get the plugin
- Create two new pages
moudules/example/route_a.dart
withmoudules/example/RouterBPage.dart
- Stateful was set to stateless, and the name was stateful
- Quick fix, guide package
- 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
-
Route_b. Dart Repeat the preceding steps.
-
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';
-
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
-
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
-
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
-
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
-
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
-
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
-
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
-
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;
-
Vscode open a new terminal and run the following command
flutter pub run build_runner build Copy the code
-
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
-
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
-
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
- Create a new page
lib/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
- 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
- Run Run
- 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
-
Modify the directory
android/app/src/main/java/com/example/
==>android/app/src/main/java/com/mldong/
-
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
-
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
-
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
-
Generating signature Files
keytool -genkey -v -keystore key.jks -keyalg RSA -keysize 2048 -validity 36500 -alias fluttermvp Copy the code
-
Copy the signature file to the Android root directory
fluttermvp/android/key.jks
-
Viewing signature File Information (On demand)
keytool -list -v -keystore android/key.jks -storepass 123456 Copy the code
-
Create the key.properties configuration file
storeFile=../key.jks storePassword=123456 keyAlias=fluttermvp keyPassword=123456 Copy the code
-
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!