Starter Architecture for Flutter & Firebase Apps
Tags: flutter, dart, firebase, architecture
Published: 2020-02-10 09:26:10

---
published: true
title:  Starter Architecture for Flutter & Firebase Apps
description: In this tutorial I give a detailed overview of a production-ready architecture that I've fine-tuned over the last two years. You can use the included starter project as the foundation for your Flutter & Firebase apps.
cover_image: https://dev-to-uploads.s3.amazonaws.com/i/bhlo8unavxi93vhttiu3.png
tags: flutter, dart, firebase, architecture
canonical_url: https://codewithandrea.com/videos/starter-architecture-flutter-firebase/
---

[This article was originally published on my website.](https://codewithandrea.com/videos/2020-02-10-starter-architecture-flutter-firebase/)

[Watch Video Tutorial on YouTube.](https://youtu.be/rMDRgXnBMq0)

In this tutorial I give a detailed overview of a production-ready architecture that I've fine-tuned over the last two years. You can use the included [starter project](https://github.com/bizz84/starter_architecture_flutter_firebase) as the foundation for your Flutter & Firebase apps.

<!--
## Table of contents

- [Motivation](#motivation)
- [Overview](#overview)
- [What is architecture?](#what-is-architecture)
  - [Composition](#composition)
  - [Example: Sign-in page](#example-sign-in-page)
  - [Good architecture](#good-architecture)
  - [Not-so-good architecture 😅](#not-so-good-architecture-%f0%9f%98%85)
- [Application layers](#application-layers)
- [Unidirectional data flow](#unidirectional-data-flow)
- [Mutable and immutable state](#mutable-and-immutable-state)
- [Stream-based architecture](#stream-based-architecture)
- [Desirable code properties](#desirable-code-properties)
  - [Clear](#clear)
  - [Reusable](#reusable)
  - [Scalable](#scalable)
  - [Testable](#testable)
  - [Performant](#performant)
  - [Maintainable](#maintainable)
- [Conclusion](#conclusion)
-->

## Motivation

Flutter & Firebase are a great combo for getting apps to market in record time.

Without a sound architecture, codebases can quickly become hard to test, maintain, and **reason about**. This **severely** impacts the development speed, and results in buggy products, sad developers and unhappy users.

I have already witnessed this first-hand with various client projects, where the lack of a formal architecture led to days, weeks - even **months** of extra work.

Is "architecture" hard? How can one find the "right" or "correct" architecture in the ever-changing landscape of front-end development?

Every app has different requirements, so does the "right" architecture even exist in the first place?

While I don't claim to have a silver bullet, I have refined and fine-tuned a **production-ready** architecture that I have already used in multiple Flutter & Firebase apps. 

We will explore this and see how it's used in practice in the time tracker application included with the [starter project](https://github.com/bizz84/starter_architecture_flutter_firebase):

![Screenshots for the time tracker app](https://dev-to-uploads.s3.amazonaws.com/i/vvuizqd8rvunh86rshr6.png)

So grab a drink & sit comfortably. And let's dive in!

## Overview

We will start with an overview:

- what is architecture and why we need it.
- the importance of **composition** in good architecture.
- good things that happen when you **do** have a good architecture.
- bad things that happen when you **don't** have a good architecture.

Then we will focus on good architecture for Flutter & Firebase apps, and talk about:

- application layers
- unidirectional data flow
- mutable and immutable state
- stream-based architecture

I will explain some important principles, and desirable properties that we want in our code.

And we will see how everything fits together with some practical examples.

What you read here **is the result of over two years of my own work**, learning concepts, writing code, and refining it across multiple personal and client projects.

Ready? Let's go! 🚀

## What is architecture?

I like to think of architecture as the **foundation** that holds everything together, and supports your codebase as it grows.

If you have a good foundation, it becomes easier to make changes and add new things.

Architecture uses **design patterns** to solve problems efficiently.

And you **have to choose** the design patterns that are **most appropriate** for the problem that **you're trying to solve**.

For example, an e-commerce application and a chat app will have very different requirements.

### Composition

Regardless of what you're trying to build, it's likely that you'll have a set of problems, and you need to break them up into smaller, more manageable ones. 

You can create basic building blocks for each problem, and you can build your app by **composing** blocks together. In fact:

> **Composition** is a fundamental principle that is used extensively in Flutter, and more widely in software development. 

Since we're here to build Flutter apps, what kind of building blocks do we need?

### Example: Sign-in page

Let's say that you're building a page for email & password sign-in. 

You will need some input fields and a button, and you need to compose these inputs together to make a form.

But the form by itself doesn't really do much.

You will also need to talk to an authentication service. The code for that is **very different** from your UI code.

To build this feature, you'll need code for UI, input validation and authentication:

![UI, login and API components](https://dev-to-uploads.s3.amazonaws.com/i/1rhngjm0abpqq80msn5j.png)

### Good architecture

The sign-in page above has a **good** architecture if it's made with **well-defined building blocks** (or components) that we can **compose** together.

We can take this same approach and scale it up to the entire application. This has some very clear benefits:

- **Adding new features becomes easier**, because you can build upon the foundation that you already have.
- **The codebase becomes easier to understand**, and you're likely to spot some recurring patterns and conventions as you read the code.
- Components have **clear responsibilities** and **don't do too many things**. This happens **by design** if your architecture is highly composable.
- Entire classes of problems go away (more on this later).
- You can have different **kinds** of components, by defining separate application layers for different areas of concern (UI, logic, services).

### Not-so-good architecture 😅

If we fail to define a good architecture, we don't have clear conventions for how to structure our app.

The lack of composable components leads to code that has a lot of dependencies.

This kind of code is **hard to understand**. Adding new features becomes problematic, and it's not even clear where new code should go.

Some other potential issues are also common:

- the app has a lot of mutable state, making it hard to know which widgets rebuild and when.
- it's not clear when certain variables can or cannot be `null`, as they are passed across multiple widgets.

All these issues can significantly slow down development, and negate the productivity advantages that are common in Flutter.

> Bottom line: good architecture is important.

## Application layers

Here's a diagram that shows my architecture for Flutter & Firebase apps:

![The application layers](https://dev-to-uploads.s3.amazonaws.com/i/9s78qwnyzztsnd8161f7.png)

The dotted horizontal lines define some clear **application layers**.

I think it's a good idea to always think about them. When you write new code, you should ask yourself: *where does this belong*?

----

Example: if you're writing some UI code for a new feature, you're likely to be inside a widget class. Maybe you need to call some external web-service API when a button is pressed. In this case, you need to stop and think: *where does my API code go*?

```dart
@override
Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(
      title: Text('New Job'),
      actions: [
        FlatButton(
          child: Text('Save'),
          onPressed: () {
            // web API call here. Where should this code go?
          },
        ),
      ],
    ),
    body: _buildContents(),
  );
}
```

Thinking in terms of application layers is really helpful here.

This boils down to the **single responsibility principle**: each component in your app should do **one thing only**.

UI and networking code are two completely different things. They should not belong together, and they live in very different places.

## Unidirectional data flow

In the diagram above, data flows from the outside world, into the services, view models, and all the way down to the widgets.

The call flow goes into the opposite direction. Widgets may call methods inside the view models and services. In turn, these may call APIs inside external Dart packages.

**Very important**: components that live on a certain application layer **do not know** about the existence of components in the layers **below**.

View models do **not** reference any widgets objects (or import any UI code for that matter). Instead:

> Widgets **subscribe** themselves as listeners, while view models **publish** updates when something changes.

![Publish-subscribe pattern](https://dev-to-uploads.s3.amazonaws.com/i/a9eie5g8y74qw8pbryna.png)

This is known as the **publish/subscribe** pattern, and it has various implementations in Flutter. You have already used this if you have **ChangeNotifiers** or **BLoCs** in your apps.

----

To connect everything together, we can use the [`Provider`](https://pub.dev/packages/provider) package. 
In fairness, it can take some time to get familiar with all the [different kinds](https://pub.dev/packages/provider#existing-providers) of providers. But we can use them to enforce an **unidirectional** data flow with **immutable** model classes. This has various benefits, which are discussed below.

> To learn more about Provider and how to use it in practice, you can watch my [video series on YouTube](https://www.youtube.com/watch?v=O71rYKcxUgA&list=PLNnAcB93JKV-IarNvMKJv85nmr5nyZis8).

For reference, here is a simplified diagram of the widget tree for the application included in the [starter project](https://github.com/bizz84/starter_architecture_flutter_firebase):

![Time tracker widget tree](https://dev-to-uploads.s3.amazonaws.com/i/trhb9prpgshzsssgc6qs.png).

This should give you a high-level understanding of how Provider is used in this project.

And it uses [`MultiProvider`](https://pub.dev/packages/provider#multiprovider) to group together multiple services and values. For more advanced use cases, you can use [`ProxyProvider`](https://pub.dev/packages/provider#proxyprovider).

## Mutable and immutable state

One important aspect of this architecture lies in the differences between services and view models. In particular:

- View models can hold and modify state.
- Services can't.

In other words, we can think of services as **pure**, functional components. 

Services can transform the data they receive from external Dart packages, and make it available to the rest of the app via **domain-specific** APIs.

For example, when working with Firestore we can use a wrapper service to do serialization:

- **Data in (read)**: This transforms streams of key-value pairs from Firestore documents into strongly-typed immutable data models.
- **Data out (write)**: This converts data models back to key-value pairs for writing to Firestore.

On the other hand, view models contain the business logic for your application, and are likely to hold mutable state.

This is ok, because widgets can be notified of state changes and rebuild themselves, according to the publish/subscribe pattern described above.

> By combining the **uni-directional** data flow with the **publish/subscribe** pattern, we can **minimise** mutable application state, along with the problems that often come with it.

## Stream-based architecture

Unlike traditional REST APIs, with Firebase we can build **realtime** apps.

That's because Firebase can **push** updates directly to **subscribed** clients when something changes.

For example, widgets can **rebuild** themselves when certain Firestore *documents* or *collections* are updated.

Many Firebase APIs are **inherently** stream-based. As a result, the **simplest** way of making our widgets reactive is to use [`StreamBuilder`](https://api.flutter.dev/flutter/widgets/StreamBuilder-class.html) (or [`StreamProvider`](https://pub.dev/documentation/provider/latest/provider/StreamProvider-class.html)).

Yes, you could use [`ChangeNotifier`](https://api.flutter.dev/flutter/foundation/ChangeNotifier-class.html) or other state management techniques that implement observables/listeners. 

But you would need additional "glue" code if you wanted to "convert" your input streams into reactive models based on `ChangeNotifier`.

> Note: streams are the default way of pushing changes not only with Firebase, but with many other services as well. For example, you can get location updates with the `onLocationChanged()` stream of the [location](https://pub.dev/packages/location) package. Whether you use Firestore, or want to get data from your device's input sensors, streams are the most convenient way of delivering **asynchronous** data over time.

---

In summary, this architecture defines **separate application layers** with an **unidirectional data flow**. Data is read from Firebase via **streams**, and widgets are rebuilt according to the **publish/subscribe** pattern.

## Desirable code properties

When used correctly, this architecture leads to code that is:

- clear
- reusable
- scalable
- testable
- performant
- maintainable

Let's look at each point with some examples:

### Clear

Suppose we want to build a page that shows a list of jobs.

Here is how I have implemented this in my project (explanation below):

```dart
class JobsPage extends StatelessWidget {
  Future<void> _delete(BuildContext context, Job job) async {
    try {
      final database = Provider.of<FirestoreDatabase>(context, listen: false);
      // database call
      await database.deleteJob(job);
    } on PlatformException catch (e) {
      PlatformExceptionAlertDialog(
        title: 'Operation failed',
        exception: e,
      ).show(context);
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(Strings.jobs),
        actions: <Widget>[
          IconButton(
            icon: Icon(Icons.add, color: Colors.white),
            // navigation call
            onPressed: () => EditJobPage.show(context),
          ),
        ],
      ),
      body: _buildContents(context),
    );
  }

  Widget _buildContents(BuildContext context) {
    final database = Provider.of<FirestoreDatabase>(context, listen: false);
    // Read jobsStream from Firestore & build UI when updated
    return StreamBuilder<List<Job>>(
      stream: database.jobsStream(),
      builder: (context, snapshot) {
        // Generic widget for showing a list of items
        return ListItemsBuilder<Job>(
          snapshot: snapshot,
          itemBuilder: (context, job) => Dismissible(
            key: Key('job-${job.id}'),
            background: Container(color: Colors.red),
            direction: DismissDirection.endToStart,
            // database call
            onDismissed: (direction) => _delete(context, job),
            child: JobListTile(
              job: job,
              // navigation call
              onTap: () => JobEntriesPage.show(context, job),
            ),
          ),
        );
      },
    );
  }
}
```

The `build()` method returns a scaffold with an `AppBar`.

The `_buildContents()` method returns a `StreamBuilder`, that is used to read some data as a stream from Firestore.

Inside it, we can pass the snapshot to a `ListItemsBuilder`, which is a **generic** widget (that I created) for showing a list of items.

In just 50 lines, this widget shows a list of items, and handles three different callbacks for:

- creating a new job
- deleting an existing job
- routing to a job details page

Each of these operations only requires one line of code, because it **delegates** the actual work to external classes.

As a result this code is clear and readable.

It would be much harder to make sense of everything if we had database code, serialization, routing and UI all in one class.
And our code would also be less reusable as a result.

### Reusable

Here's the code for a different page, which shows a daily breakdown of all the jobs along with the pay:

```dart
class EntriesPage extends StatelessWidget {
  static Widget create(BuildContext context) {
    final database = Provider.of<FirestoreDatabase>(context, listen: false);
    return Provider<EntriesViewModel>(
      create: (_) => EntriesViewModel(database: database),
      child: EntriesPage(),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(Strings.entries),
        elevation: 2.0,
      ),
      body: _buildContents(context),
    );
  }

  Widget _buildContents(BuildContext context) {
    final vm = Provider.of<EntriesViewModel>(context);
    return StreamBuilder<List<EntriesListTileModel>>(
      stream: vm.entriesTileModelStream,
      builder: (context, snapshot) {
        return ListItemsBuilder<EntriesListTileModel>(
          snapshot: snapshot,
          itemBuilder: (context, model) => EntriesListTile(model: model),
        );
      },
    );
  }
}
```

Note how I'm reusing some of the same components that I had in the previous page.

I use my `ListItemsBuilder` widget again, this time with a different model type (`EntriesListTileModel`).

And this time, the `StreamBuilder`'s input stream comes from an `EntriesViewModel`.

But the data flows into the UI in the same way as before.

> Bottom line: it pays off to build **reusable components** that can be used in multiple places.

### Scalable

Let's talk about scalable code. If you have implemented Firestore CRUD operations before, you're probably familiar with this kind of syntax:

```dart
final ref = Firestore.instance.collection('users').document(uid).collection('jobs');
final snapshots = ref.snapshots();
// TODO: Manipulate snapshots stream and read documents' data
```

This can get quite unwieldly, especially if your documents have a lot of key-value pairs.

You don't want to have code like this inside your widgets, or even in your view models.

Rather, you can define a **domain-specific** Firestore API using some service classes, and keep things tidy.

Here is a `FirestorePath` class that I have created to list all possible read/write locations in my Firestore database:

```dart
class FirestorePath {
  static String job(String uid, String jobId) => 'users/$uid/jobs/$jobId';
  static String jobs(String uid) => 'users/$uid/jobs';
  static String entry(String uid, String entryId) =>
      'users/$uid/entries/$entryId';
  static String entries(String uid) => 'users/$uid/entries';
}
```

Alongside this, I have a `FirestoreDatabase` class, which I use to provide access to the various documents and collections.

```dart
class FirestoreDatabase {
  FirestoreDatabase({@required this.uid}) : assert(uid != null);
  final String uid;

  // CRUD operations - implementations omitted for simplicity
  Future<void> setJob(Job job) { ... }
  Future<void> deleteJob(Job job) { ... }
  Stream<Job> jobStream({@required String jobId}) { ... }
  Stream<List<Job>> jobsStream() { ... }
  Future<void> setEntry(Entry entry) { ... }
  Future<void> deleteEntry(Entry entry) { ... }
  Stream<List<Entry>> entriesStream({Job job}) { ... }
}
```

This class exposes all the various CRUD operations to the rest of the app, behind a nice API that uses strongly-typed model classes. 

With this setup, adding a new type of document or collection in Firestore becomes a **repeatable** process:

- add some additional paths to `FirestorePath`
- add the corresponding `Future` and `Stream` based APIs to `FirestoreDatabase` to support the various operations
- create strongly-typed model classes as needed. These include the serialization code for the new kind of documents that I need to use.

All of this code remains confined inside a `services` folder in my project.

Widgets can use the new database APIs with `Provider`:

```dart
final database = Provider.of<FirestoreDatabase>(context, listen: false);
// TODO: call database APIs as needed
```

The code above is easily scalable. I can add new functionality by following repeatable steps, and ensure that the code is consistent. This is very valuable if you work in a team. 

### Testable

This architecture leads to testable code.

This is true for my unit tests, because my classes are small and have few dependencies.

But it also applies to widget tests as well, because all the widgets have **scoped access** to the dependencies that they need.

And thanks to the [`Provider`](https://pub.dev/packages/provider) package, it is easy to swap out services classes with mock objects and run tests against them:

```dart
void main() {
  MockAuthService mockAuthService;
  MockDatabase mockDatabase;

  setUp(() {
    mockAuthService = MockAuthService();
    mockDatabase = MockDatabase();
  });

  Future<void> pumpAuthWidget(
      WidgetTester tester,
      {@required
          Widget Function(BuildContext, AsyncSnapshot<User>) builder}) async {
    await tester.pumpWidget(
      Provider<FirebaseAuthService>(
        create: (_) => mockAuthService,
        child: AuthWidgetBuilder(
          databaseBuilder: (_, uid) => mockDatabase,
          builder: builder,
        ),
      ),
    );
    await tester.pump(Duration.zero);
  }

  // TODO: Widget tests here
}
```

This leads to widget tests are fast and predictable, because they don't call any networking code.

With the right setup it's even possible to make the **entire** app testable, as long as we can swap out any services with mock objects **at the root of the widget tree**.

This is particularly useful when running integration tests, that can be used to test entire **user flows** in the app.

### Performant

One great thing about this architecture is that it **minimises widget rebuids**.

This is accomplished using `Provider` and `StreamBuilder`/`FutureBuilder` as needed.

How?

Well, once we read some state or data **asynchronously** from an external service, we can make that available **synchronously** to all **descendant** widgets.

For example, this app requires the user to sign-in with Firebase. This code returns either the `HomePage` or the `SignInPage` depending on the authentication status of the user:

```dart
@override
Widget build(BuildContext context) {
  final authService =
      Provider.of<FirebaseAuthService>(context, listen: false);
  return StreamBuilder<User>(
    // asynchronous data in
    stream: authService.onAuthStateChanged,
    builder: (BuildContext context, AsyncSnapshot<User> snapshot) {
      final User user = snapshot.data;
      if (user != null) {
        // make data available synchronously to all descendants
        return MultiProvider(
          providers: [
            Provider<User>.value(value: user),
            Provider<FirestoreDatabase>(
              create: (_) => FirestoreDatabase(user.uid),
            ),
          ],
          // HomePage and all descendant widgets can get the current user with `Provider.of<User>(context)`,
          // rather than `await FirebaseAuth.instance.currentUser()`
          child: HomePage(),
        );
      }
      return SignInPage();
    },
  );
}
```

Once a non-null `User` object is extracted from the snapshot, it can be made available **synchronously** to all descendants:

```dart
Widget build(BuildContext context) {
  final user = Provider.of<User>(context);
  // show UI
}
```

The code above is **much** better than this:

```dart
Widget build(BuildContext context) {
  // so much boilerplate code 😅
  return FutureBuilder<FirebaseUser>(
    future: FirebaseAuth.instance.currentUser(),
    builder: (context, snapshot) {
      if (snapshot.data != null) {
        // show UI
      }
      return CircularProgressIndicator();
    },
  );
}
```

Here we create a `FutureBuilder`, just so that we can get the current user **asynchronously**. This is unnecessary, because we have already obtained it in one of the **ancestor** widgets. And we have to write more boilerplate code in the builder, and show some loading indicator until this `Future` returns.

Instead, `Provider` can solve this problem for us, and we should use it to our advantage.
 
> Bottom line: we can use `Provider` to minimise widget rebuilds, avoid any unnecessary API calls to Firebase, and reduce boilerplate code.

### Maintainable

This architecture leads to maintainable code, and the examples above should serve as evidence.

Maintainable code will save you (and your team) days, weeks and months of extra effort.

Beyond that, your code will be much nicer to work with, and you'll sleep better at night. 😴

## Conclusion

I hope that this overview has inspired you to invest in good architecture.

If you're starting a new project, consider planning out your architecture upfront, **based on your requirements**.

If you're struggling with a codebase that doesn't follow good software design principles, start refactoring in small iterations. You don't have to fix everything at once, but it helps to move slowly towards your desired architecture.

And if you are building a project with Flutter and Firebase or any other kind of streaming APIs, do check out my [starter project](https://github.com/bizz84/starter_architecture_flutter_firebase) on GitHub. This is a complete time tracking application:

![Screenshots for the time tracker app](https://dev-to-uploads.s3.amazonaws.com/i/ll2do8felj9sh9usupyn.png)

The README is a good place to get more familiar with all the concepts that we covered.

After that, you can take a look at the source code, run the project (note: [Firebase configuration needed](https://github.com/bizz84/starter_architecture_flutter_firebase#running-the-project-with-firebase)), and get a good understanding of how everything fits together.

And if you want to learn all these principles more in depth, and build the time-tracking application from scratch, then there's no better place than my [Flutter & Firebase course](https://codewithandrea.com/courses/flutter-firebase/).

With over 20 hours of content, it covers everything that you need to know, from the basics of the Dart language, all the way to more advanced topics.

---

Thank you very much for following this tutorial. If you end up using this architecture, I would love to hear your feedback.

Happy coding!