Xamarin Forms Tutorial | Build Entirely Native Mobile Apps for Android and iOS using Xamarin Forms

Angular 8 Forms Tutorial - Reactive Forms Validation Example

Angular 8 Forms Tutorial - Reactive Forms Validation Example

The example is a simple registration form with pretty standard fields for title, first name, last name, email, password, confirm password and an accept Ts & Cs checkbox. All fields are required including the checkbox, the email field must be a valid email address and the password field must have a min length of 6. There's also a custom validator called MustMatch which is used to validate that the confirm password and password fields match.

I've setup the form to validate on submit rather than as soon as each field is changed, this is implemented with a submitted property in the app component that is set to true when the form is submitted for the first time, and reset to false if the cancel button is clicked.

Styling of the example is all done with Bootstrap 4.3 CSS.

See on StackBlitz at https://stackblitz.com/edit/angular-8-reactive-form-validation

Reactive Forms Validation App Component

The app component defines the form fields and validators for our registration form using an Angular FormBuilder to create an instance of a FormGroup that is stored in the registerForm property. The registerForm is then bound to the form in the app template below using the [formGroup] directive.

I also added a getter f as a convenience property to make it easier to access form controls from the template. So for example you can access the confirmPassword field in the template using f.confirmPassword instead of registerForm.controls.confirmPassword.

import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';

// import custom validator to validate that password and confirm password fields match
import { MustMatch } from './_helpers/must-match.validator';

@Component({ selector: 'app', templateUrl: 'app.component.html' })
export class AppComponent implements OnInit {
    registerForm: FormGroup;
    submitted = false;

    constructor(private formBuilder: FormBuilder) { }

    ngOnInit() {
        this.registerForm = this.formBuilder.group({
            title: ['', Validators.required],
            firstName: ['', Validators.required],
            lastName: ['', Validators.required],
            email: ['', [Validators.required, Validators.email]],
            password: ['', [Validators.required, Validators.minLength(6)]],
            confirmPassword: ['', Validators.required],
            acceptTerms: [false, Validators.requiredTrue]
        }, {
            validator: MustMatch('password', 'confirmPassword')
        });
    }

    // convenience getter for easy access to form fields
    get f() { return this.registerForm.controls; }

    onSubmit() {
        this.submitted = true;

        // stop here if form is invalid
        if (this.registerForm.invalid) {
            return;
        }

        // display form values on success
        alert('SUCCESS!! :-)\n\n' + JSON.stringify(this.registerForm.value, null, 4));
    }

    onReset() {
        this.submitted = false;
        this.registerForm.reset();
    }
}

Reactive Forms Validation App Template

The app component template contains all the html markup for displaying the example registration form in your browser. The form element uses the [formGroup] directive to bind to the registerForm FormGroup in the app component above.

The form binds the form submit event to the onSubmit() handler in the app component using the Angular event binding (ngSubmit)="onSubmit()". Validation messages are displayed only after the user attempts to submit the form for the first time, this is controlled with the submitted property of the app component.

The cancel button click event is bound to the onReset() handler in the app component using the Angular event binding (click)="onReset()".

<!-- main app container -->
<div class="card m-3">
    <h5 class="card-header">Angular 8 Reactive Form Validation</h5>
    <div class="card-body">
        <form [formGroup]="registerForm" (ngSubmit)="onSubmit()">
            <div class="form-row">
                <div class="form-group col">
                    <label>Title</label>
                    <select formControlName="title" class="form-control" [ngClass]="{ 'is-invalid': submitted && f.title.errors }">
                        <option value=""></option>
                        <option value="Mr">Mr</option>
                        <option value="Mrs">Mrs</option>
                        <option value="Miss">Miss</option>
                        <option value="Ms">Ms</option>
                    </select>
                    <div *ngIf="submitted && f.title.errors" class="invalid-feedback">
                        <div *ngIf="f.title.errors.required">Title is required</div>
                    </div>
                </div>
                <div class="form-group col-5">
                    <label>First Name</label>
                    <input type="text" formControlName="firstName" class="form-control" [ngClass]="{ 'is-invalid': submitted && f.firstName.errors }" />
                    <div *ngIf="submitted && f.firstName.errors" class="invalid-feedback">
                        <div *ngIf="f.firstName.errors.required">First Name is required</div>
                    </div>
                </div>
                <div class="form-group col-5">
                    <label>Last Name</label>
                    <input type="text" formControlName="lastName" class="form-control" [ngClass]="{ 'is-invalid': submitted && f.lastName.errors }" />
                    <div *ngIf="submitted && f.lastName.errors" class="invalid-feedback">
                        <div *ngIf="f.lastName.errors.required">Last Name is required</div>
                    </div>
                </div>
            </div>
            <div class="form-group">
                <label>Email</label>
                <input type="text" formControlName="email" class="form-control" [ngClass]="{ 'is-invalid': submitted && f.email.errors }" />
                <div *ngIf="submitted && f.email.errors" class="invalid-feedback">
                    <div *ngIf="f.email.errors.required">Email is required</div>
                    <div *ngIf="f.email.errors.email">Email must be a valid email address</div>
                </div>
            </div>
            <div class="form-row">
                <div class="form-group col">
                    <label>Password</label>
                    <input type="password" formControlName="password" class="form-control" [ngClass]="{ 'is-invalid': submitted && f.password.errors }" />
                    <div *ngIf="submitted && f.password.errors" class="invalid-feedback">
                        <div *ngIf="f.password.errors.required">Password is required</div>
                        <div *ngIf="f.password.errors.minlength">Password must be at least 6 characters</div>
                    </div>
                </div>
                <div class="form-group col">
                    <label>Confirm Password</label>
                    <input type="password" formControlName="confirmPassword" class="form-control" [ngClass]="{ 'is-invalid': submitted && f.confirmPassword.errors }" />
                    <div *ngIf="submitted && f.confirmPassword.errors" class="invalid-feedback">
                        <div *ngIf="f.confirmPassword.errors.required">Confirm Password is required</div>
                        <div *ngIf="f.confirmPassword.errors.mustMatch">Passwords must match</div>
                    </div>
                </div>
            </div>
            <div class="form-group form-check">
                <input type="checkbox" formControlName="acceptTerms" id="acceptTerms" class="form-check-input" [ngClass]="{ 'is-invalid': submitted && f.acceptTerms.errors }" />
                <label for="acceptTerms" class="form-check-label">Accept Terms & Conditions</label>
                <div *ngIf="submitted && f.acceptTerms.errors" class="invalid-feedback">Accept Ts & Cs is required</div>
            </div>
            <div class="text-center">
                <button class="btn btn-primary mr-1">Register</button>
                <button class="btn btn-secondary" type="reset" (click)="onReset()">Cancel</button>
            </div>
        </form>
    </div>
</div>

Reactive Forms Custom "Must Match" Validator

The custom MustMatch validator is used in this example to validate that both of the password fields - password and confirmPassword - are matching. However it can be used to validate that any pair of fields is matching (e.g. email and confirm email fields).

It works slightly differently than a typical custom validator because I'm setting the error on the second field instead of returning it to be set on the formGroup. I did it this way because I think it makes the template a bit cleaner and more intuitive, the mustMatch validation error is displayed below the confirmPassword field so I think it makes sense that the error is attached the the confirmPassword form control.

import { FormGroup } from '@angular/forms';

// custom validator to check that two fields match
export function MustMatch(controlName: string, matchingControlName: string) {
    return (formGroup: FormGroup) => {
        const control = formGroup.controls[controlName];
        const matchingControl = formGroup.controls[matchingControlName];

        if (matchingControl.errors && !matchingControl.errors.mustMatch) {
            // return if another validator has already found an error on the matchingControl
            return;
        }

        // set error on matchingControl if validation fails
        if (control.value !== matchingControl.value) {
            matchingControl.setErrors({ mustMatch: true });
        } else {
            matchingControl.setErrors(null);
        }
    }
}

Reactive Forms Validation App Module

There isn't much going on in the app module other than the standard stuff, the main thing you need to remember for using reactive forms in Angular is to import the ReactiveFormsModule from '@angular/forms' and include it in the imports array of the @NgModule decorator.

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { ReactiveFormsModule } from '@angular/forms';

import { AppComponent } from './app.component';

@NgModule({
    imports: [
        BrowserModule,
        ReactiveFormsModule
    ],
    declarations: [
        AppComponent
    ],
    bootstrap: [AppComponent]
})
export class AppModule { }

Thanks for reading

If you liked this post, share it with all of your programming buddies!

Follow us on Facebook | Twitter

Further reading

Angular 8 (formerly Angular 2) - The Complete Guide

Angular & NodeJS - The MEAN Stack Guide

The Web Developer Bootcamp

Angular 8 is coming

MEAN Stack Angular 8 CRUD Web Application

Angular 8 + Spring Boot 2.2: Build a CRUD App Today!

Best 17 Angular Libraries Every Angular Developers Should Know in 2019

React vs Angular vs Vue.js by Example

Best JavaScript Frameworks, Libraries and Tools to Use in 2019

Angular Authentication with JWT 

Flutter: Adding Bluetooth Functionality

Flutter: Adding Bluetooth Functionality

Introduction:


There is little documentation to no documentation on using Bluetooth in Flutter. In this article, I will help you by demonstrating some basic concepts needed to implement Bluetooth functionality in your app.

Firstly, plugin/dependency we will be using in this app to add Bluetooth is “flutter_bluetooth_serial”, this plugin is implemented from another parent plugin called “flutter_blue”. This is a very new plugin, the only plugin for bluetooth available as of now. It contains a few bugs but trust me, this will surely get your job done for most basic projects.

Note: Before we go any further, it is worth mentioning that this plugin will only work for Android

Implementation:

Add this dependency in your “pubspec.yaml” file :

<pre class="ql-syntax" spellcheck="false">dependencies: flutter_bluetooth_serial: ^0.0.4 </pre>

In the “main.dart” file the base code of the app will look like this:

<pre class="ql-syntax" spellcheck="false">import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: BluetoothApp(), // BluetoothApp() would be defined later
);
}
}
</pre>

Now, let’s create a StatefulWidget called “BluetoothApp”. In _BluetoothAppState, we need to define some variables and a Key. We also have to get an instance of FlutterBluetoothSerial in this class. This class will allow us to control and retrieve Bluetooth information.

<pre class="ql-syntax" spellcheck="false">class BluetoothApp extends StatefulWidget {
@override
_BluetoothAppState createState() => _BluetoothAppState();
}

class _BluetoothAppState extends State<BluetoothApp> {
// Initializing a global key, as it would help us in showing a SnackBar later
final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>();
// Get the instance of the bluetooth
FlutterBluetoothSerial bluetooth = FlutterBluetoothSerial.instance;

// Define some variables, which will be required later
List<BluetoothDevice> _devicesList = [];
BluetoothDevice _device;
bool _connected = false;
bool _pressed = false;

@override
Widget build(BuildContext context) {
return Container(
// We have to work on the UI in this part
);
}
}
</pre>

Now, it’s time for implementing the critical portion of the app. We have to get the list of Paired Bluetooth devices and check whether the Bluetooth is connected. This is done asynchronously. We also have to create a list of devices, to be shown in the UI later.

These operations should be done in a “Future” method, which should be called from initState().

<pre class="ql-syntax" spellcheck="false">class _BluetoothAppState extends State<BluetoothApp> {
...

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

// We are using async callback for using await
Future<void> bluetoothConnectionState() async {
List<BluetoothDevice> devices = [];

// To get the list of paired devices
try {
  devices = await bluetooth.getBondedDevices();
} on PlatformException {
  print("Error");
}

// For knowing when bluetooth is connected and when disconnected
bluetooth.onStateChanged().listen((state) {
  switch (state) {
    case FlutterBluetoothSerial.CONNECTED:
      setState(() {
        _connected = true;
        _pressed = false;
      });

      break;

    case FlutterBluetoothSerial.DISCONNECTED:
      setState(() {
        _connected = false;
        _pressed = false;
      });
      break;

    default:
      print(state);
      break;
  }
});

// It is an error to call [setState] unless [mounted] is true.
if (!mounted) {
  return;
}

// Store the [devices] list in the [_devicesList] for accessing
// the list outside this class
setState(() {
  _devicesList = devices;
});

}

@override
Widget build(BuildContext context) {
return Container(
// We have to work on the UI in this part
);
}
}
</pre>

Time to move on to the UI , the most beautiful part of Flutter coding. The code would be a little bit long but it would mostly contain easily readable code, if you are somewhat familiar with the Flutter Widgets. After completing this UI, we have to implement some methods.

<pre class="ql-syntax" spellcheck="false">...
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
key: _scaffoldKey,
appBar: AppBar(
title: Text("Flutter Bluetooth"),
backgroundColor: Colors.deepPurple,
),
body: Container(
// Defining a Column containing FOUR main Widgets wrapped with some padding:
// 1. Text
// 2. Row
// 3. Card
// 4. Text (wrapped with "Expanded" and "Padding")
child: Column(
mainAxisSize: MainAxisSize.max,
children: <Widget>[
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Text(
"PAIRED DEVICES",
style: TextStyle(fontSize: 24, color: Colors.blue),
textAlign: TextAlign.center,
),
),
Padding(
padding: const EdgeInsets.all(8.0),
// Defining a Row containing THREE main Widgets:
// 1. Text
// 2. DropdownButton
// 3. RaisedButton
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Text(
'Device:',
style: TextStyle(
fontWeight: FontWeight.bold,
),
),
DropdownButton(
// To be implemented : _getDeviceItems()
items: _getDeviceItems(),
onChanged: (value) => setState(() => _device = value),
value: _device,
),
RaisedButton(
onPressed:
// To be implemented : _disconnect and _connect
_pressed ? null : _connected ? _disconnect : _connect,
child: Text(_connected ? 'Disconnect' : 'Connect'),
),
],
),
),
Padding(
padding: const EdgeInsets.all(16.0),
child: Card(
elevation: 4,
child: Padding(
padding: const EdgeInsets.all(8.0),
// Defining a Row containing THREE main Widgets:
// 1. Text (wrapped with "Expanded")
// 2. FlatButton
// 3. FlatButton
child: Row(
children: <Widget>[
Expanded(
child: Text(
"DEVICE 1",
style: TextStyle(
fontSize: 20,
color: Colors.green,
),
),
),
FlatButton(
onPressed:
// To be implemented : _sendOnMessageToBluetooth()
_connected ? _sendOnMessageToBluetooth : null,
child: Text("ON"),
),
FlatButton(
onPressed:
// To be implemented : _sendOffMessageToBluetooth()
_connected ? _sendOffMessageToBluetooth : null,
child: Text("OFF"),
),
],
),
),
),
),
Expanded(
child: Padding(
padding: const EdgeInsets.all(20),
child: Center(
child: Text(
"NOTE: If you cannot find the device in the list, "
"please turn on bluetooth and pair the device by "
"going to the bluetooth settings",
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.bold,
color: Colors.red),
),
),
),
)
],
),
),
),
);
}
</pre>

So, now it’s time for implementing the remaining methods. At first let us start with the _getDeviceItems() method.

<pre class="ql-syntax" spellcheck="false"> ...
// Create the List of devices to be shown in Dropdown Menu
List<DropdownMenuItem<BluetoothDevice>> _getDeviceItems() {
List<DropdownMenuItem<BluetoothDevice>> items = [];
if (_devicesList.isEmpty) {
items.add(DropdownMenuItem(
child: Text('NONE'),
));
} else {
_devicesList.forEach((device) {
items.add(DropdownMenuItem(
child: Text(device.name),
value: device,
));
});
}
return items;
}
</pre>

With the UI out of the way, we are left with four methods. For this example, we will be implementing the connect and disconnect methods. We’ll also implement a method to display a “SnackBar” to the user if there are no Bluetooth device is selected when the user tries to connect.

<pre class="ql-syntax" spellcheck="false">...
// Method to connect to bluetooth
void _connect() {
if (_device == null) {
show('No device selected');
} else {
bluetooth.isConnected.then((isConnected) {
if (!isConnected) {
bluetooth
.connect(_device)
.timeout(Duration(seconds: 10))
.catchError((error) {
setState(() => _pressed = false);
});
setState(() => _pressed = true);
}
});
}
}

// Method to disconnect bluetooth
void _disconnect() {
bluetooth.disconnect();
setState(() => _pressed = true);
}

// Method to show a Snackbar,
// taking message as the text
Future show(
String message, {
Duration duration: const Duration(seconds: 3),
}) async {
await new Future.delayed(new Duration(milliseconds: 100));
_scaffoldKey.currentState.showSnackBar(
new SnackBar(
content: new Text(
message,
),
duration: duration,
),
);
}
...
</pre>

At this point, we are almost finished. We are now left with two methods, one for sending a message to turn on Bluetooth and the other for sending a message to turn off Bluetooth.

<pre class="ql-syntax" spellcheck="false"> ...
// Method to send message,
// for turning the bletooth device on
void _sendOnMessageToBluetooth() {
bluetooth.isConnected.then((isConnected) {
if (isConnected) {
bluetooth.write("1");
show('Device Turned On');
}
});
}

// Method to send message,
// for turning the bletooth device off
void _sendOffMessageToBluetooth() {
bluetooth.isConnected.then((isConnected) {
if (isConnected) {
bluetooth.write("0");
show('Device Turned Off');
}
});
}
...
</pre>

That’s it! the Dart code required to make this work is now complete. That said, if we try running our app it will crash:

To fix this, we need to add the sdk to the AndroidManifest. Navigate to your project folder and follow these steps: android -> app -> src -> main -> AndroidManifest.xml

Add these two lines of code in your “AndroidManifest.xml” file :

<pre class="ql-syntax" spellcheck="false"><manifest ...
<!-- Add this line (inside manifest tag) -->
xmlns:tools="http://schemas.android.com/tools">

&lt;!-- and this line (outside manifest tag) --&gt;
&lt;uses-sdk tools:overrideLibrary="io.github.edufolly.flutterbluetoothserial"/&gt;
....

</manifest>
</pre>

Conclusion:

As I said at the beginning of this article, this plugin contains some bugs and is still under development.

Below are some screenshots showing various phases. If the user doesn’t have permission, the first thing the user will see is a prompt to grant the app location access. This is completely normal, just click “Allow” and everything should be fine.


Screenshots:

You are free to modify the code to add more functionality to the app.

The GitHub repo link for this project is here

If you like this project, please give “Stars” in my GitHub repo. Thank you for reading, if you enjoyed the article make sure to show me some love by hitting that clap button!

Happy coding…


Learn More

Getting started with Flutter

Flutter Tutorial - Flight List UI Example In Flutter

Let’s Develop a Mobile App in Flutter

Mastering styled text in Flutter

A Design Pattern for Flutter

Weather App with “flutter_bloc”

How to integrate your iOS Flutter App with Firebase on MacOS

An introduction to Dart and Flutter

Learn Flutter & Dart to Build iOS & Android Apps

Flutter & Dart - The Complete Flutter App Development Course

Dart and Flutter: The Complete Developer’s Guide

Flutter - Advanced Course

Originally published by Souvik Biswas at https://medium.com/flutter-community

5 Best Mobile App Development Frameworks You Should Know in 2020

5 Best Mobile App Development Frameworks You Should Know in 2020

The mobile app development frameworks not only offer a cheaper option to build mobile apps, but they also deliver native-like user experience, which is what a business or a company with strict budget needs.

Although, it is critical to choose the right mobile app development framework, as the choice of framework can make or break the success of your mobile app.

What is a Mobile App Development Framework?

A mobile app development framework is a library that is designed to help developers in building mobile apps. It basically provides a fundamental structure that not only supports mobile application development, but it also boosts the process while reducing the costs at the same time.

In this article, I have listed the 5 best mobile app development frameworks that we highly recommend to all of our clients.

5 Best Mobile App Development Frameworks in Detail

Choosing the right mobile app development framework depends on a number of factors.

That’s being said, let’s understand the best 5 mobile app frameworks individually that make the mobile app development simple.

1 — React Native

React Native was launched by Facebook in 2013, and since then it has become the prime choice of mobile app developers.

React Native is a Javascript-based, open-source cross-platform app development framework that allows developers to build high-performance mobile apps with shorter development cycles and faster deployment times.

The best part about React Native is that it’s a JSX-based (JavaScript-XML) framework, which means developers need not learn any complex programming languages such as Swift or Java in order to build mobile apps.

The framework takes care of converting the React Native code into native views before rendering so that grants a native-like experience for its end-users.

Advantages of React-Native:

  • Optimal Performance
  • Reusability
  • Proficiency
  • Lots of third-party plug-ins
  • Modular architecture

2 — Flutter

Flutter was launched by Google and is basically an open-source software development kit (SDK) for developing mobile apps. Unlike React Native apps or Hybrid Apps, Flutter is capable of fully compiling to native code.

This compilation allows mobile app developers to give not native-like but truly-native feel. Google made this possible through its graphics engine called Skia, which is a popular 2D graphics engine built by Google.

Flutter uses Dart as its primary programming language and it can construct cross-platform mobile apps for both iOS and Android that can run on any device.

In a nutshell, Flutter is considered to be the best mobile application development framework amongst all frameworks available in the market.

However, you should utilize Flutter only for a certain type of mobile app projects. And this is why you must consult with a mobile app development company if you’re not completely confident about the framework you’re planning to use.

Advantages of Flutter:

  • Flexible UI
  • Light-weight components
  • Lighting fast speed
  • Native performance

3 — Ionic

Ionic is free, open-source, and also one of the most widely used mobile app framework for building mobile apps.

Ionic basically enables mobile app developers to build native-based apps for various platforms with the combination of HTML, CSS3, and JavaScript.

It uses minimal DOM to maximize performance and better efficiency and also provides native APIs like Ionic Native and Cordova.

Apart from this, the element of HTML5 in Ionic also allows developers to build hybrid apps.

Advantages of Ionic:

  • Fast Execution
  • Near-Native Performance
  • Cordova Plugins
  • Easy to adopt

4 — Xamarin

Microsoft bought Xamarin from its founder in 2016 and they made it open-source ever since.

Xamarin is basically a cross-platform app development framework based on C# programming language that takes a slightly different approach than the rest of the mobile app frameworks.

For instance, Xamarin allows code sharing, which ultimately saves a lot of developers’ time in building mobile apps for multiple platforms.

In addition, Xamarin also comes with an excellent native UI, which allows businesses to build truly-native apps to give the best user experience to their customers.

Advantages of Xamarin:

  • Native UI
  • Easy API Integration
  • Shared Code Base
  • Huge Community
  • Less complex development environment

5 — Adobe PhoneGap

Adobe PhoneGap is another popular mobile app framework that uses HTML5, CSS3, and JavaScript to build mobile apps.

One of the main reasons why it’s in the list of best mobile app development frameworks is its best-in-class performance, which enables mobile app developers to build apps without facing any hardware restrictions.

Apart from this, PhoneGap also supports using inbuilt hardware features such as Storage, Phonebook, Accelerometer, Camera, GPS, etc.

In fact, unlike most hybrid mobile app frameworks, PhoneGap can build mobile apps for almost all major operating systems including Blackberry, Windows, Ubuntu, Firefox OS, Mac OS, and of course, Android & iOS.

Advantages of Adobe PhoneGap:

  • Supports most major OS
  • Based on HTML5, CSS3, JavaScript
  • Uniformity across platform
  • Robust support
  • Flexible with web technologies

Conclusion

Mobile app development frameworks are basically driving tools to build mobile apps. And the above-mentioned 5 frameworks have proved to be the best mobile app development frameworks in the market.

10 Dart Tips That Every Beginner Should Learn

10 Dart Tips That Every Beginner Should Learn

Dart is a programming language that has been gaining traction in the last year or two, thanks to Flutter. Even though the language was released back in 2013, Google deciding to use Dart in Flutter has been a real game-changer for the language. In 2019 Dart made it to the Jetbrains survey of programming languages for the first time.

10 Dart Tips That Every Beginner Should Learn

Dart ranked 18 in the Jetbrains Survey

So if you’re new to the developer ecosystem, Dart is something that I would definitely suggest learning alongside JavaScript (wasn't it already obvious from the picture?) It’s sure to gain more popularity with the growth of flutter.

So let’s dive in and go through some of the most useful methods in Dart in three sections : lists, strings, and date-time.

Lists or Arrays

1. map()

Themap() method creates a new list that contains the results of running a callback on every element in the original list.

var cars = [ 'mercedes’,‘bmw’,‘audi’,'tesla'];
var mapCars = cars.map((car) => ‘My car is  $car’).toList();
print(mapCars);

The output of the above code would be:

[My car is mercedes, My car is bmw, My car is audi, My car is tesla]

2. sort()

sort() can be used to order the elements based on the provided sorting function. It works without a function too.

cars.sort();
print(cars);
or using method cascades
print(cars..sort());

The above snippets will print this:

[audi, bmw, mercedes, tesla]

We can always define our own comparison logic as well. Say we need to sort by comparing the price of each car, which will be obtained from a function getPrice(). We write our sort logic, as shown below, and it will sort the cars list based on their given price:

cars.sort((a, b) => getPrice(a).compareTo(getPrice(b)));

3. contains()

This is used to check if a given element is present in a list. contains()returns a boolean value.

print(cars.contains('bmw')); // output => true
print(cars.contains('toyota')); // output => false

4. reduce()

Thismethod also runs a callback for each element of an array. But reduce passes the result of each callback to the next iteration. This passed value is commonly known as an accumulator(value is accumulated through each iteration).

var numbers = [1, 3, 2, 5, 4];
var product = numbers.reduce((curr, next) => curr * next);
print(product);

The output of the above snippet will be 120.

Which is obtained from: 13 + 32 + 65 + 304.

Here accumulators after each iteration would be 3, 6, 30, and 120 respectively_._

String

5. startsWith() and endsWith()

As the name suggests, thestartsWith() method can be used to check whether a string starts with the given sequence of characters and theendsWith() method can be used to check if a string ends with the given sequence of characters. As you can see below, it’s case sensitive:

var name = ‘Medium is a great place to write articles’;
print(name.startsWith(‘Medium’)); // output => true
print(name.startsWith(‘medium’)); // output => false
print(name.endsWith(‘articles’)); // output => true
print(name.endsWith(‘medium’)); // output => false

6. padLeft() and padRight()

This method adds padding to the string using a given character or string, if the length of the string is less than the specified length:

var name = ‘I love Medium’;
print(name.padLeft(20, ‘xa’));
// output => xaxaxaxaxaxaxaI love Medium
print(name.padRight(20, ‘x’));
// output => I love Mediumxxxxxxx

7. splitMapJoin()

Basically, this does all three operations together. According to flutter.io it:

Splits the string, converts its parts, and combines them into a new string.

var newString = ‘I love medium I love medium’.splitMapJoin((new RegExp(r’medium’)),
 onMatch: (m) => ‘like ${m.group(0)}\n’,
 onNonMatch: (n) => ‘’);
 print(newString);

The output of the above snippet will be:

like medium
like medium

Date

8. isAfter() and isBefore()

Returns true if the given date occurs after the other.

var independenceDay = DateTime.utc(2019, 07, 4);
var halloween2019 = DateTime.utc(2019, 10, 31);
print(halloween2019.isAfter(independenceDay)); // output => true
print(halloween2019.isBefore(independenceDay)); // ouput => false

9. toLocal()

Converts DateTime value to the current time zone.

var date = DateTime.utc(2019, 10, 15);
print(DateTime.utc(2019, 10, 15)); 
// output => 2019–10–15 00:00:00.000Z
print(date.toLocal());
// output => 2019–10–15 05:30:00.000

10. weekday and month

These are properties and not methods. weekday can be used to check the day of a given date. It returns an integer 1–7 ( Monday to Friday).

var independenceDay = DateTime.utc(2019, 07, 4);
print(independenceDay.weekday); 
// output => 4
print(independenceDay.weekday == DateTime.sunday);  
// output => false

monthcan be used to check the month of a given date. It returns an integer 1–12 ( January to December).

var independenceDay = DateTime.utc(2019, 07, 4);
print(independenceDay.month == 5); 
// output => 7
print(independenceDay.month == DateTime.july);
// output => true

Thank you for reading!

Flutter Todos Tutorial with “flutter_bloc”

Flutter Todos Tutorial with “flutter_bloc”

In the following tutorial, we’re going to build a Todos App in Flutter using the Bloc Library. By the time we’re done, our app should look something like this:

Let’s get started!


Setup

We’ll start off by creating a brand new Flutter project

<pre class="ql-syntax" spellcheck="false">flutter create flutter_todos </pre>

We can then replace the contents of pubspec.yaml with:

<pre class="ql-syntax" spellcheck="false">name: flutter_todos description: A new Flutter project.

environment:
sdk: ">=2.0.0 <3.0.0"

dependencies:
meta: ">=1.1.0 <2.0.0"
equatable: ^0.2.0
flutter_bloc: ^0.7.0
flutter:
sdk: flutter

dependency_overrides:
todos_app_core:
git:
url: git://github.com/brianegan/flutter_architecture_samples
path: todos_app_core
todos_repository_core:
git:
url: git://github.com/brianegan/flutter_architecture_samples
path: todos_repository_core
todos_repository_simple:
git:
url: git://github.com/brianegan/flutter_architecture_samples
path: todos_repository_simple

flutter:
uses-material-design: true
</pre>

and finally install all of our dependencies

<pre class="ql-syntax" spellcheck="false">flutter packages get
</pre>

Note: We’re overriding some dependencies because we’re going to be reusing them from Brian Egan’s Flutter Architecture Samples.


Todos Repository

In this tutorial we’re not going to go into the implementation details of the TodosRepository because it was implemented by Brian Egan and is shared among all of the Todo Architecture Samples.

At a high level, the TodosRepository will expose a method to loadTodos and to saveTodos. That’s pretty much all we need to know so for the rest of the tutorial we’ll focus on the Bloc and Presentation layers.


Todos Bloc

Our TodosBloc will be responsible for converting TodosEvents into TodosStates and will manage the list of todos in our application.

Model

The first thing we need to do is define our Todo model. Each todo will need to have an id, a task, an optional note, and an optional completed flag.

Let’s create a models directory and create todo.dart.

<pre class="ql-syntax" spellcheck="false">import 'package:todos_app_core/todos_app_core.dart';
import 'package:meta/meta.dart';
import 'package:equatable/equatable.dart';
import 'package:todos_repository_core/todos_repository_core.dart';

@immutable
class Todo extends Equatable {
final bool complete;
final String id;
final String note;
final String task;

Todo(this.task, {this.complete = false, String note = '', String id})
: this.note = note ?? '',
this.id = id ?? Uuid().generateV4(),
super([complete, id, note, task]);

Todo copyWith({bool complete, String id, String note, String task}) {
return Todo(
task ?? this.task,
complete: complete ?? this.complete,
id: id ?? this.id,
note: note ?? this.note,
);
}

@override
String toString() {
return 'Todo { complete: $complete, task: $task, note: $note, id: $id }';
}

TodoEntity toEntity() {
return TodoEntity(task, id, note, complete);
}

static Todo fromEntity(TodoEntity entity) {
return Todo(
entity.task,
complete: entity.complete ?? false,
note: entity.note,
id: entity.id ?? Uuid().generateV4(),
);
}
}
</pre>

Note: We’re using the Equatable package so that we can compare instances of Todos without having to manually override == and hashCode.

Next up, we need to create the TodosState which our presentation layer will receive.


States

Let’s create blocs/todos/todos_state.dart and define the different states we’ll need to handle.

The three states we will implement are:

  • TodosLoading - the state while our application is fetching todos from the repository.
  • TodosLoaded - the state of our application after the todos have successfully been loaded.
  • TodosNotLoaded - the state of our application if the todos were not successfully loaded.
<pre class="ql-syntax" spellcheck="false">import 'package:equatable/equatable.dart';
import 'package:meta/meta.dart';
import 'package:flutter_todos/models/models.dart';

@immutable
abstract class TodosState extends Equatable {
TodosState([List props = const []]) : super(props);
}

class TodosLoading extends TodosState {
@override
String toString() => 'TodosLoading';
}

class TodosLoaded extends TodosState {
final List<Todo> todos;

TodosLoaded([this.todos = const []]) : super([todos]);

@override
String toString() => 'TodosLoaded { todos: $todos }';
}

class TodosNotLoaded extends TodosState {
@override
String toString() => 'TodosNotLoaded';
}
</pre>

Note: We are annotating our base TodosState with the immutable decorator so that we can indicate that all TodosStates cannot be changed.

Next, let’s implement the events we will need to handle.


Events

The events we will need to handle in our TodosBloc are:

  • LoadTodos - tells the bloc that it needs to load the todos from the TodosRepository.
  • AddTodo - tells the bloc that it needs to add an new todo to the list of todos.
  • UpdateTodo - tells the bloc that it needs to update an existing todo.
  • DeleteTodo - tells the bloc that it needs to remove an existing todo.
  • ClearCompleted - tells the bloc that it needs to remove all completed todos.
  • ToggleAll - tells the bloc that it needs to toggle the completed state of all todos.

Create blocs/todos/todos_event.dart and let’s implement the events we described above.

<pre class="ql-syntax" spellcheck="false">import 'package:equatable/equatable.dart';
import 'package:meta/meta.dart';
import 'package:flutter_todos/models/models.dart';

@immutable
abstract class TodosEvent extends Equatable {
TodosEvent([List props = const []]) : super(props);
}

class LoadTodos extends TodosEvent {
@override
String toString() => 'LoadTodos';
}

class AddTodo extends TodosEvent {
final Todo todo;

AddTodo(this.todo) : super([todo]);

@override
String toString() => 'AddTodo { todo: $todo }';
}

class UpdateTodo extends TodosEvent {
final Todo updatedTodo;

UpdateTodo(this.updatedTodo) : super([updatedTodo]);

@override
String toString() => 'UpdateTodo { updatedTodo: $updatedTodo }';
}

class DeleteTodo extends TodosEvent {
final Todo todo;

DeleteTodo(this.todo) : super([todo]);

@override
String toString() => 'DeleteTodo { todo: $todo }';
}

class ClearCompleted extends TodosEvent {
@override
String toString() => 'ClearCompleted';
}

class ToggleAll extends TodosEvent {
@override
String toString() => 'ToggleAll';
}
</pre>

Now that we have our TodosStates and TodosEvents implemented we can implement our TodosBloc.

Bloc

Let’s create blocs/todos/todos_bloc.dart and get started! We just need to implement initialState and mapEventToState.

Tip: Check out the Bloc VSCode Extension which provides tools for effectively creating blocs for both Flutter and AngularDart apps.

<pre class="ql-syntax" spellcheck="false">import 'dart:async';
import 'package:bloc/bloc.dart';
import 'package:meta/meta.dart';
import 'package:flutter_todos/blocs/todos/todos.dart';
import 'package:flutter_todos/models/models.dart';
import 'package:todos_repository_simple/todos_repository_simple.dart';

class TodosBloc extends Bloc<TodosEvent, TodosState> {
final TodosRepositoryFlutter todosRepository;

TodosBloc({@required this.todosRepository});

@override
TodosState get initialState => TodosLoading();

@override
Stream<TodosState> mapEventToState(
TodosState currentState,
TodosEvent event,
) async* {
if (event is LoadTodos) {
yield* _mapLoadTodosToState();
} else if (event is AddTodo) {
yield* _mapAddTodoToState(currentState, event);
} else if (event is UpdateTodo) {
yield* _mapUpdateTodoToState(currentState, event);
} else if (event is DeleteTodo) {
yield* _mapDeleteTodoToState(currentState, event);
} else if (event is ToggleAll) {
yield* _mapToggleAllToState(currentState);
} else if (event is ClearCompleted) {
yield* _mapClearCompletedToState(currentState);
}
}

Stream<TodosState> mapLoadTodosToState() async* {
try {
final todos = await this.todosRepository.loadTodos();
yield TodosLoaded(
todos.map(Todo.fromEntity).toList(),
);
} catch (
) {
yield TodosNotLoaded();
}
}

Stream<TodosState> _mapAddTodoToState(
TodosState currentState,
AddTodo event,
) async* {
if (currentState is TodosLoaded) {
final List<Todo> updatedTodos = List.from(currentState.todos)
..add(event.todo);
yield TodosLoaded(updatedTodos);
_saveTodos(updatedTodos);
}
}

Stream<TodosState> _mapUpdateTodoToState(
TodosState currentState,
UpdateTodo event,
) async* {
if (currentState is TodosLoaded) {
final List<Todo> updatedTodos = currentState.todos.map((todo) {
return todo.id == event.updatedTodo.id ? event.updatedTodo : todo;
}).toList();
yield TodosLoaded(updatedTodos);
_saveTodos(updatedTodos);
}
}

Stream<TodosState> _mapDeleteTodoToState(
TodosState currentState,
DeleteTodo event,
) async* {
if (currentState is TodosLoaded) {
final updatedTodos =
currentState.todos.where((todo) => todo.id != event.todo.id).toList();
yield TodosLoaded(updatedTodos);
_saveTodos(updatedTodos);
}
}

Stream<TodosState> _mapToggleAllToState(TodosState currentState) async* {
if (currentState is TodosLoaded) {
final allComplete = currentState.todos.every((todo) => todo.complete);
final List<Todo> updatedTodos = currentState.todos
.map((todo) => todo.copyWith(complete: !allComplete))
.toList();
yield TodosLoaded(updatedTodos);
_saveTodos(updatedTodos);
}
}

Stream<TodosState> _mapClearCompletedToState(TodosState currentState) async* {
if (currentState is TodosLoaded) {
final List<Todo> updatedTodos =
currentState.todos.where((todo) => !todo.complete).toList();
yield TodosLoaded(updatedTodos);
_saveTodos(updatedTodos);
}
}

Future _saveTodos(List<Todo> todos) {
return todosRepository.saveTodos(
todos.map((todo) => todo.toEntity()).toList(),
);
}
}
</pre>

When we yield a state in the private mapEventToState handlers, we are always yielding a new state instead of mutating the currentState. This is because every time we yield, bloc will compare the currentState to the nextState and will only trigger a state change (transition) if the two states are not equal. If we just mutate and yield the same instance of state, then currentState == nextState would evaluate to true and no state change would occur.

Our TodosBloc will have a dependency on the TodosRepository so that it can load and save todos. It will have an initial state of TodosLoading and defines the private handlers for each of the events. Whenever the TodosBloc changes the list of todos it calls the saveTodos method in the TodosRepository in order to keep everything persisted locally.


Barrel File

Now that we’re done with our TodosBloc we can create a barrel file to export all of our bloc files and make it convenient to import them later on.

Create blocs/todos/todos.dart and export the bloc, events, and states:

<pre class="ql-syntax" spellcheck="false">export './todos_bloc.dart';
export './todos_event.dart';
export './todos_state.dart';
</pre>

Filtered Todos Bloc

The FilteredTodosBloc will be responsible for reacting to state changes in the TodosBloc we just created and will maintain the state of filtered todos in our application.

Model

Before we start defining and implementing the TodosStates, we will need to implement a VisibilityFilter model that will determine which todos our FilteredTodosState will contain. In this case, we will have three filters:

  • all - show all Todos (default)
  • active - only show Todos which have not been completed
  • completed only show Todos which have been completed

We can create models/visibility_filter.dart and define our filter as an enum:

<pre class="ql-syntax" spellcheck="false">enum VisibilityFilter { all, active, completed }
</pre>

States

Just like we did with the TodosBloc, we’ll need to define the different states for our FilteredTodosBloc.

In this case, we only have two states:

  • FilteredTodosLoading - the state while we are fetching todos
  • FilteredTodosLoaded - the state when we are no longer fetching todos

Let’s create blocs/filtered_todos/filtered_todos_state.dart and implement the two states.

<pre class="ql-syntax" spellcheck="false">import 'package:equatable/equatable.dart';
import 'package:meta/meta.dart';
import 'package:flutter_todos/models/models.dart';

@immutable
abstract class FilteredTodosState extends Equatable {
FilteredTodosState([List props = const []]) : super(props);
}

class FilteredTodosLoading extends FilteredTodosState {
@override
String toString() => 'FilteredTodosLoading';
}

class FilteredTodosLoaded extends FilteredTodosState {
final List<Todo> filteredTodos;
final VisibilityFilter activeFilter;

FilteredTodosLoaded(this.filteredTodos, this.activeFilter)
: super([filteredTodos, activeFilter]);

@override
String toString() {
return 'FilteredTodosLoaded { filteredTodos: $filteredTodos, activeFilter: $activeFilter }';
}
}
</pre>

Note: The FilteredTodosLoaded state contains the list of filtered todos as well as the active visibility filter.


Events

We’re going to implement two events for our FilteredTodosBloc:

  • UpdateFilter - which notifies the bloc that the visibility filter has changed
  • UpdateTodos - which notifies the bloc that the list of todos has changed

Create blocs/filtered_todos/filtered_todos_event.dart and let’s implement the two events.

<pre class="ql-syntax" spellcheck="false">import 'package:equatable/equatable.dart';
import 'package:meta/meta.dart';
import 'package:flutter_todos/models/models.dart';

@immutable
abstract class FilteredTodosEvent extends Equatable {
FilteredTodosEvent([List props = const []]) : super(props);
}

class UpdateFilter extends FilteredTodosEvent {
final VisibilityFilter filter;

UpdateFilter(this.filter) : super([filter]);

@override
String toString() => 'UpdateFilter { filter: $filter }';
}

class UpdateTodos extends FilteredTodosEvent {
final List<Todo> todos;

UpdateTodos(this.todos) : super([todos]);

@override
String toString() => 'UpdateTodos { todos: $todos }';
}
</pre>

We’re ready to implement our FilteredTodosBloc next!


Bloc

Our FilteredTodosBloc will be similar to our TodosBloc; however, instead of having a dependency on the TodosRepository, it will have a dependency on the TodosBloc itself. This will allow the FilteredTodosBloc to update its state in response to state changes in the TodosBloc.

Create blocs/filtered_todos/filtered_todos_bloc.dart and let’s get started.

<pre class="ql-syntax" spellcheck="false">import 'dart:async';
import 'package:bloc/bloc.dart';
import 'package:meta/meta.dart';
import 'package:flutter_todos/blocs/filtered_todos/filtered_todos.dart';
import 'package:flutter_todos/blocs/todos/todos.dart';
import 'package:flutter_todos/models/models.dart';

class FilteredTodosBloc extends Bloc<FilteredTodosEvent, FilteredTodosState> {
final TodosBloc todosBloc;
StreamSubscription todosSubscription;

FilteredTodosBloc({@required this.todosBloc}) {
todosSubscription = todosBloc.state.listen((state) {
if (state is TodosLoaded) {
dispatch(UpdateTodos((todosBloc.currentState as TodosLoaded).todos));
}
});
}

@override
FilteredTodosState get initialState {
return todosBloc.currentState is TodosLoaded
? FilteredTodosLoaded(
(todosBloc.currentState as TodosLoaded).todos,
VisibilityFilter.all,
)
: FilteredTodosLoading();
}

@override
Stream<FilteredTodosState> mapEventToState(
FilteredTodosState currentState,
FilteredTodosEvent event,
) async* {
if (event is UpdateFilter) {
yield* _mapUpdateFilterToState(currentState, event);
} else if (event is UpdateTodos) {
yield* _mapTodosUpdatedToState(currentState, event);
}
}

Stream<FilteredTodosState> _mapUpdateFilterToState(
FilteredTodosState currentState,
UpdateFilter event,
) async* {
if (todosBloc.currentState is TodosLoaded) {
yield FilteredTodosLoaded(
_mapTodosToFilteredTodos(
(todosBloc.currentState as TodosLoaded).todos,
event.filter,
),
event.filter,
);
}
}

Stream<FilteredTodosState> _mapTodosUpdatedToState(
FilteredTodosState currentState,
UpdateTodos event,
) async* {
final visibilityFilter = currentState is FilteredTodosLoaded
? currentState.activeFilter
: VisibilityFilter.all;
yield FilteredTodosLoaded(
_mapTodosToFilteredTodos(
(todosBloc.currentState as TodosLoaded).todos,
visibilityFilter,
),
visibilityFilter,
);
}

List<Todo> _mapTodosToFilteredTodos(
List<Todo> todos, VisibilityFilter filter) {
return todos.where((todo) {
if (filter == VisibilityFilter.all) {
return true;
} else if (filter == VisibilityFilter.active) {
return !todo.complete;
} else if (filter == VisibilityFilter.completed) {
return todo.complete;
}
}).toList();
}

@override
void dispose() {
todosSubscription.cancel();
super.dispose();
}
}
</pre>

We create a StreamSubscription for the stream of TodosStates so that we can listen to the state changes in the TodosBloc. We override the bloc’s dispose method and cancel the subscription so that we can clean up after the bloc is disposed.


Barrel File

Just like before, we can create a barrel file to make it more convenient to import the various filtered todos classes.

Create blocs/filtered_todos/filtered_todos.dart and export the three files:

<pre class="ql-syntax" spellcheck="false">export './filtered_todos_bloc.dart';
export './filtered_todos_event.dart';
export './filtered_todos_state.dart';
</pre>

Stats Bloc

The StatsBloc will be responsible for maintaining the statistics for number of active todos and number of completed todos. Similarly, to the FilteredTodosBloc, it will have a dependency on the TodosBloc itself so that it can react to changes in the TodosBloc state.

State

Our StatsBloc will have two states that it can be in:

  • StatsLoading - the state when the statistics have not yet been calculated.
  • StatsLoaded - the state when the statistics have been calculated.

Create blocs/stats/stats_state.dart and let’s implement our StatsState.

<pre class="ql-syntax" spellcheck="false">import 'package:equatable/equatable.dart';
import 'package:meta/meta.dart';

@immutable
abstract class StatsState extends Equatable {
StatsState([List props = const []]) : super(props);
}

class StatsLoading extends StatsState {
@override
String toString() => 'StatsLoading';
}

class StatsLoaded extends StatsState {
final int numActive;
final int numCompleted;

StatsLoaded(this.numActive, this.numCompleted)
: super([numActive, numCompleted]);

@override
String toString() {
return 'StatsLoaded { numActive: $numActive, numCompleted: $numCompleted }';
}
}
</pre>

Next, let’s define and implement the StatsEvents.


Events

There will just be a single event our StatsBloc will respond to: UpdateStats. This event will be dispatched whenever the TodosBloc state changes so that our StatsBloc can recalculate the new statistics.

Create blocs/stats/states_event.dart and let’s implement it.

<pre class="ql-syntax" spellcheck="false">import 'package:equatable/equatable.dart';
import 'package:meta/meta.dart';
import 'package:flutter_todos/models/models.dart';

@immutable
abstract class StatsEvent extends Equatable {
StatsEvent([List props = const []]) : super(props);
}

class UpdateStats extends StatsEvent {
final List<Todo> todos;

UpdateStats(this.todos) : super([todos]);

@override
String toString() => 'UpdateStats { todos: $todos }';
}
</pre>

Now we’re ready to implement our StatsBloc which will look very similar to the FilteredTodosBloc.


Bloc

Our StatsBloc will have a dependency on the TodosBloc itself which will allow it to update its state in response to state changes in the TodosBloc.

Create blocs/stats/stats_bloc.dart and let’s get started.

<pre class="ql-syntax" spellcheck="false">import 'dart:async';
import 'package:meta/meta.dart';
import 'package:bloc/bloc.dart';
import 'package:flutter_todos/blocs/blocs.dart';

class StatsBloc extends Bloc<StatsEvent, StatsState> {
final TodosBloc todosBloc;
StreamSubscription todosSubscription;

StatsBloc({@required this.todosBloc}) {
todosSubscription = todosBloc.state.listen((state) {
if (state is TodosLoaded) {
dispatch(UpdateStats(state.todos));
}
});
}

@override
StatsState get initialState => StatsLoading();

@override
Stream<StatsState> mapEventToState(
StatsState currentState,
StatsEvent event,
) async* {
if (event is UpdateStats) {
int numActive =
event.todos.where((todo) => !todo.complete).toList().length;
int numCompleted =
event.todos.where((todo) => todo.complete).toList().length;
yield StatsLoaded(numActive, numCompleted);
}
}

@override
void dispose() {
todosSubscription.cancel();
super.dispose();
}
}
</pre>

That’s all there is to it! Our StatsBloc recalculates its state which contains the number of active todos and the number of completed todos on each state change of our TodosBloc.

Now that we’re done with the StatsBloc we just have one last bloc to implement: the TabBloc.


Tab Bloc

The TabBloc will be responsible for maintaining the state of the tabs in our application. It will be taking TabEvents as input and outputting AppTabs.

Model / State

We need to define an AppTab model which we will also use to represent the TabState. The AppTab will just be an enum which represents the active tab in our application. Since the app we’re building will only have two tabs: todos and stats, we just need two values.

Create models/app_tab.dart:

<pre class="ql-syntax" spellcheck="false">enum AppTab { todos, stats }
</pre>

Event

Our TabBloc will be responsible for handling a single TabEvent:

  • UpdateTab - which notifies the bloc that the active tab has updated

Create blocs/tab/tab_event.dart:

<pre class="ql-syntax" spellcheck="false">import 'package:equatable/equatable.dart';
import 'package:meta/meta.dart';
import 'package:flutter_todos/models/models.dart';

@immutable
abstract class TabEvent extends Equatable {
TabEvent([List props = const []]) : super(props);
}

class UpdateTab extends TabEvent {
final AppTab tab;

UpdateTab(this.tab) : super([tab]);

@override
String toString() => 'UpdateTab { tab: $tab }';
}
</pre>

Bloc

Our TabBloc implementation will be super simple. As always, we just need to implement initialState and mapEventToState.

Create blocs/tab/tab_bloc.dart and let’s quickly do the implementation.

<pre class="ql-syntax" spellcheck="false">import 'dart:async';
import 'package:bloc/bloc.dart';
import 'package:flutter_todos/blocs/tab/tab.dart';
import 'package:flutter_todos/models/models.dart';

class TabBloc extends Bloc<TabEvent, AppTab> {
@override
AppTab get initialState => AppTab.todos;

@override
Stream<AppTab> mapEventToState(
AppTab currentState,
TabEvent event,
) async* {
if (event is UpdateTab) {
yield event.tab;
}
}
}
</pre>

I told you it’d be simple. All the TabBloc is doing is setting the initial state to the todos tab and handling the UpdateTab event by yielding a new AppTab instance.


Barrel File

Lastly, we’ll create another barrel file for our TabBloc exports. Create blocs/tab/tab.dart and export the two files:

<pre class="ql-syntax" spellcheck="false">export './tab_bloc.dart';
export './tab_event.dart';
</pre>

Bloc Delegate

Before we move on to the presentation layer, we will implement our own BlocDelegate which will allow us to handle all state changes and errors in a single place. It’s really useful for things like developer logs or analytics.

Create blocs/simple_bloc_delegate.dart and let’s get started.

<pre class="ql-syntax" spellcheck="false">import 'package:bloc/bloc.dart';

class SimpleBlocDelegate extends BlocDelegate {
@override
void onTransition(Transition transition) {
print(transition);
}

@override
void onError(Object error, StackTrace stacktrace) {
print(error);
}
}
</pre>

All we’re doing in this case is printing all state changes (transitions) and errors to the console just so that we can see what’s going on when we’re running our app locally. You can hook up your BlocDelegate to Google Analytics, Sentry, Crashlytics, etc…


Blocs Barrel File

Now that we have all of our blocs implemented we can create a barrel file. Create blocs/blocs.dart and export all of our blocs so that we can conveniently import any bloc code with a single import.

<pre class="ql-syntax" spellcheck="false">export './filtered_todos/filtered_todos.dart';
export './stats/stats.dart';
export './tab/tab.dart';
export './todos/todos.dart';
export './simple_bloc_delegate.dart';
</pre>

Up next, we’ll focus on implementing the major screens in our Todos application.


Screens

Home Screen

Our HomeScreen will be responsible for creating the Scaffold of our application. It will maintain the AppBar, BottomNavigationBar, as well as the Stats/FilteredTodos widgets (depending on the active tab).

Let’s create a new directory called screens where we will put all of our new screen widgets and then create screens/home_screen.dart.

Our HomeScreen will be a StatefulWidget because it will need to create and dispose the TabBloc, FilteredTodosBloc, and StatsBloc.

<pre class="ql-syntax" spellcheck="false">import 'package:flutter/material.dart';
import 'package:todos_app_core/todos_app_core.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_todos/blocs/blocs.dart';
import 'package:flutter_todos/widgets/widgets.dart';
import 'package:flutter_todos/localization.dart';
import 'package:flutter_todos/models/models.dart';

class HomeScreen extends StatefulWidget {
final void Function() onInit;

HomeScreen({@required this.onInit}) : super(key: ArchSampleKeys.homeScreen);

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

class _HomeScreenState extends State<HomeScreen> {
final TabBloc _tabBloc = TabBloc();
FilteredTodosBloc _filteredTodosBloc;
StatsBloc _statsBloc;

@override
void initState() {
widget.onInit();
_filteredTodosBloc = FilteredTodosBloc(
todosBloc: BlocProvider.of<TodosBloc>(context),
);
_statsBloc = StatsBloc(
todosBloc: BlocProvider.of<TodosBloc>(context),
);
super.initState();
}

@override
Widget build(BuildContext context) {
return BlocBuilder(
bloc: _tabBloc,
builder: (BuildContext context, AppTab activeTab) {
return BlocProviderTree(
blocProviders: [
BlocProvider<TabBloc>(bloc: _tabBloc),
BlocProvider<FilteredTodosBloc>(bloc: _filteredTodosBloc),
BlocProvider<StatsBloc>(bloc: _statsBloc),
],
child: Scaffold(
appBar: AppBar(
title: Text(FlutterBlocLocalizations.of(context).appTitle),
actions: [
FilterButton(visible: activeTab == AppTab.todos),
ExtraActions(),
],
),
body: activeTab == AppTab.todos ? FilteredTodos() : Stats(),
floatingActionButton: FloatingActionButton(
key: ArchSampleKeys.addTodoFab,
onPressed: () {
Navigator.pushNamed(context, ArchSampleRoutes.addTodo);
},
child: Icon(Icons.add),
tooltip: ArchSampleLocalizations.of(context).addTodo,
),
bottomNavigationBar: TabSelector(
activeTab: activeTab,
onTabSelected: (tab) => _tabBloc.dispatch(UpdateTab(tab)),
),
),
);
},
);
}

@override
void dispose() {
_statsBloc.dispose();
_filteredTodosBloc.dispose();
_tabBloc.dispose();
super.dispose();
}
}
</pre>

The HomeScreen creates the TabBloc, FilteredTodosBloc, and StatsBloc as part of its state. It uses BlocProvider.of<TodosBloc>(context) in order to access the TodosBloc which will be made available from our root TodosApp widget (we’ll get to it later in this tutorial).

Since the HomeScreen needs to respond to changes in the TodosBloc state, we use BlocBuilder in order to build the correct widget based on the current TodosState.

The HomeScreen also makes the TabBloc, FilteredTodosBloc, and StatsBloc available to the widgets in its subtree by using the BlocProviderTree widget from flutter_bloc.

<pre class="ql-syntax" spellcheck="false">BlocProviderTree(
blocProviders: [
BlocProvider<TabBloc>(bloc: _tabBloc),
BlocProvider<FilteredTodosBloc>(bloc: _filteredTodosBloc),
BlocProvider<StatsBloc>(bloc: _statsBloc),
],
child: Scaffold(...),
);
</pre>

is equivalent to writing

<pre class="ql-syntax" spellcheck="false">BlocProvider<TabBloc>(
bloc: _tabBloc,
child: BlocProvider<FilteredTodosBloc>(
bloc: _filteredTodosBloc,
child: BlocProvider<StatsBloc>(
bloc: _statsBloc,
child: Scaffold(...),
),
),
);
</pre>

You can see how using BlocProviderTree helps reduce the levels of nesting and makes the code easier to read and maintain.

Next, we’ll implement the DetailsScreen.

Details Screen

The DetailsScreen displays the full details of the selected todo and allows the user to either edit or delete the todo.

Create screens/details_screen.dart and let’s build it.

<pre class="ql-syntax" spellcheck="false">import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:todos_app_core/todos_app_core.dart';
import 'package:flutter_todos/blocs/todos/todos.dart';
import 'package:flutter_todos/screens/screens.dart';
import 'package:flutter_todos/flutter_todos_keys.dart';

class DetailsScreen extends StatelessWidget {
final String id;

DetailsScreen({Key key, @required this.id})
: super(key: key ?? ArchSampleKeys.todoDetailsScreen);

@override
Widget build(BuildContext context) {
final todosBloc = BlocProvider.of<TodosBloc>(context);
return BlocBuilder(
bloc: todosBloc,
builder: (BuildContext context, TodosState state) {
final todo = (state as TodosLoaded)
.todos
.firstWhere((todo) => todo.id == id, orElse: () => null);
final localizations = ArchSampleLocalizations.of(context);
return Scaffold(
appBar: AppBar(
title: Text(localizations.todoDetails),
actions: [
IconButton(
tooltip: localizations.deleteTodo,
key: ArchSampleKeys.deleteTodoButton,
icon: Icon(Icons.delete),
onPressed: () {
todosBloc.dispatch(DeleteTodo(todo));
Navigator.pop(context, todo);
},
)
],
),
body: todo == null
? Container(key: FlutterTodosKeys.emptyDetailsContainer)
: Padding(
padding: EdgeInsets.all(16.0),
child: ListView(
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: EdgeInsets.only(right: 8.0),
child: Checkbox(
key: FlutterTodosKeys.detailsScreenCheckBox,
value: todo.complete,
onChanged: (_) {
todosBloc.dispatch(
UpdateTodo(
todo.copyWith(complete: !todo.complete),
),
);
}),
),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Hero(
tag: '${todo.id}__heroTag',
child: Container(
width: MediaQuery.of(context).size.width,
padding: EdgeInsets.only(
top: 8.0,
bottom: 16.0,
),
child: Text(
todo.task,
key: ArchSampleKeys.detailsTodoItemTask,
style:
Theme.of(context).textTheme.headline,
),
),
),
Text(
todo.note,
key: ArchSampleKeys.detailsTodoItemNote,
style: Theme.of(context).textTheme.subhead,
),
],
),
),
],
),
],
),
),
floatingActionButton: FloatingActionButton(
key: ArchSampleKeys.editTodoFab,
tooltip: localizations.editTodo,
child: Icon(Icons.edit),
onPressed: todo == null
? null
: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) {
return AddEditScreen(
key: ArchSampleKeys.editTodoScreen,
onSave: (task, note) {
todosBloc.dispatch(
UpdateTodo(
todo.copyWith(task: task, note: note),
),
);
},
isEditing: true,
todo: todo,
);
},
),
);
},
),
);
},
);
}
}
</pre>

Note: The DetailsScreen requires a todo id so that it can pull the todo details from the TodosBloc and so that it can update whenever a todo’s details have been changed (a todo’s id cannot be changed).

The main things to note are that there is an IconButton which dispatches a DeleteTodo event as well as a checkbox which dispatches an UpdateTodo event.

There is also another FloatingActionButton which navigates the user to the AddEditScreen with isEditing set to true. We’ll take a look at the AddEditScreen next.


Add/Edit Screen

The AddEditScreen widget allows the user to either create a new todo or update an existing todo based on the isEditing flag that is passed via the constructor.

Create screens/add_edit_screen.dart and let’s have a look at the implementation.

<pre class="ql-syntax" spellcheck="false">import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:todos_app_core/todos_app_core.dart';
import 'package:flutter_todos/models/models.dart';

typedef OnSaveCallback = Function(String task, String note);

class AddEditScreen extends StatefulWidget {
final bool isEditing;
final OnSaveCallback onSave;
final Todo todo;

AddEditScreen({
Key key,
@required this.onSave,
@required this.isEditing,
this.todo,
}) : super(key: key ?? ArchSampleKeys.addTodoScreen);

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

class _AddEditScreenState extends State<AddEditScreen> {
static final GlobalKey<FormState> _formKey = GlobalKey<FormState>();

String _task;
String _note;

bool get isEditing => widget.isEditing;

@override
Widget build(BuildContext context) {
final localizations = ArchSampleLocalizations.of(context);
final textTheme = Theme.of(context).textTheme;

return Scaffold(
  appBar: AppBar(
    title: Text(
      isEditing ? localizations.editTodo : localizations.addTodo,
    ),
  ),
  body: Padding(
    padding: EdgeInsets.all(16.0),
    child: Form(
      key: _formKey,
      child: ListView(
        children: [
          TextFormField(
            initialValue: isEditing ? widget.todo.task : '',
            key: ArchSampleKeys.taskField,
            autofocus: !isEditing,
            style: textTheme.headline,
            decoration: InputDecoration(
              hintText: localizations.newTodoHint,
            ),
            validator: (val) {
              return val.trim().isEmpty
                  ? localizations.emptyTodoError
                  : null;
            },
            onSaved: (value) =&gt; _task = value,
          ),
          TextFormField(
            initialValue: isEditing ? widget.todo.note : '',
            key: ArchSampleKeys.noteField,
            maxLines: 10,
            style: textTheme.subhead,
            decoration: InputDecoration(
              hintText: localizations.notesHint,
            ),
            onSaved: (value) =&gt; _note = value,
          )
        ],
      ),
    ),
  ),
  floatingActionButton: FloatingActionButton(
    key:
        isEditing ? ArchSampleKeys.saveTodoFab : ArchSampleKeys.saveNewTodo,
    tooltip: isEditing ? localizations.saveChanges : localizations.addTodo,
    child: Icon(isEditing ? Icons.check : Icons.add),
    onPressed: () {
      if (_formKey.currentState.validate()) {
        _formKey.currentState.save();
        widget.onSave(_task, _note);
        Navigator.pop(context);
      }
    },
  ),
);

}
}
</pre>

There’s nothing bloc-specific in this widget. It’s simply presenting a form and:

  • if isEditing is true the form is populated it with the existing todo details.
  • otherwise the inputs are empty so that the user can create a new todo.

It uses an onSave callback function to notify its parent of the updated or newly created todo.

That’s it for the screens in our application so before we forget, let’s create a barrel file to export them.


Screens Barrel File

Create screens/screens.dart and export all three.

<pre class="ql-syntax" spellcheck="false">export './add_edit_screen.dart';
export './details_screen.dart';
export './home_screen.dart';
</pre>

Widgets

FilterButton

The FilterButton widget will be responsible for providing the user with a list of filter options and will notify the FilteredTodosBloc when a new filter is selected.

Let’s create a new directory called widgets and put our FilterButton implementation in widgets/filter_button.dart.

<pre class="ql-syntax" spellcheck="false">import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:todos_app_core/todos_app_core.dart';
import 'package:flutter_todos/blocs/filtered_todos/filtered_todos.dart';
import 'package:flutter_todos/models/models.dart';

class FilterButton extends StatelessWidget {
final bool visible;

FilterButton({this.visible, Key key}) : super(key: key);

@override
Widget build(BuildContext context) {
final defaultStyle = Theme.of(context).textTheme.body1;
final activeStyle = Theme.of(context)
.textTheme
.body1
.copyWith(color: Theme.of(context).accentColor);
final FilteredTodosBloc filteredTodosBloc =
BlocProvider.of<FilteredTodosBloc>(context);
return BlocBuilder(
bloc: filteredTodosBloc,
builder: (BuildContext context, FilteredTodosState state) {
final button = _Button(
onSelected: (filter) {
filteredTodosBloc.dispatch(UpdateFilter(filter));
},
activeFilter: state is FilteredTodosLoaded
? state.activeFilter
: VisibilityFilter.all,
activeStyle: activeStyle,
defaultStyle: defaultStyle,
);
return AnimatedOpacity(
opacity: visible ? 1.0 : 0.0,
duration: Duration(milliseconds: 150),
child: visible ? button : IgnorePointer(child: button),
);
});
}
}

class _Button extends StatelessWidget {
const _Button({
Key key,
@required this.onSelected,
@required this.activeFilter,
@required this.activeStyle,
@required this.defaultStyle,
}) : super(key: key);

final PopupMenuItemSelected<VisibilityFilter> onSelected;
final VisibilityFilter activeFilter;
final TextStyle activeStyle;
final TextStyle defaultStyle;

@override
Widget build(BuildContext context) {
return PopupMenuButton<VisibilityFilter>(
key: ArchSampleKeys.filterButton,
tooltip: ArchSampleLocalizations.of(context).filterTodos,
onSelected: onSelected,
itemBuilder: (BuildContext context) => <PopupMenuItem<VisibilityFilter>>[
PopupMenuItem<VisibilityFilter>(
key: ArchSampleKeys.allFilter,
value: VisibilityFilter.all,
child: Text(
ArchSampleLocalizations.of(context).showAll,
style: activeFilter == VisibilityFilter.all
? activeStyle
: defaultStyle,
),
),
PopupMenuItem<VisibilityFilter>(
key: ArchSampleKeys.activeFilter,
value: VisibilityFilter.active,
child: Text(
ArchSampleLocalizations.of(context).showActive,
style: activeFilter == VisibilityFilter.active
? activeStyle
: defaultStyle,
),
),
PopupMenuItem<VisibilityFilter>(
key: ArchSampleKeys.completedFilter,
value: VisibilityFilter.completed,
child: Text(
ArchSampleLocalizations.of(context).showCompleted,
style: activeFilter == VisibilityFilter.completed
? activeStyle
: defaultStyle,
),
),
],
icon: Icon(Icons.filter_list),
);
}
}
</pre>

The FilterButton needs to respond to state changes in the FilteredTodosBloc so it uses BlocProvider to access the FilteredTodosBloc from the BuildContext. It then uses BlocBuilder to re-render whenever the FilteredTodosBloc changes state.

The rest of the implementation is pure Flutter and there isn’t much going on so we can move on to the ExtraActions widget.


Extra Actions

Similarly to the FilterButton, the ExtraActions widget is responsible for providing the user with a list of extra options: Toggling Todos and Clearing Completed Todos.

Since this widget doesn’t care about the filters it will interact with the TodosBloc instead of the FilteredTodosBloc.

Let’s create widgets/extra_actions.dart and implement it.

<pre class="ql-syntax" spellcheck="false">import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:todos_app_core/todos_app_core.dart';
import 'package:flutter_todos/blocs/todos/todos.dart';
import 'package:flutter_todos/models/models.dart';
import 'package:flutter_todos/flutter_todos_keys.dart';

class ExtraActions extends StatelessWidget {
ExtraActions({Key key}) : super(key: ArchSampleKeys.extraActionsButton);

@override
Widget build(BuildContext context) {
final todosBloc = BlocProvider.of<TodosBloc>(context);
return BlocBuilder(
bloc: todosBloc,
builder: (BuildContext context, TodosState state) {
if (state is TodosLoaded) {
bool allComplete = (todosBloc.currentState as TodosLoaded)
.todos
.every((todo) => todo.complete);
return PopupMenuButton<ExtraAction>(
key: FlutterTodosKeys.extraActionsPopupMenuButton,
onSelected: (action) {
switch (action) {
case ExtraAction.clearCompleted:
todosBloc.dispatch(ClearCompleted());
break;
case ExtraAction.toggleAllComplete:
todosBloc.dispatch(ToggleAll());
break;
}
},
itemBuilder: (BuildContext context) => <PopupMenuItem<ExtraAction>>[
PopupMenuItem<ExtraAction>(
key: ArchSampleKeys.toggleAll,
value: ExtraAction.toggleAllComplete,
child: Text(
allComplete
? ArchSampleLocalizations.of(context)
.markAllIncomplete
: ArchSampleLocalizations.of(context).markAllComplete,
),
),
PopupMenuItem<ExtraAction>(
key: ArchSampleKeys.clearCompleted,
value: ExtraAction.clearCompleted,
child: Text(
ArchSampleLocalizations.of(context).clearCompleted,
),
),
],
);
}
return Container(key: FlutterTodosKeys.extraActionsEmptyContainer);
},
);
}
}
</pre>

Just like with the FilterButton, we use BlocProvider to access the TodosBloc from the BuildContext and BlocBuilder to respond to state changes in the TodosBloc.

Based on the action selected, the widget dispatches an event to the TodosBloc to either ToggleAll todos’ completion states or ClearCompleted todos.

Next we’ll take a look at the TabSelector widget.


Tab Selector

The TabSelector widget is responsible for displaying the tabs in the BottomNavigationBar and handling user input.

Let’s create widgets/tab_selector.dart and implement it.

<pre class="ql-syntax" spellcheck="false">import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:todos_app_core/todos_app_core.dart';
import 'package:flutter_todos/models/models.dart';

class TabSelector extends StatelessWidget {
final AppTab activeTab;
final Function(AppTab) onTabSelected;

TabSelector({
Key key,
@required this.activeTab,
@required this.onTabSelected,
}) : super(key: key);

@override
Widget build(BuildContext context) {
return BottomNavigationBar(
key: ArchSampleKeys.tabs,
currentIndex: AppTab.values.indexOf(activeTab),
onTap: (index) => onTabSelected(AppTab.values[index]),
items: AppTab.values.map((tab) {
return BottomNavigationBarItem(
icon: Icon(
tab == AppTab.todos ? Icons.list : Icons.show_chart,
key: tab == AppTab.todos
? ArchSampleKeys.todoTab
: ArchSampleKeys.statsTab,
),
title: Text(tab == AppTab.stats
? ArchSampleLocalizations.of(context).stats
: ArchSampleLocalizations.of(context).todos),
);
}).toList(),
);
}
}
</pre>

You can see that there is no dependency on blocs in this widget; it just calls onTabSelected when a tab is selected and also takes an activeTab as input so it knows which tab is currently selected.

Next, we’ll take a look at the FilteredTodos widget.


Filtered Todos

The FilteredTodos widget is responsible for showing a list of todos based on the current active filter.

Create widgets/filtered_todos.dart and let’s implement it.

<pre class="ql-syntax" spellcheck="false">import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:todos_app_core/todos_app_core.dart';
import 'package:flutter_todos/blocs/blocs.dart';
import 'package:flutter_todos/widgets/widgets.dart';
import 'package:flutter_todos/screens/screens.dart';
import 'package:flutter_todos/flutter_todos_keys.dart';

class FilteredTodos extends StatelessWidget {
FilteredTodos({Key key}) : super(key: key);

@override
Widget build(BuildContext context) {
final todosBloc = BlocProvider.of<TodosBloc>(context);
final filteredTodosBloc = BlocProvider.of<FilteredTodosBloc>(context);
final localizations = ArchSampleLocalizations.of(context);

return BlocBuilder(
  bloc: filteredTodosBloc,
  builder: (
    BuildContext context,
    FilteredTodosState state,
  ) {
    if (state is FilteredTodosLoading) {
      return LoadingIndicator(key: ArchSampleKeys.todosLoading);
    } else if (state is FilteredTodosLoaded) {
      final todos = state.filteredTodos;
      return ListView.builder(
        key: ArchSampleKeys.todoList,
        itemCount: todos.length,
        itemBuilder: (BuildContext context, int index) {
          final todo = todos[index];
          return TodoItem(
            todo: todo,
            onDismissed: (direction) {
              todosBloc.dispatch(DeleteTodo(todo));
              Scaffold.of(context).showSnackBar(DeleteTodoSnackBar(
                key: ArchSampleKeys.snackbar,
                todo: todo,
                onUndo: () =&gt; todosBloc.dispatch(AddTodo(todo)),
                localizations: localizations,
              ));
            },
            onTap: () async {
              final removedTodo = await Navigator.of(context).push(
                MaterialPageRoute(builder: (_) {
                  return DetailsScreen(id: todo.id);
                }),
              );
              if (removedTodo != null) {
                Scaffold.of(context).showSnackBar(DeleteTodoSnackBar(
                  key: ArchSampleKeys.snackbar,
                  todo: todo,
                  onUndo: () =&gt; todosBloc.dispatch(AddTodo(todo)),
                  localizations: localizations,
                ));
              }
            },
            onCheckboxChanged: (_) {
              todosBloc.dispatch(
                UpdateTodo(todo.copyWith(complete: !todo.complete)),
              );
            },
          );
        },
      );
    } else {
      return Container(key: FlutterTodosKeys.filteredTodosEmptyContainer);
    }
  },
);

}
}
</pre>

Just like the previous widgets we’ve written, the FilteredTodos widget uses BlocProvider to access blocs (in this case both the FilteredTodosBloc and the TodosBloc are needed).

