初代实现一个页面的结构是这样的:前端
class XXXScreen extends StatefulWidget {
@override
_XXXScreenState createState() => _XXXScreenState();
}
class _XXXScreenState extends State<XXXScreen> {
@override
Widget build(BuildContext context) {
return StoreConnector<AppState, _XXXViewModel>(
converter: (store) => _XXXViewModel.fromStore(store),
builder: (BuildContext context, _XXXViewModel vm) =>
Container());
}
}
复制代码
会有两个问题:UI视图和Redux数据通用逻辑耦和在一块儿,无发经过mock数据来对UI进行UT;你们习惯套路代码,上来就是一个stful,不会想是否是stless更科学点(事实上初代实现80%的Screen是Statefull的,重构后90%都能写成Stateless,提高了页面刷新效率)。java
咱们的API就是一个静态方法:git
static fetchxxx() {
final access = StoreContainer.access;
final apiFuture = Services.rest.get(
'/zpartner_api/${access.path}/${access.businessGroupUid}/xxxx/');
Services.asyncRequest(
apiFuture,
xxxRequestAction(),
(json) => xxxSuccessAction(payload: xxxInfo.fromJson(json)),
(errorInfo) => xxxFailureAction(errorInfo: errorInfo));
}
复制代码
优势是简单,有java味,缺点是:静态方法没法使用mockIto;一个Api call触发,那就发出去了,没法撤销没法重试;天然也没法进行UT覆盖。github
上面提到的页面和API call都体现了不Functional,还有咱们初代Reducer的写法也是你们很熟悉的OO写法数据库
class xxxReducer {
xxxState reducer(xxxState state, ActionType action) {
switch (action.runtimeType) {
case xxxRequestAction:
return state.copyWith(isLoading: );
case xxxSuccessAction:
return state.copyWith(isLoading: );
case xxxFailureAction:
return state.copyWith(isLoading: );
default:
return state;
}
}
}
复制代码
从上到下流水写法,static,switch case这都是咱们OO的老朋友。但既然Dart是偏前端特性,Functional才是科学的方向啊。编程
业务已经写完,小伙伴边自测边写UT,为了达到50%的coverage能够说是很是蛋疼了。某大佬眉头一皱发现问题并不简单,UT很差写,是否是结构搓?因而召集你们讨论一波,得出这些局限性。改仍是不改是个问题,不改开发算是提早完成,反正Rn也没有写UT;改的话,改动量是巨大的。你们都停下手中的工做,思考并深入讨论这个问题,因而咱们从三个方向衡量这个问题:json
离排期提测时间只有1个星期,加入Middleware会有80%的代码须要挪动,改完还要补UT,从新自测。emmm,工做量超大。和产品沟通了下,其实这个业务就是技术重构性质,线上Rn多跑一个礼拜也无碍,测试组也刚好特别忙,delay一周他们以为ok。倾向改。redux
从长远看,改动是进步的。对UT友好,更严谨的结构,也更Functional。小伙伴们以为本身也能驾驭,不过是多写点套路代码~,技术栈倾向改。api
引入Middleware带来的好处可否让小伙伴愿意加班把本身的模块都改写了,还补上UT?实践出真知,因此你们讨论决定,用半天时间理解并改写一个小模块,再投票决定是否改。讨论很激烈,话题一度跑偏。。。bash
讨论下来,最终决定是改,一星期后你们都说,真香!
删掉原来Service的static API定义,加入Middleware和Repository。Middleware负责网络请求,数据处理,并根据数据状态进行Action的分发。Repository功能是定义了一个数据来源(可能来源于网络,也多是数据库),由于引入Dio,因此会很精简,形式上能够当作是一个Endpoint定义。
class XXXMiddlewareFactory extends MiddlewareFactory {
XXXMiddlewareFactory(AppRepository repository) : super(repository);
@override
List<Middleware<AppState>> generate() {
return [
TypedMiddleware<AppState, FetchAction>(_fetchXXX),
];
}
void _fetchXXX(Store<AppState> store, FetchAction action,
NextDispatcher next) {
Services.asyncRequest(
() => repository.fetch(),
FetchRequestAction(),
(json) => FetchSuccessAction(), (errorInfo) =>
FetchFailureAction(errorInfo: errorInfo));
}
}
复制代码
Future<Response> fetchXXX(String uid) {
return Services.rest.get(
'/xxx_api/${path}/${groupUid}/manual_activities/$uid/');
}
复制代码
Screen把UI都抽到Presentation里,它依赖一个vm。数据填充并驱动UI变化,这样UI也能够写很全面的UT。Reducer则是利用Flutter_redux库提供的combineReducers方法,将原来一个大的Reducer粒度切到最小。方便写UT和业务增量迭代。
class XXXPresentation extends StatelessWidget {
final XXXViewModel vm;
const XXXPresentation({Key key, this.vm}) : super(key: key);
@override
Widget build(BuildContext context) {
return Container();
}
}
class XXXScreen extends StatelessWidget {
static const String routeName = 'xxx_screen';
@override
Widget build(BuildContext context) {
return StoreConnector<AppState, XXXViewModel>(
distinct: true,
onInit: (store) {
store.dispatch(FetchXXXAction(isRefresh: true));
},
onDispose: (store) => store.dispatch(XXXResetAction()),
converter: XXXViewModel.fromStore,
builder: (context, vm) {
return XXXPresentation(vm: vm);
},
);
}
}
class XXXViewModel {
static XXXViewModel fromStore(Store<AppState> store) {
return XXXViewModel();
}
}
复制代码
@immutable
class XXXState {
final bool isLoading;
XXXState({this.isLoading,
});
XXXState copyWith({bool isLoading,
}) {
return XXXState(
isLoading: isLoading ?? this.isLoading,
);
}
XXXState.initialState()
: isLoading = false;
}
final xXXReducer = combineReducers<XXXState>([
TypedReducer<XXXState, Action>(_onRequest),
]);
XXXState _onRequest(XXXState state, Action action) =>
state.copyWith(isLoading: false);
复制代码
如今的coverage是48%,核心模块有80%+,有必要的话达到95%以上时彻底ok的。缘由是解耦之后方方面面均可以UT了
// 官方文档写的清楚明白
https://flutter.io/docs/testing
复制代码
被屡次使用的才会抽成工具类,纯逻辑也很容易写测试,UT应该先满上。
group('test string util', () {
test('isValidPhone', () {
var boolNull = StringUtil.isValidPhone(null);
var boolStarts1 = StringUtil.isValidPhone('17012341234');
var boolStarts2 = StringUtil.isValidPhone('27012341234');
var boolLength10 = StringUtil.isValidPhone('1701234123');
var boolLength11 = StringUtil.isValidPhone('17012341234');
expect(boolNull, false);
expect(boolStarts1, true);
expect(boolStarts2, false);
expect(boolLength10, false);
expect(boolLength11, true);
});
}
复制代码
业务的载体。对于比较核心的业务,不管是流程规范定义仍是数据边界条件均可以用UT来自动化保障。
group('test login presentation', () {
Store<AppState> store;
setUp(() {
store = Store<AppState>(reduxReducer,
initialState: initialReduxState(), distinct: true);
StoreContainer.setStoreForTest(store);
});
testWidgets('test loading', (WidgetTester tester) async {
final vm = LoginViewModel(isLoading: true, isSendPinSuccess: false);
await TestHelper.pumpWidget(tester, store, LoginPresentation(vm: vm));
expect(find.byType(CupertinoActivityIndicator), findsOneWidget);
...
});
testWidgets('test has data',(WidgetTester tester) async {
...
});
testWidgets('test has no data',(WidgetTester tester) async {
...
});
}
复制代码
存放数据,能够用UT来验证特定Action是否改变了特定的数据。
group('notificationReducer', () {
test('FetchMessageUnreadRequestAction', () {
store.dispatch(FetchMessageUnreadRequestAction());
expect(store.state.notification.isLoading, true);
});
test('FetchMessageUnreadSuccessAction', () {
final payload = MessageUnreadInfo.initialState();
store.dispatch(FetchMessageUnreadSuccessAction(payload: payload));
expect(store.state.notification.messageUnreadInfo, payload);
expect(store.state.notification.isLoading, false);
});
...
}
复制代码
叫中间件表明它不是必须,是能够被插拔,能够叠加多个的。每一个中间件会有一个明确的任务,咱们引入的中间件在这里是处理网络数据,根据状况发对应Action。
group('Middleware', () {
final repo = MockAppRepository();
Store<AppState> store;
setUpAll(() async {
await mockApiSuc(repo);
});
setUp(() {
store = Store<AppState>(reduxReducer,
initialState: initialReduxState(),
middleware: initialMiddleware(repo),
distinct: true);
StoreContainer.setStoreForTest(store);
});
group('NotificationMiddlewareFactory', () {
test('FetchMessageUnreadAction', () {
store.dispatch(FetchMessageUnreadAction());
verify(repo.fetchMessagesUnread());
});
test('FetchMessageForHomeAction', () {
store.dispatch(FetchMessageForHomeAction());
verify(repo.fetchMessagesForHome());
});
...
}
复制代码
本文源码:[flutter_redux_sample](https://github.com/hyjfine/flutter_redux_sample)
TNT,让你的工做效率提升几百倍,老罗认真严肃的说。开个玩笑,这个有待验证。但Live Templates,提高你的编程效率和体验,确定是真的
使用地址:https://github.com/hui-z/live-templates
(完)
@子路宇, 本文版权属于再惠研发团队,欢迎转载,转载请保留出处。