Если вы знакомы с Flutter, то наверняка знакомы с FutureBuilder
. В конце концов, это единственный способ получить данные во Flutter с правильным состоянием, или нет? Но вы, возможно, заметили, что каждый раз, когда виджет перестраивается, Future тоже запускается, если вы не объявляете свой Future
в initState
.
Но даже если вы объявите свое будущее в initState
, чтобы избежать повторных запусков при перестроениях, он все равно будет перезапускаться, когда виджет будет снова удален и смонтирован. Итак, каково решение? Ответ: FutureProvider
от riverpod или provider
Подождите секунду, но это не очередная статья о речных подах/поставщиках? Нет, это не так.
Да, для кэширования ваших будущих результатов или ответа сервера вы можете использовать FutureProvider
. Он может хранить и распространять результат по всему приложению без многократного повторного запуска одной и той же операции. Но что происходит, когда срок действия данных истекает или устаревает? Что, если через какое-то время вашему приложению не понадобятся эти данные, но они по-прежнему занимают ОЗУ?
Именно здесь вступает в игру Fl-Query. Это Async Data + Mutation Manager для Flutter, который кэширует, извлекает и автоматически обновляет устаревшие данные. Похож на Tanstack Query в мире веб-разработки, но реализована только концепция, а API очень похоже на то, к чему привыкли разработчики Flutter, поэтому все чувствуют себя как дома
Хватит разговоров, давайте перейдем к большой части.
Что он предлагает?
- Асинхронное кэширование и аннулирование данных
- Интеллектуальный и легко настраиваемый механизм обновления, который разумно обновляет устаревшие данные или данные с истекшим сроком действия в фоновом режиме, когда это необходимо.
- Декларативный способ определения асинхронных операций
- Мусор собирает запросы и мутации. Это означает, что неиспользуемые запросы, находящиеся в кэше в течение длительного времени, автоматически удаляются.
- Повторное использование кода и данных благодаря сохраненным данным и Query/Mutation Job API
- Оптимистичные обновления
- Ленивая загрузка запросов. Запускайте определенную вами асинхронную задачу или операцию, когда это необходимо
- Нулевая конфигурация из коробки Глобальный магазин, к которому никому не нужно прикасаться
- Поддерживает как Vanilla Flutter, так и Flutter Hooks.
Давайте посмотрим код
// A QueryJob is where the Logic of how the data should be // fetched can defined. The task callback is a PURE Function // & have access to external resources through the second // parameter where the first parameter is the queryKey final successJob = QueryJob<String, void>( queryKey: "query-example", task: (queryKey, externalData) => Future.delayed( const Duration(seconds: 2), () => "The work successfully executed. Data: key=($queryKey) value=${ Random.secure().nextInt(100) }", ), );
class MyApp extends StatelessWidget { const MyApp({Key? key}) : super(key: key);
@override Widget build(BuildContext context) { // QueryBowlScope creates a Bowl (metaphor for Collection/Store) // for all the Queries & Mutations return QueryBowlScope( child: MaterialApp( title: 'Fl-Query Quick Start', theme: ThemeData( useMaterial3: true, primarySwatch: Colors.blue, ), home: const MyHomePage(), ), ); } }
class BasicExample extends StatelessWidget { const BasicExample({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 Widget provides the expected query // instances through the builder callback based on // the passed job & externalData argument QueryBuilder<String, void>( 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(); }, ), // Text ], ); // Row }, ), // QueryBuilder ], ); } }
Вот три ключевые важные части в коде
QueryBowlScope
: это хранилище, в котором все запросы и мутации хранятся и распределяются по приложению.QueryJob
: Это то, что можно использовать для объявления и определения асинхронных операций, которые извлекают данные с сервера или из любого другого места.QueryBuilder
: он создает соответствующийQuery
, используя предоставленныйjob
и предоставляетQuery
в методе компоновщика. Это способ соединения данных и пользовательского интерфейса.
QueryBowlScope
и QueryJob
имеют множество свойств, которые можно настроить и изменить в соответствии с вашим приложением. Большинство их свойств некоторые. Но свойства, определенные в QueryBowlScope
, являются глобальной конфигурацией, где QueryJob
специфично для этого самого Query
Запрос внешних данных
Одна вещь, которую вы, возможно, заметили или думали, что функция задачи отделена от виджета, поэтому как бы вы использовали в ней другие службы, классы, методы или данные. Просто, как дать входные данные для задачи запроса или асинхронной операции? Здесь вступает в игру второй параметр обратного вызова задачи QueryJob
. Вы можете использовать именованный аргумент QueryBuilder
externalData
для передачи данных обратному вызову задачи. Вы можете буквально использовать что угодно в качестве externalData. Просто передайте тип как параметр типа в QueryJob
и QueryBuilder
Пример QueryJob с externalData:
// This job requires a pre-configured HTTP Client from the http package
// as externalData
// The first Type Parameter is the Type of returned Data & 2nd one is the Type
// of externalData
final anotherJob = QueryJob<String, Client>(
queryKey: "another-unique-key",
task: (queryKey, httpClient){
return httpClient
.get("https://jsonplaceholder.typicode.com/todos/1")
.then((response) => response.body);
}
);
Теперь давайте используем это задание внутри виджета.
Widget build(BuildContext context) { // getting the instance of Client provided by the [provider] package final client = Provider.of<Client>(context);
return QueryBuilder<String, void>( job: anotherJob, // passing the client as externalData externalData: client, builder: (context, query) { if (!query.hasData || query.isLoading) { return const CircularProgressIndicator(); } // remember to always show a fallback widget/screen for errors too. // It keeps the user aware of status of the application their using // & saves their time else if(query.hasError && query.isError){ return Text( "My disappointment is immeasurable & my day is ruined for this stupid error: $error", ); } return Row( children: [ // accessing the returned data & showing it Text(query.data["title"]), ElevatedButton( child: const Text("Refetch"), onPressed: () async { await query.refetch(); }, ), ], ); }, ); }
Вот и все. Это так просто предоставить externalData
задаче Query
По умолчанию при изменении
externalData
запрос не обновляется. Но вы можете изменить это поведение, если хотите, чтобы запрос обновлялся каждый раз при измененииexternalData
. Просто установите дляrefetchOnExternalDataChange
значение true вQueryJob
для этого конкретного запроса или вQueryBowlScope
для всех запросов.
Обновление запроса и время устаревания
Каждый запрос обновляется по мере необходимости. Но вы можете инициировать повторную загрузку для запроса или нескольких запросов вручную. Это может быть полезно после мутации или точно изменились данные приложения. Query.refetch
позволяет повторно получать один запрос, а QueryBowl.of(context).refetchQueries
позволяет повторно получать несколько запросов одновременно.
Вот пример повторной выборки по одному запросу:
ElevatedButton(
onPressed: () async {
await query.refetch();
}
child: Text("Refetch")
);
Теперь для повторного получения нескольких запросов:
TextField(
controller: controller,
onSubmitted: (value){
QueryBowl.of(context).refetchQueries(
[exampleJob1.queryKey, exampleJob2.queryKey]
);
}
);
Метод
QueryBowl.of
предоставляет доступ ко многим методам, используемым внутри Fl-Query. Это просто императивный способ сделать некоторые вещи, когда это необходимо. Он обеспечивает доступ ко многим полезным методам и свойствам, например.refetchQueries
,invalidateQueries
,resetQueries
,isFetching
и т. д.
Stale Time означает количество времени, в течение которого после запроса или нескольких запросов данные должны считаться устаревшими.
По умолчанию запросы становятся устаревшими, как только завершается выборка/восстановление. Но это можно настроить с помощью свойства staleTime
QueryBowlScope
для глобальной конфигурации или для каждого запроса с использованием свойства QueryJob
.
final job = QueryJob<String, Client>(
queryKey: "another-unique-key",
// now the data of the query will become stale after 30 seconds when the
// fetch/refetch executes
staleTime: Duration(seconds: 30),
task: (queryKey, httpClient){
return httpClient
.get("https://jsonplaceholder.typicode.com/todos/1")
.then((response) => response.body);
}
);
Статус запроса
Запрос может находиться в следующих состояниях
isSuccess
: Когда функция задачи успешно вернула данныеisError
: Когда функция задачи вернула ошибкуisLoading
: Когда функция задачи запущенаisRefetching
: Когда извлекаются новые данные или просто выполняется методrefetch
isIdle
: когда нет данных и задачаQuery
еще не запущена
Это статус выполнения запроса, есть и другой тип статуса запроса. Они называются Статус доступности данных. Ниже приведены:
hasData
: Когда запрос содержит данные, независимо от того, что происходит В основном,query.data != null
hasError
: Когда запрос содержит ошибку. В общем,query.error != null
Помните, не используйте
isLoading
только для индикаторов загрузки, так как даже если запросisSucessful
,data
все еще может быть нулевым. Так что используйте!query.hasData
||query.isLoading
, чтобы гарантировать отсутствие нулевых исключений
Это все о запросах. У него так много полезных функций и возможностей, что их невозможно описать в одной статье. Пожалуйста, посетите документы для получения дополнительной информации (https://fl-query.vercel.app)
Мутации
Запросы используются для получения данных или для запросов GET, где мутация, в отличие от запросов, обычно используется для создания/обновления/удаления данных или выполнения побочных эффектов сервера.
По сути, мутация — это тип асинхронной операции, которая изменяет уже имеющиеся данные или добавляет данные в хранилище или на удаленный сервер.
Так же, как QueryJob Mutation имеет MutationJob
, который можно использовать для определения операции мутации или настройки различных вещей.
Вот пример MutationJob:
final basicMutationJob = MutationJob<Map, Map<String, dynamic>>(
// instead of queryKey mutation has mutationKey
mutationKey: "unique-mutation-key",
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);
},
);
Обратный вызов task
MutationJob имеет переменный параметр. Может показаться, что externalData из QueryJob, но это немного другое. Вместо того, чтобы передавать данные через аргумент externalData QueryBuider, вы должны передать их через метод мутации mutate
или mutateAsync
.
Теперь давайте используем этот MutationJob
с нашим MutationBuilder
.
Widget build(context){
return MutationBuilder<Map, Map<String, dynamic>>(
job: basicMutationJob,
builder: (context, mutation) {
return Padding(
padding: const EdgeInsets.all(8.0),
// Its just basic Form
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;
// calling the mutate of the mutation
mutation.mutate({
"title": title,
"body": body,
"id": 42069, // the holy number as ID
}, onData: (data) {
// resetting the form
titleController.text = "";
bodyController.text = "";
});
},
child: const Text("Post"),
),
const SizedBox(height: 20),
// accessing the mutation result
if (mutation.hasData) Text("Response\n${mutation.data}"),
if (mutation.hasError) Text(mutation.error.toString()),
],
),
);
});
}
Выше есть 2 текстовых поля, которые предоставляют заголовок и тело публикации, и мы запускаем мутацию всякий раз, когда нажимается кнопка отправки. Метод mutate
принимает 3 аргумента
- 1-й параметр/переменные
onData
: обратный вызов, который запускается, когда результат доступен и мутация прошла успешноonError
: обратный вызов, который запускается, когда результат доступен и мутация прошла успешно
Оптимистичные обновления
Самая интересная часть Mutation
s — это обратный вызов onMutate
для MutationBuilder
. Это обратный вызов, который запускается непосредственно перед выполнением MutationJob.task
. Здесь вы можете делать всякие сумасшедшие вещи. Например, добавление прогнозируемых данных к различным запросам до того, как данные будут возвращены с сервера, или удаление всего запроса или всего, что вы хотите. Но в сочетании с onData
вы можете оптимизировать данные своего приложения, чтобы пользователю не пришлось ждать
Оптимистическое обновление означает обновление данных запроса предсказуемыми данными, прежде чем фактически будет получен какой-либо результат. Затем, когда поступят реальные данные, замените спрогнозированные данные реальными данными, даже не сообщая пользователю об этом.
Вот простой пример оптимистичных обновлений:
MutationBuilder<Map, String>(
job: newUsernameMutation,
onMutate: (value) {
// getting the query that needs to be updated optimistically
QueryBowl.of(context)
.setQueryData<UserData, void>(job.queryKey, (oldData) {
oldData.name = value;
// you've to return a new instance of the oldData else fl-query
// will assume data hasn't been updated thus won't trigger any changes
return UserData.from(oldData);
});
},
onData: (data){
// replacing the predicted fake data with real data
QueryBowl.of(context)
.setQueryData<UserData, void>(job.queryKey, (oldData) {
oldData.name = data["name"];
// you've to return a new instance of the oldData else fl-query
// will assume data hasn't been updated thus won't trigger any changes
return UserData.from(oldData);
});
}
builder: (context, mutation){
....
....
....
}
);
Это все для мутаций. Чтобы узнать больше о мутации, прочитайте документы
Крючки
Fl-Query поддерживает как Vanilla Flutter, так и flutter_hooks. Оба не сильно отличаются, все одинаково, но в fl_query_hooks вы дополнительно получаете 2 хука useQuery
и useMutation
, которые вы можете использовать вместо QueryBuilder
и MutationBuilder
.
использовать запрос
Это в основном QueryBuilder
без всего типичного шаблона Builder. Итак, когда я напишу первый пример с хуками, он будет выглядеть так:
class BasicHookExample extends HookWidget { const BasicHookExample({Key? key}) : super(key: key);
@override Widget build(BuildContext context) { final query = useQuery(job: successJob, externalData: null);
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(); }, ), // Text ], ); // Row } }
использованиеМутация
Думаю, вы знаете, что делает useMutation
. Это замена MutationBuilder
для flutter_hooks, которая делает код более чистым и легким для чтения.
Вот пример useMutation
:
Widget build(context){ // the mutation object is the same passed as // parameter in the builder method of MutationBuilder final mutation = useMutation(job);
return /* .... (Imaginary Form) */; }
В этой статье рассматриваются только простые и наиболее часто используемые функции Fl-Query. С помощью Fl-Query мы можем делать множество других вещей. Все доступно в официальной документации, которая является WIP. Так что вы можете внести свой вклад в это, если хотите. Это действительно поможет проекту двигаться вперед
Fl-Query все еще находится в стадии интенсивной разработки, и ожидается, что в нем будут ошибки и непредвиденное поведение. Поэтому, если вы найдете что-то, пожалуйста, создайте проблему с соответствующими подробностями. Кроме того, мы открыты для любых предложений. Предложите, что вам нравится или нет, или хотите, чтобы Fl-Query был. Возможно, вы внесете свой вклад в проект со своим собственным кодом и функцией. Мы будем очень признательны
Поскольку проект находится на очень ранней стадии, ему нужны соответствующие тесты, а я — худшее оправдание для тестировщика, поэтому Fl-Query нужны хорошие тестировщики, которые готовы внести свой вклад в проект с помощью тестов. Если вы хотите внести свой вклад в тесты. Пожалуйста, присоединитесь к обсуждению, создав его
Дайте Fl-Query ⭐Звезду⭐ на Github
Социальное
Следите за мной в:
- Твиттер
- ЛинкедИн
- Гитхаб
- DEVCommunity
- "Середина"