Remoter aims to simplify handling asynchronous operations and revalidating them, inspired by React Query
- 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
dependencies:
flutter_remoter: ^0.2.0
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(),
),
);
}
}
There are three types of widgets: RemoterQuery, PaginatedRemoterQuery and RemoterMutation.
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 ...
},
)
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"),
),
],
),
);
})
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),
),
);
});
There are 2 ways to retrieve RemoterClient
RemoterProvider.of(context).client
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(),
),
);
}
}