Git Product home page Git Product logo

solidart's Introduction

License: MIT GitHub stars Coverage GitHub issues GitHub pull-requests solidart Pub Version (including pre-releases) flutter_solidart Pub Version (including pre-releases) All Contributors

A simple state-management library inspired by SolidJS.

The objectives of this project are:

  1. Being simple and easy to learn
  2. Fits well with the framework's good practices
  3. Do not have a single global state, but multiple states only in the most appropriate places
  4. No code generation

Learning

For a comprehensive and updated documentation go to The Official Documentation

There are 5 main concepts you should be aware:

  1. Signals
  2. Effects
  3. Computed
  4. Resources
  5. Solid (only flutter_solidart)

Signals

Signals are the cornerstone of reactivity in solidart. They contain values that change over time; when you change a signal's value, it automatically updates anything that uses it.

To create a signal, you have to use the Signal class:

final counter = Signal(0);

The argument passed to the class is the initial value, and the return value is the signal.

To retrieve the current value, you can use:

print(counter.value); // prints 0
// or
print(counter());

To change the value, you can use:

// Increments by 1
counter.value++; 
// Set the value to 2
counter.value = 2;
// equivalent to
counter.set(2);
// Update the value based on the current value
counter.updateValue((value) => value * 2);

Effects

Signals are trackable values, but they are only one half of the equation. To complement those are observers that can be updated by those trackable values. An effect is one such observer; it runs a side effect that depends on signals.

An effect can be created by using the Effect class. The effect automatically subscribes to any signal and reruns when any of them change. So let's create an Effect that reruns whenever counter changes:

final disposeFn = Effect((_) {
    print("The count is now ${counter.value}");
});

Computed

A computed signal is a signal that depends on other signals. To create a computed signal, you have to use the Computed class.

A Computed automatically subscribes to any signal provided and reruns when any of them change.

final name = Signal('John');
final lastName = Signal('Doe');
final fullName = Computed(() => '${name.value} ${lastName.value}');
print(fullName()); // prints "John Doe"

// Update the name
name.set('Jane');
print(fullName()); // prints "Jane Doe"

Resources

Resources are special Signals designed specifically to handle Async loading. Their purpose is wrap async values in a way that makes them easy to interact with.

Resources can be driven by a source signal that provides the query to an async data fetcher function that returns a Future.

The contents of the fetcher function can be anything. You can hit typical REST endpoints or GraphQL or anything that generates a future. Resources are not opinionated on the means of loading the data, only that they are driven by futures.

Let's create a Resource:

// The source
final userId = Signal(1);

// The fetcher
Future<String> fetchUser() async {
    final response = await http.get(
      Uri.parse('https://swapi.dev/api/people/${userId.value}/'),
    );
    return response.body;
}

// The resource
final user = Resource(fetcher: fetchUser, source: userId);

A Resource can also be driven from a [stream] instead of a Future. In this case you just need to pass the stream field to the Resource class.

If you're using ResourceBuilder you can react to the state of the resource:

ResourceBuilder(
  resource: user,
  builder: (_, userState) {
    return userState.on(
      ready: (data) {
        return Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            ListTile(
              title: Text(data),
              subtitle:
                  Text('refreshing: ${userState.isRefreshing}'),
            ),
            userState.isRefreshing
                ? const CircularProgressIndicator()
                : ElevatedButton(
                    onPressed: user.refresh,
                    child: const Text('Refresh'),
                  ),
          ],
        );
      },
      error: (e, _) {
        return Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            Text(e.toString()),
            userState.isRefreshing
                ? const CircularProgressIndicator()
                : ElevatedButton(
                    onPressed: user.refresh,
                    child: const Text('Refresh'),
                  ),
          ],
        );
      },
      loading: () {
        return const RepaintBoundary(
          child: CircularProgressIndicator(),
        );
      },
    );
  },
)

The on method forces you to handle all the states of a Resource (ready, error and loading). The are also other convenience methods to handle only specific states.

Solid

The Flutter framework works like a Tree. There are ancestors and there are descendants.

You may incur the need to pass a Signal deep into the tree, this is discouraged. You should never pass a signal as a parameter.

To avoid this there's the Solid widget.

With this widget you can pass a signal down the tree to anyone who needs it.

You will have already seen Theme.of(context) or MediaQuery.of(context), the procedure is practically the same.

Let's see an example to grasp the concept.

