floop 1.1.0

  • Readme
  • Changelog
  • Example
  • Installing
  • 79

Floop #

Dynamic values for Flutter widgets. Allows building interactive apps using purely Stateless Widgets. Inspired by react-recollect.

How to use #

  • Add with Floop at the end of the widget class definition
  • Read any value from floop and the widget will reactively update on changes to the value

Example:

-class Clicker extends StatelessWidget {
+class Clicker extends StatelessWidget with Floop {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      // Text widget always displays the current value of #clicks
      body: Center(
        child: Text(floop[#clicks].toString())
      ),
      floatingActionButton: FloatingActionButton(
        child: Icon(Icons.add),
        // change #clicks from anywhere in the app (except build methods)
        onPressed: () => floop[#clicks]++,
      ),
    );
  }
}

Extra step:

  • Use transition(ms) within the build method to have a value transition from 0 to 1 in ms milliseconds.

Other options:

  • [DynamicWidget] is a convenient Floop widget that has it's own map of dynamic values accessed through [dyn].
  • ... extends StatelessWidget with Floop is equivalent to ... extends FloopWidget.
  • On stateful widgets use ...extends StatefulWidget with FloopStateful or extend [FloopStatefulWidget].
  • Maps of dynamic values like floop can be instantiated using [ObservedMap].

Suggested use cases #

  • Store data that affects many widgets (eg. user data).

  • Asynchronous operations like data fetching. Conditionally check floop['myData'] == null ? LoadingWidget() : DisplayDataWidget(floop['myData']).

  • Perform simple animations.

Install #

Add floop dependency to the project's pubspec.yaml

depedencies:
  floop:

Run flutter pub get in the root directory of the project.

Transitions and Animations #

All floop widgets can be easily animated using [transition], which returns a [double] that will go from 0 to 1 in the specified time. Example:

@override
Widget build(BuildContext context) {
  return Opacity(
    opacity: transition(3000), // transitions a number from 0 to 1
    child: Container(
      Text('Text will completely appear after 3 seconds')),
  );
}

[transition] reads internally from an [ObservadMap] instance, being basically a shortcut of what would be writing ...opacity: floop['opacityValue'] and creating an asynchronous callback that updates floop['opacityValue'].

Disclaimer about animations (to be updated when more knowledge is acquired): I have not digged into the Flutter animations API, neither have I used it. I suspect animated widgets work directly on an internal layer of the framework that makes them quite more efficient than regular widgets when updating. Making a fully animated app with Floop is possible as it can be corroborated with the examples, however I cannot ensure this is the best idea, since there is an overhead that would be unnecesary with a specialized API. What I can ensure for now is that it is perfectly fine to use [transition] for simple sporadic animations, that's what it is designed for. The great advantage is that it is flexible and easy to use. If someone creates a fully animated app using this library and compares its performance to an equivalent app using Flutter Animations, please message me.

Special Considerations #

Builders #

Dynamic values do not work inside [Builder] functions. A workaround is to read the dynamic values outside of the builder definition. Example:

Works fine:

  @override
  Widget build(BuildContext context) {
    final opacity = transition(3000);
    final text = floop[myText];
    return Builder(
      builder: (context) => Opacity(
        opacity: opacity,
        child: Container(
          Text(text),
      );
    );
  }

Should not do:

@override
Widget build(BuildContext context) {
  return Builder(
    builder: (context) => Opacity(
      // This is an assertion error, [transition] cannot be used outside
      // a Floop widget's build method.
      opacity: transition(3000),
      child: Container(
        // The widget will not update if floop[myText] changes.
        Text(floop[myText])),
    );
  );
}

Reading a value from floop inside a [Builder] does not subscribe the value to the context used by the builder, because the builder function is a callback that executes outside of the encompassing [build] method.

Transitions and Keys in Stateless Widgets #

Use keys on widgets that invoke [transition] when the following conditions are met:

  • The widgets belong to the same array of children widgets ...children: [widget1, widget2,...],
  • They belong to the same class (more precisely, they have equal [Object.runtimeType])
  • The list of children can be reordered

Which are the same conditions when keys should be used on Stateful widgets.

Reasoning: Keys are not normally useful in stateless widgets, because when the widgets are reordered, it doesn't matter if they get rebuilt from another context, there is nothing on a context that could affect their build output. When using Floop's transitions API, the transitions are internally associated with the context that created them. If a list of children with no keys defined is reordered, the contexts do not get reordered. As a result, the widgets will not rebuild using their original transitions, but rather the transition that exists in the context from where they are getting rebuilt. When using keys, the widgets are paired with their corresponding contexts and they correctly reorder together.

Details #

ObsevedMap #

floop is an instance of [ObservedMap], which implements [Map]. Other instances can be created in the same way as any map is created, e.g: Map<String, int> myDynamicInts = ObservedMap().

Widgets only subscribe to the keys read during the last build. This is consistent, if a key is not read during the last build, a change on it's value has no impact on the widget's build output.

Maps and Lists #

[Map] and [List] values are not stored as they are when using []= operator, but rather they get deep copied. Every [Map] gets copied as an [ObservedMap] instance, while lists get copied using [List.unmodifiable]. This behavior gives consistency to [ObservedMap], by ensuring that either the values cannot be changed or if they change, the changes will be detected to update elements accordingly. Maps and lists can still be stored as they are by using the method [ObservedMap.setValue]. It also receives optional parameter to prevent triggering updates on elements.

Initializing and Disposing a Context #

[Floop.initContext] is invoked by an [Element] instance when it's added for the first time to the element tree.

[Floop.disposeContext] is invoked by an [Element] instance when it's unmounted (removed from tree to never be used again).

Those methods would be the equivalent of what [State.init] and [State.dispose] are. It can be useful to override them to for example initialize or dispose dynamic values in floop that are only used by the widget. Be careful not to write values of existing keys that are subscribed to other widgets, as it would trigger a rebuild when it is not allowed, causing a Flutter error.

Performance #

As performance rules of thumb:

  • Including [Floop] on a widget is far less impactful than wrapping a widget with another widget as child.
  • Reading one value from floop inside a build method is like reading five [Map] values

The only impact Floop has on a widget is to its build time, which does not go beyond x1.2 on minimal widgets (a container, a button and a text). On the other hand, wrapping a widget with another widget implies having to perform another build during the element's tree building phase, causing a net build impact time of about x2 for small widgets. Besides from that, there is no impact that goes beyond the widget's build time, while wrapping widgets increases the size of the element tree.

The following build time increases can be considered as rough references when comparing reading data from an [ObservedMap] in Floop widgets, to reading the same data from a [LinkedHashMap] in widgets without Floop. Only integer numbers were used as keys and values. It was also assumed that the same context would access the same keys on every invocation to [StatelessWidget.build]. It's a bit more expensive when there are different keys read, but that should be an uncommon case.

On small Widgets (less than 10 lines in the build method), including Floop implies the following performance hits in build times:

  • x1.15 when 0 values are read.
  • x1.9 when up to 5 values are read.
  • x2.9 when up to 20 values are read.

On medium Widgets:

  • x1.1 when 0 values are read.
  • x1.6 when up to 5 values are read.
  • x2.5 when up to 20 values are read.

The more values that are read,the more the Map.[] operation starts becoming the bottleneck of the Widget's build time even when reading from a regular [Map] and so the performance hit starts approaching the difference between reading from a [Map] and an [ObservedMap] while listening. The performance hit when reading from an [ObservedMap] in comparison to a regular [LinkedHashMap] is the following:

  • x1.1 using the map like a regular map (outside build methods).
  • x3 while Floop is on 'listening' mode (when a Widget is building).
  • x5 considering the whole preprocessing (start listening) and post processing (stop listening), which means preparing to listen and commiting all the reads that were 'observed' during the build of a widget.

The x5 performance hit is reasonable considering the amount of operations every read to an [OberservedMap] object implies for the whole build cycle. Additionally from retrieving the value, at least there is a secondary map read, a value added to a set and a value retrieved from a set. With only those extra operations (total of four Map.[] like operations instead of one) the minumum possible overhead is x4, but there is also conditional checks, function calls and iterations.

Generally the performance hit increases slightly with the amount of data read, for example it's about x4.8 for 10^4 values and x5.5 for 10^5 values read.

Writing performance #

Writing to an [ObservedMap] has a performance hit of x3.2 in all circumstances, disregarding the extra time that takes Flutter to run [Element.markNeedsBuild] in case there are widgets subscribed to the key that changed it's value.

Collaborate #

Write code, report bugs, give advice or ideas to improve the library.

[1.1.0] - 01/10/2019

  • Added [DynamicWidget], a widget with it's own [ObservedMap].
  • Added [Transitions.shift], which allows shifting a transition.
  • Included all transition parameters to type transitions.
  • Some internal changes to the mixins and [FloopController].

[1.0.0] - 06/09/2019

  • Now only with Floop is required to enable Floop on a widget
  • No more [buildWithFloop], the build method is just [build]
  • Major refactor to core functionality. Listening is faster.
  • Eliminated [FloopLight], examples updated accordingly.
  • Updated the clicker example. Now it fetches an image.

[0.3.0] - 01/09/2019

  • Adds transition API with examples.
  • Changes to Repeater.
  • Mixin and classes name changes, add disposeContext methods to Floop stateless widgets.
  • Some API name changes and added functionality.

[0.2.3] - 20/07/2019

Rolls back library dependency.

[0.2.2] - 20/07/2019

Updates to latest version of meta library.

[0.2.1] - 20/07/2019

Exposes the internals of the library.

[0.2.0] - 09/07/2019

Update Docs, readme, reworks file organization.

[0.1.1] - 09/07/2019

Update Readme and Docs. Adds asynchronous data fetch example.

[0.1.1] - 09/07/2019

Update Readme and Docs. Adds asynchronous data fetch example.

[0.1.0] - 09/07/2019

Update Docs, Readme and version to comply with pub mantainance suggestions.

example/example.dart

import 'package:flutter/material.dart';
import 'package:floop/floop.dart';
import 'package:http/http.dart' as http;

void main() {
  fetchAndUpdateImage();
  runApp(MaterialApp(title: 'Fetch image', home: ImageDisplay2()));
}

var _fetching = false;

Future<bool> fetchAndUpdateImage(
    [String url = 'https://picsum.photos/300/200']) async {
  if (_fetching) {
    return false;
  }
  try {
    _fetching = true; // locks the fetching function
    final response = await http.get(url);
    floop['image'] = TransitionImage(Image.memory(response.bodyBytes));
    return true;
  } finally {
    _fetching = false;
  }
}

class ImageDisplay extends StatelessWidget with Floop {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      // `floop['image']` is null while fetching an image. When the
      // imaged is downloaded, an image widget is stored on `floop['image']`
      // and the widget automatically updates.
      body: floop['image'] == null
          ? Center(
              child: Text(
                'Loading...',
                textScaleFactor: 2,
              ),
            )
          : Align(
              alignment: Alignment(0, transition(2000, delayMillis: 800) - 1),
              child: floop['image']),
      floatingActionButton: FloatingActionButton(
        child: Icon(Icons.refresh),
        onPressed: () async {
          floop['image'] = null;
          await fetchAndUpdateImage();
          // print('image fetched: ${floop['image']}');
          // Restarting context transitions after the new image has loaded
          // causes the new image to also transition from top to center.
          Transitions.restart(context: context);
        },
      ),
    );
  }
}

// `extends FloopWidget` is equivalent to `...StatelessWidget with Floop`.
class TransitionImage extends FloopWidget {
  final Image image;
  const TransitionImage(this.image);

  @override
  Widget build(BuildContext context) {
    // Opacity transitions from 0 to 1 in 1.5 seconds.
    return GestureDetector(
      child: Opacity(opacity: transition(1500), child: image),
      onTap: () async {
        if (await fetchAndUpdateImage()) {
          Transitions.restart(context: context);
        }
      },
    );
  }
}

// Same example but using a class that access the values on `floop`. Serves
// as a model to organize the code in a big app. Shared dynamic values, like
// user data can be stored in a class with static values and access `floop`
// only from there.

class DynamicValues {
  static Widget get image => floop['image'];
  static set image(Widget widget) => floop['image'] = widget;
}

Future<bool> fetchAndUpdateImage2(
    [String url = 'https://picsum.photos/300/200']) async {
  if (_fetching) {
    return false;
  }
  try {
    _fetching = true; // locks the fetching function
    final response = await http.get(url);
    DynamicValues.image = TransitionImage(Image.memory(response.bodyBytes));
    return true;
  } finally {
    _fetching = false;
  }
}

class ImageDisplay2 extends StatelessWidget with Floop {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: DynamicValues.image == null
          ? Center(
              child: Text(
                'Loading...',
                textScaleFactor: 2,
              ),
            )
          : Align(
              alignment: Alignment(0, transition(2000, delayMillis: 800) - 1),
              child: DynamicValues.image),
      floatingActionButton: FloatingActionButton(
        child: Icon(Icons.refresh),
        onPressed: () async {
          DynamicValues.image = null;
          await fetchAndUpdateImage2();
          // Restarting context transitions after the new image has loaded
          // causes the new image to also transition from top to center.
          Transitions.restart(context: context);
        },
      ),
    );
  }
}

Use this package as a library

1. Depend on it

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


dependencies:
  floop: ^1.1.0

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:floop/floop.dart';
  
Popularity:
Describes how popular the package is relative to other packages. [more]
58
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]
79
Learn more about scoring.

We analyzed this package on Oct 11, 2019, and provided a score, details, and suggestions below. Analysis was completed with status completed using:

  • Dart: 2.5.1
  • pana: 0.12.21
  • Flutter: 1.9.1+hotfix.4

Platforms

Detected platforms: Flutter

References Flutter, and has no conflicting libraries.

Dependencies

Package Constraint Resolved Available
Direct dependencies
Dart SDK >=2.1.0 <3.0.0
flutter 0.0.0
Transitive dependencies
collection 1.14.11 1.14.12
meta 1.1.7
sky_engine 0.0.99
typed_data 1.1.6
vector_math 2.0.8
Dev dependencies
flutter_test
mockito ^4.1.0
pedantic ^1.0.0