Giter VIP home page Giter VIP logo

flutter-state-management-pocs's Introduction

Flutter state management

Why state management?

Flutter apps consist of widgets. A widget can have multiple child widgets, so an app quickly becomes a tree-like structure of widgets depending on other widgets.

Data can be shared across multiple widgets, which quickly becomes painful to pass along through each constructor. With a state management solution, the data becomes accessible from anywhere in the app. So instead of having a widget holding data solely to pass it along, the data now only lives in the widgets that actually use it. When data changes that require a UI element to rebuild, only the widget and its child widgets will be rebuilt, rather than everything in between the widget mutating the data and the widget displaying it. This makes developing easier and increases performance.

Popular state management approaches:

  • Provider
  • BloC Pattern/Rx
  • Scoped Model
  • Redux
  • MobX

Provider

  • Little setup needed
  • Easy to understand
  • Efficient way of rebuilding UI
  • Very popular approach, so there's a lot of documentation
  • Needs to have its Consumer type declared (safe, but restricting)

Installation:

dependencies:
  flutter:
    sdk: flutter
  provider: ^3.0.0
import 'package:provider/provider.dart';

Here we provide the Counter() model to MyApp(). ChangeNotifierProvider will rebuild the UI everytime Counter() changes. Counter() is initialized inside the builder, making Provider take care of Counter's lifecycle, so it will call dispose() when Counter() isn't needed anymore.

void main() => runApp(
  ChangeNotifierProvider(
    create: (context) => Counter(), 
    child: MyApp(),
  ),
);

Counter model:

class Counter with ChangeNotifier {
  int value = 0;

  void increment() {
    value += 1;
    notifyListeners();
  }
}

Inside increment, notifyListeners() is called. This will let our ChangeNotifierProvider know that it needs to rebuild the UI element it's listening to.

Ui element:

Column(
  mainAxisAlignment: MainAxisAlignment.center,
  children: <Widget>[
    Text('You have pushed the button this many times:'),
    Consumer<Counter>(
      builder: (context, counter, child) => Text(
        '${counter.value}',
        style: Theme.of(context).textTheme.display1,
      ),
    ),
  ],
);

Here a Consumer() is used with type Counter. Provider needs to know the type to know which element to rebuild. The context, instance of Counter and an optional child Widget are passed into the Text() widget, which is rebuilt every time notifyListeners() is called within Counter's increment() function.

Ui element:

floatingActionButton: FloatingActionButton(
  onPressed: () =>
    Provider.of<Counter>(context, listen: false).increment(),
    tooltip: 'Increment',
    child: Icon(Icons.add),
),

This is the button which calls the increment() function. Provider.of is another way to access the model object held by an ancestor Provider. This will also listen to changes in the model by default and rebuilds the whole widget when notified. listen: false makes sure that the previous mentioned behaviour is disabled. Without it, it would rebuild MyHomepage entirely. We only need to access increment() here, our Consumer will take care of rebuilding the Text() widget.

BloC - overview

BloC: Business logic component

BloC is a pattern very similar to Redux the way it's used in React. It was created by Google and announced at Google I/O 2018. There is a BloC library that implements the InheritedWidget under the hood, and also makes passing state along the widget tree much more intuitive. The BloC lives between the UI that consumes the data, and the source (e.g. an API response) that delivers the data.

BloC creates a very explicit one-way data flow that is clearly divided into three steps:

  • Event: Something happens, e.g.: a user pushes a button
  • BLOC: The event is dispatched to the block, which will create the next state and omit it to
  • The stream: Which we can listen to in our UI so it will be rebuilt once data changes.

Installation

dev_dependencies:
  flutter_test:
    sdk: flutter
  bloc: ^3.0.0
  flutter_bloc: ^3.1.0

counter_bloc.dart:

The event is an enum that is handled by the switch-case in mapEventToState(). mapEventToState() returns a Stream of the state, the count, and takes in events (increment or decrement).

import 'dart:async';
import 'package:bloc/bloc.dart';

enum CounterEvent { increment, decrement }

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

main.dart:

Here, we wrap the CounterPage in a BlocProvider that serves the CounterBloc, giving the CounterPage access to CounterEvent.increment and CounterEvent.decrement, as well as the count data.

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      home: BlocProvider<CounterBloc>(
        create: (context) => CounterBloc(),
        child: CounterPage(),
      ),
    );
  }
}

counter_page.dart:

counterBloc is available because CounterPage inside main.dart was wrapped with a BlockProvider.

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';

import 'counter_bloc.dart';

class CounterPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {

    final CounterBloc counterBloc = BlocProvider.of<CounterBloc>(context);

