A state management library that enables concise, fluid, readable and testable business logic code.

Overview

Pub build

Creator is a state management library that enables concise, fluid, readable, and testable business logic code.

Read and update state with compile time safety:

// Creator creates a stream of data.
final counter = Creator.value(0);
Widget build(BuildContext context) {
  return Column(
    children: [
      // Watcher will rebuild whenever counter changes.
      Watcher((context, ref, _) => Text('${ref.watch(counter)}')),
      TextButton(
        // Update state is easy.
        onPressed: () => context.ref.update<int>(counter, (count) => count + 1),
        child: const Text('+1'),
      ),
    ],
  );
}

Write clean and testable business logic:

// repo.dart

// Pretend calling a backend service to get fahrenheit temperature.
Future<int> getFahrenheit(String city) async {
  await Future.delayed(const Duration(milliseconds: 100));
  return 60 + city.hashCode % 20;
}
// logic.dart

// Simple creators bind to UI.
final cityCreator = Creator.value('London');
final unitCreator = Creator.value('Fahrenheit');

// Write fluid code with methods like map, where, etc.
final fahrenheitCreator = cityCreator.asyncMap(getFahrenheit);

// Combine creators for business logic. 
final temperatureCreator = Emitter<String>((ref, emit) async {
  final f = await ref.watch(fahrenheitCreator);
  final unit = ref.watch(unitCreator);
  emit(unit == 'Fahrenheit' ? '$f F' : '${f2c(f)} C');
});

// Fahrenheit to celsius converter.
int f2c(int f) => ((f - 32) * 5 / 9).round();
// main.dart

Widget build(BuildContext context) {
  return Watcher((context, ref, _) => 
      Text(ref.watch(temperatureCreator.asyncData).data ?? 'loading'));
}
... context.ref.set(cityCreator, 'Paris');  // triggers backend call
... context.ref.set(unitCreator, 'Celsius');  // doesn't trigger backend call

Getting started:

dart pub add creator

Table of content:

Why Creator?

When we built our Flutter app, we started with flutter_bloc. Later we switched to riverpod. However, we encountered several issues related to its async providers and realized we wanted a different mechanism.

So we built Creator. It is heavily inspired by riverpod, but with a simpler data model, better async support, and a much simpler implementation.

The benefit of using Creator:

  • Enables concise, fluid, readable, and testable business logic. Sync or async.
  • No need to worry when to "provide" creators.
  • Concept is extremely simple and easy to learn.
  • No magic. Build this library yourself with 100 lines of code.

Concept

Creator's concept is extremely simple. There are only two types of creators:

  • Creator which creates a stream of T.
  • Emitter which creates a stream of Future<T>.

Here stream is in its logical term, not the Stream class.

Both Creator and Emitter:

  • Can depend on other creators, and update its state when others' state changes.
  • Are loaded lazily and disposed automatically.

Dependencies form a graph, for example, this is the graph for the weather example above:

weather

The library simply maintains the graph with an adjacency list and propagates state changes along the edges.

Usage

Creator

Creator takes a function you write to create a state. The function takes a Ref, which provides API to interact with the internal graph.

final number = Creator.value(42);  // Same as Creator((ref) => 42)
final double = Creator((ref) => ref.watch(number) * 2);

Calling watch adds an edge number -> double to the graph, so double's create function will rerun whenever number's state changes.

The nice part is that creator comes with methods like map, where, reduce, etc. They are similar to those methods in Iterable or Stream. So double can simply be:

final double = number.map((n) => n * 2);

You can also read a creator when watch doesn't make sense, for example, inside a touch event handler.

TextButton(
  onPressed: () => print(context.ref.read(number)),
  child: const Text('Print'));

To update the creator's state, use either set or update:

... ref.set(number, 42);  // No-op if value is the same
... ref.update<int>(number, (n) => n + 10);  // Same as read then set

Note creator determines state change using T.==, so it should work with immutable data.

  • If T is a class, use const constructor and override == and hashCode. Or use package like equatable.
  • If T is a list, create a new list rather than update the existing one.

Creator's dependency can be dynamic:

final C = Creator((ref) {
  final value = ref.watch(A);
  return value >= 0 ? value : ref.watch(B);
});

In this example, A -> C always exists, B -> C may or may not exist. The library will update the graph properly as dependency changes.

Emitter

Emitter works very similar to Creator, but it creates Future<T> instead of <T>. The main difference is Creator has valid data to begin with, while Emitter might need to wait for some async work before it yields the first data.

In practice, Emitter is very useful to deal with data from backend services, which is async by nature.

final stockCreator = Creator.value('TSLA');
final priceCreator = Emitter<int>((ref, emit) async {
  final stock = ref.watch(stockCreator);
  final price = await fetchStockPrice(stock);
  emit(price);
});

