Flutter项目复盘整理记录

刚刚使用Flutter开发完一个项目,对项目来说不大基本功能都会有,主流的一些插件都有使用到。现在回过来做一个项目复盘,针对项目使用的一些插件和遇到的一些问题进行分析总结以作记录。

网络框架+路由导航+状态管理+json数据序列化

Android打包release版本之Androidx支持包兼容问题

项目中使用的主要功能插件

  • 路由导航 fluro
  • 状态管理 provider
  • 网络框架 Dio
  • JSON和数据序列化

路由导航之fluro

使用fluro对路由进行统一配置、统一管理,应用开发使用很是方便。

配置路由

创建routes.dart类进行路由配置。
APP打开的第一个页面路径必须是'/',这里涉及到两个参数:

  • routePath:对应页面路径
  • handler:对应打开页面的handler
class Routes {
  static String root = "/";
  static String demoSimple = "/demo";

  static void configureRoutes(Router router) {
    router.notFoundHandler = Handler(
        handlerFunc: (BuildContext context, Map<String, List<String>> params) {
      print("ROUTE WAS NOT FOUND !!!");
      return;
    });
    router.define(root, handler: rootHandler);
    router.define(demoSimple, handler: demoRouteHandler);
  }
}
构建页面handler

创建route_handler.dart进行页面handler配置。
不需要传递参数的页面使用rootHandler的写法,如果要传递参数按demoRouteHandler的写法。
fluro有个问题不能传递中文,需要编码解决传递。

var rootHandler = Handler(
    handlerFunc: (BuildContext context, Map<String, List<String>> params) {
  return HomePage();
});

var demoRouteHandler = Handler(
    handlerFunc: (BuildContext context, Map<String, List<String>> params) {
  String message = params["message"]?.first;
  String colorHex = params["color_hex"]?.first;
  String result = params["result"]?.first;
  Color color = Color(0xFFFFFFFF);
  if (colorHex != null && colorHex.length > 0) {
    color = Color(ColorHelpers.fromHexString(colorHex));
  }
  return DemoSimpleComponent(message: message, color: color, result: result);
});
初始化fluro路由初始化

在main.dart进行初始化

    final router = Router();
    Routes.configureRoutes(router);
    Application.router = router;
执行页面跳转
Application.router.navigateTo(context, '${Routes.demoSimple}?message=xxx&colorHex=xxx');
解决中文不能传递问题

将中文参数在传递前进行解码转换,传递后取出参数解析

/// fluro 参数编码解码工具类
class FluroConvertUtils {
  /// fluro 传递中文参数前,先转换,fluro 不支持中文传递
  static String fluroCnParamsEncode(String originalCn) {
    StringBuffer sb = StringBuffer();
    var encoded = Utf8Encoder().convert(originalCn);
    encoded.forEach((val) => sb.write('$val,'));
    return sb.toString().substring(0, sb.length - 1).toString();
  }

  /// fluro 传递后取出参数,解析
  static String fluroCnParamsDecode(String encodedCn) {
    var decoded = encodedCn.split('[').last.split(']').first.split(',');
    var list = <int>[];
    decoded.forEach((s) => list.add(int.parse(s.trim())));
    return Utf8Decoder().convert(list);
  }
}

provider

使用provider插件包对APP状态进行管理。实现provider对状态进行管理分为三部分:

  • ChangeNotifier
  • ChangeNotifierProvider
  • Consumer
ChangeNotifier

ChangeNotifier它是Flutter开发包中的一个简单类,为监听器提供更改通知。
ChangeNotifier 是封装应用状态的一种方法。对于非常简单的应用程序,可以使用一个ChangeNotifier。在复杂的模型中,你将有几个模型,因此有几个ChangeNotifiers。

创建model继承ChangeNotifier

class CartModel extends ChangeNotifier {
  /// Internal, private state of the cart.
  final List<Item> _items = [];

  /// An unmodifiable view of the items in the cart.
  UnmodifiableListView<Item> get items => UnmodifiableListView(_items);

  /// The current total price of all items (assuming all items cost $42).
  int get totalPrice => _items.length * 42;

  /// Adds [item] to cart. This is the only way to modify the cart from outside.
  void add(Item item) {
    _items.add(item);
    // This call tells the widgets that are listening to this model to rebuild.
    notifyListeners();
  }
}

这里特定于ChangeNotifier代码就是notifyListeners()。只要模型更改调用此方法UI就会更改。

ChangeNotifierProvider

ChangeNotifierProvider是一个widget,为其后台widget提供ChangeNotiffier实例。
ChangeNotifier至于必要的范围至上,不要污染范围。

