Skip to main content

flutter_solo 1.0.0 copy "flutter_solo: ^1.0.0" to clipboard
flutter_solo: ^1.0.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 of the README's Usage, its job without `isExpectedError`,
/// 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
160
points
993
downloads

Documentation

API reference

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

BSD-3-Clause (license)

Dependencies

flutter, solo

More

Packages that depend on flutter_solo