Nimbostratus is a reactive data-fetching and client-side cache management library built on top of Cloud Firestore.

Overview

Nimbostratus ๐ŸŒฉ

Nimbostratus is a reactive data-fetching and client-side cache management library built on top of Cloud Firestore.

The Cloud Firestore client API for Flutter is great at fetching and streaming documents. Nimbostratus extends that API to include some additional features:

  1. APIs for reading, writing and subscribing to documents changes on the client using the Nimbostratus in-memory cache.
  2. New data fetching policies like cache-first and cache-and-server to implement common data fetching practices for responsive UIs.
  3. Support for optimistic updates through cache-write policies.

Usage

Reading documents ๐Ÿ“–

import 'package:nimbostratus/nimbostratus.dart';

final snap = await Nimbostratus.instance.getDocument(
  FirebaseFirestore.instance.collection('users').doc('alice'),
  fetchPolicy: GetFetchPolicy.cacheFirst,
);

In this example, we request to read a Firestore document from the cache first, falling back to the server if it is unavailable. There are a few handly fetch policies to choose from which you can look at here.

Streaming documents ๐ŸŒŠ

Documents can similarly be streamed from the cache, server, or a combination of both:

final documentStream = Nimbostratus.instance
  .streamDocument(
    FirebaseFirestore.instance.collection('users').doc('user-1'),
    fetchPolicy: StreamFetchPolicy.cacheAndServer,
  ).listen((snap) {
    print(snap.data());
    // { 'id': 'user-1', 'name': 'Anakin Skywalker' }
    // Later when the document changes on the server:
    // { 'id': 'user-1', 'name': 'Darth Vader' }
  });

In this case, we're streaming the document users/user-1 from both the cache and the server. A fetch policy like this can be valuable since data can be eagerly returned from the cache in order to create a zippy user experience, while maintaining a subscription to changes from the server in the future.

Streamed documents will also update when changes are made to the cache. In the example below, we can manually update a value in the in-memory cache, causing all of the places across the client that are streaming the document to update:

final docRef = FirebaseFirestore.instance.collection('users').doc('user-1');

final documentStream = Nimbostratus.instance
  .streamDocument(
    docRef,
    fetchPolicy: StreamFetchPolicy.cacheAndServer,
  ).listen((snap) {
    print(snap.data());
    // { 'id': 'user-1', 'name': 'Anakin Skywalker' }
    // { 'id': 'user-1', 'name': 'Darth Vader' }
  });

await NimbostratusInstance.updateDocument(
  docRef,
  { 'name': 'Darth Vader' }
  writePolicy: WritePolicy.cacheOnly,
);

Executing and reacting to client-side cache changes is an intentional gap in the included feature set of the cloud_firestore, since it is meant to function as a relatively simple document-fetching layer rather than a document management layer. Nimbostratus aims to fill that gap and provide functionality similar to other data fetching libraries that have more extensive client APIs.

Querying documents ๐Ÿ”Ž

Querying for documents follows a similar pattern:

final stream = Nimbostratus.instance
  .streamDocuments(
    store.collection('users').where('first_name', isEqualTo: 'Ben'),
    fetchPolicy: StreamFetchPolicy.serverFirst,
  ).listen((snap) {
    print(snap.data());
    // [
    //   { 'id': 'user-1', 'first_name': 'Ben', 'last_name': 'Kenobi', 'side': 'light' },
    //   { 'id': 'user-2', 'first_name': 'Ben', 'last_name': 'Solo', 'side': 'light' }
    // ]
  });

With the serverFirst policy shown above, data will first be delivered to the stream from the server once and then the stream will listen to any changes to the cached data. When a later cache update occurs like this:

await NimbostratusInstance.updateDocument(
   store.collection('users').doc('user-2'),
  { 'side': 'dark' }
  writePolicy: WritePolicy.cacheAndServer,
);

Since the stream is subscribed to any changes to documents user-1 and user-2 with a cacheAndServer write policy, the stream will immediately receive the updated query snapshot from the cache:

// [
//   { 'id': 'user-1', 'first_name': 'Ben', 'last_name': 'Kenobi', 'side': 'light' },
//   { 'id': 'user-2', 'first_name': 'Ben', 'last_name': 'Solo', 'side': 'dark' }
// ]

and then later emit another value based on the server response if it differs from the initial cached response, such as if another field had been added to the document on the server since we last queried for it:

// [
//   { 'id': 'user-1', 'first_name': 'Ben', 'last_name': 'Kenobi', 'side': 'light' },
//   { 'id': 'user-2', 'first_name': 'Ben', 'last_name': 'Solo', 'side': 'dark', 'relationship_status: 'complicated' }
// ]

Optimistic updates โšก๏ธ

The cacheAndServer policy in the example above is an optimistic write policy. The update is first written to the cache optimistically and then if the server response then fails, the cached change will be rolled back to the most up-to-date value. Optimistic updates make it possible to present a user with an immediately updated value and make an application feel live and zippy, while making sure that if something goes wrong, the experience can be rolled back to a consistent server state.

Batch updates ๐Ÿ“š

Firestore supports batching of multiple document updates together with the batch API. We can take advantage of the Nimbostratus data fetching and writing features when batching using the batchUpdateDocuments API:

await Nimbostratus.instance.batchUpdateDocuments((batch) async {
  await batch.update(
    store.collection('users').doc('darth_maul'),
    { "alive": false },
    writePolicy: WritePolicy.cacheAndServer,
  );

  await batch.update(
    store.collection('users').doc('qui_gon'),
    { "alive": false },
    writePolicy: WritePolicy.cacheAndServer,
  );

  await batch.commit();
});

In this example, we're using the cacheAndServer policy again to optimistically apply our cache updates. The difference when using Firestore batches is that the server updates aren't finalized until the commit() call succeeds. If the server response fails and an exception is thrown by commit(), the optimistic cached changes will be rolled back as well.

There are other cases where you still want to perform optimistic updates in the cache for remote updates that aren't made through Firestore, such as updating a document in Firestore indirectly through a Cloud Function:

await Nimbostratus.instance.batchUpdateDocuments((batch) async {
  await batch.update(
    store.collection('users').doc('darth_maul'),
    { "alive": false },
    writePolicy: WritePolicy.cacheOnly,
  );

  await batch.update(
    store.collection('users').doc('qui_gon'),
    { "alive": false },
    writePolicy: WritePolicy.cacheOnly,
  );

  await FirebaseFunctions.instance.httpsCallable('finish_episode_1').call();
});

In this example, we optimistically update documents in the cache before making the call the Cloud Function. If the call fails and throws an error, the optimistic cache updates will automatically be rolled back. If the batch update function finishes without throwing an error, then the optimistic updates are committed and made permanent.

Gotchas

  1. The cache updates made on the client are in-memory. The Firestore cache does not support direct writing to it, so instead the Nimbostratus caching layer sits on top of the Firestore persistent cache in memory. Restarting your application will not persist your cached changes, you'll need to make server changes that Firestore will then persist in its separate cache.

  2. Queries that stream documents only from the cache such as when using StreamFetchPolicy.cacheOnly will only update in response to changes to their current documents, not new documents that are added. If for example, we have a query like this:

final stream = Nimbostratus.instance
  .streamDocuments(
    store.collection('users').where('first_name', isEqualTo: 'Ben'),
    fetchPolicy: StreamFetchPolicy.cacheOnly,
  ).listen((snap) {
    print(snap.data());
    // [
    //   { 'id': 'user-1', 'first_name': 'Ben', 'last_name': 'Kenobi', 'side': 'light' },
    //   { 'id': 'user-2', 'first_name': 'Ben', 'last_name': 'Solo', 'side': 'light' }
    // ]
  });