void main() {
  runApp(
    ChangeNotifierProvider(
      builder: (context) => CartModel(),
      child: MyApp(),
    ),
  );
}

如果需要提供多个类,可以使用MultiProvider:

void main() {
  runApp(
    MultiProvider(
      providers: [
        ChangeNotifierProvider(builder: (context) => CartModel()),
        Provider(builder: (context) => SomeOtherClass()),
      ],
      child: MyApp(),
    ),
  );
}
Consumer

现在CartModel通过顶部的ChangeNotifierProvider声明提供给我们应用程序中的小部件,我们可以开始使用它。

return Consumer<CartModel>(
  builder: (context, cart, child) {
    return Text("Total price: ${cart.totalPrice}");
  },
);

指定想要访问的模型类型,在这里我们需要CartModel,因此写为Consumer<CartModel>。

Provider.of

有时,并不真正需要模型中的数据来更改UI,但仍然需要访问它。

Provider.of<CartModel>(context, listen: false).add(item);

http请求库Dio

dio是一个强大的Dart Http请求库,支持Restful API、FormData、拦截器、请求取消、Cookie管理、文件上传/下载、超时、自定义适配器等...

发起一个Get请求:

Response response;
Dio dio = new Dio();
response = await dio.get("/test?id=12&name=wendu");
print(response.data.toString());
// 请求参数也可以通过对象传递,上面的代码等同于:
response = await dio.get("/test", queryParameters: {"id": 12, "name": "wendu"});
print(response.data.toString());

发起一个Post请求:

response = await dio.post("/test", data: {"id": 12, "name": "wendu"});

以流的方式接收响应数据:

Response<ResponseBody> rs = await Dio().get<ResponseBody>(url,
  options: Options(responseType: ResponseType.stream), //设置接收类型为stream
);
print(rs.data.stream); //响应流

以二进制数组的方式接收响应数据:

Response<List<int>> rs = await Dio().get<List<int>>(url,
 options: Options(responseType: ResponseType.bytes), //设置接收类型为bytes
);
print(rs.data); //二进制数组

发送 FormData:

FormData formData = new FormData.from({
    "name": "wendux",
    "age": 25,
  });
response = await dio.post("/info", data: formData);

使用代码生成库序列化JSON

json_serializable包是一个自动生成源代码生成器,可以生成JSON序列化样板。

在项目中设置json_serializable

要在项目中包含json_serializable,需要一个常规依赖和两个dev依赖项。dev依赖项是我们项目源代码中未包含的依赖项-它们仅在开发环境中使用。
pubspec.yaml

dependencies:
  # Your other regular dependencies here
  json_annotation: ^2.0.0

dev_dependencies:
  # Your other dev_dependencies here
  build_runner: ^1.0.0
  json_serializable: ^2.0.0

在项目根目录下运行flutter pub以在项目中使用这些新的依赖项。

以json_serializable方式创建模型类

user.dart

import 'package:json_annotation/json_annotation.dart';

/// This allows the `User` class to access private members in
/// the generated file. The value for this is *.g.dart, where
/// the star denotes the source file name.
part 'user.g.dart';

/// An annotation for the code generator to know that this class needs the
/// JSON serialization logic to be generated.
@JsonSerializable()

class User {
  User(this.name, this.email);

  String name;
  String email;

  /// A necessary factory constructor for creating a new User instance
  /// from a map. Pass the map to the generated `_$UserFromJson()` constructor.
  /// The constructor is named after the source class, in this case, User.
  factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);

  /// `toJson` is the convention for a class to declare support for serialization
  /// to JSON. The implementation simply calls the private, generated
  /// helper method `_$UserToJson`.
  Map<String, dynamic> toJson() => _$UserToJson(this);
}

如果需要,还可以轻松自定义命名策略。例如,如果API返回带有snake_case的对象,并且您希望在模型中使用lowerCamelCase,则可以将@JsonKey批注与name参数一起使用:

/// Tell json_serializable that "registration_date_millis" should be
/// mapped to this property.
@JsonKey(name: 'registration_date_millis')
final int registrationDateMillis;
生成实用文件

在第一次创建json_serializable类时,您将得到类似于下图所示的错误。

image

这些错误完全正常,仅仅是因为模型类的生成代码尚不存在。要解决此问题,请运行生成序列化样板的代码生成器

有两种运行代码生成器的方法。

One-time code generation

通过在项目根目录中运行flutter pub run build_runner build,可以在需要时为模型生成JSON序列化代码。这会触发一次性构建,该构建遍历源文件,选择相关文件,并为它们生成必要的序列化代码。
虽然这很方便,但如果您不必每次在模型类中进行更改时都必须手动运行构建,那将是很好的

