Skip to content

Flutter

Use SoloListenable<S> from flutter_solo as the controller base class. It extends Solo<S> and implements ValueListenable<S>. The profile controller keeps the same states and load method:

import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_solo/flutter_solo.dart';
final class ProfileController extends SoloListenable<ProfileState> {
final ProfileApi api;
ProfileController(this.api) : super(const Initial());
// ...the jobs from the quick start...
}

A screen can own its controller: create it in initState and close it in dispose. A shared controller can instead live in your existing dependency container, such as provider, get_it or an InheritedWidget. The code that owns it is responsible for closing it; flutter_solo does not provide a SoloProvider or close controllers automatically.

ValueListenableBuilder rebuilds when state changes. To navigate or show a message after a particular operation, await that job’s outcome at the call site. Check mounted after waiting before using the widget’s context. Here, ProfilePage is the destination screen in the application:

class ProfileScreen extends StatefulWidget {
const ProfileScreen({super.key});
@override
State<ProfileScreen> createState() => _ProfileScreenState();
}
class _ProfileScreenState extends State<ProfileScreen> {
late final ProfileController profile;
@override
void initState() {
super.initState();
profile = ProfileController(ProfileApi());
}
@override
void dispose() {
unawaited(profile.close());
super.dispose();
}
Future<void> _open() async {
final outcome = await profile.load().done;
if (!mounted) {
return;
}
switch (outcome) {
case Done():
await Navigator.of(context).push(
MaterialPageRoute<void>(builder: (_) => const ProfilePage()),
);
case Failed(:final error):
ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(content: Text('$error')));
case Cancelled():
break;
}
}
@override
Widget build(BuildContext context) => Scaffold(
body: Center(
child: ValueListenableBuilder<ProfileState>(
valueListenable: profile,
builder: (context, state, _) => state is Loading
? const CircularProgressIndicator()
: ElevatedButton(
onPressed: _open,
child: const Text('Open profile'),
),
),
),
);
}

value and state refer to the same object. There is no value setter; controller jobs perform updates through their context. ListenableBuilder and AnimatedBuilder also accept the controller when the builder does not need the state value itself.