内容简介:初代实现一个页面的结构是这样的:会有两个问题:UI视图和Redux数据通用逻辑耦和在一起,无发通过mock数据来对UI进行UT;大家习惯套路代码,上来就是一个stful,不会想是不是stless更科学点(事实上初代实现80%的Screen是Statefull的,重构后90%都能写成Stateless,提升了页面刷新效率)。我们的API就是一个静态方法:
- 认识Flutter Redux局限性
- 引入Middleware必要性
- 全方位集成UT
Flutter Redux初代实现局限性
UT不好覆盖
- 页面
初代实现一个页面的结构是这样的:
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,提升了页面刷新效率)。
- API call
我们的API就是一个静态方法:
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覆盖。
不够Functional
上面提到的页面和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才是科学的方向啊。
引入Middleware必要性
业务已经写完,小伙伴边自测边写UT,为了达到50%的coverage可以说是非常蛋疼了。某大佬眉头一皱发现问题并不简单,UT不好写,是不是结构搓?于是召集大家讨论一波,得出这些局限性。改还是不改是个问题,不改开发算是提前完成,反正Rn也没有写UT;改的话,改动量是巨大的。大家都停下手中的工作,思考并深刻讨论这个问题,于是我们从三个方向衡量这个问题:
业务影响
离排期提测时间只有1个星期,加入Middleware会有80%的代码需要挪动,改完还要补UT,重新自测。emmm,工作量超大。和产品沟通了下,其实这个业务就是技术重构性质,线上Rn多跑一个礼拜也无碍,测试组也恰好特别忙,delay一周他们觉得ok。倾向改。
技术栈影响
从长远看,改动是进步的。对UT友好,更严谨的结构,也更Functional。小伙伴们觉得自己也能驾驭,不过是多写点套路代码~,技术栈倾向改。
伙伴支持度
引入Middleware带来的好处能否让小伙伴愿意加班把自己的模块都改写了,还补上UT?实践出真知,所以大家讨论决定,用半天时间理解并改写一个小模块,再投票决定是否改。讨论很激烈,话题一度跑偏。。。
讨论下来,最终决定是改,一星期后大家都说,真香!
改动点
增删
删掉原来Service的static API定义,加入Middleware和Repository。Middleware负责网络请求,数据处理,并根据数据状态进行Action的分发。Repository功能是定义了一个数据来源(可能来源于网络,也可能是数据库),因为引入Dio,所以会很精简,形式上可以看成是一个Endpoint定义。
- Middleware
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));
}
}
复制代码
- Repository
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和业务增量迭代。
- Screen
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();
}
}
复制代码
- Reducer
@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);
复制代码
UT集成
现在的coverage是48%,核心模块有80%+,有必要的话达到95%以上时完全ok的。原因是解耦以后方方面面都可以UT了
- widget(纯)
// 官方文档写的清楚明白 https://flutter.io/docs/testing 复制代码
- Utils
被多次使用的才会抽成 工具 类,纯逻辑也很容易写测试,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);
});
}
复制代码
- Presentation
业务的载体。对于比较核心的业务,无论是流程规范定义还是数据边界条件都可以用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 {
...
});
}
复制代码
- Reducer
存放数据,可以用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);
});
...
}
复制代码
- Middleware
叫中间件代表它不是必须,是可以被插拔,可以叠加多个的。每个中间件会有一个明确的任务,我们引入的中间件在这里是处理网络数据,根据情况发对应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 进阶》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Cascading Style Sheets 2.0 Programmer's Reference
Eric A. Meyer / McGraw-Hill Osborne Media / 2001-03-20 / USD 19.99
The most authoritative quick reference available for CSS programmers. This handy resource gives you programming essentials at your fingertips, including all the new tags and features in CSS 2.0. You'l......一起来看看 《Cascading Style Sheets 2.0 Programmer's Reference》 这本书的介绍吧!