dataflow 2.0.0-beta.5 copy "dataflow: ^2.0.0-beta.5" to clipboard
dataflow: ^2.0.0-beta.5 copied to clipboard

A reactive state management library for Flutter with a simple and intuitive API which allows you to build Flutter applications with ease.

DataFlow #

Pub Version License

A reactive state management library for Flutter with a simple and intuitive API which allows you to build Flutter applications with ease.

Documentation #


Table of Contents #


Why DataFlow? #

DataFlow was designed with these principles in mind:

Principle Description
Minimal Boilerplate No code generation, no complex setup. Just extend a class and go.
Action-Centric Every async operation is an Action with built-in status tracking.
Reactive by Default UI automatically updates when actions complete.
Error Handling Built-in Actions catch errors automatically with stack traces.
Flexible Works for simple apps and scales to complex ones.

Quick Start #

Step 1: Add Dependency #

dependencies:
  dataflow: ^2.0.0-beta.4

Step 2: Create Your Store #

import 'package:dataflow/dataflow.dart';

class AppStore extends DataStore {
  List<String> todos = [];
  bool isLoggedIn = false;
  String? username;
}

Step 3: Create Your First Action #

class AddTodoAction extends DataAction<AppStore> {
  final String todo;

  AddTodoAction(this.todo);

  @override
  dynamic execute() {
    store.todos.add(todo);
  }
}

Step 4: Initialize and Use #

void main() {
  DataFlow.init(AppStore());
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: DataSync<AppStore>(
        actions: const {AddTodoAction},
        builder: (context, store, hasData) {
          return TodoListScreen(todos: store.todos);
        },
      ),
    );
  }
}

// Trigger the action from anywhere
ElevatedButton(
  onPressed: () => AddTodoAction('Buy groceries'),
  child: Text('Add Todo'),
)

That's it! The UI automatically updates when the action completes.


Core Concepts #

DataStore #

The DataStore is your single source of truth. It holds all application state.

class AppStore extends DataStore {
  // User state
  User? currentUser;
  bool isLoggedIn = false;

  // Todo state
  List<Todo> todos = [];
  bool isLoadingTodos = false;

  // App state
  ThemeMode themeMode = ThemeMode.system;
  String? errorMessage;
}

Accessing the store:

// From anywhere in your code
final store = DataFlow.getStore<AppStore>();

// From a BuildContext (extension method)
final store = context.getStore<AppStore>();

// From inside a DataAction
@override
dynamic execute() {
  store.currentUser = user; // 'store' is available automatically
}

DataAction #

A DataAction represents an operation that can be sync or async. It has automatic status tracking: idleloadingsuccess or error or cancelled.

Basic Action

class FetchTodosAction extends DataAction<AppStore> {
  @override
  dynamic execute() async {
    final response = await api.getTodos();
    store.todos = response.data;
  }
}

// Trigger it
FetchTodosAction();

Action with Parameters

class LoginAction extends DataAction<AppStore> {
  final String email;
  final String password;

  LoginAction(this.email, this.password);

  @override
  dynamic execute() async {
    final user = await authService.login(email, password);
    store.currentUser = user;
    store.isLoggedIn = true;
  }
}

// Trigger with parameters
LoginAction('user@example.com', 'password123');

Action Status

Every action tracks its status automatically:

enum DataActionStatus {
  idle,       // Initial state
  loading,    // Execute is running
  success,    // Execute completed without error
  error,      // Execute threw an error
  cancelled,  // Action was cancelled (v2.0+)
}

Error Handling

Errors are caught automatically:

class FetchDataAction extends DataAction<AppStore> {
  @override
  dynamic execute() async {
    final data = await api.getData(); // May throw
    store.data = data;
  }

  // Optional: Custom error handling
  @override
  void onException(Object error, StackTrace stackTrace) {
    super.onException(error, stackTrace); // Logs in debug mode
    analytics.logError(error, stackTrace);
  }
}

After an error, access it via:

  • action.error - The error object
  • action.errorStackTrace - The stack trace (v2.0+)

Awaiting Actions (v2.0+)

// Wait for action to complete
await LoginAction(email, password).future;

// Now check the result
if (store.isLoggedIn) {
  navigateToHome();
}

DataSync #

DataSync is a widget that rebuilds when specified actions emit events.

