Last updated:
0 purchases
future widget
future_widget #
A pragmatic, clear and more declarative alternative to FutureBuilder/StreamBuilder.
Motivation
The future_widget package provides a solution for caching state locally and prioritizes local state as a first-class citizen.
FutureWidget does not have the refreshing limitation that is present in FutureBuilder, which cannot refresh the widget without disposing of its state. The usual solution to this it to use StreamWidget combined with a StreamController. While this works, FutureWidget allows you to achieve the same results without using streams.
Unlike FutureBuilder (which can be cumbersome to write code with), FutureWidget offers onError, onData, onLoading callbacks.
The package takes inspiration from Riverpod's FutureProvider, but works entirely locally. Its invalidate/refresh callbacks also inspired achieve something similar (see refresh types).
Recommended approach
If you want to jump straight to the recommended approach, defer reading the
FutureWidget section for now and consult FutureWidgetWrapper directly.
Direct usage of FutureBuilder is not bad or wrong, however it cannot be
recommended by me given the abstraction layer FutureWidgetWrapper provides.
FutureWidget #
FutureWidget is brought into the widget tree similarly to FutureBuilder/StreamBuilder, since it has to be wrapped around a
stateful widget and setState calls are necessary for refreshes (i.e. changing the future being
listened to).
!! TODO: add FutureWidget documentation !! #
TODO: add FutureWidget documentation
/// The reason for having the abstract class [AbstractFutureGen] and separating
/// [FutureGen] and [FutureGenWithInitialData] is that this way there is a
/// 1-to-1 mapping between constructors and exact value notifier types.
///
/// E.g.:
/// - [FutureWidget.initialData] - [FutureGenWithInitialData]
/// - [FutureWidget.unsafeNoError] - [FutureGen]
///
/// If all the logic would have been in [FutureGen], and
/// [FutureGenWithInitialData] would have just extended [FutureGen], then the
/// following would have been allowed:
///
/// - [FutureWidget] (default ctor) - [FutureGenWithInitialData]
/// - [FutureWidget.unsafeNoError] - [FutureGenWithInitialData]
Direct usage of FutureBuilder not recommended #
As written on top, this is a widget you most likely do not want to use directly in your
projects/libraries.
FutureWidgetWrapper offers an abstraction level, and providers a required builder param
providing the callback argument futureWidgetCtor (which kind of points to the right FutureWidget
constructor, based on the FutureWidgetWrapper constructor used).
To be exact, futureWidgetCtor does not point to the constructor. This technical detail is explained in the drawbacks.
FutureWidgetWrapper #
FutureWidgetWrapper provides a high-level abstraction layer for FutureWidget, which allows developers to perform many
actions using a single refresh callback, along with optional parameters.
By providing an abstraction layer for FutureWidget refreshes, FutureWidgetWrapper eliminates
the requirement for a stateful widget (which it uses internally to manage setState calls).
However, it should be noted that a stateful widget may still be necessary for other
functionalities when used in conjunction with FutureWidgetWrapper.
A glimpse into FutureWidgetWrapper #
FutureWidgetWrapper<int>(
futureProvider: () => myFuture4(), // or just: myFuture4
builder: (context, futureWidgetCtor, isRefreshing, refresh) {
...
futureWidgetCtor(
onData: (context, data) {
return Center(
child: Text(
'data is: $data${isRefreshing ? '(REFRESHING)' : ''}'),
);
},
onError: (context, error, stackTrace) {
return Center(
child: Text(
'error is: $error${isRefreshing ? '(REFRESHING)' : ''}'),
);
},
onLoading: (context) {
return const Center(
child: Text('loading'),
);
},
);
...
}
)
copied to clipboard
futureProvider #
FutureWidgetWrapper's constructor has a required param futureProvider: a function returning a Future<T> is to be passed there.
NB: the user does not specify a simple Future<T> because that way refreshing
the initial future would be impossible.
For refreshing purposes, a function
returning a Future makes therefore more sense than a plain Future, since it is
possible to "regenerate the initial future" (by invoking futureProvider, i.e., futureProvider()), so
that it can be awaited again.
(deprecated) It is possible to update the future provider function with a refresh:
refresh(
setFutureProvider: () => myFuture9(), // or just: myFuture9
);
copied to clipboard
What happens here?
myFuture9 waits for 2 seconds and then returns 9. isRefreshing will therefore be true for 2 seconds.
Constructors #
FutureWidgetWrapper has the same constructors names as FutureWidget:
Default constructor
initialData
unsafeNoError
unsafeOnlyData
The arguments of futureWidgetCtor will automatically adapt to the FutureWidgetWrapper constructor in use, resulting in a nice IDE experience as well.
Example with unsafeOnlyData:
FutureWidgetWrapper<int>.unsafeOnlyData(
futureProvider: () => myFuture4(), // or just: myFuture4
builder: (context, futureWidgetCtor, isRefreshing, refresh) {
...
futureWidgetCtor(
initialData: 33,
onData: (context, data) {
return Center(
child: Text(
'data is: $data${isRefreshing ? '(REFRESHING)' : ''}'),
);
},
);
...
}
)
copied to clipboard
myFuture4 will return 4 after 2 seconds. So the FutureWidget will display 33 for 2 seconds and then display 4.
Refresh #
FutureWidgetWrapper's build function provides refresh, which can be used without passing any param:
refresh();
copied to clipboard
which usually corresponds to:
refresh(
setFutureProvider: last future provider if omitted, // or initial future provider (if tmp../refreshFallbackValues: FallbackValues.initState)
tmpRefreshType: initial refresh type if omitted,
tmpFutureFallbackValue: FutureFallbackValue.lastState, // or FallbackValues.initState (if set in refreshFallbackValues)
disposeState: false // always false by default.
);
copied to clipboard
Depending on one's needs, the future provider can be updated using setFutureProvider (see below). However, this is deprecated. The same should be possible to be achieved by tying logic inside the futureProvider callback. In case this is not satisfactory, please open an issue.
The refresh type and the fallback values will always default to the same values as those defined in FutureWidgetWrapper's constructor (refreshType and futureFallbackValue params), unless temporarily overridden (for one refresh):
refresh(
setFutureProvider: () => myFuture9(), // or just: myFuture9
tmpRefreshType: RefreshType.nonPriorityDebounced,
tmpFutureFallbackValue: FutureFallbackValue.lastState,
disposeState: false
);
copied to clipboard
The state of the widget can also be disposed by passing true to disposeState. In that case, onLoading will be invoked the next time the FutureWidget instantiated by futureWidgetCtor calls build. However, instead of disposing state like this, try first using isRefreshing in the onData and/or onError callbacks.
isRefreshing makes it very simple, e.g., to add a spinner next to the last fetched error or data, making the end user aware that new data is being fetched (and will likely override what is shown on the screen). This is arguably more user friendly than disposing state and invoking onLoading while awaiting.
Where to use refresh
Invoke refresh inside callbacks. Do not call it inside build or other lifecycle functions.
Refresh Types
[nonPriorityDebounced]:
This option only allows refreshing one future
at a time — with the precondition being there is no refresh already
happening.
All future [nonPriorityDebounced] and [nonPriorityUnfiltered]
refresh requests are discarded as long as the current one has not
completed.
"Debounced" indicates the process of filtering out noise.
[nonPriorityUnfiltered]:
This option only allows refreshing one future
at a time — with the precondition being there is no refresh already
happening.
All future [nonPriorityDebounced] and [nonPriorityUnfiltered]
refresh requests are discarded as long as the current one has not
completed.
Unlike [nonPriorityDebounced], if one of [priorityDebounced]
or [priorityUnfiltered] are
called while the future is in progress, the [FutureWidget] will
display the value for a short amount of time, before being replaced by
the value/exception emitted by later invoked, completed future.
"Unfiltered" indicates the absence of such a filtering process.
[priorityDebounced]:
It has higher priority than non-priority refreshes.
This option allows multiple refreshes, but debounces the refreshes to
prevent rebuilds in quick succession.
[priorityUnfiltered]:
It has higher priority than non-priority refreshes.
This option allows immediate rebuilds in quick succession: it doesn't
debounce the refreshes; instead, it triggers a rebuild
immediately after a refresh is requested.
Do not try to use FutureWidget directly inside FutureWidgetWrapper's builder body #
It won't work anyway.
During development, in a previous version of this library, the FutureWidget was supposed to be used directly inside FutureWidgetWrapper's builder.
Example:
FutureWidgetWrapper<List<List<StoreItems>>>(
futureProvider: ...,
builder: (context, futureContext, refresh) { // isRefreshing used to be provided with onData and onError callbacks
...
FutureWidget<List<List<StoreItems>>>(
futureContext: futureContext,
onData: (context, data, isRefreshing) { // isRefreshing used to be here
return Center(
child: Text(
'data is: $data${isRefreshing ? '(REFRESHING)' : ''}'),
);
},
onError: (context, error, stackTrace, isRefreshing) { // isRefreshing used to be here
return Center(
child: Text(
'error is: $error${isRefreshing ? '(REFRESHING)' : ''}'),
);
},
onLoading: (context) {
return const Center(
child: Text('loading'),
);
},
);
...
}
)
copied to clipboard
Drawbacks
Using FutureWidget directly inside FutureWidgetWrapper's builder body had/would have the following drawbacks:
Repeated casting
FutureWidgetWrapper and FutureWidget
especially bad if very long
Unintuitive futureContext variable (wrapper builder argument)
Passed to FutureWidget
Felt somewhat unnecessary
The current version of flutter_widget no longer has these issues.
The drawbacks that comes with the current, futureWidgetCtor approach are:
The absence of docstring, however this is always the case with callback arguments.
Impossibility to CTRL+click to jump to FutureWidget in IDEs, since futureWidgetCtor
is not even directly referencing the actual constructor, but it is a special function
whose params match all of the actual constructor param except for futureContext.
(now it gets technical, feel free to skip)
This is done internally via currying: a Future, a FutureContext is already ready to
be passed as argument (to the actual constructor) inside FutureWidgetWrapper's build function.
Using currying to pass the FutureContext helps achieve the current result, since
it would not be beneficial to require a futureContext in FutureWidget's builder, because futureContext's future needs
to be processed in FutureWidgetWrapper, before FutureWidget's constructor is called.
This is exactly what happens when working with Flutter's FutureBuilder and its enclosing stateful widget.
If futureWidgetCtor was referencing the actual constructor, the state of the FutureWidget object would
therefore be lost on every FutureWidgetWrapper rebuild. I.e., on every rebuild of the FutureWidget instance
onLoading would be invoked.
The CTRL+click issue can be mitigated by jumping to FutureWidgetWrapper's definition and then
CTRL+clicking on the import on top:
import 'future_widget.dart';
copied to clipboard
Familiarity with this library can mitigate the drawbacks.
Extra #
Mention library i took inspiration from #
For personal and professional use. You cannot resell or redistribute these repositories in their original state.
There are no reviews.