Skip to content

sofiyevsr/flutter_remoter

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

88 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

GitHub top language License: MIT


Remoter aims to simplify handling asynchronous operations and revalidating them, inspired by React Query

Features

  • Global and individual options
  • Cache is collected after cache time if there is no listener
  • Query is refetched if query is stale
  • Fetch only once when multiple widget mounts at the same time
  • Pagination
  • Invalidate query
  • Set query data manually
  • Retry query when new widget mounts
  • Auto retry with exponential backoff
  • Mutation widget

Getting started

Install dependencies

dependencies:
  flutter_remoter: ^0.2.0

Wrap your app with RemoterProvider

RemoterProvider expects a RemoterClient which you can export from package and use everywhere without context.

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return RemoterProvider(
      client: RemoterClient(
        // This line defines default options for all queries
        // You can override options in each query
        options: RemoterOptions(
            // staleTime defines how many ms after query fetched can be refetched
            // Use infinite staleTime if you don't need queries to be refetched when new query mounts
            // 1 << 31 is max int32
            // default is 0ms
            staleTime: 0,
            // cacheTime defines how many ms after all listeners are gone query data should be cleared,
            // default is 5 minutes
            cacheTime: 5 * 60 * 1000,
            // Maximum delay between retries in ms
            maxDelay: 5 * 60 * 1000,
            // Maximum amount of retries
            maxRetries: 3,
            // Flag that decides if query that has error status should be refetched on mount
            retryOnMount: true,
        ),
      ),
      child: const MaterialApp(
        home: MyHomePage(),
      ),
    );
  }
}

Usage

There are three types of widgets: RemoterQuery, PaginatedRemoterQuery and RemoterMutation.

Remoter Query

Used for 'single page' data

See full example

    RemoterQuery<T>(
      remoterKey: "key",
      listener: (oldState, newState) async {
        // Optional state listener
      },
      execute: () async {
        // Fetch data here
      },
      // Override default options defined in RemoterClient
      // You don't have to copy the fields you don't want to override
      // e.g Default is RemoterOptions(cacheTime: 2000, staleTime: 1000).
      // You want to override staleTime for specific query, use RemoterOptions(staleTime: 1000).
      // In this case cacheTime won't be overriden and will still be 2000
      options: RemoterOptions(),
      // Query won't start if this is true
      disabled: false,
      builder: (context, snapshot, utils) {
        if (snapshot.status == RemoterStatus.idle) {
          // You can skip this check if you don't use disabled parameter
        }
        if (snapshot.status == RemoterStatus.fetching) {
          // Handle fetching state here
        }
        if (snapshot.status == RemoterStatus.error) {
          // Handle error here
        }
        // It is okay to use snapshot.data! here
        return ...
      },
    )

Paginated Remoter Query

Used for data that has multiple pages or "infinite scroll" like experience.

See full example

    PaginatedRemoterQuery<FactsPage>(
          // remoterKey should be unique
          remoterKey: "facts",
          // Data returned from these functions will be passed
          // as param to execute function
          getNextPageParam: (pages) {
            return pages[pages.length - 1].nextPage;
          },
          getPreviousPageParam: (pages) {
            return pages[0].previousPage;
          },
          // Override default options defined in RemoterClient
          // You don't have to copy the fields you don't want to override
          // e.g Default is RemoterOptions(cacheTime: 2000, staleTime: 1000).
          // You want to override staleTime for specific query, use RemoterOptions(staleTime: 1000).
          // In this case cacheTime won't be overriden and will still be 2000
          options: RemoterOptions(),
          execute: (param) async {
            // Fetch data here
          },
          // Query won't start if this is true
          disabled: false,
          builder: (context, snapshot, utils) {
            if (snapshot.status == RemoterStatus.idle) {
              // You can skip this check if you don't use disabled parameter
            }
            if (snapshot.status == RemoterStatus.fetching) {
              // Handle fetching state here
            }
            if (snapshot.status == RemoterStatus.error) {
              // Handle error here
            }
            // It is okay to use snapshot.data! here
            return SingleChildScrollView(
              child: Column(
                children: [
                  if (snapshot.hasPreviousPage)
                    ElevatedButton(
                      onPressed: () {
                        utils.fetchPreviousPage();
                      },
                      child: snapshot.isFetchingPreviousPage == true
                          ? const CircularProgressIndicator(
                                color: Colors.white,
                            )
                          : const Text("Load previous"),
                    ),
                  ...snapshot.data!
                      .expand((el) => el.facts)
                      .map(
                        (d) => Text(d.fact),
                      )
                      .toList(),
                  if (snapshot.hasNextPage)
                    ElevatedButton(
                      onPressed: () {
                        utils.fetchNextPage();
                      },
                      child: snapshot.isFetchingNextPage == true
                          ? const CircularProgressIndicator(
                              color: Colors.white,
                            )
                          : const Text("Load more"),
                    ),
                ],
              ),
            );
          })

Remoter Mutation

Used to simplify handling asynchronous calls
T represents type of the value execute function returns
S represents type of the value passed to mutate function which will be passed to execute function as parameter

See example

  RemoterMutation<T, S>(
   execute: (param) async {
     await Future.delayed(const Duration(seconds: 1));
     return ...
   },
   builder: (context, snapshot, utils) {
          if (snapshot.status == RemoterStatus.idle) {
            // Mutation hasn't started yet
          }
          if (snapshot.status == RemoterStatus.fetching) {
            // Handle fetching state here
          }
          if (snapshot.status == RemoterStatus.error) {
            // Handle error here
          }
          // It is okay to use snapshot.data! here
          return Text(
           snapshot.data!,
          );
       },
       floatingActionButton: FloatingActionButton(
         onPressed: snapshot.status == RemoterStatus.fetching
             ? null
             : () {
                 // Starts mutation
                 // In this case null will be passed to execute as param
                 utils.mutate(null);
               },
         child: const Icon(Icons.add),
       ),
     );
  });

Using RemoterClient

There are 2 ways to retrieve RemoterClient

With BuildContext

   RemoterProvider.of(context).client

Without BuildContext

To use RemoterClient without context, you can create RemoterClient in separate file.
Then that instance should be use with RemoterProvider which wraps the App.
Finally, you can import and use the instance anywhere in your app.

import 'path to RemoterClient instance';

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return RemoterProvider(
      // 'client' is the instance from import
      client: client,
      child: const MaterialApp(
        home: MyHomePage(),
      ),
    );
  }
}