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

Flutter with Redux and Testing

Sponsored · Ship Features Fearlessly Turn features on and off without deploys. Used by thousands of Ruby developers.
Avatar for Eoin Fogarty 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

Avatar for Eoin Fogarty

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