    return Scaffold(
      ...
      body: BlocBuilder<CounterBloc, int>(
        builder: (context, count) {
          return Center(
            child: Text(
              '$count',
              style: TextStyle(fontSize: 24.0),
            ),
        }
      floatingActionButton: ...
      onPressed: () {
                counterBloc.add(CounterEvent.increment);
              },
              ...
              onPressed: () {
                counterBloc.add(CounterEvent.decrement);
              },
      
      ),
    );

BLoC - A more complex implementation

Weather application

I'm using this repository as an example on how to handle more complex state. This also shows a good approach on how to structure the bloc, state and events. It also features unit tests using bloc_test and mockito.

Event

Events are the input to a Bloc. Can be as simple as an enum, or a class on its own.

States

State is the output of a Bloc and represents a part of the application's state. UI components can be notified of states and redraw (portions of) themselves.

Transitions

The change from one state to another. A Transition consists of the current state, the event and the next state. onTransition is a method that can be overridden to handle every local Bloc's Transition. onTransition is called just before a Bloc's state has been updated. An instance of Transition stores the currentState, nextState and the event. It's possible to gather all Transitions in one place.

Streams

A sequence of asynchronous data.

Blocs

Business Logic Component. A component which converts a Stream of incoming Events into a Stream of outgoing States. Always extends the base Bloc class from the core package. A Bloc has to define an initial state. It must also implement a function called mapEventToState. The current bloc state is accessible through the state property. Duplicate states are ignored. The add() method of a Bloc takes an event and triggers mapEventToState. Events are handled in order of which they were added and can be enqueued.

onError is a method that can be overridden to handle every local Bloc Exception.

BlocBuilder

BlocBuilder is a Flutter widget from the flutter_bloc package. BlocBuilder handles building the widget in response to new states. The builder function can be called many times, so it needs to be a pure function. Note: if the bloc parameter is omitted, BlocBuilder will automatically perform a lookup using BlocProvider and the current BuildContext.

BlocProvider

BlocProvider is a Flutter widget which provides a bloc to its children via BlocProvider.of<T>(context). It's used as a dependency injection widget so that a single instance of a bloc can be provided to multiple widgets within a subtree. BlocProvider is also used to create new blocs and it automatically handles closing the bloc.

If BlocProvider is used to inject an existing bloc, it will not handle automatically closing it (as it didn't create it).

MultiBlocProvider

MultiBlocProvider merges multiple BlocProviders into one. MultiBlocProvider improves readability and eleminates nesting multiple BlocProviders.

Weather app flow

This example app uses a data model, weather.dart, which is a basic Dart class extended with Equatable, which makes comparing objects easier. The data source is the weather_repository.dart, which contains two fetch methods that return a Future of Weather model (based on randomly generating a temperature).

The bloc layer is split up in three parts:

  • weather_bloc.dart, the bloc that handles events and outputs states.
  • weather_event.dart, a set of basic Dart classes: FetchedWeather and FetchedDetailedWeather.
  • weather_state.dart, a set of basic Dart classes: WeatherInitial, WeatherLoadSuccess, WeatherLoadInProgress and WeatherError.

Snippets

weather_event.dart

The WeatherEvent has one field: cityName, by which it is fetched.

abstract class WeatherEvent extends Equatable {
  const WeatherEvent();
}

class FetchedWeather extends WeatherEvent {
  final String cityName;

  const FetchedWeather(this.cityName);

  @override
  List<Object> get props => [cityName];
}

weather_state.dart

abstract class WeatherState extends Equatable {
  const WeatherState();
}
...
class WeatherLoadSuccess extends WeatherState {
  final Weather weather;
  const WeatherLoadSuccess(this.weather);
  @override
  List<Object> get props => [weather];
}

weather_bloc.dart

class WeatherBloc extends Bloc<WeatherEvent, WeatherState> {
  final WeatherRepository weatherRepository;

  WeatherBloc(this.weatherRepository);

  @override
  WeatherState get initialState => WeatherInitial();

  @override
  Stream<WeatherState> mapEventToState(
    WeatherEvent event,
  ) async* {
    yield WeatherLoadInProgress();
    ...
    if (event is FetchedWeather) {
      try {
        final weather = await weatherRepository.fetchWeather(event.cityName);
        yield WeatherLoadSuccess(weather);
      } on NetworkError {
        yield WeatherError("Couldn't fetch weather. Is the device online?");
      }
    }
    ...
  }
}

weather_search_page.dart

...
child: BlocBuilder<WeatherBloc, WeatherState>(
  builder: (context, state) {
    if (state is WeatherInitial) {
      return buildInitialInput();
    }
  }
),
...

Testing

Bloc makes testing very straight forward. It requires the following package:

dev_dependencies:
  bloc_test: ^3.0.0

Note: bloc_test depends on mockito, meta, rxdart, test as well as bloc itself.

blocTest - generics

First of all, the WeatherRepository is mocked with the Mockito package.

class MockWeatherRepository extends Mock implements WeatherRepository {}

As usual with unit testing, there is a setUp and tearDown method. These are run before and after every unit test, respectively. These are optional. In our case, we only need the setUp method. Before each test the mocked WeatherRepository is instantiated.

setUp(() {
  mockWeatherRepository = MockWeatherRepository();
});

The blocTest case

The blocTest has a description, a build method, the act method (where the event is added to the bloc) and the expected outcome, in this case a series of states.

blocTest(
  'emits [WeatherLoading, WeatherLoaded] when successful',
  build: () {
    when(mockWeatherRepository.fetchWeather(any))
        .thenAnswer((_) async => weather);
    return WeatherBloc(mockWeatherRepository);
  },
  act: (bloc) => bloc.add(FetchedWeather('London')),
  expect: [
    WeatherInitial(),
    WeatherLoadInProgress(),
    WeatherLoadSuccess(weather),
  ],
);

Testing bloc dependencies

To test code that depends on other blocs, or blocs that depend on other blocs, we can mock our bloc. First of all, we create a Mock class:

class MockWeatherBloc extends MockBloc<WeatherEvent, WeatherState>
    implements WeatherBloc {}

Which is instatiated inside the setUp() method:

setUp(() {
  mockWeatherBloc = MockWeatherBloc();
});

In this test case, the expectLater() function is used. The difference between that and a regular expect() is that this returns a Future that that completes when the matcher has finished matching.

test('Example mocked BLoC test', () {
  whenListen(
    mockWeatherBloc,
    Stream.fromIterable([WeatherInitial(), WeatherLoadInProgress()]),
  );

  expectLater(
    mockWeatherBloc,
    emitsInOrder([WeatherInitial(), WeatherLoadInProgress()]),
  );
});

Test groups

Test cases can be grouped by using group(). These just feature a description and the unit tests themselves. This can be particulary useful when combined with an IDE like Visual Studio Code. Here is an example of a group:

group('blocTests', () {
  // Your tests go here
});

Inside the IDE, it now gives you the ability to run/debug test groups from within the code files.

And the test output is also grouped by the IDE:

Conventions

Naming conventions

The BloC Documentation writes the following on naming conventions:

Events

Events should be named in the past tense because events are things that have already occurred from the bloc's perspective.

Good examples: CounterStarted, CounterIncremented, CounterDecremented, CounterIncrementRetried

Bad examples: Initial, CounterInitialized, Increment, DoIncrement, IncrementCounter

State

States should be nouns because a state is just a snapshot at a particular point in time.

Good examples: CounterInitial, CounterLoadSuccess, CounterLoadInProgress

Bad examples: Initial, Loading, Success, Succeeded, Loaded, Failure, Failed



MobX

The user fires an action, which mutates an observable, that notifies a reaction.

Observables

Observables represent the reactive state. Like Redux, MobX uses a Store to collect the related observable state under one class.

Actions

Actions define how the observable is mutated. Actions do not mutate them directly. An action adds a semantic meaning to the mutations. In the Counter example, an action would be increment() rather than counter++.

The observable is only notified upon the completion of the action.

Reactions

Whenever an observable is changed, a reaction is notified (e.g. to rebuild the UI). A nice feature of reaction is that it automatically tracks all the observables without any explicit code. The act of reading an observable within a reaction is enough to track it.

Note: The package mobx_codegen allows the use of annotations to mark fields/functions as @observable or @action.

Installation:

dependencies:
 flutter:
   sdk: flutter
 mobx: ^0.4.0
 flutter_mobx: ^0.3.4
dev_dependencies:
 flutter_test:
   sdk: flutter
 build_runner: ^1.3.1
 mobx_codegen: ^0.3.11

Store

import 'package:mobx/mobx.dart';

part 'counter.g.dart';

class Counter = _Counter with _$Counter;

abstract class _Counter with Store {
  @observable
  int value = 0;

  @action
  void increment() {
    value++;
  }
}

The abstract class _Counter includes the Store mixin. Using flutter packages pub run build_runner build, we generate a file named counter.g.dart. To do this automatically we can make the builder watch, using this command: flutter packages pub run build_runner watch.

The generated counter.g.dart looks like this:

// GENERATED CODE - DO NOT MODIFY BY HAND

part of 'counter.dart';

// **************************************************************************
// StoreGenerator
// **************************************************************************

// ignore_for_file: non_constant_identifier_names, unnecessary_lambdas, prefer_expression_function_bodies, lines_longer_than_80_chars, avoid_as, avoid_annotating_with_dynamic

mixin _$Counter on _Counter, Store {
  final _$valueAtom = Atom(name: '_Counter.value');

  @override
  int get value {
    _$valueAtom.context.enforceReadPolicy(_$valueAtom);
    _$valueAtom.reportObserved();
    return super.value;
  }

  @override
  set value(int value) {
    _$valueAtom.context.conditionallyRunInAction(() {
      super.value = value;
      _$valueAtom.reportChanged();
    }, _$valueAtom, name: '${_$valueAtom.name}_set');
  }

  final _$_CounterActionController = ActionController(name: '_Counter');

  @override
  void increment() {
    final _$actionInfo = _$_CounterActionController.startAction();
    try {
      return super.increment();
    } finally {
      _$_CounterActionController.endAction(_$actionInfo);
    }
  }
}

main.dart

import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import './counter.dart';

// Instantiate the store
final counter = Counter();

...
// inside build method:
Observer(
  builder: (_) => Text(
    '${counter.value}',
    style: Theme.of(context).textTheme.display1,
  ),
),
...

The Text() widget is wrapped in an Observer with a builder function. This makes sure the Text() widget is notified on changes.

flutter-state-management-pocs's People

Contributors

tomvanlieshout avatar

Watchers

 avatar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.