You're going to see how to build a toggle theme feature using Solid, this example is present also here https://github.com/nank1ro/solidart/tree/main/examples/toggle_theme

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    // Provide the theme mode signal to descendats
    return Solid( // [1]
      providers: [
        Provider<Signal<ThemeMode>>(
          create: () => Signal(ThemeMode.light),
        ),
      ],
      // using the builder method to immediately access the signal
      builder: (context) {
        // observe the theme mode value this will rebuild every time the themeMode signal changes.
        final themeMode = context.observe<ThemeMode>(); // [2]
        return MaterialApp(
          title: 'Toggle theme',
          themeMode: themeMode,
          theme: ThemeData.light(),
          darkTheme: ThemeData.dark(),
          home: const MyHomePage(),
        );
      },
    );
  }
}

class MyHomePage extends StatelessWidget {
  const MyHomePage({super.key});

  @override
  Widget build(BuildContext context) {
    // retrieve the theme mode signal
    final themeMode = context.get<Signal<ThemeMode>>(); // [3]
    return Scaffold(
      appBar: AppBar(
        title: const Text('Toggle theme'),
      ),
      body: Center(
        child:
            // Listen to the theme mode signal rebuilding only the IconButton
            SignalBuilder( // [4]
          signal: themeMode,
          builder: (_, mode, __) {
            return IconButton(
              onPressed: () { // [5]
                // toggle the theme mode
                if (themeMode.value == ThemeMode.light) {
                  themeMode.value = ThemeMode.dark;
                } else {
                  themeMode.value = ThemeMode.light;
                }
              },
              icon: Icon(
                mode == ThemeMode.light ? Icons.dark_mode : Icons.light_mode,
              ),
            );
          },
        ),
      ),
    );
  }
}

First at [1] we've used the Solid widget to provide the themeMode signal to descendants.

The Solid widgets takes a list of providers: The Provider has a create function that returns the signal. You may create a signal or a derived signal. The value is a Function because the signal is created lazily only when used for the first time, if you never access the signal it never gets created. In the Provider you can also specify an identifier for having multiple signals of the same type.

At [2] we observe the value of a signal. The observe method listen to the signal value and rebuilds the widget when the value changes. It takes an optional id that is the signal identifier that you want to use. This method must be called only inside the build method.

At [3] we get the signal with the given signal type. This doesn't listen to signal value. You may use this method inside the initState and build methods.

At [4] using the SignalBuilder widget we rebuild the IconButton every time the signal value changes.

And finally at [5] we update the signal value.

It is mandatory to pass the type of signal value otherwise you're going to encounter an error, for example:

Provider<Signal<ThemeMode>>(create: () => Signal(ThemeMode.light))

and context.observe<ThemeMode> where ThemeMode is the type of the signal value. context.get<Signal<ThemeMode>> where Signal<ThemeMode> is the type of signal with its type value.

DevTools

You can debug your application using the Solidart DevTools extension and filter your signals.

Examples

Sample features using flutter_solidart:

Showcase of all flutter_solidart features

Learn every feature of flutter_solidart including:

  1. Signal
  2. Show widget
  3. Computed
  4. Effects
  5. SignalBuilder, DualSignalBuilder and TripleSignalBuilder
  6. Resource and ResourceBuilder
  7. Solid and its fine-grained reactivity

Contributors

Alexandru Mariuti
Alexandru Mariuti

๐Ÿ’ป ๐Ÿ› ๐Ÿšง ๐Ÿ’ฌ ๐Ÿ‘€ ๐Ÿ“– โš ๏ธ
manuel-plavsic
manuel-plavsic

๐Ÿ’ป
Luke Greenwood
Luke Greenwood

๐Ÿ“–
9dan
9dan

๐Ÿ’ป ๐Ÿ› ๐Ÿ“–

solidart's People

Contributors

9dan avatar allcontributors[bot] avatar idrisadeyemi01 avatar nank1ro avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar

solidart's Issues

'Wrap with' assist does not apply indentation

When using the assists, the indentation is not applied.

