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)

Libraries

state_flow