create folder with app icon instead of default folder icon

I want to add my app Icon to created folder instead of default folder icon.

I want to add my app Icon to created folder instead of default folder icon.

I know i can create folder with following code.

String extr = Environment.getExternalStorageDirectory().toString();
File mFolder = new File(extr + "/My Application/Downloads");
if (!mFolder.exists()) {
    mFolder.mkdirs();
}

By doing this, i'm getting following result.

but what i want is..

I have searched a lot, but no luck. can anyone help me out?

Thank you.

Flutter: Adding Bluetooth Functionality

Flutter: Adding Bluetooth Functionality

This article will help you use Bluetooth functionality with Flutter.

This article will help you use Bluetooth functionality with Flutter.

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 :

dependencies:
flutter_bluetooth_serial: ^0.0.4

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

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 
    );
  }
}

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.

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
    );
  }
}

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().

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
    );
  }
}

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.

...
@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),
                    ),
                  ),
                ),
              )
            ],
          ),
        ),
      ),
    );
}

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

  ...
  // 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;
}

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.

...
// 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,
      ),
    );
  }
...

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.

  ...
  // 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');
      }
    });
  }
...

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 :

<manifest ...
    <!-- Add this line (inside manifest tag) -->
    xmlns:tools="http://schemas.android.com/tools">
    
    <!-- and this line (outside manifest tag) -->
    <uses-sdk tools:overrideLibrary="io.github.edufolly.flutterbluetoothserial"/>
    ....

</manifest>

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

Angular 7 + Spring Boot CRUD Example

Angular 7 + Spring Boot CRUD Example

In this tutorial we will be implementing CRUD operations using Angular 7 + Spring Boot.

In this tutorial we will be implementing CRUD operations using Angular 7 + Spring Boot.

In previous tutorial we had implemented - Angular 7 + Spring Boot Hello World Example. We exposed REST endpoint using Spring Boot and consumed this endpoint using Angular 7 application and displayed the data.

In this tutorial we will be implementing CRUD operations using Angular 7 + Spring Boot. We will also be adding the header with menu and footer to our application. So following will be the components on our page.

Angular 7 + Spring Boot CRUD Demo

Angular 7 + Spring Boot Application Hello World Example

Angular 7 + Spring Boot Application CRUD Example

Angular 7 + Spring Boot Application Login Example

The code we had developed in previous application for Angular 7 + Spring Boot Application Hello World Example will be the starting point for this tutorial.

Spring Boot Application

Previous application we had created a simple spring boot application which exposed a REST endpoint for fetching a list of employees. In this tutorial we will be adding 2 more REST endpoints - One for creating an employee and other for deleting it.

package com.javainuse.controllers;

import java.util.ArrayList;
import java.util.List;

import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.javainuse.model.Employee;

@CrossOrigin(origins = "http://localhost:4200")
@RestController
@RequestMapping({ "/employees" })
public class TestController {

	private List<Employee> employees = createList();

	@GetMapping(produces = "application/json")
	public List<Employee> firstPage() {
		return employees;
	}

	@DeleteMapping(path = { "/{id}" })
	public Employee delete(@PathVariable("id") int id) {
		Employee deletedEmp = null;
		for (Employee emp : employees) {
			if (emp.getEmpId().equals(id)) {
				employees.remove(emp);
				deletedEmp = emp;
				break;
			}
		}
		return deletedEmp;
	}

	@PostMapping
	public Employee create(@RequestBody Employee user) {
		employees.add(user);
		System.out.println(employees);
		return user;
	}

	private static List<Employee> createList() {
		List<Employee> tempEmployees = new ArrayList<>();
		Employee emp1 = new Employee();
		emp1.setName("emp1");
		emp1.setDesignation("manager");
		emp1.setEmpId("1");
		emp1.setSalary(3000);

		Employee emp2 = new Employee();
		emp2.setName("emp2");
		emp2.setDesignation("developer");
		emp2.setEmpId("2");
		emp2.setSalary(3000);
		tempEmployees.add(emp1);
		tempEmployees.add(emp2);
		return tempEmployees;
	}

}

Angular 7 development

Modify existing HtppClient Service

We modify the HttpClient service to add methods for performing add new employee and deleting employee in addition to getting the list of employees using the httpClient.

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';

export class Employee{
  constructor(
    public empId:string,
    public name:string,
    public designation:string,
    public salary:string,
  ) {}
  
}

@Injectable({
  providedIn: 'root'
})
export class HttpClientService {

  constructor(
    private httpClient:HttpClient
  ) { 
     }

     getEmployees()
  {
    console.log("test call");
    return this.httpClient.get<Employee[]>('http://localhost:8080/employees');
  }

  public deleteEmployee(employee) {
    return this.httpClient.delete<Employee>("http://localhost:8080/employees" + "/"+ employee.empId);
  }

  public createEmployee(employee) {
    return this.httpClient.post<Employee>("http://localhost:8080/employees", employee);
  }
}

Adding BootStrap CSS

In the style.css add the bootstrap css url-

