Skip to content

Async Operations

NeatNotifier simplifies asynchronous operations with the runTask method. It automatically handles loading states and error capturing, allowing you to focus on your business logic.

Using runTask

Wrap your async logic inside runTask. It will: 1. Set isLoading to true. 2. Clear any previous errors. 3. Execute your specific task. 4. Set isLoading to false when finished. 5. Capture any exceptions into the error property.

class UserNotifier extends NeatNotifier<String?, void> {
  UserNotifier() : super(null);

  Future<void> fetchUser() async {
    // runTask automatically handles isLoading and error records.
    await runTask(() async {
      // Simulate network delay
      await Future.delayed(const Duration(seconds: 2));

      // Update value on success
      value = 'John Doe';
    });
  }
}

UI Handling

NeatState provides built-in builders for loading and error states, making it easy to show the appropriate UI.

NeatState<UserNotifier, String?, void>(
  create: (_) => UserNotifier(),

  // Custom loading UI
  loadingBuilder: (context, loading, child) {
    return const Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          CircularProgressIndicator(),
          SizedBox(height: 16),
          Text('Loading user...'),
        ],
      ),
    );
  },

  // Custom error UI
  errorBuilder: (context, error, child) {
    return Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Icon(Icons.error_outline, color: Colors.red, size: 64),
          SizedBox(height: 16),
          Text('Error: ${error.error}'),
          SizedBox(height: 24),
          ElevatedButton(
            onPressed: () => context.read<UserNotifier>().fetchUser(),
            child: const Text('Retry'),
          ),
        ],
      ),
    );
  },

  // Success UI
  builder: (context, user, child) {
    return Center(
      child: Text(
        user != null ? 'Welcome, $user!' : 'No user data.',
        style: const TextStyle(fontSize: 24),
      ),
    );
  },
)

Accessing Loading State directly

You can also access the loading state manually if you are not using NeatState or want to use it in a different way (e.g. disabling a button).

final isLoading = context.select<UserNotifier, String?, bool>(
  (state) => context.read<UserNotifier>().isLoading
);

Example

import 'package:flutter/material.dart';
import 'package:neat_state/neat_state.dart';

void main() {
  runApp(const MyApp());
}

/// A notifier that simulates fetching a user name from a network.
class UserNotifier extends NeatNotifier<String?, void> {
  UserNotifier() : super(null);

  /// Fetches a user name asynchronously using [runTask].
  Future<void> fetchUser({bool simulateError = false}) async {
    // runTask automatically handles isLoading and error records.
    await runTask(() async {
      // Simulate network delay
      await Future.delayed(const Duration(seconds: 2));

      if (simulateError) {
        throw Exception('Failed to connect to the server');
      }

      // Manually update the value upon success.
      value = 'John Doe';
    });
  }
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      theme: ThemeData(useMaterial3: true, primarySwatch: Colors.blue),
      home: NeatState<UserNotifier, String?, void>(
        create: (_) => UserNotifier(),
        loadingBuilder: (context, loading, child) => const Scaffold(
          body: Center(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                CircularProgressIndicator(),
                SizedBox(height: 16),
                const Text('Loading user...'),
              ],
            ),
          ),
        ),
        errorBuilder: (context, error, child) => Scaffold(
          body: Center(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                const Icon(Icons.error_outline, color: Colors.red, size: 64),
                const SizedBox(height: 16),
                Text('Error: ${error.error}'),
                const SizedBox(height: 24),
                Builder(
                  builder: (context) => ElevatedButton(
                    onPressed: () => context.read<UserNotifier>().fetchUser(),
                    child: const Text('Retry'),
                  ),
                ),
              ],
            ),
          ),
        ),
        builder: (context, user, child) {
          return Scaffold(
            appBar: AppBar(title: const Text('NeatNotifier: runTask')),
            body: Center(
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  Text(
                    user != null ? 'Welcome, $user!' : 'No user data.',
                    style: const TextStyle(fontSize: 24),
                  ),
                  const SizedBox(height: 32),
                  Row(
                    mainAxisAlignment: MainAxisAlignment.center,
                    children: [
                      ElevatedButton(
                        onPressed: () =>
                            context.read<UserNotifier>().fetchUser(),
                        child: const Text('Fetch Success'),
                      ),
                      const SizedBox(width: 16),
                      ElevatedButton(
                        onPressed: () => context.read<UserNotifier>().fetchUser(
                          simulateError: true,
                        ),
                        style: ElevatedButton.styleFrom(
                          foregroundColor: Colors.red,
                        ),
                        child: const Text('Fetch Error'),
                      ),
                    ],
                  ),
                ],
              ),
            ),
          );
        },
      ),
    );
  }
}

Next Steps