Emitter takes a FutureOr<void> Function(Ref ref, void Function(T) emit), where ref allows getting data from the graph, and emit allows pushing data back to the graph. You can emit multiple times.

Existing Stream can be converted to Emitter easily with Emitter.stream. It works both sync or async:

final authCreator = Emitter.stream(
    (ref) => FirebaseAuth.instance.authStateChanges());

final userCreator = Emitter.stream((ref) async {
  final authId = await ref.watch(
      authCreator.where((auth) => auth != null).map((auth) => auth!.uid));
  return FirebaseFirestore.instance.collection('users').doc(authId).snapshots();
});

This example also shows the extension method where and map. With them, userCreator will only recreate when auth id changes, and ignore changes on other auth properties.

In some sense, you can think Emitter as a different version of Stream, which makes combining streams super easy.

Emitter generates Future<T>, so it can be hooked to Flutter's FutureBuilder for UI. Or you can use Emitter.asyncData, which is a creator of AsyncData<T>. AsyncData is similar to AsyncSnapshot for future/stream:

enum AsyncDataStatus { waiting, active }

class AsyncData<T> {
  const AsyncData._(this.status, this.data);
  const AsyncData.waiting() : this._(AsyncDataStatus.waiting, null);
  const AsyncData.withData(T data) : this._(AsyncDataStatus.active, data);

  final AsyncDataStatus status;
  final T? data;
}

With it building widget with Emitter is easy:

Watcher((context, ref, _) {
  final user = ref.watch(userCreator.asyncData).data;
  return user != null ? Text(user!.name) : const CircularProgressIndicator();
});

CreatorGraph

To make creators work, wrap your app in a CreatorGraph:

void main() {
  runApp(CreatorGraph(child: const MyApp()));
}

CreatorGraph is a InheritedWidget. It holds a Ref object (which holds the graph) and exposes it through context.ref.

CreatorGraph uses DefaultCreatorObserver by default, which prints logs when creator state changes. It can be replaced with your own log collection observer.

Watcher

Watcher is a simple StatefulWidget which holds a Creator<Widget> internally and calls setState when its dependency changes.

It takes builder function Widget Function(BuildContext context, Ref ref, Widget child). You can use ref to watch creators to populate the widget. child can be used optionally if the subtree should not rebuild when dependency changes:

Watcher((context, ref, child) {
  final color = ref.watch(userFavoriteColor);
  return Container(color: color, child: child);
}, child: ExpensiveAnimation());  // this child is passed into the builder above

Listen to change

Watching a creator will get its latest state. What if you also want previous state? Simply call watch(someCreator.change) to get a Change<T>, which is an object with two properties T? before and T after.

For your convenience, Watcher can also take a listener. It can be used to achieve side effects or run background tasks:

// If builder is null, child widget is directly returned. You can set both
// builder and listener. They are independent of each other.
Watcher(null, listener: (ref) {
  final change = ref.watch(number.change);
  print('Number changed from ${change.before} to ${change.after}');
}, child: SomeChildWidget());

Name

Creators can have names for logging purpose. Setting name is recommended for any serious app.

final numberCreator = Creator.value(0, name: 'number');
final doubleCreator = numberCreator.map((n) => n * 2, name: 'double');

Keep alive

By default, creators are disposed when losing all its watchers. This can be overridden with keepAlive parameter. It is useful if the creator maintains a connection to backend (e.g. listen to firestore realtime updates).

final userCreator = Emitter.stream((ref) {
  return FirebaseFirestore.instance.collection('users').doc('123').snapshots();
}, keepAlive: true);

Creator group

Creator group can generate creators with external parameter. For example, in Instagram app, there might be multiple profile pages on navigation stack, thus we need multiple instance of profileCreator.

// Instagram has four tabs: instagram, reels, video, tagged
final tabCreator = Creator.arg1<Tab, String>((ref, userId) => 'instagram');
final profileCreator = Emitter.arg1<Profile, String>((ref, userId, emit) async {
  final tab = ref.watch(tabCreator(userId));
  emit(await fetchProfileData(userId, tab));  // Call backend
});

// Now switching tab in user A's profile page will not affect user B.
... ref.watch(profileCreator('userA'));
... ref.set(tabCreator('userA'), 'reels');

Creators comes with factory methods arg1 arg2 arg3 which take in 1-3 arguments.

Extension method

Our favorite part of the library is that you can use methods like map, where, reduce on creators (full list here). They are similar to those methods in Iterable or Stream.

final numberCreator = Creator.value(0);
final oddCreator = numberCreator.where((n) => n.isOdd);

Note that Creator needs to have valid state at the beginning, while where((n) => n.isOdd) cannot guarantee that. This is why where returns an Emitter rather than a Creator. Here is the implementation of the where method. It is quite simple and you can write similar extensions if you want:

extension CreatorExtension<T> on Creator<T> {
  Emitter<T> where(bool Function(T) test) {
    return Emitter((ref, emit) {
      final value = ref.watch(this);
      if (test(value)) {
        emit(value);
      }
    });
  }
}

You can use extension methods in two ways:

// Define oddCreator explicitly as a stable variable.
final oddCreator = numberCreator.where((n) => n.isOdd);
final someCreator = Creator((ref) {
  return 'this is odd: ${ref.watch(oddCreator)}');
})
// Create "oddCreator" anonymously on the fly.
final someCreator = Creator((ref) {
  return 'this is odd: ${ref.watch(numberCreator.where((n) => n.isOdd))}');
})

If you use the "on the fly" approach, please read next section about creator equality.

Creator equality

The graph checks whether two creators are equal using ==. This means creator should be defined in global variables, static variables or any other ways which can keep variable stable during its life cycle.

What happens if creators are defined in local variables on the fly?

final text = Creator((ref) {
  final double = Creator((ref) => ref.watch(number) * 2);
  return 'double: ${ref.watch(double)}';
})

Here double is a local variable, it has different instances whenever text is recreated. The internal graph could change from number -> double_A -> text to number -> double_B -> text as the number changes. text still generates correct data, but there is an extra cost to swap the node in the graph. Because the change is localized to only one node, the cost can be ignored as long as the create function is simple.

If needed, an optional List<Object?> args can be set to ask the library to find an existing creator with the same args in the graph. Now when number changes, the graph won't change:

final text = Creator((ref) {
  // args need to be globally unique. ['text', 'double'] is likely unique.
  final double = Creator((ref) => ref.watch(number) * 2, args: ['text', 'double']);
  return 'double: ${ref.watch(double)}';
})

The same applies to using extension methods on the fly:

final text = Creator((ref) {
  return 'double: ${ref.watch(number.map((n) => n * 2, args: ['text', 'double']))}';
})

Internally, args powers these features:

  • Creator group. profileCreator('userA') is a creator with args [profileCreator, 'userA'].
  • Async data. userCreator.asyncData is a creator with args [userCreator, 'asyncData'].
  • Change. number.change is a creator with args [number, 'change'].

Service locator

State management libraries are commonly used as service locators:

class UserRepo {
  void changeName(User user, String name) {...}
}
final userRepo = Creator.value(UserRepo(), keepAlive: true);

... context.ref.read(userRepo).changeName(user, name);

If needed, ref can be passed to UserRepo Creator((ref) => UserRepo(ref)). This allows UserRepo read or set other creators. Do not watch though, because it might recreate UserRepo.

Error handling

The library will:

  • For Creator, store exception happened during create and throw it when watch.
  • For Emitter, naturally use Future.error, so error is returned when watch.
  • In either case, error is treated as a state change.

This means that errors can be handled in the most natural way, at the place makes the most sense. Use the weather app above as an example:

// Here we don't handle error, meaning it returns Future.error if network error
// occurs. Alternately we can catch network error and return some default value,
// add retry logic, convert network error to our own error class, etc.
final fahrenheitCreator = cityCreator.mapAsync(getFahrenheit);