/* You can add global styles to this file, and also import other style files */
@import url(https://unpkg.com/[email protected]/dist/css/bootstrap.min.css)

Modify existing employee component to add delete functionality

In the employee.component.ts file add a call for deleting the employee in addition to getting the list of employees.

import { Component, OnInit } from '@angular/core';
import { HttpClientService, Employee } from '../service/http-client.service';

@Component({
  selector: 'app-employee',
  templateUrl: './employee.component.html',
  styleUrls: ['./employee.component.css']
})
export class EmployeeComponent implements OnInit {

  employees: Employee[];
   
  constructor(
    private httpClientService: HttpClientService
  ) { }

  ngOnInit() {
    this.httpClientService.getEmployees().subscribe(
     response =>{this.employees = response;}
    );
  }

  deleteEmployee(employee: Employee): void {
    this.httpClientService.deleteEmployee(employee)
      .subscribe( data => {
        this.employees = this.employees.filter(u => u !== employee);
      })
  };
}

We will be displaying the list of employees in employee.component.html and give users option to delete the employee.

<div class="col-md-6">
  <table class="table table-striped">
  <thead>
  <tr>
    <th>name</th>
    <th>designation</th>
    </tr>
  </thead>
  <tbody>
      <tr *ngFor="let employee of employees">
          <td>{{employee.name}}</td>
          <td>{{employee.designation}}</td>
          <td><button class="btn btn-danger" (click)="deleteEmployee(employee)"> Delete Employee</button></td>
          </tr>
  </tbody>
    </table>

  </div>

Now if we go to localhost:4200

Creating the add employee component

We will be creating a new component named add-employee

ng generate component add-employee

Modify the add-employee.component.ts to make call to spring boot for creating new employee using the httpClientService which is injected in the class using constructor injection.

import { Component, OnInit } from '@angular/core';
import { HttpClientService, Employee } from '../service/http-client.service';

@Component({
  selector: 'app-add-employee',
  templateUrl: './add-employee.component.html',
  styleUrls: ['./add-employee.component.css']
})
export class AddEmployeeComponent implements OnInit {

  user: Employee = new Employee("","","","");

  constructor(
    private httpClientService: HttpClientService
  ) { }

  ngOnInit() {
  }

  createEmployee(): void {
    this.httpClientService.createEmployee(this.user)
        .subscribe( data => {
          alert("Employee created successfully.");
        });

  };

}
	

Modify the add-employee.component.html to create form for getting the new employee details for the employee object to be created.

<div class="col-md-6">
<h2 class="text-center">Add Employee</h2>
<form>
  <div class="form-group">
    <label for="name">Name:</label>
    <input type="name" [(ngModel)]="user.name" placeholder="Name" name="name" class="form-control" id="name">
  </div>

  <div class="form-group">
    <label for="designation">Designation:</label>
    <input [(ngModel)]="user.designation" placeholder="Designation" name="designation" class="form-control" id="designation">
  </div>

  <div class="form-group">
    <label for="empId">Employee Id</label>
    <input [(ngModel)]="user.empId" placeholder="Employee Id" name="Employee Id" class="form-control" id="employeeid">
  </div>

  <button class="btn btn-success" (click)="createEmployee()">Create</button>
</form>
</div>

Since we are using ngModel directive, we will need to add the FormsModule in the app.module.ts- If this is not done you will get an exception as follows - Can’t bind to ‘ngModel’ since it isn’t a known property of ‘input’. ("

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

import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { EmployeeComponent } from './employee/employee.component';
import { HttpClientModule } from '@angular/common/http';
import { AddEmployeeComponent } from './add-employee/add-employee.component';
import { FormsModule } from '@angular/forms';
import { HeaderComponent } from './header/header.component';
import { FooterComponent } from './footer/footer.component';

@NgModule({
  declarations: [
    AppComponent,
    EmployeeComponent,
    AddEmployeeComponent,
    HeaderComponent,
    FooterComponent
  ],
  imports: [
    BrowserModule,
    AppRoutingModule,
    HttpClientModule,
    FormsModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

In the app.routing.js add the route for this add employee component

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { EmployeeComponent } from './employee/employee.component';
import { AddEmployeeComponent } from './add-employee/add-employee.component';

const routes: Routes = [
  { path:'', component: EmployeeComponent},
  { path:'addemployee', component: AddEmployeeComponent},
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }

Go to localhost:8080/addemployee

Creating the header menu

Create new header component-

ng generate component header

Modify the header.component.html to create the menu with links to the view employees and add new employee pages.

  <header>
  <nav class ="navbar navbar-expand-md navbar-dark bg-dark">
  <div><a href="https://www.javainuse.com" class="navbar-brand">JavaInUse</a></div>
  
  <ul class="navbar-nav">
    <li><a  routerLink="/" class="nav-link">View Employees</a></li>
    <li><a  routerLink="/addemployee" class="nav-link">Add New Employee</a></li>
  
  </ul>
  
  </nav>
  
  </header>

Create Footer Component

Create a new component named footer-

ng generate component footer

In the footer specify the data-

<footer class="footer">
  <div class="container">
      <span class="text-muted">All Rights Reserved 2019 @JavaInUse</span>
  </div>

</footer>

Modify the footer.component.css-

    .footer {
    position: absolute;
    bottom: 0;
    width:100%;
    height: 40px;
    background-color: #222222;
}

  • Modify the app.component.html to add the footer and menu to the application byadding the selector tags.
<app-header></app-header>
<router-outlet></router-outlet> 
<app-footer></app-footer>

Go to localhost:4200

Download Source Code

Download it -

GITHUB- Angular 7 CRUD example code

Spring Boot CRUD example code

Popular Posts

Full Stack Web Development with Angular and Spring MVC

Building Web App using ASP.NET Web API Angular 7 and SQL Server

Angular 7 Tutorial - Learn Angular 7 by Example

Build a Basic App with Spring Boot and JPA using PostgreSQL

Build a Simple CRUD App with Spring Boot and Vue.js

Build a Basic CRUD App with Laravel and Angular

AngularJS tutorial for beginners with NodeJS, ExpressJS and MongoDB

MEAN Stack Tutorial MongoDB, ExpressJS, AngularJS and NodeJS

Build a CRUD App with Angular and Firebase

Angular Authentication Tutorial

Angular 7 (formerly Angular 2) - The Complete Guide

Angular & NodeJS - The MEAN Stack Guide

Learn and Understand AngularJS

Angular Crash Course for Busy Developers

The Complete Angular Course: Beginner to Advanced

Angular (Full App) with Angular Material, Angularfire & NgRx

The Web Developer Bootcamp

Securing RESTful API with Spring Boot, Security, and Data MongoDB

Securing RESTful API with Spring Boot, Security, and Data MongoDB

A comprehensive step by step tutorial on securing or authentication RESTful API with Spring Boot, Security, and Data MongoDB

A comprehensive step by step tutorial on securing or authentication RESTful API with Spring Boot, Security, and Data MongoDB. Previously, we have shown you how to securing Spring Boot, MVC and MongoDB web application. In this tutorial, the secure endpoint will restrict the access from an unauthorized request. Every request to secure endpoint should bring authorization token with it. Of course, there will be an endpoint for login which will get authorization token after successful login.

Table of Contents:

The following software, tools, and frameworks are required for this tutorial:

We assume that you already installed all required software, tools, and frameworks. So, we will not cover how to install that software, tools, and frameworks.

1. Generate a New Spring Boot Gradle Project

To create or generate a new Spring Boot Application or Project, simply go to Spring Initializer. Fill all required fields as below then click on Generate Project button.

The project will automatically be downloaded as a Zip file. Next, extract the zipped project to your java projects folder. On the project folder root, you will find build.gradle file for register dependencies, initially it looks like this.

buildscript {
&nbsp;&nbsp; &nbsp;ext {
&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;springBootVersion = '2.1.2.RELEASE'
&nbsp;&nbsp; &nbsp;}
&nbsp;&nbsp; &nbsp;repositories {
&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;mavenCentral()
&nbsp;&nbsp; &nbsp;}
&nbsp;&nbsp; &nbsp;dependencies {
&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
&nbsp;&nbsp; &nbsp;}
}

apply plugin: 'java'
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'

group = 'com.djamware'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '1.8'

repositories {
&nbsp;&nbsp; &nbsp;mavenCentral()
}

dependencies {
&nbsp;&nbsp; &nbsp;implementation 'org.springframework.boot:spring-boot-starter-data-mongodb'
&nbsp;&nbsp; &nbsp;implementation 'org.springframework.boot:spring-boot-starter-security'
&nbsp;&nbsp; &nbsp;implementation 'org.springframework.boot:spring-boot-starter-web'
&nbsp;&nbsp; &nbsp;testImplementation 'org.springframework.boot:spring-boot-starter-test'
&nbsp;&nbsp; &nbsp;testImplementation 'org.springframework.security:spring-security-test'
}

Now, you can work with the source code of this Spring Boot Project using your own IDE or Text Editor. We are using Spring Tool Suite (STS). In STS, import the extracted zipped file as Existing Gradle Project.

Next, we have to add the JWT library to the build.gradle as the dependency. Open and edit build.gradle then add this line to dependencies after other implementation.

implementation 'io.jsonwebtoken:jjwt:0.9.1'

Next, compile the Gradle Project by type this command from Terminal or CMD.

./gradlew compile

Or you can compile directly from STS by right-clicking the project name then choose Gradle -> Refresh Gradle Project. Next, open and edit src/main/resources/application.properties then add these lines.

spring.data.mongodb.database=springmongodb
spring.data.mongodb.host=localhost
spring.data.mongodb.port=27017

2. Create Product, User and Role Model or Entity Classes

We will be creating all required models or entities for products, user and role. In STS, right-click the project name -> New -> Class. Fill the package with com.djamware.SecurityRest.models, the name with Product, and leave other fields and checkbox as default then click Finish Button.

Next, open and edit src/main/java/com/djamware/SecurityRest/models/Product.java then add this annotation above the class name that will point to MongoDB collection.

@Document(collection = "products")

Inside Product class, add these variables.

@Id
String id;
String prodName;
String prodDesc;
Double prodPrice;
String prodImage;

Add constructors after the variable or fields.

public Product() {
}

public Product(String prodName, String prodDesc, Double prodPrice, String prodImage) {
&nbsp;&nbsp; &nbsp;super();
&nbsp;&nbsp; &nbsp;this.prodName = prodName;
&nbsp;&nbsp; &nbsp;this.prodDesc = prodDesc;
&nbsp;&nbsp; &nbsp;this.prodPrice = prodPrice;
&nbsp;&nbsp; &nbsp;this.prodImage = prodImage;
}

Generate or create Getter and Setter for each field.

public String getId() {
&nbsp;&nbsp; &nbsp;return id;
}

public void setId(String id) {
&nbsp;&nbsp; &nbsp;this.id = id;
}

public String getProdName() {
&nbsp;&nbsp; &nbsp;return prodName;
}

public void setProdName(String prodName) {
&nbsp;&nbsp; &nbsp;this.prodName = prodName;
}

public String getProdDesc() {
&nbsp;&nbsp; &nbsp;return prodDesc;
}

public void setProdDesc(String prodDesc) {
&nbsp;&nbsp; &nbsp;this.prodDesc = prodDesc;
}

public Double getProdPrice() {
&nbsp;&nbsp; &nbsp;return prodPrice;
}

public void setProdPrice(Double prodPrice) {
&nbsp;&nbsp; &nbsp;this.prodPrice = prodPrice;
}

public String getProdImage() {
&nbsp;&nbsp; &nbsp;return prodImage;
}

public void setProdImage(String prodImage) {
&nbsp;&nbsp; &nbsp;this.prodImage = prodImage;
}

Using STS you can organize imports automatically from the menu Source -> Organize Imports then you can see the imports after the package name.

package com.djamware.SecurityRest.models;

import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;

You can do the same way as the above step for User and Role class. Here’s the User class looks like.

package com.djamware.SecurityRest.models;

import java.util.Set;

import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.index.IndexDirection;
import org.springframework.data.mongodb.core.index.Indexed;
import org.springframework.data.mongodb.core.mapping.DBRef;
import org.springframework.data.mongodb.core.mapping.Document;

@Document(collection = "users")
public class User {

&nbsp;&nbsp; &nbsp;@Id
&nbsp;&nbsp; &nbsp;private String id;
&nbsp;&nbsp; &nbsp;@Indexed(unique = true, direction = IndexDirection.DESCENDING, dropDups = true)
&nbsp;&nbsp; &nbsp;private String email;
&nbsp;&nbsp; &nbsp;private String password;
&nbsp;&nbsp; &nbsp;private String fullname;
&nbsp;&nbsp; &nbsp;private boolean enabled;
&nbsp;&nbsp; &nbsp;@DBRef
&nbsp;&nbsp; &nbsp;private Set<Role> roles;
&nbsp;&nbsp; &nbsp;public String getId() {
&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;return id;
&nbsp;&nbsp; &nbsp;}
&nbsp;&nbsp; &nbsp;public void setId(String id) {
&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;this.id = id;
&nbsp;&nbsp; &nbsp;}
&nbsp;&nbsp; &nbsp;public String getEmail() {
&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;return email;
&nbsp;&nbsp; &nbsp;}
&nbsp;&nbsp; &nbsp;public void setEmail(String email) {
&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;this.email = email;
&nbsp;&nbsp; &nbsp;}
&nbsp;&nbsp; &nbsp;public String getPassword() {
&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;return password;
&nbsp;&nbsp; &nbsp;}
&nbsp;&nbsp; &nbsp;public void setPassword(String password) {
&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;this.password = password;
&nbsp;&nbsp; &nbsp;}
&nbsp;&nbsp; &nbsp;public String getFullname() {
&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;return fullname;
&nbsp;&nbsp; &nbsp;}
&nbsp;&nbsp; &nbsp;public void setFullname(String fullname) {
&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;this.fullname = fullname;
&nbsp;&nbsp; &nbsp;}
&nbsp;&nbsp; &nbsp;public boolean isEnabled() {
&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;return enabled;
&nbsp;&nbsp; &nbsp;}
&nbsp;&nbsp; &nbsp;public void setEnabled(boolean enabled) {
&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;this.enabled = enabled;
&nbsp;&nbsp; &nbsp;}
&nbsp;&nbsp; &nbsp;public Set<Role> getRoles() {
&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;return roles;
&nbsp;&nbsp; &nbsp;}
&nbsp;&nbsp; &nbsp;public void setRoles(Set<Role> roles) {
&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;this.roles = roles;
&nbsp;&nbsp; &nbsp;}

}

And the Role class will be like this.

package com.djamware.SecurityRest.models;

import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.index.IndexDirection;
import org.springframework.data.mongodb.core.index.Indexed;
import org.springframework.data.mongodb.core.mapping.Document;

@Document(collection = "roles")
public class Role {

&nbsp;&nbsp; &nbsp;@Id
&nbsp; &nbsp; private String id;
&nbsp; &nbsp; @Indexed(unique = true, direction = IndexDirection.DESCENDING, dropDups = true)

&nbsp; &nbsp; private String role;
&nbsp;&nbsp; &nbsp;public String getId() {
&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;return id;
&nbsp;&nbsp; &nbsp;}
&nbsp;&nbsp; &nbsp;public void setId(String id) {
&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;this.id = id;
&nbsp;&nbsp; &nbsp;}
&nbsp;&nbsp; &nbsp;public String getRole() {
&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;return role;
&nbsp;&nbsp; &nbsp;}
&nbsp;&nbsp; &nbsp;public void setRole(String role) {
&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;this.role = role;
&nbsp;&nbsp; &nbsp;}

}

3. Create Product, User and Role Repository Interfaces

Next steps to create Product, User, and Role Repository Interfaces. From the STS, right-click the project name -> New -> Interface then fill all required fields and checkboxes as below before click Finish button.

Next, open and edit src/main/java/com/djamware/SecurityRest/repositories/ProductRepository.java then add extends to MongoDB CRUD Repository.

public interface ProductRepository extends CrudRepository<Product, String> {

}

Inside the class name add this method.

@Override
void delete(Product deleted);

Organize all required imports.

import org.springframework.data.repository.CrudRepository;
import com.djamware.SecurityRest.models.Product;

The same way can be applied to User and Role repositories. So, the User Repository Interface will look like this.

package com.djamware.SecurityRest.repositories;

import org.springframework.data.mongodb.repository.MongoRepository;
import com.djamware.SecurityRest.models.User;

public interface UserRepository extends MongoRepository<User, String> {

&nbsp;&nbsp; &nbsp;User findByEmail(String email);
}

And the Role Repository Interface will look like this.

package com.djamware.SecurityRest.repositories;

import org.springframework.data.mongodb.repository.MongoRepository;
import com.djamware.SecurityRest.models.Role;

public interface RoleRepository extends MongoRepository<Role, String> {

&nbsp;&nbsp; &nbsp;Role findByRole(String role);
}

4. Create a Custom User Details Service

To implements authentication using existing User and Role from MongoDB, we have to create a custom user details service. From the STS, right-click the project name -> New -> Class File then fill all required fields and checkboxes as below before clicking the finish button.

Next, open and edit src/main/java/com/djamware/SecurityRest/services/CustomUserDetailsService.java then give an annotation above the class name and implement the Spring Security User Details Service.

@Service
public class CustomUserDetailsService implements UserDetailsService {
}

Next, inject all required beans at the first line of the class bracket.

@Autowired
private UserRepository userRepository;

@Autowired
private RoleRepository roleRepository;

@Autowired
private PasswordEncoder bCryptPasswordEncoder;

Add a method to find a user by email field.

public User findUserByEmail(String email) {
&nbsp; &nbsp; return userRepository.findByEmail(email);
}

Add a method to save a new user.

public void saveUser(User user) {
&nbsp; &nbsp; user.setPassword(bCryptPasswordEncoder.encode(user.getPassword()));
&nbsp; &nbsp; user.setEnabled(true);
&nbsp; &nbsp; Role userRole = roleRepository.findByRole("ADMIN");
&nbsp; &nbsp; user.setRoles(new HashSet<>(Arrays.asList(userRole)));
&nbsp; &nbsp; userRepository.save(user);
}

Override the Spring Security User Details to load User by email.

@Override
public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {

&nbsp; &nbsp; User user = userRepository.findByEmail(email);
&nbsp; &nbsp; if(user != null) {
&nbsp; &nbsp; &nbsp; &nbsp; List<GrantedAuthority> authorities = getUserAuthority(user.getRoles());
&nbsp; &nbsp; &nbsp; &nbsp; return buildUserForAuthentication(user, authorities);
&nbsp; &nbsp; } else {
&nbsp; &nbsp; &nbsp; &nbsp; throw new UsernameNotFoundException("username not found");
&nbsp; &nbsp; }
}

Add a method to get a set of Roles that related to a user.

private List<GrantedAuthority> getUserAuthority(Set<Role> userRoles) {
&nbsp; &nbsp; Set<GrantedAuthority> roles = new HashSet<>();
&nbsp; &nbsp; userRoles.forEach((role) -> {
&nbsp; &nbsp; &nbsp; &nbsp; roles.add(new SimpleGrantedAuthority(role.getRole()));
&nbsp; &nbsp; });

&nbsp; &nbsp; List<GrantedAuthority> grantedAuthorities = new ArrayList<>(roles);
&nbsp; &nbsp; return grantedAuthorities;
}

Add a method for authentication purpose.

private UserDetails buildUserForAuthentication(User user, List<GrantedAuthority> authorities) {
&nbsp; &nbsp; return new org.springframework.security.core.userdetails.User(user.getEmail(), user.getPassword(), authorities);
}

5. Configure Spring Boot Security Rest

Now, the main purpose of this tutorial is configuring Spring Security Rest. First, we have to create a bean for JWT token generation and validation. Right-click the project name -> New -> Class File. Fill the package name as com.djamware.SecurityRest.configs and the Class name as JwtTokenProvider then click the Finish button. Next, open and edit that newly created class file then give it an annotation above the class name.

@Component
public class JwtTokenProvider {
}

Add variables and injected bean inside the class bracket at the top lines.

@Value("${security.jwt.token.secret-key:secret}")
private String secretKey = "secret";

@Value("${security.jwt.token.expire-length:3600000}")
private long validityInMilliseconds = 3600000; // 1h

@Autowired
private CustomUserDetailsService userDetailsService;

Add a method for initialization.

@PostConstruct
protected void init() {
&nbsp; &nbsp; secretKey = Base64.getEncoder().encodeToString(secretKey.getBytes());
}

Add a method to create a JWT token.

public String createToken(String username, Set<Role> set) {
&nbsp; &nbsp; Claims claims = Jwts.claims().setSubject(username);
&nbsp; &nbsp; claims.put("roles", set);
&nbsp; &nbsp; Date now = new Date();
&nbsp; &nbsp; Date validity = new Date(now.getTime() + validityInMilliseconds);
&nbsp; &nbsp; return Jwts.builder()//
&nbsp; &nbsp; &nbsp; &nbsp; .setClaims(claims)//
&nbsp; &nbsp; &nbsp; &nbsp; .setIssuedAt(now)//
&nbsp; &nbsp; &nbsp; &nbsp; .setExpiration(validity)//
&nbsp; &nbsp; &nbsp; &nbsp; .signWith(SignatureAlgorithm.HS256, secretKey)//
&nbsp; &nbsp; &nbsp; &nbsp; .compact();
}

Add a method to load User by username.

public Authentication getAuthentication(String token) {
&nbsp; &nbsp; UserDetails userDetails = this.userDetailsService.loadUserByUsername(getUsername(token));
&nbsp; &nbsp; return new UsernamePasswordAuthenticationToken(userDetails, "", userDetails.getAuthorities());
}

Add a method to get the username by JWT token.

public String getUsername(String token) {
&nbsp; &nbsp; return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).getBody().getSubject();
}

Add a method to resolve JWT token from request headers of Authorization that has a Bearer prefix.

public String resolveToken(HttpServletRequest req) {
&nbsp; &nbsp; String bearerToken = req.getHeader("Authorization");
&nbsp; &nbsp; if (bearerToken != null && bearerToken.startsWith("Bearer ")) {
&nbsp; &nbsp; &nbsp; &nbsp; return bearerToken.substring(7, bearerToken.length());
&nbsp; &nbsp; }
&nbsp; &nbsp; return null;
}

Add a method to validate a JWT token.

public boolean validateToken(String token) {
&nbsp; &nbsp; try {
&nbsp; &nbsp; &nbsp; &nbsp; Jws<Claims> claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token);
&nbsp; &nbsp; &nbsp; &nbsp; if (claims.getBody().getExpiration().before(new Date())) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; return false;
&nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp; return true;
&nbsp; &nbsp; } catch (JwtException | IllegalArgumentException e) {
&nbsp; &nbsp; &nbsp; &nbsp; throw new JwtException("Expired or invalid JWT token");
&nbsp; &nbsp; }
}

Finally, organize imports like below.

package com.djamware.SecurityRest.configs;

import java.util.Base64;
import java.util.Date;
import java.util.Set;

import javax.annotation.PostConstruct;
import javax.servlet.http.HttpServletRequest;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;

import com.djamware.SecurityRest.models.Role;
import com.djamware.SecurityRest.services.CustomUserDetailsService;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.JwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;

Next, create a JWT filter class with the name JwtTokenFilter in configs package that extends Spring GenericFilterBean. Replace all Java codes with these lines of codes.

package com.djamware.SecurityRest.configs;

import java.io.IOException;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;

import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.GenericFilterBean;

public class JwtTokenFilter extends GenericFilterBean {

&nbsp;&nbsp; &nbsp;private JwtTokenProvider jwtTokenProvider;

&nbsp; &nbsp; public JwtTokenFilter(JwtTokenProvider jwtTokenProvider) {
&nbsp; &nbsp; &nbsp; &nbsp; this.jwtTokenProvider = jwtTokenProvider;
&nbsp; &nbsp; }

&nbsp; &nbsp; @Override
&nbsp; &nbsp; public void doFilter(ServletRequest req, ServletResponse res, FilterChain filterChain)
&nbsp; &nbsp; &nbsp; &nbsp; throws IOException, ServletException {
&nbsp; &nbsp; &nbsp; &nbsp; String token = jwtTokenProvider.resolveToken((HttpServletRequest) req);
&nbsp; &nbsp; &nbsp; &nbsp; if (token != null && jwtTokenProvider.validateToken(token)) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; Authentication auth = token != null ? jwtTokenProvider.getAuthentication(token) : null;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; SecurityContextHolder.getContext().setAuthentication(auth);
&nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp; filterChain.doFilter(req, res);
&nbsp; &nbsp; }
}

Next, create a class with the name JwtConfigurer for JWT configuration in configs package then replace all codes with these lines of codes.

package com.djamware.SecurityRest.configs;

import org.springframework.security.config.annotation.SecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.DefaultSecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

public class JwtConfigurer extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {

&nbsp;&nbsp; &nbsp;private JwtTokenProvider jwtTokenProvider;

&nbsp; &nbsp; public JwtConfigurer(JwtTokenProvider jwtTokenProvider) {
&nbsp; &nbsp; &nbsp; &nbsp; this.jwtTokenProvider = jwtTokenProvider;
&nbsp; &nbsp; }

&nbsp; &nbsp; @Override
&nbsp; &nbsp; public void configure(HttpSecurity http) throws Exception {
&nbsp; &nbsp; &nbsp; &nbsp; JwtTokenFilter customFilter = new JwtTokenFilter(jwtTokenProvider);
&nbsp; &nbsp; &nbsp; &nbsp; http.addFilterBefore(customFilter, UsernamePasswordAuthenticationFilter.class);
&nbsp; &nbsp; }
}

Finally, we have to configure the Spring Security by creating a Java class file inside configs package with the name WebSecurityConfig. Give annotations to this class and extends Spring WebSecurityConfigurerAdapter.

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
}

Inject JWT token provider inside this class.

@Autowired
JwtTokenProvider jwtTokenProvider;

Add an override method to configure Authentication Manager Builder.

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
&nbsp;&nbsp; &nbsp;UserDetailsService userDetailsService = mongoUserDetails();
&nbsp;&nbsp; &nbsp;auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder());

}

Next, add an override method to configure Spring Security Http Security.

@Override
protected void configure(HttpSecurity http) throws Exception {
&nbsp;&nbsp; &nbsp;http.httpBasic().disable().csrf().disable().sessionManagement()
&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;.sessionCreationPolicy(SessionCreationPolicy.STATELESS).and().authorizeRequests()
&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;.antMatchers("/api/auth/login").permitAll().antMatchers("/api/auth/register").permitAll()
&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;.antMatchers("/api/products/**").hasAuthority("ADMIN").anyRequest().authenticated().and().csrf()
&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;.disable().exceptionHandling().authenticationEntryPoint(unauthorizedEntryPoint()).and()
&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;.apply(new JwtConfigurer(jwtTokenProvider));
}

Next, declare all required beans for this configuration.

@Bean
public PasswordEncoder bCryptPasswordEncoder() {
&nbsp;&nbsp; &nbsp;return new BCryptPasswordEncoder();
}

@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
&nbsp;&nbsp; &nbsp;return super.authenticationManagerBean();
}

@Bean
public AuthenticationEntryPoint unauthorizedEntryPoint() {
&nbsp;&nbsp; &nbsp;return (request, response, authException) -> response.sendError(HttpServletResponse.SC_UNAUTHORIZED,
&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;"Unauthorized");
}

@Bean
public UserDetailsService mongoUserDetails() {
&nbsp;&nbsp; &nbsp;return new CustomUserDetailsService();
}

6. Create Product and Authentication Controllers

Now it’s time for REST API endpoint. All RESTful API will be created from each controller. Product controller will handle CRUD endpoint of product and Authentication controller will handle login and register endpoint. Right-click project name -> New -> Class then fill the package with com.djamware.SecurityRest.controllers and the class name as ProductController. Open and edit the newly created class file then replace all codes with these lines of codes.

package com.djamware.SecurityRest.controllers;

import java.util.Optional;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

import com.djamware.SecurityRest.models.Product;
import com.djamware.SecurityRest.repositories.ProductRepository;

@RestController
public class ProductController {

&nbsp;&nbsp; &nbsp;@Autowired
&nbsp; &nbsp; ProductRepository productRepository;

&nbsp; &nbsp; @RequestMapping(method=RequestMethod.GET, value="/api/products")
&nbsp; &nbsp; public Iterable<Product> product() {
&nbsp; &nbsp; &nbsp; &nbsp; return productRepository.findAll();
&nbsp; &nbsp; }

&nbsp; &nbsp; @RequestMapping(method=RequestMethod.POST, value="/api/products")
&nbsp; &nbsp; public String save(@RequestBody Product product) {
&nbsp; &nbsp; &nbsp; &nbsp; productRepository.save(product);

&nbsp; &nbsp; &nbsp; &nbsp; return product.getId();
&nbsp; &nbsp; }

&nbsp; &nbsp; @RequestMapping(method=RequestMethod.GET, value="/api/products/{id}")
&nbsp; &nbsp; public Optional<Product> show(@PathVariable String id) {
&nbsp; &nbsp; &nbsp; &nbsp; return productRepository.findById(id);
&nbsp; &nbsp; }

&nbsp; &nbsp; @RequestMapping(method=RequestMethod.PUT, value="/api/products/{id}")
&nbsp; &nbsp; public Product update(@PathVariable String id, @RequestBody Product product) {
&nbsp; &nbsp; &nbsp;&nbsp; &nbsp;Optional<Product> prod = productRepository.findById(id);
&nbsp; &nbsp; &nbsp; &nbsp; if(product.getProdName() != null)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; prod.get().setProdName(product.getProdName());
&nbsp; &nbsp; &nbsp; &nbsp; if(product.getProdDesc() != null)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; prod.get().setProdDesc(product.getProdDesc());
&nbsp; &nbsp; &nbsp; &nbsp; if(product.getProdPrice() != null)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; prod.get().setProdPrice(product.getProdPrice());
&nbsp; &nbsp; &nbsp; &nbsp; if(product.getProdImage() != null)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; prod.get().setProdImage(product.getProdImage());
&nbsp; &nbsp; &nbsp; &nbsp; productRepository.save(prod.get());
&nbsp; &nbsp; &nbsp; &nbsp; return prod.get();
&nbsp; &nbsp; }

&nbsp; &nbsp; @RequestMapping(method=RequestMethod.DELETE, value="/api/products/{id}")
&nbsp; &nbsp; public String delete(@PathVariable String id) {
&nbsp; &nbsp; &nbsp; &nbsp; Optional<Product> product = productRepository.findById(id);
&nbsp; &nbsp; &nbsp; &nbsp; productRepository.delete(product.get());

&nbsp; &nbsp; &nbsp; &nbsp; return "product deleted";
&nbsp; &nbsp; }
}

For login, we need to create a POJO to mapping required fields of User. Create a new class file with the name AuthBody inside controllers package then replace all Java codes with these lines of codes.

package com.djamware.SecurityRest.controllers;

public class AuthBody {

&nbsp;&nbsp; &nbsp;private String email;
&nbsp; &nbsp; private String password;

&nbsp;&nbsp; &nbsp;public String getEmail() {
&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;return email;
&nbsp;&nbsp; &nbsp;}
&nbsp;&nbsp; &nbsp;public void setEmail(String email) {
&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;this.email = email;
&nbsp;&nbsp; &nbsp;}
&nbsp;&nbsp; &nbsp;public String getPassword() {
&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;return password;
&nbsp;&nbsp; &nbsp;}
&nbsp;&nbsp; &nbsp;public void setPassword(String password) {
&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;this.password = password;
&nbsp;&nbsp; &nbsp;}

}

Finally, create a controller for authentication with the name AuthController inside the controllers’ package. Open and edit that newly created file then replace all Java codes with these lines of codes.

package com.djamware.SecurityRest.controllers;

import static org.springframework.http.ResponseEntity.ok;

import java.util.HashMap;
import java.util.Map;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.AuthenticationException;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.djamware.SecurityRest.configs.JwtTokenProvider;
import com.djamware.SecurityRest.models.User;
import com.djamware.SecurityRest.repositories.UserRepository;
import com.djamware.SecurityRest.services.CustomUserDetailsService;

@RestController
@RequestMapping("/api/auth")
public class AuthController {

&nbsp;&nbsp; &nbsp;@Autowired
&nbsp;&nbsp; &nbsp;AuthenticationManager authenticationManager;

&nbsp;&nbsp; &nbsp;@Autowired
&nbsp;&nbsp; &nbsp;JwtTokenProvider jwtTokenProvider;

&nbsp;&nbsp; &nbsp;@Autowired
&nbsp;&nbsp; &nbsp;UserRepository users;

&nbsp;&nbsp; &nbsp;@Autowired
&nbsp;&nbsp; &nbsp;private CustomUserDetailsService userService;

&nbsp;&nbsp; &nbsp;@SuppressWarnings("rawtypes")
&nbsp;&nbsp; &nbsp;@PostMapping("/login")
&nbsp;&nbsp; &nbsp;public ResponseEntity login(@RequestBody AuthBody data) {
&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;try {
&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;String username = data.getEmail();
&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, data.getPassword()));
&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;String token = jwtTokenProvider.createToken(username, this.users.findByEmail(username).getRoles());
&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;Map<Object, Object> model = new HashMap<>();
&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;model.put("username", username);
&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;model.put("token", token);
&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;return ok(model);
&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;} catch (AuthenticationException e) {
&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;throw new BadCredentialsException("Invalid email/password supplied");
&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;}
&nbsp;&nbsp; &nbsp;}

&nbsp;&nbsp; &nbsp;@SuppressWarnings("rawtypes")
&nbsp;&nbsp; &nbsp;@PostMapping("/register")
&nbsp;&nbsp; &nbsp;public ResponseEntity register(@RequestBody User user) {
&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;User userExists = userService.findUserByEmail(user.getEmail());
&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;if (userExists != null) {
&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;throw new BadCredentialsException("User with username: " + user.getEmail() + " already exists");
&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;}
&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;userService.saveUser(user);
&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;Map<Object, Object> model = new HashMap<>();
&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;model.put("message", "User registered successfully");
&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;return ok(model);
&nbsp;&nbsp; &nbsp;}
}

7. Run and Test Spring Boot Security Rest using Postman

Before run and test the application, we have to populate a Role data first. Open and edit src/main/java/com/djamware/SecurityRest/SecurityRestApplication.java then add these lines of codes inside the initialization method.

@Bean
CommandLineRunner init(RoleRepository roleRepository) {

&nbsp; &nbsp; return args -> {

&nbsp; &nbsp; &nbsp; &nbsp; Role adminRole = roleRepository.findByRole("ADMIN");
&nbsp; &nbsp; &nbsp; &nbsp; if (adminRole == null) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; Role newAdminRole = new Role();
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; newAdminRole.setRole("ADMIN");
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; roleRepository.save(newAdminRole);
&nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; };

}

Next, make sure you have run the MongoDB server on your local machine then run the Gradle application using this command.

./gradlew bootRun

Or in STS just right-click the project name -> Run As -> Spring Boot App. Next, open the Postman application then change the method to GET and address to localhost:8080/api/products then click Send button.

You will see this response in the bottom panel of Postman.

{
&nbsp; &nbsp; "timestamp": "2019-03-07T13:16:34.935+0000",
&nbsp; &nbsp; "status": 401,
&nbsp; &nbsp; "error": "Unauthorized",
&nbsp; &nbsp; "message": "Unauthorized",
&nbsp; &nbsp; "path": "/api/products"
}

Next, change the method to POST then address to localhost:8080/api/auth/register then fill the body with raw data as below image then click Send button.

You will get the response in the bottom panel of Postman.

{
&nbsp; &nbsp; "message": "User registered successfully"
}

Next, change the address to localhost:8080/api/auth/login and change the body as below then click Send button.

{ "email":"[email&nbsp;protected]", "password": "q1w2we3r4" }

You will see this response in the bottom panel of Postman.

{
&nbsp; &nbsp; "username": "[email&nbsp;protected]",
&nbsp; &nbsp; "token": "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJpbmZvQGRqYW13YXJlLmNvbSIsInJvbGVzIjpbeyJpZCI6IjVjODBjNjIzYjIwMTkxNGIyYTY5N2U4ZCIsInJvbGUiOiJBRE1JTiJ9XSwiaWF0IjoxNTUxOTY0OTc3LCJleHAiOjE1NTE5Njg1Nzd9.j5CHZ_LCmeQtdxQeH9eluxVXcOsHPWV1p8WnBn0CULo"
}

