Если вы знакомы с 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: обратный вызов, который запускается, когда результат доступен и мутация прошла успешно

Оптимистичные обновления

Самая интересная часть Mutations — это обратный вызов 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

Социальное

Следите за мной в: