Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Flutter with Redux and Testing

Eoin Fogarty
October 06, 2018

Flutter with Redux and Testing

How well does Flutter work with Redux and Thunk action?
Take a look at flutter using store, state, actions and reducers to build an app.
Top it off with a quick look at testing these parts

Eoin Fogarty

October 06, 2018
Tweet

More Decks by Eoin Fogarty

Other Decks in Programming

Transcript

  1. # What is Flutter? - Cross platform mobile SDK developed

    by Google - Written in Dart (a modern, terse, object-oriented language) - Free and open source - Hot reload - Everything is a widget - Currently Release Preview 2
  2. # What is Redux Redux is a unidirectional data flow

    architecture with 3 principals. - Single source of truth - State is read only - Changes are made with pure functions View Reducer Store Action
  3. # What is Thunk Middleware Thunk allows us to return

    a function instead of an action. The thunk can be used to delay the dispatch of an action, or to dispatch only if a certain condition is met. View Reducer Store Action / Thunk Repository
  4. # The App Push the button to view a random

    trending gif. Dependencies - flutter_redux: ^0.5.2 - redux: ^3.0.0 - redux_thunk: ^0.2.0
  5. # Store Store A store holds the whole state tree

    of your application. The only way to change the state inside it is to dispatch an action on it. final store = Store<HomePageState>( homePageReducer, /*function defined in reducers*/ middleware: [ thunkMiddleware ], /*function defined in library*/ initialState: HomePageState.init(), );
  6. # App Structure void main() => runApp(MyApp()); class MyApp extends

    StatelessWidget { final store = /*create store*/; @override Widget build(BuildContext context) => MaterialApp( home: StoreProvider<HomePageState>( store: store, child: Scaffold(body: HomePage(title: 'Gifs!')), ), ); } Create a single store at the root of the app
  7. # Home Page State @immutable class HomePageState { HomePageState({this.status, this.error,

    this.gifObject}); final LoadingStatus status; final Exception error; final GifObject gifObject; factory HomePageState.init() => /**/; HomePageState copyWith(/**/)); } Create a state for the home page
  8. # View class HomePage extends StatelessWidget { @override Widget build(BuildContext

    context) { return StoreConnector<HomePageState, HomePageViewModel>( converter: (store) { return HomePageViewModel( store.state, onPressedShuffle: () { store.dispatch(shuffleGif()); }, ); }, builder: (context, viewModel) => HomePageContent(title, viewModel), ); } View A ViewModel connected to a Widget to route actions
  9. Widget _buildPageContent(HomePageViewModel viewModel) { switch (viewModel.state.status) { case LoadingStatus.fetching: return

    _buildProgressIndicator(); case LoadingStatus.fetched: return _buildGifImage(viewModel); case LoadingStatus.failed: case LoadingStatus.initial: return Container(key: errorKey); } throw Exception('Non handled status ${viewModel.state.status}'); } # View Setting page content based on state status
  10. Center _buildGifImage(HomePageViewModel viewModel) { return Center( key: contentKey, child: Column(

    mainAxisAlignment: MainAxisAlignment.center, children: [ Expanded( child: Image.network( viewModel.state.gifObject.url, fit: BoxFit.contain, ), ), ], ), ); } # View Loading image from view model state
  11. # Action ThunkAction<HomePageState> shuffleGif([ GifRepository repository = const GifRepository(), ])

    { return (store) { store.dispatch(HomePageRequest()); repository .getRandomTrending() .then((gifObject) => store.dispatch(HomePageFetched(gifObject))) .catchError((exception) => store.dispatch(HomePageFailed(exception))); }; } class HomePageRequest {} class HomePageFetched { HomePageFetched(this.gifObject); final GifObject gifObject; } class HomePageFailed { HomePageFailed(this.exception); final Exception exception; } Action / Thunk Thunk action is a simple function, Actions are just regular classes
  12. # Reducer final Reducer<HomePageState> homePageReducer = combineReducers([ TypedReducer(_updateRequesting), TypedReducer(_updateFetched), TypedReducer(_updateFailed),

    ]); HomePageState _updateRequesting( HomePageState state, HomePageRequest action, ) { return state.copyWith( status: LoadingStatus.fetching, ); } Reducer Reducers simple functions to change state
  13. # View Tests testWidgets('shows loading progress', (WidgetTester tester) async {

    when(mockViewModel.state).thenReturn( mockViewModel.state.copyWith( status: LoadingStatus.fetching, ), ); await _buildHomePage(tester); expect(find.byKey(HomePageContent.loadingKey), findsOneWidget); expect(find.byKey(HomePageContent.errorKey), findsNothing); expect(find.byKey(HomePageContent.contentKey), findsNothing); }); Testing if UI displays correct ui for status
  14. # Action Tests Testings thunk actions is simple as we

    test the correct actions are sent in the correct order testWidgets('gets a random gif ', (WidgetTester tester) async { final response = GifObject.empty(); when(mockRepository.getRandomTrending()).thenAnswer((_) => Future.value(response)); final expectedActions = [ HomePageRequest(), HomePageFetched(response), ]; actionLog.clear(); final store = createTestStore(); await thunkMiddleware.call(store, shuffleGif(mockRepository), addToLog); expect(actionLog.length, expectedActions.length); _checkActionOrderAndType(expectedActions); });
  15. # Reducer Tests test('reduces to fetched state', () { final

    state = HomePageState.init(); final gifObject = GifObject.empty(); final reducedState = homePageReducer( state, HomePageFetched(gifObject), ); expect(reducedState.status, LoadingStatus.fetched); expect(reducedState.gifObject, gifObject); expect(state, isNot(reducedState)); }); Testing reducers just means checking state has been updated as expected
  16. # The Conclusion - Redux and Flutter allows to create

    easily repeatable tests with very few dependencies. - UI can be tested as we use widgets (not xml or xib)) - Unidirectional data flow is predictable and easy to reason with - Can be a lot of boilerplate. Source code can be found here: https://github.com/eoinfogarty/giphy_app