Skip to main content

flutter_solo 0.5.0 copy "flutter_solo: ^0.5.0" to clipboard
flutter_solo: ^0.5.0 copied to clipboard

State management for Flutter: sequential jobs, exclusive state ownership, cooperative cancellation, rebuilds through SoloBuilder or ValueListenable.

example/lib/main.dart

import 'dart:async';

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

/// The model and the job of the README's Usage, with a state to fall back
/// to, on a screen that answers every way a load can end: with a name,
/// with a failure, cancelled, or handed to a tap that came while it ran.
void main() => runApp(MaterialApp(home: ProfileScreen(api: ProfileApi())));

/// A fake profile service: slow, and failing every third call.
class ProfileApi {
  int _calls = 0;

  /// How many times [fetchName] has been called.
  int get calls => _calls;

  Future<String> fetchName() async {
    final call = ++_calls;
    await Future<void>.delayed(const Duration(seconds: 2));
    if (call % 3 == 0) {
      throw StateError('the network is having a day');
    }

    return 'Ada Lovelace';
  }
}

sealed class Profile {}

final class Empty extends Profile {}

final class Loading extends Profile {}

final class Loaded extends Profile {
  final String name;

  Loaded(this.name);
}

/// One state, one job over it, a policy for the second tap, and the state
/// to fall back to when the job does not get to the end.
final class ProfileController extends Solo<Profile> {
  final ProfileApi api;

  ProfileController(this.api) : super(Empty());

  Job<String> load() => enqueue<Profile, String>(
        key: 'load',
        policy: Policy.droppable, // a second tap gets the first job
        (ctx) async {
          ctx.emit(Loading());
          final name = await ctx.abandonable(api.fetchName);
          ctx.emit(Loaded(name));

          return name;
        },
        // Without these a load that fails or is cancelled leaves the state
        // on Loading: a spinner, and nothing on the screen to tap.
        ifFailed: (state, error, stackTrace) => Empty(),
        ifCancelled: (state, cancelled) => Empty(),
      );
}

class ProfileScreen extends StatefulWidget {
  final ProfileApi api;

  const ProfileScreen({required this.api, super.key});

  @override
  State<ProfileScreen> createState() => _ProfileScreenState();
}

class _ProfileScreenState extends State<ProfileScreen> {
  late ProfileController controller = ProfileController(widget.api);

  @override
  void didUpdateWidget(ProfileScreen oldWidget) {
    super.didUpdateWidget(oldWidget);
    // A State outlives its widget, and a field is built once: a screen
    // handed another api keeps the controller over the old one unless it
    // builds a new one here. Closing the old one cancels its load.
    if (!identical(widget.api, oldWidget.api)) {
      unawaited(controller.close());
      controller = ProfileController(widget.api);
    }
  }

  @override
  void dispose() {
    // Nothing closes the controller for you, and closing it cancels the
    // job that is still in flight: leaving the screen is that cheap.
    unawaited(controller.close());
    super.dispose();
  }

  Future<void> _load() async {
    switch (await controller.load().done) {
      case Done(:final value):
        _say('hello $value');
      case Failed(:final error):
        _say('$error');
      case Cancelled():
        break; // the Cancel button, or close() when the screen went away
    }
  }

  void _say(String message) {
    if (!mounted) {
      return;
    }
    ScaffoldMessenger.of(context)
      ..hideCurrentSnackBar()
      ..showSnackBar(SnackBar(content: Text(message)));
  }

  @override
  Widget build(BuildContext context) => Scaffold(
        appBar: AppBar(
          title: const Text('flutter_solo'),
          actions: [
            // There in every state, so it can be tapped while a load runs:
            // the policy hands that tap the running job, and both taps hear
            // the one outcome.
            IconButton(
              onPressed: _load,
              tooltip: 'Reload',
              icon: const Icon(Icons.refresh),
            ),
          ],
        ),
        body: Center(
          child: SoloBuilder<Profile>(
            solo: controller,
            builder: (context, state, _) => switch (state) {
              Empty() => TextButton(
                  onPressed: _load,
                  child: const Text('Load'),
                ),
              Loading() => Column(
                  mainAxisSize: MainAxisSize.min,
                  children: [
                    const CircularProgressIndicator(),
                    TextButton(
                      onPressed: controller.cancelAll,
                      child: const Text('Cancel'),
                    ),
                  ],
                ),
              Loaded(:final name) => Text(name),
            },
          ),
        ),
      );
}
0
likes
0
points
1.01k
downloads

Publisher

verified publisheryet-another.dev

Weekly Downloads

State management for Flutter: sequential jobs, exclusive state ownership, cooperative cancellation, rebuilds through SoloBuilder or ValueListenable.

Repository (GitHub)
View/report issues

Topics

#state-management #async #cancellation #concurrency

License

unknown (license)

Dependencies

flutter, solo

More

Packages that depend on flutter_solo