states_rebuilder 5.2.0-dev-2 states_rebuilder: ^5.2.0-dev-2 copied to clipboard
a simple yet powerful state management technique for Flutter
import 'package:example/i18n.dart';
import 'package:flutter/material.dart';
import 'package:states_rebuilder/states_rebuilder.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'name_repository.dart';
// Inject the repository, so it can be mocked in test.
//
// As the behavior of the repository is not predicted because it depends on a
// random number.
// In test we define a fake implementation of NameRepository, and injected it
// using : repository.injectMock(()=> FakeNameRepository()); and voila, just
// pump the widget and test it predictably. (See test folder)
final repository = RM.inject(() => NameRepository());
// create a name state and inject it.
final name = RM.inject(() => '');
final helloName = RM.inject<String>(
() => 'Hello, ${name.state}',
// helloName depends on the name injected model.
// Whenever the name state changes the helloName will recalculate its
// creation function and notify its listeners.
//
// helloName state status is a combination of its own state and the state
// of the injected models that it depends on.
// ex: if name is waiting => helloName is waiting,
// if name has error => helloName has error,
// if name has data => helloName state will be recalculated
dependsOn: DependsOn(
{name},
// Do not recalculate until 400 ms has passed without any
// further notification from name injected model.
debounceDelay: 400,
),
// Execute side effects while notify the state
//
// It take on On objects, it has many named constructor: On.data, On.error,
// On.waiting, On.all and On.or
sideEffects: SideEffects(
onSetState: (snapState) {
snapState.onOrElse(
onWaiting: () => RM.scaffold.showSnackBar(
SnackBar(
content: Row(
children: const [
Text('Waiting ...'),
Spacer(),
CircularProgressIndicator(),
],
),
),
),
onError: (err, refresh) => RM.scaffold.showSnackBar(
SnackBar(content: Text('${err.message}')),
),
// the other case. hide the snackbar
orElse: (_) => RM.scaffold.hideCurrentSnackBar(),
);
},
),
//Set the undoStackLength to 5. This will automatically
// enable doing and undoing of the state
undoStackLength: 5,
);
//Stream that emits the entered name letter by letter
final streamedHelloName = RM.injectStream<String>(
() async* {
if (name.isActive && name.state.isEmpty) {
throw Exception(i18n.state.enterYourName);
}
final letters = name.state.trim().split('');
var n = '';
for (var letter in letters) {
await Future.delayed(const Duration(milliseconds: 50));
// yield the name letter by letter
yield n += letter;
}
},
initialState: '',
onInitialized: (state, subscription) {
// As the stream will start automatically on creation,
// we use the onInitialized hook to pause it.
subscription.pause();
},
);
//
//
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key key}) : super(key: key);
@override
Widget build(BuildContext context) {
return TopAppWidget(
injectedI18N: i18n,
builder: (context) {
return MaterialApp(
// To navigate and show snackBars without the BuildContext, we define
// the navigator key
navigatorKey: RM.navigate.navigatorKey,
locale: i18n.locale,
localeResolutionCallback: i18n.localeResolutionCallback,
localizationsDelegates: const [
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
],
home: const HomeWidget(),
debugShowCheckedModeBanner: false,
);
},
);
}
}
class HomeWidget extends StatelessWidget {
const HomeWidget({
Key key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(i18n.of(context).helloWorldExample),
actions: [
OnReactive(
() => DropdownButton<Locale>(
value: i18n.locale,
onChanged: (Locale locale) {
i18n.locale = locale;
},
items: i18n.supportedLocales
.map(
(locale) => DropdownMenuItem<Locale>(
child: Text('$locale'),
value: locale,
),
)
.toList(),
),
),
],
),
body: OnReactive(
//One OnReactiveModel on top can detects all states it depends on, even
//deeply nested in the widget tree provided that the widget is not loaded lazily
//as inside the builder of ListView.builder.
() => Column(
// ignore: prefer_const_literals_to_create_immutables
children: [
//For demo purpose, App is fractured to smaller widget.
//Notes that widget are const,
const TextFieldWidget(),
const Spacer(),
// ignore: prefer_const_constructors
HelloNameWidget(), // Not const to make it rebuildable
const Spacer(),
const RaisedButtonWidget(),
const SizedBox(height: 20),
// ignore: prefer_const_constructors
StreamNameWidget(),
const Spacer(),
],
),
),
);
}
}
class RaisedButtonWidget extends StatelessWidget {
const RaisedButtonWidget({
Key key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return ElevatedButton(
child: Text(i18n.of(context).startStreaming),
onPressed: () {
// Calling refresh on any injected will re-execute its creation
// Function and notify its listeners
streamedHelloName.refresh();
},
);
}
}
class HelloNameWidget extends StatelessWidget {
const HelloNameWidget({
Key key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Row(
children: [
IconButton(
icon: const Icon(Icons.arrow_left_rounded, size: 40),
onPressed:
helloName.canUndoState ? () => helloName.undoState() : null,
),
const Spacer(),
Center(
child: helloName.onAll(
// This part will be re-rendered each time the helloName
// emits notification of any kind of status (idle, waiting,
// error, data).
onIdle: () => Text(i18n.of(context).enterYourName),
onWaiting: () => const CircularProgressIndicator(),
onError: (err, refresh) => Text('${err.message}'),
onData: (data) => Text(data),
),
),
const Spacer(),
IconButton(
icon: const Icon(Icons.arrow_right_rounded, size: 40),
onPressed:
helloName.canRedoState ? () => helloName.redoState() : null,
),
],
);
}
}
class TextFieldWidget extends StatelessWidget {
const TextFieldWidget({
Key key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return TextField(
onChanged: (String value) {
// state mutation
name.setState(
(s) => repository.state.getNameInfo(value),
// You can debounce from here so that the getNameInfo method
// will not be invoked unless 400ms has passed without and other
// setState call.
// debounceDelay: 400,
);
// After state mutation, notify helloName to recalculate
// and rebuild
},
);
}
}
class StreamNameWidget extends StatelessWidget {
const StreamNameWidget({
Key key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return streamedHelloName.onOrElse(
onError: (err, refresh) => Text(
err.message,
style: const TextStyle(color: Colors.red),
),
//This will rebuild if the stream emits valid data only
orElse: (data) => Text(data),
);
}
}