  • The FilteredTodosBloc is needed to help us render the correct todos based on the current filter
  • The TodosBloc is needed to allow us to add/delete todos in response to user interactions such as swiping on an individual todo.

Todo Item

TodoItem is a stateless widget which is responsible for rendering a single todo and handling user interactions (taps/swipes).

Create widgets/todo_item.dart and let’s build it.

<pre class="ql-syntax" spellcheck="false">import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:todos_app_core/todos_app_core.dart';
import 'package:flutter_todos/models/models.dart';

class TodoItem extends StatelessWidget {
final DismissDirectionCallback onDismissed;
final GestureTapCallback onTap;
final ValueChanged<bool> onCheckboxChanged;
final Todo todo;

TodoItem({
Key key,
@required this.onDismissed,
@required this.onTap,
@required this.onCheckboxChanged,
@required this.todo,
}) : super(key: key);

@override
Widget build(BuildContext context) {
return Dismissible(
key: ArchSampleKeys.todoItem(todo.id),
onDismissed: onDismissed,
child: ListTile(
onTap: onTap,
leading: Checkbox(
key: ArchSampleKeys.todoItemCheckbox(todo.id),
value: todo.complete,
onChanged: onCheckboxChanged,
),
title: Hero(
tag: '${todo.id}__heroTag',
child: Container(
width: MediaQuery.of(context).size.width,
child: Text(
todo.task,
key: ArchSampleKeys.todoItemTask(todo.id),
style: Theme.of(context).textTheme.title,
),
),
),
subtitle: todo.note.isNotEmpty
? Text(
todo.note,
key: ArchSampleKeys.todoItemNote(todo.id),
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.subhead,
)
: null,
),
);
}
}
</pre>

Again, notice that the TodoItem has no bloc-specific code in it. It simply renders based on the todo we pass via the constructor and calls the injected callback functions whenever the user interacts with the todo.

Next up, we’ll create the DeleteTodoSnackBar.


Delete Todo SnackBar

The DeleteTodoSnackBar is responsible for indicating to the user that a todo was deleted and allows the user to undo his/her action.

Create widgets/delete_todo_snack_bar.dart and let’s implement it.

<pre class="ql-syntax" spellcheck="false">import 'package:flutter/material.dart';
import 'package:todos_app_core/todos_app_core.dart';
import 'package:flutter_todos/models/models.dart';

class DeleteTodoSnackBar extends SnackBar {
final ArchSampleLocalizations localizations;

DeleteTodoSnackBar({
Key key,
@required Todo todo,
@required VoidCallback onUndo,
@required this.localizations,
}) : super(
key: key,
content: Text(
localizations.todoDeleted(todo.task),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
duration: Duration(seconds: 2),
action: SnackBarAction(
label: localizations.undo,
onPressed: onUndo,
),
);
}
</pre>

By now, you’re probably noticing a pattern: this widget also has no bloc-specific code. It simply takes in a todo in order to render the task and calls a callback function called onUndo if a user presses the undo button.

We’re almost done; just two more widgets to go!


Loading Indicator

The LoadingIndicator widget is a stateless widget that is responsible for indicating to the user that something is in progress.

Create widgets/loading_indicator.dart and let’s write it.

<pre class="ql-syntax" spellcheck="false">import 'package:flutter/material.dart';

class LoadingIndicator extends StatelessWidget {
LoadingIndicator({Key key}) : super(key: key);

@override
Widget build(BuildContext context) {
return Center(
child: CircularProgressIndicator(),
);
}
}
</pre>

Not much to discuss here; we’re just using a CircularProgressIndicator wrapped in a Center widget (again no bloc-specific code).

Lastly, we need to build our Stats widget.


Stats

The Stats widget is responsible for showing the user how many todos are active (in progress vs completed.

Let’s create widgets/stats.dart and take a look at the implementation.

<pre class="ql-syntax" spellcheck="false">import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:todos_app_core/todos_app_core.dart';
import 'package:flutter_todos/blocs/stats/stats.dart';
import 'package:flutter_todos/widgets/widgets.dart';
import 'package:flutter_todos/flutter_todos_keys.dart';

class Stats extends StatelessWidget {
Stats({Key key}) : super(key: key);

@override
Widget build(BuildContext context) {
final StatsBloc statsBloc = BlocProvider.of<StatsBloc>(context);
return BlocBuilder(
bloc: statsBloc,
builder: (BuildContext context, StatsState state) {
if (state is StatsLoading) {
return LoadingIndicator(key: FlutterTodosKeys.statsLoadingIndicator);
} else if (state is StatsLoaded) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Padding(
padding: EdgeInsets.only(bottom: 8.0),
child: Text(
ArchSampleLocalizations.of(context).completedTodos,
style: Theme.of(context).textTheme.title,
),
),
Padding(
padding: EdgeInsets.only(bottom: 24.0),
child: Text(
'${state.numCompleted}',
key: ArchSampleKeys.statsNumCompleted,
style: Theme.of(context).textTheme.subhead,
),
),
Padding(
padding: EdgeInsets.only(bottom: 8.0),
child: Text(
ArchSampleLocalizations.of(context).activeTodos,
style: Theme.of(context).textTheme.title,
),
),
Padding(
padding: EdgeInsets.only(bottom: 24.0),
child: Text(
"${state.numActive}",
key: ArchSampleKeys.statsNumActive,
style: Theme.of(context).textTheme.subhead,
),
)
],
),
);
} else {
return Container(key: FlutterTodosKeys.emptyStatsContainer);
}
},
);
}
}
</pre>