Basic Usage

DataSync<AppStore>(
  actions: const {FetchTodosAction},
  builder: (context, store, hasActionExecuted) {
    return ListView.builder(
      itemCount: store.todos.length,
      itemBuilder: (context, index) => TodoTile(store.todos[index]),
    );
  },
)

With Loading and Error Builders

DataSync<AppStore>(
  actions: const {FetchTodosAction},
  loadingBuilder: (context) {
    return Center(child: CircularProgressIndicator());
  },
  errorBuilder: (context, error) {
    return Center(
      child: Column(
        children: [
          Text('Error: $error'),
          ElevatedButton(
            onPressed: () => FetchTodosAction(),
            child: Text('Retry'),
          ),
        ],
      ),
    );
  },
  builder: (context, store, hasData) {
    return TodoList(todos: store.todos);
  },
)

Handling Multiple Actions

DataSync<AppStore>(
  actions: const {FetchUsersAction, FetchPostsAction},
  disableLoadingBuilder: true, // Handle loading manually
  disableErrorBuilder: true,   // Handle errors manually
  builder: (context, store, hasData) {
    final state = context.dataSync<AppStore>();

    // Check specific action status
    if (state.getStatus(FetchUsersAction) == DataActionStatus.loading) {
      return Text('Loading users...');
    }

    // Check for specific errors
    final usersError = state.getError(FetchUsersAction);
    if (usersError != null) {
      return Text('Failed to load users: $usersError');
    }

    return YourWidget();
  },
)

Action Notifier (Side Effects)

Use actionNotifier for navigation, snackbars, or other side effects:

DataSync<AppStore>(
  actions: const {LoginAction},
  actionNotifier: {
    LoginAction: (context, action, status) {
      if (status == DataActionStatus.success) {
        Navigator.pushReplacementNamed(context, '/home');
      } else if (status == DataActionStatus.error) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text('Login failed: ${action.error}')),
        );
      }
    },
  },
  builder: (context, store, hasData) => LoginForm(),
)

DataSyncNotifier #

DataSyncNotifier is for side effects only - it doesn't rebuild UI.

DataSyncNotifier(
  actions: {
    LogoutAction: (context, action, status) {
      if (status == DataActionStatus.success) {
        Navigator.of(context).pushReplacementNamed('/login');
      }
    },
    PaymentAction: (context, action, status) {
      if (status == DataActionStatus.success) {
        showDialog(
          context: context,
          builder: (_) => PaymentSuccessDialog(),
        );
      }
    },
  },
  child: MyApp(),
)

DataSyncState #

Access DataSyncState for fine-grained control over action statuses.

// Get the state (throws if not found)
final state = context.dataSync<AppStore>();

// Safe version (returns null if not found)
final state = context.tryDataSync<AppStore>();

// Available methods and properties:
state.getStatus(ActionType)           // Get status of specific action
state.isAnyActionLoading              // Any action loading?
state.whichActionIsLoading            // Which action is loading?
state.hasAnyActionError               // Any action failed?
state.whichActionHasError             // Which action failed?
state.firstActionError                // Get first error
state.firstActionStackTrace           // Get first stack trace (v2.0+)
state.isAnyActionSuccessful           // Any action succeeded?
state.areAllActionsSuccessful         // All actions succeeded?
state.isAnyActionCancelled            // Any action cancelled? (v2.0+)
state.getError(ActionType)            // Get error for specific action
state.getStackTrace(ActionType)       // Get stack trace (v2.0+)
state.resetStatus(ActionType)         // Reset specific action to idle
state.resetAllStatuses()              // Reset all actions to idle

Step-by-Step Guides #

Building a Todo App #

1. Define the Store

class TodoStore extends DataStore {
  List<Todo> todos = [];
}

class Todo {
  final String id;
  final String title;
  bool isCompleted;

  Todo({required this.id, required this.title, this.isCompleted = false});
}

2. Create Actions

class FetchTodosAction extends DataAction<TodoStore> {
  @override
  dynamic execute() async {
    // Simulate API call
    await Future.delayed(Duration(seconds: 1));
    store.todos = [
      Todo(id: '1', title: 'Learn DataFlow'),
      Todo(id: '2', title: 'Build an app'),
    ];
  }
}

class AddTodoAction extends DataAction<TodoStore> {
  final String title;
  AddTodoAction(this.title);

  @override
  dynamic execute() {
    store.todos.add(Todo(
      id: DateTime.now().toString(),
      title: title,
    ));
  }
}

class ToggleTodoAction extends DataAction<TodoStore> {
  final String id;
  ToggleTodoAction(this.id);

  @override
  dynamic execute() {
    final todo = store.todos.firstWhere((t) => t.id == id);
    todo.isCompleted = !todo.isCompleted;
  }
}

class DeleteTodoAction extends DataAction<TodoStore> {
  final String id;
  DeleteTodoAction(this.id);

  @override
  dynamic execute() {
    store.todos.removeWhere((t) => t.id == id);
  }
}

3. Build the UI

class TodoApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Todo App')),
      body: DataSync<TodoStore>(
        actions: const {
          FetchTodosAction,
          AddTodoAction,
          ToggleTodoAction,
          DeleteTodoAction,
        },
        loadingBuilder: (_) => Center(child: CircularProgressIndicator()),
        builder: (context, store, hasData) {
          if (!hasData) {
            // First build - trigger fetch
            FetchTodosAction();
            return Center(child: CircularProgressIndicator());
          }

          return ListView.builder(
            itemCount: store.todos.length,
            itemBuilder: (context, index) {
              final todo = store.todos[index];
              return ListTile(
                leading: Checkbox(
                  value: todo.isCompleted,
                  onChanged: (_) => ToggleTodoAction(todo.id),
                ),
                title: Text(
                  todo.title,
                  style: TextStyle(
                    decoration: todo.isCompleted
                        ? TextDecoration.lineThrough
                        : null,
                  ),
                ),
                trailing: IconButton(
                  icon: Icon(Icons.delete),
                  onPressed: () => DeleteTodoAction(todo.id),
                ),
              );
            },
          );
        },
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => _showAddDialog(context),
        child: Icon(Icons.add),
      ),
    );
  }

  void _showAddDialog(BuildContext context) {
    final controller = TextEditingController();
    showDialog(
      context: context,
      builder: (_) => AlertDialog(
        title: Text('Add Todo'),
        content: TextField(controller: controller),
        actions: [
          TextButton(
            onPressed: () => Navigator.pop(context),
            child: Text('Cancel'),
          ),
          ElevatedButton(
            onPressed: () {
              AddTodoAction(controller.text);
              Navigator.pop(context);
            },
            child: Text('Add'),
          ),
        ],
      ),
    );
  }
}

Authentication Flow #

1. Store

class AuthStore extends DataStore {
  User? currentUser;
  String? authToken;
  bool get isLoggedIn => currentUser != null;
}

2. Actions

class LoginAction extends DataAction<AuthStore> {
  final String email;
  final String password;

  LoginAction(this.email, this.password);

  @override
  dynamic execute() async {
    final response = await authApi.login(email, password);
    store.authToken = response.token;
    store.currentUser = response.user;
  }
}

class LogoutAction extends DataAction<AuthStore> {
  @override
  dynamic execute() async {
    await authApi.logout();
    store.authToken = null;
    store.currentUser = null;
  }
}

class CheckAuthAction extends DataAction<AuthStore> {
  @override
  dynamic execute() async {
    final token = await secureStorage.read('token');
    if (token != null) {
      store.authToken = token;
      store.currentUser = await authApi.getProfile(token);
    }
  }
}

3. Auth-Aware App

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: DataSync<AuthStore>(
        actions: const {CheckAuthAction, LoginAction, LogoutAction},
        builder: (context, store, hasData) {
          if (!hasData) {
            CheckAuthAction();
            return SplashScreen();
          }
          return store.isLoggedIn ? HomeScreen() : LoginScreen();
        },
        actionNotifier: {
          LoginAction: (context, action, status) {
            if (status == DataActionStatus.error) {
              ScaffoldMessenger.of(context).showSnackBar(
                SnackBar(content: Text('Login failed: ${action.error}')),
              );
            }
          },
        },
      ),
    );
  }
}

Pagination Example #

class PostStore extends DataStore {
  List<Post> posts = [];
  int currentPage = 0;
  bool hasMore = true;
  bool isLoadingMore = false;
}

class FetchPostsAction extends DataAction<PostStore> {
  final bool refresh;

  FetchPostsAction({this.refresh = false});

  @override
  dynamic execute() async {
    if (refresh) {
      store.currentPage = 0;
      store.posts.clear();
    }

    store.isLoadingMore = true;

    final response = await api.getPosts(page: store.currentPage);
    store.posts.addAll(response.posts);
    store.hasMore = response.hasMore;
    store.currentPage++;
    store.isLoadingMore = false;
  }
}

// In your widget
NotificationListener<ScrollNotification>(
  onNotification: (notification) {
    if (notification.metrics.pixels >=
        notification.metrics.maxScrollExtent - 200) {
      final store = context.getStore<PostStore>();
      if (store.hasMore && !store.isLoadingMore) {
        FetchPostsAction();
      }
    }
    return false;
  },
  child: ListView.builder(
    itemCount: store.posts.length + (store.hasMore ? 1 : 0),
    itemBuilder: (context, index) {
      if (index == store.posts.length) {
        return Center(child: CircularProgressIndicator());
      }
      return PostCard(store.posts[index]);
    },
  ),
)

Advanced Features #

Middleware #

Middleware intercepts actions before and after execution.

class LoggingMiddleware extends DataMiddleware {
  @override
  bool preDataAction(DataAction action) {
    print('▶ Starting: ${action.runtimeType}');
    return true; // Return false to cancel the action
  }

  @override
  void postDataAction(DataAction action) {
    print('✓ Finished: ${action.runtimeType} - ${action.status}');
    if (action.status == DataActionStatus.error) {
      print('  Error: ${action.error}');
    }
  }
}

class AnalyticsMiddleware extends DataMiddleware {
  @override
  bool preDataAction(DataAction action) {
    analytics.trackEvent('action_started', {'action': action.runtimeType.toString()});
    return true;
  }

  @override
  void postDataAction(DataAction action) {
    analytics.trackEvent('action_completed', {
      'action': action.runtimeType.toString(),
      'status': action.status.toString(),
    });
  }
}

// Register middlewares
void main() {
  DataFlow.init(
    AppStore(),
    middlewares: [LoggingMiddleware(), AnalyticsMiddleware()],
  );
  runApp(MyApp());
}

// Dynamic middleware management (v2.0+)
DataFlow.addMiddleware(DebugMiddleware());
DataFlow.removeMiddleware(debugMiddleware);
DataFlow.clearMiddlewares();

Action Chaining #

Execute actions in sequence:

class FetchUserAction extends DataAction<AppStore> {
  @override
  dynamic execute() async {
    store.user = await api.getUser();

    // Chain next action
    next(() => FetchUserPostsAction(store.user!.id));
  }
}

Or use DataChain for cleaner chaining:

class FetchAndProcessAction extends DataAction<AppStore> with DataChain<User> {
  @override
  dynamic execute() async {
    return await api.getUser(); // Return result for fork
  }

  @override
  dynamic fork(User user) async {
    // Called after execute succeeds with the returned value
    store.userPosts = await api.getUserPosts(user.id);
  }
}

Action Cancellation (v2.0+) #

Cancel running actions:

class SearchAction extends DataAction<AppStore> {
  final String query;
  static SearchAction? _current;

  SearchAction(this.query) {
    // Cancel previous search
    _current?.cancel();
    _current = this;
  }

  @override
  dynamic execute() async {
    await Future.delayed(Duration(milliseconds: 300)); // Debounce

    if (isCancelled) return; // Check before expensive operation

    store.searchResults = await api.search(query);
  }
}

Check cancellation status:

final state = context.dataSync<AppStore>();
if (state.isAnyActionCancelled) {
  // Handle cancelled state
}

DataFlow Reset (v2.0+) #

Fully reinitialize DataFlow (useful for logout):

void logout() async {
  await LogoutAction().future;

  // Reset everything with a fresh store
  DataFlow.reset(
    AppStore(), // New empty store
    middlewares: [LoggingMiddleware()], // Re-add middlewares if needed
  );

  Navigator.pushReplacementNamed(context, '/login');
}

// Check if DataFlow was disposed
if (DataFlow.isDisposed) {
  DataFlow.reset(AppStore());
}

DataFlow Inspector #

DataFlow Inspector is a powerful visual debugging tool that helps you understand and debug your application's state management.

Setup #

Wrap your MaterialApp with DataFlowInspector:

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return DataFlowInspector(
      enabled: true, // Set to false or use kReleaseMode in production
      child: MaterialApp(
        home: HomeScreen(),
      ),
    );
  }
}

Actions Panel #

The Actions Panel shows all dispatched actions in real-time:

  • Status indicators: Loading (spinner), Success (✓), Error (✗), Cancelled (⊘)
  • Duration: How long each action took
  • Filter: Filter actions by type
  • Error details: See error messages for failed actions

Time Travel Debugging #

Step through your app's state history:

  • State snapshots: Captured after each action completes
  • Slider navigation: Scrub through state timeline
  • State diff: See exactly how state changed
  • Jump to any point: Click any snapshot to view that state

Insights #

Automatic detection of potential issues:

  • Slow Actions: Actions taking >5 seconds
  • Rapid-Fire Calls: Same action called multiple times quickly (may need debouncing)
  • Repeated Failures: Action failing multiple times in a row
  • Excessive Rebuilds: Widgets rebuilding too frequently

Retry/Replay Actions #

Re-run actions directly from the inspector:

  1. Add the Retryable mixin to your action:
class LoadPostsAction extends DataAction<AppStore> with Retryable<AppStore> {
  final bool refresh;

  LoadPostsAction({this.refresh = false});

  @override
  DataAction<AppStore> retry() => LoadPostsAction(refresh: refresh);

  @override
  Future<void> execute() async {
    // Your implementation
  }
}
  1. Failed actions show a Retry button (red)
  2. Successful actions show a Replay button (cyan)

Bug Report #

Capture a comprehensive bug report:

  • Screenshot of current screen
  • Current store state
  • Action history with timestamps
  • Device information

Inspector Controls #

  • Floating button (bottom-right): Toggle inspector panels
  • Actions panel: Tap header to expand/collapse
  • Time Travel panel: Use slider to navigate history
  • Insights badge: Shows count of detected issues

Comparison with Other Libraries #

Feature DataFlow Bloc Provider Riverpod GetX
Learning Curve Low High Low Medium Low
Boilerplate Minimal High Minimal Medium Minimal
Code Generation None Optional None None None
Async Built-in Yes (DataAction) Yes (Events) No Yes Yes
Status Tracking Automatic Manual Manual Manual Manual
Error Handling Automatic Manual Manual Manual Manual
Middleware Yes Yes No No No
Action Chaining Yes No No No No
Cancellation Yes (v2.0+) Yes No Yes No
Visual Debugger Yes (Inspector) Yes Yes Yes No

When to Use DataFlow #

Choose DataFlow if you want:

  • Minimal setup with no code generation
  • Automatic async status tracking
  • Built-in error handling with stack traces
  • Simple action-based architecture
  • Easy middleware integration

Consider alternatives if you need:

  • Extensive DevTools support (use Bloc)
  • Dependency injection built-in (use Riverpod)
  • Maximum flexibility (use Provider)
  • All-in-one solution (use GetX)

API Reference #

DataFlow (Static Class) #

Method Description
init<T>(store, {middlewares}) Initialize with store and optional middlewares
reset<T>(store, {middlewares}) Fully reinitialize (clears everything)
getStore<T>() Get the current store
events Stream of all actions
streamOf(Type) Stream filtered to specific action type
notify(action) Manually emit an action
addMiddleware(middleware) Add a middleware
removeMiddleware(middleware) Remove a specific middleware
clearMiddlewares() Remove all middlewares
dispose() Close the event stream
isDisposed Check if disposed

DataAction #

Property/Method Description
store Access the typed DataStore
status Current DataActionStatus
error Error if status is error
errorStackTrace Stack trace if status is error (v2.0+)
isCancelled Whether action was cancelled (v2.0+)
future Future that completes when action finishes (v2.0+)
execute() Override to define action logic
onException(e, s) Override to handle errors
next(builder) Chain another action
cancel() Cancel the action (v2.0+)

DataSync Widget #

Property Description
actions Set of action types to listen to (required)
builder Widget builder with (context, store, hasActionExecuted)
loadingBuilder Widget shown during loading
errorBuilder Widget shown on error with (context, error)
actionNotifier Map of action types to callbacks for side effects
disableLoadingBuilder Don't show loading widget automatically
disableErrorBuilder Don't show error widget automatically

