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

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 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
);

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 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'),
              );
            },
        );
        ...
    }
)

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';
      

Familiarity with this library can mitigate the drawbacks.

Extra

Mention library i took inspiration from

Libraries

future_widget
Library providing FutureWidget and FutureWidgetWrapper.