Flutter Data

tests codecov pub.dev license

Persistent reactive models in Flutter with zero boilerplate

Flutter Data is an offline-first data framework with a customizable REST client and powerful model relationships, built on Riverpod.

Inspired by Ember Data and ActiveRecord.

Features

  • Adapters for all models 🚀
    • Default CRUD and custom remote endpoints
    • StateNotifier watcher APIs
  • Built for offline-first 🔌
    • SQLite3-based local storage at its core, with adapters for many other engines: Objectbox, Isar, etc (coming soon)
    • Failure handling & retry API
  • Intuitive APIs, effortless setup 💙
    • Truly configurable and composable via Dart mixins and codegen
    • Built-in Riverpod providers for all models
  • Exceptional relationship support ⚡️
    • Automatically synchronized, fully traversable relationship graph
    • Reactive relationships

👩🏾‍💻 Quick introduction

In Flutter Data, every model gets is adapter. These adapters can be extended by mixing in custom adapters.

Annotate a model with @DataAdapter and pass a custom adapter:

@JsonSerializable()
@DataAdapter([MyJSONServerAdapter])
class User extends DataModel<User> {
  @override
  final int? id; // ID can be of any type
  final String name;
  User({this.id, required this.name});
  // `User.fromJson` and `toJson` optional
}

mixin MyJSONServerAdapter on RemoteAdapter<User> {
  @override
  String get baseUrl => "https://my-json-server.typicode.com/flutterdata/demo/";
}

After code-gen, Flutter Data will generate the resulting Adapter<User> which is accessible via Riverpod's ref.users or container.users.

@override
Widget build(BuildContext context, WidgetRef ref) {
  final state = ref.users.watchOne(1);
  if (state.isLoading) {
    return Center(child: const CircularProgressIndicator());
  }
  final user = state.model;
  return Text(user.name);
}

To update the user:

TextButton(
  onPressed: () => ref.users.save(User(id: 1, name: 'Updated')),
  child: Text('Update'),
),

