Flutter 入坑指南
Flutter + Dio + Fish_Redux
目前網(wǎng)上只有單個的介紹,沒有整合使用的demo,那就由我來吧。要給我點星星?。。?!
代碼地址: https://github.com/liangwei0101/liang_flutter_demo
本次將實現(xiàn)的界面如下:
之前做的一個管理端界面(spring boot + vue )數(shù)據(jù),下圖網(wǎng)址為:http://101.132.124.171:8000/about
看完能懂啥?
看完能大概知道目錄結構,Dio基礎請求的封裝, Fish_Redux狀態(tài)的使用,和一些亂七八糟的坑。筆者也是差不多摸石頭過河,這些天也是踩了一些坑。
選型
因為組人員有限,所以沒有過多的精力去維護一套IOS和Android代碼。經(jīng)過和組里的人員套論以后,決定放棄選用React Native,選用Flutter。因為組人人員都是React Native和Flutter都沒有接觸過,差不多都是從0開始,然后覺得Flutter可能是以后的趨勢,性能還行,就開始搞了。
搭環(huán)境
此處省略一萬字,按Flutter官網(wǎng)來即可:Flutter中文網(wǎng)。
項目結構
- api 用來存放和后端接口請求
- apiModel 用來存放后端返回的json對應的model對象集合
- asset 用來存放一些例如圖片啥的資源
- component 為能抽出來的單個的組件的集合
- page 為單個的頁面的集合
- utils 為功能的方法和類的集合
- pubspec.yaml是管理包的地方
demo需要用的依賴包
dependencies:
json_annotation: ^2.4.0 # json序列化和反序列化用的
dio: ^2.1.7 # http請求用的
fish_redux: ^0.2.2 # 狀態(tài)管理用的
flutter:
sdk: flutter
# 開發(fā)環(huán)境的依賴
dev_dependencies:
flutter_test:
sdk: flutter
build_runner: ^1.5.2 # 開發(fā)時生成model用的
json_serializable: ^3.0.0 # json序列化和反序列化用的
為啥要使用狀態(tài)管理
- 為了View(界面)和VM(邏輯)解耦。
- 為了代碼的可維護性。
- 為了以后的可擴展性。
使用 fish_redux前戲
- 在狀態(tài)管理的選型中,有bloc和redux,后面選了阿里的 fish_redux。一是因為相信阿里的團隊,二是看了他們的狀態(tài)管理和其他的對比(狀態(tài)管理對比),三是它的星星是最多的。
- 官方推出了一個插件:FishReduxTemplate,分別有vscode 和Android studio,不過Android studio創(chuàng)建了文件以后,過許久以后才會刷新,有點小慢。
- 開始有點不是很理解他的意思,接觸和看文檔以后發(fā)現(xiàn)他的這個和Vuex狀態(tài)有點類似。
- 看的知乎上的一個解釋(不知道為啥,翻墻才搜到了知乎的,講的挺好的,為啥某度的搜索引擎就沒有出來),覺得下面幾個還是很重要的:View、Effect、Reducer組件三要素。 (掘金解釋)、(知乎解釋)
使用Flutter的坑
- fish_redux去github上找官方的example,我是沒有跑起來。是因為我的flutter的版本是1.74的。報錯如下,原因也挺簡單,是因為咸魚團隊的Action和官方的Action的類名重復了。解決辦法:降級,將flutter版本降至:v1.5.4-hotfix.2及以下。
Error: 'Action' is imported from both 'package:flutter/src/widgets/actions.dart' and 'package:fish_redux/src/redux/basic.dart'.
- fish_redux 創(chuàng)建以后effect文件報錯,錯誤如下。
The function '_onAction' has type 'void Function(Action, Context<DemoState>)' that isn't of expected type 'dynamic Function(dynamic, Context<DemoState>)'. This means its parameter or return type does not match what is expected.
-
在開始模式調用API的時候,遇上一個大坑,因為之前做JAVA 、C#、Vue居多,移動端偏少,然后在使用Dio封裝后,開始連接后臺API,報錯。原因也是找了一會,是因為,我自己開的后臺去調試的,開的node的后臺和java的后臺都不行,BaseUrl寫的是:localhost,后面看見有大兄弟說,linux系統(tǒng)下,被映射的端口是啥10.20啥的,我猜這個dart的底層虛擬機是linux的,我果斷換了遠程的IP的API,立馬來事。
// 錯誤是這個, DioError [DioErrorType.DEFAULT] // 官網(wǎng)的解釋如下 enum DioErrorType { /// Default error type, usually occurs before connecting the server. DEFAULT, }
- 沒啥文檔,很多靠猜和網(wǎng)上的例子。有些不是很理解,也沒有人講解啥的,還是有點痛苦的。
fish_redux幾個重要的概念
Action定義一種行為,可以攜帶信息,發(fā)往Store。換言之Store發(fā)生改變須由Action觸發(fā),F(xiàn)ish redux 有以下約定:Action 包含兩個字段type和payload;推薦寫法是在action.dart里定義一個type枚舉類和一個ActionCreator類,這樣有利于約束payload的類型。
Reducer/Effect這兩個函數(shù)都是用來處理數(shù)據(jù)的函數(shù),Reducer是純函數(shù)響應Action對Store數(shù)據(jù)進行改變。Effect用來處理來自視圖的意圖,如點擊事件,發(fā)起異步請求,這些有副作用的操作。
Page可以看成是一個容器,它用來配置聚合State,Effect,Reduce,View,Dependencies等。
Adapter(可選),這個不咋懂。
view 解耦出來的純頁面。
首先創(chuàng)建一個簡單的頁面
使用咸魚提供插件新建后,將出現(xiàn)上圖的文件。
- action,里面是定義一些動作,給view或者effect用的。
import 'package:fish_redux/fish_redux.dart';
import '../../../apiModel/user.dart';
//TODO replace with your own action
enum BusinessAction { query }
class BusinessActionCreator {
static Action updateAction(List<User> userList){
print('由effect請求后dispatch的值來了');
return Action(BusinessAction.query, payload: userList);
}
}
- effect 里頭是一些事件,發(fā)起異步請求等
import 'package:fish_redux/fish_redux.dart';
import 'action.dart';
import 'state.dart';
import '../../../api/user.dart';
Effect<BusinessState> buildEffect() {
return combineEffects(<Object, Effect<BusinessState>>{
Lifecycle.initState: _init,
});
}
void _init(Action action, Context<BusinessState> ctx) {
// Http請求
UserApi.getUser().then((value){
ctx.dispatch(BusinessActionCreator.updateAction(value));
});
}
- page 是配置聚合State,Effect,Reduce,View
import 'package:fish_redux/fish_redux.dart';
import 'effect.dart';
import 'reducer.dart';
import 'state.dart';
import 'view.dart';
class BusinessPage extends Page<BusinessState, Map<String, dynamic>> {
BusinessPage()
: super(
initState: initState,
effect: buildEffect(),
reducer: buildReducer(),
view: buildView,
dependencies: Dependencies<BusinessState>(
adapter: null,
slots: <String, Dependent<BusinessState>>{
}),
middleware: <Middleware<BusinessState>>[
],);
}
- reducer 修改值的地方
import 'package:fish_redux/fish_redux.dart';
import 'action.dart';
import 'state.dart';
Reducer<BusinessState> buildReducer() {
return asReducer(
<Object, Reducer<BusinessState>>{
BusinessAction.query: _onQuery,
},
);
}
BusinessState _onQuery(BusinessState state, Action action) {
print('我是值真正更新的地方');
final BusinessState newState = state.clone();
newState.userList = action.payload;
return newState;
}
- state 初始化,存屬性的地方
import 'package:fish_redux/fish_redux.dart';
import '../../../apiModel/user.dart';
class BusinessState implements Cloneable<BusinessState> {
BusinessState({this.userList});
List<User> userList = new List<User>();
@override
BusinessState clone() {
return BusinessState();
}
}
// 初始化
BusinessState initState(Map<String, dynamic> args) {
List<User> tempList = new List<User>();
User user = new User();
user.no = 0;
user.name = '梁二狗';
user.email = '1@qq.com';
tempList.add(user);
return BusinessState(userList: tempList);
}
- view 解耦出來的界面
import 'package:fish_redux/fish_redux.dart';
import 'package:flutter/material.dart';
import 'action.dart';
import 'state.dart';
Widget buildView(BusinessState state, Dispatch dispatch, ViewService viewService) {
var buildListView = ListView.builder(
itemCount: state.userList.length,
itemBuilder: (BuildContext context, int index) {
return Card(
child: ListTile(
leading: FlutterLogo(),
title: Text('編號:' +
state.userList[index].no.toString() +
'名稱:' +
state.userList[index].name.toString() +
'郵箱:' +
state.userList[index].email.toString()),
),
);
},
);
// 中間頁面的視圖
return Scaffold(appBar: null, body: Center(child: buildListView));
}
Http的封裝
在上面,我們使用的Http請求,是用Dio封裝過一次的,代碼如下:
import 'package:dio/dio.dart';
import 'dart:io';
import 'dart:async';
/*
* 封裝 restful 請求
*
* GET、POST、DELETE、PATCH
* 主要作用為統(tǒng)一處理相關事務:
* - 統(tǒng)一處理請求前綴;
* - 統(tǒng)一打印請求信息;
* - 統(tǒng)一打印響應信息;
* - 統(tǒng)一打印報錯信息;
*/
class HttpUtils {
/// global dio object
static Dio dio;
/// default options
static const String API_PREFIX = 'http://101.132.124.171:8080/demo-1.0/api';
static const int CONNECT_TIMEOUT = 10000;
static const int RECEIVE_TIMEOUT = 3000;
/// http request methods
static const String GET = 'get';
static const String POST = 'post';
static const String PUT = 'put';
static const String PATCH = 'patch';
static const String DELETE = 'delete';
static Future<dynamic> request (
String url,
{ data, method }) async {
data = data ?? {};
method = method ?? 'GET';
/// restful 請求處理
data.forEach((key, value) {
if (url.indexOf(key) != -1) {
url = url.replaceAll(':$key', value.toString());
}
});
Dio dio = createInstance();
/// 打印請求相關信息:請求地址、請求方式、請求參數(shù)
print('請求地址:【' + dio.options.baseUrl + url + '】');
print('請求參數(shù):' + data.toString());
var result;
try {
Response response = await dio.request(url, data: data, options: new Options(method: method));
result = response.data;
/// 打印響應相關信息
print('響應數(shù)據(jù)成功!');
} on DioError catch (e) {
/// 打印請求失敗相關信息
print('請求出錯:' + e.toString());
}
return result;
}
/// 創(chuàng)建 dio 實例對象
static Dio createInstance () {
if (dio == null) {
/// 全局屬性:請求前綴、連接超時時間、響應超時時間
BaseOptions option = new BaseOptions(
baseUrl: API_PREFIX,
connectTimeout: CONNECT_TIMEOUT,
receiveTimeout: RECEIVE_TIMEOUT,
headers: {
"user-agent": "dio",
"api": "1.0.0"
},
contentType: ContentType.JSON,
// Transform the response data to a String encoded with UTF8.
// The default value is [ResponseType.JSON].
responseType: ResponseType.plain
);
dio = new Dio(option);
}
return dio;
}
/// 清空 dio 對象
static clear () {
dio = null;
}
}
json 字符串解析
在上面http中,我們的數(shù)據(jù)為json數(shù)組,數(shù)據(jù)如下,我們使用 json_serializable 將json串轉化成List數(shù)組。
[{"no":10,"name":"12","email":"12@qq.com"},{"no":11,"name":"11","email":"1@qq.com"},{"no":12,"name":"asdf","email":"asf@asdf.com"},{"no":14,"name":"li","email":"22@qq.com"},{"no":15,"name":"w","email":"1@qq.com"},{"no":16,"name":"梁","email":"17@qq.com"},{"no":17,"name":"李","email":"1@qq.com"},{"no":18,"name":"li","email":"2@qq.com"},{"no":112,"name":"里","email":"56@qq.com"},{"no":122,"name":"1","email":"1@qq.com"}]
他這個東西還是有點復雜的,沒有java或者C#轉json方便。我們建立對應的Model的文件user.dart,示例代碼如下:
import 'package:json_annotation/json_annotation.dart';
part 'user.g.dart';
@JsonSerializable()
class User {
User({this.no, this.name, this.email});
int no;
String name;
String email;
factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
Map<String, dynamic> toJson() => _$UserToJson(this);
}
開始建立文件的時候,最后兩行會報錯。然后我們使用如下命令行,將會生成一個"user.g.dart",然后就能使用了。
flutter packages pub run build_runner watch
在json串轉List的時候,找了很久,終于找到啦,這樣寫:
// json 為后臺返回的json數(shù)組串
var usersJson = json.decode(result);
List<User> userList = (usersJson as List).map((i)=>User.fromJson(i)).toList();
就寫到這里吧。希望能幫到大家哦,還有,記得給我點星星,寫文不容易啊。