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 inFutureBuilder
, which cannot refresh the widget without disposing of its state. The usual solution to this it to useStreamWidget
combined with aStreamController
. 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
offersonError
,onData
,onLoading
callbacks.- The package takes inspiration from Riverpod's
FutureProvider
, but works entirely locally. Itsinvalidate
/refresh
callbacks also inspired achieve something similar (see refresh types).
- The package takes inspiration from Riverpod's
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'),
);
},
);
...
}
)
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 plainFuture
, since it is possible to "regenerate the initial future" (by invokingfutureProvider
, 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
);
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)' : ''}'),
);
},
);
...
}
)
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();
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.
);
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
);
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
andnonPriorityUnfiltered
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
andnonPriorityUnfiltered
refresh requests are discarded as long as the current one has not completed. - Unlike
nonPriorityDebounced
, if one ofpriorityDebounced
orpriorityUnfiltered
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'),
);
},
);
...
}
)
Drawbacks
Using FutureWidget directly inside FutureWidgetWrapper's builder body had/would have the following drawbacks:
- Repeated casting
FutureWidgetWrapper
andFutureWidget
- especially bad if very long
- Unintuitive
futureContext
variable (wrapperbuilder
argument)- Passed to
FutureWidget
- Felt somewhat unnecessary
- Passed to
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 toFutureWidget
in IDEs, sincefutureWidgetCtor
is not even directly referencing the actual constructor, but it is a special function whose params match all of the actual constructor param except forfutureContext
.- (now it gets technical, feel free to skip)
This is done internally via currying: a Future, aFutureContext
is already ready to be passed as argument (to the actual constructor) insideFutureWidgetWrapper
'sbuild
function.- Using currying to pass the
FutureContext
helps achieve the current result, since it would not be beneficial to require afutureContext
inFutureWidget
's builder, becausefutureContext
's future needs to be processed inFutureWidgetWrapper
, beforeFutureWidget
'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 theFutureWidget
object would therefore be lost on everyFutureWidgetWrapper
rebuild. I.e., on every rebuild of theFutureWidget
instanceonLoading
would be invoked.
- This is exactly what happens when working with Flutter's
- Using currying to pass the
- The
CTRL+click
issue can be mitigated by jumping toFutureWidgetWrapper
's definition and thenCTRL+click
ing on the import on top:import 'future_widget.dart';
- (now it gets technical, feel free to skip)
Familiarity with this library can mitigate the drawbacks.
Extra
Mention library i took inspiration from
Libraries
- future_widget
- Library providing
FutureWidget
andFutureWidgetWrapper
.