Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up

A Dart library for data loading, validation, and caching

License

NotificationsYou must be signed in to change notification settings

motorro/DartLceModel

Repository files navigation

A reactive data loading for Dart platform to load data and report anoperation state (Loading/Content/Error).

WORK IN PROGRESS. Refer toAndroid library docs to get an overview

Features

  • Widely used design withLoading/Content/Error states
  • Uses cache as a 'source of truth' withCacheThenNetLceModel.
  • Checks data is valid (up-to-date or whatever).
  • Falls back to invalid cache data if failed to refresh which allows offline application use.
  • Supports datarefresh orupdate to implement reload or server data update operations.
  • Cache may beinvalidated separately from loading to allow lazy data updates and complex data linking.
  • Extendable architecture on every level.
  • Thoroughly tested.

Example

The project contains an example app that:

  • loads a list of GitHub user repositories, caching resulting data
  • externally invalidates cache that makes active listeners to reload data

Table of Contents

Setting up the dependency:

$ dart pub add dartlcemodel

LceState

A modern approach to architecting the reactive application suggests packing the combined state of the application into aflow of immutable state-objects. Each of them should contain the whole set of data required to process, transform,and display according to the business requirement. The most commonly used information besides the data itself is a stateof data-loading pipeline.

LceState class diagram

EachLceState<DATA, PARAMS> subclass represents a data-loading phase and contains the following data:

  • DATA? data - Loaded data
  • bool dataIsValid - The validity of data at the time of emission. May be used by caching services to indicatethe need of data refresh. More about it inCacheThenNetLceModel section.

States being emitted are:

  • Loading - data is being loaded or updated. May contain some data. The exact state is defined bytype property:
/// Loading typeenumLoadingType {/// Just loads. May be initial load operation  loading,/// Loading more items for paginated view  loadingMore,/// Refreshing content  refreshing,/// Updating data on server  updating}
  • Content - data is loaded.
  • Error - some error while loading or updating data. May also contain some data.
  • Terminated - a special state to indicate that resource identified byparams is not available anymore.

LceModel

LceState<DATA, PARAMS> in this library is being produced by the simple use-caseinterface:

/// Base LCE use-case with[state] and[refresh]///[DATA] Data type of data being loadedabstractclassLceUseCase<DATAextendsObject> {/// Model state. Subscription starts data load for the first subscriber.    /// Whenever last subscriber cancels, the model unsubscribes internal components for data updatesStream<LceState<DATA>>get state;/// Requests a refresh of data.    /// Data will be updated asynchronouslyFuture<void>refresh();}

The use-case contains the following properties:

  • state - theStream that emitsLceState
  • refresh - theFuture to perform data refresh

The direct extension of the use-case is theLceModel that binds the expected data withthe data identifyingPARAMS:

/// A model interface to load data and transmit it to subscribers along with loading operation state/// The model is bound with[params] that identify the data///[DATA] Data type of data being loaded///[PARAMS] Params type that identify data being loadedabstractclassLceModel<DATAextendsObject,PARAMSextendsObject>implementsLceUseCase<DATA> {/// Params that identify data being loadedPARAMSget params;}

As you may see, parameters for model is a property - thus making the model immutable itself. This approach makesthings a bit easier in many cases like:

  • When you share your model and it's emission
  • You don't need to supply a Stream forPARAMS which complicates design a lotIf you need dynamic params - just flat-map your params by creating a new model for each parameter value like this:
Stream<string> params=Stream.fromIterable(['peach','banana','apple']);Stream<LceState<FruitData>> state= params.asyncExpand((params)=>createModel(params).state);

CacheThenNetLceModel

As you may guess from its name this kind of model tries to get cached data first and then loads data from network ifnothing is found or the cached data is stalled. Here is the sequence diagram of data loading using this type of model:

CacheThenNet loading sequence

The model creates a data stream for givenPARAMS in acache-serviceand transmits it to subscriber. If cache does not contain any data or data is not valid (more on validation later) themodel subscribes anet-service to download data from network and saves itto the cache for a later use.

