Awesome Flutter package which simplifies navigation flows with a flexible, declarative API, flutter_builder
Introduction
Flutter plugins are lightweight Dart wrappers for native mobile APIs and services (Java, Kotlin, ObjC, Swift). For example, the only method to access a sensor on the phone is to develop a plugin (or utilize one that already exists).
A package is a namespace that groups together classes and interfaces that are related. Packages can be compared to different folders on your computer in terms of concept. HTML pages might go in one folder, graphics in another, and scripts or apps in a third.
The plugin’s API is written in Dart. The plugin is developed in Java/Kotlin (for Android support), ObjC/Swift (for iOS support), or both (for both Android and iOS support) (for cross-platform support). End-to-end testing of plugins is difficult currently. This is precisely what it appears to be. A package is written entirely in Dart.
Plugins are (special) Dart packages, as well. They’re published to Pub, and you can interact with them using Dart. The key difference between the two is that you don’t have to create any native code with a pure Dart package, and testing is a breeze.
Flutter Flows made easy!
Usage
Define a Flow State
The flow state will be the state which drives the flow. Each time this state changes, a new navigation stack will be generated based on the new flow state.
class Profile { const Profile({this.name, this.age, this.weight}); final String? name; final int? age; final int? weight; Profile copyWith({String? name, int? age, int? weight}) { return Profile( name: name ?? this.name, age: age ?? this.age, weight: weight ?? this.weight, ); } }
Create a FlowBuilder
FlowBuilder
is a widget which builds a navigation stack in response to changes in the flow state. onGeneratePages
will be invoked for each state change and must return the new navigation stack as a list of pages.
FlowBuilder<Profile>( state: const Profile(), onGeneratePages: (profile, pages) { return [ MaterialPage(child: NameForm()), if (profile.name != null) MaterialPage(child: AgeForm()), ]; }, );
Update the Flow State
The state of the flow can be updated via context.flow<T>().update
.
class NameForm extends StatefulWidget { @override _NameFormState createState() => _NameFormState(); } class _NameFormState extends State<NameForm> { var _name = ''; void _continuePressed() { context.flow<Profile>().update((profile) => profile.copyWith(name: _name)); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('Name')), body: Center( child: Column( children: <Widget>[ TextField( onChanged: (value) => setState(() => _name = value), decoration: InputDecoration( labelText: 'Name', hintText: 'John Doe', ), ), RaisedButton( child: const Text('Continue'), onPressed: _name.isNotEmpty ? _continuePressed : null, ) ], ), ), ); } }
Complete the Flow
The flow can be completed via context.flow<T>().complete
.
class AgeForm extends StatefulWidget { @override _AgeFormState createState() => _AgeFormState(); } class _AgeFormState extends State<AgeForm> { int? _age; void _continuePressed() { context .flow<Profile>() .complete((profile) => profile.copyWith(age: _age)); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('Age')), body: Center( child: Column( children: <Widget>[ TextField( onChanged: (value) => setState(() => _age = int.parse(value)), decoration: InputDecoration( labelText: 'Age', hintText: '42', ), keyboardType: TextInputType.number, ), RaisedButton( child: const Text('Continue'), onPressed: _age != null ? _continuePressed : null, ) ], ), ), ); } }
FlowController
A FlowBuilder
can also be created with a custom FlowController
in cases where the flow can be manipulated outside of the sub-tree.
class MyFlow extends StatefulWidget { @override State<MyFlow> createState() => _MyFlowState(); } class _MyFlowState extends State<MyFlow> { late FlowController<Profile> _controller; @override void initState() { super.initState(); _controller = FlowController(const Profile()); } @override Widget build(BuildContext context) { return FlowBuilder( controller: _controller, onGeneratePages: ..., ); } @override dispose() { _controller.dispose(); super.dispose(); } }
Example
- Create a flutter app, using flutter command: flutter create app SMS. Here SMS being the name of the app.
- In the lib folder is where we store all our .dart codes.
- Create files main.dart.
- Create authentication_flow, location_flow, onboarding_flow, profile_flow folders in lib itself.
- Create files inside the above folders as given below:
- main.dart
import 'package:example/authentication_flow/authentication_flow.dart';
import 'package:example/location_flow/location_flow.dart';
import 'package:example/onboarding_flow/onboarding_flow.dart';
import 'package:example/profile_flow/profile_flow.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
void main() => runApp(MyApp(locationRepository: LocationRepository()));
class MyApp extends StatelessWidget {
MyApp({Key? key, required LocationRepository locationRepository})
: _locationRepository = locationRepository,
super(key: key);
final LocationRepository _locationRepository;
@override
Widget build(BuildContext context) {
return RepositoryProvider.value(
value: _locationRepository,
child: MaterialApp(home: Home()),
);
}
}
class Home extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Home')),
body: Builder(
builder: (context) {
return ListView(
children: [
ListTile(
leading: const Icon(Icons.help_outline),
title: const Text('Onboarding Flow'),
trailing: const Icon(Icons.chevron_right),
onTap: () async {
await Navigator.of(context).push(OnboardingFlow.route());
ScaffoldMessenger.of(context)
..hideCurrentSnackBar()
..showSnackBar(
const SnackBar(
content: Text('Onboarding Flow Complete!'),
),
);
},
),
ListTile(
leading: const Icon(Icons.person_outline),
title: const Text('Profile Flow'),
trailing: const Icon(Icons.chevron_right),
onTap: () async {
final profile = await Navigator.of(context).push(
ProfileFlow.route(),
);
ScaffoldMessenger.of(context)
..hideCurrentSnackBar()
..showSnackBar(
SnackBar(
content: Text('Profile Flow Complete!\n$profile'),
),
);
},
),
ListTile(
leading: const Icon(Icons.location_city),
title: const Text('Location Flow'),
trailing: const Icon(Icons.chevron_right),
onTap: () async {
final location = await Navigator.of(context).push(
LocationFlow.route(),
);
ScaffoldMessenger.of(context)
..hideCurrentSnackBar()
..showSnackBar(
SnackBar(
content: Text('Location Flow Complete!\n$location'),
),
);
},
),
ListTile(
leading: const Icon(Icons.security_rounded),
title: const Text('Authentication Flow'),
trailing: const Icon(Icons.chevron_right),
onTap: () async {
await Navigator.of(context).push<AuthenticationState>(
AuthenticationFlow.route(),
);
ScaffoldMessenger.of(context)
..hideCurrentSnackBar()
..showSnackBar(
const SnackBar(
content: Text('Authentication Flow Complete!'),
),
);
},
),
],
);
},
),
);
}
}
- authentication.dart
import 'package:flow_builder/flow_builder.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
enum AuthenticationState { authenticated, unauthenticated }
class AuthenticationCubit extends Cubit<AuthenticationState> {
AuthenticationCubit() : super(AuthenticationState.unauthenticated);
void login() {
emit(AuthenticationState.authenticated);
}
void logout() {
emit(AuthenticationState.unauthenticated);
}
}
class AuthenticationFlow extends StatelessWidget {
static Route<AuthenticationState> route() {
return MaterialPageRoute(
builder: (_) => BlocProvider(
create: (_) => AuthenticationCubit(),
child: AuthenticationFlow(),
),
);
}
@override
Widget build(BuildContext context) {
return FlowBuilder<AuthenticationState>(
state: context.select((AuthenticationCubit cubit) => cubit.state),
onGeneratePages: (AuthenticationState state, List<Page> pages) {
switch (state) {
case AuthenticationState.authenticated:
return [HomePage.page()];
case AuthenticationState.unauthenticated:
default:
return [SplashPage.page()];
}
},
);
}
}
class SplashPage extends StatelessWidget {
static Page page() => MaterialPage<void>(child: SplashPage());
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
leading: BackButton(
onPressed: () {
context.flow<AuthenticationState>().complete();
},
),
),
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ElevatedButton(
child: const Text('Onboarding'),
onPressed: () {
Navigator.of(context).push(OnboardingPage.route());
},
),
ElevatedButton(
child: const Text('Sign In'),
onPressed: () {
context.read<AuthenticationCubit>().login();
},
),
],
),
),
);
}
}
class OnboardingPage extends StatefulWidget {
static Route<void> route() {
return MaterialPageRoute(builder: (_) => OnboardingPage());
}
@override
_OnboardingPageState createState() => _OnboardingPageState();
}
class _OnboardingPageState extends State<OnboardingPage> {
late FlowController<int> _controller;
@override
void initState() {
super.initState();
_controller = FlowController(0);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
leading: IconButton(
icon: const Icon(Icons.close),
onPressed: () => _controller.complete(),
),
),
body: FlowBuilder<int>(
controller: _controller,
onGeneratePages: (int state, List<Page> pages) {
return [
for (var i = 0; i <= state; i++) OnboardingStep.page(i),
];
},
),
);
}
}
class OnboardingStep extends StatelessWidget {
const OnboardingStep({Key? key, required this.step}) : super(key: key);
static Page page(int step) {
return MaterialPage<void>(child: OnboardingStep(step: step));
}
final int step;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text('Step $step', style: theme.textTheme.headline1),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
TextButton(
child: const Text('Previous'),
onPressed: context.flow<int>().state <= 0
? null
: () => context.flow<int>().update((s) => s - 1),
),
TextButton(
child: const Text('Next'),
onPressed: () {
context.flow<int>().update((s) => s + 1);
},
)
],
),
],
),
);
}
}
class HomePage extends StatelessWidget {
static Page page() => MaterialPage<void>(child: HomePage());
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
actions: [
IconButton(
icon: const Icon(Icons.exit_to_app),
onPressed: () {
context.read<AuthenticationCubit>().logout();
},
)
],
),
body: const Center(child: Text('Home')),
);
}
}
Source Code: flutter_builder_example.
GitHub
Source Code: Awesome Flutter package which simplifies navigation flows with a flexible, declarative API, flutter builder.