App Architecture

One of the goals of the pet project is to get familiar with Flutter architecture approaches. Unfortunately, there are no official recommendations from the Flutter team.

It seems that the most popular pattern is something called BLoc. BLoC has been developed at Google and stands for Business Logic Component.

BLoC is:

The fact that BLoC has no references to the view in any and instead exposes the data for the view to use should ring the bell for Android developers. In this aspect it is similar to the Android ViewModel.

The library by the pattern authors provides a lot of useful stuff besides the basic BLoC class. There are BLoC builders, BLoC provider, something that looks like state reducing and more. While it may sound tempting to use the library I am going to implement the pattern myself. This way I will fully experience all the pros and cons.

I started with the basic template of the BLoc to see what it looks like.

abstract class Bloc {
  void dispose();
}

Since BLoCs are reactive we need a mechanism to cancel the stream subscription after the BLoC is no longer in use. Note for Android developers: this is something like onCleared method of ViewModel.

Time to create the first screen of the app and the first BLoC for it. The screen will display a list of items fetched from the Firebase Cloudstore.

This BLoC will be quite trivial. Its job is to expose the stream of questions list.

class QuestionListBloc extends Bloc {
  Stream<List<Question>> get questions => Firestore.instance
      .collection("test_questions")
      .snapshots()
      .map((snapshot) => snapshot.documents
          .map(
              (doc) => Question(doc.documentID, doc.data["question"] as String))
          .toList());

  @override
  void dispose() {}
}

Now is the time to somehow use the BLoC to get the items to the widgets. I need some kind of generic solution to provide the BLoCs. The simplest way is to create a stateful widget that will host the bloc. This widget (it will be called BlocProvider, obviously) will live higher in the widget hierarchy than the widget that needs the bloc. The child widget will look up the provider via findAncestorWidgetOfExactType method of BuildContext and access the bloc.

Other widgets that are down the tree from the BlocProvider can also look up and get the BLoC by its type. This allows us to create and provide BLoCs of different granularity: BLoCs that are needed only by a single widget and BLoCs that manage some wider logic and state, even app-wide BLoCs.

class BlocProvider<T extends Bloc> extends StatefulWidget {
  final Widget child;
  final T bloc;

  const BlocProvider({Key key, @required this.bloc, @required this.child})
      : super(key: key);

  static T of<T extends Bloc>(BuildContext context) {
    final BlocProvider<T> provider = context.findAncestorWidgetOfExactType();
    return provider.bloc;
  }

  @override
  State createState() => _BlocProviderState();
}

class _BlocProviderState extends State<BlocProvider> {
  @override
  Widget build(BuildContext context) => widget.child;

  @override
  void dispose() {
    widget.bloc.dispose();
    super.dispose();
  }
}

Now let’s wrap the QuestionListWidget with BlocProvider so it can access the QuestionListBloc.

class MyHomePage extends StatelessWidget {
  MyHomePage({Key key, this.title}) : super(key: key);

  final String title;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(title),
      ),
      body: Center(
        child: BlocProvider<QuestionListBloc>(
            bloc: QuestionListBloc(),
            child: QuestionListWidget()),
      ),
    );
  }
}

Getting access to the bloc inside the QuestionListWidget is now a piece of cake:

BlocProvider.of<QuestionListBloc>(context)

Looks a bit like magic. Let’s find out how it works. Flutter is based on a very simple idea: your app is just a tree of widgets. The widget BlocProvider<QuestionListBloc> is a parent or an ancestor of the widget QuestionListWidget. Using the BuildContext (a class that describes the context in which the widget is built and its position in the tree) when building QuestionListWidget I can look up the ancestor that has the the type of BlocProvider<QuestionListBloc>. BuildContext has the method exactly for that: T findAncestorWidgetOfExactType<T extends Widget>();. How cool is that Dart does not erase the type parameters in runtime. Right, Java?

Now I have one piece left: using the stream that emits lists of questions to actually display something on the screen. Turns out, Flutter has a widget (of course) exactly for that. It is called StreamBuilder and using it is very easy:

class QuestionListWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return StreamBuilder<List<Question>>(
      stream: BlocProvider.of<QuestionListBloc>(context).questions,
      builder: (context,  AsyncSnapshot<List<Question>> snapshot) {
        if (snapshot.hasData) {
          return _questionList(snapshot.data);
        }
        if (snapshot.hasError) {
          throw Exception(snapshot.error);
        }

        switch (snapshot.connectionState) {
          case ConnectionState.none:
            return const Text("None.");
          case ConnectionState.waiting:
            return const Text("Waiting.");
          default:
            throw Exception(
                "Unexpected connectionState: ${snapshot.connectionState}");
        }
      },
    );
  }
}

What it does is it subscribes to the provided stream and calls the builder method each time something happens: a new item has been emitted, the stream has failed or it has completed. My job is to return a proper widget depending on the data in AsyncSnapshot.

I have omitted some details like creating lists and rows because it would bloat the post and is quite trivial. The setup of Cloudstore and why I chose it will be covered in another post.

You can find the whole source code of the project in the Github repo here: https://github.com/amatkovsky/CommunicateThis.

The following articles and resources have been an amazing help while getting started with Flutter architecture and the BLoC patter. Give them a read: