Flutter Tutorial

Flutter Intermediate Lesson 54 of 135 3 min read

Signals in Flutter: Fine-Grained Reactive State Made Simple

Learn the signals package for Flutter with easy runnable examples: signal, computed, effect and the Watch widget for fine-grained reactive state management.

On this page

Signals is a small, fast reactive state library. A signal holds a value. Anything that reads it, a computed value, an effect or a Watch widget, updates automatically when it changes. Only the exact widgets that read a signal rebuild, which is called fine-grained reactivity.

The same idea is used by web frameworks such as SolidJS, Preact and Angular.

Install

flutter pub add signals

The counter

import 'package:flutter/material.dart';
import 'package:signals/signals_flutter.dart';

final counter = signal(0);
final doubled = computed(() => counter.value * 2);

void main() => runApp(const MaterialApp(home: CounterPage()));

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Signals')),
      body: Center(
        child: Watch((context) => Text(
              '${counter.value} × 2 = ${doubled.value}',
              style: const TextStyle(fontSize: 36),
            )),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => counter.value++,
        child: const Icon(Icons.add),
      ),
    );
  }
}
  • signal(0) creates a value you can read and set with .value.
  • computed(() => ...) is recalculated only when a signal it reads changes.
  • Watch((context) => ...) rebuilds only the widget inside it.

A shopping list

import 'package:flutter/material.dart';
import 'package:signals/signals_flutter.dart';

final items = listSignal<String>(['Milk', 'Bread']);
final count = computed(() => items.length);

void main() => runApp(const MaterialApp(home: ListPage()));

class ListPage extends StatefulWidget {
  const ListPage({super.key});

  @override
  State<ListPage> createState() => _ListPageState();
}

class _ListPageState extends State<ListPage> {
  final _controller = TextEditingController();

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  void _add() {
    final text = _controller.text.trim();
    if (text.isEmpty) return;
    items.add(text); // listSignal notifies on add/remove
    _controller.clear();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Watch((context) => Text('Shopping (${count.value})'))),
      body: Column(
        children: [
          Padding(
            padding: const EdgeInsets.all(12),
            child: TextField(
              controller: _controller,
              onSubmitted: (_) => _add(),
              decoration: InputDecoration(
                hintText: 'Add an item',
                suffixIcon: IconButton(icon: const Icon(Icons.add), onPressed: _add),
              ),
            ),
          ),
          Expanded(
            child: Watch((context) => ListView(
                  children: [
                    for (var i = 0; i < items.length; i++)
                      ListTile(
                        title: Text(items[i]),
                        trailing: IconButton(
                          icon: const Icon(Icons.delete),
                          onPressed: () => items.removeAt(i),
                        ),
                      ),
                  ],
                )),
          ),
        ],
      ),
    );
  }
}

Effects: react to changes

final dispose = effect(() {
  debugPrint('Counter changed to ${counter.value}');
});
// Call dispose() to stop.

Use effects for side effects: saving, logging, analytics. Do not change the signal you are watching inside its own effect.

Building blocks

FunctionWhat it gives you
signal(v)A readable and writable value
computed(() => ...)A read-only derived value
effect(() => ...)Code that reruns when signals it reads change
listSignal, mapSignal, setSignalCollections that notify on change
futureSignal, streamSignalAsync values with loading and error states
batch(() => ...)Several changes, one update
Watch((context) => ...)A widget that rebuilds when signals inside change

Pros and cons

  • Pros: tiny API; very fast; only the widgets that read a value rebuild; works in pure Dart too.
  • Cons: newer than Provider, Riverpod and Bloc, so fewer tutorials and team conventions; global signals need discipline in big apps.

Try it yourself

Add a computed signal for “items starting with B” and show it in a second Watch at the bottom of the screen.

Practise in the playground Updated