E.g., when selecting "Wrap with SignalBuilder" on some Widget (e.g. CupertinoListTile), i.e.:

            return CupertinoListTile(
// ...

instead of seeing:

            return SignalBuilder(
              signal: null,
              builder: (context, value, child) {
                return CupertinoListTile(
// ...

what is autocompleted is:

```dart
            return SignalBuilder(
signal: null,
builder: (context, value, child) {
return CupertinoListTile(
// ...

is `Solid()` here have the same concept like useContext in ReactJS or createContext in SolidJS ?

Hey, I've been wandering all these years searching for nice state management in Flutter.

As for my background, I use jotai in react and used nanostores in SolidJS once ( not really using solid since many of my works asking for "future maintainability with inhouse dev" (and they don't wanna know how good solidjs is).

So I've been wondering, is there a feature where you can do createStore ? Because in your readme you stated Do not have a single global state, but multiple states only in the most appropriate places. So I assume it's like Context ? But I'm not really sure.

v1.0.0 release steps

  • bump code coverage to 100%
  • add logger to signals/effects/resources
  • update documentation
    • Improve docs about automatic Resource resolution
  • Remove resolve from Resource
  • the resource refetch and resubscribe should be combined in a unique refresh
  • avoid_dynamic_solid_provider is not working using Bloc.new
  • make Signal() working exactly like createSignal and do the same for other classes (if possible)

A few questions regarding your library

  1. In the docs it is stated that "You may incur the need to pass a Signal deep into the tree, this is discouraged. You should never pass a signal as a parameter.". Is this recommended only to prevent having a huge number of params as the app grows? Or does it have something to do with functionality too.

  2. I have used Riverpod, but I want to move away from it because of many reasons (global nature of providers, ref is needed everywhere, ...). Some questions on how to move from Riverpod (I never used solidJS, so some questions might sound a bit silly ๐Ÿ˜‚).

  • 2.1. What do you use internally? ChangeNotifier (and/or ValueNotifiers)? Or some custom class extending ChangeNotifier? Or StateNotifier?
  • 2.2. Riverpod providers are only lazy. What about solidart signals, resources, and so on? Are they strict by default, lazy by default, only strict or only lazy?
  • 2.3. What is the analogous of Effect, in Riverpod?
  • 2.4. In Riverpod I usually create my own notifiers to manage custom logic (something more advance than just changing state of a signal). What is the equivalent in your lib? It is my understanding, that when working with signals readers don't have direct access to the notifiers. How would you achieve functionality that is similar to the TodoNotifier in this example https://riverpod.dev/docs/providers/state_notifier_provider?
  1. Are memos supported (yet)? Or are there just the same as derived signals in your lib?

  2. If I were to only use Soliddart throughout my app, is reactivity really like in SolidJS (i.e. rebuilds only when really needed)?

  3. Is the Solid widget needed only for inheriting a value, e.g.:

    final themeMode = context.observe<ThemeMode>(SignalId.themeMode);
    

    Or is a root Solid widget necessary when using this library in general, i.e., also before calling e.g.:

    SignalBuilder(
      signal: counter,
      builder: (_, value, __) {
        return Text('$value');
      },
    )
    

Maybe it would make sense to create a Discussion section and move this issue there.

Some clarification

HI, thanks for this amazing package. I have some question regarding disposing of a signal.

When using this way -

Solid(
      providers: [
        SolidProvider<FilesController>(
          create: () => FilesController(),
          dispose: (controller) => controller.dispose(), // does dispose gets automatically called, I dont have to dispose else where?
        ),
      ],
...
}

or

Solid(
      signals: {MySignals.someSignal: () => createSignal<bool>(false)}, // I have to dispose, when I dont need it any more?
...
)

Hope I manage to explain it properly. Thanks.

Make interaction with collections (List, Set, Map) easier

Problem

Currently dealing with signals with collections as values is a bit painful.
Let us take a very simple example:

final numbers = Signal([1, 2]);
// you may expect this to work
numbers.value.add(3);
// but the `numbers` signal will not update and won't notify listeners because you're modifying the value of its instance directly

// instead you have to provide a new instance
numbers.value = [...numbers.value, 3];

This is the same behaviour present in ValueNotifier by Flutter.

Proposal

In SolidJS there is an object called Store, a store is a collection of Signals.
What this mean is that each value of the Store is a Signal itself, and you can listen to each one individually.

In solidart I don't think this is needed because it may add an extra complexity and the goal of solidart is to stay simple.

Instead I'd propose to create 3 new kind of signals:

  1. ListSignal, to interact with a list of objects
  2. SetSignal, to interact with a set of objects
  3. MapSignal, to interact with a map of objects

The previous example will become as simple as:

final numbers = ListSignal([1, 2]);
numbers.add(3);

This will make solidart even easier and will give a better developer experience for its users.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.