剛剛使用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類時,您將得到類似于下圖所示的錯誤。
這些錯誤完全正常,僅僅是因為模型類的生成代碼尚不存在。要解決此問題,請運行生成序列化樣板的代碼生成器
有兩種運行代碼生成器的方法。
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
- 打開android/gradle/wrapper/gradle-wrapper.properties,修改distributionUrl的值
distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.2-all.zip
- 打開android/build.gradle,修改com.android.tools.build:gradle 到3.3.0
dependencies {
classpath 'com.android.tools.build:gradle:3.3.0'
}
- 打開android/gradle.properties,添加兩行
android.enableJetifier=true
android.useAndroidX=true
- 打開android/app/build.gradle,在android{}里面,確保compileSdkVersion 和 targetSdkVersion 值為28
替換所有過時的依賴庫android.support到androidx。