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

Flutter at scale: 2020 Edition

Jorge Coca
January 24, 2020

Flutter at scale: 2020 Edition

Flutter has been out of beta for more than a year. It is an amazing framework UI that lets you run your code pretty much anywhere. There's a lot of expectations around it, incredible apps running at 60fps, but... is it ready to be adopted at scale?

At BMW, we faced that question a while ago, and the answer was YES! We tried it, we loved it, and we collected all the necessary information to prove that Flutter was the right choice for us. In this talk, let me show you our process to make Flutter our preferred tool for developing applications, and how we enable multiple teams around the world to deliver incredible experiences fast and safely.

Jorge Coca

January 24, 2020
Tweet

More Decks by Jorge Coca

Other Decks in Programming

Transcript

  1. What do you mean by Flutter at scale? 8 —

    Flutter Europe 2020 / Jorge Coca @ Very Good Ventures
  2. What do you mean by **** at scale? 9 —

    Flutter Europe 2020 / Jorge Coca @ Very Good Ventures
  3. What is a problem? 11 — Flutter Europe 2020 /

    Jorge Coca @ Very Good Ventures
  4. What is a problem? → A question raised for inquiry,

    consideration, or solution 11 — Flutter Europe 2020 / Jorge Coca @ Very Good Ventures
  5. What is a problem? → A question raised for inquiry,

    consideration, or solution → A proposition in mathematics or physics stating something to be done 11 — Flutter Europe 2020 / Jorge Coca @ Very Good Ventures
  6. What is a problem? → A question raised for inquiry,

    consideration, or solution → A proposition in mathematics or physics stating something to be done → A source of perplexity, distress, or vexation 11 — Flutter Europe 2020 / Jorge Coca @ Very Good Ventures
  7. In programming, we solve problems with APIs, and positive and

    efficient communication 12 — Flutter Europe 2020 / Jorge Coca @ Very Good Ventures
  8. What problems are we solving in UI development? 13 —

    Flutter Europe 2020 / Jorge Coca @ Very Good Ventures
  9. What problems are we solving in UI development? → How

    to simplify and/or improve our UI development? 13 — Flutter Europe 2020 / Jorge Coca @ Very Good Ventures
  10. What problems are we solving in UI development? → How

    to simplify and/or improve our UI development? → How to be more productive? 13 — Flutter Europe 2020 / Jorge Coca @ Very Good Ventures
  11. What problems are we solving in UI development? → How

    to simplify and/or improve our UI development? → How to be more productive? → How can we be more cost efficient? 13 — Flutter Europe 2020 / Jorge Coca @ Very Good Ventures
  12. What problems are we solving in UI development? → How

    to simplify and/or improve our UI development? → How to be more productive? → How can we be more cost efficient? → How can we guarantee that our app is robust? 13 — Flutter Europe 2020 / Jorge Coca @ Very Good Ventures
  13. What problems are we solving in UI development? → How

    to simplify and/or improve our UI development? → How to be more productive? → How can we be more cost efficient? → How can we guarantee that our app is robust? 14 — Flutter Europe 2020 / Jorge Coca @ Very Good Ventures
  14. Dart and Flutter are... → Simple, UI-oriented -> widgets 16

    — Flutter Europe 2020 / Jorge Coca @ Very Good Ventures
  15. Dart and Flutter are... → Simple, UI-oriented -> widgets →

    Productive -> hot reload 16 — Flutter Europe 2020 / Jorge Coca @ Very Good Ventures
  16. Dart and Flutter are... → Simple, UI-oriented -> widgets →

    Productive -> hot reload → Cost efficient -> single codebase 16 — Flutter Europe 2020 / Jorge Coca @ Very Good Ventures
  17. Dart and Flutter are... → Simple, UI-oriented -> widgets →

    Productive -> hot reload → Cost efficient -> single codebase → Robustness -> unit, widget, and driver tests 16 — Flutter Europe 2020 / Jorge Coca @ Very Good Ventures
  18. Whatever we do that breaks any of those 4 pillars

    will not let us scale ! 18 — Flutter Europe 2020 / Jorge Coca @ Very Good Ventures
  19. Whatever we do that potentiate any of those 4 pillars

    will skyrocket your development! ! 19 — Flutter Europe 2020 / Jorge Coca @ Very Good Ventures
  20. So... what can we do? 20 — Flutter Europe 2020

    / Jorge Coca @ Very Good Ventures
  21. Flutter is a UI toolkit 21 — Flutter Europe 2020

    / Jorge Coca @ Very Good Ventures
  22. UI = f (state) 22 — Flutter Europe 2020 /

    Jorge Coca @ Very Good Ventures
  23. How we manage state has a big impact on our

    app 23 — Flutter Europe 2020 / Jorge Coca @ Very Good Ventures
  24. Try it by yourself... 24 — Flutter Europe 2020 /

    Jorge Coca @ Very Good Ventures
  25. Try it by yourself... → setState 24 — Flutter Europe

    2020 / Jorge Coca @ Very Good Ventures
  26. Try it by yourself... → setState → Redux 24 —

    Flutter Europe 2020 / Jorge Coca @ Very Good Ventures
  27. Try it by yourself... → setState → Redux → Mobx

    24 — Flutter Europe 2020 / Jorge Coca @ Very Good Ventures
  28. Try it by yourself... → setState → Redux → Mobx

    → ScopedModel 24 — Flutter Europe 2020 / Jorge Coca @ Very Good Ventures
  29. Try it by yourself... → setState → Redux → Mobx

    → ScopedModel → Provider 24 — Flutter Europe 2020 / Jorge Coca @ Very Good Ventures
  30. Try it by yourself... → setState → Redux → Mobx

    → ScopedModel → Provider → ... others? 24 — Flutter Europe 2020 / Jorge Coca @ Very Good Ventures
  31. ! No one likes developers with their own methods 25

    — Flutter Europe 2020 / Jorge Coca @ Very Good Ventures
  32. Please use a tool/ system/pattern that is documented, so everyone

    can learn about it 26 — Flutter Europe 2020 / Jorge Coca @ Very Good Ventures
  33. ..but, if you ask me... 27 — Flutter Europe 2020

    / Jorge Coca @ Very Good Ventures
  34. Why Bloc? It's simple, predictable, productive, and robust! 30 —

    Flutter Europe 2020 / Jorge Coca @ Very Good Ventures
  35. class CounterBloc extends Bloc<CounterEvent, int> { @override int get initialState

    => 0; @override Stream<int> mapEventToState(CounterEvent event) async* { switch (event) { case CounterEvent.decrement: yield state - 1; break; case CounterEvent.increment: yield state + 1; break; } } } 31 — Flutter Europe 2020 / Jorge Coca @ Very Good Ventures
  36. class CounterBloc extends Bloc<CounterEvent, int> { @override int get initialState

    => 0; @override Stream<int> mapEventToState(CounterEvent event) async* { switch (event) { case CounterEvent.decrement: yield state - 1; break; case CounterEvent.increment: yield state + 1; break; } } } 31 — Flutter Europe 2020 / Jorge Coca @ Very Good Ventures
  37. class CounterBloc extends Bloc<CounterEvent, int> { @override int get initialState

    => 0; @override Stream<int> mapEventToState(CounterEvent event) async* { switch (event) { case CounterEvent.decrement: yield state - 1; break; case CounterEvent.increment: yield state + 1; break; } } } 31 — Flutter Europe 2020 / Jorge Coca @ Very Good Ventures
  38. class CounterBloc extends Bloc<CounterEvent, int> { @override int get initialState

    => 0; @override Stream<int> mapEventToState(CounterEvent event) async* { switch (event) { case CounterEvent.decrement: yield state - 1; break; case CounterEvent.increment: yield state + 1; break; } } } 31 — Flutter Europe 2020 / Jorge Coca @ Very Good Ventures
  39. class CounterBloc extends Bloc<CounterEvent, int> { @override int get initialState

    => 0; @override Stream<int> mapEventToState(CounterEvent event) async* { switch (event) { case CounterEvent.decrement: yield state - 1; break; case CounterEvent.increment: yield state + 1; break; } } } 31 — Flutter Europe 2020 / Jorge Coca @ Very Good Ventures
  40. class CounterPage extends StatelessWidget { @override Widget build(BuildContext context) {

    final CounterBloc counterBloc = BlocProvider.of<CounterBloc>(context); return Scaffold( appBar: AppBar(title: Text('Counter')), body: BlocBuilder<CounterBloc, int>( builder: (context, count) { return Text('$count'); }, ), floatingActionButton: Column( children: [ FloatingActionButton( child: Icon(Icons.add), onPressed: () { counterBloc.add(CounterEvent.increment); }, ), FloatingActionButton( child: Icon(Icons.remove), onPressed: () { counterBloc.add(CounterEvent.decrement); }, ), ], ), ); } } 32 — Flutter Europe 2020 / Jorge Coca @ Very Good Ventures
  41. class CounterPage extends StatelessWidget { @override Widget build(BuildContext context) {

    final CounterBloc counterBloc = BlocProvider.of<CounterBloc>(context); return Scaffold( appBar: AppBar(title: Text('Counter')), body: BlocBuilder<CounterBloc, int>( builder: (context, count) { return Text('$count'); }, ), floatingActionButton: Column( children: [ FloatingActionButton( child: Icon(Icons.add), onPressed: () { counterBloc.add(CounterEvent.increment); }, ), FloatingActionButton( child: Icon(Icons.remove), onPressed: () { counterBloc.add(CounterEvent.decrement); }, ), ], ), ); } } 32 — Flutter Europe 2020 / Jorge Coca @ Very Good Ventures
  42. class CounterPage extends StatelessWidget { @override Widget build(BuildContext context) {

    final CounterBloc counterBloc = BlocProvider.of<CounterBloc>(context); return Scaffold( appBar: AppBar(title: Text('Counter')), body: BlocBuilder<CounterBloc, int>( builder: (context, count) { return Text('$count'); }, ), floatingActionButton: Column( children: [ FloatingActionButton( child: Icon(Icons.add), onPressed: () { counterBloc.add(CounterEvent.increment); }, ), FloatingActionButton( child: Icon(Icons.remove), onPressed: () { counterBloc.add(CounterEvent.decrement); }, ), ], ), ); } } 32 — Flutter Europe 2020 / Jorge Coca @ Very Good Ventures
  43. class CounterPage extends StatelessWidget { @override Widget build(BuildContext context) {

    final CounterBloc counterBloc = BlocProvider.of<CounterBloc>(context); return Scaffold( appBar: AppBar(title: Text('Counter')), body: BlocBuilder<CounterBloc, int>( builder: (context, count) { return Text('$count'); }, ), floatingActionButton: Column( children: [ FloatingActionButton( child: Icon(Icons.add), onPressed: () { counterBloc.add(CounterEvent.increment); }, ), FloatingActionButton( child: Icon(Icons.remove), onPressed: () { counterBloc.add(CounterEvent.decrement); }, ), ], ), ); } } 32 — Flutter Europe 2020 / Jorge Coca @ Very Good Ventures
  44. class CounterPage extends StatelessWidget { @override Widget build(BuildContext context) {

    final CounterBloc counterBloc = BlocProvider.of<CounterBloc>(context); return Scaffold( appBar: AppBar(title: Text('Counter')), body: BlocBuilder<CounterBloc, int>( builder: (context, count) { return Text('$count'); }, ), floatingActionButton: Column( children: [ FloatingActionButton( child: Icon(Icons.add), onPressed: () { counterBloc.add(CounterEvent.increment); }, ), FloatingActionButton( child: Icon(Icons.remove), onPressed: () { counterBloc.add(CounterEvent.decrement); }, ), ], ), ); } } 32 — Flutter Europe 2020 / Jorge Coca @ Very Good Ventures
  45. class WeatherBloc extends Bloc<WeatherEvent, WeatherState> { final WeatherRepository weatherRepository; WeatherBloc({@required

    this.weatherRepository}); @override WeatherState get initialState => WeatherEmpty(); @override Stream<WeatherState> mapEventToState(WeatherEvent event) async* { if (event is FetchWeather) { yield WeatherLoading(); try { final Weather weather = await weatherRepository.getWeather(event.city); yield WeatherLoaded(weather: weather); } catch (_) { yield WeatherError(); } } } } 33 — Flutter Europe 2020 / Jorge Coca @ Very Good Ventures
  46. class WeatherBloc extends Bloc<WeatherEvent, WeatherState> { final WeatherRepository weatherRepository; WeatherBloc({@required

    this.weatherRepository}); @override WeatherState get initialState => WeatherEmpty(); @override Stream<WeatherState> mapEventToState(WeatherEvent event) async* { if (event is FetchWeather) { yield WeatherLoading(); try { final Weather weather = await weatherRepository.getWeather(event.city); yield WeatherLoaded(weather: weather); } catch (_) { yield WeatherError(); } } } } 33 — Flutter Europe 2020 / Jorge Coca @ Very Good Ventures
  47. class WeatherBloc extends Bloc<WeatherEvent, WeatherState> { final WeatherRepository weatherRepository; WeatherBloc({@required

    this.weatherRepository}); @override WeatherState get initialState => WeatherEmpty(); @override Stream<WeatherState> mapEventToState(WeatherEvent event) async* { if (event is FetchWeather) { yield WeatherLoading(); try { final Weather weather = await weatherRepository.getWeather(event.city); yield WeatherLoaded(weather: weather); } catch (_) { yield WeatherError(); } } } } 33 — Flutter Europe 2020 / Jorge Coca @ Very Good Ventures
  48. class WeatherBloc extends Bloc<WeatherEvent, WeatherState> { final WeatherRepository weatherRepository; WeatherBloc({@required

    this.weatherRepository}); @override WeatherState get initialState => WeatherEmpty(); @override Stream<WeatherState> mapEventToState(WeatherEvent event) async* { if (event is FetchWeather) { yield WeatherLoading(); try { final Weather weather = await weatherRepository.getWeather(event.city); yield WeatherLoaded(weather: weather); } catch (_) { yield WeatherError(); } } } } 33 — Flutter Europe 2020 / Jorge Coca @ Very Good Ventures
  49. ... and it is also robust! blocTest( 'emits [0, 1]

    when CounterEvent.increment is added', build: () => counterBloc, act: (bloc) => bloc.add(CounterEvent.increment), expect: [0, 1], ); blocTest( 'emits [0, -1] when CounterEvent.decrement is added', build: () => counterBloc, act: (bloc) => bloc.add(CounterEvent.decrement), expect: [0, -1], ); 35 — Flutter Europe 2020 / Jorge Coca @ Very Good Ventures
  50. ... and it is also robust! blocTest( 'emits [0, 1]

    when CounterEvent.increment is added', build: () => counterBloc, act: (bloc) => bloc.add(CounterEvent.increment), expect: [0, 1], ); blocTest( 'emits [0, -1] when CounterEvent.decrement is added', build: () => counterBloc, act: (bloc) => bloc.add(CounterEvent.decrement), expect: [0, -1], ); 35 — Flutter Europe 2020 / Jorge Coca @ Very Good Ventures
  51. ... and it is also robust! blocTest( 'emits [0, 1]

    when CounterEvent.increment is added', build: () => counterBloc, act: (bloc) => bloc.add(CounterEvent.increment), expect: [0, 1], ); blocTest( 'emits [0, -1] when CounterEvent.decrement is added', build: () => counterBloc, act: (bloc) => bloc.add(CounterEvent.decrement), expect: [0, -1], ); 35 — Flutter Europe 2020 / Jorge Coca @ Very Good Ventures
  52. ... even with async code! blocTest( 'emits [empty, loading, error]

    when fetching weather and network fails', build: () { when(weatherRepository.getWeather(any)).thenThrow(Exception('ooops')); return weatherBloc; }, act: (bloc) => bloc.add(FetchWeather('Warsaw')), expect: [WeatherEmpty(), WeatherLoading(), WeatherError()], ); 36 — Flutter Europe 2020 / Jorge Coca @ Very Good Ventures
  53. ... even with async code! blocTest( 'emits [empty, loading, error]

    when fetching weather and network fails', build: () { when(weatherRepository.getWeather(any)).thenThrow(Exception('ooops')); return weatherBloc; }, act: (bloc) => bloc.add(FetchWeather('Warsaw')), expect: [WeatherEmpty(), WeatherLoading(), WeatherError()], ); 36 — Flutter Europe 2020 / Jorge Coca @ Very Good Ventures
  54. ... even with async code! blocTest( 'emits [empty, loading, error]

    when fetching weather and network fails', build: () { when(weatherRepository.getWeather(any)).thenThrow(Exception('ooops')); return weatherBloc; }, act: (bloc) => bloc.add(FetchWeather('Warsaw')), expect: [WeatherEmpty(), WeatherLoading(), WeatherError()], ); 36 — Flutter Europe 2020 / Jorge Coca @ Very Good Ventures
  55. ... even with async code! blocTest( 'emits [empty, loading, error]

    when fetching weather and network fails', build: () { when(weatherRepository.getWeather(any)).thenThrow(Exception('ooops')); return weatherBloc; }, act: (bloc) => bloc.add(FetchWeather('Warsaw')), expect: [WeatherEmpty(), WeatherLoading(), WeatherError()], ); 36 — Flutter Europe 2020 / Jorge Coca @ Very Good Ventures
  56. With bloc, so far... 37 — Flutter Europe 2020 /

    Jorge Coca @ Very Good Ventures
  57. With bloc, so far... → Simple 37 — Flutter Europe

    2020 / Jorge Coca @ Very Good Ventures
  58. With bloc, so far... → Simple → Productive 37 —

    Flutter Europe 2020 / Jorge Coca @ Very Good Ventures
  59. With bloc, so far... → Simple → Productive → Cost

    efficient 37 — Flutter Europe 2020 / Jorge Coca @ Very Good Ventures
  60. With bloc, so far... → Simple → Productive → Cost

    efficient → Robust 37 — Flutter Europe 2020 / Jorge Coca @ Very Good Ventures
  61. Wouldn't be awesome to have an system that is capable

    of telling us what's going on at any given point in time? !"# 38 — Flutter Europe 2020 / Jorge Coca @ Very Good Ventures
  62. Bloc as a BlocDelegate! class SimpleBlocDelegate extends BlocDelegate { @override

    void onEvent(Bloc bloc, Object event) { super.onEvent(bloc, event); print(event); } @override void onTransition(Bloc bloc, Transition transition) { super.onTransition(bloc, transition); print(transition); } @override void onError(Bloc bloc, Object error, StackTrace stacktrace) { super.onError(bloc, error, stacktrace); print('$error, $stacktrace'); } } 39 — Flutter Europe 2020 / Jorge Coca @ Very Good Ventures
  63. Bloc as a BlocDelegate! class SimpleBlocDelegate extends BlocDelegate { @override

    void onEvent(Bloc bloc, Object event) { super.onEvent(bloc, event); print(event); } @override void onTransition(Bloc bloc, Transition transition) { super.onTransition(bloc, transition); print(transition); } @override void onError(Bloc bloc, Object error, StackTrace stacktrace) { super.onError(bloc, error, stacktrace); print('$error, $stacktrace'); } } 39 — Flutter Europe 2020 / Jorge Coca @ Very Good Ventures
  64. Bloc as a BlocDelegate! class SimpleBlocDelegate extends BlocDelegate { @override

    void onEvent(Bloc bloc, Object event) { super.onEvent(bloc, event); print(event); } @override void onTransition(Bloc bloc, Transition transition) { super.onTransition(bloc, transition); print(transition); } @override void onError(Bloc bloc, Object error, StackTrace stacktrace) { super.onError(bloc, error, stacktrace); print('$error, $stacktrace'); } } 39 — Flutter Europe 2020 / Jorge Coca @ Very Good Ventures
  65. Bloc as a BlocDelegate! class SimpleBlocDelegate extends BlocDelegate { @override

    void onEvent(Bloc bloc, Object event) { super.onEvent(bloc, event); print(event); } @override void onTransition(Bloc bloc, Transition transition) { super.onTransition(bloc, transition); print(transition); } @override void onError(Bloc bloc, Object error, StackTrace stacktrace) { super.onError(bloc, error, stacktrace); print('$error, $stacktrace'); } } 39 — Flutter Europe 2020 / Jorge Coca @ Very Good Ventures
  66. All this for free! LoginBloc: { currentState: LoginInitial, event: LoginFormChanged,

    nextState: LoginFormValidationFailure } LoginBloc: { currentState: LoginFormValidationFailure, event: LoginFormChanged, nextState: LoginFormValidationSuccess } LoginBloc: { currentState: LoginFormValidationSuccess, event: LoginSubmitted, nextState: LoginInProgress } LoginBloc: { currentState: LoginInProgress, event: LoginSubmitted, nextState: LoginSuccess } AuthenticationBloc: { currentState: AuthenticationFailure, event: LoggedIn { user: this_is_a_fake_user }, nextState: AuthenticationSuccess } HomeBloc: { currentState: HomeState.storesTab, event: TabSelected { index: 1 }, nextState: HomeState.tasksTab } HomeBloc: { currentState: HomeState.tasksTab, event: TabSelected { index: 2 }, nextState: HomeState.profileTab } 40 — Flutter Europe 2020 / Jorge Coca @ Very Good Ventures
  67. All this for free! LoginBloc: { currentState: LoginInitial, event: LoginFormChanged,

    nextState: LoginFormValidationFailure } LoginBloc: { currentState: LoginFormValidationFailure, event: LoginFormChanged, nextState: LoginFormValidationSuccess } LoginBloc: { currentState: LoginFormValidationSuccess, event: LoginSubmitted, nextState: LoginInProgress } LoginBloc: { currentState: LoginInProgress, event: LoginSubmitted, nextState: LoginSuccess } AuthenticationBloc: { currentState: AuthenticationFailure, event: LoggedIn { user: this_is_a_fake_user }, nextState: AuthenticationSuccess } HomeBloc: { currentState: HomeState.storesTab, event: TabSelected { index: 1 }, nextState: HomeState.tasksTab } HomeBloc: { currentState: HomeState.tasksTab, event: TabSelected { index: 2 }, nextState: HomeState.profileTab } 40 — Flutter Europe 2020 / Jorge Coca @ Very Good Ventures
  68. All this for free! LoginBloc: { currentState: LoginInitial, event: LoginFormChanged,

    nextState: LoginFormValidationFailure } LoginBloc: { currentState: LoginFormValidationFailure, event: LoginFormChanged, nextState: LoginFormValidationSuccess } LoginBloc: { currentState: LoginFormValidationSuccess, event: LoginSubmitted, nextState: LoginInProgress } LoginBloc: { currentState: LoginInProgress, event: LoginSubmitted, nextState: LoginSuccess } AuthenticationBloc: { currentState: AuthenticationFailure, event: LoggedIn { user: this_is_a_fake_user }, nextState: AuthenticationSuccess } HomeBloc: { currentState: HomeState.storesTab, event: TabSelected { index: 1 }, nextState: HomeState.tasksTab } HomeBloc: { currentState: HomeState.tasksTab, event: TabSelected { index: 2 }, nextState: HomeState.profileTab } 40 — Flutter Europe 2020 / Jorge Coca @ Very Good Ventures
  69. All this for free! LoginBloc: { currentState: LoginInitial, event: LoginFormChanged,

    nextState: LoginFormValidationFailure } LoginBloc: { currentState: LoginFormValidationFailure, event: LoginFormChanged, nextState: LoginFormValidationSuccess } LoginBloc: { currentState: LoginFormValidationSuccess, event: LoginSubmitted, nextState: LoginInProgress } LoginBloc: { currentState: LoginInProgress, event: LoginSubmitted, nextState: LoginSuccess } AuthenticationBloc: { currentState: AuthenticationFailure, event: LoggedIn { user: this_is_a_fake_user }, nextState: AuthenticationSuccess } HomeBloc: { currentState: HomeState.storesTab, event: TabSelected { index: 1 }, nextState: HomeState.tasksTab } HomeBloc: { currentState: HomeState.tasksTab, event: TabSelected { index: 2 }, nextState: HomeState.profileTab } 40 — Flutter Europe 2020 / Jorge Coca @ Very Good Ventures
  70. All this for free! LoginBloc: { currentState: LoginInitial, event: LoginFormChanged,

    nextState: LoginFormValidationFailure } LoginBloc: { currentState: LoginFormValidationFailure, event: LoginFormChanged, nextState: LoginFormValidationSuccess } LoginBloc: { currentState: LoginFormValidationSuccess, event: LoginSubmitted, nextState: LoginInProgress } LoginBloc: { currentState: LoginInProgress, event: LoginSubmitted, nextState: LoginSuccess } AuthenticationBloc: { currentState: AuthenticationFailure, event: LoggedIn { user: this_is_a_fake_user }, nextState: AuthenticationSuccess } HomeBloc: { currentState: HomeState.storesTab, event: TabSelected { index: 1 }, nextState: HomeState.tasksTab } HomeBloc: { currentState: HomeState.tasksTab, event: TabSelected { index: 2 }, nextState: HomeState.profileTab } 40 — Flutter Europe 2020 / Jorge Coca @ Very Good Ventures
  71. All this for free! LoginBloc: { currentState: LoginInitial, event: LoginFormChanged,

    nextState: LoginFormValidationFailure } LoginBloc: { currentState: LoginFormValidationFailure, event: LoginFormChanged, nextState: LoginFormValidationSuccess } LoginBloc: { currentState: LoginFormValidationSuccess, event: LoginSubmitted, nextState: LoginInProgress } LoginBloc: { currentState: LoginInProgress, event: LoginSubmitted, nextState: LoginSuccess } AuthenticationBloc: { currentState: AuthenticationFailure, event: LoggedIn { user: this_is_a_fake_user }, nextState: AuthenticationSuccess } HomeBloc: { currentState: HomeState.storesTab, event: TabSelected { index: 1 }, nextState: HomeState.tasksTab } HomeBloc: { currentState: HomeState.tasksTab, event: TabSelected { index: 2 }, nextState: HomeState.profileTab } 40 — Flutter Europe 2020 / Jorge Coca @ Very Good Ventures
  72. All this for free! LoginBloc: { currentState: LoginInitial, event: LoginFormChanged,

    nextState: LoginFormValidationFailure } LoginBloc: { currentState: LoginFormValidationFailure, event: LoginFormChanged, nextState: LoginFormValidationSuccess } LoginBloc: { currentState: LoginFormValidationSuccess, event: LoginSubmitted, nextState: LoginInProgress } LoginBloc: { currentState: LoginInProgress, event: LoginSubmitted, nextState: LoginSuccess } AuthenticationBloc: { currentState: AuthenticationFailure, event: LoggedIn { user: this_is_a_fake_user }, nextState: AuthenticationSuccess } HomeBloc: { currentState: HomeState.storesTab, event: TabSelected { index: 1 }, nextState: HomeState.tasksTab } HomeBloc: { currentState: HomeState.tasksTab, event: TabSelected { index: 2 }, nextState: HomeState.profileTab } 40 — Flutter Europe 2020 / Jorge Coca @ Very Good Ventures
  73. All this for free! LoginBloc: { currentState: LoginInitial, event: LoginFormChanged,

    nextState: LoginFormValidationFailure } LoginBloc: { currentState: LoginFormValidationFailure, event: LoginFormChanged, nextState: LoginFormValidationSuccess } LoginBloc: { currentState: LoginFormValidationSuccess, event: LoginSubmitted, nextState: LoginInProgress } LoginBloc: { currentState: LoginInProgress, event: LoginSubmitted, nextState: LoginSuccess } AuthenticationBloc: { currentState: AuthenticationFailure, event: LoggedIn { user: this_is_a_fake_user }, nextState: AuthenticationSuccess } HomeBloc: { currentState: HomeState.storesTab, event: TabSelected { index: 1 }, nextState: HomeState.tasksTab } HomeBloc: { currentState: HomeState.tasksTab, event: TabSelected { index: 2 }, nextState: HomeState.profileTab } 40 — Flutter Europe 2020 / Jorge Coca @ Very Good Ventures
  74. Jorge, you won't trick me this time! Those are only

    print statements! ! 41 — Flutter Europe 2020 / Jorge Coca @ Very Good Ventures
  75. lumberdash https://github.com/bmw-tech/lumberdash void main() { putLumberdashToWork(withClients: [ ColorizeLumberdash(), FirebaseLumberdash(...), ]);

    logWarning('Hello Warning'); logFatal('Hello Fatal!'); logMessage('Hello Message!'); logError(Exception('Hello Error')); } 42 — Flutter Europe 2020 / Jorge Coca @ Very Good Ventures
  76. lumberdash https://github.com/bmw-tech/lumberdash void main() { putLumberdashToWork(withClients: [ ColorizeLumberdash(), FirebaseLumberdash(...), ]);

    logWarning('Hello Warning'); logFatal('Hello Fatal!'); logMessage('Hello Message!'); logError(Exception('Hello Error')); } 42 — Flutter Europe 2020 / Jorge Coca @ Very Good Ventures
  77. lumberdash https://github.com/bmw-tech/lumberdash void main() { putLumberdashToWork(withClients: [ ColorizeLumberdash(), FirebaseLumberdash(...), ]);

    logWarning('Hello Warning'); logFatal('Hello Fatal!'); logMessage('Hello Message!'); logError(Exception('Hello Error')); } 42 — Flutter Europe 2020 / Jorge Coca @ Very Good Ventures
  78. Bloc is cost efficient import 'package:lumberdash/lumberdash.dart'; class LumberdashBlocDelegate extends BlocDelegate

    { @override void onTransition(Bloc bloc, Transition transition) { super.onTransition(bloc, transition); logMessage(transition); } @override void onError(Bloc bloc, Object error, StackTrace stacktrace) { super.onError(bloc, error, stacktrace); logError(error, stacktrace); } } 43 — Flutter Europe 2020 / Jorge Coca @ Very Good Ventures
  79. With that setup, you get analytics and debug logs without

    development effort 44 — Flutter Europe 2020 / Jorge Coca @ Very Good Ventures
  80. ...not bad, right? ! 45 — Flutter Europe 2020 /

    Jorge Coca @ Very Good Ventures
  81. It looks like this... 46 — Flutter Europe 2020 /

    Jorge Coca @ Very Good Ventures
  82. With bloc, so far... 47 — Flutter Europe 2020 /

    Jorge Coca @ Very Good Ventures
  83. With bloc, so far... → Simple 47 — Flutter Europe

    2020 / Jorge Coca @ Very Good Ventures
  84. With bloc, so far... → Simple → Productive 47 —

    Flutter Europe 2020 / Jorge Coca @ Very Good Ventures
  85. With bloc, so far... → Simple → Productive → Cost

    efficient 47 — Flutter Europe 2020 / Jorge Coca @ Very Good Ventures
  86. With bloc, so far... → Simple → Productive → Cost

    efficient → Robust 47 — Flutter Europe 2020 / Jorge Coca @ Very Good Ventures
  87. Do you think we can do more? 48 — Flutter

    Europe 2020 / Jorge Coca @ Very Good Ventures
  88. Do you think we can do more? 50 — Flutter

    Europe 2020 / Jorge Coca @ Very Good Ventures
  89. Do you think we can do more? → Simple 50

    — Flutter Europe 2020 / Jorge Coca @ Very Good Ventures
  90. Do you think we can do more? → Simple →

    Productive 50 — Flutter Europe 2020 / Jorge Coca @ Very Good Ventures
  91. Do you think we can do more? → Simple →

    Productive → Cost efficient 50 — Flutter Europe 2020 / Jorge Coca @ Very Good Ventures
  92. Do you think we can do more? → Simple →

    Productive → Cost efficient → Robust 50 — Flutter Europe 2020 / Jorge Coca @ Very Good Ventures
  93. We've only seen how to unit test our blocs... 51

    — Flutter Europe 2020 / Jorge Coca @ Very Good Ventures
  94. How can we truly measure the performance of our app?

    52 — Flutter Europe 2020 / Jorge Coca @ Very Good Ventures
  95. Ozzie With Ozzie, during integration tests, we can capture screenshots

    of every frame we want, and we can also measure the performance of our app, breaking the build if necessary. 54 — Flutter Europe 2020 / Jorge Coca @ Very Good Ventures
  96. flutter driver tests void main() { FlutterDriver driver; setUpAll(() async

    { driver = await FlutterDriver.connect(); }); tearDownAll(() async { if (driver != null) driver.close(); }); test('initial counter is 0', () async { await driver.waitFor(find.text('0')); }); test('initial counter is 0', () async { await driver.tap(find.byType('FloatingActionButton')); await driver.waitFor(find.text('1')); }); } 55 — Flutter Europe 2020 / Jorge Coca @ Very Good Ventures
  97. flutter driver tests void main() { FlutterDriver driver; setUpAll(() async

    { driver = await FlutterDriver.connect(); }); tearDownAll(() async { if (driver != null) driver.close(); }); test('initial counter is 0', () async { await driver.waitFor(find.text('0')); }); test('initial counter is 0', () async { await driver.tap(find.byType('FloatingActionButton')); await driver.waitFor(find.text('1')); }); } 55 — Flutter Europe 2020 / Jorge Coca @ Very Good Ventures
  98. flutter driver tests void main() { FlutterDriver driver; setUpAll(() async

    { driver = await FlutterDriver.connect(); }); tearDownAll(() async { if (driver != null) driver.close(); }); test('initial counter is 0', () async { await driver.waitFor(find.text('0')); }); test('initial counter is 0', () async { await driver.tap(find.byType('FloatingActionButton')); await driver.waitFor(find.text('1')); }); } 55 — Flutter Europe 2020 / Jorge Coca @ Very Good Ventures
  99. flutter driver tests void main() { FlutterDriver driver; setUpAll(() async

    { driver = await FlutterDriver.connect(); }); tearDownAll(() async { if (driver != null) driver.close(); }); test('initial counter is 0', () async { await driver.waitFor(find.text('0')); }); test('initial counter is 0', () async { await driver.tap(find.byType('FloatingActionButton')); await driver.waitFor(find.text('1')); }); } 55 — Flutter Europe 2020 / Jorge Coca @ Very Good Ventures
  100. flutter driver tests void main() { FlutterDriver driver; setUpAll(() async

    { driver = await FlutterDriver.connect(); }); tearDownAll(() async { if (driver != null) driver.close(); }); test('initial counter is 0', () async { await driver.waitFor(find.text('0')); }); test('initial counter is 0', () async { await driver.tap(find.byType('FloatingActionButton')); await driver.waitFor(find.text('1')); }); } 55 — Flutter Europe 2020 / Jorge Coca @ Very Good Ventures
  101. How does it wotk with Ozzie? 56 — Flutter Europe

    2020 / Jorge Coca @ Very Good Ventures
  102. void main() { FlutterDriver driver; Ozzie ozzie; setUpAll(() async {

    driver = await FlutterDriver.connect(); ozzie = Ozzie.initWith(driver, groupName: 'counter'); }); tearDownAll(() async { if (driver != null) driver.close(); ozzie.generateHtmlReport(); }); test('initial counter is 0', () async { await ozzie.profilePerformance('counter0', () async { await driver.waitFor(find.text('0')); await ozzie.takeScreenshot('initial_counter_is_0'); }); }); test('initial counter is 0', () async { await ozzie.profilePerformance('counter1', () async { await driver.tap(find.byType('FloatingActionButton')); await driver.waitFor(find.text('1')); await ozzie.takeScreenshot('counter_is_1'); }); }); } 57 — Flutter Europe 2020 / Jorge Coca @ Very Good Ventures
  103. void main() { FlutterDriver driver; Ozzie ozzie; setUpAll(() async {

    driver = await FlutterDriver.connect(); ozzie = Ozzie.initWith(driver, groupName: 'counter'); }); tearDownAll(() async { if (driver != null) driver.close(); ozzie.generateHtmlReport(); }); test('initial counter is 0', () async { await ozzie.profilePerformance('counter0', () async { await driver.waitFor(find.text('0')); await ozzie.takeScreenshot('initial_counter_is_0'); }); }); test('initial counter is 0', () async { await ozzie.profilePerformance('counter1', () async { await driver.tap(find.byType('FloatingActionButton')); await driver.waitFor(find.text('1')); await ozzie.takeScreenshot('counter_is_1'); }); }); } 57 — Flutter Europe 2020 / Jorge Coca @ Very Good Ventures
  104. void main() { FlutterDriver driver; Ozzie ozzie; setUpAll(() async {

    driver = await FlutterDriver.connect(); ozzie = Ozzie.initWith(driver, groupName: 'counter'); }); tearDownAll(() async { if (driver != null) driver.close(); ozzie.generateHtmlReport(); }); test('initial counter is 0', () async { await ozzie.profilePerformance('counter0', () async { await driver.waitFor(find.text('0')); await ozzie.takeScreenshot('initial_counter_is_0'); }); }); test('initial counter is 0', () async { await ozzie.profilePerformance('counter1', () async { await driver.tap(find.byType('FloatingActionButton')); await driver.waitFor(find.text('1')); await ozzie.takeScreenshot('counter_is_1'); }); }); } 57 — Flutter Europe 2020 / Jorge Coca @ Very Good Ventures
  105. void main() { FlutterDriver driver; Ozzie ozzie; setUpAll(() async {

    driver = await FlutterDriver.connect(); ozzie = Ozzie.initWith(driver, groupName: 'counter'); }); tearDownAll(() async { if (driver != null) driver.close(); ozzie.generateHtmlReport(); }); test('initial counter is 0', () async { await ozzie.profilePerformance('counter0', () async { await driver.waitFor(find.text('0')); await ozzie.takeScreenshot('initial_counter_is_0'); }); }); test('initial counter is 0', () async { await ozzie.profilePerformance('counter1', () async { await driver.tap(find.byType('FloatingActionButton')); await driver.waitFor(find.text('1')); await ozzie.takeScreenshot('counter_is_1'); }); }); } 57 — Flutter Europe 2020 / Jorge Coca @ Very Good Ventures
  106. void main() { FlutterDriver driver; Ozzie ozzie; setUpAll(() async {

    driver = await FlutterDriver.connect(); ozzie = Ozzie.initWith(driver, groupName: 'counter'); }); tearDownAll(() async { if (driver != null) driver.close(); ozzie.generateHtmlReport(); }); test('initial counter is 0', () async { await ozzie.profilePerformance('counter0', () async { await driver.waitFor(find.text('0')); await ozzie.takeScreenshot('initial_counter_is_0'); }); }); test('initial counter is 0', () async { await ozzie.profilePerformance('counter1', () async { await driver.tap(find.byType('FloatingActionButton')); await driver.waitFor(find.text('1')); await ozzie.takeScreenshot('counter_is_1'); }); }); } 57 — Flutter Europe 2020 / Jorge Coca @ Very Good Ventures
  107. Recap → Complex problems, simple solutions → Goals: Simple, productive,

    cost effective, and robust 60 — Flutter Europe 2020 / Jorge Coca @ Very Good Ventures
  108. Recap → Complex problems, simple solutions → Goals: Simple, productive,

    cost effective, and robust → flutter_bloc by @felangelov checks all the boxes 60 — Flutter Europe 2020 / Jorge Coca @ Very Good Ventures
  109. Recap → Complex problems, simple solutions → Goals: Simple, productive,

    cost effective, and robust → flutter_bloc by @felangelov checks all the boxes → lumberdash and ozzie flutter_bloc 60 — Flutter Europe 2020 / Jorge Coca @ Very Good Ventures
  110. Stay in touch! → @jcocaramos → Chicago Flutter Meetup 61

    — Flutter Europe 2020 / Jorge Coca @ Very Good Ventures
  111. Stay in touch! → @jcocaramos → Chicago Flutter Meetup →

    Very Good Ventures ! 61 — Flutter Europe 2020 / Jorge Coca @ Very Good Ventures