Ако сте запознати с „Flutter“, определено сте запознати с FutureBuilder. В крайна сметка това е единственият начин, по който можете да извлечете данни във Flutter с правилно състояние, или не е така? Но може би сте забелязали, че всеки път, когато Widget се преустройва, Future също се изпълнява, освен ако не декларирате своя Future в initState

Но дори и да декларирате бъдещето си в initState, за да избегнете повторно изпълнение при реконструкции, то пак ще се стартира отново, когато Widget бъде премахнат и монтиран отново. И така, какво е решението? Отг.: FutureProvider от riverpod или доставчик

Чакай малко, но това не е друга статия за riverpod/доставчик? Не, не е.

Да, за да кеширате вашите бъдещи резултати или отговор на сървъра, можете да използвате FutureProvider. Той може да съхранява и разпространява резултата в цялото приложение без множество повторения на една и съща операция. Но какво се случва, когато данните изтекат или остареят? Какво ще стане, ако приложението ви няма да се нуждае от тези данни след известно време, но данните все още губят RAM?

Тук Fl-Query влиза в действие. Това е Async Data + Mutation Manager за Flutter, който кешира, извлича, автоматично извлича остарели данни. Подобно на Tanstack Query в света на уеб разработката, но само концепцията е внедрена и API е много подобно на това, с което Flutter Developers са свикнали и така кара всеки да се чувства като у дома си

Стига приказки, нека преминем към голямата част.

Какво предлага?

  • Асинхронно кеширане и обезсилване на данни
  • Интелигентен и силно конфигурируем механизъм за повторно извличане, който интелигентно актуализира остарели/изтекли данни във фонов режим, когато е необходимо
  • Декларативен начин за дефиниране на асинхронни операции
  • Заявка за събиране на отпадъци и мутация. Това означава, че неизползваните заявки, които стоят в кеша за дълго време, се премахват автоматично
  • Повторна употреба на код и данни поради постоянни данни и 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

Запитване за външни данни

Едно нещо, което може би сте забелязали или сте си мислили, че функцията за задачи е отделена от Widget, така че как бихте използвали други услуги, класове, методи или данни в него. Просто как ще се дадат входни данни за задача за заявка или асинхронна операция? Тук влиза в действие вторият параметър на обратното извикване на задачата на QueryJob. Можете да използвате именувания аргумент externalData на QueryBuilder, за да предавате данни към обратното извикване на задачата. Можете буквално да използвате всичко като 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

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 на задача за заявка

По подразбиране, когато 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 и т.н.

Остаряло време означава периодът от време, когато след заявка или данни от множество заявки трябва да се считат за остарели

По подразбиране заявките стават остарели веднага след като извличането/повторното извличане завърши. Но това може да се конфигурира с помощта на свойството 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 заявки, където Mutation са, за разлика от заявките, обикновено използвани за създаване/актуализиране/изтриване на данни или извършване на странични ефекти на сървъра

По принцип мутацията е вид асинхронна операция, която променя вече наличните данни или добавя данни в магазин или отдалечен сървър

Точно както 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 метод на Mutation

Сега нека използваме този 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.

useQuery

По принцип е 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

Предполагам, че знаете какво прави 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 да има. Вероятно ще допринесете за проекта със собствен код и функция. Ще бъде много оценено

Тъй като проектът е в много ранен етап, той се нуждае от подходящи тестове и аз съм най-лошото извинение за Tester, така че Fl-Query се нуждае от няколко добри тестери, които желаят да допринесат за проекта с тестове. Ако искате да допринесете с тестове. Моля, присъединете се към дискусията, като създадете такава

Дайте ⭐звезда⭐ на Fl-Query в Github

Социални

Следвайте ме на: