just a flutter project called working_project that projects the project on the working.

Overview

Flutter & Firebase Realtime Apps

This is a Shipper app that can be used as a shipper hooker using Flutter & Firebase.

Go drawsql.app/c-5/diagrams/working-project to see the database structure of this project.

Disclaimer

Flutter & Firebase are hard af, to be fair

I call this "working_project which using Flutter & Firebase Realtime Apps" because i couldnt think of a good name.

Stream-based Architecture for Flutter & Firebase Realtime Apps

Two words are key here: Stream and Realtime.

Unlike with traditional REST APIs, using Firebase this app is realtime.

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 StreamProvider from the Riverpod package. This provides a convenient way of watching changes in your Firebase streams, and automatically rebuilding widgets with minimal boilerplate code.

Yes, you could use ChangeNotifier or other state management techniques that implement observables/listeners. But you would need additional "glue" code if you want 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 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.

A more detailed overview of this architecture is outlined below. But first, here are the goals for this project.

Project Goals

Define a reference architecture that can be used as the foundation for Flutter apps using Firebase (or other streaming APIs).

This architecture should:

  • minimize mutable state by adopting an unidirectional data flow
  • clearly define application layers and their boundaries
  • require little boilerplate code

The resulting code should be:

  • clear
  • reusable
  • scalable
  • testable
  • performant
  • maintainable

These are all nice properties, but how do they all fit together in practice?

By introducing application layers with clear boundaries, and defining how the data flows through them.

The Application Layers

To ensure a good separation of concerns, this architecture defines three main application layers.

  • UI Layer: where the widgets live
  • Logic & Presentation Layer: this contains the application's business and presentation logic
  • Domain Layer: this contains domain-specific services for interacting with 3rd party APIs

These layers may be named differently in other literature.

What matters here is that the data flows from the services into the widgets, and the call flow goes in the opposite direction.

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


The publish/subscribe pattern comes in many variants (e.g. ChangeNotifier, BLoC), and this architecture does not prescribe which one to use.

By using Riverpod, we can easily use the most convenient pattern on a case-by-case bsis. In practice, this means using Streams with StreamProvider when reading and manipulating data from Firestore. But when dealing with local application state, StatefulWidget+setState or ChangeNotifier are sometimes used.

Let's look at the three application layers in more detail.

Domain Layer: Services

Services are pure, functional components that don't hold any state.

Services serve as an abstraction from external data sources, and provide domain-specific APIs to the rest of the app (more on this below).

Because service APIs return strongly-typed, immutable, domain-specific model objects, the rest of the app doesn't directly manipulate the raw data from the outside world (e.g. Firestore documents represented as key-value pairs).

As a bonus, breaking changes in external packages are easier to deal with, because they only affect the corresponding service classes.

Presentation & Logic Layer: View Models

View models abstract the widgets' state and presentation.

View models do not have any reference to the widgets themselves. Rather, they define an interface for publishing updates when something changes.

View models can talk directly to service classes to read or write data, and access other domain-specific APIs.

But unlike service classes, they can hold and modify state, according to some business logic.

View models can also be used to hold local state. This is common when converting a StatefulWidget into a StatelessWidget

NOTE: View models are completely independent from the UI. View model classes never import Flutter code (e.g. material.dart)

UI Layer: Widgets

Widgets are used to specify how the application UI looks like, and provide callbacks in response to user interaction.

Strictly speaking, we can introduce a distinction:

  • pure UI widgets: these are the usual buttons, texts, containers
  • logic or presentational widgets: these are used to decide what widget to return, based on some condition (e.g. to return the home page or sign page based on the authentication status of the user).

This project contains a demo app as a practical implementation of this architecture.

Demo App: maybe i should rename it something meaningfull

Tables:

Main screens:

Riverpod

Riverpod is a rewrite of the popular Provider package, and improves on its weaknesses. It is a natural fit for this app.

Creating Providers with Riverpod

For example, here are some providers that are created using Riverpod:

// 1
final firebaseAuthProvider =
    Provider<FirebaseAuth>((ref) => FirebaseAuth.instance);

// 2
final authStateChangesProvider = StreamProvider<User>(
    (ref) => ref.watch(firebaseAuthProvider).authStateChanges());