and then at a later time, a new document with first_name 'Ben' is added in the cache, this query will not update with that new user, since it does not know that they should be included. The opposite is true if we update user-2 to first_name: Han. The query will still re-emit the user-2 on the stream, even though they no longer satisfy the query's filters. In order to stream documents that re-evaluate the query, you will need to use a server policy like StreamFetchPolicy.cacheAndServer or StreamFetchPolicy.serverOnly.

  1. Cache updates currently do not merge data the same as on the server. The set and update APIs from Firestore support some advanced data merging options for document fields. When we make an update to the cache like this:
await NimbostratusInstance.setDocument(
  FirebaseFirestore.instance.collection('users').doc('user-1'),
  { 'name': 'Darth Vader', ...nestedFields }
  options: SetOptions(
    mergeFields: ['name', 'nestedFields1.nestedFields2...']
  )
  writePolicy: WritePolicy.cacheAndServer,
);

The first cache update uses just a simple merge of the maps and not making the more advanced nested field path changes that will subsequently be reflected in the server response. When the server response comes back, the cache will also be updated to that fully merged server update. This is currently a TODO that if anyone wants to work on to achieve parity with the server update feel free to reach out.

You might also like...

FlutterAgoraFirebaseVideoCall (Agora RTC, Bloc Pattern, Cubit, Firestore, Cloud Function, FCM)

FlutterAgoraFirebaseVideoCall (Agora RTC, Bloc Pattern, Cubit, Firestore, Cloud Function, FCM)

Flutter Agora Fully Functional Video Call module Tech Stack Client: Dart, Flutter Server: Firebase firestore, Google cloud functions Techniques: *BloC

Dec 16, 2022

A fully functional Movies Application built with Flutter. The application built with null safety and clean architecture, also uses OMDB API for fetching movies in the search item

A fully functional Movies Application built with Flutter. The application built with null safety and clean architecture, also uses OMDB API for fetching movies in the search item

