feat: useQuery hook with working example
This commit is contained in:
@@ -0,0 +1,29 @@
|
||||
import 'package:fl_query_hooks_example/router.dart';
|
||||
import 'package:fl_query/fl_query.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
void main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
await QueryClient.initialize(cachePrefix: 'fl_query_example');
|
||||
runApp(
|
||||
QueryClientProvider(
|
||||
child: const MainApp(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class MainApp extends StatelessWidget {
|
||||
const MainApp({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp.router(
|
||||
theme: ThemeData(
|
||||
colorSchemeSeed: Colors.red[100],
|
||||
useMaterial3: true,
|
||||
),
|
||||
title: 'FL Query Hooks Example',
|
||||
routerConfig: router,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
class Product {
|
||||
int id;
|
||||
String title;
|
||||
String description;
|
||||
double price;
|
||||
double discountPercentage;
|
||||
double rating;
|
||||
double stock;
|
||||
String brand;
|
||||
String category;
|
||||
String thumbnail;
|
||||
List<String> images;
|
||||
|
||||
Product({
|
||||
required this.id,
|
||||
required this.title,
|
||||
required this.description,
|
||||
required this.price,
|
||||
required this.discountPercentage,
|
||||
required this.rating,
|
||||
required this.stock,
|
||||
required this.brand,
|
||||
required this.category,
|
||||
required this.thumbnail,
|
||||
required this.images,
|
||||
});
|
||||
|
||||
Product.fromJson(Map<String, dynamic> json)
|
||||
: id = json['id'],
|
||||
title = json['title'],
|
||||
description = json['description'],
|
||||
price = json['price'].toDouble(),
|
||||
discountPercentage = json['discountPercentage'].toDouble(),
|
||||
rating = json['rating'].toDouble(),
|
||||
stock = json['stock'].toDouble(),
|
||||
brand = json['brand'],
|
||||
category = json['category'],
|
||||
thumbnail = json['thumbnail'],
|
||||
images = json['images'].cast<String>();
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final Map<String, dynamic> data = <String, dynamic>{
|
||||
'id': id,
|
||||
'title': title,
|
||||
'description': description,
|
||||
'price': price,
|
||||
'discountPercentage': discountPercentage,
|
||||
'rating': rating,
|
||||
'stock': stock,
|
||||
'brand': brand,
|
||||
'category': category,
|
||||
'thumbnail': thumbnail,
|
||||
'images': images,
|
||||
};
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
class PagedProducts {
|
||||
List<Product> products;
|
||||
int total;
|
||||
int skip;
|
||||
int limit;
|
||||
|
||||
PagedProducts({
|
||||
this.products = const [],
|
||||
required this.total,
|
||||
required this.skip,
|
||||
required this.limit,
|
||||
});
|
||||
|
||||
PagedProducts.fromJson(Map<String, dynamic> json)
|
||||
: products = json['products']
|
||||
?.map<Product>((v) => Product.fromJson(
|
||||
Map.castFrom<dynamic, dynamic, String, dynamic>(v),
|
||||
))
|
||||
.toList() ??
|
||||
[],
|
||||
total = json['total'],
|
||||
skip = json['skip'],
|
||||
limit = json['limit'];
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final Map<String, dynamic> data = <String, dynamic>{
|
||||
'products': products.map((v) => v.toJson()).toList(),
|
||||
'total': total,
|
||||
'skip': skip,
|
||||
'limit': limit,
|
||||
};
|
||||
return data;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
class HomePage extends StatelessWidget {
|
||||
const HomePage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('FL Query Example'),
|
||||
),
|
||||
body: ListView(
|
||||
children: [
|
||||
ListTile(
|
||||
title: const Text('Query'),
|
||||
onTap: () => GoRouter.of(context).push('/query'),
|
||||
),
|
||||
ListTile(
|
||||
title: const Text('Infinite Query'),
|
||||
onTap: () => GoRouter.of(context).push('/infinite-query'),
|
||||
),
|
||||
ListTile(
|
||||
title: const Text('Mutation'),
|
||||
onTap: () => GoRouter.of(context).push('/mutation'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:fl_query_hooks_example/models/product.dart';
|
||||
import 'package:fl_query/fl_query.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:http/http.dart';
|
||||
|
||||
class InfiniteQueryPageWidget extends StatefulWidget {
|
||||
const InfiniteQueryPageWidget({super.key});
|
||||
|
||||
@override
|
||||
State<InfiniteQueryPageWidget> createState() =>
|
||||
_InfiniteQueryPageWidgetState();
|
||||
}
|
||||
|
||||
class _InfiniteQueryPageWidgetState extends State<InfiniteQueryPageWidget> {
|
||||
final controller = ScrollController();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
controller.addListener(() async {
|
||||
if (controller.position.pixels == controller.position.maxScrollExtent) {
|
||||
final query = QueryClient.of(context).getInfiniteQuery("products");
|
||||
await query?.fetchNext();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Infinite Query'),
|
||||
),
|
||||
floatingActionButton:
|
||||
InfiniteQueryListenable("products", builder: (context, query) {
|
||||
return FloatingActionButton(
|
||||
onPressed: () {
|
||||
query?.fetchNext();
|
||||
},
|
||||
child: Text(query?.pages.length.toString() ?? "-69"),
|
||||
);
|
||||
}),
|
||||
body: InfiniteQueryBuilder<PagedProducts, ClientException, int>(
|
||||
"products",
|
||||
(page) async {
|
||||
final res = await get(Uri.parse(
|
||||
"https://dummyjson.com/products?limit=10&skip=${page * 10}",
|
||||
));
|
||||
|
||||
if (res.statusCode == 200) {
|
||||
return PagedProducts.fromJson(jsonDecode(res.body));
|
||||
} else {
|
||||
throw ClientException(res.statusCode.toString(), res.request?.url);
|
||||
}
|
||||
},
|
||||
nextPage: (lastPage, pages) {
|
||||
if (pages.isNotEmpty &&
|
||||
pages.last.products.length < pages[lastPage].limit) {
|
||||
/// returning [null] will set [hasNextPage] to [false]
|
||||
return null;
|
||||
}
|
||||
return lastPage + 1;
|
||||
},
|
||||
initialPage: 0,
|
||||
jsonConfig: JsonConfig(
|
||||
fromJson: (json) => PagedProducts.fromJson(json),
|
||||
toJson: (data) => data.toJson(),
|
||||
),
|
||||
builder: (context, query) {
|
||||
final products = query.pages.map((e) => e.products).expand((e) => e);
|
||||
if (!query.hasPages) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
}
|
||||
return ListView(
|
||||
controller: controller,
|
||||
children: [
|
||||
for (final product in products)
|
||||
ListTile(
|
||||
title: Text(product.title),
|
||||
subtitle: Text(product.description),
|
||||
leading: Image.network(product.thumbnail),
|
||||
),
|
||||
if (query.hasNextPage)
|
||||
const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
if (query.hasErrors)
|
||||
...query.errors.map((e) => Text(e.message)).toList(),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
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, Map<String, dynamic>,
|
||||
dynamic>(
|
||||
'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 ['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'),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:fl_query/fl_query.dart';
|
||||
import 'package:fl_query_hooks/fl_query_hooks.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
|
||||
class QueryPage extends HookWidget {
|
||||
const QueryPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final value = Random().nextInt(200000);
|
||||
final query = useQuery<String, dynamic>(
|
||||
'hello',
|
||||
() {
|
||||
return Future.delayed(
|
||||
const Duration(seconds: 6), () => 'Hello World! $value');
|
||||
},
|
||||
initial: 'Hello',
|
||||
jsonConfig: JsonConfig(
|
||||
fromJson: (json) => json['data'],
|
||||
toJson: (data) => {'data': data},
|
||||
),
|
||||
onData: (value) {
|
||||
debugPrint('onData: $value');
|
||||
},
|
||||
onError: (error) {
|
||||
debugPrint('onError: $error');
|
||||
},
|
||||
);
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Query'),
|
||||
),
|
||||
floatingActionButton:
|
||||
QueryListenable<String, dynamic>('hello', builder: (context, query) {
|
||||
if (query == null) {
|
||||
return const SizedBox();
|
||||
}
|
||||
return FloatingActionButton(
|
||||
onPressed: () {
|
||||
query.refresh();
|
||||
},
|
||||
child: Text(query.data ?? 'No Data'),
|
||||
);
|
||||
}),
|
||||
body: query.isLoading
|
||||
? const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
)
|
||||
: query.hasError
|
||||
? Center(
|
||||
child: Text(query.error.toString()),
|
||||
)
|
||||
: Center(
|
||||
child: Text(query.data ?? "Unfortunately, there's no data"),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import "package:fl_query_hooks_example/pages/home.dart";
|
||||
import "package:fl_query_hooks_example/pages/infinite_query.dart";
|
||||
import "package:fl_query_hooks_example/pages/mutation/mutation.dart";
|
||||
import "package:fl_query_hooks_example/pages/query.dart";
|
||||
import "package:go_router/go_router.dart";
|
||||
|
||||
final router = GoRouter(
|
||||
initialLocation: '/',
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: '/',
|
||||
builder: (context, state) => const HomePage(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/query',
|
||||
builder: (context, state) => const QueryPage(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/infinite-query',
|
||||
builder: (context, state) => const InfiniteQueryPageWidget(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/mutation',
|
||||
builder: (context, state) => const MutationPage(),
|
||||
),
|
||||
],
|
||||
);
|
||||
Reference in New Issue
Block a user