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.

Flow Builder

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

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.

SHARE Awesome Flutter package which simplifies navigation flows with a flexible, declarative API, flutter_builder

You may also like...

Leave a Reply

Your email address will not be published.

Share