futuristic 0.1.3

  • Readme
  • Changelog
  • Example
  • Installing
  • 82

futuristic #

Makes it possible to safely execute and retry a Future inside a StatelessWidget.

See the Mainstream package for a similar API for working with Streams.

Problem #

If you've ever tried to use the FutureBuilder widget in Flutter, you've probably been surprised by its behavior. When used inside a StatelessWidget, it will re-execute its Future every time it is rebuilt. Since a widget can be rebuilt many times in Flutter (including due to hot reload), this can be undesirable if our Future calls a non-idempotent REST API endpoint, for example.

class Home extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return FutureBuilder(
      future: myExpensiveFuture(),  //Will be executed every time Home is rebuilt
      builder: (_context, snapshot) {
        ...
      },
    );
  }
}

To execute our Future only once, we could use a StatefulWidget, but now we have the extra boilerplate of using a StatefulWidget and holding onto our Future in a state variable.

class Home extends StatefulWidget {
  @override
  _HomeState createState() => _HomeState();
}

class _HomeState extends State<Home> {
  Future _future;

  @override
  void initState() {
    super.initState();
    _future = myExpensiveFuture();
  }

  @override
  Widget build(BuildContext context) {
    return FutureBuilder(
      future: _future,  // Will be executed only once
      builder: (_context, snapshot) {
        ...
      },
    );
  }
}

Solution #

The problem with FutureBuilder is, ironically, that it takes a Future instance as its input. Instead, the Futuristic widget takes a Function that returns a Future and holds onto it in its own State. This means:

  • It can be used in a StatelessWidget.
  • It can let child widgets start or retry a Future.

Additionally, Futuristic provides:

  • Multiple builder callbacks to provide mutually exclusive initial/busy/data/error widget states.
  • Optional onData/onError callbacks to perform additional actions when a Future succeeds or fails.
  • Generic type safety for the data provided to callbacks. The type parameter <T> can be omitted if it can be inferred from the futureBuilder function.

Usage #

We can use the Futuristic widget to wrap a single component like a button, or even an entire screen.

Button example #

To start executing a Future in response to a button press, connect its onPressed handler to the start function provided in the initialBuilder callback.

Future<int> myFuture(int first, int second) async {
  await Future.delayed(const Duration(seconds: 1));
  return first + second;
}

class MyButton extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Futuristic<int>(
      futureBuilder: () => myFuture(1, 2),
      initialBuilder: (context, start) => RaisedButton(child: Text('Go'), onPressed: start),
      busyBuilder: (context) => CircularProgressIndicator(),
      errorBuilder: (context, error, retry) => RaisedButton(child: Text('Oops'), onPressed: retry),
      dataBuilder: (context, data) => Text(data.toString()),
    );
  }
}

The futureBuilder parameter is required.

The initialBuilder parameter is required if autoStart is false (see example below).

The optional busyBuilder displays a widget when the Future is busy executing. By default, it shows a centered CircularProgressIndicator. By displaying this, we inform the user that the operation is in progress and also prevent the Future from being triggered twice accidentally.

The optional errorBuilder displays a widget when the Future has failed, typically with an Error or Exception. This is provided as a parameter, together with a retry function that can be called to "retry" the Future.

The optional dataBuilder displays a widget when the Future has succeded. The resulting T value of the Future<T> is provided as a parameter to the callback. Note that this will be null in the case of a Future<void>.

Screen example #

To automatically start executing a Future upon navigating to a screen, set the autoStart parameter to true instead of providing an initialBuilder. The busyBuilder will be displayed immediately.

Future<int> myFuture(int first, int second) async {
  await Future.delayed(const Duration(seconds: 1));
  throw Exception('something happened');
  return first + second;
}

class MyScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Futuristic<int>(
      autoStart: true,
      futureBuilder: () => myFuture(1, 2),
      busyBuilder: (context) => CircularProgressIndicator(),
      onError: (error, retry) => showDialog(...),
      onData: (data) => showDialog(...),
    );
  }
}

The futureBuilder parameter is required.

The optional onError callback can be used to handle the error event by displaying an alert dialog or sending to a logging provider, for example. It can be used in place of or together with the errorBuilder. A retry function is provided as a parameter that can be called to "retry" the Future. Be careful not to call retry without user interaction to avoid creating an infinite loop. This callback will not be retriggered as a result of a widget rebuild.

The optional onData callback can be used to handle a successful result by displaying an alert dialog or performing navigation. for example. This can be used in place of or together with the dataBuilder. This callback will not be retriggered as a result of a widget rebuild.

0.1.3 #

  • Added link to futuristic

0.1.2 #

  • Added pedantic
  • Readme copy updates
  • Code cleanup

