diff --git a/packages/example/lib/another_component.dart b/packages/example/lib/another_component.dart deleted file mode 100644 index f068388..0000000 --- a/packages/example/lib/another_component.dart +++ /dev/null @@ -1,17 +0,0 @@ -import 'package:fl_query/fl_query.dart'; -import 'package:flutter/material.dart'; - -class AnotherComponent extends StatelessWidget { - const AnotherComponent({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - final lol = QueryBowl.of(context).getQuery("greetings"); - final deadQuery = - QueryBowl.of(context).getQuery("external_data"); - if (lol?.data == null) return const CircularProgressIndicator(); - return Text( - "${lol!.data!} from AnotherComponent\nDeadQuery (It should be null after 10 seconds): ${deadQuery?.data}", - ); - } -} diff --git a/packages/example/lib/components/basic_mutation.dart b/packages/example/lib/components/basic_mutation.dart new file mode 100644 index 0000000..03c8078 --- /dev/null +++ b/packages/example/lib/components/basic_mutation.dart @@ -0,0 +1,115 @@ +import 'dart:convert'; +import 'dart:math'; + +import 'package:example/components/basic_query.dart'; +import 'package:http/http.dart' as http; + +import 'package:fl_query/fl_query.dart'; +import 'package:flutter/material.dart'; + +final basicMutationJob = MutationJob>( + mutationKey: "basic-mutation-example", + task: (key, data) async { + final response = await http.post( + Uri.parse( + // to simulate a failing response environment + Random().nextBool() + ? "https://jsonplaceholder.typicode.com/posts" + : "https://google.com", + ), + headers: {'Content-type': 'application/json; charset=UTF-8'}, + body: jsonEncode(data), + ); + return jsonDecode(response.body); + }, +); + +class BasicMutationExample extends StatefulWidget { + const BasicMutationExample({Key? key}) : super(key: key); + + @override + State createState() => _BasicMutationExampleState(); +} + +class _BasicMutationExampleState extends State { + late TextEditingController titleController; + late TextEditingController bodyController; + late int id; + @override + void initState() { + super.initState(); + id = Random().nextInt(2000000); + titleController = TextEditingController(); + bodyController = TextEditingController(); + } + + @override + void dispose() { + titleController.dispose(); + bodyController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "# Basic Mutation Example", + style: Theme.of(context).textTheme.headline5, + ), + MutationBuilder>( + job: basicMutationJob, + onMutate: (v) { + QueryBowl.of(context) + .setQueryData(successJob.queryKey, (oldData) { + if (oldData?.contains("After Mutate (OPTIMISTIC UPDATE)") == + true) { + return "$oldData"; + } + return "$oldData - After Mutate (OPTIMISTIC UPDATE)"; + }); + }, + builder: (context, mutation) { + return Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + children: [ + TextField( + controller: titleController, + decoration: const InputDecoration(labelText: "Title"), + ), + TextField( + controller: bodyController, + decoration: const InputDecoration(labelText: "Body"), + ), + const SizedBox(height: 20), + ElevatedButton( + onPressed: () { + final title = titleController.value.text; + final body = bodyController.value.text; + if (body.isEmpty || title.isEmpty) return; + mutation.mutate({ + "title": title, + "body": body, + "id": id, + }, onData: (data) { + // resetting the form + titleController.text = ""; + bodyController.text = ""; + }); + }, + child: const Text("Post"), + ), + const SizedBox(height: 20), + if (mutation.hasData) Text("Response\n${mutation.data}"), + if (mutation.hasError) Text(mutation.error.toString()), + ], + ), + ); + }), + ], + ); + } +} diff --git a/packages/example/lib/components/basic_query.dart b/packages/example/lib/components/basic_query.dart new file mode 100644 index 0000000..341c8f2 --- /dev/null +++ b/packages/example/lib/components/basic_query.dart @@ -0,0 +1,86 @@ +import 'dart:math'; + +import 'package:fl_query/fl_query.dart'; +import 'package:flutter/material.dart'; + +final successJob = QueryJob( + queryKey: "greetings-example", + task: (queryKey, _, __) => Future.delayed( + const Duration(seconds: 2), + () => + "The work successfully executed. Data: key=($queryKey) value=${Random.secure().nextInt(100)}", + ), +); + +final canFailJob = QueryJob( + queryKey: "failure-example", + task: (queryKey, _, __) => Random().nextBool() + ? Future.error("$queryKey operation failed for unknown reason") + : Future.value( + "Successful execution. Result: $queryKey=${Random().nextInt(100)}", + ), +); + +class BasicQueryExample extends StatelessWidget { + const BasicQueryExample({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "# Basic Query Example", + style: Theme.of(context).textTheme.headline5, + ), + QueryBuilder( + job: successJob, + externalData: null, + builder: (context, query) { + if (!query.hasData || query.isLoading || query.isRefetching) { + return const CircularProgressIndicator(); + } + return Row( + children: [ + Text(query.data!), + ElevatedButton( + child: const Text("Refetch"), + onPressed: () async { + await query.refetch(); + }, + ), + ], + ); + }, + ), + QueryBuilder( + job: canFailJob, + externalData: null, + builder: (context, query) { + if (!query.hasData || query.isLoading || query.isRefetching) { + return const CircularProgressIndicator(); + } + return Row( + children: [ + if (query.hasError) + Text( + "${query.error}. Retrying: ${query.retryAttempts}", + ), + if (query.hasData) + Text( + "Success after ${query.retryAttempts}\nData: ${query.data}", + ), + ElevatedButton( + child: const Text("Refetch"), + onPressed: () async { + await query.refetch(); + }, + ), + ], + ); + }, + ), + ], + ); + } +} diff --git a/packages/example/lib/components/hooks/basic_hook_mutation.dart b/packages/example/lib/components/hooks/basic_hook_mutation.dart new file mode 100644 index 0000000..6e6541f --- /dev/null +++ b/packages/example/lib/components/hooks/basic_hook_mutation.dart @@ -0,0 +1,94 @@ +import 'dart:convert'; +import 'dart:math'; + +import 'package:example/components/hooks/basic_hook_query.dart'; +import 'package:fl_query/fl_query_hooks.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:http/http.dart' as http; + +import 'package:fl_query/fl_query.dart'; +import 'package:flutter/material.dart'; + +final basicMutationHookJob = MutationJob>( + mutationKey: "basic-hook-mutation-example", + task: (key, data) async { + final response = await http.post( + Uri.parse( + // to simulate a failing response environment + Random().nextBool() + ? "https://jsonplaceholder.typicode.com/posts" + : "https://google.com", + ), + headers: {'Content-type': 'application/json; charset=UTF-8'}, + body: jsonEncode(data), + ); + return jsonDecode(response.body); + }, +); + +class BasicHookMutationExample extends HookWidget { + const BasicHookMutationExample({super.key}); + @override + Widget build(BuildContext context) { + final id = useMemoized(() => Random().nextInt(2000000), []); + final titleController = useTextEditingController(); + final bodyController = useTextEditingController(); + final mutation = useMutation( + job: basicMutationHookJob, + onMutate: (v) { + QueryBowl.of(context) + .setQueryData(successHookJob.queryKey, (oldData) { + if (oldData?.contains("After Mutate (OPTIMISTIC UPDATE)") == true) { + return "$oldData"; + } + return "$oldData - After Mutate (OPTIMISTIC UPDATE)"; + }); + }, + ); + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "# Basic Mutation Hook Example", + style: Theme.of(context).textTheme.headline5, + ), + Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + children: [ + TextField( + controller: titleController, + decoration: const InputDecoration(labelText: "Title"), + ), + TextField( + controller: bodyController, + decoration: const InputDecoration(labelText: "Body"), + ), + const SizedBox(height: 20), + ElevatedButton( + onPressed: () { + final title = titleController.value.text; + final body = bodyController.value.text; + if (body.isEmpty || title.isEmpty) return; + mutation.mutate({ + "title": title, + "body": body, + "id": id, + }, onData: (data) { + // resetting the form + titleController.text = ""; + bodyController.text = ""; + }); + }, + child: const Text("Post"), + ), + const SizedBox(height: 20), + if (mutation.hasData) Text("Response\n${mutation.data}"), + if (mutation.hasError) Text(mutation.error.toString()), + ], + ), + ) + ], + ); + } +} diff --git a/packages/example/lib/components/hooks/basic_hook_query.dart b/packages/example/lib/components/hooks/basic_hook_query.dart new file mode 100644 index 0000000..05cf3e0 --- /dev/null +++ b/packages/example/lib/components/hooks/basic_hook_query.dart @@ -0,0 +1,80 @@ +import 'dart:math'; + +import 'package:fl_query/fl_query.dart'; +import 'package:fl_query/fl_query_hooks.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; + +final successHookJob = QueryJob( + queryKey: "greetings-hook-example", + task: (queryKey, _, __) => Future.delayed( + const Duration(seconds: 2), + () => + "The work successfully executed. Data: key=($queryKey) value=${Random.secure().nextInt(100)}", + ), +); + +final canFailHookJob = QueryJob( + queryKey: "failure-hook-example", + task: (queryKey, _, __) => Random().nextBool() + ? Future.error("$queryKey operation failed for unknown reason") + : Future.value( + "Successful execution. Result: $queryKey=${Random().nextInt(100)}", + ), +); + +class BasicHookQueryExample extends HookWidget { + const BasicHookQueryExample({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final successQuery = useQuery(job: successHookJob, externalData: null); + final canFailQuery = useQuery(job: canFailHookJob, externalData: null); + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "# Basic Query Hook Example", + style: Theme.of(context).textTheme.headline5, + ), + !successQuery.hasData || + successQuery.isLoading || + successQuery.isRefetching + ? const CircularProgressIndicator() + : Row( + children: [ + Text(successQuery.data!), + ElevatedButton( + child: const Text("Refetch"), + onPressed: () async { + await successQuery.refetch(); + }, + ), + ], + ), + !canFailQuery.hasData || + canFailQuery.isLoading || + canFailQuery.isRefetching + ? const CircularProgressIndicator() + : Row( + children: [ + if (canFailQuery.hasError) + Text( + "${canFailQuery.error}. Retrying: ${canFailQuery.retryAttempts}", + ), + if (canFailQuery.hasData) + Text( + "Success after ${canFailQuery.retryAttempts}\nData: ${canFailQuery.data}", + ), + ElevatedButton( + child: const Text("Refetch"), + onPressed: () async { + await canFailQuery.refetch(); + }, + ), + ], + ) + ], + ); + } +} diff --git a/packages/example/lib/components/hooks/lazy_hook_query.dart b/packages/example/lib/components/hooks/lazy_hook_query.dart new file mode 100644 index 0000000..7176e98 --- /dev/null +++ b/packages/example/lib/components/hooks/lazy_hook_query.dart @@ -0,0 +1,43 @@ +import 'package:fl_query/fl_query.dart'; +import 'package:fl_query/fl_query_hooks.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; + +final lazyHookQueryJob = QueryJob( + queryKey: "lazy-hook-query", + enabled: false, + task: (queryKey, data, _) { + return Future.delayed(const Duration(milliseconds: 500), + () => "Result: key=$queryKey value=$data"); + }, +); + +class LazyHookQueryExample extends HookWidget { + const LazyHookQueryExample({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final query = useQuery( + job: lazyHookQueryJob, + externalData: "Love", + ); + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "# Lazy Hook Query Example", + style: Theme.of(context).textTheme.headline5, + ), + Row( + children: [ + Text("Current Data: ${query.data ?? "Loading"}"), + ElevatedButton( + child: const Text("Refetch Query"), + onPressed: () => query.refetch(), + ), + ], + ), + ], + ); + } +} diff --git a/packages/example/lib/components/hooks/mutation_hook_variable_key.dart b/packages/example/lib/components/hooks/mutation_hook_variable_key.dart new file mode 100644 index 0000000..14a1795 --- /dev/null +++ b/packages/example/lib/components/hooks/mutation_hook_variable_key.dart @@ -0,0 +1,49 @@ +import 'dart:math'; + +import 'package:fl_query/fl_query.dart'; +import 'package:fl_query/fl_query_hooks.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; + +final mutationHookVariableKeyJob = MutationJob.withVariableKey( + task: (queryKey, variables) { + return Future.value("$variables"); + }, +); + +class MutationHookVariableKeyExample extends HookWidget { + const MutationHookVariableKeyExample({Key? key}) : super(key: key); + @override + Widget build(BuildContext context) { + final id = useState(Random().nextDouble()); + final mutation = useMutation( + job: mutationHookVariableKeyJob("mutation-hook-variable-key#${id.value}"), + ); + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "# Mutation Hook Variable Key Example", + style: Theme.of(context).textTheme.headline5, + ), + Row( + children: [ + Text("${mutation.mutationKey} Result: ${mutation.data}"), + ElevatedButton( + child: const Text("Generate Random Data"), + onPressed: () { + mutation.mutate(Random().nextDouble()); + }, + ), + ElevatedButton( + child: const Text("New Mutation"), + onPressed: () { + id.value = Random().nextDouble(); + }, + ), + ], + ), + ], + ); + } +} diff --git a/packages/example/lib/components/hooks/query_hook_external_data.dart b/packages/example/lib/components/hooks/query_hook_external_data.dart new file mode 100644 index 0000000..b8b2a96 --- /dev/null +++ b/packages/example/lib/components/hooks/query_hook_external_data.dart @@ -0,0 +1,58 @@ +import 'dart:math'; + +import 'package:fl_query/fl_query.dart'; +import 'package:fl_query/fl_query_hooks.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; + +final queryHookExternalDataJob = QueryJob( + queryKey: "query-hook-external-data", + cacheTime: const Duration(seconds: 10), + task: (queryKey, data, _) { + return Future.delayed(const Duration(milliseconds: 500), + () => "Hello from $queryKey with $data"); + }, +); + +class QueryHookExternalDataExample extends HookWidget { + const QueryHookExternalDataExample({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final externalData = useState(Random().nextDouble() * 200); + final query = useQuery( + job: queryHookExternalDataJob, + externalData: externalData.value, + ); + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "# Query Hook With External Data Example", + style: Theme.of(context).textTheme.headline5, + ), + query.isLoading || query.isRefetching || !query.hasData + ? const CircularProgressIndicator() + : Row( + children: [ + Container( + width: query.externalData, + height: query.externalData, + decoration: const BoxDecoration( + shape: BoxShape.circle, + color: Colors.blue, + ), + child: Center(child: Text(query.externalData.toString())), + ), + ElevatedButton( + child: const Text("New Id"), + onPressed: () { + externalData.value = Random().nextDouble() * 200; + }, + ) + ], + ) + ], + ); + } +} diff --git a/packages/example/lib/components/hooks/query_hook_variable_key.dart b/packages/example/lib/components/hooks/query_hook_variable_key.dart new file mode 100644 index 0000000..0868309 --- /dev/null +++ b/packages/example/lib/components/hooks/query_hook_variable_key.dart @@ -0,0 +1,49 @@ +import 'dart:math'; + +import 'package:fl_query/fl_query.dart'; +import 'package:fl_query/fl_query_hooks.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; + +final queryHookVariableKeyJob = QueryJob.withVariableKey( + task: (queryKey, externalData, query) { + return Future.delayed( + const Duration(milliseconds: 500), + () => "QueryKey:${queryKey.split("#").last}", + ); +}); + +class QueryHookVariableKeyExample extends HookWidget { + const QueryHookVariableKeyExample({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final id = useState(Random().nextDouble() * 200); + final query = useQuery( + job: queryHookVariableKeyJob("hook-variable-query#${id.value}"), + externalData: null, + ); + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "# Query Hook Variable Example", + style: Theme.of(context).textTheme.headline5, + ), + query.isLoading || query.isRefetching || !query.hasData + ? const CircularProgressIndicator() + : Row( + children: [ + Text("Query Result: ${query.data}"), + ElevatedButton( + child: const Text("New Id"), + onPressed: () { + id.value = Random().nextDouble() * 200; + }, + ), + ], + ) + ], + ); + } +} diff --git a/packages/example/lib/components/lazy_query.dart b/packages/example/lib/components/lazy_query.dart new file mode 100644 index 0000000..5f64734 --- /dev/null +++ b/packages/example/lib/components/lazy_query.dart @@ -0,0 +1,43 @@ +import 'package:fl_query/fl_query.dart'; +import 'package:flutter/material.dart'; + +final lazyQueryJob = QueryJob( + queryKey: "lazy-query", + enabled: false, + task: (queryKey, data, _) { + return Future.delayed(const Duration(milliseconds: 500), + () => "Result: key=$queryKey value=$data"); + }, +); + +class LazyQueryExample extends StatelessWidget { + const LazyQueryExample({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "# Lazy Query Example", + style: Theme.of(context).textTheme.headline5, + ), + QueryBuilder( + job: lazyQueryJob, + externalData: "Love", + builder: (context, query) { + return Row( + children: [ + Text("Current Data: ${query.data ?? "Loading"}"), + ElevatedButton( + child: const Text("Refetch Query"), + onPressed: () => query.refetch(), + ), + ], + ); + }, + ), + ], + ); + } +} diff --git a/packages/example/lib/components/mutation_variable_key.dart b/packages/example/lib/components/mutation_variable_key.dart new file mode 100644 index 0000000..ff11fb5 --- /dev/null +++ b/packages/example/lib/components/mutation_variable_key.dart @@ -0,0 +1,66 @@ +import 'dart:math'; + +import 'package:fl_query/fl_query.dart'; +import 'package:flutter/material.dart'; + +final mutationVariableKeyJob = MutationJob.withVariableKey( + task: (queryKey, variables) { + return Future.value("$variables"); + }, +); + +class MutationVariableKeyExample extends StatefulWidget { + const MutationVariableKeyExample({Key? key}) : super(key: key); + + @override + State createState() => + _MutationVariableKeyExampleState(); +} + +class _MutationVariableKeyExampleState + extends State { + late double id; + + @override + void initState() { + super.initState(); + id = Random().nextDouble(); + } + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "# Mutation Variable Key Example", + style: Theme.of(context).textTheme.headline5, + ), + MutationBuilder( + job: mutationVariableKeyJob("mutation-variable-key#$id"), + builder: (context, mutation) { + return Row( + children: [ + Text("${mutation.mutationKey} Result: ${mutation.data}"), + ElevatedButton( + child: const Text("Generate Random Data"), + onPressed: () { + mutation.mutate(Random().nextDouble()); + }, + ), + ElevatedButton( + child: const Text("New Mutation"), + onPressed: () { + setState(() { + id = Random().nextDouble(); + }); + }, + ), + ], + ); + }, + ) + ], + ); + } +} diff --git a/packages/example/lib/components/query_external_data.dart b/packages/example/lib/components/query_external_data.dart new file mode 100644 index 0000000..0c3cce6 --- /dev/null +++ b/packages/example/lib/components/query_external_data.dart @@ -0,0 +1,74 @@ +import 'dart:math'; + +import 'package:fl_query/fl_query.dart'; +import 'package:flutter/material.dart'; + +final jobWithExternalData = QueryJob( + queryKey: "query-external-data", + cacheTime: const Duration(seconds: 10), + task: (queryKey, data, _) { + return Future.delayed(const Duration(milliseconds: 500), + () => "Hello from $queryKey with $data"); + }, +); + +class QueryExternalDataExample extends StatefulWidget { + const QueryExternalDataExample({Key? key}) : super(key: key); + + @override + State createState() => + _QueryExternalDataExampleState(); +} + +class _QueryExternalDataExampleState extends State { + late double externalData; + + @override + void initState() { + super.initState(); + externalData = Random().nextDouble() * 200; + } + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "# Query With External Data Example", + style: Theme.of(context).textTheme.headline5, + ), + QueryBuilder( + job: jobWithExternalData, + externalData: externalData, + builder: (context, query) { + if (query.isLoading || query.isRefetching || !query.hasData) { + return const CircularProgressIndicator(); + } + return Row( + children: [ + Container( + width: query.externalData, + height: query.externalData, + decoration: const BoxDecoration( + shape: BoxShape.circle, + color: Colors.blue, + ), + child: Center(child: Text(query.externalData.toString())), + ), + ElevatedButton( + child: const Text("New Id"), + onPressed: () { + setState(() { + externalData = Random().nextDouble() * 200; + }); + }, + ) + ], + ); + }, + ), + ], + ); + } +} diff --git a/packages/example/lib/components/query_variable_key.dart b/packages/example/lib/components/query_variable_key.dart new file mode 100644 index 0000000..039fba2 --- /dev/null +++ b/packages/example/lib/components/query_variable_key.dart @@ -0,0 +1,64 @@ +import 'dart:math'; + +import 'package:fl_query/fl_query.dart'; +import 'package:flutter/material.dart'; + +final queryVariableKeyJob = QueryJob.withVariableKey( + task: (queryKey, externalData, query) { + return Future.delayed( + const Duration(milliseconds: 500), + () => "QueryKey:${queryKey.split("#").last}", + ); +}); + +class QueryVariableKeyExample extends StatefulWidget { + const QueryVariableKeyExample({Key? key}) : super(key: key); + + @override + State createState() => + _QueryVariableKeyExampleState(); +} + +class _QueryVariableKeyExampleState extends State { + late double id; + @override + void initState() { + super.initState(); + id = Random().nextDouble() * 200; + } + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "# Query Variable Example", + style: Theme.of(context).textTheme.headline5, + ), + QueryBuilder( + job: queryVariableKeyJob("variable-query#$id"), + externalData: null, + builder: (context, query) { + if (query.isLoading || query.isRefetching || !query.hasData) { + return const CircularProgressIndicator(); + } + return Row( + children: [ + Text("Query Result: ${query.data}"), + ElevatedButton( + child: const Text("New Id"), + onPressed: () { + setState(() { + id = Random().nextDouble() * 200; + }); + }, + ), + ], + ); + }, + ), + ], + ); + } +} diff --git a/packages/example/lib/dependent_query_example.dart b/packages/example/lib/dependent_query_example.dart deleted file mode 100644 index 3705e5d..0000000 --- a/packages/example/lib/dependent_query_example.dart +++ /dev/null @@ -1,36 +0,0 @@ -import 'package:example/main.dart'; -import 'package:fl_query/fl_query.dart'; -import 'package:flutter/material.dart'; - -final dependentQueryJob = QueryJob( - queryKey: "dependent-query-job", - task: (queryKey, externalData, query) { - final successQuery = - query.dependOnQuery(successJob, externalData: null); - final failedQuery = - query.dependOnQuery(failedJob, externalData: null); - if (failedQuery.hasError) return failedQuery.error.toString(); - if (successQuery.hasData) return successQuery.data!; - return "No data from success query yet"; - }); - -class DependentQueryExample extends StatelessWidget { - const DependentQueryExample({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar(), - body: QueryBuilder( - job: dependentQueryJob, - externalData: null, - builder: (context, query) { - if (query.isLoading || query.isRefetching || !query.hasData) { - return const CircularProgressIndicator(); - } - return Text(query.data!); - }, - ), - ); - } -} diff --git a/packages/example/lib/hooks_example.dart b/packages/example/lib/hooks_example.dart deleted file mode 100644 index 2f07d2f..0000000 --- a/packages/example/lib/hooks_example.dart +++ /dev/null @@ -1,27 +0,0 @@ -import 'package:example/main.dart'; -import 'package:flutter/material.dart'; -import 'package:fl_query/fl_query_hooks.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; - -class HookExample extends HookWidget { - const HookExample({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - final query = useQuery(job: successJob, externalData: null); - - return Scaffold( - appBar: AppBar( - title: const Text("Running the 1st example but with hooks instead"), - ), - body: !query.hasData || query.isLoading || query.isRefetching - ? const CircularProgressIndicator() - : TextButton( - child: Text(query.data!), - onPressed: () async { - await query.refetch(); - }, - ), - ); - } -} diff --git a/packages/example/lib/lazy_query.dart b/packages/example/lib/lazy_query.dart deleted file mode 100644 index 5f76bde..0000000 --- a/packages/example/lib/lazy_query.dart +++ /dev/null @@ -1,36 +0,0 @@ -import 'package:fl_query/fl_query.dart'; -import 'package:flutter/material.dart'; - -final lazyQueryJob = QueryJob( - queryKey: "non_enabled_query", - enabled: false, - task: (queryKey, data, _) { - return Future.delayed(const Duration(milliseconds: 500), - () => "Hello from $queryKey with $data"); - }); - -class LazyQuery extends StatelessWidget { - const LazyQuery({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar(), - body: QueryBuilder( - job: lazyQueryJob, - externalData: "Love", - builder: (context, query) { - return Column( - children: [ - Text("Query Data::: ${query.data ?? "Loading"}"), - ElevatedButton( - child: const Text("Fetch Query"), - onPressed: () => query.refetch(), - ), - ], - ); - }, - ), - ); - } -} diff --git a/packages/example/lib/main.dart b/packages/example/lib/main.dart index e16e821..78ff20c 100644 --- a/packages/example/lib/main.dart +++ b/packages/example/lib/main.dart @@ -1,11 +1,17 @@ import 'dart:math'; -import 'package:example/another_component.dart'; -import 'package:example/dependent_query_example.dart'; -import 'package:example/hooks_example.dart'; -import 'package:example/lazy_query.dart'; -import 'package:example/mutation_example.dart'; -import 'package:example/query_with_external_data.dart'; +import 'package:example/components/basic_mutation.dart'; +import 'package:example/components/basic_query.dart'; +import 'package:example/components/hooks/basic_hook_mutation.dart'; +import 'package:example/components/hooks/basic_hook_query.dart'; +import 'package:example/components/hooks/lazy_hook_query.dart'; +import 'package:example/components/hooks/mutation_hook_variable_key.dart'; +import 'package:example/components/hooks/query_hook_external_data.dart'; +import 'package:example/components/hooks/query_hook_variable_key.dart'; +import 'package:example/components/lazy_query.dart'; +import 'package:example/components/mutation_variable_key.dart'; +import 'package:example/components/query_external_data.dart'; +import 'package:example/components/query_variable_key.dart'; import 'package:fl_query/fl_query.dart'; import 'package:flutter/material.dart'; import 'dart:async'; @@ -33,21 +39,6 @@ class MyApp extends StatelessWidget { } } -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, _, __) => Random().nextBool() - ? Future.error("[$queryKey] Failed for unknown reason") - : Future.value( - "Success, you'll get slowly ${Random().nextInt(100)}!", - ), -); - class MyHomePage extends StatefulWidget { const MyHomePage({Key? key}) : super(key: key); @@ -56,142 +47,47 @@ class MyHomePage extends StatefulWidget { } class _MyHomePageState extends State with WidgetsBindingObserver { - @override - void initState() { - super.initState(); - WidgetsBinding.instance.addObserver(this); - } - - @override - void dispose() { - WidgetsBinding.instance.removeObserver(this); - super.dispose(); - } - - @override - void didChangeAppLifecycleState(AppLifecycleState state) { - super.didChangeAppLifecycleState(state); - print("LIFE CYCLE STATE: $state"); - } - @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text("Fl Query Example"), ), - body: Column( - children: [ - Row( - children: [ - QueryBuilder( - job: successJob, - externalData: null, - builder: (context, query) { - if (!query.hasData || query.isLoading || query.isRefetching) { - return const CircularProgressIndicator(); - } - return TextButton( - child: Text(query.data!), - onPressed: () async { - await query.refetch(); - }, - ); - }, + body: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + children: [ + // Regular Flutter Examples + const BasicQueryExample(), + const QueryExternalDataExample(), + const LazyQueryExample(), + const QueryVariableKeyExample(), + const Divider(), + const BasicMutationExample(), + const MutationVariableKeyExample(), + + const Divider(color: Colors.amber, thickness: 5), + Align( + alignment: Alignment.topLeft, + child: Text( + "!Warning! Cool people only...\nFlutter Hooks Example", + style: Theme.of(context).textTheme.headline3, ), - QueryBuilder( - job: successJob, - externalData: null, - builder: (context, query) { - if (!query.hasData || 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) { - return Row( - children: [ - if (query.hasError) - Text( - "${query.error}. Retrying: ${query.retryAttempts}", - ), - if (query.hasData) - Text( - "Success after ${query.retryAttempts}. Data: ${query.data}"), - ElevatedButton( - child: Text("Refetch ${query.queryKey}"), - onPressed: () => query.refetch(), - ) - ], - ); - }, - ), - ], - ), - ElevatedButton( - child: const Text("External Data Example"), - onPressed: () { - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => const QueryWithExternalData(), - ), - ); - }, - ), - const SizedBox(height: 10), - ElevatedButton( - child: const Text("Non Enabled Query Example"), - onPressed: () { - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => const LazyQuery(), - ), - ); - }, - ), - const SizedBox(height: 10), - ElevatedButton( - child: const Text("Mutation Example"), - onPressed: () { - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => const MutationExample(), - ), - ); - }, - ), - ElevatedButton( - child: const Text("flutter_hooks Example"), - onPressed: () { - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => const HookExample(), - ), - ); - }, - ), - ElevatedButton( - child: const Text("Dependent Query Example"), - onPressed: () { - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => const DependentQueryExample(), - ), - ); - }, - ), - const AnotherComponent(), - ], - ), + ), + const Divider(color: Colors.amber, thickness: 5), + // elite flutter_hooks examples for only elite flutter + // developers + const BasicHookQueryExample(), + const QueryHookExternalDataExample(), + const LazyHookQueryExample(), + const QueryHookVariableKeyExample(), + const Divider(), + const BasicHookMutationExample(), + const MutationHookVariableKeyExample(), + ], + ), + )), ); } } diff --git a/packages/example/lib/mutation_example.dart b/packages/example/lib/mutation_example.dart deleted file mode 100644 index 5147425..0000000 --- a/packages/example/lib/mutation_example.dart +++ /dev/null @@ -1,93 +0,0 @@ -import 'dart:convert'; -import 'dart:math'; - -import 'package:fl_query/fl_query.dart'; -import 'package:flutter/material.dart'; -import 'package:http/http.dart' as http; - -final postSomethingJob = MutationJob>( - mutationKey: "post-something-job", - task: (key, data) async { - final response = await http.post( - Uri.parse( - "https://jsonplaceholder.typicode.com/posts", - ), - headers: {'Content-type': 'application/json; charset=UTF-8'}, - body: jsonEncode(data), - ); - return jsonDecode(response.body); - }, -); - -class MutationExample extends StatefulWidget { - const MutationExample({Key? key}) : super(key: key); - - @override - State createState() => _MutationExampleState(); -} - -class _MutationExampleState extends State { - late TextEditingController titleController; - late TextEditingController bodyController; - late int id; - @override - void initState() { - super.initState(); - id = Random().nextInt(2000000); - titleController = TextEditingController(); - bodyController = TextEditingController(); - } - - @override - void dispose() { - titleController.dispose(); - bodyController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar(title: const Text("Post Something")), - body: MutationBuilder>( - job: postSomethingJob, - builder: (context, mutation) { - return Padding( - padding: const EdgeInsets.all(8.0), - child: Column( - children: [ - TextField( - controller: titleController, - decoration: const InputDecoration(labelText: "Title"), - ), - TextField( - controller: bodyController, - decoration: const InputDecoration(labelText: "Body"), - ), - const SizedBox(height: 20), - ElevatedButton( - onPressed: () { - final title = titleController.value.text; - final body = bodyController.value.text; - if (body.isEmpty || title.isEmpty) return; - mutation.mutate({ - "title": title, - "body": body, - "id": id, - }, onData: (data) { - // resetting the form - titleController.text = ""; - bodyController.text = ""; - }); - }, - child: const Text("Post"), - ), - const SizedBox(height: 20), - if (mutation.hasData) Text("Response\n${mutation.data}") - ], - ), - ); - }), - ); - } -} diff --git a/packages/example/lib/query_with_external_data.dart b/packages/example/lib/query_with_external_data.dart deleted file mode 100644 index e05af05..0000000 --- a/packages/example/lib/query_with_external_data.dart +++ /dev/null @@ -1,41 +0,0 @@ -import 'dart:math'; - -import 'package:fl_query/fl_query.dart'; -import 'package:flutter/material.dart'; - -final jobWithExternalData = QueryJob( - queryKey: "external_data", - cacheTime: const Duration(seconds: 10), - 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.isRefetching || !query.hasData) { - return const CircularProgressIndicator(); - } - return Container( - width: double.parse(query.externalData), - height: double.parse(query.externalData), - decoration: const BoxDecoration( - shape: BoxShape.circle, - color: Colors.blue, - ), - child: Text(query.externalData), - ); - }, - ), - ); - } -} diff --git a/packages/fl_query/lib/src/base_operation.dart b/packages/fl_query/lib/src/base_operation.dart index 2e3f0fb..ad1c3e3 100644 --- a/packages/fl_query/lib/src/base_operation.dart +++ b/packages/fl_query/lib/src/base_operation.dart @@ -1,3 +1,4 @@ +import 'package:fl_query/src/query_bowl.dart'; import 'package:flutter/widgets.dart'; abstract class BaseOperation extends ChangeNotifier { @@ -28,11 +29,14 @@ abstract class BaseOperation extends ChangeNotifier { /// storage/cache Set> _mounts = {}; + final QueryBowl queryBowl; + BaseOperation({ required this.cacheTime, required this.retries, required this.retryDelay, required this.status, + required this.queryBowl, this.data, }) : updatedAt = DateTime.now(); diff --git a/packages/fl_query/lib/src/hooks/use_mutation.dart b/packages/fl_query/lib/src/hooks/use_mutation.dart index 513ec53..03e11ea 100644 --- a/packages/fl_query/lib/src/hooks/use_mutation.dart +++ b/packages/fl_query/lib/src/hooks/use_mutation.dart @@ -21,96 +21,63 @@ Mutation useMutation({ MutationListener? onMutate, List? keys, }) { - return use(_UseMutation( - job: job, - onData: onData, - onError: onError, - onMutate: onMutate, - keys: keys, - )); -} + final context = useContext(); + final QueryBowl queryBowl = QueryBowl.of(context); + final ValueKey uKey = useMemoized(() => ValueKey(uuid.v4()), []); + final mutation = + useRef(Mutation.fromOptions(job, queryBowl: queryBowl)); -class _UseMutation extends Hook> { - final MutationJob job; + final init = useCallback(() { + mutation.value = queryBowl.addMutation( + mutation.value, + onData: onData, + onError: onError, + onMutate: onMutate, + key: uKey, + ); + }, [mutation.value, job, onData, onError, onMutate, uKey]); - /// Called when the query returns new data, on query - /// refetch or query gets expired - final MutationListener? onData; + final disposeMutation = useCallback(() { + mutation.value.unmount(uKey); + if (onData != null) mutation.value.onDataListeners.remove(onData); + if (onError != null) mutation.value.onErrorListeners.remove(onError); + if (onMutate != null) mutation.value.onMutateListeners.remove(onMutate); + }, [mutation.value, onData, onError, onMutate]); - /// Called when the query returns error - final MutationListener? onError; + final oldJob = usePrevious(job); + final oldOnData = usePrevious(onData); + final oldOnError = usePrevious(onError); + final oldOnMutate = usePrevious(onMutate); - /// called right before the mutation is about to run - /// - /// perfect scenario for doing optimistic updates - final MutationListener? onMutate; - const _UseMutation({ - required this.job, - this.onData, - this.onError, - this.onMutate, - super.keys, + useEffect(() { + init(); + return disposeMutation; + }, []); + + useEffect(() { + if (oldJob != null && oldJob.mutationKey != job.mutationKey) { + disposeMutation(); + mutation.value = Mutation.fromOptions( + job, + queryBowl: queryBowl, + ); + init(); + } else { + if (oldOnData != onData && oldOnData != null) { + mutation.value.onDataListeners.remove(oldOnData); + if (onData != null) mutation.value.onDataListeners.add(onData); + } + if (oldOnError != onError && oldOnError != null) { + mutation.value.onErrorListeners.remove(oldOnError); + if (onError != null) mutation.value.onErrorListeners.add(onError); + } + if (oldOnMutate != onMutate && oldOnMutate != null) { + mutation.value.onMutateListeners.remove(oldOnMutate); + if (onMutate != null) mutation.value.onMutateListeners.add(onMutate); + } + } + return null; }); - @override - HookState, Hook>> createState() => - _UseMutationHookState(); -} - -class _UseMutationHookState - extends HookState, _UseMutation> { - late QueryBowl queryBowl; - late final ValueKey uKey; - late Mutation mutation; - - @override - void initHook() { - super.initHook(); - uKey = ValueKey(uuid.v4()); - mutation = Mutation.fromOptions(hook.job); - WidgetsBinding.instance.addPostFrameCallback((_) { - queryBowl = QueryBowl.of(context); - mutation = queryBowl.addMutation( - mutation, - onData: hook.onData, - onError: hook.onError, - onMutate: hook.onMutate, - key: uKey, - ); - }); - } - - @override - void didUpdateHook(_UseMutation oldHook) { - if (oldHook.onData != hook.onData && oldHook.onData != null) { - mutation.onDataListeners.remove(oldHook.onData); - if (hook.onData != null) mutation.onDataListeners.add(hook.onData!); - } - if (oldHook.onError != hook.onError && oldHook.onError != null) { - mutation.onErrorListeners.remove(oldHook.onError); - if (hook.onError != null) mutation.onErrorListeners.add(hook.onError!); - } - if (oldHook.onMutate != hook.onMutate && oldHook.onMutate != null) { - mutation.onMutateListeners.remove(oldHook.onMutate); - if (hook.onMutate != null) mutation.onMutateListeners.add(hook.onMutate!); - } - super.didUpdateHook(oldHook); - } - - @override - void dispose() { - mutation.unmount(uKey); - if (hook.onData != null) mutation.onDataListeners.remove(hook.onData); - if (hook.onError != null) mutation.onErrorListeners.remove(hook.onError); - if (hook.onMutate != null) mutation.onMutateListeners.remove(hook.onMutate); - } - - @override - Mutation build(BuildContext context) { - queryBowl = QueryBowl.of(context); - return queryBowl.getMutation(mutation.mutationKey) ?? mutation; - } - - @override - String get debugLabel => 'useQuery'; + return queryBowl.getMutation(job.mutationKey) ?? mutation.value; } diff --git a/packages/fl_query/lib/src/hooks/use_query.dart b/packages/fl_query/lib/src/hooks/use_query.dart index fe2f0bc..025fbe8 100644 --- a/packages/fl_query/lib/src/hooks/use_query.dart +++ b/packages/fl_query/lib/src/hooks/use_query.dart @@ -18,45 +18,59 @@ Query useQuery({ List? keys, }) { final context = useContext(); - QueryBowl queryBowl = QueryBowl.of(context); + final QueryBowl queryBowl = QueryBowl.of(context); final ValueKey uKey = useMemoized(() => ValueKey(uuid.v4()), []); - Query query = useMemoized( - () => Query.fromOptions( - job, - externalData: externalData, - queryBowl: queryBowl, - onData: onData, - onError: onError, - ), - []); + final query = useRef( + Query.fromOptions( + job, + externalData: externalData, + queryBowl: queryBowl, + ), + ); + final oldJob = usePrevious(job); final oldExternalData = usePrevious(externalData); final oldOnData = usePrevious(onData); final oldOnError = usePrevious(onError); - useEffect(() { - queryBowl.addQuery( - query, + final init = useCallback(() { + query.value = queryBowl.addQuery( + query.value, key: uKey, onData: onData, onError: onError, ); - final hasExternalDataChanged = query.externalData != null && - query.prevUsedExternalData != null && - !isShallowEqual(query.externalData!, query.prevUsedExternalData!); - (query.fetched && query.refetchOnMount == true) || hasExternalDataChanged - ? query.refetch() - : query.fetch(); + final hasExternalDataChanged = query.value.externalData != null && + query.value.prevUsedExternalData != null && + !isShallowEqual( + query.value.externalData!, query.value.prevUsedExternalData!); + (query.value.fetched && query.value.refetchOnMount == true) || + hasExternalDataChanged + ? query.value.refetch() + : query.value.fetch(); + }, [queryBowl, query.value, uKey, onData, onError, job]); - return () { - query.unmount(uKey); - if (onData != null) query.onDataListeners.remove(onData); - if (onError != null) query.onErrorListeners.remove(onError); - }; + final disposeQuery = useCallback(() { + query.value.unmount(uKey); + if (onData != null) query.value.onDataListeners.remove(onData); + if (onError != null) query.value.onErrorListeners.remove(onError); + }, [query.value, onData, onError, uKey]); + + useEffect(() { + init(); + return disposeQuery; }, []); useEffect(() { - if (oldExternalData != null && + if (oldJob != null && oldJob.queryKey != job.queryKey) { + disposeQuery(); + query.value = Query.fromOptions( + job, + externalData: externalData, + queryBowl: queryBowl, + ); + init(); + } else if (oldExternalData != null && externalData != null && !isShallowEqual(oldExternalData, externalData)) { QueryBowl.of(context).fetchQuery( @@ -68,16 +82,16 @@ Query useQuery({ ); } else { if (oldOnData != onData && oldOnData != null) { - query.onDataListeners.remove(oldOnData); - if (onData != null) query.onDataListeners.add(onData); + query.value.onDataListeners.remove(oldOnData); + if (onData != null) query.value.onDataListeners.add(onData); } if (oldOnError != onError && oldOnError != null) { - query.onErrorListeners.remove(oldOnError); - if (onError != null) query.onErrorListeners.add(onError); + query.value.onErrorListeners.remove(oldOnError); + if (onError != null) query.value.onErrorListeners.add(onError); } } return null; }); - return queryBowl.getQuery(job.queryKey) ?? query; + return queryBowl.getQuery(job.queryKey) ?? query.value; } diff --git a/packages/fl_query/lib/src/mutation.dart b/packages/fl_query/lib/src/mutation.dart index 26f839d..caa5e12 100644 --- a/packages/fl_query/lib/src/mutation.dart +++ b/packages/fl_query/lib/src/mutation.dart @@ -2,7 +2,11 @@ import 'dart:async'; import 'package:fl_query/src/base_operation.dart'; import 'package:fl_query/src/models/mutation_job.dart'; +import 'package:fl_query/src/models/query_job.dart'; +import 'package:fl_query/src/query.dart'; +import 'package:fl_query/src/utils.dart'; import 'package:flutter/widgets.dart'; +import 'package:collection/collection.dart'; enum MutationStatus { error, @@ -13,7 +17,8 @@ enum MutationStatus { typedef MutationListener = FutureOr Function(T); -typedef MutationTaskFunction = FutureOr Function(String, V); +typedef MutationTaskFunction = FutureOr Function( + String queryKey, V variables); class Mutation extends BaseOperation { // all params @@ -32,6 +37,7 @@ class Mutation extends BaseOperation { required this.task, required super.retries, required super.retryDelay, + required super.queryBowl, required Duration cacheTime, MutationListener? onData, MutationListener? onError, @@ -47,6 +53,7 @@ class Mutation extends BaseOperation { MutationListener? onData, MutationListener? onError, MutationListener? onMutate, + required super.queryBowl, }) : mutationKey = options.mutationKey, task = options.task, super( diff --git a/packages/fl_query/lib/src/mutation_builder.dart b/packages/fl_query/lib/src/mutation_builder.dart index 95280a4..1822a2a 100644 --- a/packages/fl_query/lib/src/mutation_builder.dart +++ b/packages/fl_query/lib/src/mutation_builder.dart @@ -5,7 +5,7 @@ import 'package:fl_query/src/utils.dart'; import 'package:flutter/widgets.dart'; class MutationBuilder extends StatefulWidget { - final Function(BuildContext, Mutation) builder; + final Function(BuildContext context, Mutation mutation) builder; final MutationJob job; /// Called when the query returns new data, on query @@ -39,60 +39,72 @@ class _MutationBuilderState late ValueKey uKey; - late Mutation mutation; + Mutation? mutation; @override void initState() { super.initState(); uKey = ValueKey(uuid.v4()); - mutation = Mutation.fromOptions(widget.job); - WidgetsBinding.instance.addPostFrameCallback((_) { - queryBowl = QueryBowl.of(context); - mutation = queryBowl.addMutation( - mutation, - onData: widget.onData, - onError: widget.onError, - onMutate: widget.onMutate, - key: uKey, - ); - }); + WidgetsBinding.instance.addPostFrameCallback(init); + } + + void init([_]) { + queryBowl = QueryBowl.of(context); + mutation = queryBowl.addMutation( + Mutation.fromOptions(widget.job, queryBowl: queryBowl), + onData: widget.onData, + onError: widget.onError, + onMutate: widget.onMutate, + key: uKey, + ); } @override void didUpdateWidget(covariant MutationBuilder oldWidget) { - if (oldWidget.onData != widget.onData && oldWidget.onData != null) { - mutation.onDataListeners.remove(oldWidget.onData); - if (widget.onData != null) mutation.onDataListeners.add(widget.onData!); - } - if (oldWidget.onError != widget.onError && oldWidget.onError != null) { - mutation.onErrorListeners.remove(oldWidget.onError); - if (widget.onError != null) - mutation.onErrorListeners.add(widget.onError!); - } - if (oldWidget.onMutate != widget.onMutate && oldWidget.onMutate != null) { - mutation.onMutateListeners.remove(oldWidget.onMutate); - if (widget.onMutate != null) - mutation.onMutateListeners.add(widget.onMutate!); + if (oldWidget.job.mutationKey != widget.job.mutationKey) { + _mutationDispose(); + init(); + } else { + if (oldWidget.onData != widget.onData && oldWidget.onData != null) { + mutation?.onDataListeners.remove(oldWidget.onData); + if (widget.onData != null) + mutation?.onDataListeners.add(widget.onData!); + } + if (oldWidget.onError != widget.onError && oldWidget.onError != null) { + mutation?.onErrorListeners.remove(oldWidget.onError); + if (widget.onError != null) + mutation?.onErrorListeners.add(widget.onError!); + } + if (oldWidget.onMutate != widget.onMutate && oldWidget.onMutate != null) { + mutation?.onMutateListeners.remove(oldWidget.onMutate); + if (widget.onMutate != null) + mutation?.onMutateListeners.add(widget.onMutate!); + } } super.didUpdateWidget(oldWidget); } @override void dispose() { - mutation.unmount(uKey); - if (widget.onData != null) mutation.onDataListeners.remove(widget.onData); - if (widget.onError != null) - mutation.onErrorListeners.remove(widget.onError); - if (widget.onMutate != null) - mutation.onMutateListeners.remove(widget.onMutate); + _mutationDispose(); super.dispose(); } + void _mutationDispose() { + mutation?.unmount(uKey); + if (widget.onData != null) mutation?.onDataListeners.remove(widget.onData); + if (widget.onError != null) + mutation?.onErrorListeners.remove(widget.onError); + if (widget.onMutate != null) + mutation?.onMutateListeners.remove(widget.onMutate); + } + @override Widget build(BuildContext context) { queryBowl = QueryBowl.of(context); final latestMutation = - queryBowl.getMutation(mutation.mutationKey) ?? mutation; + queryBowl.getMutation(widget.job.mutationKey) ?? mutation; + if (latestMutation == null) return Container(); return widget.builder(context, latestMutation); } } diff --git a/packages/fl_query/lib/src/query.dart b/packages/fl_query/lib/src/query.dart index 56970ed..b558010 100644 --- a/packages/fl_query/lib/src/query.dart +++ b/packages/fl_query/lib/src/query.dart @@ -73,8 +73,6 @@ class Query extends BaseOperation { Timer? _refetchIntervalTimer; - final QueryBowl queryBowl; - Query({ required this.queryKey, required this.task, @@ -83,7 +81,7 @@ class Query extends BaseOperation { required Outside externalData, required super.retries, required super.retryDelay, - required this.queryBowl, + required super.queryBowl, this.refetchOnMount, this.refetchOnReconnect, this.refetchInterval, @@ -108,7 +106,7 @@ class Query extends BaseOperation { Query.fromOptions( QueryJob options, { - required this.queryBowl, + required super.queryBowl, required Outside externalData, QueryListener? onData, QueryListener? onError, @@ -209,12 +207,11 @@ class Query extends BaseOperation { } Future fetch() async { + if (!enabled) return null; + /// if isLoading/isRefetching is true that means its already fetching/ /// refetching. So [_execute] again can create a race condition - if (!enabled || isLoading || isRefetching) return null; - if (hasData) { - return data; - } + if (isLoading || isRefetching || hasData) return data; status = QueryStatus.loading; notifyListeners(); return _execute().then((_) { @@ -226,12 +223,12 @@ class Query extends BaseOperation { Future refetch() async { /// if isLoading/isRefetching is true that means its already fetching/ /// refetching. So [_execute] again can create a race condition - if (isRefetching || isLoading) return null; + if (isRefetching || isLoading) return data; + if (enabled && !fetched) await fetch(); status = QueryStatus.refetching; refetchCount++; // disabling the lazy query bound when query was actually called if (!enabled) enabled = true; - if (enabled && !fetched) await fetch(); notifyListeners(); return await _execute().then((_) => data); } @@ -247,11 +244,7 @@ class Query extends BaseOperation { /// immutability void setQueryData(QueryUpdateFunction updateFn) async { final newData = await updateFn(data); - if (data == newData) { - // TODO: Better Error handling & Error structure - throw Exception( - "[fl_query] new instance of data should be returned because of immutability"); - } + if (data == newData) return; data = newData; status = QueryStatus.success; notifyListeners(); @@ -269,11 +262,6 @@ class Query extends BaseOperation { fetched = false; status = QueryStatus.idle; retryAttempts = 0; - for (final queryEntry in _dependencyQueries.entries) { - queryEntry.value.unmount(queryEntry.key); - queryEntry.value.removeListener(refetch); - } - _dependencyQueries = {}; onDataListeners.clear(); onErrorListeners.clear(); mounts.clear(); @@ -306,40 +294,8 @@ class Query extends BaseOperation { notifyListeners(); } - Map, Query> _dependencyQueries = {}; - - /// only usable inside [QueryJob.task] method - Query dependOnQuery( - QueryJob job, { - required Outside externalData, - }) { - final key = ValueKey(uuid.v4()); - final query = queryBowl.addQuery( - Query.fromOptions( - job, - externalData: externalData, - queryBowl: queryBowl, - ), - key: key, - ); - // removing listener if it was already hooked to it previously - query.removeListener(refetch); - query.addListener(refetch); - final uKey = - _dependencyQueries.keys.firstWhereOrNull((k) => k.value == key.value) ?? - key; - _dependencyQueries[uKey] = query; - if (!query.fetched) query.fetch(); - return query; - } - - @override - void dispose() { - for (final queryEntry in _dependencyQueries.entries) { - queryEntry.value.unmount(queryEntry.key); - queryEntry.value.removeListener(refetch); - } - super.dispose(); + Future _internalRefetch(X dataOrError) { + return refetch(); } bool get isStale { diff --git a/packages/fl_query/lib/src/query_bowl.dart b/packages/fl_query/lib/src/query_bowl.dart index ac74046..71f4123 100644 --- a/packages/fl_query/lib/src/query_bowl.dart +++ b/packages/fl_query/lib/src/query_bowl.dart @@ -74,7 +74,7 @@ class _QueryBowlScopeState extends State { .listen((ConnectivityResult result) async { if (isConnectedToInternet(result)) { for (final query in queries) { - if (query.refetchOnReconnect == false) continue; + if (query.refetchOnReconnect == false || !query.enabled) continue; await query.refetch(); await Future.delayed(widget.refetchOnReconnectDelay); } @@ -108,75 +108,73 @@ class _QueryBowlScopeState extends State { } void updateQueries(Query query) { - // checking & not including inactive queries - // basically garbage collecting queries - if (query.isInactive) { - /// there's a bug currently, - /// where if somehow a [Query] (queryA) is depending on another - /// [Query] (queryB) & queryA has become inactive so its getting - /// disposed but at the same moment queryB got refetched will make - /// queryB's [BaseOperation.unmount] to throw [RangeError] for - /// calling [ChangeNotifier.notifyListener] after [cacheDelay] - /// inside [Timer.periodic] callback - /// - /// To mitigate this, [Query.reset] is used instead of using - /// [Query.dispose] as it doesn't call [super.dispose] - query.reset(); - } - - setState(() { - queries = Set.from( - query.isInactive - ? queries.where((el) => el.queryKey != query.queryKey) - : queries, - ); + WidgetsBinding.instance.addPostFrameCallback((_) { + // checking & not including inactive queries + // basically garbage collecting queries + setState(() { + queries = Set.from( + query.isInactive + ? queries.where((el) => el.queryKey != query.queryKey) + : queries, + ); + }); }); } void updateMutations(Mutation mutation) { - setState(() { - // checking & not including inactive mutations - // basically garbage collecting mutations - mutations = Set.from( - mutation.isInactive - ? mutations.where( - (el) => el.mutationKey != mutation.mutationKey, - ) - : mutations, - ); + WidgetsBinding.instance.addPostFrameCallback((_) { + setState(() { + // checking & not including inactive mutations + // basically garbage collecting mutations + mutations = Set.from( + mutation.isInactive + ? mutations.where( + (el) => el.mutationKey != mutation.mutationKey, + ) + : mutations, + ); + }); }); } void addQuery(Query query) { - setState(() { - queries = Set.from({...queries, query}); + WidgetsBinding.instance.addPostFrameCallback((_) { + setState(() { + queries = Set.from({...queries, query}); + }); }); } void addMutation(Mutation mutation) { - setState(() { - mutations = Set.from({...mutations, mutation}); + WidgetsBinding.instance.addPostFrameCallback((_) { + setState(() { + mutations = Set.from({...mutations, mutation}); + }); }); } int removeQueries(List queryKeys) { int count = 0; - setState(() { - mutations = Set.from( - queries.whereNot((query) { - final isAboutToRip = queryKeys.contains(query.queryKey); - if (isAboutToRip) count++; - return isAboutToRip; - }), - ); + WidgetsBinding.instance.addPostFrameCallback((_) { + setState(() { + mutations = Set.from( + queries.whereNot((query) { + final isAboutToRip = queryKeys.contains(query.queryKey); + if (isAboutToRip) count++; + return isAboutToRip; + }), + ); + }); }); return count; } void clear() { - setState(() { - queries = Set(); - mutations = Set(); + WidgetsBinding.instance.addPostFrameCallback((_) { + setState(() { + queries = Set(); + mutations = Set(); + }); }); } @@ -270,9 +268,7 @@ class QueryBowl extends InheritedWidget { if (onError != null) prevQuery.onErrorListeners.add(onError); if (!prevQuery.hasData || hasExternalDataChanged) { if (hasExternalDataChanged) prevQuery.setExternalData(externalData); - return prevQuery.fetched - ? await prevQuery.refetch() - : await prevQuery.fetch(); + return await prevQuery.refetch(); } // mounting the widget that is using the query in the prevQuery return prevQuery.data; @@ -353,6 +349,9 @@ class QueryBowl extends InheritedWidget { prevMutation.mount(key); return prevMutation; } else { + if (onData != null) mutation.onDataListeners.add(onData); + if (onError != null) mutation.onErrorListeners.add(onError); + if (onMutate != null) mutation.onMutateListeners.add(onMutate); mutation.updateDefaultOptions(cacheTime: cacheTime); mutation.mount(key); _addMutation(mutation); @@ -413,8 +412,9 @@ class QueryBowl extends InheritedWidget { Future refetchQueries(List queryKeys) async { for (final query in _queries) { - if (!queryKeys.contains(query.queryKey)) continue; - await query.refetch(); + if (queryKeys.contains(query.queryKey)) { + await query.refetch(); + } } } diff --git a/packages/fl_query/lib/src/query_builder.dart b/packages/fl_query/lib/src/query_builder.dart index 96f0dc6..51bc2fc 100644 --- a/packages/fl_query/lib/src/query_builder.dart +++ b/packages/fl_query/lib/src/query_builder.dart @@ -5,7 +5,7 @@ import 'package:fl_query/src/utils.dart'; import 'package:flutter/widgets.dart'; class QueryBuilder extends StatefulWidget { - final Function(BuildContext, Query) builder; + final Function(BuildContext context, Query query) builder; final QueryJob job; final Outside externalData; @@ -40,30 +40,36 @@ class _QueryBuilderState void initState() { super.initState(); uKey = ValueKey(uuid.v4()); - WidgetsBinding.instance.addPostFrameCallback((_) async { - query = QueryBowl.of(context).addQuery( - Query.fromOptions( - widget.job, - externalData: widget.externalData, - queryBowl: QueryBowl.of(context), - ), - key: uKey, - onData: widget.onData, - onError: widget.onError, - ); - final hasExternalDataChanged = query!.externalData != null && - query!.prevUsedExternalData != null && - !isShallowEqual(query!.externalData!, query!.prevUsedExternalData!); - (query!.fetched && query!.refetchOnMount == true) || - hasExternalDataChanged - ? await query!.refetch() - : await query!.fetch(); - }); + WidgetsBinding.instance.addPostFrameCallback((_) => init()); + } + + void init([QueryBowl? bowl]) async { + bowl ??= QueryBowl.of(context); + query = bowl.addQuery( + Query.fromOptions( + widget.job, + externalData: widget.externalData, + queryBowl: QueryBowl.of(context), + ), + key: uKey, + onData: widget.onData, + onError: widget.onError, + ); + final hasExternalDataChanged = query!.externalData != null && + query!.prevUsedExternalData != null && + !isShallowEqual(query!.externalData!, query!.prevUsedExternalData!); + (query!.fetched && query!.refetchOnMount == true) || hasExternalDataChanged + ? await query!.refetch() + : await query!.fetch(); } @override void didUpdateWidget(covariant oldWidget) { - if (oldWidget.externalData != null && + // re-init the query-builder when new queryJob is appended + if (oldWidget.job.queryKey != widget.job.queryKey) { + _queryDispose(); + init(); + } else if (oldWidget.externalData != null && widget.externalData != null && !isShallowEqual(oldWidget.externalData!, widget.externalData!)) { QueryBowl.of(context).fetchQuery( @@ -87,11 +93,15 @@ class _QueryBuilderState super.didUpdateWidget(oldWidget); } - @override - void dispose() { + _queryDispose() { query?.unmount(uKey); if (widget.onData != null) query?.onDataListeners.remove(widget.onData); if (widget.onError != null) query?.onErrorListeners.remove(widget.onError); + } + + @override + void dispose() { + _queryDispose(); super.dispose(); }