// 3
final databaseProvider = Provider<FirestoreDatabase?>((ref) {
  final auth = ref.watch(authStateChangesProvider);

  // we only have a valid DB if the user is signed in
  if (auth.data?.value?.uid != null) {
    return FirestoreDatabase(uid: auth.data!.value!.uid);
  }
  return null;
});

As we can see, authStateChangesProvider depends on firebaseAuthProvider, and can get access to it using ref.watch.

Similarly, databaseProvider depends on authStateChangesProvider.

One powerful feature of Riverpod is that we can watch a provider's value and rebuild all dependent providers and widgets when the value changes.

An example of this is the databaseProvider above. This provider's value is rebuilt every time the authStateChangesProvider's value changes. This is used to return either a FirestoreDatabase object or null depending on the authentication state.

Using Riverpod inside widgets

Widgets can access these providers with a ScopedReader, either via Consumer or ConsumerWidget.

For example, here is some sample code demonstrating how to use StreamProvider to read some data from a stream:

final jobStreamProvider =
    StreamProvider.autoDispose.family<Job, String>((ref, jobId) {
  final database = ref.watch(databaseProvider)!;
  return database.jobStream(jobId: jobId);
});

In this case the StreamProvider can auto-dispose itself when all its listeners unsubscribe. And we're using .family to read a jobId parameter that is only known at runtime.

Here's a widget that watches this StreamProvider and uses it to show some UI based on the stream's latest state (data available / loading / error):

class JobEntriesAppBarTitle extends ConsumerWidget {
  const JobEntriesAppBarTitle({required this.job});
  final Job job;

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    // 1: watch changes in the stream
    final jobAsyncValue = ref.watch(jobStreamProvider(job.id));
    // 2: return the correct widget depending on the stream value
    return jobAsyncValue.when(
      data: (job) => Text(job.name),
      loading: () => Container(),
      error: (_, __) => Container(),
    );
  }
}

This widget class is as simple as it can be, as it only needs to watch for changes in the stream (step 1), and return the correct widget depending on the stream value (step 2).

This is great because all the logic for setting up the StreamProvider lives inside the provider itself, and is completely separate from the UI code.


In addition to the top-level providers and the StreamProviders that read data from Firestore, Riverpod is also used to create and configure view models for widgets that require local state.

These view models can hold any app-specific business logic, and if they're based on ChangeNotifier or StateNotifier, they can be easily hooked up to their widgets with corresponding providers. See the SignInViewModel and SignInPage widget for an example of this.

Project structure

Folders are grouped by feature/page. Each feature may define its own models and view models.

Services and routing classes are defined at the root, along with constants and common widgets shared by multiple features.

/lib
  /app
    /home
      /account
      /entries
      /job_entries
      /jobs
      /models
    /sign_in
  /common_widgets
  /constants
  /routing
  /services

This is an arbitrary structure. Choose what works best for your project.

Use Case: Firestore Service

Widgets can subscribe to updates from Firestore data via streams. Equally, write operations can be issued with Future-based APIs.

Here's the entire Database API for the demo app, showing all the supported CRUD operations:

class FirestoreDatabase { // implementation omitted for brevity
  Future<void> setJob(Job job); // create / update
  Future<void> deleteJob(Job job); // delete
  Stream<List<Job>> jobsStream(); // read
  Stream<Job> jobStream({required String jobId}); // read

  Future<void> setEntry(Entry entry); // create / update
  Future<void> deleteEntry(Entry entry); // delete
  Stream<List<Entry>> entriesStream({Job job}); // read
}

As shown above, widgets can read these input streams via StreamProviders, and use a watch to reactively rebuild the UI.

For convenience, all available collections and documents are listed in a single class:

class APIPath {
  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';
}

Domain-level model classes are defined, along with fromMap() and toMap() methods for serialization. These classes are strongly-typed and immutable.

See the FirestoreDatabase and FirestoreService classes for a full picture of how everything fits together.

Routing

The app uses named routes, which are defined in a Routes class:

class AppRoutes {
  static const emailPasswordSignInPage = '/email-password-sign-in-page';
  static const editJobPage = '/edit-job-page';
  static const entryPage = '/entry-page';
}