It is worth noting thatcache andnet here is just a common use-case of data-sources: locally stored data (cache)and some data that is maybe not that easy to get (net). You may easily adopt data sources of your choice to thatapproach. Say you have a resource-consuming computation result which may be cached and consumed later. The computationitself than becomes anet-service while the result is being stashed to acache-service ofyour choice for later reuse.

To create newCacheThenNet model call a factory function:

final useCase=LceModel.cacheThenNet('params',// params that identify the data being loaded    serviceSet,// A set of cache + net services (see below)    startWith:constLceState.loading(null,false),// Optional initial state to emit at subscription    logger: logger// Optional logger to get what's going on inside the use-case);

Getting and caching data

As already mentioned above caching model uses two services to get data from network and to store it locally.

Caching data always brings up a problem of cache updates and invalidation. Be it a caching policy of your backend teamor some internal logic of your application the data validity evaluation may be easily implemented:

Entity and validation

TheNetService retrieved data and packages it toEntitywrapper - effectively the data itself and someEntityValidator to provide information when data expires.Validator is a simple interface with only three essential methods:

  • bool isValid() - being used by loading pipeline to determine if data is still valid
  • string serialize() - being called byCacheService to save data validation parameters along with data
  • EntityValidator createSnapshot() - creates a 'snapshot' ofisVAlid() value at the moment of creation (moreabout it later).

The resultingEntity is then saved usingCacheService preserving the data itself and the way to tell when dataexpires.

To convertAny data toEntity within your services use the following function:

EntityValidatorcreateValidator() {// Create a validator}val data="Some data";val entity= data.toEntity(createValidator());

Choosing EntityValidator

There are some validatorsavailable already:

  • Simple - just a data-class that is initialized with boolean validity status.
  • Never - never valid.
  • Always - always valid.
  • Lifespan - A validator that is initialized with Time-To-Live value and becomes invalid after itexpires.

While the first three of above-listed validators are easy to use and intuitive the last one needs to be explained.Lifespan when created gets a reference to a system clock and evaluates its validity against it every time it is beingasked of it. ThusLifespan is not an immutable and is an object with a self-changing internal state. A validEntitywithLifespan validator that just seats in memory will expire eventually and become non-valid. That may be a desiredbehavior however in most cases the most useful way to deal with validity is to take a snapshot of data state at the timedata is being emitted from theCacheService. To be able to do this bothEntityValidator andEntity wrappers bothhavecreateSnapshot() methods that fix the validation status at the time of function is called.

To create aLifeSpan validator, there is a helper-factory that takes a single parameter of TTL in a constructor:

// Creates validators that are valid for 5 secondsfinal validatorFactory=EntityValidatorFactory.lifespan(5000);

TheLifespanValidatorFactory is an implementation ofEntityValidatorFactorythat you may implement in case you need your own custom validator.

Displaying 'invalid' data and cache fall-back

Having a cache of required data, besides eliminating extra network calls, gives us the ability to fall back to cached datain case network is not available and to keep working. This is an easy way to create an offline-capable mobile app whencomplex state synchronization between the app and server is not required. With 'cache-then-net' model you get the cachefall-back already implemented. Here is what you get when network connection is not available:

Cache fallback

When there is no cached data available you just getnull fordata property in emittedLceState.Error.

Cache invalidation and data updates

A common task in complex applications may be the need to refresh some data in a part of application whenever somethinghappens in another part. Reloading a list of messages in a chat application when push arrives may be a simple example.There are different ways of doing this - event-buses, Stream-subjects, you name it.With reactive cache-service the library provides, such an invalidation is made in a simple and clean way:

Cache invalidation

If the push-message brings a payload that is enough to display data change you could simply save the new data to cashwithsave method or delete it withdelete method ofCacheService interface:

The sample application demonstrates cache invalidation with a click orRefresh button. Here is how the invalidation isimplemented:

/// Globally available service-setlateServiceSet<List<Repository>,String> serviceSet;voidmain() {// Creates a set of services// - memory cache for data// - a service to get data from server. `isolated()` runs networking and parsing in [Isolate]  serviceSet=ServiceSet(CacheService<List<Repository>,String>.withSyncDelegate(MemoryCacheDelegate.map()),RepositoryNetService().isolated()  );runApp(constMyApp());}// Later in widgetfinal refresh=GestureDetector(  onTap: ()async {await serviceSet.cache.invalidateAll();  },  child:constIcon(Icons.refresh,    size:26.0,  ));

On-demand cache refetch

Consider a cache service with complex internal structure that is updated by some internal logic.For example a database that saves entities and something that updates records directly.In case of Room you may observe a query and get updates if something changes underneath. But sometimesyou have a complex entity with relations that are not so easy to fetch as they need conditional processingin synchronous way.In this case you may write an SQL delegate for sync-delegate service (see below) to implement reactive cache.When you get/put the whole entity the solution works. But as soon as you start to update entity partsyou need some way to notify subscribers of data change.

Cache refetch

CacheServicehas two methods that when called makes it to refetch data and update its active clients:

  • Future<void> refetch(P params): Completable - makes cache service to refetch data forparams and update corresponding clients
  • Future<void> refetchAll() - makes cache service to refetch data for all subscribers

Cache service implementation

While you can implement any cache-service you like the library comes with simpleAsyncDelegateCacheServiceandSyncDelegateCacheService which use the following async/sync delegates for data IO:

Cache delegate

The interface is self-explanatory and does all the IO forCacheService in sync/async way.

There is a simple in-memory cache service available so far. Disk cache port is awork in progress.To create an in-memory cache, use the following:

final cache=CacheService<SomeData,String>.withSyncDelegate(MemoryCacheDelegate.map());

A complete example of model setup in a widget

Here is a complete setup ofLceModel in a widget :

classLceWidget<DextendsObject>extendsStatefulWidget {finalString params;constLceWidget({Key? key,requiredthis.params}):super(key: key);@overrideState<LceWidget<D>>createState()=>_LceWidgetState();}class_LceWidgetState<DextendsObject>extendsState<LceWidget<D>> {LceUseCase<D>? _useCase;StreamSubscription<LceState<D>>? _subscription;lateLceState<D> _lceState;@overridevoidinitState() {super.initState();_subscribe();  }@overridevoiddidUpdateWidget(LceWidget<D> oldWidget) {super.didUpdateWidget(oldWidget);// Check if we need to load another dataif (widget.params!= oldWidget.params) {_unsubscribe();_subscribe();    }  }@overridevoiddispose() {super.dispose();_unsubscribe();  }_subscribe() {// Initial state to display    _lceState=constLceState.loading(null,false);    _useCase=LceModel.cacheThenNet(        widget.params,// params that identify the data being loaded        serviceSet// A set of cache + net services - defined globally or provided with DI    );    _subscription= _useCase!.state.listen((newLceState) {setState(() { _lceState= newLceState; });    });  }_unsubscribe() {    _subscription?.cancel();    _subscription=null;    _useCase=null;  }Future<void>_refresh()async {return _useCase?.refresh()??Future.value();  }@overrideWidgetbuild(BuildContext context)=> lceState.when(      loading: (_)=>constLoadingState(),// Widget for loading      content: (state)=>ContentState(repositories: state.data),// Widget for content      error: (state)=>ErrorState(error: state.error, onRetry: ()async {await_refresh(); })// Error widget  );}

Getting data-only stream and stream transformations

You may transform thestate propertyStream to strip state information and to get only the data. The libraryships with some functions already implemented like:

  • dataWithErrors - emits data emitting stream error on any error
  • dataWithEmptyErrors - emits data and emits an error only if there is no data in original emission(LceError withnull fordata property)
  • dataNoErrors - emits data and ignores errors
  • validData - emits data only if it is valid

More information and the complete list of extensions may be found in generated documentation or thesource code and tests.

State transformations

Sometimes you need to mix several LCE streams from different sources or transform a data. For that there are severalextensionsavailable to map and combine them. For example:

  • map - maps state data to another type with a mapper
  • mapEmptyDataItem - replaces the empty data with the default item
  • combine - combines one state with another and presenting the 'average' value of both

About

A Dart library for data loading, validation, and caching

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published

[8]ページ先頭

©2009-2025 Movatter.jp