flutter_observable_state 1.2.0

  • Readme
  • Changelog
  • Example
  • Installing
  • 77

flutter_observable_state #

Observable state for flutter applications

Motivation #

Coming from the world of state management in Javascript/Typescript, I felt that the current solutions to state management with Flutter was too verbose. Building libraries like CerebralJS and OvermindJS, I took inspiration from that work and built this simple approach to managing state in Flutter.

The concept #

class AppState {
  final count = Observable(0);
}

Now you have an observable piece of state, lets use it in a widget.

// For simplicity we just instantiate, please read
// further for proper setup
final _state = AppState()

class MyWidget extends StatelessWidget {
  @override
  Widget build(context) {
    return Container(
      child: observe(() => (
        Row(
          children: [
            Text(_state.count.get()),
            FlatButton(
              onPressed: () {
                _state.count.change((count) => count + 1)
              },
              child: Text("Increase count")
            )
          ]
        )
      ))
    )
  }
}

Any widgets returned within the scope of the observe function will rerender when any state it accesses changes. You can use as many observe you want within a widget, even nest them.

Organizing your project #

When you think about state as application state you will rather define and change the state of the application outside of your widgets. You will still need local widget state, but you primarily want to put your state outside the widgets. To effectively share this state and the logic to change it with all widgets of your application it is highly recommended to use the get_it project. Let us create our initial setup.

We want to create two classes. AppState and Actions. We can use get_it to create a single instance of these classes, which can then be used in any widget.

// services.dart
import 'package:my_project/AppState.dart';
import 'package:my_project/Actions.dart';
import 'package:get_it/get_it.dart';

final getIt = GetIt();

void initialize() {
  getIt.registerSingleton(AppState());
  getIt.registerSingleton(Actions());
}

You can use this file to also register effects you want to perform. For example classes that manages communication with Firebase etc.

// AppState.dart
import 'package:flutter_observable_state/flutter_observable_state.dart';

class AppState {
  final count = Observable(0);
}

// Actions.dart
import 'package:my_project/AppState.dart';
import 'package:my_project/services.dart';

class Actions {
  final _state = getIt.get<AppState>();

  void changeCount(int count) {
    _state.count.change((currentCount) => currentCount + count)
  }
}

Now in a widget you are able to do:

class MyWidget extends StatelessWidget {
  final _state = getIt.get<AppState>()
  final _actions = getIt.get<Actions>()

  @override
  Widget build(context) {
    return Container(
      child: observe(() => (
        Row(
          children: [
            Text(_state.count.get()),
            FlatButton(
              onPressed: () {
                _actions.changeCount(1)
              },
              child: Text("Increase count")
            )
          ]
        )
      ))
    )
  }
}

We have now effectively allowed any widget to access our count and any widget can change it, making sure that any widget observing the state will rerender.

API #

Observable #

Create an observable value.

Observable(0);
Observable<List<String>> = Observable([]);
Observable<User>(null);

Observable.get #

Get the value of an Observable.

var count = Observable(0);

count.get(); // 0

Observable.set #

Set the value of an Observable.

var count = Observable(0);

count.set(1);

Observable.change #

Change the value of an Observable.

var count = Observable(0);

count.change((currentCount) => currentCount + 1);

Observable.setStream #

Connect a stream of values, making the Observable update whenever the stream passes a new value.

var user = Observable<FirebaseUser>(null);

user.setStream(FirebaseAuth.instance.onAuthStateChanged);

// Unset stream
user.setStream(null);

When a stream is set you can still set and change to a new value.

Observable.hasSetInitialValue #

Check if an Observable has actually set a value (Something else than null). This is especially valuable in combination with setStream as you do not know when the first value hits, meaning indicating loading a value is difficult.

var user = Observable<FirebaseUser>(null);

user.setStream(FirebaseAuth.instance.onAuthStateChanged);

class SomeWidget extends StatelessWidget {
  @override
  Widget build(context) {
    return observe(() => user.hasSetInitialValue() ? Text("Yeah, we may or may not have the user") : Text("Still waiting for a value to be set"))
  }
}

Computed #

You can derive state. This works much like the observe, but it only flags the computed as dirty. The next time something gets the value, it will be recalculated.

var foo = Observable('bar');
var upperFoo = Computed(() => foo.get().toUpperCase());

You will typically define computeds with your AppState class.

class AppState {
  final foo = Observable('bar');

  Computed<String> upperFoo;

  AppState() {
    upperFoo = Computed(() => foo.get().toUpperCase());
  }
}

observe #

To observe state in widgets you use the observe function. It returns a StreamBuilder and can be used wherever you typically insert a child widget.

