Slide 1

Slide 1 text

Flutter with Redux and Testing @M0dge

Slide 2

Slide 2 text

# Introduction - Eoin Fogarty - Native Engineer at Cyberagent. @M0dge https://github.com/eoinfogarty

Slide 3

Slide 3 text

# 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

Slide 4

Slide 4 text

# 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

Slide 5

Slide 5 text

# 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

Slide 6

Slide 6 text

# 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

Slide 7

Slide 7 text

# 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( homePageReducer, /*function defined in reducers*/ middleware: [ thunkMiddleware ], /*function defined in library*/ initialState: HomePageState.init(), );

Slide 8

Slide 8 text

# App Structure void main() => runApp(MyApp()); class MyApp extends StatelessWidget { final store = /*create store*/; @override Widget build(BuildContext context) => MaterialApp( home: StoreProvider( store: store, child: Scaffold(body: HomePage(title: 'Gifs!')), ), ); } Create a single store at the root of the app

Slide 9

Slide 9 text

# 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

Slide 10

Slide 10 text

# View class HomePage extends StatelessWidget { @override Widget build(BuildContext context) { return StoreConnector( 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

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

# Action ThunkAction 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

Slide 14

Slide 14 text

# Reducer final Reducer 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

Slide 15

Slide 15 text

# 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

Slide 16

Slide 16 text

# 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); });

Slide 17

Slide 17 text

# 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

Slide 18

Slide 18 text

# 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