DataSyncState #

Property/Method Description
allActionsStatus Map of all tracked action statuses
getStatus(Type) Get status of specific action
isAnyActionLoading Any action currently loading
whichActionIsLoading Type of loading action
hasAnyActionError Any action has error
whichActionHasError Type of errored action
firstActionError First error encountered
firstActionStackTrace Stack trace of first error (v2.0+)
isAnyActionSuccessful Any action succeeded
areAllActionsSuccessful All actions succeeded
isAnyActionCancelled Any action cancelled (v2.0+)
getError(Type) Get error for specific action
getStackTrace(Type) Get stack trace for specific action (v2.0+)
resetStatus(Type) Reset specific action to idle
resetAllStatuses() Reset all actions to idle

Context Extensions #

Method Description
context.getStore<T>() Get the typed store
context.dataSync<T>() Get DataSyncState (throws if not found)
context.tryDataSync<T>() Get DataSyncState or null

Best Practices #

1. Keep Actions Focused #

// Good: Single responsibility
class FetchUserAction extends DataAction<AppStore> { ... }
class UpdateUserAction extends DataAction<AppStore> { ... }

// Avoid: Multiple responsibilities
class UserAction extends DataAction<AppStore> {
  final String operation; // 'fetch', 'update', 'delete'
  ...
}

2. Use Meaningful Names #

// Good
class FetchUserProfileAction extends DataAction { ... }
class SubmitContactFormAction extends DataAction { ... }

// Avoid
class Action1 extends DataAction { ... }
class DoStuffAction extends DataAction { ... }

3. Handle Errors Gracefully #

DataSync<AppStore>(
  actions: const {FetchDataAction},
  errorBuilder: (context, error) {
    return ErrorWidget(
      message: _getErrorMessage(error),
      onRetry: () {
        context.dataSync<AppStore>().resetStatus(FetchDataAction);
        FetchDataAction();
      },
    );
  },
  builder: (context, store, hasData) => ...,
)

4. Use ActionNotifier for Navigation #

// Good: Side effects in actionNotifier
DataSync<AppStore>(
  actionNotifier: {
    LoginAction: (context, action, status) {
      if (status == DataActionStatus.success) {
        Navigator.pushReplacementNamed(context, '/home');
      }
    },
  },
  builder: (context, store, hasData) => LoginForm(),
)

// Avoid: Navigation in builder
builder: (context, store, hasData) {
  if (store.isLoggedIn) {
    Navigator.pushReplacementNamed(context, '/home'); // Causes issues
  }
  return LoginForm();
}

5. Reset on Logout #

void logout() async {
  await LogoutAction().future;
  DataFlow.reset(AppStore()); // Fresh start
  Navigator.pushReplacementNamed(context, '/login');
}

Migration Guide #

See MIGRATION.md for detailed v1.x to v2.0 migration instructions.

Quick Migration Checklist #

  • ❌ Update actions parameter (now required, non-nullable)
  • ❌ Update errorBuilder signature (Object instead of Exception)
  • ❌ Check areAllActionsSuccessful behavior (now false for empty)
  • ❌ Consider using new features: cancel(), future, getStackTrace()

FAQ #

Q: Do I need code generation? A: No, DataFlow works without any code generation.

Q: Can I use multiple stores? A: Currently DataFlow supports a single store. Use composition within your store for complex state.

Q: How do I test actions? A: Actions are plain classes - test execute() directly or use DataFlow.init() in test setup.

Q: Is DataFlow production-ready? A: v2.0 is in beta. v1.x is stable and used in production apps.

Q: How do I debug actions? A: Use LoggingMiddleware to trace all actions and their statuses.


License #

This project is licensed under the Apache 2.0 License - see the LICENSE file for details.


Happy coding with DataFlow!

11
likes
150
points
201
downloads

Publisher

verified publishercodepur.dev

Weekly Downloads

A reactive state management library for Flutter with a simple and intuitive API which allows you to build Flutter applications with ease.

Homepage
Repository (GitHub)
View/report issues

Documentation

Documentation
API reference

License

MIT (license)

Dependencies

flutter, rxdart

More

Packages that depend on dataflow