We’re accessing the StatsBloc using BlocProvider and using BlocBuilder to rebuild in response to state changes in the StatsBloc state.


Putting it all together

Let’s create main.dart and our TodosApp widget. We need to create a main function and run our TodosApp.

<pre class="ql-syntax" spellcheck="false">void main() {
BlocSupervisor().delegate = SimpleBlocDelegate();
runApp(TodosApp());
}
</pre>

Note: We are setting our BlocSupervisor’s delegate to the SimpleBlocDelegate we created earlier so that we can hook into all transitions and errors.

Next, let’s implement our TodosApp widget.

<pre class="ql-syntax" spellcheck="false">class TodosApp extends StatelessWidget {
final todosBloc = TodosBloc(
todosRepository: const TodosRepositoryFlutter(
fileStorage: const FileStorage(
'flutter_bloc_app',
getApplicationDocumentsDirectory,
),
),
);

@override
Widget build(BuildContext context) {
return BlocProvider(
bloc: todosBloc,
child: MaterialApp(
title: FlutterBlocLocalizations().appTitle,
theme: ArchSampleTheme.theme,
localizationsDelegates: [
ArchSampleLocalizationsDelegate(),
FlutterBlocLocalizationsDelegate(),
],
routes: {
ArchSampleRoutes.home: (context) {
return HomeScreen(
onInit: () => todosBloc.dispatch(LoadTodos()),
);
},
ArchSampleRoutes.addTodo: (context) {
return AddEditScreen(
key: ArchSampleKeys.addTodoScreen,
onSave: (task, note) {
todosBloc.dispatch(
AddTodo(Todo(task, note: note)),
);
},
isEditing: false,
);
},
},
),
);
}
}
</pre>

Our TodosApp is a stateless widget which creates a TodosBloc and makes it available through the entire application by using the BlocProvider widget from flutter_bloc.

The TodosApp has two routes:

  • Home - which renders a HomeScreen
  • AddTodo - which renders a AddEditScreen with isEditing set to false.

The entire main.dart should look like this:

<pre class="ql-syntax" spellcheck="false">import 'package:flutter/material.dart';
import 'package:bloc/bloc.dart';
import 'package:path_provider/path_provider.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:todos_repository_simple/todos_repository_simple.dart';
import 'package:todos_app_core/todos_app_core.dart';
import 'package:flutter_todos/localization.dart';
import 'package:flutter_todos/blocs/blocs.dart';
import 'package:flutter_todos/models/models.dart';
import 'package:flutter_todos/screens/screens.dart';

void main() {
BlocSupervisor().delegate = SimpleBlocDelegate();
runApp(TodosApp());
}

class TodosApp extends StatelessWidget {
final todosBloc = TodosBloc(
todosRepository: const TodosRepositoryFlutter(
fileStorage: const FileStorage(
'flutter_bloc_app',
getApplicationDocumentsDirectory,
),
),
);

@override
Widget build(BuildContext context) {
return BlocProvider(
bloc: todosBloc,
child: MaterialApp(
title: FlutterBlocLocalizations().appTitle,
theme: ArchSampleTheme.theme,
localizationsDelegates: [
ArchSampleLocalizationsDelegate(),
FlutterBlocLocalizationsDelegate(),
],
routes: {
ArchSampleRoutes.home: (context) {
return HomeScreen(
onInit: () => todosBloc.dispatch(LoadTodos()),
);
},
ArchSampleRoutes.addTodo: (context) {
return AddEditScreen(
key: ArchSampleKeys.addTodoScreen,
onSave: (task, note) {
todosBloc.dispatch(
AddTodo(Todo(task, note: note)),
);
},
isEditing: false,
);
},
},
),
);
}
}
</pre>

That’s all there is to it! We’ve now implemented a todos app in flutter using the bloc and flutter_bloc packages and we’ve successfully separated our presentation layer from our business logic.

The full source for this example can be found here.

If you enjoyed this exercise as much as I did you can support me by ⭐️the repository, or 👏 for this story.


Learn More

Getting started with Flutter

Flutter Tutorial - Flight List UI Example In Flutter

Let’s Develop a Mobile App in Flutter

A Design Pattern for Flutter

Weather App with “flutter_bloc”

How to integrate your iOS Flutter App with Firebase on MacOS

An introduction to Dart and Flutter

Learn Flutter & Dart to Build iOS & Android Apps

Flutter & Dart - The Complete Flutter App Development Course

Dart and Flutter: The Complete Developer’s Guide

Flutter - Advanced Course

Originally published by Felix Angelov at https://medium.com/flutter-community