Dart package for Async Data Loading and Caching. Combine local (DB, cache) and network data simply and safely.

Related tags

Templates stock
Overview

stock

Pub Codecov Dart CI style: very good analysis

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

Its main goal is to prevent excessive calls to the network and disk cache. By utilizing it, you eliminate the possibility of flooding your network with the same request while, at the same time, adding layers of caching.

Although you can use it without a local source, the greatest benefit comes from combining Stock with a local database such as Floor, Drift, Sqflite, Realm, etc.

Features

  • Combine local (DB, cache) and network data simply. It provides a data Stream where you can listen and work with your data.
  • Know the data Stream state. It's useful for displaying errors or loading indicators, for example.
  • If you are not using a local DB, Stock provides a memory cache, to share and improve upon your app's experience.
  • Work with your data more safely. If an error is thrown, Stock will catch it and return it into the stream so it can be handled easily.

Overview

A Stock is responsible for managing a particular data request.

It is based on two important classes:

  • Fetcher: defines how data will be fetched over network.
  • SourceOfTruth: defines how local data will be read and written in your local cache. Although Stock can be used without it, its use is recommended.

Stock uses generic keys as identifiers for data. A key can be any value object that properly implements toString(), equals() and hashCode().

When you create Stock, it provides you with a bunch of methods in order to access the data. The most important one is stream(), which provides you with a Stream of your data, which can be used to update your UI or to do a specific action.

Getting started

To use this package, add stock as a dependency in your pubspec.yaml file.

1. Create a Fetcher

The Fetcher is required to fetch new data from the network. You can create it from a Future or from a Stream.

FutureFetcher is usually used alongside a RestApi, whereas StreamFetcher is used with a Stream source like a Web Socket.

  final futureFetcher = Fetcher.ofFuture<String, List<Tweet>>(
    (userId) => _api.getUserTweets(userId),
  );

  final streamFetcher = Fetcher.ofStream<String, List<Tweet>>(
    (userId) => _api.getUserTweetsStream(userId),
  );

2. Create a Source Of Truth

The SourceOfTruth is used to read and write the remote data in a local cache.

Generally you will implement the SourceOfTruth using a local database. However, if you are not using a local database / cache, the library provides the CachedSourceOfTruth, a source of truth which stores the data in memory.

  final sourceOfTruth = SourceOfTruth<String, List<Tweet>>(
    reader: (userId) => _database.getUserTweets(userId),
    writer: (userId, tweets) => _database.writeUserTweets(userId, tweets),
  );

In this example, Fetcher type is exactly the SourceOfTruth type. If you need to use multiple types, you can use the mapTo extension.

Note: to the proper operation, when write is invoked with new data, the source of truth has to emit the new value in the reader.

3. Create the Stock

Stock lets you combine the different data sources and get the data.

 final stock = Stock<String, List<Tweet>>(
    fetcher: fetcher,
    sourceOfTruth: sourceOfTruth,
  );

Get a data Stream from Stock

You can generate a data Stream using stream().

You need to invoke it with a specific key, and an optional refresh value that tells Stock if a refresh is optional or mandatory.

That returns a data stream of StockResponse, which has 3 possible values:

  • StockResponseLoading informs that a network request is in progress. It can be useful to display a loading indicator in your UI.
  • StockResponseData holds the response data. It has a value field which includes an instance of the type returned by Stock.
  • StockResponseError indicates that an error happened. When an error happens, Stock does not throw an exception, instead, it wraps it in this class. It includes an error field that contains the exception thrown by the given origin.

Each StockResponse includes an origin field which specifies where the event is coming from.

  stock
      .stream('key', refresh: true)
      .listen((StockResponse<List<Tweet>> stockResponse) {
    if (stockResponse is StockResponseLoading) {
      _displayLoadingIndicator();
    } else if (stockResponse is StockResponseData) {
      _displayTweetsInUI((stockResponse is StockResponseData).data);
    } else {
      _displayErrorInUi((stockResponse as StockResponseError).error);
    }
  });