Cinema DB Project Details This project uses null safety feature Project uses clean code architecture (Uncle Bob's Architecture) Project can run on bot

Oct 1, 2022

A light, powerful and reactive state management for Flutter Apps.

A light, powerful and reactive state management for Flutter Apps.

A light, powerful and reactive state management. Features โšก๏ธ Build for speed. ๐Ÿ“ Reduce boilerplate code significantly. ๐Ÿ“ Improve code readability. ?

Dec 15, 2022

This is an applications for fetching pokemon data from API and showed in Listview with infinity scrolling

This is an applications for fetching pokemon data from API and showed in Listview with infinity scrolling

pokedex This is an applications for fetching pokemon data from API and showed in Listview with infinity scrolling #Author Created by Yusril Rapsanjani

Dec 13, 2019

๐Ÿงพ Flutter widget allowing easy cache-based data display in a ListView featuring pull-to-refresh and error banners.

Often, apps just display data fetched from some server. This package introduces the concept of fetchable streams. They are just like normal Streams, b

Jan 18, 2022

An all-in-one Fllutter package for state management, reactive objects, animations, effects, timed widgets etc.

An all-in-one Fllutter package for state management, reactive objects, animations, effects, timed widgets etc.

Frideos An all-in-one package for state management, streams and BLoC pattern, animations and timed widgets, effects. Contents 1. State management Gett

Dec 23, 2022

This repository is Online_Learning Screen UI - Flutter. I am fetching the data from the local JSON API.

online_learning A new Flutter Online_Learning application. Designed by Arun PP, Code with Flutter by Kishor Kc. Kishor Kc I am fetching the data from

Jul 5, 2022

A powerful official extension library of Tab/TabBar/TabView, which support to scroll ancestor or child Tabs when current is overscroll, and set scroll direction and cache extent.

extended_tabs Language: English | ไธญๆ–‡็ฎ€ไฝ“ A powerful official extension library of Tab/TabBar/TabView, which support to scroll ancestor or child Tabs whe

Dec 13, 2022
Comments
  • rework cache and server first to remove duplicate event

    rework cache and server first to remove duplicate event

    The current implementation of cacheAndServerFirst could cause a duplicate event to be emitted when you transition between the serverFirst policy it was using under the hood to the cacheAndServer one. By relying instead on serverOnly and mapping over the results as is done in the new streamDocuments implementation, we should be able to remove that one-time duplicate event.

    opened by danReynolds 0
  • Feature/add cache and server first

    Feature/add cache and server first

    Adds a cache and server first stream fetch policy to fill the functionality gap mentioned in #3 . The goal of this new policy is to stream data from both the cache and server but wait until an initial server response before reading data from the cache.

    opened by danReynolds 0
  • Add fromFirestore support

    Add fromFirestore support

    This PR introduces the fromFirestore API for filtering what fields returned from Firestore should be merged into the cache on the client. This is helpful if you've made cache writes on the client optimistically for fields you intend to persist to the server later, but in the meantime you need to refetch from the server.

    Up until now, the assumption had been made that whatever comes back from the server is the source of truth and should overwrite what's in the cache. The fromFirestore API makes it possible to specify a custom merge function for when values come back from the server and to complements the toFirestore API that provides the opposite filtering of what values you want sent to the server.

    opened by danReynolds 0
  • Collections and Snapshot modes

    Collections and Snapshot modes

    Hi there,

    great idea, thanks for adding the project. I have some questions:

    • if i've read the documentation correctly, this also works for streaming Firestore collections?
    • comparing to snapshots you currently support the mode QuerySnapshot.docs i'd say. Do you plan to support .docChanges as well?
    • which StreamFetchPolicy would i need to choose if i'm interested in the most recent version from the server, and if there is no network available a fallback to the cache version (is that the Nimbostratus or the Firebase cache) should be performed?

    I was looking for such a solution and i'm curious if i've found it with your package.

    opened by rivella50 16
Owner
Dan Reynolds
Not the musician.
Dan Reynolds
A most easily usable cache management library in Dart. With CacheStorage, you can easily manage cache on your application.

A most easily usable cache management library in Dart! 1. About 1.1. Introduction 1.1.1. Install Library 1.1.2. Import It 1.1.3. Use CacheStorage 1.2.

Kato Shinya 1 Dec 13, 2021
Nexus is a state management library that makes it easy to create and consume your application's reactive data to the user interface.

Nexus ?? Nexus is a state management library that makes it easy to create and consume your application's reactive data to the user interface. With nex

Gor Mkhitaryan 3 Sep 7, 2022
Memory Cache is simple, fast and global in-memory cache with CRUD features.

Memory Cache Memory Cache is simple, fast and global in-memory cache. Features Create, read, update, delete and invalidate cache. Expirable Cache Gett

Gรถkberk Bardakรงฤฑ 6 Dec 25, 2022
โšก Cache Manager A tidy utility to handle cache of your flutter app like a Boss.

โšก Cache Manager A tidy utility to handle cache of your flutter app like a Boss. It provides support for both iOS and Android platforms (offcourse). ??

Abhishek Chavhan 10 Oct 25, 2022
House Record manager application, cloud firestore will manage the data

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

kyle reginaldo 3 Aug 29, 2022
Dart package for Async Data Loading and Caching. Combine local (DB, cache) and network data simply and safely.

Stock is a dart package for loading data from both remote and local sources. It is inspired by the Store Kotlin library.

xmartlabs 59 Dec 24, 2022
A font loader to download, cache and load web fonts in flutter with support for Firebase Cloud Storage.

Dynamic Cached Fonts A simple, easy to use yet customizable font loader to use web fonts. Demo: https://sidrao2006.github.io/dynamic_cached_fonts ?? I

Aneesh Rao 18 Dec 21, 2022
The prime objective of this app is to store the real time information of the user using firebase cloud firestore and also can delete, remove and update the customer information

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

Muhammad Zakariya 0 Mar 15, 2022
A Flutter application with proper navigation and routes handling and API data fetching and posting.

Flutter-Navigation-and-API-Integration A Flutter application with proper navigation and routes handling and API data fetching and posting. โฎ Preview G

Ehmad Saeedโšก 7 Oct 5, 2022
Notey - A noteApp build using flutter with firebase cloud firestore

notey A project to create and store notes for later purposes. Notey is built usi

Al-amin O. 1 Aug 14, 2022