๐ก Signal - Reactive State Management for Flutter
Signal is a modern reactive state management library for Flutter applications. It provides a simple yet powerful way to manage application state with automatic UI updates using Streams internally but exposing a clean API.
โจ Key Features
- โ Automatic state management (busy, success, error states)
- โ Parent-child signal relationships with automatic updates
- โ Proper disposal and memory management
- โ Flutter integration with Provider pattern
- โ Type-safe signal subscription
- โ Debug tools for development
- โ Stream-based reactivity with clean API
๐ฆ Installation
Add to your pubspec.yaml:
dependencies:
signal: ^3.0.0
Then import:
import 'package:signal/signal.dart';
๐ Quick Start
1. Create a Signal class
class CounterSignal extends Signal {
int _count = 0;
int get count => _count;
void increment() {
setState(() async {
_count++;
});
}
}
2. Provide the Signal
SignalProvider<CounterSignal>(
signal: (context) => CounterSignal(),
child: MyApp(),
)
3. Listen to Signal updates
SignalBuilder<CounterSignal>(
builder: (context, signal, child) {
return Column(
children: [
Text('Count: ${signal.count}'),
if (signal.busy) CircularProgressIndicator(),
if (signal.error != null) Text('Error: ${signal.error}'),
ElevatedButton(
onPressed: signal.increment,
child: Text('Increment'),
),
],
);
},
)
๐ Complete Example
Here's a complete working example that demonstrates the core features of Signal:
import 'package:flutter/material.dart';
import 'package:signal/signal.dart';
void main() {
runApp(const MyApp());
}
// 1. Create your Signal class
class CounterSignal extends Signal {
int _count = 0;
int get count => _count;
void increment() {
setState(() async {
_count++;
});
}
void decrement() {
setState(() async {
_count--;
});
}
Future<void> loadData() async {
setState(() async {
// Simulate network request
await Future.delayed(Duration(seconds: 2));
_count = 100;
});
}
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Signal Example',
home: SignalProvider<CounterSignal>(
signal: (context) => CounterSignal(),
child: const MyHomePage(),
),
);
}
}
class MyHomePage extends StatelessWidget {
const MyHomePage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Signal Counter'),
),
body: SignalBuilder<CounterSignal>(
builder: (context, signal, child) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (signal.busy)
const CircularProgressIndicator()
else
Text(
'Count: ${signal.count}',
style: Theme.of(context).textTheme.headlineMedium,
),
if (signal.error != null)
Padding(
padding: const EdgeInsets.all(16.0),
child: Text(
'Error: ${signal.error}',
style: const TextStyle(color: Colors.red),
),
),
const SizedBox(height: 20),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
ElevatedButton(
onPressed: signal.decrement,
child: const Text('-'),
),
ElevatedButton(
onPressed: signal.increment,
child: const Text('+'),
),
ElevatedButton(
onPressed: signal.loadData,
child: const Text('Load Data'),
),
],
),
],
),
);
},
),
);
}
}
๐๏ธ Advanced Usage
Multiple Signals with MultiSignalProvider
MultiSignalProvider(
signals: [
signalItem<UserSignal>((context) => UserSignal()),
signalItem<CounterSignal>((context) => CounterSignal()),
signalItem<ThemeSignal>((context) => ThemeSignal()),
],
child: MyApp(),
)
Parent-Child Signal Relationships
class ParentSignal extends Signal {
String _data = 'Initial Data';
String get data => _data;
void updateData(String newData) {
setState(() async {
_data = newData;
});
}
}
class ChildSignal extends Signal {
String get parentData {
// Automatically subscribes to ParentSignal updates
final parent = subscribeToParent<ParentSignal>();
return parent.data;
}
void processParentData() {
setState(() async {
final processed = 'Processed: ${parentData}';
// Handle processed data
});
}
}
๐ Debug Tools
Enable debug mode during development:
void main() {
SignalDebugConfig.enabled = true;
SignalDebugConfig.logLevel = SignalLogLevel.all;
runApp(MyApp());
}
๐ API Reference
Signal Class
The base class for all signals. Extend this class to create your reactive state:
abstract class Signal {
// State management
bool get busy; // Loading state
dynamic get error; // Error state
bool get success; // Success state
// Methods to override
void setState(Future<void> Function() callback);
T subscribeToParent<T extends Signal>();
}
SignalProvider
Provides a signal to the widget tree:
SignalProvider<MySignal>(
signal: (context) => MySignal(),
child: Widget,
)
SignalBuilder
Builds UI that automatically updates when the signal changes:
SignalBuilder<MySignal>(
builder: (context, signal, child) {
return Widget();
},
)
MultiSignalProvider
Provides multiple signals efficiently:
MultiSignalProvider(
signals: [
signalItem<Signal1>((context) => Signal1()),
signalItem<Signal2>((context) => Signal2()),
],
child: Widget,
)
๐ฏ Best Practices
1. Keep Signals Focused
// โ
Good - Single responsibility
class UserSignal extends Signal {
User? _user;
User? get user => _user;
Future<void> loadUser(String id) async {
setState(() async {
_user = await userRepository.getUser(id);
});
}
}
// โ Avoid - Too many responsibilities
class AppSignal extends Signal {
// Don't put everything in one signal
}
2. Use setState for All Updates
// โ
Good - Always use setState
void updateData() {
setState(() async {
_data = newData;
});
}
// โ Avoid - Direct updates won't notify listeners
void updateData() {
_data = newData; // UI won't update
}
3. Handle Errors Properly
void loadData() {
setState(() async {
try {
_data = await api.getData();
} catch (e) {
// Error automatically handled by setState
rethrow;
}
});
}
๐งช Testing
Signal is designed to be easily testable:
void main() {
group('CounterSignal', () {
late CounterSignal signal;
setUp(() {
signal = CounterSignal();
});
test('should increment count', () {
expect(signal.count, 0);
signal.increment();
expect(signal.count, 1);
});
test('should handle async operations', () async {
expect(signal.busy, false);
final future = signal.loadData();
expect(signal.busy, true);
await future;
expect(signal.busy, false);
expect(signal.count, 100);
});
});
}
๐ค Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
๐ License
This project is licensed under the MIT License - see the LICENSE file for details.
@override Future
// Instant state change change() { _isOpen = !_isOpen; doneSuccess(); }
// Async state change with loading Future
// 2. Provide the Signal class MyApp extends StatelessWidget { const MyApp({Key? key}) : super(key: key);
@override Widget build(BuildContext context) { return SignalProvider
// 3. Use SignalBuilder to reactively update UI class HomePage extends StatelessWidget { const HomePage({Key? key}) : super(key: key);
@override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Center(child: Text('Signal Example')), ), body: ListView( children:
### Key Concepts Demonstrated
1. **Signal Creation**: Extend the `Signal` class and override lifecycle methods
2. **State Management**: Use `wait()`, `doneSuccess()`, and `doneError()` for state control
3. **Provider Pattern**: Wrap your app with `SignalProvider` to make signals available
4. **Reactive UI**: Use `SignalBuilder` to automatically rebuild when signal state changes
5. **Loading States**: The `busy` property automatically handles loading indicators
6. **Error Handling**: Built-in error state management
## Advanced Usage
### Using setState for Automatic State Management
```dart
class ApiSignal extends Signal {
List<User> _users = [];
List<User> get users => _users;
Future<void> fetchUsers() async {
await setState(() async {
final response = await api.getUsers();
_users = response;
});
}
Future<void> saveUser(User user) async {
await setState(
() => api.saveUser(user),
onDoneError: (e) => 'Failed to save user: ${e.toString()}',
);
}
}
Performance Optimization with Child Widget
SignalBuilder<NotificationSignal>(
child: () => ExpensiveWidget(), // This won't rebuild
builder: (context, signal, child) {
return Column(
children: [
Text('Status: ${signal.isOpen ? "On" : "Off"}'),
child!, // Reuse the expensive widget
],
);
},
)
API Reference
Signal Class
Properties:
busy- Whether an async operation is in progresssuccess- Whether the last operation was successfulerror- Error message (cleared after being read)
Methods:
setState(operation)- Manages async operations with automatic state handlingwait()- Manually set loading statedoneSuccess()- Manually set success statedoneError(message)- Manually set error stateinitState()- Called when signal is first initializedafterInitState()- Called after signal is added to widget treedispose()- Clean up resources
SignalProvider
A StatefulWidget that provides Signal instances throughout the widget tree and manages their lifecycle.
Constructor Parameters:
signal- Factory function that creates the signal instancechild- Widget subtree that will have access to the signal
Static Methods:
SignalProvider.of<T>(context)- Retrieves the nearest signal of type T
SignalBuilder
A widget that rebuilds when a Signal's state changes.
Constructor Parameters:
builder- Function called when signal state changeschild- Optional child widget factory for performance optimization
Best Practices
- Keep Signals Focused: Each signal should manage a specific piece of state
- Use setState for Async Operations: Let the framework handle loading and error states automatically
- Leverage Type Safety: Use generic types to ensure type safety throughout your app
- Optimize with Child Widgets: Use the child parameter in SignalBuilder for expensive widgets that don't need rebuilding
- Handle Lifecycle: Always override
initState()andafterInitState()when needed - Clean Up Resources: The framework automatically calls
dispose(), but you can override it for custom cleanup
Why Choose Signal?
- ๐ Simple: Minimal boilerplate, maximum productivity
- โก Fast: Efficient updates with automatic optimization
- ๐ Type-Safe: Full Dart type system support
- ๐ Reactive: Automatic UI updates when state changes
- ๐ Flexible: Works with any async operation or state pattern
Contributing
๐ค Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
๐ License
This project is licensed under the MIT License - see the LICENSE file for details.
โญ Support
If you find this package helpful, please give it a โญ on GitHub!
For issues and feature requests, please visit the GitHub repository.