feat: add mutation and mutation builder with example
This commit is contained in:
@@ -18,8 +18,12 @@ class MainApp extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return MaterialApp.router(
|
return MaterialApp.router(
|
||||||
|
theme: ThemeData(
|
||||||
|
colorSchemeSeed: Colors.red[100],
|
||||||
|
useMaterial3: true,
|
||||||
|
),
|
||||||
title: 'FL Query Example',
|
title: 'FL Query Example',
|
||||||
showPerformanceOverlay: true,
|
// showPerformanceOverlay: true,
|
||||||
routerConfig: router,
|
routerConfig: router,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,10 @@ class HomePage extends StatelessWidget {
|
|||||||
title: const Text('Infinite Query'),
|
title: const Text('Infinite Query'),
|
||||||
onTap: () => GoRouter.of(context).push('/infinite-query'),
|
onTap: () => GoRouter.of(context).push('/infinite-query'),
|
||||||
),
|
),
|
||||||
|
ListTile(
|
||||||
|
title: const Text('Mutation'),
|
||||||
|
onTap: () => GoRouter.of(context).push('/mutation'),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,130 @@
|
|||||||
|
import 'package:fl_query/fl_query.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class MutationPage extends StatefulWidget {
|
||||||
|
const MutationPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<MutationPage> createState() => _MutationPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MutationPageState extends State<MutationPage> {
|
||||||
|
late TextEditingController _nameController;
|
||||||
|
late TextEditingController _emailController;
|
||||||
|
late TextEditingController _passwordController;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_nameController = TextEditingController();
|
||||||
|
_emailController = TextEditingController();
|
||||||
|
_passwordController = TextEditingController();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_nameController.dispose();
|
||||||
|
_emailController.dispose();
|
||||||
|
_passwordController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text('Mutation'),
|
||||||
|
),
|
||||||
|
body: MutationBuilder<Map<String, dynamic>, dynamic, String,
|
||||||
|
Map<String, dynamic>, dynamic>(
|
||||||
|
const ValueKey('sign-up'),
|
||||||
|
(variables) {
|
||||||
|
return Future.delayed(
|
||||||
|
const Duration(seconds: 1),
|
||||||
|
() => {
|
||||||
|
'name': variables['name'],
|
||||||
|
'email': variables['email'],
|
||||||
|
'password': variables['password'],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
onMutate: (variables) {
|
||||||
|
print('onMutate: $variables');
|
||||||
|
return "Recover ME";
|
||||||
|
},
|
||||||
|
onData: (data, recoveryData) {
|
||||||
|
print('onData: $data');
|
||||||
|
print('recoveryData: $recoveryData');
|
||||||
|
},
|
||||||
|
refreshQueries: const [
|
||||||
|
ValueKey('hello'),
|
||||||
|
],
|
||||||
|
builder: (context, mutation) {
|
||||||
|
if (mutation.hasData) {
|
||||||
|
return ListView(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
children: [
|
||||||
|
Center(
|
||||||
|
child: Text('Welcome ${mutation.data!['name']}'),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Center(
|
||||||
|
child: Text('Your email is ${mutation.data!['email']}'),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () {
|
||||||
|
mutation.reset();
|
||||||
|
},
|
||||||
|
child: const Text('Log out'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ListView(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
children: [
|
||||||
|
TextField(
|
||||||
|
controller: _nameController,
|
||||||
|
keyboardType: TextInputType.name,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Name',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
TextField(
|
||||||
|
controller: _emailController,
|
||||||
|
keyboardType: TextInputType.emailAddress,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Email',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
TextField(
|
||||||
|
controller: _passwordController,
|
||||||
|
obscureText: true,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Password',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () async {
|
||||||
|
await mutation.mutate({
|
||||||
|
'name': _nameController.text,
|
||||||
|
'email': _emailController.text,
|
||||||
|
'password': _passwordController.text,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
child: mutation.isMutating
|
||||||
|
? const CircularProgressIndicator()
|
||||||
|
: const Text('Sign Up'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import "package:example/pages/home.dart";
|
import "package:example/pages/home.dart";
|
||||||
import "package:example/pages/infinite_query.dart";
|
import "package:example/pages/infinite_query.dart";
|
||||||
|
import "package:example/pages/mutation/mutation.dart";
|
||||||
import "package:example/pages/query.dart";
|
import "package:example/pages/query.dart";
|
||||||
import "package:go_router/go_router.dart";
|
import "package:go_router/go_router.dart";
|
||||||
|
|
||||||
@@ -18,5 +19,9 @@ final router = GoRouter(
|
|||||||
path: '/infinite-query',
|
path: '/infinite-query',
|
||||||
builder: (context, state) => const InfiniteQueryPageWidget(),
|
builder: (context, state) => const InfiniteQueryPageWidget(),
|
||||||
),
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: '/mutation',
|
||||||
|
builder: (context, state) => const MutationPage(),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -10,8 +10,10 @@ export 'src/core/client.dart';
|
|||||||
export 'src/core/infinite_query.dart';
|
export 'src/core/infinite_query.dart';
|
||||||
export 'src/core/provider.dart';
|
export 'src/core/provider.dart';
|
||||||
export 'src/core/query.dart';
|
export 'src/core/query.dart';
|
||||||
|
export 'src/core/mutation.dart';
|
||||||
|
|
||||||
export 'src/widgets/query_builder.dart';
|
export 'src/widgets/query_builder.dart';
|
||||||
export 'src/widgets/query_listenable.dart';
|
export 'src/widgets/query_listenable.dart';
|
||||||
export 'src/widgets/infinite_query_builder.dart';
|
export 'src/widgets/infinite_query_builder.dart';
|
||||||
export 'src/widgets/infinite_query_listenable.dart';
|
export 'src/widgets/infinite_query_listenable.dart';
|
||||||
|
export 'src/widgets/mutation_builder.dart';
|
||||||
|
|||||||
@@ -3,13 +3,16 @@ import 'dart:async';
|
|||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:fl_query/src/collections/default_configs.dart';
|
import 'package:fl_query/src/collections/default_configs.dart';
|
||||||
import 'package:fl_query/src/core/infinite_query.dart';
|
import 'package:fl_query/src/core/infinite_query.dart';
|
||||||
|
import 'package:fl_query/src/core/mutation.dart';
|
||||||
import 'package:fl_query/src/core/query.dart';
|
import 'package:fl_query/src/core/query.dart';
|
||||||
|
|
||||||
enum QueryCacheEventType {
|
enum QueryCacheEventType {
|
||||||
addQuery,
|
addQuery,
|
||||||
addInfiniteQuery,
|
addInfiniteQuery,
|
||||||
|
addMutation,
|
||||||
removeQuery,
|
removeQuery,
|
||||||
removeInfiniteQuery,
|
removeInfiniteQuery,
|
||||||
|
removeMutation,
|
||||||
}
|
}
|
||||||
|
|
||||||
class QueryCacheEvent {
|
class QueryCacheEvent {
|
||||||
@@ -22,6 +25,7 @@ class QueryCacheEvent {
|
|||||||
class QueryCache {
|
class QueryCache {
|
||||||
final Set<Query> _queries;
|
final Set<Query> _queries;
|
||||||
final Set<InfiniteQuery> _infiniteQueries;
|
final Set<InfiniteQuery> _infiniteQueries;
|
||||||
|
final Set<Mutation> _mutations;
|
||||||
|
|
||||||
final Duration cacheDuration;
|
final Duration cacheDuration;
|
||||||
|
|
||||||
@@ -30,16 +34,41 @@ class QueryCache {
|
|||||||
QueryCache({
|
QueryCache({
|
||||||
this.cacheDuration = DefaultConstants.cacheDuration,
|
this.cacheDuration = DefaultConstants.cacheDuration,
|
||||||
}) : _queries = Set<Query>(),
|
}) : _queries = Set<Query>(),
|
||||||
_infiniteQueries = Set<InfiniteQuery>() {
|
_infiniteQueries = Set<InfiniteQuery>(),
|
||||||
|
_mutations = Set<Mutation>() {
|
||||||
Timer.periodic(cacheDuration, (timer) {
|
Timer.periodic(cacheDuration, (timer) {
|
||||||
_queries.removeWhere((query) => query.isInactive);
|
_queries.removeWhere((query) {
|
||||||
_infiniteQueries.removeWhere((query) => query.isInactive);
|
if (query.isInactive) {
|
||||||
|
_eventController.add(
|
||||||
|
QueryCacheEvent(QueryCacheEventType.removeQuery, query),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return query.isInactive;
|
||||||
|
});
|
||||||
|
_infiniteQueries.removeWhere((query) {
|
||||||
|
if (query.isInactive) {
|
||||||
|
_eventController.add(
|
||||||
|
QueryCacheEvent(QueryCacheEventType.removeInfiniteQuery, query),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return query.isInactive;
|
||||||
|
});
|
||||||
|
_mutations.removeWhere((mutation) {
|
||||||
|
if (mutation.isInactive) {
|
||||||
|
_eventController.add(
|
||||||
|
QueryCacheEvent(QueryCacheEventType.removeMutation, mutation),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return mutation.isInactive;
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
UnmodifiableSetView<Query> get queries => UnmodifiableSetView(_queries);
|
UnmodifiableSetView<Query> get queries => UnmodifiableSetView(_queries);
|
||||||
UnmodifiableSetView<InfiniteQuery> get infiniteQueries =>
|
UnmodifiableSetView<InfiniteQuery> get infiniteQueries =>
|
||||||
UnmodifiableSetView(_infiniteQueries);
|
UnmodifiableSetView(_infiniteQueries);
|
||||||
|
UnmodifiableSetView<Mutation> get mutations =>
|
||||||
|
UnmodifiableSetView(_mutations);
|
||||||
|
|
||||||
Stream<QueryCacheEvent> get events => _eventController.stream;
|
Stream<QueryCacheEvent> get events => _eventController.stream;
|
||||||
|
|
||||||
@@ -70,4 +99,18 @@ class QueryCache {
|
|||||||
QueryCacheEvent(QueryCacheEventType.removeInfiniteQuery, query),
|
QueryCacheEvent(QueryCacheEventType.removeInfiniteQuery, query),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void addMutation(Mutation mutation) {
|
||||||
|
_mutations.add(mutation);
|
||||||
|
_eventController.add(
|
||||||
|
QueryCacheEvent(QueryCacheEventType.addMutation, mutation),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void removeMutation(Mutation mutation) {
|
||||||
|
_mutations.remove(mutation);
|
||||||
|
_eventController.add(
|
||||||
|
QueryCacheEvent(QueryCacheEventType.removeMutation, mutation),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import 'package:fl_query/src/collections/refresh_config.dart';
|
|||||||
import 'package:fl_query/src/collections/retry_config.dart';
|
import 'package:fl_query/src/collections/retry_config.dart';
|
||||||
import 'package:fl_query/src/core/cache.dart';
|
import 'package:fl_query/src/core/cache.dart';
|
||||||
import 'package:fl_query/src/core/infinite_query.dart';
|
import 'package:fl_query/src/core/infinite_query.dart';
|
||||||
|
import 'package:fl_query/src/core/mutation.dart';
|
||||||
import 'package:fl_query/src/core/provider.dart';
|
import 'package:fl_query/src/core/provider.dart';
|
||||||
import 'package:fl_query/src/core/query.dart';
|
import 'package:fl_query/src/core/query.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
@@ -176,6 +177,60 @@ class QueryClient {
|
|||||||
.then((qs) => Map.fromEntries(qs));
|
.then((qs) => Map.fromEntries(qs));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Mutation<DataType, ErrorType, KeyType, VariablesType>
|
||||||
|
createMutation<DataType, ErrorType, KeyType, VariablesType>(
|
||||||
|
ValueKey<KeyType> key,
|
||||||
|
MutationFn<DataType, VariablesType> mutationFn, {
|
||||||
|
RetryConfig retryConfig = DefaultConstants.retryConfig,
|
||||||
|
}) {
|
||||||
|
final mutation = cache.mutations
|
||||||
|
.firstWhere(
|
||||||
|
(query) => query.key == key,
|
||||||
|
orElse: () => Mutation<DataType, ErrorType, KeyType, VariablesType>(
|
||||||
|
key,
|
||||||
|
mutationFn,
|
||||||
|
retryConfig: retryConfig,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.cast<DataType, ErrorType, KeyType, VariablesType>();
|
||||||
|
|
||||||
|
mutation.updateMutationFn(mutationFn);
|
||||||
|
cache.addMutation(mutation);
|
||||||
|
return mutation;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<DataType?> mutateMutation<DataType, ErrorType, KeyType, VariablesType>(
|
||||||
|
ValueKey<KeyType> key,
|
||||||
|
VariablesType variables, {
|
||||||
|
MutationFn<DataType, VariablesType>? mutationFn,
|
||||||
|
RetryConfig retryConfig = DefaultConstants.retryConfig,
|
||||||
|
List<ValueKey> refreshQueries = const [],
|
||||||
|
List<ValueKey> refreshInfiniteQueries = const [],
|
||||||
|
}) async {
|
||||||
|
final mutation = getMutation<DataType, ErrorType, KeyType, VariablesType>(
|
||||||
|
key,
|
||||||
|
) ??
|
||||||
|
(mutationFn != null
|
||||||
|
? createMutation<DataType, ErrorType, KeyType, VariablesType>(
|
||||||
|
key,
|
||||||
|
mutationFn,
|
||||||
|
retryConfig: retryConfig,
|
||||||
|
)
|
||||||
|
: null);
|
||||||
|
final result = await mutation?.mutate(variables);
|
||||||
|
await this.refreshQueries(refreshQueries);
|
||||||
|
await refreshInfiniteQueriesAllPages(refreshInfiniteQueries);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
Mutation<DataType, ErrorType, KeyType, VariablesType>?
|
||||||
|
getMutation<DataType, ErrorType, KeyType, VariablesType>(
|
||||||
|
ValueKey<KeyType> key) {
|
||||||
|
return cache.mutations
|
||||||
|
.firstWhereOrNull((query) => query.key == key)
|
||||||
|
?.cast<DataType, ErrorType, KeyType, VariablesType>();
|
||||||
|
}
|
||||||
|
|
||||||
static QueryClient of(BuildContext context) {
|
static QueryClient of(BuildContext context) {
|
||||||
return context
|
return context
|
||||||
.dependOnInheritedWidgetOfExactType<QueryClientProvider>()!
|
.dependOnInheritedWidgetOfExactType<QueryClientProvider>()!
|
||||||
|
|||||||
@@ -202,7 +202,7 @@ class InfiniteQuery<DataType, ErrorType, KeyType, PageType>
|
|||||||
|
|
||||||
Future<void> _operation(PageType page) {
|
Future<void> _operation(PageType page) {
|
||||||
return _mutex.protect(() async {
|
return _mutex.protect(() async {
|
||||||
retryOperation(
|
return await retryOperation(
|
||||||
() => state.queryFn(page),
|
() => state.queryFn(page),
|
||||||
config: retryConfig,
|
config: retryConfig,
|
||||||
onSuccessful: (data) async {
|
onSuccessful: (data) async {
|
||||||
@@ -219,20 +219,21 @@ class InfiniteQuery<DataType, ErrorType, KeyType, PageType>
|
|||||||
state = state.copyWith(
|
state = state.copyWith(
|
||||||
pages: {...state.pages..remove(dataPage), dataPage},
|
pages: {...state.pages..remove(dataPage), dataPage},
|
||||||
);
|
);
|
||||||
if (dataPage.data != null)
|
if (dataPage.data is DataType) {
|
||||||
_dataController.add(PageEvent.fromPage(dataPage));
|
_dataController.add(PageEvent.fromPage(dataPage));
|
||||||
if (jsonConfig != null) {
|
if (jsonConfig != null) {
|
||||||
await _box.put(
|
await _box.put(
|
||||||
key.value,
|
key.value,
|
||||||
Map.fromEntries(
|
Map.fromEntries(
|
||||||
state.pages.map(
|
state.pages.map(
|
||||||
(e) => MapEntry(
|
(e) => MapEntry(
|
||||||
e.page,
|
e.page,
|
||||||
e.data != null ? jsonConfig!.toJson(e.data!) : null,
|
e.data != null ? jsonConfig!.toJson(e.data!) : null,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
);
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onFailed: (error) {
|
onFailed: (error) {
|
||||||
@@ -252,7 +253,7 @@ class InfiniteQuery<DataType, ErrorType, KeyType, PageType>
|
|||||||
errorPage,
|
errorPage,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
if (errorPage.error != null)
|
if (errorPage.error is ErrorType)
|
||||||
_errorController.add(PageEvent.fromPage(errorPage));
|
_errorController.add(PageEvent.fromPage(errorPage));
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,138 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:fl_query/src/collections/default_configs.dart';
|
||||||
|
import 'package:fl_query/src/collections/retry_config.dart';
|
||||||
|
import 'package:fl_query/src/core/retryer.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:mutex/mutex.dart';
|
||||||
|
import 'package:state_notifier/state_notifier.dart';
|
||||||
|
|
||||||
|
typedef MutationFn<DataType, VariablesType> = Future<DataType> Function(
|
||||||
|
VariablesType variables,
|
||||||
|
);
|
||||||
|
|
||||||
|
class MutationState<DataType, ErrorType, VariablesType> {
|
||||||
|
final DataType? data;
|
||||||
|
final ErrorType? error;
|
||||||
|
final MutationFn<DataType, VariablesType> mutationFn;
|
||||||
|
final DateTime updatedAt;
|
||||||
|
|
||||||
|
MutationState({
|
||||||
|
required this.mutationFn,
|
||||||
|
this.data,
|
||||||
|
this.error,
|
||||||
|
DateTime? updatedAt,
|
||||||
|
}) : updatedAt = updatedAt ?? DateTime.now();
|
||||||
|
|
||||||
|
MutationState<DataType, ErrorType, VariablesType> copyWith({
|
||||||
|
DataType? data,
|
||||||
|
ErrorType? error,
|
||||||
|
DateTime? updatedAt,
|
||||||
|
MutationFn<DataType, VariablesType>? mutationFn,
|
||||||
|
}) {
|
||||||
|
return MutationState<DataType, ErrorType, VariablesType>(
|
||||||
|
mutationFn: mutationFn ?? this.mutationFn,
|
||||||
|
data: data ?? this.data,
|
||||||
|
error: error ?? this.error,
|
||||||
|
updatedAt: updatedAt ?? DateTime.now(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Mutation<DataType, ErrorType, KeyType, VariablesType>
|
||||||
|
extends StateNotifier<MutationState<DataType, ErrorType, VariablesType>>
|
||||||
|
with Retryer<DataType, ErrorType> {
|
||||||
|
final ValueKey<KeyType> key;
|
||||||
|
final MutationFn<DataType, VariablesType> mutationFn;
|
||||||
|
|
||||||
|
final RetryConfig retryConfig;
|
||||||
|
|
||||||
|
Mutation(
|
||||||
|
this.key,
|
||||||
|
this.mutationFn, {
|
||||||
|
this.retryConfig = DefaultConstants.retryConfig,
|
||||||
|
}) : _dataController = StreamController.broadcast(),
|
||||||
|
_errorController = StreamController.broadcast(),
|
||||||
|
_mutationController = StreamController.broadcast(),
|
||||||
|
super(
|
||||||
|
MutationState<DataType, ErrorType, VariablesType>(
|
||||||
|
mutationFn: mutationFn,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
bool get isInactive => !hasListeners;
|
||||||
|
bool get isMutating => _mutex.isLocked;
|
||||||
|
bool get hasData => state.data != null;
|
||||||
|
bool get hasError => state.error != null;
|
||||||
|
|
||||||
|
DataType? get data => state.data;
|
||||||
|
ErrorType? get error => state.error;
|
||||||
|
Stream<DataType> get dataStream => _dataController.stream;
|
||||||
|
Stream<ErrorType> get errorStream => _errorController.stream;
|
||||||
|
Stream<VariablesType> get mutationStream => _mutationController.stream;
|
||||||
|
|
||||||
|
final _mutex = Mutex();
|
||||||
|
final StreamController<VariablesType> _mutationController;
|
||||||
|
final StreamController<DataType> _dataController;
|
||||||
|
final StreamController<ErrorType> _errorController;
|
||||||
|
|
||||||
|
Future<void> _operate(VariablesType variables) {
|
||||||
|
return _mutex.protect(() async {
|
||||||
|
return await retryOperation(
|
||||||
|
() {
|
||||||
|
_mutationController.add(variables);
|
||||||
|
return state.mutationFn(variables);
|
||||||
|
},
|
||||||
|
config: retryConfig,
|
||||||
|
onSuccessful: (data) {
|
||||||
|
state = state.copyWith(data: data);
|
||||||
|
if (data is DataType) {
|
||||||
|
_dataController.add(data);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onFailed: (error) {
|
||||||
|
state = state.copyWith(error: error);
|
||||||
|
if (error is ErrorType) {
|
||||||
|
_errorController.add(error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<DataType?> mutate(
|
||||||
|
VariablesType variables, {
|
||||||
|
bool scheduleToQueue = false,
|
||||||
|
}) {
|
||||||
|
if (isMutating && !scheduleToQueue) {
|
||||||
|
return Future.value(state.data);
|
||||||
|
}
|
||||||
|
return _operate(variables).then((_) => data);
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateMutationFn(MutationFn<DataType, VariablesType> mutationFn) {
|
||||||
|
if (mutationFn == state.mutationFn) return;
|
||||||
|
state = state.copyWith(mutationFn: mutationFn, updatedAt: state.updatedAt);
|
||||||
|
}
|
||||||
|
|
||||||
|
void reset() {
|
||||||
|
state = MutationState<DataType, ErrorType, VariablesType>(
|
||||||
|
mutationFn: state.mutationFn,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
operator ==(Object other) {
|
||||||
|
return identical(this, other) ||
|
||||||
|
(other is Mutation && key.value == other.key.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => key.hashCode;
|
||||||
|
|
||||||
|
Mutation<NewDataType, NewErrorType, NewKeyType, NewVariablesType>
|
||||||
|
cast<NewDataType, NewErrorType, NewKeyType, NewVariablesType>() {
|
||||||
|
return this
|
||||||
|
as Mutation<NewDataType, NewErrorType, NewKeyType, NewVariablesType>;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -118,7 +118,7 @@ class Query<DataType, ErrorType, KeyType>
|
|||||||
|
|
||||||
Future<void> _operate() {
|
Future<void> _operate() {
|
||||||
return _mutex.protect(() async {
|
return _mutex.protect(() async {
|
||||||
retryOperation(
|
return await retryOperation(
|
||||||
state.queryFn,
|
state.queryFn,
|
||||||
config: retryConfig,
|
config: retryConfig,
|
||||||
onSuccessful: (DataType? data) {
|
onSuccessful: (DataType? data) {
|
||||||
@@ -126,17 +126,19 @@ class Query<DataType, ErrorType, KeyType>
|
|||||||
data: data,
|
data: data,
|
||||||
updatedAt: DateTime.now(),
|
updatedAt: DateTime.now(),
|
||||||
);
|
);
|
||||||
if (data != null) _dataController.add(data);
|
if (data is DataType) {
|
||||||
if (jsonConfig != null && data != null) {
|
_dataController.add(data);
|
||||||
_box.put(
|
if (jsonConfig != null) {
|
||||||
key.toString(),
|
_box.put(
|
||||||
jsonConfig!.toJson(data),
|
key.toString(),
|
||||||
);
|
jsonConfig!.toJson(data),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onFailed: (ErrorType? error) {
|
onFailed: (ErrorType? error) {
|
||||||
state = state.copyWith(error: error, updatedAt: DateTime.now());
|
state = state.copyWith(error: error, updatedAt: DateTime.now());
|
||||||
if (error != null) _errorController.add(error);
|
if (error is ErrorType) _errorController.add(error);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import 'package:fl_query/src/collections/retry_config.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
mixin Retryer<T, E> {
|
mixin Retryer<T, E> {
|
||||||
void retryOperation(
|
Future<void> retryOperation(
|
||||||
FutureOr<T?> Function() operation, {
|
FutureOr<T?> Function() operation, {
|
||||||
required RetryConfig config,
|
required RetryConfig config,
|
||||||
required void Function(T?) onSuccessful,
|
required void Function(T?) onSuccessful,
|
||||||
@@ -21,19 +21,19 @@ mixin Retryer<T, E> {
|
|||||||
onSuccessful(result);
|
onSuccessful(result);
|
||||||
break;
|
break;
|
||||||
} catch (e, stack) {
|
} catch (e, stack) {
|
||||||
if (attempts == config.maxRetries - 1) {
|
if (e is E?) {
|
||||||
if (e is E?) {
|
if (attempts == config.maxRetries - 1) {
|
||||||
onFailed(e as E?);
|
onFailed(e as E?);
|
||||||
} else {
|
|
||||||
FlutterError.reportError(
|
|
||||||
FlutterErrorDetails(
|
|
||||||
exception: e,
|
|
||||||
library: 'fl_query',
|
|
||||||
context: ErrorDescription('retryOperation'),
|
|
||||||
stack: stack,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
FlutterError.reportError(
|
||||||
|
FlutterErrorDetails(
|
||||||
|
exception: e,
|
||||||
|
library: 'fl_query',
|
||||||
|
context: ErrorDescription('retryOperation'),
|
||||||
|
stack: stack,
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -83,8 +83,10 @@ class _InfiniteQueryBuilderState<DataType, ErrorType, KeyType, PageType>
|
|||||||
jsonConfig: widget.jsonConfig,
|
jsonConfig: widget.jsonConfig,
|
||||||
);
|
);
|
||||||
|
|
||||||
dataSubscription = query!.dataStream.listen(widget.onData);
|
if (widget.onData != null)
|
||||||
errorSubscription = query!.errorStream.listen(widget.onError);
|
dataSubscription = query!.dataStream.listen(widget.onData);
|
||||||
|
if (widget.onError != null)
|
||||||
|
errorSubscription = query!.errorStream.listen(widget.onError);
|
||||||
|
|
||||||
removeListener = query!.addListener(update);
|
removeListener = query!.addListener(update);
|
||||||
});
|
});
|
||||||
@@ -130,11 +132,13 @@ class _InfiniteQueryBuilderState<DataType, ErrorType, KeyType, PageType>
|
|||||||
}
|
}
|
||||||
if (oldWidget.onData != widget.onData) {
|
if (oldWidget.onData != widget.onData) {
|
||||||
dataSubscription?.cancel();
|
dataSubscription?.cancel();
|
||||||
dataSubscription = query!.dataStream.listen(widget.onData);
|
if (widget.onData != null)
|
||||||
|
dataSubscription = query!.dataStream.listen(widget.onData);
|
||||||
}
|
}
|
||||||
if (oldWidget.onError != widget.onError) {
|
if (oldWidget.onError != widget.onError) {
|
||||||
errorSubscription?.cancel();
|
errorSubscription?.cancel();
|
||||||
errorSubscription = query!.errorStream.listen(widget.onError);
|
if (widget.onError != null)
|
||||||
|
errorSubscription = query!.errorStream.listen(widget.onError);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,251 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:fl_query/src/collections/default_configs.dart';
|
||||||
|
import 'package:fl_query/src/collections/retry_config.dart';
|
||||||
|
import 'package:fl_query/src/core/client.dart';
|
||||||
|
import 'package:fl_query/src/core/mutation.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/src/foundation/diagnostics.dart';
|
||||||
|
|
||||||
|
typedef MutationBuilderFn<DataType, ErrorType, KeyType, VariablesType> = Widget
|
||||||
|
Function(
|
||||||
|
BuildContext context,
|
||||||
|
Mutation<DataType, ErrorType, KeyType, VariablesType> mutation,
|
||||||
|
);
|
||||||
|
typedef MutationOnDataFn<DataType, RecoveryType> = void Function(
|
||||||
|
DataType data,
|
||||||
|
RecoveryType? recoveryData,
|
||||||
|
);
|
||||||
|
typedef MutationOnErrorFn<ErrorType, RecoveryType> = void Function(
|
||||||
|
ErrorType error,
|
||||||
|
RecoveryType? recoveryData,
|
||||||
|
);
|
||||||
|
typedef MutationOnMutationFn<VariablesType, RecoveryType>
|
||||||
|
= FutureOr<RecoveryType?> Function(
|
||||||
|
VariablesType variables,
|
||||||
|
);
|
||||||
|
|
||||||
|
class MutationBuilder<DataType, ErrorType, KeyType, VariablesType, RecoveryType>
|
||||||
|
extends StatefulWidget {
|
||||||
|
final MutationFn<DataType, VariablesType> mutationFn;
|
||||||
|
final ValueKey<KeyType> mutationKey;
|
||||||
|
|
||||||
|
final RetryConfig retryConfig;
|
||||||
|
|
||||||
|
final MutationOnDataFn<DataType, RecoveryType>? onData;
|
||||||
|
final MutationOnErrorFn<ErrorType, RecoveryType>? onError;
|
||||||
|
final MutationOnMutationFn<VariablesType, RecoveryType>? onMutate;
|
||||||
|
|
||||||
|
// widget specific
|
||||||
|
final MutationBuilderFn<DataType, ErrorType, KeyType, VariablesType> builder;
|
||||||
|
final List<ValueKey>? refreshQueries;
|
||||||
|
final List<ValueKey>? refreshInfiniteQueries;
|
||||||
|
|
||||||
|
const MutationBuilder(
|
||||||
|
this.mutationKey,
|
||||||
|
this.mutationFn, {
|
||||||
|
required this.builder,
|
||||||
|
this.retryConfig = DefaultConstants.retryConfig,
|
||||||
|
this.onData,
|
||||||
|
this.onError,
|
||||||
|
this.onMutate,
|
||||||
|
this.refreshQueries,
|
||||||
|
this.refreshInfiniteQueries,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<
|
||||||
|
MutationBuilder<DataType, ErrorType, KeyType, VariablesType,
|
||||||
|
RecoveryType>> createState() => _MutationBuilderState<DataType,
|
||||||
|
ErrorType, KeyType, VariablesType, RecoveryType>();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MutationBuilderState<DataType, ErrorType, KeyType, VariablesType,
|
||||||
|
RecoveryType>
|
||||||
|
extends State<
|
||||||
|
MutationBuilder<DataType, ErrorType, KeyType, VariablesType,
|
||||||
|
RecoveryType>> {
|
||||||
|
Mutation<DataType, ErrorType, KeyType, VariablesType>? mutation;
|
||||||
|
|
||||||
|
VoidCallback? removeListener;
|
||||||
|
|
||||||
|
StreamSubscription<VariablesType>? mutationSubscription;
|
||||||
|
StreamSubscription<DataType>? dataSubscription;
|
||||||
|
StreamSubscription<ErrorType>? errorSubscription;
|
||||||
|
|
||||||
|
RecoveryType? recoveryData;
|
||||||
|
|
||||||
|
void update(_) {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void subscribeOnMutate() {
|
||||||
|
if (widget.onMutate != null)
|
||||||
|
mutationSubscription = mutation!.mutationStream.listen(
|
||||||
|
(event) async {
|
||||||
|
recoveryData = await widget.onMutate?.call(event);
|
||||||
|
|
||||||
|
if (widget.onData != null) {
|
||||||
|
dataSubscription?.cancel();
|
||||||
|
subscribeOnData();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (widget.onError != null) {
|
||||||
|
errorSubscription?.cancel();
|
||||||
|
subscribeOnError();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void subscribeOnData() {
|
||||||
|
if (widget.onData != null ||
|
||||||
|
widget.refreshInfiniteQueries != null ||
|
||||||
|
widget.refreshQueries != null)
|
||||||
|
dataSubscription = mutation!.dataStream.listen(
|
||||||
|
(event) {
|
||||||
|
final data = widget.onData?.call(event, recoveryData);
|
||||||
|
if (widget.refreshQueries != null && mounted) {
|
||||||
|
QueryClient.of(context).refreshQueries(widget.refreshQueries!);
|
||||||
|
}
|
||||||
|
if (widget.refreshInfiniteQueries != null && mounted) {
|
||||||
|
QueryClient.of(context)
|
||||||
|
.refreshInfiniteQueries(widget.refreshInfiniteQueries!);
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void subscribeOnError() {
|
||||||
|
if (widget.onError != null)
|
||||||
|
errorSubscription = mutation!.errorStream.listen(
|
||||||
|
(event) {
|
||||||
|
return widget.onError?.call(event, recoveryData);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> initialize() async {
|
||||||
|
setState(() {
|
||||||
|
mutation = QueryClient.of(context).createMutation(
|
||||||
|
widget.mutationKey,
|
||||||
|
widget.mutationFn,
|
||||||
|
retryConfig: widget.retryConfig,
|
||||||
|
);
|
||||||
|
subscribeOnMutate();
|
||||||
|
subscribeOnData();
|
||||||
|
subscribeOnError();
|
||||||
|
removeListener = mutation!.addListener(update);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||||
|
await initialize();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
mutationSubscription?.cancel();
|
||||||
|
dataSubscription?.cancel();
|
||||||
|
errorSubscription?.cancel();
|
||||||
|
removeListener?.call();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(
|
||||||
|
MutationBuilder<DataType, ErrorType, KeyType, VariablesType, RecoveryType>
|
||||||
|
oldWidget,
|
||||||
|
) {
|
||||||
|
super.didUpdateWidget(oldWidget);
|
||||||
|
|
||||||
|
if (oldWidget.mutationKey != widget.mutationKey) {
|
||||||
|
mutationSubscription?.cancel();
|
||||||
|
dataSubscription?.cancel();
|
||||||
|
errorSubscription?.cancel();
|
||||||
|
removeListener?.call();
|
||||||
|
initialize();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (oldWidget.mutationFn != widget.mutationFn) {
|
||||||
|
mutation!.updateMutationFn(widget.mutationFn);
|
||||||
|
}
|
||||||
|
if (oldWidget.onMutate != widget.onMutate) {
|
||||||
|
mutationSubscription?.cancel();
|
||||||
|
subscribeOnMutate();
|
||||||
|
}
|
||||||
|
if (oldWidget.onData != widget.onData ||
|
||||||
|
oldWidget.refreshQueries != widget.refreshQueries ||
|
||||||
|
oldWidget.refreshInfiniteQueries != widget.refreshInfiniteQueries) {
|
||||||
|
dataSubscription?.cancel();
|
||||||
|
subscribeOnData();
|
||||||
|
mutationSubscription?.cancel();
|
||||||
|
subscribeOnMutate();
|
||||||
|
}
|
||||||
|
if (oldWidget.onError != widget.onError) {
|
||||||
|
errorSubscription?.cancel();
|
||||||
|
subscribeOnError();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
if (mutation == null) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
return widget.builder(context, mutation!);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||||
|
super.debugFillProperties(properties);
|
||||||
|
properties.add(
|
||||||
|
DiagnosticsProperty<
|
||||||
|
Mutation<DataType, ErrorType, KeyType, VariablesType>>(
|
||||||
|
'mutation', mutation),
|
||||||
|
);
|
||||||
|
properties.add(
|
||||||
|
DiagnosticsProperty<ValueKey<KeyType>>('mutationKey', widget.mutationKey),
|
||||||
|
);
|
||||||
|
properties.add(
|
||||||
|
DiagnosticsProperty<MutationFn<DataType, VariablesType>>(
|
||||||
|
'mutationFn', widget.mutationFn),
|
||||||
|
);
|
||||||
|
properties.add(
|
||||||
|
DiagnosticsProperty<RetryConfig>('retryConfig', widget.retryConfig),
|
||||||
|
);
|
||||||
|
properties.add(
|
||||||
|
DiagnosticsProperty<MutationOnDataFn<DataType, RecoveryType>>(
|
||||||
|
'onData',
|
||||||
|
widget.onData,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
properties.add(
|
||||||
|
DiagnosticsProperty<MutationOnErrorFn<ErrorType, RecoveryType>>(
|
||||||
|
'onError',
|
||||||
|
widget.onError,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
properties.add(
|
||||||
|
DiagnosticsProperty<MutationOnMutationFn<VariablesType, RecoveryType>>(
|
||||||
|
'onMutation',
|
||||||
|
widget.onMutate,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
properties.add(
|
||||||
|
DiagnosticsProperty<
|
||||||
|
MutationBuilderFn<DataType, ErrorType, KeyType, VariablesType>>(
|
||||||
|
'builder',
|
||||||
|
widget.builder,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -77,8 +77,10 @@ class _QueryBuilderState<DataType, ErrorType, KeyType>
|
|||||||
jsonConfig: widget.jsonConfig,
|
jsonConfig: widget.jsonConfig,
|
||||||
);
|
);
|
||||||
|
|
||||||
dataSubscription = query!.dataStream.listen(widget.onData);
|
if (widget.onData != null)
|
||||||
errorSubscription = query!.errorStream.listen(widget.onError);
|
dataSubscription = query!.dataStream.listen(widget.onData);
|
||||||
|
if (widget.onData != null)
|
||||||
|
errorSubscription = query!.errorStream.listen(widget.onError);
|
||||||
|
|
||||||
removeListener = query!.addListener(update);
|
removeListener = query!.addListener(update);
|
||||||
});
|
});
|
||||||
@@ -123,11 +125,13 @@ class _QueryBuilderState<DataType, ErrorType, KeyType>
|
|||||||
}
|
}
|
||||||
if (oldWidget.onData != widget.onData) {
|
if (oldWidget.onData != widget.onData) {
|
||||||
dataSubscription?.cancel();
|
dataSubscription?.cancel();
|
||||||
dataSubscription = query!.dataStream.listen(widget.onData);
|
if (widget.onData != null)
|
||||||
|
dataSubscription = query!.dataStream.listen(widget.onData);
|
||||||
}
|
}
|
||||||
if (oldWidget.onError != widget.onError) {
|
if (oldWidget.onError != widget.onError) {
|
||||||
errorSubscription?.cancel();
|
errorSubscription?.cancel();
|
||||||
errorSubscription = query!.errorStream.listen(widget.onError);
|
if (widget.onError != null)
|
||||||
|
errorSubscription = query!.errorStream.listen(widget.onError);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user