Copy the token then back to the GET product. Add a header with the name Authorization and the value that paste from a token that gets by login with additional Bearer prefix (with space) as below.

Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJpbmZvQGRqYW13YXJlLmNvbSIsInJvbGVzIjpbeyJpZCI6IjVjODBjNjIzYjIwMTkxNGIyYTY5N2U4ZCIsInJvbGUiOiJBRE1JTiJ9XSwiaWF0IjoxNTUxOTY0OTc3LCJleHAiOjE1NTE5Njg1Nzd9.j5CHZ_LCmeQtdxQeH9eluxVXcOsHPWV1p8WnBn0CULo

You should see this response after clicking the Send button.

[
&nbsp; &nbsp; {
&nbsp; &nbsp; &nbsp; &nbsp; "id": "5c80dc6cb20191520567b68a",
&nbsp; &nbsp; &nbsp; &nbsp; "prodName": "Dummy Product 1",
&nbsp; &nbsp; &nbsp; &nbsp; "prodDesc": "The Fresh Dummy Product in The world part 1",
&nbsp; &nbsp; &nbsp; &nbsp; "prodPrice": 100,
&nbsp; &nbsp; &nbsp; &nbsp; "prodImage": "https://dummyimage.com/600x400/000/fff"
&nbsp; &nbsp; }
]

You can test the POST product with the token in headers using the same way.

That it’s, the Securing RESTful API with Spring Boot, Security, and Data MongoDB tutorial. You can get the full source code from our GitHub.

Learn More

Build a Simple CRUD App with Spring Boot and Vue.js

Creating RESTful APIs with NodeJS and MongoDB Tutorial

MongoDB with Python Crash Course - Tutorial for Beginners

How to build RESTful APIs with ASP.NET Core

Understanding the basics of RESTful APIs

Developing RESTful APIs with Lumen (A PHP Micro-framework)

Java Programming Masterclass for Software Developers

Java In-Depth: Become a Complete Java Engineer!

JSP, Servlets and JDBC for Beginners: Build a Database App

JSP, Servlet, JSLT + Hibernate: A complete guide

Mastering styled text in Flutter

Mastering styled text in Flutter

In this tutorial we are going to start with an overview of Dart strings and Unicode. Next we’ll move on to styling text for your app, first for entire strings and then for spans within a string.

Introduction

In this tutorial we are going to start with an overview of Dart strings and Unicode. Next we’ll move on to styling text for your app, first for entire strings and then for spans within a string.

Prerequisites

To go through this tutorial you should have the Flutter development environment set up and know how to run an app. I’m using Android Studio with the Flutter 1.1 plugin, which uses Dart 2.1.

Setup

Create a new Flutter app. I’m calling mine flutter_text.

Open main.dart and replace the code with the following:

    import 'package:flutter/material.dart';

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

    class MyApp extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          debugShowCheckedModeBanner: false,
          home: Scaffold(
            appBar: AppBar(title: Text('Styling text')),
            body: Container(
              child: Center(
                child: _myWidget(context),
              ),
            ),
          ),
        );
      }
    }

    // modify this widget with the example code below
    Widget _myWidget(BuildContext context) {
      String myString = 'I ❤️ Flutter';
      print(myString);
      return Text(
        myString,
        style: TextStyle(fontSize: 30.0),
      );
    }

Note the _myWidget() function at the end. You can modify or replace it using the examples below. The more you experiment on your own, the more you will learn.

If you are already familiar with concepts like grapheme clusters and Dart strings, you can skip down to the text styling sections below.

Unicode

Coded messages

When I was a kid I liked to write “secret” messages in code, where 1=a, 2=b, 3=c and so on until 26=z. A message using this code might be:

    9   12 9 11 5   6 12 21 20 20 5 18


To make the code even more secret you could shift the numbers, where 1=b, 2=c, 3=d and so on until it wrapped around where 26=a. As long as my friend and I had the same code key, we could decode each other’s messages. The wrong code key, though, would give garbled nonsense.

Computers are similar, except most of the time we don’t want secret messages. We want to make our messages easy to decode, so we agree on a code key, or should I say, a standard. ASCII was an early example of this, where the code key was 97=a, 98=b, 99=c, and so on. That worked fine for English but ASCII only had 128 codes (from 7 bits of data) and that wasn’t enough for all of the characters in other languages. So people made other code keys with more numbers. The problem was that the numbers overlapped and when you used the wrong decoding key you ended up with garbled nonsense.

Unicode to the rescue

Unicode is an international standard that assigns unique code numbers for the characters of every language in the world. The code numbers are called code points. In addition to what we normally think of as characters, there are also code points for control characters (like a new line), diacritical marks (like the accent over an é), and pictures (like 😊). As long as everyone agrees to use this code standard, there are no more fewer garbled messages.

Unicode is just a long list of code points. Saving these code points or sending them is another matter. To help you understand this, take my secret message from above as an example. If I write it as a string of numbers without whitespace and try to send it to you, you get:

    9129115612212020518


This is almost impossible to decode now. Does 912 mean 9, 1, 2 or does it mean 9, 12? It’s the same situation with Unicode. We have to use an agreed upon means to save and send Unicode text, or else it would be very difficult to decode. There are three main ways to do it: UTF-8, UTF-16, and UTF-32. UTF stands for Unicode Transformation Format, and each method of encoding has its advantages and disadvantages.

  • UTF-8 saves each code point using one to four bytes of data.
  • UTF-16 saves each code point as two or four bytes of data. One 16-bit code unit is big enough to uniquely reference a lot of Unicode code points, but not big enough for all of them (emojis, for example). In order to save code points with numbers higher than 16 bits (that is, higher than the number 65,535), UTF-16 uses two 16-bit code units (called surrogate pairs) to map the other code points.
  • UTF-32 saves each code point using four bytes of data. It provides a direct one-to-one mapping of UTF-32 code units to Unicode code points.

When working with UTF-16 code units, you need to be careful not to forget about the other half of a surrogate pair. And even if you are working with UTF-32, you shouldn’t assume that a single code point is the same as what a user perceives to be a character. For example, country flags (like 🇨🇦) are made of two code points. An accented character (like é) can also optionally be made from two code points. In addition to this, there are emoji with skin tone (like 👩🏾, 2 code points) and family emoji (like 👨‍👩‍👧, 5 code points).

So as a programmer, it is better not to think of UTF code units or Unicode code points as characters themselves. That will lead to bugs (for example, when trying to move the cursor one place to the left). Instead, you should think about what Unicode calls a grapheme cluster. These are user-perceived characters. So 🇨🇦, é, 👩🏾, and 👨‍👩‍👧 are each a single grapheme cluster because they each look like a single character even though they are made up of multiple Unicode code points.

Further reading

If you find this interesting or would like a deeper understand of the issues related to Unicode, I encourage you to read the following articles:

  • UTF-8 saves each code point using one to four bytes of data.
  • UTF-16 saves each code point as two or four bytes of data. One 16-bit code unit is big enough to uniquely reference a lot of Unicode code points, but not big enough for all of them (emojis, for example). In order to save code points with numbers higher than 16 bits (that is, higher than the number 65,535), UTF-16 uses two 16-bit code units (called surrogate pairs) to map the other code points.
  • UTF-32 saves each code point using four bytes of data. It provides a direct one-to-one mapping of UTF-32 code units to Unicode code points.
Dart strings

Let’s move on from talking about Unicode in a general way to seeing how Dart uses it.

Code units

In Dart, strings are sequences of UTF-16 code units. That makes string manipulation look deceptively easy because you can get the string value of a code unit by a random integer index:

    String myString = 'Flutter';
    String myChar = myString[0]; // F

But this creates bugs if you split a surrogate pair.

    String myString = '🍎';                    // apple emoji
    List<int> codeUnits = myString.codeUnits;  // [55356, 57166]
    String myChar = myString[0];               // 55356 (half of a surrogate pair)

This will throw an exception if you try to display myChar in a Text widget.

Runes

A better alternative is to work with code points, which are called runes in Dart.

    String myString = '🍎π';

    List<int> codeUnits = myString.codeUnits;    // [55356, 57166, 960]
    int numberOfCodeUnits = myString.length;     // 3
    int firstCodeUnit = myString.codeUnitAt(0);  // 55356

    Runes runes = myString.runes;                // (127822, 960)
    int numberOfCodPoints = runes.length;        // 2
    int firstCodePoint = runes.first;            // 127822

Grapheme clusters

Even runes will fail when you have grapheme clusters composed of multiple code points.

    String myString = '🇨🇦';
    Runes runes = myString.runes;                // (127464, 127462)
    int numberOfCodePoints = runes.length;       // 2
    int firstCodePoint = runes.first;            // 127464
    String halfFlag = String.fromCharCode(firstCodePoint); // 🇨

