Flutter Sample Part 9 – Using the Bloc Pattern

(This is my small series for getting started with Flutter mobile SDK for iOS and Android. See below for previous posts)

Until now this sample Flutter app has been built without much consideration to organisation or architecture. All the files are in the same root lib folder, widgets are too big and hard to read. I also haven’t really made any attempt to separate business logic from presentation and as a result widgets have lots of responsibilites other than than just displaying UI. So it seems appropriate that the next step would be to refactor the app around a well understood architectural pattern, and in Flutter one of the most popular is the Bloc Pattern.

flutter_bloc package

But before attempting to refactor the app to use this pattern I’ve tried to tidy up the workspace by separating widgets into their own files and moving files into folders where appropriate. The workspace hierarchy now looks like this:

  • flutter_flickr
    •  lib
      • blocs: bloc related files (see below)
      • dataModel: Contains files for the Flickr data objects
      • dataProvider: Classes for connecting to the Flickr API and local Storage
      • repositories: Repositories for providing access to Flickr images and Favourites data
      • routes: Custom routes, only one right now
      • screens: Widgets representing our screens, one for the image list and one for our favourites
      • widgets: Widgets for small pieces of UI such as the owner badge)

The dataModel folder contains the Flickr data objects as before, while the dataProvider folder now has classes that are responsible for fetching the data from the API and also saving data to local storage. These have been separated from the classes in the repositories folder which contain similar classes for images and favorites, but don’t need to understand anything about their underlying implementation.

The widgets and screens folders contain all the widget files – for small pieces of UI and screens. I’ve split up a few previously large widgets into smaller ones that are hopefully easier to read.

That just leaves the blocs folder for, well, Blocs. The Bloc pattern is one of the architecural patterns of choice for Flutter apps. I’ve been very used to using MVVM (on iOS and particularly in XAML apps) and the Bloc pattern on flutter feels very similar. As with MVVM it decouples the UI and business logic into separate classes so that the views are only repsonsible for displaying the UI and aren’t dependent on the implementation of the business logic. As before, widgets correspond to Views while Blocs are now roughly (in my mind) equivalent to ViewModels. One key difference to MVVM though is that data-binding is one way, from the Bloc to the widget. Widgets can update application state via events and Blocs use these events to determine state and notify widgets of these state changes.

In place of the native data-binding provided in XAML apps, the mechanism used in Flutter is Streams (similar to how MVVM can be achieved in iOS apps using RxSwift). There are great introductions to Streams here and here but very briefly the Dart langauge provides the StreamController class and which enables to you write to a stream (to the stream “sink”) and read from the stream whenever a new value is published. The StreamBuilder widget can used to specify a stream and an associated child widget that should be rebuilt whenever a new value is read from the stream. This will then rebuild the widget heirarchy without having to explicitly call setState() from within the widget.

There are a few different packages available which simplify the boiler-plate code needed to set up and create blocs. I’m using the flutter_bloc package which wraps up the creation and management of Blocs into a few key classes:

BlocBuilder – the widget used in place of StreamBuilder, that handles re-building widgets when the Bloc state changes.
BlocProvider – this allows you to reference a Bloc created in the parent context from a child widget.

So with that very small introduction, let’s apply the Bloc pattern to the Flickr sample application. I’ve used the layout here to organise each file for a bloc into its own folder. I’ve created two blocs in the blocs folder, one for each screen. The first, flickrInterestingBloc

  • blocs
    • flickrInteresting
      • flickrInterestingBloc
      • flickrInterestingEvent
      • flickrInterestingState

The FlickrInterestingEvent class serves as the base class of all the associated events for the flickrInterestingBloc. It has three events:
FlickrInterestingAddFavorite and FlickrInterestingRemoveFavorite are related to the user adding and removing favourites. Both of them take a reference to the photo to be added or removed. The third, FlickrInterestingRefreshEvent, is used to allow working with pull to refresh on the list.

abstract class FlickrInterestingEvent { }

class FlickrInterestingRefreshEvent extends FlickrInterestingEvent { }

class FlickrInterestingAddFavorite extends FlickrInterestingEvent {

  FlickrPhoto photo;

  FlickrInterestingAddFavorite({this.photo});
}

class FlickrInterestingRemoveFavorite extends FlickrInterestingEvent {

  FlickrPhoto photo;

  FlickrInterestingRemoveFavorite({this.photo});
}