// Here we choose to handle the error in widget.
Widget build(BuildContext context) {
  return Watcher((context, ref, _) {
    try {
      return Text(ref.watch(temperatureCreator.asyncData).data ?? 'loading');
    } catch (error) {
      return TextButton('Something went wrong, click to retry', 
          onPressed: () => ref.recreate(fahrenheitCreator));
    }
  };
}

Testing

Testing creator is quite easy by combining watch, read, set. Use the weather app above as an example:

// No setUp, no tearDown, no mocks. Writing tests becomes fun.

test('temperature creator change unit', () async {
  final ref = Ref();
  expect(await ref.watch(temperatureCreator), "60 F");
  ref.set(unitCreator, 'Celsius');
  await Future.delayed(const Duration()); // allow emitter to propagate
  expect(await ref.watch(temperatureCreator), "16 C");
});

test('temperature creator change fahrenheit value', () async {
  final ref = Ref();
  expect(await ref.watch(temperatureCreator), "60 F");
  ref.set(fahrenheitCreator, Future.value(90));
  await Future.delayed(const Duration()); // allow emitter to propagate
  expect(await ref.watch(temperatureCreator), "90 F");
});

Example

Simple example

Source code here.

Counter

DartPad

A counter app shows basic Creator/Watcher usage.

Decremental counter

DartPad

A counter app shows how to hide state and expose state mutate APIs.

Weather

DartPad

Simple weather app shows splitting backend/logic/ui code and writing logic with Creator and Emitter.

News

DartPad

Simple news app with infinite list of news. It shows combining creators for loading indicator and fetching data with pagination.

Graph

DartPad

Simple app shows how the creator library builds the internal graph dynamically.

Starter template

Source code here. An async counter app with login! A minimal template to start a Flutter project with:

  • Go router for routing
  • Creator for state management.
  • Optional Firebase Auth, or your own auth mechanism.

Best practice

Creator is quite flexible and doesn't force a particular style. Best practices also depend on the project and personal preference. Here we just list a few things we follow:

  • Split code into repo files (backend service call), logic files (creator), and UI files (widget).
  • Define creator in global variables.
  • Keep creator small for testability. Put derived state in derived creators (using map, where, etc).

Read source code

Creator's implementation is surprisingly simple. In fact, the core logic is less than 500 lines of code.

You can optionally read this article first, which describes how we built the first version with 100 lines of code.

Read creator_core library in this order:

  • graph.dart: a simple implementation of a bi-directed graph using adjacency list. It can automatically delete nodes which become zero out-degree.
  • creator.dart: the CreatorBase class and its two sub classes, Creator and Emitter. Their main job is to recreate state when asked.
  • ref.dart: manages the graph and provides watch, read, set methods to user.
  • extension.dart: implement extension methods map, where, etc.

Read creator library in this order:

  • creator_graph.dart: A simple InheritedWidget which expose Ref through context.
  • watcher.dart: A stateful widget which holds a Creator<Widget> internally.

FAQ

Is it production ready?

Well, we have been using it in production for our own app (Chooly). However, since it is new to the community, the API might change as we take feedback. So the suggestion for now: read the source code and make your own call.

Is it bad to define creator as global variable?

It's not. Creator itself doesn't hold states. States are held in Ref (in CreatorGraph). Defining a creator is more like defining a function or a class.

How to watch a property while still accessing the whole object?

final someCreator = Creator((ref) {
  ref.watch(userCreator.map((user) => user.email));
  final user = ref.read(userCreator);
  return '${user.name}\'s email is changed';
});

How does creator's life cycle work?

  • It is added to the graph when firstly being watched or set.
  • It can be removed from the graph manually by Ref.dispose.
  • If it has watchers, it is automatically removed from the graph when losing all its watchers, unless keepAlive property is set.

What's the difference between context.ref vs ref in Creator((ref) => ...)?

They both point to the same internal graph, the only difference is that the first ref's _owner field is null, while the second ref's _owner field is the creator itself. This means:

  • It is the same to read, set or update any creators with either ref. The operation is passed to the internal graph.
  • If ref._owner is null, ref.watch(foo) will simply add foo to the graph.
  • If ref._owner is not null, ref.watch(foo) will also add an edge foo -> ref._owner to the graph.

What's the difference between Creator<Future<T>> vs Emitter<T>?

They are both extended from CreatorBase<Future<T>>, whose state is Future<T>. However, there are two important differences, which make Emitter<T> better for async tasks:

  • Emitter<T> stores T in addition to Future<T>, so that we can log change of T or populate AsyncData<T> properly.
  • Emitter<T> notify its watcher when T is emitted, so its watchers can start their work immediately. Creator<Future<T>> notify its watchers when the future is started, so its watchers are still blocked until the future is finished.

What do I need to know if I'm a riverpod user?

Check FAQ for riverpod user.

That's it

Hope you enjoyed reading this doc and will enjoy using Creator. Feedback and contribution are welcome!

  • Let us know your experience using Creator.
  • Upvote this issue so we can build DartPad example easier.
Comments
  • Any way to retrieve/pass ref, not from the UI side?

    Any way to retrieve/pass ref, not from the UI side?

    Hi Terry,

    The below code updates Text(myAccountID(ref)) each time setAccountID(ref, 'newID') was called.

    final Creator<String?> _accountID = Creator<String?>.value(null, name: 'Account ID');
    String? myAccountID(Ref ref) => ref.watch(_myAccountID);
    void setAccountID(Ref ref, String? value) => ref.set(_myAccountID, value);
    

    I have a case where the _accountID is not used in UI but used as a variable (document ID) in Emitter<Account>.stream... from Firestore. setAccountID is being called not from the UI side but from a class Controller.

    class Controller extends Something {
      _doSomething() {
        if (firebaseUser != null) {
          setAccountID(ref, 'firebaseUser.uid');
        }
      }
    }
    

    1. How can ref be passed or retrieved in this class?

    2. ref.recreate is one interesting item that I am quite curious about. I would probably use it to reset variables to default states. Is there a ref.recreateAll?


    Just sharing my feedback. I came across Creator through Medium's recommendation of your article. Found 'Graph' very interesting, (the nodes, its elements and the edges... did a little googling on graph data structure... pardon me if I am wrong...). In my own opinion, I would say Creator is not the easiest to understand as compared to GetX but definitely much easier and straight-forward than 39 state management libraries. I have only 1.5 years of dev experience and I am still lost with the other libraries (except Getx and setState) and it took about 2 days for my 42-year-old frail brain to grasp Creator.

    I did my own small performance/memory comparison (listview of network images with texts and search function), Creator vs GetX's Rx/GetBuilder. I must say I am quite pleased with the preliminary outcome particularly on memory usage. The only thing with Creator is that there is slightly a little more to code.

    I like the "defining creators in global variables". It is really convenient. No more worries that I will forget to inject dependencies.

    Anyway, I will be attempting to switch to Creator for my published marketplace apps. Hopefully, the apps can gain an overall performance boost.

    Look forward to more interesting updates and examples, medium articles and growing popularity on Creator!

    opened by iamshaunwu 41
  • Coverage of Riverpod's Providers

    Coverage of Riverpod's Providers

    Hi there, i'm looking forward to test Creator in a new mobile app. Since the naming is quite different and there are actually only Creator and Emitter around i wonder if this package covers all different types of Providers and cases which Riverpod offers? Since the app has to be wrapped with CreatorGraph i have my doubts that Riverpod and Creator can be used side by side in the same app. If that's the case it would be interesting to see a section in your documentation "Migrating from Riverpod" which shows how the various provider usages would look with Creator. Thanks for offering the package. Making state management easier is always a good task - and i've also discovered some flaws when using Riverpod.

    opened by rivella50 21
  • ref.update No-op

    ref.update No-op

    If I am understanding this correctly, the current logic behind ref.update seems to be calling ref.set internally and checking if the state has changed before notifying the relevant Watchers

      /// Set state of the creator. Typically this is used to set the state for
      /// creator with no dependency, but the framework allows setting state for any
      /// creator. No-op if the state doesn't change.
      void set<T>(CreatorBase<T> creator, T state) {
        final element = _element<T>(creator, recreate: false);
        final before = element.state;
        if (before != state) {
          element.prevState = element.state;
          element.state = state;
          element.error = null;
          _onStateChange(creator, before, state);
        }
      }
    
      /// Set state of creator using an update function. See [set].
      void update<T>(CreatorBase<T> creator, T Function(T) update) {
        set<T>(creator, update(_element(creator).state));
      }
    

    I believe there are going to be some potential issues with this check below when the type of the creator is more complex?

    if (before != state)
    

    Such as a class which has it's own fields and values, ex:

    https://dartpad.dev/?id=9cb6c58e667da441c6d3256964c247dd

    I would expect the code above to work when calling update, but it doesn't.

    Even in simpler cases, I would think the current documentation implies, the only No-op should be ref.set whereas ref.update should notify watchers even if the state is the same?

    opened by borjandev 7
  • Weird expectations on the context extension methods

    Weird expectations on the context extension methods

    Hey, found this package and tried it out (really nice btw), the concept was really clear from the start, but I think the Flutter integration could be improved to resemble other state management frameworks out there.

    The first really striking difference is that to really watch creator changes in the flutter widget tree you need a special widget (I get it, but it's not really flutter-like). I mean, with provider it has extension methods on build context to makeup inherited widgets and make it all look simpler, I think that could be done with ContextRef as well.

    Maybe adding an Inherited widget on the fly to the widget tree as descendant of CreatorGraph would be possible, and then to watch (and rebuild - that's the main expectation here) when using context.ref.watch(...).

    opened by johnnyasantoss 6
  • Why emitter will call unstoppable while use group to give some params;

    Why emitter will call unstoppable while use group to give some params;

    Here's the problem i met. I need to fetch datas from backend through http, so i write an emitter to fetch datas like this:

    final examplesEmitter = Emitter.arg1<List<Example>, Map<String, dynamic>>(
      (ref,credential,emit) async {
        final examples = await ExampleApi().getExamples(credential);
        emit(examples);
      }
    )
    

    when i use watch to fetch datas, it will call the api unstoppable even the data has been fetched.

    final examples = ref.watch(examplesEmitter({'page': 1, 'pageSize': 50}));
    

    Then i find a solution to solve this for temp.

    final examplesEmitter = Emitter.arg2<List<Example>, int, int>(
      (ref,page,pageSize,emit) async {
        final examples = await ExampleApi().getExamples(page, pageSize);
        emit(examples);
      }
    )
    

    And the problem solved.

    But it confuse me why this problem will occured, and maybe you guys can explain this or something. Creator is a good state manager, it's simple to understand and use.Hope it goes well.

    opened by CalsRanna 5
  • Request for the documentation of using Creator with Firebase Authentication and Firebase Firestore

    Request for the documentation of using Creator with Firebase Authentication and Firebase Firestore

    A more complex example with Firebase will accelerate the adoption of the newly created library. The example should include at least 3 layers: UI, logic (with Creator), and repo (interface with Firebase API).

    opened by rhc 3
  • Emitter Stream of Firebase AuthStateChanges not updating asyncData.status/data from AsyncDataStatus.waiting

    Emitter Stream of Firebase AuthStateChanges not updating asyncData.status/data from AsyncDataStatus.waiting

    Hi @liangxianzhe,

    Creating this as a new post.

    Below is the minimum code for Firebase Phone Auth without GoRouter or Device Preview with the full details from CreatorObserver in debug console.

    import 'dart:async';
    
    import 'package:creator/creator.dart';
    import 'package:firebase_auth/firebase_auth.dart';
    import 'package:firebase_core/firebase_core.dart';
    import 'package:flutter/material.dart';
    import 'package:my_app/firebase_options.dart';
    
    final Emitter<User?> authStream = Emitter<User?>.stream(
      (Ref ref) => FirebaseAuth.instance.authStateChanges(),
      name: 'Firebase Auth Stream',
      keepAlive: true,
    );
    
    Future<void> main() async {
      WidgetsFlutterBinding.ensureInitialized();
    
      // Initialize Firebase
      await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
    
      runApp(
        CreatorGraph(observer: const MyCreatorObserver(), child: MyApp()));
    }
    
    class MyApp extends StatelessWidget {
      const MyApp({super.key});
    
      @override
      Widget build(BuildContext context) => MaterialApp(home: LoginPage());
    
    }
    
    class LoginPage extends StatelessWidget {
      final TextEditingController signInMobileNumberController = TextEditingController();
      final TextEditingController signInOTPController = TextEditingController();
    
      LoginPage({super.key});
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          body: SafeArea(
            child: SingleChildScrollView(
              child: Column(
                children: <Widget>[
                  const Text('Stream Status'),
                  Watcher(
                    (BuildContext context, Ref ref, _) => Text('${ref.watch(authStream.asyncData).status}'.toUpperCase()),
                  ),
                  const SizedBox(height: 20),
                  const Text('Stream Data'),
                  Watcher(
                    (BuildContext context, Ref ref, _) =>
                        Text(ref.watch(authStream.asyncData).data == null ? 'NULL' : 'VALID'),
                  ),
                  const SizedBox(height: 20),
                  const Text('Enter Mobile Number'),
                  TextFormField(
                    controller: signInMobileNumberController,
                    keyboardType: TextInputType.number,
                  ),
                  const SizedBox(height: 20),
                  Row(
                    mainAxisAlignment: MainAxisAlignment.spaceAround,
                    children: <Widget>[
                      ElevatedButton(
                        child: const Text('Sign In'),
                        onPressed: () async {
                          await FirebaseAuth.instance.verifyPhoneNumber(
                            // replace country code
                            phoneNumber: '+1${signInMobileNumberController.text}',
                            verificationCompleted: (PhoneAuthCredential credential) async =>
                                FirebaseAuth.instance.signInWithCredential(credential),
                            verificationFailed: (FirebaseAuthException exception) async => print(exception.code),
                            codeSent: (
                              String verificationId, [int? forceResendingToken]) async {
                              await showModalBottomSheet(
                                context: context,
                                builder: (BuildContext context) => Builder(
                                  builder: (BuildContext context) {
                                    return Container(
                                      height: MediaQuery.of(context).size.height * 0.8,
                                      decoration: const BoxDecoration(color: Colors.white),
                                      child: Padding(
                                        padding: const EdgeInsets.all(8.0),
                                        child: Column(
                                          mainAxisSize: MainAxisSize.min,
                                          crossAxisAlignment: CrossAxisAlignment.start,
                                          children: <Widget>[
                                            const Text('Enter OTP once received'),
                                            TextFormField(
                                              controller: signInOTPController,
                                              keyboardType: TextInputType.number,
                                              maxLength: 6,
                                            ),
                                            Row(
                                              children: <Widget>[
                                                ElevatedButton(
                                                  onPressed: () => Navigator.pop(context),
                                                  child: const Text('Cancel'),
                                                ),
                                                const Spacer(),
                                                ElevatedButton(
                                                  onPressed: () => Navigator.pop(context),
                                                  child: const Text('Verify'),
                                                ),
                                              ],
                                            )
                                          ],
                                        ),
                                      ),
                                    );
                                  },
                                ),
                              );
                              final AuthCredential credential = PhoneAuthProvider.credential(
                                verificationId: verificationId,
                                smsCode: signInOTPController.text.trim(),
                              );
                              signInOTPController.clear();
    
                              await FirebaseAuth.instance.signInWithCredential(credential);
                            },
                            codeAutoRetrievalTimeout: (String verificationId) {
                              verificationId = verificationId;
                            },
                          );
                        },
                      ),
                      ElevatedButton(
                        child: const Text('Sign Out'),
                        onPressed: () async => FirebaseAuth.instance.signOut(),
                      ),
                      ElevatedButton(
                        child: const Text('Print'),
                        onPressed: () {
                          print('${authStream.asyncData.debugName}: ${context.ref.watch(authStream.asyncData).status}');
                          print('${authStream.asyncData.debugName}: ${context.ref.watch(authStream.asyncData).data}');
                        },
                      ),
                    ],
                  ),
                ],
              ),
            ),
          ),
        );
      }
    }
    
    class MyCreatorObserver extends CreatorObserver {
      const MyCreatorObserver();
    
      @override
      void onStateChange(CreatorBase creator, Object? before, Object? after) {
        debugPrint('[Creator] ${creator.debugName}: $after');
      }
    
      @override
      void onError(CreatorBase creator, Object? error) {
        debugPrint('[Creator][Error] ${creator.debugName}: $error');
      }
    }
    
    

    DEBUG CONSOLE Step 1: When app launches.

    flutter: [Creator] Firebase Auth Stream_asyncData(cf1bf): waiting
    flutter: [Creator] watcher(ccd11): Text("ASYNCDATASTATUS.WAITING")
    flutter: [Creator] watcher(73740): Text("NULL")
    

    Step 2: After signing in with OTP.

    flutter: [Creator] Firebase Auth Stream(1df65): User(*** user details ***)
    

    Step 3: No change in the Text Widgets. Tried tapping Print Button.

    flutter: Firebase Auth Stream_asyncData(cf1bf): AsyncDataStatus.waiting
    flutter: Firebase Auth Stream_asyncData(cf1bf): null
    

    Step 4: Did a hot-restart (now that there is a valid auth).

    flutter: [Creator] Firebase Auth Stream_asyncData(e74a4): waiting
    flutter: [Creator] watcher(b3feb): Text("ASYNCDATASTATUS.WAITING")
    flutter: [Creator] watcher(51ff5): Text("NULL")
    flutter: [Creator] Firebase Auth Stream(db1ad): User(*** user details ***)
    flutter: [Creator] Firebase Auth Stream_asyncData(e74a4): User(*** user details ***)
    flutter: [Creator] watcher(b3feb): Text("ASYNCDATASTATUS.ACTIVE")
    flutter: [Creator] watcher(51ff5): Text("VALID")
    

    Step 5: Sign out.

    flutter: [Creator] Firebase Auth Stream(db1ad): null
    flutter: [Creator] Firebase Auth Stream_asyncData(e74a4): null
    flutter: [Creator] watcher(b3feb): Text("ASYNCDATASTATUS.ACTIVE")
    flutter: [Creator] watcher(51ff5): Text("NULL")
    

    If you repeat Step 1, 2 and then do a sign out.

    // Step 1
    flutter: [Creator] Firebase Auth Stream_asyncData(f470f): waiting
    flutter: [Creator] watcher(7a423): Text("ASYNCDATASTATUS.WAITING")
    flutter: [Creator] watcher(f63e2): Text("NULL")
    
    // Step 2
    flutter: [Creator] Firebase Auth Stream(5d9aa): User(*** user details ***)
    
    // Signout
    flutter: [Creator] Firebase Auth Stream(5d9aa): null
    
    // Tap Print Button
    flutter: Firebase Auth Stream_asyncData(f470f): AsyncDataStatus.waiting
    flutter: Firebase Auth Stream_asyncData(f470f): null
    
    opened by iamshaunwu 3
  • Observe Creator dispose events

    Observe Creator dispose events

    First, many thanks for this library, like it a lot, I was looking something to migrate away from Getx, was pondering on using Riverpod, but luckily found Creator. It's simple to grasp and easy to onboard a dev team starting with Flutter.

    While migrating some controllers, one thing that help me fully understand the lifecycle of the Creator values was using a custom observer that also prints the dispose events because the default observer skips those events.

    This post is only to record this so it could help someone else.

    The custom observer:

    class CustomCreatorObserver extends DefaultCreatorObserver {
      const CustomCreatorObserver();
    
      @override
      void onDispose(CreatorBase creator) {
        if (ignore(creator)) {
          return;
        }
        print('[Creator] Dispose: ${creator.infoName}');
      }
    }
    

    And you could use it like this:

    void main() {
      runApp(CreatorGraph(
        child: const MyApp(),
        observer: const CustomCreatorObserver(),
      ));
    }
    

    Cheers, Diego

    opened by diegosz 2
  • simple grammar fixes

    simple grammar fixes

    some simple grammar fixes for the readme - and I removed some trailing spaces that the linter complained about.

    This looks nice and I look forward to trying it out.

    opened by timmaffett 1
  • Request for the documentation of a typical ViewModel/Controller implementation

    Request for the documentation of a typical ViewModel/Controller implementation

    As the title says would like to have an official example how to implement a typical view model/controller implementation. Basically Creator version of this documentation: https://riverpod.dev/docs/providers/state_notifier_provider/

    This is to facilitate easier transition (by having reference documentation) for people wishing to refactor from other state management solutions (eg bloc).

    Aside: I keep automatically trying to access creator via a non existing .value getter as if it was a hook or observable.

    opened by Kypsis 8
Owner
Xianzhe Liang
Keep creating things.
Xianzhe Liang
Learn how to build a multi-step form flow and how to use bloc to effectively isolate the presentation layer from the business logic layer.

Multi-page Form Flow Learn how to build a multi-step form flow and how to use bloc to effectively isolate the presentation layer from the business log

Sandip Pramanik 15 Dec 19, 2022
Generic validator - A generic validator with business logic separation in mind

This package provides APIs to facilitate separating validation and business rule

Ahmed Aboelyazeed 5 Jan 3, 2023
MV* Architectures - Responsible for the business logic of the application.

MV* Architectures MVC Model Responsible for the business logic of the application. Persisting application state: communication with the database. Data

Mekni_Wassime 2 Mar 15, 2022
Business Card - Business Card App Built With Flutter

Business Card app. Basic single page app with functionality. For now you can cha

buraktaha 5 Apr 20, 2022
A simple dart package to convert large numbers to a human readable format. 1278 to 1.2K instead, for example.

A simple dart package to convert large numbers to a human readable format. 1278 to 1.2K instead, for example. Features Represents large numbers in ter

Rohit V 1 Oct 8, 2022
Additional alignments to help make your layouts more readable (TopLeft, TopRight, etc)

extra alignments Why should Center get all the love? This package adds additional alignments to help make your layouts more readable. The full set inc

gskinner team 13 Jan 6, 2023
State Persistence - Persist state across app launches. By default this library store state as a local JSON file called `data.json` in the applications data directory. Maintainer: @slightfoot

State Persistence Persist state across app launches. By default this library store state as a local JSON file called data.json in the applications dat

Flutter Community 70 Sep 28, 2022
An extension to the bloc state management library which lets you create State Machine using a declarative API

An extension to the bloc state management library which lets you create State Machine using a declarative API

null 25 Nov 28, 2022
Call Kit is a prebuilt feature-rich call component, which enables you to build one-on-one and group voice/video calls into your app with only a few lines of code.

Call Kit (ZegoUIKitPrebuiltCall) Call Kit is a prebuilt feature-rich call component, which enables you to build one-on-one and group voice/video calls

ZEGOCLOUD 9 Dec 26, 2022
A flutter plugin about qr code or bar code scan , it can scan from file、url、memory and camera qr code or bar code .Welcome to feedback your issue.

r_scan A flutter plugin about qr code or bar code scan , it can scan from file、url、memory and camera qr code or bar code .Welcome to feedback your iss

PengHui Li 112 Nov 11, 2022
A simple project with cool animations and fluid UI

Pokedex This task was quite an interesting one. As with all my projects, I started with understanding the scope and state of this app. I decided to us

JasperEssien 7 Dec 31, 2022
Shopify Tag and Product Management App using Flutter and Riverpod State Management

Myshopify App A Simple Flutter Application project to get List of Tags, Products and Product Details from shopify https://shopicruit.myshopify.com/adm

Idowu Tomiwa 5 Nov 12, 2022
The component created with Flutter using Dart programming language, inspired in Fluid Slider by Ramotion.

Fluid Slider Flutter The component created with Flutter using Dart programming language, inspired in Fluid Slider by Ramotion. About The component was

Wilton Neto 41 Sep 30, 2022
A powerful state machine for MobX management, that can be used in almost any application state.

A powerful state machine for MobX management, which can be used in almost any application state. It has 3 states - loading, success, error - and is pe

Daniel Magri 8 Oct 31, 2022
Practice building basic animations in apps along with managing app state by BLoC State Management, Flutter Slider.

Practice building basic animations in apps along with managing app state by BLoC State Management including: Cubit & Animation Widget, Flutter Slider.

TAD 1 Jun 8, 2022
An atomic state management library for dart (and Flutter). Simple, fast and flexible.

nucleus An atomic dependency and state management toolkit. Design goals Simplicity - simple API with no surprises Performance - allow for millions of

Tim 12 Jan 2, 2023
FlutterQRcode - Flutter QR Code Scanner app for a specific type of QR using GetX State Management Architecture

qrcode A new Flutter QR Scanner Project for Data Entry using GetX state manageme

null 8 Dec 11, 2022
Practice code of simple flutter app to explain how provider works as across the widget state management.

vdo A new Flutter project. Getting Started This project is a starting point for a Flutter application. A few resources to get you started if this is y

Syed Uzair 9 Nov 7, 2022
Intel Corporation 238 Dec 24, 2022