Displaying the halfFlag string in your app won’t crash it, but users will perceive it as a bug since it only contains one of the two regional indicator symbols used to make the Canadian flag.

Unfortunately, at the time of this writing, there is no support for grapheme clusters in Dart, though there is talk of implementing it. You should still keep them in mind while writing tests and working with strings, though.

Hexadecimal notation

If you are starting with a Unicode hex value, this is how you get a string:

    String s1 = '\u0043';                // C
    String s2 = '\u{43}';                // C
    String s3 = '\u{1F431}';             // 🐱 (cat emoji)
    String s4 = '\u{65}\u{301}\u{20DD}'; //  é⃝ = "e" + accent mark + circle
    int charCode = 0x1F431;              // 🐱 (cat emoji)
    String s5 = String.fromCharCode(charCode);

Substrings

The String documentation (here and here) is pretty good, and you should read it if you haven’t already. I want to review substrings before we go on to text styling, though, since we will be using it later.

To get a substring you do the following:

    String myString = 'I ❤️ Flutter.';
    int startIndex = 5;
    int endIndex = 12;
    String mySubstring = myString.substring(startIndex, endIndex); // Flutter

You can find index numbers with indexOf():

    int startIndex = myString.indexOf('Flutter');

OK, that’s enough background information. Let’s get on to styling text in Flutter.

Text styling with the Text widget

We are going to look first at styling strings in a Text widget. After that we will see how to style substrings within a RichText widget. Both of these widgets use a TextStyle widget to hold the styling information.

Replace _myWidget() with the following code:

    Widget _myWidget(BuildContext context) {
      return Text(
        'Styling text in Flutter',
        style: TextStyle(
          fontSize: 30.0,
        ),
      );
    }

Or, if you would like to compare multiple style settings at once, you can use the following column layout.

    Widget _myWidget(BuildContext context) {
      return Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: <Widget>[
          Text(
            'Styling text in Flutter',
            style: TextStyle(
              fontSize: 8,
            ),
          ),
          Text(
            'Styling text in Flutter',
            style: TextStyle(
              fontSize: 12,
            ),
          ),
          Text(
            'Styling text in Flutter',
            style: TextStyle(
              fontSize: 16,
            ),
          ),
        ],
      );
    }

Note that I am setting the TextStyle using the style property of the Text widget. I will modify the TextStyle options below. Try them out yourself by pressing hot reload between every change. You may want to leave a large font size (like fontSize: 30) for some of the later examples below so that you can see what is happening.

Text size

    TextStyle(
      fontSize: 30.0,
    )

When fontSize is not given, the default size is 14 logical pixels. Logical pixels are independent of a device’s density. That is, the text should appear to be to be basically the same size no matter what the pixel density of a user’s device may be. However, this font size is also multiplied by a textScaleFactor depending on the user’s preferred font size.

If you wish to disable accessibility scaling, you can set it on the Text widget. (I’m very impressed that Flutter has accessibility enabled by default, and I definitely don’t suggest that you disable it without reason. In some rare cases, though, an oversized font might break a layout…in which case it would still probably be better to redesign your layout rather than disable accessibility.)

    // This text will always display at 30.0 logical pixels, no matter
    // what the user's preferred size is.
    Text(
      'Some text',
      textScaleFactor: 1.0, // disables accessibility
      style: TextStyle(
        fontSize: 30.0
      ),
    )

You can also use the theme data to set the text size. See the section on themes below.

Text color

    TextStyle(
      color: Colors.green,
    )

In addition to predefined colors like Colors.green and Colors.red, you can also set shades on a color, like Colors.blue[100] or Colors.blue[700].

Background color

    Widget _myWidget(BuildContext context) {
      Paint paint = Paint();
      paint.color = Colors.green;
      return Text(
        'Styling text in Flutter',
        style: TextStyle(
          background: paint,
          fontSize: 30.0,
        ),
      );
    }

For a Text widget you could also just wrap it in a Container and set the color on the Container.

Bold

    TextStyle(
      fontWeight: FontWeight.bold,
    )

You can set the weight with numbers like FontWeight.w100 where w400 is the same as normal and w700 is the same as bold.

Italic

    TextStyle(
      fontStyle: FontStyle.italic,
    )

The only choices are italic and normal.

Shadow

    TextStyle(
      shadows: [
        Shadow(
          blurRadius: 10.0,
          color: Colors.blue,
          offset: Offset(5.0, 5.0),
        ),
      ],
    )

When setting the shadow you can change the blur radius (bigger means more blurry), color, and offset. You can even set multiple shadows as if there were more than one light source.

    TextStyle(
      shadows: [
        Shadow(
          color: Colors.blue,
          blurRadius: 10.0,
          offset: Offset(5.0, 5.0),
        ),
        Shadow(
          color: Colors.red,
          blurRadius: 10.0,
          offset: Offset(-5.0, 5.0),
        ),
      ],
    )

I’m not sure if more than one shadow is useful or not, but it is interesting.

Underline

    TextStyle(
      decoration: TextDecoration.underline,
      decorationColor: Colors.black,
      decorationStyle: TextDecorationStyle.solid,
    )

The decoration can be underline, lineThrough, or overline. The last line of text in the image above has an overline.

The choices for decorationStyle are solid, double, dashed, dotted, and wavy.

Spacing

    TextStyle(
      letterSpacing: -1.0,
      wordSpacing: 5.0,
    )

In the example image, the six lines on top use letter spacing ranging from -2.0 to 3.0. The six lines on bottom use word spacing ranging from -3.0 to 12.0. A negative value moves the letters or words closer together.

Font

Using a custom font requires a few more steps:

  1. Add a directory called assets to the root of your project.
  2. Copy a font into it. (I downloaded the Dancing Script font from here, unzipped it, and renamed the regular one to dancing_script.ttf.)
  3. In pubspec.yaml register the font:
    flutter:
      fonts:
      - family: DancingScript
        fonts:
        - asset: assets/dancing_script.ttf

  1. Add a directory called assets to the root of your project.
  2. Copy a font into it. (I downloaded the Dancing Script font from here, unzipped it, and renamed the regular one to dancing_script.ttf.)
  3. In pubspec.yaml register the font:
    TextStyle(
      fontFamily: 'DancingScript',
    )

  1. Add a directory called assets to the root of your project.
  2. Copy a font into it. (I downloaded the Dancing Script font from here, unzipped it, and renamed the regular one to dancing_script.ttf.)
  3. In pubspec.yaml register the font:

See this post for more help.

Using themes

Our root widget is a MaterialApp widget, which uses the Material Design theme. Through the BuildContext we have access to its predefined text styles. Instead of creating our own style with TextStyle, you can use a default one like this:

    Text(
      'Styling text in Flutter',
      style: Theme.of(context).textTheme.title,
    )

That was the default style for titles. There are many more defaults for other types of text. Check them out:

If a style is not specified, Text uses the DefaultTextStyle. You can use it yourself like this:

    Text(
      'default',
      style: DefaultTextStyle.of(context).style,
    )

DefaultTextStyle gets its style from the build context.

See the documentation for more about using themes.

Text styling with the RichText widget

The final thing I want to teach you is how to style part of a text string. With a Text widget the whole string has the same style. A RichText widget, though, allows us to add TextSpans that include different styles.

Basic example

Replace _myWidget() with the following code:

    Widget _myWidget(BuildContext context) {
      return RichText(
        text: TextSpan(
          // set the default style for the children TextSpans
          style: Theme.of(context).textTheme.body1.copyWith(fontSize: 30),
          children: [
            TextSpan(
                text: 'Styling ',
            ),
            TextSpan(
              text: 'text',
              style: TextStyle(
                color: Colors.blue
              )
            ),
            TextSpan(
                text: ' in Flutter',
            ),
          ]
        )
      );
    }

Note: An alternate way to make text with styled spans is to use the Text.rich() constructor, which has the same default style as the Text widget.
RichText takes a TextSpan tree. Every very TextSpan takes more TextSpan children, which inherit the style of their parent. To make the word “text” blue, I had to divide the string into three TextSpans. I used a color for the style, but I could have just as easily used any of the other styles that we have already looked at. Try adding a few more styles yourself.

Styling programmatically

In a real application we would probably have a longer string. For example, let’s highlight every occurrence of “text” in the following string:

To do that we have to look at the string and find the indexes of the text that we want to style. Then we use substring to cut the string up and put it in a list of TextSpans.

