StateFlow: State Management
StateFlow provides a simple and efficient way to manage state in your Flutter applications. This guide will walk you through the core concepts and usage of StateFlow's state management features.
Core Concepts
StateFlow's state management is built around these key concepts:
- StateFlowController: A base class for creating controllers that manage application logic and state.
- take(): A reactive value holder that notifies listeners when its value changes.
- StateFlowApp: A widget that sets up the StateFlow environment and dependency injection.
- StateValueBuilder: A widget that rebuilds when specified states change.
Setting Up
Add the dependency to your pubspec.yaml
file:
dependencies:
state_flow: ^0.0.8
Wrap your app with StateFlowApp
to enable the StateFlow functionality. Make sure to pass the controllers you want to use in the controllers
parameter.
void main() {
runApp(StateFlowApp(
controllers: [
() => CounterController(),
],
child: MyApp(),
));
}
Creating Controllers
Controllers are the core of StateFlow's state management. They manage application logic and state, and can be used to create reactive UIs.
To create a controller, extend StateFlowController
:
Declare Variables
// int
final counter = take(0);
// String
final name = take('');
// bool
final isLoading = take(false);
// List
final todos = take([]);
// Map
final user = take({});
// Object
final user = take(User());
class CounterController extends StateFlowController {
final counter = take(0);
void increment() {
counter.value++;
}
}
Using StateValueBuilder
final counterController = listen(CounterController)
StateValueBuilder(
value: counterController.counter,
builder: (value) => Text('Counter: $value'),
),
Networking
StateFlow provides networking features using the StateFlowClient
.
Use the StateFlowClient
At first you need to create a StateFlowClient. This is used to send requests to the server.
final client = StateFlowClient(baseUrl: 'example.com');
final response = await client.sendRequest('/todos', HttpMethod.GET);
HttpMethod
You use these methods to send requests to the server.
enum HttpMethod {
GET,
POST,
PUT,
DELETE,
}
Creating a Network Controller
To create a network controller, extend StateFlowController
:
If you want to use the Controller to fetch data or or perform any kind of logic then you can extend StateFlowController
with your normal controller.
class TodoController extends StateFlowController {
final todos = take<List<Todo>>([]);
// Base url for the api
final StateFlowClient _client =
StateFlowClient(baseUrl: 'example.com');
Future<List<Todo>> fetchTodos() async {
// Set the state to loading
todos.setLoading();
try {
final response = await _client.sendRequest('/todos', HttpMethod.GET);
if (response.statusCode == 200) {
// Set the state to loaded
final List<dynamic> jsonData = jsonDecode(response.body);
final todoList = jsonData.map((json) => Todo.fromJson(json)).toList();
// Set the state to loaded
todos.setLoaded(todoList);
return todos.value;
} else {
// Set the state to error
todos.setError('Failed to load todos');
return [];
}
} catch (e) {
// Set the state to error
todos.setError(e);
todos.stopLoading();
rethrow;
}
}
}
Building Reactive UI
You can use the listen
function to listen to the state of the controller. If you want to build a widget that rebuilds when the state changes, you can use the WidgetStateValueBuilder
widget.
final todoController = listen(TodoController);
WidgetStateValueBuilder<List<Todo>>(
state: todoController.todos,
dataBuilder: (data) {
if (data.isEmpty) {
return Text('No todos');
}
return Expanded(
child: ListView.builder(
itemCount: data.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(data[index].title),
);
},
),
);
},
errorBuilder: (error) => Text('Error: ${error.toString()}'),
loadingBuilder: () => CircularProgressIndicator(),
),
You can pass multiple values to the StateValueBuilder
by using the StateValueBuilder
.
StateValueBuilder(
values: [
counterController.count,
false,
'Hello',
false,
],
builder: (values) {
var [count, isActive, greeting, otherValue] = values;
return Column(
children: [
Text('Counter: $count'),
Icon(isActive ? Icons.check : Icons.close),
Text(greeting),
Text('Other value: $otherValue'),
],
);
},
),
Animation Controller
If you want to use the animation controller without StateFulWidget, you can use the takeAnimationController
function.
class TestApp extends StatelessWidget {
const TestApp({super.key});
@override
Widget build(BuildContext context) {
final animationController = takeAnimationController();
return const Placeholder();
}
}
StateFlowWidget
If you want to use initState and disposeState to be called, you can use StateFlowWidget.
class TestApp extends StateFlowWidget {
TestApp({super.key}) : super();
@override
void onInit(context) {
print('TestApp onInit');
}
@override
void onDispose() {
print('TestApp onDispose');
}
@override
Widget build(BuildContext context) {
return const Placeholder();
}
}
StateFlow Navigation
For enabling navigation in your app, you can use the navigatorKey: StateFlow.navigatorKey
in your MaterialApp.
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'StateFlow Demo',
navigatorKey: StateFlow.navigatorKey,
theme: ThemeData(primarySwatch: Colors.blue),
);
}
}
Use the StateFlow.to
method to navigate to a new screen.
StateFlow.to(DetailScreen());
For going back to the previous screen, you can use the StateFlow.back
method.
StateFlow.back();
StateFlow.off(DetailScreen());
StateFlow.offAll(DetailScreen());
StateFlow.maybePop();
StateFlow.currentRoute();
StateFlow.getHistory();
onGenerateRoute: (settings) => StateFlow.onGenerateRoute(settings),
You can create your App Routes like this
class AppRoutes {
static const String home = '/';
static const String second = '/second';
static final routes = {
home: (context) => HomeScreen(),
second: (context) => SecondScreen(),
};
}
Modify the MaterialApp like this use onGenerateRoute: (settings) => StateFlow.onGenerateRoute(settings)
, routes: AppRoutes.routes
and navigatorKey: StateFlow.navigatorKey
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'StateFlow Demo',
navigatorKey: StateFlow.navigatorKey,
onGenerateRoute: (settings) => StateFlow.onGenerateRoute(settings),
theme: ThemeData(primarySwatch: Colors.blue),
routes: AppRoutes.routes,
);
}
}
Now You can use with your routes
onPressed: () => StateFlow.to('/second')
onPressed: () => StateFlow.to(AppRoutes.second)