Flutter Data is a local-first data framework with a customizable REST client and powerful model relationships, built on Riverpod.
Inspired by Ember Data and ActiveRecord.
In Flutter Data, every model gets its default 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')
),
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 |
Adapter
, there is no Repository
, RemoteAdapter
or LocalAdapter
. Any method you are looking for is probably on Adapter
, for example, findAll
from LocalAdapter
is now called findAllLocal
configure...
method on the Riverpod overrides, we just do localStorageProvider.overrideWithValue
and pass a LocalStorage
instance; the actual initialization is done via initializeFlutterData
which needs an adapter map. An adapterProvidersMap
is conveniently code-generated and available on main.data.dart
First you need to supply a local storage provider, via Riverpod configuration. A popular option for the base directory is using the path_provider
package.
ProviderScope(
overrides: [
localStorageProvider.overrideWithValue(
LocalStorage(
baseDirFn: () async {
return (await getApplicationSupportDirectory()).path;
},
busyTimeout: 5000,
clear: LocalStorageClearStrategy.never,
),
)
],
// ...
),
And initialize like so:
return Scaffold(
body: ref.watch(initializeFlutterData(adapterProvidersMap)).when(
data: (_) => child,
error: (e, _) => const Text('Error'),
loading: () => const Center(child: CircularProgressIndicator()),
),
WIP. Method names should be self explanatory. All of these methods have a reasonable default implementation.
All models are identified by key
s, that might be associated to an id
(either self-assigned or fetched from a remote source).
Keys have the format model#5
.
List<T> findAllLocal();
List<T> findManyLocal(Iterable<String> keys);
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;
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 = false,
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;
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;
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});
Please use Github to ask questions, open issues and send PRs. Thanks!
Tests can be run with: dart test
See LICENSE.