Replace _myWidget() with the following code:

    Widget _myWidget(BuildContext context) {

      final String myString =
          'Styling text in Flutter Styling text in Flutter '
          'Styling text in Flutter Styling text in Flutter '
          'Styling text in Flutter Styling text in Flutter '
          'Styling text in Flutter Styling text in Flutter '
          'Styling text in Flutter Styling text in Flutter ';

      final wordToStyle = 'text';
      final style = TextStyle(color: Colors.blue);
      final spans = _getSpans(myString, wordToStyle, style);

      return RichText(
        text: TextSpan(
          style: Theme.of(context).textTheme.body1.copyWith(fontSize: 30),
          children: spans,
        ),
      );
    }

    List<TextSpan> _getSpans(String text, String matchWord, TextStyle style) {

      List<TextSpan> spans = [];
      int spanBoundary = 0;

      do {

        // look for the next match
        final startIndex = text.indexOf(matchWord, spanBoundary);

        // if no more matches then add the rest of the string without style
        if (startIndex == -1) {
          spans.add(TextSpan(text: text.substring(spanBoundary)));
          return spans;
        }

        // add any unstyled text before the next match
        if (startIndex > spanBoundary) {
          spans.add(TextSpan(text: text.substring(spanBoundary, startIndex)));
        }

        // style the matched text
        final endIndex = startIndex + matchWord.length;
        final spanText = text.substring(startIndex, endIndex);
        spans.add(TextSpan(text: spanText, style: style));

        // mark the boundary to start the next search from
        spanBoundary = endIndex;

      // continue until there are no more matches
      } while (spanBoundary < text.length);

      return spans;
    }

Experiment with changing the search word and style.

In this example we searched for plain text, but you can also do pattern matching using regular expressions.

Clickable spans

You can make a span clickable by adding a TapGestureRecognizer:

    TextSpan(
      text: spanText,
      style: style,
      recognizer: TapGestureRecognizer()
        ..onTap = () {
          // do something
        },
    )

This would allow you to open a URL, for example, if used along with the url_launcher plugin.

Final notes

Here are a few more related concepts that I didn’t have time or space to cover:

  • UTF-8 saves each code point using one to four bytes of data.
  • UTF-16 saves each code point as two or four bytes of data. One 16-bit code unit is big enough to uniquely reference a lot of Unicode code points, but not big enough for all of them (emojis, for example). In order to save code points with numbers higher than 16 bits (that is, higher than the number 65,535), UTF-16 uses two 16-bit code units (called surrogate pairs) to map the other code points.
  • UTF-32 saves each code point using four bytes of data. It provides a direct one-to-one mapping of UTF-32 code units to Unicode code points.
Conclusion

Text seems like it should be so simple, but it really isn’t. Language is messy and dealing with it as a programmer can be difficult. Much progress has been made in recent years, though. Unicode has solved a lot of problems. Dart and Flutter also give us a lot of tools to manipulate and style text. I expect to see these tools improve even more in the future.

The source code for this project is available on GitHub.

By the way, in case you were curious but lazy, my secret message was “I like Flutter”.

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.

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

flutter create flutter_todos

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

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

and finally install all of our dependencies

flutter packages get

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.

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(),
    );
  }
}

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.
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';
}

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:

  • 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.

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

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';
}

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.

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(),
    );
  }
}

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:

export './todos_bloc.dart';
export './todos_event.dart';
export './todos_state.dart';

Filtered Todos Bloc

Our TodosBloc will be responsible for converting TodosEvents into TodosStates and will manage the list of 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:

  • 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.

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

enum VisibilityFilter { all, active, completed }

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:

  • 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.

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

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 }';
  }
}

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:

  • 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.

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

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 }';
}

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.

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();
  }
}

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:

export './filtered_todos_bloc.dart';
export './filtered_todos_event.dart';
export './filtered_todos_state.dart';

Stats Bloc

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

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

  • 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.

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

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 }';
  }
}

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.

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 }';
}

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.

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();
  }
}

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

Our TodosBloc will be responsible for converting TodosEvents into TodosStates and will manage the list of todos in our application.#### 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:

enum AppTab { todos, stats }

Event

Our TabBloc will be responsible for handling a single TabEvent:

  • 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.

Create blocs/tab/tab_event.dart:

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 }';
}

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.

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;
    }
  }
}

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:

export './tab_bloc.dart';
export './tab_event.dart';

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.

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);
  }
}

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.

export './filtered_todos/filtered_todos.dart';
export './stats/stats.dart';
export './tab/tab.dart';
export './todos/todos.dart';
export './simple_bloc_delegate.dart';

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

Screens

Home Screen

Our TodosBloc will be responsible for converting TodosEvents into TodosStates and will manage the list of todos in our application.
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.

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();
  }
}

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.

BlocProviderTree(
  blocProviders: [
    BlocProvider<TabBloc>(bloc: _tabBloc),
    BlocProvider<FilteredTodosBloc>(bloc: _filteredTodosBloc),
    BlocProvider<StatsBloc>(bloc: _statsBloc),
  ],
  child: Scaffold(...),
);

is equivalent to writing

BlocProvider<TabBloc>(
  bloc: _tabBloc,
  child: BlocProvider<FilteredTodosBloc>(
    bloc: _filteredTodosBloc,
    child: BlocProvider<StatsBloc>(
      bloc: _statsBloc,
      child: Scaffold(...),
    ),
  ),
);

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

Our TodosBloc will be responsible for converting TodosEvents into TodosStates and will manage the list of todos in our application.
Create screens/details_screen.dart and let’s build it.

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,
                          );
                        },
                      ),
                    );
                  },
          ),
        );
      },
    );
  }
}

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

Our TodosBloc will be responsible for converting TodosEvents into TodosStates and will manage the list of todos in our application.
Create screens/add_edit_screen.dart and let’s have a look at the implementation.

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) => _task = value,
              ),
              TextFormField(
                initialValue: isEditing ? widget.todo.note : '',
                key: ArchSampleKeys.noteField,
                maxLines: 10,
                style: textTheme.subhead,
                decoration: InputDecoration(
                  hintText: localizations.notesHint,
                ),
                onSaved: (value) => _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);
          }
        },
      ),
    );
  }
}

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

  • 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.

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.

export './add_edit_screen.dart';
export './details_screen.dart';
export './home_screen.dart';

Widgets

FilterButton

Our TodosBloc will be responsible for converting TodosEvents into TodosStates and will manage the list of todos in our application.
Let’s create a new directory called widgets and put our FilterButton implementation in widgets/filter_button.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/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),
    );
  }
}

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

Our TodosBloc will be responsible for converting TodosEvents into TodosStates and will manage the list of todos in our application.
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.

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);
      },
    );
  }
}

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

Our TodosBloc will be responsible for converting TodosEvents into TodosStates and will manage the list of todos in our application.
Let’s create widgets/tab_selector.dart and implement it.

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(),
    );
  }
}

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

Our TodosBloc will be responsible for converting TodosEvents into TodosStates and will manage the list of todos in our application.
Create widgets/filtered_todos.dart and let’s implement it.

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: () => 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: () => todosBloc.dispatch(AddTodo(todo)),
                      localizations: localizations,
                    ));
                  }
                },
                onCheckboxChanged: (_) {
                  todosBloc.dispatch(
                    UpdateTodo(todo.copyWith(complete: !todo.complete)),
                  );
                },
              );
            },
          );
        } else {
          return Container(key: FlutterTodosKeys.filteredTodosEmptyContainer);
        }
      },
    );
  }
}

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).

  • 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.

Todo Item

Our TodosBloc will be responsible for converting TodosEvents into TodosStates and will manage the list of todos in our application.
Create widgets/todo_item.dart and let’s build it.

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,
      ),
    );
  }
}

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

Our TodosBloc will be responsible for converting TodosEvents into TodosStates and will manage the list of todos in our application.
Create widgets/delete_todo_snack_bar.dart and let’s implement it.

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,
          ),
        );
}

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

Our TodosBloc will be responsible for converting TodosEvents into TodosStates and will manage the list of todos in our application.
Create widgets/loading_indicator.dart and let’s write it.

import 'package:flutter/material.dart';

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

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

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

Our TodosBloc will be responsible for converting TodosEvents into TodosStates and will manage the list of todos in our application.
Let’s create widgets/stats.dart and take a look at the implementation.

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);
        }
      },
    );
  }
}

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.

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

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.

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,
            );
          },
        },
      ),
    );
  }
}

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:

  • 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.

The entire main.dart should look like this:

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,
            );
          },
        },
      ),
    );
  }
}

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.