Please note that the example we'll be looking at is purelly for illustration purposes. If you would like to answer using a different example that's ok, as we're trying to get to an idiomatic way of solving this type of problem rather that this problem.
Setup:
Provider
library availabe. We also use flutter_hooks
but that's not required for the answer.PageView
and a Continue
button.PageView
is not "swippable", the idea is that you navigate through some sort of onboarding using the "Continue" button. The PageView
is coupled to a PageController
.Currently, it's the top-level widget that controls this continue button... And it's very cumbersome, as every time the page index changes, the conditions are in the top-level widget to change the title of the button, to disable it, run the async actions between pages, etc.
We are looking for a cleaner way of doing this - for example, what would be very nice would be to design an architecture that would instead allow:
Something like this:
The communication from the pages to the top-level widget is easy - a context.read<OnboardingProvider>().doSomething()
does the trick. It's the other way around which seems difficult.
The idea would be to have each page have some degree of control (both visual and business logic) over the top-level widget button.
Any ideas? Thanks!
In the end this is what we came up with:
We created an interface for all pages to comply with. Something like:
typedef ContinueCallback = Future<void> Function(BuildContext);
@immutable
abstract class IOnboardingView extends StatelessWidget {
const IOnboardingView({super.key});
ContinueCallback get onContinue;
String buttonLabel(BuildContext context);
}
The idea is to make sure that all children pages implement it, and can provide both a callback to provide validation rules when the button is pushed (Or null to disable the button), and the title of the button itself.
On the top-level page, the build
function can now look like this:
@override
Widget build(BuildContext context) {
final state = context.watch<ob_state.OnboardingState>();
final pageIndex = state.index;
final pagerMessage = state.message;
final page = state.pages[state.index];
void nextPage() => state.nextPage(context);
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Pager(total: _OnboardingPage.values.length, index: pageIndex),
if (pagerMessage != null)
Padding(
padding: const EdgeInsets.symmetric(vertical: AppTheme.spaceS),
child: Text(pagerMessage),
),
Container(
width: double.infinity,
height: 50,
margin: const EdgeInsets.all(20),
child: FilledButton(
onPressed: state.enabled ? nextPage : null,
child: Text(page.buttonLabel(context)),
),
),
],
);
}
Note that the state is provided via a context
provider (See context.watch<ob_state.OnboardingState>()
).
The provider looks like something like this (Note how all children pages are List<IOnboardingView> pages
and therefore "provide" what the top-level page needs to know):
ChangeNotifierProvider<OnboardingState> provider() => ChangeNotifierProvider(create: (context) => OnboardingState());
class OnboardingState with ChangeNotifier {
bool _disposed = false;
bool _enabled = true;
String? _message;
final List<IOnboardingView> pages = [
const SafetyView(),
const PreferencesView(),
const PermissionsPage(),
const VerifyAccountView()
];
final PageController pageCtrl = PageController();
int _index = 0;
OnboardingState();
@override
void dispose() {
_disposed = true;
pageCtrl.dispose();
super.dispose();
}
bool get enabled => _enabled;
set enabled(bool value) => value ? enable() : disable();
void enable() {
if (_enabled && _message == null) return;
_enabled = true;
_message = null;
_maybeNotifyListeners();
}
void disable([String? message]) {
if (!_enabled && _message == message) return;
_enabled = false;
_message = message;
_maybeNotifyListeners();
}
void nextPage(BuildContext context) {
disable();
page
.onContinue(context)
.then((value) => pageCtrl.nextPage(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
))
.whenComplete(enable);
}
String? get message => _message;
IOnboardingView get page => pages[_index];
int get index => _index;
set index(int i) {
if (_index == i) return;
_index = i;
_maybeNotifyListeners();
}
void _maybeNotifyListeners() {
if (_disposed) return;
notifyListeners();
}
}