In regards to State, there isn’t really anything other than the list of images and whether the list is loading or not. So there is a class FlickrInterestingState class which serves as the base class for two other states (and contains the reference to the list of photos (FlickrResult), and two loading related events, FlickrInterestingLoading and FlickrInterestingLoaded.

class FlickrInterestingState {

  //State
  FlickrResult interestingPhotos;
  int resultsCount = 0;

  FlickrInterestingState() {

    interestingPhotos = FlickrResult();

  }
}

class FlickrInterestingLoading extends FlickrInterestingState {}

class FlickrInterestingLoaded extends FlickrInterestingState {}

Finally, the bloc itself, FlickrInterestingBloc inherits from the Bloc class in the flutter_bloc package and uses the FlickrInterestingEvent class for events and FlickrInterstingState class for maintaining state. The Bloc class containts a method, mapEventToState, where we have to define how the state changes based on a new event.

class FlickrInterestingBloc extends Bloc<FlickrInterestingEvent, FlickrInterestingState> {

  FlickrRepository flickrRepository;

  FlickrInterestingBloc({@required this.flickrRepository});

  void refreshPhotos() {
    dispatch(FlickrInterestingRefreshEvent());
  }

  @override
  FlickrInterestingState get initialState => FlickrInterestingState();

  @override
  Stream<FlickrInterestingState> mapEventToState(FlickrInterestingEvent event) async* {

    if (event is FlickrInterestingRefreshEvent) {

      yield FlickrInterestingLoading();

      FlickrInterestingState newState = FlickrInterestingLoaded();

      newState.interestingPhotos = await this.flickrRepository.getInterestingPhotos();
      newState.resultsCount = newState?.interestingPhotos?.photos?.total ?? 0;

      yield newState;
    }
  }
}

Then in the refactored ImageListWidget we construct the widget using the BlocBuilder widget and we get access to the Bloc via a call to BlocProvider.of using the BuildContext:

class ImageListWidget extends StatefulWidget {

  State<ImageListWidget> createState() => _ImageListWidgetState();
}

class _ImageListWidgetState extends State<ImageListWidget> {

  Completer<void> _refreshCompleter;

  @override
  void initState() {

    super.initState();
    _refreshCompleter = Completer<void>();

  }

  @override
  Widget build(BuildContext context) {

    var flickrInterestingBloc = BlocProvider.of<FlickrInterestingBloc>(context);

    return Container(
      child: BlocListener(
        bloc: flickrInterestingBloc,
        listener: (BuildContext context, FlickrInterestingState state) {

          if (state is FlickrInterestingLoaded) {
            _refreshCompleter?.complete();
            _refreshCompleter = Completer();
          }
        },
        child: BlocBuilder(
            bloc: flickrInterestingBloc,
            builder: (context, FlickrInterestingState state) {
              return RefreshIndicator(
                  child: Container(
                    key: PageStorageKey('myListView'),
                    child: ListView.builder(
                        itemCount: state.resultsCount,
                        itemBuilder: (BuildContext context, int position) {
                          return getRow(position, context);
                        })
                  ),
                  onRefresh: () {
                    flickrInterestingBloc.refreshPhotos();
                    return _refreshCompleter.future;
                  });
            })
        )
      );
  }

  Widget getRow(int i, BuildContext context) {

    final flickrInterestingBloc = BlocProvider.of<FlickrInterestingBloc>(context);

    FlickrPhoto photo = flickrInterestingBloc.currentState.interestingPhotos.photos.photo[i];
    String farm = photo.farm.toString();
    String secret = photo.secret;
    String server = photo.server;

    String photoUrl = "https://farm$farm.staticflickr.com/$server/${photo.id}_$secret.jpg";

    return ImageListItemWidget(photo: photo, photoUrl: photoUrl);
  }
}

I’ve refactored out most of the code for returning a row in the ImageListWidget as the getRow method was getting too complicated. This is now part of a new widget, ImageListItemWidget. The ImageListItemWidget needs to know if a particular image is a favorite or not, so it is built using BlocBuilder and passing in an instance of the FlickrFavoritesBloc which is returned from another call to BlocProvider.of using the BuildContext:

class ImageListItemWidget extends StatelessWidget {

  final FlickrPhoto photo;
  final String photoUrl;

  @required
  ImageListItemWidget({Key key, this.photo, this.photoUrl}) : super(key: key);

  @override
  Widget build(BuildContext context) {

    var flickrFavoritesBloc = BlocProvider.of<FlickrFavoritesBloc>(context);

    return GestureDetector(
      child: Container(
          padding: new EdgeInsets.all(10.0),
          child: Stack(
              alignment: AlignmentDirectional.bottomCenter,
              children: <Widget>[
                Hero(
                    tag: photo.title,
                    child: CachedNetworkImage(
                      placeholder: (context, url) => Image.asset('images/placeholder-image.jpg'),
                      imageUrl: photoUrl,
                      height: 240.0,
                      width: double.infinity,
                      fit: BoxFit.cover,
                    )
                ),
                BlocBuilder(
                  bloc: flickrFavoritesBloc,
                  builder: (context, FlickrFavoritesState state) {
                    return FlickrBadgeWidget(
                      owner: photo.owner,
                      ownerName: photo.ownerName,
                      isFavorite: state.favoritesRepository.isFavorite(photo),
                      onFavoriteChanged: (isFavorite) {
                        if (isFavorite) {
                          flickrFavoritesBloc.addFavoritePhoto(photo);
                        } else {
                          flickrFavoritesBloc.removeFavoritePhoto(photo);
                        }
                      },
                    );
                  })
              ]
          )
      ),
      onTap: () {
        _navigateToItem(context, photo);
      },
    );
  }

  _navigateToItem(BuildContext context, FlickrPhoto photo) {
    Navigator.push(
      context,
      CustomRoute(
        builder: (context) => ImageDetailsScreen(photo: photo))
      );
  }
}

The ImageListItemWidget is also now responsible for the callback which is executed when tapping on an image and it navigates to the ImageDetailsScreen. (In a more complicated app it might be useful to create some more sophisticated handling of routing within the app but I think it’s fine to leave it here for this sample).

In the previous description of the ImageListWidget widget, it uses a Completer called _refreshCompleter. This is necessary because the RefreshIndicator expects a Future in its onRefresh callback but we’re now just calling flickrInterestingBloc.refreshPhotos() which doesn’t return a Future. A good example of this can be seen in this Weather app sample using Blocs.

So in the widgets how does the BlocProvider.of know how to return the correct instance of the Blocs? Because we need to register them in the hierarchy using BlocProvider. In the main.dart file when creating the tabs we wrap the widgets in BlocProvider widgets:

class FlickrMainPage extends StatefulWidget {
  FlickrMainPage({Key key}) : super(key: key);

  @override
  _FlickrMainPageState createState() => _FlickrMainPageState();
}

class _FlickrMainPageState extends State<FlickrMainPage> {

  final _flickrBloc = FlickrInterestingBloc(
    flickrRepository: FlickrRepository(
      flickrApi: FlickrApi()
      )
    );

  final _flickrFavoritesBloc = FlickrFavoritesBloc();

  int resultsCount = 0;
  int tabIndex = 0;
  String appBarTitle = "Flutter Flickr";

  @override
  void initState() {
    super.initState();

    _flickrBloc.refreshPhotos();
    _flickrFavoritesBloc.flickrFavoritesProvider.readAllFavorites();
  }

  @override
  Widget build(BuildContext context) {

    List<Widget> _children = createTabs();

    return Scaffold(
      appBar: AppBar(
          title: Text(appBarTitle)
      ),
      body: _children[tabIndex],
      bottomNavigationBar: BottomNavigationBar(
        onTap: onTabTapped,
        currentIndex: tabIndex,
        items: [
          BottomNavigationBarItem(
            icon: Icon(Icons.home),
            title: Text('Home')
          ),
          BottomNavigationBarItem(
              icon: Icon(Icons.favorite),
              title: Text('Favorites')
          )
        ]),
    );
  }

  List<Widget> createTabs() {

    return [

      // Provide the FlickrFavoritesBloc and FlickrInterestingBloc to the ImageListWidget
      BlocProvider(
        builder: (BuildContext context) => _flickrFavoritesBloc,
        child: BlocProvider(
          builder: (BuildContext context) => _flickrBloc,
          child: ImageListWidget()
        )
      ),
      BlocProvider(
        builder: (BuildContext context) => _flickrFavoritesBloc,
        child: FlickrFavoritesWidget(),
        dispose: false,
      )
    ];
  }

  void onTabTapped(int index) {
    setState(() {
      tabIndex = index;
      if (index == 0) {
        appBarTitle = "Flickr Interesting";
      } else {
        appBarTitle = "Favourites";
      }
    });
  }

  @override
  void dispose() {

    _flickrBloc.dispose();
    _flickrFavoritesBloc.dispose();

    super.dispose();
  }
}

It’s also important to call dispose() on all blocs to ensure that the underlying StreamControllers are cleaned up correctly and don’t cause memory leaks.

I haven’t shown the FlickrFavoritesWidget and other widgets but the refactoring is the same – they use the same BlocBuilder and BlocProvider with the BuildContext to work with the FlickrFavoritesBloc. The full source for this is available here:

https://bitbucket.org/bitsbobsetc/fluttersample/src/BlocRefactor/

Some further references:

Leave a comment