Generating code continuously

观察者使我们的源代码生成过程更加方便。它会监视项目文件中的更改,并在需要时自动构建必要的文件。通过在项目根目录中运行flutter pub run build_runner watch来启动观察程序。
启动观察器一次并使其在后台运行是安全的。

使用json_serializable模型

要以json_serializable方式解码JSON字符串,您实际上没有对我们以前的代码进行任何更改。

Map userMap = jsonDecode(jsonString);
var user = User.fromJson(userMap);

编码也是如此。调用API与以前相同。

String json = jsonEncode(user);

使用json_serializable,您可以忘记User类中的任何手动JSON序列化。源代码生成器创建一个名为user.g.dart的文件,该文件具有所有必需的序列化逻辑。您不再需要编写自动化测试来确保序列化工作 - 现在libarary有责任确保序列化正常工作。

生成嵌套类的代码

考虑以下Address类:

import 'package:json_annotation/json_annotation.dart';
part 'address.g.dart';

@JsonSerializable()
class Address {
  String street;
  String city;

  Address(this.street, this.city);

  factory Address.fromJson(Map<String, dynamic> json) => _$AddressFromJson(json);
  Map<String, dynamic> toJson() => _$AddressToJson(this);
}

Address类嵌套在User类中:

import 'address.dart';
import 'package:json_annotation/json_annotation.dart';
part 'user.g.dart';

@JsonSerializable()
class User {
  String firstName;  
  Address address;

  User(this.firstName, this.address);

  factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
  Map<String, dynamic> toJson() => _$UserToJson(this);
}

在终端中运行flutter pub run build_runner build会创建* .g.dart文件,但private _ $ UserToJson()函数类似于以下内容:

(
Map<String, dynamic> _$UserToJson(User instance) => <String, dynamic>{      
  'firstName': instance.firstName,
  'address': instance.address,      
};

所有看起来都很好,但如果你对用户对象执行print():

Address address = Address("My st.", "New York");
User user = User("John", address);
print(user.toJson());

结果却是:

{name: John, address: Instance of 'address'}

想要的结果可能是这样:

{name: John, address: {street: My st., city: New York}}

要使其工作,请在类声明的@JsonSerializable()注释中传递explicitToJson:true。 User类现在看起来如下:

import 'address.dart';
import 'package:json_annotation/json_annotation.dart';
part 'user.g.dart';

@JsonSerializable(explicitToJson: true)
class User {
  String firstName;  
  Address address;

  User(this.firstName, this.address);

  factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
  Map<String, dynamic> toJson() => _$UserToJson(this);
}

Flutter打包release Androidx兼容问题

项目在最后开发接近尾声时,我试着打包release包,发现了Androidx兼容的问题。问题原因就是项目中同时使用到了Androidx包和Support包,而这两种包只能存在一个,要么全部引用Androidx包,要么全部引用Support包。
解决Androidx包冲突有两种方式:

  • 全部引用Androidx包
  • 全部引用Support包

这里我使用的是第一种,建议也是使用第一种,官方建议使用Androidx,以后的第三包都会转到使用androidx的。
转Androidx包的方法,官方提供了两种方法,一种是自动转(官方推荐),一种是手动转。

使用Android Studio自动升级Androidx包

这里对Android studio版本的要求是3.2以上。
项目上右键android文件夹,Flutter -> Open Android module in Android Studio
在新建的Android Studio窗口选择Refactor -> Migrate to AndroidX
点击Do Refactor即可完成AndroidX的迁移工作。

手动升级Androidx
  1. 打开android/gradle/wrapper/gradle-wrapper.properties,修改distributionUrl的值
distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.2-all.zip
  1. 打开android/build.gradle,修改com.android.tools.build:gradle 到3.3.0
dependencies {
    classpath 'com.android.tools.build:gradle:3.3.0'
}
  1. 打开android/gradle.properties,添加两行
android.enableJetifier=true
android.useAndroidX=true
  1. 打开android/app/build.gradle,在android{}里面,确保compileSdkVersion 和 targetSdkVersion 值为28
    替换所有过时的依赖库android.support到androidx。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,194评论 6 490
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,058评论 2 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 156,780评论 0 346
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,388评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,430评论 5 384
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,764评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,907评论 3 406
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,679评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,122评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,459评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,605评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,270评论 4 329
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,867评论 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,734评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,961评论 1 265
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,297评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,472评论 2 348

推荐阅读更多精彩内容