0.1.1 #

  • Removed readme from example project
  • Readme copy updates
  • Updated license file

0.1.0 #

  • Initial Open Source release

example/lib/main.dart

import 'package:flutter/material.dart';
import 'package:futuristic/futuristic.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Home(),
      routes: {
        GoodScreen.routeName: (_) => GoodScreen(),
        BadScreen.routeName: (_) => BadScreen(),
      },
    );
  }
}

/// A future that completes successfully.
Future<int> goodFuture(int first, int second) async {
  await Future.delayed(const Duration(seconds: 1));
  return first + second;
}

// A future that completes with an exception.
Future<int> badFuture(int first, int second) async {
  await Future.delayed(const Duration(seconds: 1));
  throw Exception('Something happened');
}

class Home extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: <Widget>[
            Container(height: 50, child: Center(child: GoodButton())),
            Container(height: 50, child: Center(child: BadButton())),
            RaisedButton(
              child: Text('Good screen example'),
              onPressed: () {
                Navigator.of(context).pushNamed(GoodScreen.routeName);
              },
            ),
            RaisedButton(
              child: Text('Bad screen example'),
              onPressed: () {
                Navigator.of(context).pushNamed(BadScreen.routeName);
              },
            )
          ],
        ),
      ),
    );
  }
}

class GoodButton extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Futuristic<int>(
      futureBuilder: () => goodFuture(1, 2),
      initialBuilder: (_, start) => RaisedButton(child: Text('Good button example'), onPressed: start),
      busyBuilder: (_) => CircularProgressIndicator(),
      dataBuilder: (_, data) => Text(data.toString()),
    );
  }
}

class BadButton extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Futuristic<int>(
      futureBuilder: () => badFuture(1, 2),
      initialBuilder: (_, start) => RaisedButton(child: Text('Bad button example'), onPressed: start),
      busyBuilder: (_) => CircularProgressIndicator(),
      errorBuilder: (_, error, retry) => RaisedButton(child: Text('Sorry! Try again'), onPressed: retry),
    );
  }
}

class GoodScreen extends StatelessWidget {
  static const routeName = '/good';
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Good screen')),
      body: Center(
        child: Futuristic<int>(
          autoStart: true,
          futureBuilder: () => goodFuture(1, 2),
          busyBuilder: (_) => CircularProgressIndicator(),
          dataBuilder: (_, data) => Text('Data is $data'),
        ),
      ),
    );
  }
}

class BadScreen extends StatelessWidget {
  static const routeName = '/bad';
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Bad screen')),
      body: Center(
        child: Futuristic<int>(
          autoStart: true,
          futureBuilder: () => badFuture(1, 2),
          busyBuilder: (_) => CircularProgressIndicator(),
          onError: (error, retry) {
            showDialog(
              context: context,
              child: AlertDialog(
                content: Text('Sorry! Try again'),
                actions: <Widget>[
                  FlatButton(
                    child: Text('RETRY'),
                    onPressed: () {
                      Navigator.of(context).pop();
                      retry();
                    },
                  )
                ],
              ),
            );
          },
        ),
      ),
    );
  }
}

Use this package as a library

1. Depend on it

Add this to your package's pubspec.yaml file:


dependencies:
  futuristic: ^0.1.3

2. Install it

You can install packages from the command line:

with Flutter:


$ flutter pub get

Alternatively, your editor might support flutter pub get. Check the docs for your editor to learn more.

3. Import it

Now in your Dart code, you can use:


import 'package:futuristic/futuristic.dart';
  
Popularity:
Describes how popular the package is relative to other packages. [more]
63
Health:
Code health derived from static analysis. [more]
100
Maintenance:
Reflects how tidy and up-to-date the package is. [more]
100
Overall:
Weighted score of the above. [more]
82
Learn more about scoring.

We analyzed this package on Jul 9, 2020, and provided a score, details, and suggestions below. Analysis was completed with status completed using:

  • Dart: 2.8.4
  • pana: 0.13.14
  • Flutter: 1.17.5

Analysis suggestions

Package not compatible with SDK dart

Because:

  • futuristic that is a package requiring null.

Health suggestions

Format lib/futuristic.dart.

Run flutter format to format lib/futuristic.dart.

Dependencies

Package Constraint Resolved Available
Direct dependencies
Dart SDK >=2.7.0 <3.0.0
flutter 0.0.0
Transitive dependencies
collection 1.14.12 1.14.13
meta 1.1.8 1.2.1
sky_engine 0.0.99
typed_data 1.1.6 1.2.0
vector_math 2.0.8 2.1.0-nullsafety
Dev dependencies
flutter_test
pedantic ^1.9.0