Flutter redux 進階

目的

  1. 認識Flutter Redux侷限性
  2. 引入Middleware必要性
  3. 全方位集成UT

Flutter Redux初代實現侷限性

UT很差覆蓋

  1. 頁面

初代實現一個頁面的結構是這樣的:前端

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

  1. API call

咱們的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

不夠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;改的話,改動量是巨大的。你們都停下手中的工做,思考並深入討論這個問題,因而咱們從三個方向衡量這個問題: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定義。

  • 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_sample](https://github.com/hyjfine/flutter_redux_sample)

參考

flutter_architecture_samples

One More Thing

TNT,讓你的工做效率提升幾百倍,老羅認真嚴肅的說。開個玩笑,這個有待驗證。但Live Templates,提高你的編程效率和體驗,確定是真的

使用地址:https://github.com/hui-z/live-templates

(完)

@子路宇, 本文版權屬於再惠研發團隊,歡迎轉載,轉載請保留出處。

相關文章
相關標籤/搜索