From 9bcdfc9db7da65adbd61e594d7254429468bdcde Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sun, 5 Jun 2022 19:10:33 +0600 Subject: [PATCH] query reusability by QueryJob QueryJob.task External Data Support Query task is now pure function Query.data is non-nullable (even the type) --- packages/example/lib/another_component.dart | 2 +- packages/example/lib/main.dart | 71 +++++++++++++------ .../example/lib/query_with_external_data.dart | 39 ++++++++++ packages/fl_query/lib/fl_query.dart | 21 ++---- packages/fl_query/lib/models/query_job.dart | 26 +++++++ packages/fl_query/lib/query.dart | 44 ++++++++++-- packages/fl_query/lib/query_bowl.dart | 39 ++++++---- packages/fl_query/lib/query_builder.dart | 56 +++++++-------- 8 files changed, 209 insertions(+), 89 deletions(-) create mode 100644 packages/example/lib/query_with_external_data.dart create mode 100644 packages/fl_query/lib/models/query_job.dart diff --git a/packages/example/lib/another_component.dart b/packages/example/lib/another_component.dart index 24200e3..93c1e54 100644 --- a/packages/example/lib/another_component.dart +++ b/packages/example/lib/another_component.dart @@ -6,7 +6,7 @@ class AnotherComponent extends StatelessWidget { @override Widget build(BuildContext context) { - final lol = QueryBowl.of(context).getQuery("greetings"); + final lol = QueryBowl.of(context).getQuery("greetings"); if (lol?.data == null) return const CircularProgressIndicator(); return Text("${lol!.data!} from AnotherComponent"); } diff --git a/packages/example/lib/main.dart b/packages/example/lib/main.dart index a5403ba..9d3aca5 100644 --- a/packages/example/lib/main.dart +++ b/packages/example/lib/main.dart @@ -1,8 +1,8 @@ import 'dart:math'; import 'package:example/another_component.dart'; -import 'package:fl_query/query_bowl.dart'; -import 'package:fl_query/query_builder.dart'; +import 'package:example/query_with_external_data.dart'; +import 'package:fl_query/fl_query.dart'; import 'package:flutter/material.dart'; import 'dart:async'; @@ -15,19 +15,29 @@ class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { - return MaterialApp( - title: 'Flutter Demo', - theme: ThemeData( - primarySwatch: Colors.blue, - ), - home: const QueryBowlScope( - staleTime: Duration(seconds: 10), - child: MyHomePage(), + return QueryBowlScope( + child: MaterialApp( + title: 'Flutter Demo', + theme: ThemeData( + useMaterial3: true, + primarySwatch: Colors.blue, + ), + home: const MyHomePage(), ), ); } } +final successJob = QueryJob( + queryKey: "greetings", + task: (queryKey, _) => Future.delayed(const Duration(seconds: 2), + () => "Welcome ($queryKey) ${Random.secure().nextInt(100)}")); + +final failedJob = QueryJob( + queryKey: "failure", + task: (queryKey, _) => Future.value("[$queryKey] Failed for unknown reason"), +); + class MyHomePage extends StatefulWidget { const MyHomePage({Key? key}) : super(key: key); @@ -46,12 +56,9 @@ class _MyHomePageState extends State { children: [ Row( children: [ - QueryBuilder( - queryKey: "greetings", - task: (queryKey) => Future.delayed( - const Duration(seconds: 2), - () => - "Welcome ($queryKey) ${Random.secure().nextInt(100)}"), + QueryBuilder( + job: successJob, + externalData: null, builder: (context, query) { if (query.isLoading || query.isRefetching) { return const CircularProgressIndicator(); @@ -64,10 +71,24 @@ class _MyHomePageState extends State { ); }, ), - QueryBuilder( - queryKey: "failure", - task: (queryKey) => - Future.value("[$queryKey] Failed for unknown reason"), + QueryBuilder( + job: successJob, + externalData: null, + builder: (context, query) { + if (query.isLoading || query.isRefetching) { + return const CircularProgressIndicator(); + } + return ElevatedButton( + child: Text(query.data!), + onPressed: () async { + await query.refetch(); + }, + ); + }, + ), + QueryBuilder( + job: failedJob, + externalData: null, builder: (context, query) { if (query.hasError) return Text(query.error); return Text("Failure. You're a failure ${query.data}"); @@ -75,6 +96,16 @@ class _MyHomePageState extends State { ), ], ), + ElevatedButton( + child: const Text("External Data Example"), + onPressed: () { + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => const QueryWithExternalData(), + ), + ); + }, + ), const AnotherComponent(), ], ), diff --git a/packages/example/lib/query_with_external_data.dart b/packages/example/lib/query_with_external_data.dart new file mode 100644 index 0000000..b0c6ad0 --- /dev/null +++ b/packages/example/lib/query_with_external_data.dart @@ -0,0 +1,39 @@ +import 'dart:math'; + +import 'package:fl_query/fl_query.dart'; +import 'package:flutter/material.dart'; + +final jobWithExternalData = QueryJob( + queryKey: "external_data", + task: (queryKey, data) { + return Future.delayed(const Duration(milliseconds: 500), + () => "Hello from $queryKey with $data"); + }); + +class QueryWithExternalData extends StatelessWidget { + const QueryWithExternalData({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(), + body: QueryBuilder( + job: jobWithExternalData, + externalData: (Random().nextDouble() * 200).toString(), + builder: (context, query) { + if (query.isLoading || query.isLoading || query.data == null) { + return const CircularProgressIndicator(); + } + return Container( + width: double.parse(query.externalData), + height: double.parse(query.externalData), + decoration: const BoxDecoration( + shape: BoxShape.circle, + color: Colors.blue, + ), + ); + }, + ), + ); + } +} diff --git a/packages/fl_query/lib/fl_query.dart b/packages/fl_query/lib/fl_query.dart index fd3a379..20231e8 100644 --- a/packages/fl_query/lib/fl_query.dart +++ b/packages/fl_query/lib/fl_query.dart @@ -1,19 +1,6 @@ library fl_query; -import 'package:flutter/material.dart'; - -class FlQueryScope extends StatefulWidget { - final Widget child; - const FlQueryScope({required this.child, Key? key}) : super(key: key); - - @override - State createState() => _FlQueryScopeState(); -} - -class _FlQueryScopeState extends State { - @override - Widget build(BuildContext context) { - Theme.of(context); - return widget.child; - } -} +export 'query.dart'; +export 'query_bowl.dart'; +export 'query_builder.dart'; +export 'models/query_job.dart'; diff --git a/packages/fl_query/lib/models/query_job.dart b/packages/fl_query/lib/models/query_job.dart new file mode 100644 index 0000000..27ab846 --- /dev/null +++ b/packages/fl_query/lib/models/query_job.dart @@ -0,0 +1,26 @@ +import 'package:fl_query/query.dart'; + +class QueryJob { + // all params + final String queryKey; + QueryTaskFunction task; + final int? retries; + final Duration? retryDelay; + final T? initialData; + + // got from global options + final Duration? staleTime; + + final QueryListener? onData; + final QueryListener? onError; + QueryJob({ + required this.queryKey, + required this.task, + this.retries, + this.retryDelay, + this.initialData, + this.staleTime, + this.onData, + this.onError, + }); +} diff --git a/packages/fl_query/lib/query.dart b/packages/fl_query/lib/query.dart index 8c2308d..32f0b19 100644 --- a/packages/fl_query/lib/query.dart +++ b/packages/fl_query/lib/query.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:fl_query/models/query_job.dart'; import 'package:flutter/widgets.dart'; enum QueryStatus { @@ -9,16 +10,16 @@ enum QueryStatus { refetching; } -typedef QueryTaskFunction = FutureOr Function(String); +typedef QueryTaskFunction = FutureOr Function(String, Outside); typedef QueryListener = FutureOr Function(T); typedef ListenerUnsubscriber = void Function(); -class Query extends ChangeNotifier { +class Query extends ChangeNotifier { // all params final String queryKey; - QueryTaskFunction task; + QueryTaskFunction task; final int retries; final Duration retryDelay; final T? _initialData; @@ -40,10 +41,17 @@ class Query extends ChangeNotifier { final QueryListener? _onData; final QueryListener? _onError; + // externalData will always be passed to the task Callback + // it will change based on the presence of QueryBuilder + Outside _externalData; + + Outside? _prevUsedExternalData; + Query({ required this.queryKey, required this.task, required Duration staleTime, + required Outside externalData, required this.retries, required this.retryDelay, T? initialData, @@ -52,11 +60,27 @@ class Query extends ChangeNotifier { }) : status = QueryStatus.pending, _staleTime = staleTime, _initialData = initialData, + _externalData = externalData, data = initialData, _onData = onData, _onError = onError, updatedAt = DateTime.now(); + Query.fromOptions(QueryJob options, + {required Outside externalData}) + : queryKey = options.queryKey, + task = options.task, + retries = options.retries ?? 3, + retryDelay = options.retryDelay ?? const Duration(milliseconds: 200), + _staleTime = options.staleTime ?? const Duration(days: 1), + _initialData = options.initialData, + _externalData = externalData, + _onData = options.onData, + _onError = options.onError, + data = options.initialData, + status = QueryStatus.pending, + updatedAt = DateTime.now(); + // all getters & setters bool get hasData => data != null && error == null; bool get hasError => @@ -66,6 +90,8 @@ class Query extends ChangeNotifier { bool get isRefetching => status == QueryStatus.refetching && (data != null || error != null); bool get isSucceeded => status == QueryStatus.succeed && data != null; + Outside get externalData => _externalData; + Outside? get prevUsedExternalData => _prevUsedExternalData; // all methods @@ -74,7 +100,8 @@ class Query extends ChangeNotifier { Future _execute() async { try { retryAttempts = 0; - data = await task(queryKey); + data = await task(queryKey, _externalData); + _prevUsedExternalData = _externalData; updatedAt = DateTime.now(); status = QueryStatus.succeed; _onData?.call(data!); @@ -90,7 +117,8 @@ class Query extends ChangeNotifier { while (retryAttempts <= retries) { await Future.delayed(retryDelay); try { - data = await task(queryKey); + data = await task(queryKey, _externalData); + _prevUsedExternalData = _externalData; status = QueryStatus.succeed; _onData?.call(data!); notifyListeners(); @@ -149,6 +177,10 @@ class Query extends ChangeNotifier { notifyListeners(); } + setExternalData(Outside externalData) { + _externalData = externalData; + } + void reset() { refetchCount = 0; data = _initialData; @@ -163,4 +195,6 @@ class Query extends ChangeNotifier { // the data has become stale return DateTime.now().isAfter(updatedAt.add(_staleTime)); } + + A? cast() => this is A ? this as A : null; } diff --git a/packages/fl_query/lib/query_bowl.dart b/packages/fl_query/lib/query_bowl.dart index c981909..348f2c6 100644 --- a/packages/fl_query/lib/query_bowl.dart +++ b/packages/fl_query/lib/query_bowl.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:fl_query/models/query_job.dart'; import 'package:fl_query/query.dart'; import 'package:collection/collection.dart'; import 'package:flutter/widgets.dart'; @@ -73,7 +74,7 @@ class _QueryBowlScopeState extends State { }); } - void addQuery(Query query) { + void addQuery(Query query) { setState(() { queries = Set.from({...queries, query}); }); @@ -81,9 +82,9 @@ class _QueryBowlScopeState extends State { @override Widget build(BuildContext context) { + _disposeListeners(); _listenToQueryUpdate(); return QueryBowl( - onUpdate: updateQueries, addQuery: addQuery, queries: queries, staleTime: widget.staleTime, @@ -98,12 +99,14 @@ class QueryBowl extends InheritedWidget { final Set _queries; final Duration staleTime; - final void Function(Query query) _addQuery; + final void Function(Query query) + _addQuery; const QueryBowl({ required Widget child, - required final void Function() onUpdate, - required final void Function(Query query) addQuery, + required final void Function( + Query query) + addQuery, required final Set queries, required this.staleTime, Key? key, @@ -111,25 +114,33 @@ class QueryBowl extends InheritedWidget { _queries = queries, super(child: child, key: key); - Future fetchQuery(Query query) async { + Future fetchQuery(QueryJob options, + {required Outside externalData}) async { final prevQuery = - _queries.firstWhereOrNull((q) => q.queryKey == query.queryKey); - if (prevQuery is Query) { - if (!prevQuery.hasData) { + _queries.firstWhereOrNull((q) => q.queryKey == options.queryKey); + if (prevQuery is Query) { + // run the query if its still not called or if externalData has + // changed + final hasExternalDataChanged = + prevQuery.prevUsedExternalData != externalData; + if (!prevQuery.hasData || hasExternalDataChanged) { + if (hasExternalDataChanged) prevQuery.setExternalData(externalData); return prevQuery.fetched ? await prevQuery.refetch() : await prevQuery.fetch(); } return prevQuery.data; } - _addQuery(query); + final query = + Query.fromOptions(options, externalData: externalData); + _addQuery(query); return await query.fetch(); } - Query? getQuery(String queryKey) { - return _queries.firstWhereOrNull( - (query) => query.queryKey == queryKey && query is Query) - as Query?; + Query? getQuery(String queryKey) { + return _queries.firstWhereOrNull((query) { + return query.queryKey == queryKey && query is Query; + })?.cast>(); } int get isFetching { diff --git a/packages/fl_query/lib/query_builder.dart b/packages/fl_query/lib/query_builder.dart index 6522908..7b9a9fb 100644 --- a/packages/fl_query/lib/query_builder.dart +++ b/packages/fl_query/lib/query_builder.dart @@ -1,57 +1,49 @@ +import 'package:fl_query/models/query_job.dart'; import 'package:fl_query/query.dart'; import 'package:fl_query/query_bowl.dart'; import 'package:flutter/widgets.dart'; -class QueryBuilder extends StatefulWidget { - final Widget Function(BuildContext, Query) builder; - final QueryTaskFunction task; - final String queryKey; - final Duration? staleTime; - final int retries; - final T? initialData; - final Duration retryDelay; - - final QueryListener? onData; - final QueryListener? onError; +class QueryBuilder extends StatefulWidget { + final Function(BuildContext, Query) builder; + final QueryJob job; + final Outside externalData; const QueryBuilder({ + required this.job, + required this.externalData, required this.builder, - required this.task, - required this.queryKey, - this.initialData, - this.staleTime, - this.retryDelay = const Duration(milliseconds: 200), - this.retries = 3, - this.onData, - this.onError, Key? key, }) : super(key: key); @override - State> createState() => _QueryBuilderState(); + State> createState() => + _QueryBuilderState(); } -class _QueryBuilderState extends State> { +class _QueryBuilderState + extends State> { @override void initState() { super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) async { - await QueryBowl.of(context).fetchQuery(Query( - queryKey: widget.queryKey, - task: widget.task, - staleTime: widget.staleTime ?? QueryBowl.of(context).staleTime, - retries: widget.retries, - initialData: widget.initialData, - retryDelay: widget.retryDelay, - onData: widget.onData, - onError: widget.onError, - )); + await QueryBowl.of(context).fetchQuery(widget.job, + externalData: widget.externalData); }); } + @override + void didUpdateWidget(covariant oldWidget) { + if (oldWidget.externalData != widget.externalData) { + QueryBowl.of(context) + .fetchQuery(widget.job, externalData: widget.externalData); + } + super.didUpdateWidget(oldWidget); + } + @override Widget build(BuildContext context) { - final query = QueryBowl.of(context).getQuery(widget.queryKey); + final query = + QueryBowl.of(context).getQuery(widget.job.queryKey); if (query == null) return Container(); return widget.builder(context, query); }