Get non-stream data from Stock

Stock provides a couple of methods to get data without using a data stream.

  1. get returns cached data -if it is cached- otherwise will return fresh/network data (updating your caches).
  2. fresh returns fresh data updating your cache
  // Get fresh data
  final List<Tweet> freshTweets = await stock.fresh(key);
  
  // Get the previous cached data
  final List<Tweet> cachedTweets = await stock.get(key);

Use different types for Fetcher and SourceOfTruth

Sometimes you need to use different entities for Network and DB. For that case Stock provides the StockTypeMapper, a class that transforms one entity into the other.

StockTypeMapper is used in the SourceOfTruth via the method mapToUsingMapper

class TweetMapper implements StockTypeMapper<DbTweet, NetworkTweet> {
  @override
  NetworkTweet fromInput(DbTweet value) => NetworkTweet(value);

  @override
  DbTweet fromOutput(NetworkTweet value) => DbTweet(value);
}

final SourceOfTruth<int, DbTweet> sot = _createMapper();
final SourceOfTruth<int, NetworkTweet> newSot = sot.mapToUsingMapper(TweetMapper());

You can also achieve the same result using the mapTo extension.

final SourceOfTruth<int, DbTweet> sot = _createMapper();
final SourceOfTruth<int, NetworkTweet> newSot = mapTo(
      (networkTweet) => DbTweet(networkTweet),
      (dbTweet) => NetworkTweet(dbTweet),
);

Use a non-stream source of truth

Sometimes your Source of Truth does not provide you with a real-time data stream. For example, suppose that you are using shared_preferences to store your data, or you are just catching your data in memory. For these cases, Stock provides you the CachedSourceOfThruth, a SourceOfThruth that be helpful in these cases.

class SharedPreferencesSourceOfTruth extends CachedSourceOfTruth<String, String> {
  SharedPreferencesSourceOfTruth();

  @override
  @protected
  Stream<T?> reader(String key) async* {
    final prefs = await SharedPreferences.getInstance();
    // Read data from an non-stream source
    final stringValue = prefs.getString(key);
    setCachedValue(key, stringValue);
    yield* super.reader(key);
  }

  @override
  @protected
  Future<void> write(String key, String? value) async {
    final prefs = await SharedPreferences.getInstance();
    await prefs.setString(key, value);
    await super.write(key, value);
  }
}

This class can be used with StockTypeMapper to transform your data into an entity.

// The mapper json to transform a string into a User 
final StockTypeMapper<String, User> mapper = _createUserMapper();
final SharedPreferencesSourceOfTruth<String, User> = SharedPreferencesSourceOfTruth()
    .mapToUsingMapper(mapper);

Additional information

For bugs please use GitHub Issues. For questions, ideas, and discussions use GitHub Discussions.

Made with ❤️ by Xmartlabs.

License

Copyright (c) 2022 Xmartlabs SRL.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
You might also like...

A Dart/Flutter package to perform network calls. It uses Isolates to perform network calls on Dart VM environments and WebWorkers on Web.

ArDriveHTTP ArDriveHTTP is a package to perform network calls for ArDrive Web. It uses Isolates to perform network calls on Dart VM environments and W

Dec 15, 2022

A Dart utility package for easy async task cancellation

Dart Cancellation Token. Inspired by CancellationToken in C#. A Dart utility package for easy async task cancellation.

Sep 6, 2022

⚡FQuery is a powerful async state management solution for flutter. It caches, updates and fully manages asynchronous data in your flutter apps.

⚡ FQuery is a powerful async state management solution for flutter. It caches, updates and fully manages asynchronous data in your flutter apps. It ca

Dec 22, 2022

A simple state management solution that combine the power of inherited widget and rxdart

Inherited RxDart is a state management library that combine the power of InheritedWidget and RxDart, a simple and elegant state management solution for apps of any scale

Oct 26, 2022

API-Calls-from-Json-Place-holder-through-my-Local- - An app that shows the calls from the jsonplaceholder.com data from the Local

API-Calls-from-Json-Place-holder-through-my-Local- - An app that shows the calls from the jsonplaceholder.com data from the Local

finalapi A new Flutter project. Getting Started This project is a starting point

Jan 10, 2022

Know where to go safely. Describe your experiences and rate places.

Know where to go safely. Describe your experiences and rate places.

Is It Safe? 📌 Índice Sobre Showcase Features Como eu posso rodar o projeto? Ferramentas Instalação Run Suporte Como posso contribuir? Autores Info 🤔

Sep 19, 2022

Makes it possible to safely execute and retry a Future inside a StatelessWidget

Makes it possible to safely execute and retry a Future inside a StatelessWidget

futuristic Makes it possible to safely execute and retry a Future inside a StatelessWidget. See the Mainstream package for a similar API for working w

Sep 15, 2022

This Crypto App is Simply Showing data and chart by using coin gecko api.

This Crypto App is Simply Showing data and chart by using coin gecko api.

crypto_app This is Simple Crypto Currency analytics showing app using coingecko api. You Can Use this if you want to show simply crypto currency analy

Oct 7, 2022

Hive Wait provide a Hive repository to calling methods in the box as async.

Hive Wait provide a Hive repository to calling methods in the box as async.

May 10, 2022
Comments
  • Batch Operations

    Batch Operations

    Is your feature request related to a problem? Please describe.

    My data source allows fetching multiple elements at the same time, and I have to display a list of 50+ entries. Right now, these 50 fetches can't be batched together to a single request in Stock.

    Describe the solution you'd like

    Two new functions .getList and .freshList on Stock that take a list of Keys instead of a single one. This information should then be passed on to the Fetcher and the SourceOfTruth as-is. If the old .get or .fetch is used, a list with a single entry could be supplied instead (so there's still only a single function to be called).

    Describe alternatives you've considered

    It's possible to debounce the Fetcher and SourceOfTruth since they're async, and so collect all requests coming in within a certain timeframe, collect them, send a single request, and then split up the result again. However, that adds lag to the whole operation that shouldn't be necessary and is probably also pretty complicated to implement.

    opened by anlumo 5
  • Add `map` and `when` extension methods over `StockResponse`

    Add `map` and `when` extension methods over `StockResponse`

    Description

    The goal is to use the StockResponse in a more declarative way by using extension methods on StockResponse.

    Methods

    • [x] map method
    • [x] maybeMap method
    • [x] when method
    • [x] maybeWhen method

    Tests

    • [x] map test
    • [x] maybeMap test
    • [x] when test
    • [x] maybeWhen test

    Example

    // Create a stream to listen tweet changes of the user `xmartlabs`
    stock
        .stream('xmartlabs', refresh: true)
        .listen((StockResponse<List<Tweet>> stockResponse) {
      if (stockResponse is StockResponseLoading) {
        _displayLoadingIndicator();
      } else if (stockResponse is StockResponseData) {
        _displayTweetsInUI(stockResponse.requireData());
      } else {
        _displayErrorInUi((stockResponse as StockResponseError).error);
      }
    });
    

    With when:

    // Create a stream to listen tweet changes of the user `xmartlabs`
    stock
      .stream('xmartlabs', refresh: true)
      .listen((StockResponse<List<Tweet>> stockResponse) => stockResponse.when(
            onLoading: _displayLoadingIndicator,
            onData: _displayTweetsInUI,
            onError: (error, stackTrace) => _displayErrorInUi(error),
          ));
    
    opened by leynier 5
  • Why stock output type depends on Fetcher type not SourceOfTruth one ?.

    Why stock output type depends on Fetcher type not SourceOfTruth one ?.

    I need to use different entities for Network (Fetcher) and DB (SourceOfTruth), when I'm using StockTypeMapper it will convert the SourceOfTruth entities to Fetcher which is not follow the concept of SourceOfTruth. As Stock documentations says at source_of_truth.dart file:

    /// In other words, values coming from the [Fetcher] will always be sent to the
    /// [SourceOfTruth] and will be read back via [reader] to then be returned to
    /// the collector.
    

    For example if I need to observe list of tweets in my UI :

        final fetcher = Fetcher.ofFuture<String, List<NetworkTweet>>(
          (userId) => _api.getUserTweets(userId),
        );
    
        final SourceOfTruth<String, List<DbTweet>> sourceOfTruth = SourceOfTruth(
        reader: (userId) => _database.getUserTweets(userId),
        writer: (userId, tweets) => _database.writeUserTweets(userId, tweets),
       );
    
        final SourceOfTruth<void, List<NetworkTweet>> mappedSourceOfTruth = sourceOfTruth.mapTo(
          (List<DbTweet> dbTweets) => dbTweets.map((it) => it.asNetwork).toList(),
          (List<NetworkTweet> networkTweets) => networkTweets.map((it) => it.asDb).toList(),
        );
    
        final stock = Stock<String, List<NetworkTweet>>(
          fetcher: fetcher,
          sourceOfTruth: mappedSourceOfTruth,
        );
    
       stock
          .stream('user_id', refresh: true)
          .listen((StockResponse<List<NetworkTweet>> stockResponse) { // we must get List<DbTweet> instead to achieve SourceOfTruth principles.
              /// ......
          }
    
    

    The stream response must be same as DB model not Network model because it's my source of truth. I think we can add StockTypeMapper to Fetcher class rather than SourceOfTruth one, can I achieve this behavior in another way ?

    opened by Abdktefane 2
Releases(v1.0.1)
  • v1.0.1(Nov 18, 2022)

    What’s Changed

    • Document improvements (#34)
    • Make SourceOfTruth non-required in the Stock constrictor (#33)
    • Add mapData extension to transform the StockResponse<T> into a StockResponse<E> (#31).
    • Add stock.clear(id) and stock.clearAll() functionality. Thanks Abdktefane for PR #30.

    New Contributors

    • @Abdktefane made their first contribution in https://github.com/xmartlabs/stock/pull/30

    Full Changelog: https://github.com/xmartlabs/stock/compare/v1.0.0...v1.0.1

    Source code(tar.gz)
    Source code(zip)
  • v1.0.0(Oct 5, 2022)

    What's Changed

    • Document improvements (#23)
    • CI improvements (#22)
    • Add map and when extension methods over StockResponse (#21, #27). Thanks Leynier for PR (#21).

    New Contributors

    • @leynier made their first contribution in https://github.com/xmartlabs/stock/pull/21

    Full Changelog: https://github.com/xmartlabs/stock/compare/v0.0.2...v1.0.0

    Source code(tar.gz)
    Source code(zip)
  • v0.0.2(Sep 2, 2022)

  • v0.0.1(Aug 29, 2022)

Dart / Flutter package that allows discovering network devices in local network (LAN).

lan_scanner Dart / Flutter package that allows discovering network devices in local network (LAN). Note: This library is intended to be used on Class

null 12 Dec 9, 2022
Flutter shareable package of object-oriented classes for local caching of user data in json

json_cache Json Cache is an object-oriented package to serve as a layer on top of local storage packages - packages that persist data locally on the u

Dartoos 10 Dec 19, 2022
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
A Video and Audio player that can play from local assets, local files and network URLs with the powerful controls

Video/Audio Player in Flutter with Powerful controls How can we play videos in Flutter? There is a library directly from the Flutter team simply calle

Harsh Mistry 12 Jan 31, 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
A flutter package to cache network image fastly without native dependencies.

Fast Cached Network Image A flutter package to cache network image fastly without native dependencies, with loader, error builder, and smooth fade tra

CHRISTO 3 Sep 22, 2022
Local data hive - Local data hive for flutter

local_data_hive A new Flutter application. ScreenShot

Mehmet Emre ÖZ 0 Jan 8, 2022
Combine a set of dart files into one

dart-merger Combine a set of dart files into one. This is useful when you want to organize a group of files automatically generated by generator. Inst

Kenta Murai 2 Mar 17, 2022