ref.users.watchOne(1) will make a background HTTP request (to https://my-json-server.typicode.com/flutterdata/demo/users/1 in this case), deserialize data and listen for any further local changes to the user.

state is of type DataState which has loading, error and data substates.

In addition to the reactivity, models have ActiveRecord-style extension methods so the above becomes:

GestureDetector(
  onTap: () => User(id: 1, name: 'Updated').save(),
  child: Text('Update')
),

Compatibility

Fully compatible with the tools we know and love:

Flutter Or plain Dart. It does not require Flutter.
json_serializable Fully supported (but not required)
Riverpod Supported & automatically wired up
Classic JSON REST API Built-in support!
JSON:API Supported via external adapter
Firebase, Supabase, GraphQL Can be fully supported by writing custom adapters
Freezed Supported!
Flutter Web TBD

📲 Apps using Flutter Data in production

logos

📚 API

Adapters

WIP. Method names should be self explanatory. All of these methods have a reasonable default implementation.

Public API

// local storage

List<T> findAllLocal();

List<T> findManyLocal(Iterable<String> keys);

List<T> deserializeFromResult(ResultSet result);

T? findOneLocal(String? key);

T? findOneLocalById(Object id);

bool exists(String key);

T saveLocal(T model, {bool notify = true});

Future<List<String>?> saveManyLocal(Iterable<DataModelMixin> models,
      {bool notify = true, bool async = true});

void deleteLocal(T model, {bool notify = true});

void deleteLocalById(Object id, {bool notify = true});

void deleteLocalByKeys(Iterable<String> keys, {bool notify = true});

Future<void> clearLocal({bool notify = true});

int get countLocal;

Set<String> get keys;

// remote

Future<List<T>> findAll({
    bool remote = true,
    bool background = false,
    Map<String, dynamic>? params,
    Map<String, String>? headers,
    bool syncLocal = false,
    OnSuccessAll<T>? onSuccess,
    OnErrorAll<T>? onError,
    DataRequestLabel? label,
  });

Future<T?> findOne(
    Object id, {
    bool remote = true,
    bool? background,
    Map<String, dynamic>? params,
    Map<String, String>? headers,
    OnSuccessOne<T>? onSuccess,
    OnErrorOne<T>? onError,
    DataRequestLabel? label,
  });

Future<T> save(
    T model, {
    bool remote = true,
    Map<String, dynamic>? params,
    Map<String, String>? headers,
    OnSuccessOne<T>? onSuccess,
    OnErrorOne<T>? onError,
    DataRequestLabel? label,
  });

Future<T?> delete(
    Object model, {
    bool remote = true,
    Map<String, dynamic>? params,
    Map<String, String>? headers,
    OnSuccessOne<T>? onSuccess,
    OnErrorOne<T>? onError,
    DataRequestLabel? label,
  });

Set<OfflineOperation<T>> get offlineOperations;

// serialization

Map<String, dynamic> serializeLocal(T model, {bool withRelationships = true});

T deserializeLocal(Map<String, dynamic> map, {String? key});

Future<Map<String, dynamic>> serialize(T model,
      {bool withRelationships = true});

Future<DeserializedData<T>> deserialize(Object? data,
      {String? key, bool async = true});

Future<DeserializedData<T>> deserializeAndSave(Object? data,
      {String? key, bool notify = true, bool ignoreReturn = false});

// watchers

DataState<List<T>> watchAll({
    bool remote = false,
    Map<String, dynamic>? params,
    Map<String, String>? headers,
    bool syncLocal = false,
    String? finder,
    DataRequestLabel? label,
  });

DataState<T?> watchOne(
    Object model, {
    bool remote = false,
    Map<String, dynamic>? params,
    Map<String, String>? headers,
    AlsoWatch<T>? alsoWatch,
    String? finder,
    DataRequestLabel? label,
  });

DataStateNotifier<List<T>> watchAllNotifier(
      {bool remote = false,
      Map<String, dynamic>? params,
      Map<String, String>? headers,
      bool syncLocal = false,
      String? finder,
      DataRequestLabel? label});

DataStateNotifier<T?> watchOneNotifier(Object model,
      {bool remote = false,
      Map<String, dynamic>? params,
      Map<String, String>? headers,
      AlsoWatch<T>? alsoWatch,
      String? finder,
      DataRequestLabel? label});

final coreNotifierThrottleDurationProvider;

Protected API

// adapter

Future<void> onInitialized();

Future<Adapter<T>> initialize({required Ref ref});

void dispose();

Future<R> runInIsolate<R>(FutureOr<R> fn(Adapter adapter));

void log(DataRequestLabel label, String message, {int logLevel = 1});

void onModelInitialized(T model) {};

// remote

String get baseUrl;

FutureOr<Map<String, dynamic>> get defaultParams;

FutureOr<Map<String, String>> get defaultHeaders;

String urlForFindAll(Map<String, dynamic> params);

DataRequestMethod methodForFindAll(Map<String, dynamic> params);

String urlForFindOne(id, Map<String, dynamic> params);

DataRequestMethod methodForFindOne(id, Map<String, dynamic> params);

String urlForSave(id, Map<String, dynamic> params);

DataRequestMethod methodForSave(id, Map<String, dynamic> params);

String urlForDelete(id, Map<String, dynamic> params);

DataRequestMethod methodForDelete(id, Map<String, dynamic> params);

bool shouldLoadRemoteAll(
    bool remote,
    Map<String, dynamic> params,
    Map<String, String> headers,
  );

bool shouldLoadRemoteOne(
    Object? id,
    bool remote,
    Map<String, dynamic> params,
    Map<String, String> headers,
  );

bool isOfflineError(Object? error);

http.Client get httpClient;

Future<R?> sendRequest<R>(
    final Uri uri, {
    DataRequestMethod method = DataRequestMethod.GET,
    Map<String, String>? headers,
    Object? body,
    _OnSuccessGeneric<R>? onSuccess,
    _OnErrorGeneric<R>? onError,
    bool omitDefaultParams = false,
    bool returnBytes = false,
    DataRequestLabel? label,
    bool closeClientAfterRequest = true,
  });

FutureOr<R?> onSuccess<R>(
    DataResponse response, DataRequestLabel label);

FutureOr<R?> onError<R>(
    DataException e,
    DataRequestLabel? label,
  );

// serialization

Map<String, dynamic> transformSerialize(Map<String, dynamic> map,
      {bool withRelationships = true});

Map<String, dynamic> transformDeserialize(Map<String, dynamic> map);

➕ Questions and collaborating

Please use Github to ask questions, open issues and send PRs. Thanks!

Tests can be run with: dart test

📝 License

See LICENSE.