tide 0.4.0 tide: ^0.4.0 copied to clipboard
A very thin layer on top of state_notifier and provider to give guidelines when architecturing an application.
A very thin layer on top of state_notifier, provider and dart generators to give clear guidelines when architecturing an application.
Quickstart #
Define your application state class as an immutable definition.
It is important to have equality comparer implemented for your state object to optimize rebuild. To help with that, you can use the equatable or freezed packages.
import 'package:tide/tide.dart';
@immutable
class CounterState {
const CounterState(this.value, this.isLoading);
final int value;
final bool isLoading;
@override
bool operator ==(dynamic other) {
return identical(this, other) ||
(other is CounterState &&
value == other.value &&
isLoading == other.isLoading);
}
@override
int get hashCode =>
runtimeType.hashCode ^ value.hashCode ^ isLoading.hashCode;
}
Define a set of StoreActions
that will mutate the state from the store when dispatched.
import 'package:tide/tide.dart';
class Increment extends StoreAction<CounterState> {
@override
Stream<CounterState> execute(
StateReader<CounterState> state,
Dispatcher dispatch,
ServiceLocator services,
) async* {
if (!state().isLoading) {
yield CounterState(state().value + 1, false);
}
}
}
class AddServerValue extends StoreAction<CounterState> {
@override
Stream<CounterState> execute(
StateReader<CounterState> state,
Dispatcher dispatch,
ServiceLocator services,
) async* {
if (!state().isLoading) {
yield CounterState(state().value, true);
final serverValue = await ServerClient().getValue();
yield CounterState(state().value + serverValue, false);
}
}
}
class ResetThenAddValueverySecond extends StoreAction<CounterState> {
const ResetThenAddValueverySecond(this.value);
final int value;
@override
Stream<CounterState> execute(
StateReader<CounterState> state,
Dispatcher dispatch,
ServiceLocator services,
) async* {
if (!state().isLoading) {
yield CounterState(0, true);
for (var i = 0; i < 5; i++) {
await Future.delayed(const Duration(seconds: 1));
yield CounterState(state().value + value, true);
}
yield CounterState(state().value, false);
}
}
}
Now your state can be provided through a StoreProvider
at the root of your application, and accessed with the BuildContext.select
extension method.
import 'package:flutter_tide/flutter_tide.dart';
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return StoreProvider<CounterState>(
createStore: (context) => Store(
initialState: CounterState(0, false),
),
child: MaterialApp(
title: 'Tide Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: CounterPage(),
),
);
}
}
class CounterPage extends StatelessWidget {
CounterPage({Key key}) : super(key: key);
@override
Widget build(BuildContext context) {
final isLoading = context.select((CounterState state) => state.isLoading);
return Scaffold(
appBar: AppBar(
title: Text('Tide'),
),
body: Center(
child: isLoading ? Center(child: CircularProgressIndicator()) : Count(),
),
floatingActionButton: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
FloatingActionButton(
onPressed: () => context.dispatch(Increment()),
tooltip: 'Increment',
child: Icon(Icons.add),
),
FloatingActionButton(
onPressed: () => context.dispatch(AddServerValue()),
tooltip: 'AddServerValue',
child: Icon(Icons.arrow_downward),
),
FloatingActionButton(
onPressed: () => context.dispatch(ResetThenAddValueverySecond(10)),
tooltip: 'ResetThenAddValueverySecond',
child: Icon(Icons.alarm_add),
),
],
), // This trailing comma makes auto-formatting nicer for build methods.
);
}
}
class Count extends StatelessWidget {
const Count({
Key key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
final count = context.select((CounterState state) => state.value);
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times:',
),
Text(
'$count',
style: Theme.of(context).textTheme.headline4,
),
],
);
}
}
Advanced usage #
Custom store
You can subclass the Store
class if you want to observe callbacks.
class CounterStore extends Store<CounterState> {
CounterStore() : super(initialState: CounterState(0, false));
@override
void onStateChanged(CounterState oldState) {
print('State changed from ${oldState.value} to ${state.value}');
}
}
Mappers
Since the state is global, its hierarchy may grow rapidly. Reading descendant substate may start to become redundant, and actions filled with copyWith
calls. To make the code of actions clearer, you can register mappers to allow the dispatching of Action<Substate>
for scoped execution.
For example, if your state looks something like :
class MainState {
const MainState({
@required this.child,
})
final ChildState child;
MainState copyWith({
ChildState child,
}) => MainState(
child: child ?? this.child,
);
}
class ChildState {
const ChildState(this.value)
final int value;
}
Then you could have a store that registers a mapper for ChildState
:
class MyStore extends Store<MainState> {
MyStore() : super(initialState: const MainState(child: ChildState(0))) {
registerMapper<ChildState>(
read: (state) => state.child,
writer: (state, substate) => state.copyWith(child: substate),
);
}
}
And you're now able to define and dispatch scoped ChildState
actions into your store :
class Increment extends StoreAction<ChildState> {
@override
Stream<ChildState> execute(
StateReader<ChildState> state,
Dispatcher dispatch,
ServiceLocator services,
) async* {
yield ChildState(state().value + 1);
}
}
Service locator
Sometime you may depends on an external system (for example a web API client, or a system service). The locator allows your to register service builders to offers access to these services from your actions.
First, register your service at the store level :
class MyStore extends Store<MainState> {
MyStore() : super(initialState: const MainState(child: ChildState(0))) {
registerService<ServiceClient>((state, services) => HttpServiceClient(host: state.config.host));
}
}
The services
property now gives you access to your ServiceClient
instance :
class AddServerValue extends StoreAction<CounterState> {
@override
Stream<CounterState> execute(
StateReader<CounterState> state,
Dispatcher dispatch,
ServiceLocator services,
) async* {
if (!state().isLoading) {
yield CounterState(state().value, true);
final service = services.create<ServiceClient>();
final serverValue = await service.getValue();
yield CounterState(state().value + serverValue, false);
}
}
}
Q&A #
Why publishing only a few classes and defining them as a new "state-management" solution ?
Yes, tide is just a StoreNotifier, but I copy paste those classes in every one of my projects. And it adds a few guidelines on how to architecture the application, which makes it easier to share.
What are the differences with redux ?
Redux has almost the same concepts than tide. But I've founded that it introduced too much boilerplate by having asynchronous (Thunks) and synchronous actions. Moreover most of the logic was splitted between synchronous reducers and asynchronous thunks. I like more the usage of Streams
and generators for emitting multiple updates (like in flutter_bloc) instead of a setter too.
Why not simply using a StateNotifier with methods instead of actions ?
Since I like the idea of having a single state (having a single state for the application has the advantage of being able to tackle concurrency errors and track dependencies) for the whole application, it can become a mess. And having classes for actions gives you the ability to trace every action, which helps for debugging.
What are the differences with flutter_bloc ?
FLutter bloc has a different architecture, where a multiple blocs are representing portions of the logic where events (Actions) are filtered in. It is not ideal when working with a single state.