class MyWidget extends StatelessWidget {
  final _state = getIt.get<AppState>();

  @override
  Widget build(context) {
    return Container(
      child: observe(() => (
        Text(_state.foo.get())
      ))    
    )
  }
}

Reaction #

You can observe state and react to it. This is useful when you need to do some imperative logic inside your widgets. For example here we are controlling an overlay from our application state:

class MyWidget extends StatefulWidget {
  @override
  createState() => MyWidgetState();
}

class MyWidgetState extends State<MyWidget> {
  final _state = getIt.get<AppState>();
  Reaction reaction;
  OverlayEntry overlay;

  @override
  void initState() {
    reaction = Reaction(
      () => _state.isOverlayOpen.get(),
      () => {
        if (_state.isOverlayOpen.get() && overlay == null) {
          overlay = _createOverlayEntry();
          Overlay.of(context).insert(overlay);
        } else if (!_state.isOverlayOpen.get() && overlay != null) {
          overlay.remove();
          overlay = null;
        }
      }
    )
    super.initState();
  }

  @override
  Widget build(context) {
    return Container(
      child: observe(() => (
        Text(_state.foo.get())
      ))
    )
  }
}

Models #

You can use Observable with whatever classes you want, even inside widgets. Typically though you want to use it with classes representing models. For example you want to track optimistically adding a like to posts.

// Post.dart
class Post {
  String id;
  String title;
  String description;
  Observable<bool> likesCount;

  Post.fromJSON(Map<String, dynamic> json) :
    id = json["id"],
    title = json["title"],
    description = json["description"],
    likesCount = Observable(json["likesCount"]);
}

// AppState.dart
class AppState {
  final posts = Observable<List<Post>>([]);
}

// Actions.dart
class Actions {
  final _state = getIt.get<AppState>();
  final _api = getIt.get<Api>();

  void initialize() {
    _state.posts.setStream(_api.$posts);
  }

  void likePost(Post post) {
    post.likesCount.change((likesCount) => likesCount + 1);
    _api.likePost(post.id);
  }
}

How does it work? #

Dart is a single threaded language, meaning that only one observe can run at any time. That means the library orchestrates the execution of observe and Computed with the execution of any Observable.get globally. The Computed are considered to live as long as the application lives, while observe uses the StreamBuilder where it clears out existing subscriptions when it builds and when the widget is disposed.

1.2.0 #

Added hasSetInitialValue to verify if an Observable has actually set a value. Valuable with streams to indicate if any data is received

1.1.0+2 #

Fix bug with computed and drop StreamBuilder

1.1.0+1 #

Fix void typing in Reaction

1.1.0 #

Added Reaction

1.0.1+1 #

Example, description and formatting

1.0.1 #

Fix memoryleak related to disposal of observe

1.0.0 #

Initial relase

example/main.dart

import 'package:flutter/material.dart';
import './services.dart' as services;
import './App.dart';

void main() {
  services.initialize();

  runApp(App());
}

Use this package as a library

1. Depend on it

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


dependencies:
  flutter_observable_state: ^1.2.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:flutter_observable_state/flutter_observable_state.dart';
  
Popularity:
Describes how popular the package is relative to other packages. [more]
57
Health:
Code health derived from static analysis. [more]
100
Maintenance:
Reflects how tidy and up-to-date the package is. [more]
90
Overall:
Weighted score of the above. [more]
77
Learn more about scoring.

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

  • Dart: 2.6.0
  • pana: 0.12.21
  • Flutter: 1.9.1+hotfix.6

Platforms

Detected platforms: Flutter

References Flutter, and has no conflicting libraries.

Health issues and suggestions

Document public APIs. (-0.06 points)

30 out of 31 API elements have no dartdoc comment.Providing good documentation for libraries, classes, functions, and other API elements improves code readability and helps developers find and use your API.

Format lib/flutter_observable_state.dart.

Run flutter format to format lib/flutter_observable_state.dart.

Maintenance issues and suggestions

Support latest dependencies. (-10 points)

The version constraint in pubspec.yaml does not support the latest published versions for 1 dependency (rxdart).

Dependencies

Package Constraint Resolved Available
Direct dependencies
Dart SDK >=2.1.0 <3.0.0
flutter 0.0.0
rxdart ^0.21.0 0.21.0 0.22.6
Transitive dependencies
collection 1.14.11 1.14.12
meta 1.1.7 1.1.8
sky_engine 0.0.99
typed_data 1.1.6
vector_math 2.0.8
Dev dependencies
flutter_test
get_it 1.0.3+1