A AppRouter is then used to generate all the routes with a switch statement:

class AppRouter {
  static Route<dynamic> onGenerateRoute(RouteSettings settings) {
    final args = settings.arguments;
    switch (settings.name) {
      // all cases here
    }
  }
}

Given a page that needs to be presented inside a route, we can call pushNamed with the name of the route, and pass all required arguments. If more than one argument is needed, we can use a map:

class EntryPage extends ConsumerStatefulWidget {
  const EntryPage({required this.job, this.entry});
  final Job job;
  final Entry? entry;

  static Future<void> show(
      {required BuildContext context, required Job job, Entry? entry}) async {
    await Navigator.of(context, rootNavigator: true).pushNamed(
      AppRoutes.entryPage,
      arguments: {
        'job': job,
        'entry': entry,
      },
    );
  }

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

Note: previously the app was using auto_route, which uses code generation to make routes strongly-typed. This has caused subtle issues, that took some time to investigate. So the project now uses manual routes, which are much more predictable.

Running the project with Firebase

To use this project with Firebase, some configuration steps are required.

  • Create a new project with the Firebase console.
  • Add iOS and Android apps in the Firebase project settings.
  • On Android, use com.example.starter_architecture_flutter_firebase as the package name.
  • then, download and copy google-services.json into android/app.
  • On iOS, use com.example.starterArchitectureFlutterFirebase as the bundle ID.
  • then, download and copy GoogleService-Info.plist into iOS/Runner, and add it to the Runner target in Xcode.
  • finally, enable the Email/Password Authentication Sign-in provider in the Firebase Console (Authentication > Sign-in method > Email/Password > Edit > Enable > Save)

See this page for full instructions:

Running on Flutter Web

This project now runs on Flutter web.

To test this, add a web app in the Firebase project settings, and export the generated firebaseConfig variable inside a ./firebase-config.js file in your project (this file is included in .gitignore). Example:

export var firebaseConfig = {
    apiKey: "<your-api-key>",
    authDomain: "<your-auth-domain>",
    databaseURL: "<your-database-url>",
    projectId: "<your-project-id>",
    storageBucket: "<your-storage-bucket>",
    messagingSenderId: "<your-messaging-sender-id>",
    appId: "<your-app-id>",
    measurementId: "<your-measurement-id>"
};

This is then imported in the index.html file:

  <script src="./firebase-config.js"></script>

  <!-- https://stackoverflow.com/questions/950087/how-do-i-include-a-javascript-file-in-another-javascript-file -->
  <script type="module">
    // Your web app's Firebase configuration
    import { firebaseConfig } from './firebase-config.js';
    // Initialize Firebase
    firebase.initializeApp(firebaseConfig);
    firebase.analytics();
  </script>

Packages

  • firebase_auth for authentication
  • cloud_firestore for the remote database
  • flutter_riverpod for dependency injection and propagating stream values down the widget tree
  • rxdart for combining multiple Firestore collections as needed
  • intl for currency, date, time formatting
  • mockito for testing
  • equatable to reduce boilerplate code in model classes

Also imported from my flutter_core_packages repo:

  • firestore_service
  • custom_buttons
  • alert_dialogs
  • email_password_sign_in_ui

References

This project borrows many ideas from my Flutter & Firebase Course, as well as my Reference Authentication Flow with Flutter & Firebase, and takes them to the next level by using Riverpod.

Here are some other GitHub projects that also attempt to formalize a good approach to Flutter development:

Other relevant articles about app architecture:

License: MIT

You might also like...

new 2021 working Instagram clone written in Flutter

new 2021 working Instagram clone written in Flutter

instagram_ui Instagram 2021 clone made using flutter Contains reels, search grid view, Getting Started This project is a starting point for a Flutter

Nov 6, 2022

Easy to use text widget for Flutter apps, which converts inlined urls into working, clickable links

Easy to use text widget for Flutter apps, which converts inlined urls into working, clickable links

LinkText Easy to use text widget for Flutter apps, which converts inlined URLs into clickable links. Allows custom styling. Usage LinkText widget does

Nov 4, 2022

Working Instagram Clone (Frontend + Backend) created with Flutter and Firebase

Working Instagram Clone (Frontend + Backend) created with Flutter and Firebase

Instagram_clone Instagram Clone (Both frontend and backend) created with Flutter and Firebase. Show some ❤️ and star the repo to support the project.

Dec 31, 2022

The app that i built during the IITM Session on "Working with APIs in Flutter"

api_test A new Flutter project. Getting Started This project is a starting point for a Flutter application. A few resources to get you started if this

Dec 18, 2021

A Flutter Accident reporting App working in both iOS and Android

Flutter Accident Reporting App A Flutter Accident reporting App working in both iOS and Android.This project total size of all Dart files is 4714 bite

Oct 13, 2022

Flutter library that provides useful functions for working with colors

Colors Stuff A library that provides useful functions for working with colors. T

Dec 27, 2021

A working Twitter clone written in Flutter using Firebase auth,realtime,firestore database and storage

A working Twitter clone written in Flutter using Firebase auth,realtime,firestore database and storage

A working Twitter clone written in Flutter using Firebase auth,realtime,firestore database and storage

Dec 24, 2022

Flutter package implements Sign Google redirect(working for incognito mode)

google_sign_in_web_redirect Flutter package implements Sign Google redirect(working for incognito mode). Usage Import the package dependencies: goog

Dec 15, 2022

A working Twitter clone built in Flutter using Firebase auth,realtime,firestore database and storage.

A working Twitter clone built in Flutter using Firebase auth,realtime,firestore database and storage.

Fwitter - Twitter clone in flutter A working Twitter clone built in Flutter using Firebase auth,realtime,firestore database and storage. Dependencies

Oct 5, 2022
Owner
DokuroGitHub
To see the whole world with my eyes closed.
DokuroGitHub
Flashcard and Kanji writing Flutter App. Stopped working on this Project. Currently just a graveyard.

KanjiMaru Not in development anymore for reasons I shall not state here. Still loved the design I created, so it will stay up. Flashcard and Japanese

Stefan Dourado 30 Jan 1, 2023
Here is an application project that displays a list of products from the Elevenia API called STORE.

Here is an application project that displays a list of products from the Elevenia API called STORE.

Dedi Kurniawan 2 May 18, 2022
An introduction to one of the famous flutter package called bloc

BLoC Tutorial - Udemy The full documentation on BLoC package is available in bloclibrary.dev. This video tutorial is also available on youtube. If you

Md. Siam 1 Mar 26, 2022
Return a Stream that emits null and done event when didChangeDependencies is called for the first time.

did_change_dependencies Author: Petrus Nguyễn Thái Học Return a Stream that emits null and done event when State.didChangeDependencies is called for t

Petrus Nguyễn Thái Học 5 Nov 9, 2022
State Persistence - Persist state across app launches. By default this library store state as a local JSON file called `data.json` in the applications data directory. Maintainer: @slightfoot

State Persistence Persist state across app launches. By default this library store state as a local JSON file called data.json in the applications dat

Flutter Community 70 Sep 28, 2022
A group of overlapping round avatars are called face piles, a face pile is a series of overlapping avatar images that come and go as users join and leave a given group.

Flutter Face Pile A group of overlapping round avatars are called face piles. A face pile is a series of overlapping avatar images that come and go as

Amine Chamkh 3 Sep 22, 2022
A smartphone application called Easy Job goal is to make easier for businesses to find people who meet their standards as well as for job seekers to search for and choose from available positions .

Easy_Jobs 19SW54(MAD-Project) A new Flutter project. Getting Started This project is a starting point for a Flutter application. A few resources to ge

Muskan 2 Nov 6, 2022
🌈 Repository for a compass project, basically an App for displaying bank transfers, with API requests, Flag persistence, Infinite Scroll, Error Handling, Unit Tests, Extract Sharing working with SOLID, BLoC and Designer Patterns.

?? Green Bank Aplicação desenvolvida em Flutter com intuito de trabalhar conexão com API, Gerenciamento de estado usando BLoC, Refatoração, Arquitetur

André Guerra Santos 28 Oct 7, 2022
Working proof of the Go (golang) server running inside Flutter

flap Working proof of the Go server running inside Flutter Video in action Prerequisites Flutter >2.0 Go >1.16 Build Go server cd go macOS: make maco

Err 28 Dec 17, 2022