Flutter: Events Approach

In this article, I’m going to explain how to create a flutter app with events. In the events approach, the best thing is that we can trigger many events as required from the UI and the event trigger will do the work in the background. It’s like triggering a set of background processes.

Let’s build a flutter app from the scratch. I’m using VS Code but feel free to use any editor as you wish. Also, I have the current latest build of flutter which is flutter 2.8.1 and dart 2.14.4. You might feel slight differences if you’re having different versions. Please feel free to comment on them as well here. 

I have added the project to GitHub if you need to refer to it. Use the link https://github.com/rashintha/blog_flutter_event_approach

So, create a new flutter application to begin. (In the second step, choose Application)

Creating new flutter project

Selecting flutter application

Dependencies

You will require the following dependencies for this method. Please add the latest versions of them in your pubspec.yaml. I have put the versions that I have used in creating this app.

  • flutter_bloc: ^8.0.1
  • equatable: ^2.0.3

How does the flutter events app work?

In a flutter event application, there’re three major components. 

  1. Event (which is to be triggered by the UI)
  2. Listener (in this case the bloc)
  3. State

Flutter events approach design

When the user interacts with the flutter UI, the UI will trigger events that we have created. When an event is triggered, a listener is available to catch the event trigger and do the work in the background. That listener and all the processing are done in the bloc file. Once the processing is done, the bloc is updating the state of the application which will make an automatic regeneration of the UI.

In this way, the programmer is able to create a structured flutter application and a UI which will not be stuck when handling a process.

Folder Structure

I’m using a folder structure like follows, use something you feel comfortable with as your structure.

Folder structure

Let’s Code

We first need to create a screen hander that is going to handle all the views of the application.

Create a file named screen_handler.dart in the views folder and add the following code.

import 'package:flutter/cupertino.dart';

class ScreenHandler extends StatefulWidget {
  const ScreenHandler({Key? key}) : super(key: key);

  @override
  _ScreenHandlerState createState() => _ScreenHandlerState();
}

class _ScreenHandlerState extends State {
  @override
  Widget build(BuildContext context) {
    return const Text("Screen Handler");
  }
}

Then let’s create a login screen for the app views/login/login_screen.dart and add the following code.

import 'package:flutter/material.dart';

class LoginScreen extends StatefulWidget {
  const LoginScreen({Key? key}) : super(key: key);

  @override
  _LoginScreenState createState() => _LoginScreenState();
}

class _LoginScreenState extends State {
  @override
  Widget build(BuildContext context) {
    return const Scaffold(
      body: Text("Login"),
    );
  }
}

Now we need an event, bloc, and state to be created for the screen handler so we can display the login screen at the beginning.

Create an event file in the path blocs/screen/screen_event.dart and create events as follows.

import 'package:equatable/equatable.dart';

abstract class ScreenHandlerEvent extends Equatable {
  const ScreenHandlerEvent([List props = const []]);
}

class ShowLoginScreen extends ScreenHandlerEvent {
  const ShowLoginScreen();

  @override
  List<Object> get props => throw UnimplementedError();

  @override
  String toString() => 'Show Login Screen';
}

Then add an enum file to the utils folder and add the following enums.

enum Screens { none, login, home }

Then create the state file in the path blocs/screen/screen_state.dart and add the following code.

import 'package:flutter_event_approach/utils/barrell.dart';

class ScreenHandlerState {
  final Screens screen;
  final Screens? previousScreen;

  ScreenHandlerState({required this.screen, this.previousScreen});

  factory ScreenHandlerState.loginScreen() {
    return ScreenHandlerState(screen: Screens.login);
  }

  ScreenHandlerState copyWith({Screens? screen}) {
    return ScreenHandlerState(screen: screen ?? this.screen);
  }

  @override
  String toString() {
    return '''ScreenHandlerState {
      screen: $screen,
    }''';
  }
}

In the state, the factory is the initial state of the state.

And the last one is the bloc file. Create it in the path blocs/screen/screen_bloc.dart and add the following code.

import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_event_approach/blocs/screen/barrell.dart';
import 'package:flutter_event_approach/utils/barrell.dart';

class ScreenHandlerBloc extends Bloc<ScreenHandlerEvent, ScreenHandlerState> {
  ScreenHandlerBloc() : super(ScreenHandlerState.loginScreen()) {
    on(
        (event, emit) async => {emit(state.copyWith(screen: Screens.login))});
  }
}

Now change the main.dart file as follows and provide the created bloc for all the screens.

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_event_approach/blocs/screen/barrell.dart';
import 'package:flutter_event_approach/views/screen_handler.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
        title: 'Flutter Demo',
        debugShowCheckedModeBanner: false,
        theme: ThemeData(
          primarySwatch: Colors.blue,
        ),
        home: MultiBlocProvider(providers: [
          BlocProvider(
              create: (BuildContext context) => ScreenHandlerBloc())
        ], child: const ScreenHandler()));
  }
}

Then change the screen_handler.dart as follows to add handles for primary screens.

import 'package:flutter/cupertino.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_event_approach/blocs/screen/barrell.dart';
import 'package:flutter_event_approach/utils/barrell.dart';
import 'package:flutter_event_approach/views/login/login_screen.dart';

class ScreenHandler extends StatefulWidget {
  const ScreenHandler({Key? key}) : super(key: key);

  @override
  _ScreenHandlerState createState() => _ScreenHandlerState();
}

class _ScreenHandlerState extends State {
  late ScreenHandlerBloc _screenHandlerBloc;

  @override
  void initState() {
    super.initState();

    _screenHandlerBloc = BlocProvider.of(context);
  }

  @override
  Widget build(BuildContext context) {
    return BlocBuilder(
        bloc: _screenHandlerBloc,
        builder: (BuildContext context, ScreenHandlerState screenHandlerState) {
          return Stack(children: [
            AnimatedSwitcher(
              duration: const Duration(milliseconds: 500),
              child: _primaryScreen(screenHandlerState),
            )
          ]);
        });
  }

  Widget _primaryScreen(ScreenHandlerState screenHandlerState) {
    if (screenHandlerState.screen == Screens.login) {
      return const LoginScreen();
    }

    return const Text("No Screen");
  }
}

If you run the application, now you’ll be able to see your login screen now.

To get a good idea of the code, let’s create a login validation and a home screen so we can trigger a login event when the user presses the login button and if the details are verified, change the screen to the home screen.

Let’s first update the login_screen.dart as follows.

import 'package:flutter/material.dart';

class LoginScreen extends StatefulWidget {
  const LoginScreen({Key? key}) : super(key: key);

  @override
  _LoginScreenState createState() => _LoginScreenState();
}

class _LoginScreenState extends State {
  late TextEditingController usernameController;
  late TextEditingController passwordController;

  @override
  void initState() {
    super.initState();

    usernameController = TextEditingController();
    passwordController = TextEditingController();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        body: Container(
            padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
            child: Center(
                child: Stack(
              children: [
                Container(
                    padding: const EdgeInsets.symmetric(horizontal: 24),
                    child: Center(
                        child: SingleChildScrollView(
                      scrollDirection: Axis.vertical,
                      child: Column(
                        mainAxisAlignment: MainAxisAlignment.center,
                        children: [
                          Container(
                            margin: const EdgeInsets.only(bottom: 32),
                            child: const Align(
                              child: Text(
                                "Login",
                                style: TextStyle(
                                    fontWeight: FontWeight.bold, fontSize: 24),
                              ),
                              alignment: Alignment.topCenter,
                            ),
                          ),
                          TextFormField(
                            controller: usernameController,
                            textAlign: TextAlign.left,
                            decoration: const InputDecoration(
                                hintText: "User Name",
                                hintStyle: TextStyle(
                                    color: Colors.black38, fontSize: 14),
                                border: InputBorder.none),
                          ),
                          TextFormField(
                            controller: passwordController,
                            textAlign: TextAlign.left,
                            obscureText: true,
                            decoration: const InputDecoration(
                                hintText: "Password",
                                hintStyle: TextStyle(
                                    color: Colors.black38, fontSize: 14),
                                border: InputBorder.none),
                          ),
                          GestureDetector(
                              child: const Text(
                                "LOGIN",
                                style: TextStyle(fontWeight: FontWeight.bold),
                              ),
                              onTap: () => {})
                        ],
                      ),
                    )))
              ],
            ))));
  }
}

Now let’s create a set of a new event, bloc, and state for authentication. Create an event file in the path blocs/auth/auth_event.dart and create a login event as follows.

import 'package:equatable/equatable.dart';

abstract class AuthEvent extends Equatable {
  const AuthEvent([List props = const []]);
}

class Login extends AuthEvent {
  const Login({required this.username, required this.password});

  final String username;
  final String password;

  @override
  List<Object> get props => throw UnimplementedError();

  @override
  String toString() => 'Login';
}

Then update the enums.dart with the following.

enum Screens { none, login, home }
enum UserStatus { loggedOut, invalidUser, loggedIn }

And create the state in the path blocs/auth/auth_state.dart and add the following code.

import 'package:flutter_event_approach/utils/barrell.dart';

class AuthState {
  final UserStatus status;

  AuthState({required this.status});

  factory AuthState.loadDefault() {
    return AuthState(status: UserStatus.loggedOut);
  }

  AuthState copyWith({UserStatus? status}) {
    return AuthState(status: status ?? this.status);
  }

  @override
  String toString() {
    return '''AuthState {
      status: $status
    }''';
  }
}

Now we need to create a new event to show the home screen in thee screen_event.dart as follows.

import 'package:equatable/equatable.dart';

abstract class ScreenHandlerEvent extends Equatable {
  const ScreenHandlerEvent([List props = const []]);
}

class ShowLoginScreen extends ScreenHandlerEvent {
  const ShowLoginScreen();

  @override
  List<Object> get props => throw UnimplementedError();

  @override
  String toString() => 'Show Login Screen';
}

class ShowHomeScreen extends ScreenHandlerEvent {
  const ShowHomeScreen();

  @override
  List<Object> get props => throw UnimplementedError();

  @override
  String toString() => 'Show Login Screen';
}

Then update the screen_bloc.dart with the event listener code as follows.

import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_event_approach/blocs/screen/barrell.dart';
import 'package:flutter_event_approach/utils/barrell.dart';

class ScreenHandlerBloc extends Bloc<ScreenHandlerEvent, ScreenHandlerState> {
  ScreenHandlerBloc() : super(ScreenHandlerState.loginScreen()) {
    on(
        (event, emit) async => {emit(state.copyWith(screen: Screens.login))});
    on(
        (event, emit) async => {emit(state.copyWith(screen: Screens.home))});
  }
}

And we need to create a view for the home screen in the path views/home/home_screen.dart as follows.

import 'package:flutter/material.dart';

class HomeScreen extends StatefulWidget {
  const HomeScreen({Key? key}) : super(key: key);

  @override
  _HomeScreenState createState() => _HomeScreenState();
}

class _HomeScreenState extends State {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
        body: Stack(
      children: const [
        Center(
          child: Text("Home Screen"),
        )
      ],
    ));
  }
}

Now let’s create the bloc file in the path blocs/auth/auth_bloc.dart and add the following code.

import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_event_approach/blocs/barrell.dart';
import 'package:flutter_event_approach/utils/barrell.dart';

class AuthBloc extends Bloc<AuthEvent, AuthState> {
  AuthBloc({required this.screenHandlerBloc}) : super(AuthState.loadDefault()) {
    on((event, emit) async =>
        {emit(await _mapLoginToState(event.username, event.password))});
  }

  final ScreenHandlerBloc screenHandlerBloc;

  Future _mapLoginToState(String username, String password) async {
    if (username == 'admin' && password == '123') {
      screenHandlerBloc.add(const ShowHomeScreen());
      return state.copyWith(status: UserStatus.loggedIn);
    }

    return state.copyWith(status: UserStatus.invalidUser);
  }
}

And update the main.dart with the newly created auth bloc as follows.

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_event_approach/blocs/barrell.dart';
import 'package:flutter_event_approach/views/screen_handler.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
        title: 'Flutter Demo',
        debugShowCheckedModeBanner: false,
        theme: ThemeData(
          primarySwatch: Colors.blue,
        ),
        home: MultiBlocProvider(providers: [
          BlocProvider(
              create: (BuildContext context) => ScreenHandlerBloc()),
          BlocProvider(
              create: (BuildContext context) => AuthBloc(
                  screenHandlerBloc:
                      BlocProvider.of(context)))
        ], child: const ScreenHandler()));
  }
}

The update the login_screen.dart with the auth event trigger as follows.

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_event_approach/blocs/barrell.dart';
import 'package:flutter_event_approach/utils/barrell.dart';

class LoginScreen extends StatefulWidget {
  const LoginScreen({Key? key}) : super(key: key);

  @override
  _LoginScreenState createState() => _LoginScreenState();
}

class _LoginScreenState extends State {
  late TextEditingController usernameController;
  late TextEditingController passwordController;
  late AuthBloc _authBloc;

  @override
  void initState() {
    super.initState();

    _authBloc = BlocProvider.of(context);

    usernameController = TextEditingController();
    passwordController = TextEditingController();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        body: BlocBuilder(
            bloc: _authBloc,
            builder: (BuildContext context, AuthState authState) {
              return Container(
                  padding:
                      const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
                  child: Center(
                      child: Stack(
                    children: [
                      Container(
                          padding: const EdgeInsets.symmetric(horizontal: 24),
                          child: Center(
                              child: SingleChildScrollView(
                            scrollDirection: Axis.vertical,
                            child: Column(
                              mainAxisAlignment: MainAxisAlignment.center,
                              children: [
                                Container(
                                  margin: const EdgeInsets.only(bottom: 32),
                                  child: const Align(
                                    child: Text(
                                      "Login",
                                      style: TextStyle(
                                          fontWeight: FontWeight.bold,
                                          fontSize: 24),
                                    ),
                                    alignment: Alignment.topCenter,
                                  ),
                                ),
                                TextFormField(
                                  controller: usernameController,
                                  textAlign: TextAlign.left,
                                  decoration: const InputDecoration(
                                      hintText: "User Name",
                                      hintStyle: TextStyle(
                                          color: Colors.black38, fontSize: 14),
                                      border: InputBorder.none),
                                ),
                                TextFormField(
                                  controller: passwordController,
                                  textAlign: TextAlign.left,
                                  obscureText: true,
                                  decoration: const InputDecoration(
                                      hintText: "Password",
                                      hintStyle: TextStyle(
                                          color: Colors.black38, fontSize: 14),
                                      border: InputBorder.none),
                                ),
                                GestureDetector(
                                    child: const Text(
                                      "LOGIN",
                                      style: TextStyle(
                                          fontWeight: FontWeight.bold),
                                    ),
                                    onTap: () => {
                                          _authBloc.add(Login(
                                              username: usernameController.text,
                                              password:
                                                  passwordController.text))
                                        }),
                                authState.status == UserStatus.invalidUser
                                    ? Container(
                                        padding: const EdgeInsets.only(top: 8),
                                        child: const Text(
                                          "Invalid User",
                                          style: TextStyle(color: Colors.red),
                                        ),
                                      )
                                    : Container()
                              ],
                            ),
                          )))
                    ],
                  )));
            }));
  }
}

Now we have to update the screen_handler.dart with the home screen details as follows.

import 'package:flutter/cupertino.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_event_approach/blocs/screen/barrell.dart';
import 'package:flutter_event_approach/utils/barrell.dart';
import 'package:flutter_event_approach/views/home/home_screen.dart';
import 'package:flutter_event_approach/views/login/login_screen.dart';

class ScreenHandler extends StatefulWidget {
  const ScreenHandler({Key? key}) : super(key: key);

  @override
  _ScreenHandlerState createState() => _ScreenHandlerState();
}

class _ScreenHandlerState extends State {
  late ScreenHandlerBloc _screenHandlerBloc;

  @override
  void initState() {
    super.initState();

    _screenHandlerBloc = BlocProvider.of(context);
  }

  @override
  Widget build(BuildContext context) {
    return BlocBuilder(
        bloc: _screenHandlerBloc,
        builder: (BuildContext context, ScreenHandlerState screenHandlerState) {
          return Stack(children: [
            AnimatedSwitcher(
              duration: const Duration(milliseconds: 500),
              child: _primaryScreen(screenHandlerState),
            )
          ]);
        });
  }

  Widget _primaryScreen(ScreenHandlerState screenHandlerState) {
    if (screenHandlerState.screen == Screens.login) {
      return const LoginScreen();
    } else if (screenHandlerState.screen == Screens.home) {
      return const HomeScreen();
    }

    return const Text("No Screen");
  }
}

Now run the program and type user name and password to login and you will see your home screen.

So that’s it

If you have any suggestions or questions about this guide, comment here or send me an email.

 

2 thoughts on “Flutter: Events Approach

Leave a Reply

Your email address will not be published. Required fields are marked *

Back to top