Flutter - Bloc

 

What is Bloc ?

Bloc (Business Logic Components) is one of the patterns recommended y Google. It helps you separate your business logic from UI. It’s also a pattern that separates responsibilities in application and helps you write easily testable code. You can check out other patterns recommended by Google here.

Bloc contains of three structures. These are Events ClassStates Class and Bloc Class. Now let’s examine these in more detail.

1. Events:
Events represent the operations we want to perform in the application. Consider a contacts application, in this case events would be GetUsersUpdateUserDeleteUser etc. There is an abstract class for events, and other events that we will use inherit from this abstract class.

You need to create your own events according to the action you want to perform in the application.

2. States:
States represent the situations that will be found in the application. It is used to determine what to do in application in which situation. For the contacts app there could be UsersInitialUsersLoadingUsersLoaded and UsersError etc.

3. Bloc:
We can think of Bloc Class as an intermediate class. It connects Events and States. When an event is triggered, which function will be called and with states the application will transition to are determined in the Bloc Class.

Bloc Usage

I developed a project using Bloc. This project has a screen. On this screen, you can randomly fetch characters from The Last Airbender. I developed the project with Clean Architecture, but I only explain the usage of block in this story. You can read my story about Clean Architecture in detail here. I used The Last Airbender API while developing the project.

Filling Example

Firstly I created coreconfig and features folders. Then I created characters folder in features folder for feature of fetch characters. Finally I created datadomain and presentation folders in characters folder.
I created bloc folder in features folder. This is because Bloc is used for the application’s business logic.

I added the files; events, state and bloc which are the three parts of Bloc.

Code Example

First I determined the states as CharactersInitial, CharactersLoading, CharactersError and CharactersLoaded and I created CharactersState as abstract class. Then I gave inheritance from CharactersState to other states.

part of 'characters_bloc.dart';

abstract class CharactersState {}

class CharactersInitial extends CharactersState {}

class CharactersLoading extends CharactersState {}

class CharactersLoaded extends CharactersState {
CharacterEntity character;
CharactersLoaded({required this.character});
}

class CharactersError extends CharactersState {
String? errorMessage;
CharactersError(this.errorMessage);
}

Secondly I created an abstract class for events. Since we will only be fetching characters in this application, I only created the GetCharacter event. And I gave inheritance from CharactersEvent class to GetCharacters class.

part of 'characters_bloc.dart';

abstract class CharactersEvent {}

class GetCharacters extends CharactersEvent {}

I gave inheritance from Bloc to CharactersBloc and I added events to constructer method of class. Then I created getCharacters function for fetch data. In this function, I triggered situations according to the API result.

part 'characters_event.dart';
part 'characters_state.dart';

class CharactersBloc extends Bloc<CharactersEvent,CharactersState> {
final GetCharacterUseCase _getCharactersUseCase;
CharactersBloc(this._getCharactersUseCase) : super(CharactersInitial()){
on<GetCharacters>(getCharacters);
}


Future<void> getCharacters(GetCharacters event, Emitter<CharactersState> emit) async {
emit(CharactersLoading());
final dataState = await _getCharactersUseCase.call(null);
if(dataState is DataSuccess){
emit(CharactersLoaded(character: dataState.data!));
} else if(dataState is DataFailed){
emit(CharactersError(dataState.exception!.message));
}
}
}

Finally, I coded to CharactersPage. I used BlocBuilder for listen to states. This is because when state changed I could return relevant function.

_buildBloc() {
return BlocBuilder(
bloc: _bloc,
builder: (BuildContext context, state) {
if(state is CharactersLoading){
return _showLoadingAnimation();
} else if(state is CharactersLoaded){
return _buildCharactersList(state);
} else if(state is CharactersError){
return _buildErrorView(state);
} else {
return _buildInitialView();
}
},
);
}

When screen loaded I triggered GetCharacters event.

@override
void initState() {
super.initState();
_bloc.add(GetCharacters());
}

SOLID Principles in Swift

 SOLID is a coding principle and we can write flexible, dynamic, testable and effective codes with SOLID principles.

SOLID has five principle. They are

  1. Single responsibility
  2. Open-closed
  3. Liskov substitution
  4. Interface segregation
  5. Dependency inversion

Let’s examine these in detail.

1. Single Responsibility Principle

If our classes or functions are handling multiple responsibilities, we can say that this is mistake. Because each class or function should have single responsibility.

There is a CategoriesVC class in this example. And there are codes to setup the view model and navigation title in viewDidLoad function. But these codes have different responsibilities.

final class CategoriesVC: UIViewController {

//MARK: - PROPERTIES
var viewModel: CategoriesVMProtocol? {
didSet {
viewModel?.delegate = self
}
}
override func viewDidLoad() {
super.viewDidLoad()
viewModel?.fetchCategories()
navigationItem.title = "Categories"
}

}

I created setupViewModel and setupNavigationBar functions in this example. Because these functions have different responsibilities and each function have own responsibility in this example.

final class CategoriesVC: UIViewController {

//MARK: - PROPERTIES
var viewModel: CategoriesVMProtocol? {
didSet {
viewModel?.delegate = self
}
}
override func viewDidLoad() {
super.viewDidLoad()
setupViewModel()
setNavigationTitle()
}

//MARK: - PRIVATE FUNCTIONS
private func setupViewModel(){
viewModel?.fetchCategories()
}
private func setNavigationTitle() {
navigationItem.title = "Categories"
}
}

2.Open-Closed Principle

In this principle, every class and structure should closed for modification and open for extension. For instance we want to create a class to calculate the area of any shape. In this case we have to create a parent class named Shape

I created rectangles and triangles array in AreaCalculator class. I had to create two variable this is because they have different types. And I created two for loop to sum to areas of shapes.

final class Rectangle {
let width: Double
let height: Double

init(width: Double, height: Double) {
self.width = width
self.height = height
}

func calculateArea() -> Double {
return width * height
}
}

final class Triangle {
let base: Double
let height: Double

init(base: Double, height: Double) {
self.base = base
self.height = height
}

func calculateArea() -> Double {
return (base * height) / 2.0
}
}

final class AreaCalculator {

let rectangles: [Rectangle] = [
Rectangle(width: 5, height: 3),
Rectangle(width: 7, height: 4)
]

let triangles: [Triangle] = [
Triangle(base: 5, height: 3),
Triangle(base: 7, height: 4)
]

func calculateTotalArea() -> Double {
var totalArea: Double = 0.0

for rectangle in rectangles {
totalArea += rectangle.calculateArea()
}

for triangle in triangles {
totalArea += triangle.calculateArea()
}

return totalArea
}

}

I created Shape protocol. Because rectangle objects and triangle objects are same type. I mean they are just a shape. So I gave inheritance to Rectangle and Triangle classes. This way I used just one array and one for loop to calculate the area of shapes.

protocol Shape {
func calculateArea() -> Double
}

final class Rectangle: Shape {
let width: Double
let height: Double

init(width: Double, height: Double) {
self.width = width
self.height = height
}

func calculateArea() -> Double {
return width * height
}
}

final class Triangle: Shape {
let base: Double
let height: Double

init(base: Double, height: Double) {
self.base = base
self.height = height
}

func calculateArea() -> Double {
return (base * height) / 2.0
}
}

final class AreaCalculator {

let shapes: [Shape] = [
Rectangle(width: 5, height: 3),
Triangle(base: 7, height: 4)
]

func calculateTotalArea() -> Double {
var totalArea: Double = 0.0

for shape in shapes {
totalArea += shape.calculateArea()
}

return totalArea
}

}

3. Liskov Substitution Principle

It is a principle that aims for classes work together in a meaningful way. So the parent classes should use to features of the child classes without change.

We have Dog and Fish classes in this example. This classes inherited from Animal class. But fish objects shouldn’t have run speed value.

class Animal {
let name: String
let runSpeed: Int
let swimSpeed: Int
init(name: String, runSpeed: Int, swimSpeed: Int) {
self.name = name
self.runSpeed = runSpeed
self.swimSpeed = swimSpeed
}
}

final class Dod: Animal {

}

final class Fish: Animal {

}

I created CanSwim and CanRun protocols. And I created Animals class to hold the name of animal. This is because every animal have a name but every animal can’t swim or can’t run. Now our parent classes can’t change the features of sub classes.

protocol CanSwim {
var swimSpeed: Int { get set }
}

protocol CanRun {
var runSpeed: Int { get set }
}

class Animal {
let name: String
init(name: String) {
self.name = name
}
}

final class Dod: Animal, CanRun {
var runSpeed: Int
init(name: String, runSpeed: Int) {
self.runSpeed = runSpeed
super.init(name: name)
}
}

final class Fish: Animal, CanSwim {
var swimSpeed: Int
init(name: String, swimSpeed: Int) {
self.swimSpeed = swimSpeed
super.init(name: name)
}
}

4. Interface Segregation Principle

This principle aims to separate the different features of interfaces into different interfaces. Because client shouldn’t depend on they don’t need.

In this example, the Animals protocol forces every animal to have both the eat and fly methods. However, the Dog class doesn't have the ability to fly, yet it is required to implement the fly method, leaving it empty. This violates the Interface Segregation Principle because each class should only have the functionality it needs.

protocol Animals {
func eat()
func fly()
}

class Bird: Animals {
func eat() {
print("I can eat")
}

func fly() {
print("I can fly")
}
}

class Dog: Animals {
func eat() {
print("I can eat")
}

func fly() {
print("I cannot fly")
}
}

In this example, specific protocols like Flyable and Feedable are used, allowing each animal class to only implement the features it requires. The Bird class implements both the fly and eat methods, while the Dog class only implements eat. This approach adheres to the Interface Segregation Principle because each class only contains the methods it needs. By doing so, we increase flexibility and keep the code cleaner and more maintainable.

protocol Flyable {
func fly()
}

protocol Feedable {
func eat()
}

class Bird: Flyable, Feedable {
func eat() {
print("I can eat")
}

func fly() {
print("I can fly")
}
}

class Dogs: Feedable {
func eat() {
print("I can eat")
}
}

5. Dependency Inversion Principle

This principle state that high-level classes shouldn’t depend on low-level classes. Also this principle abstract high-level classes and low-level classes using protocols. Therefore, the dependency between them becomes minimalized.

In this example, the UserManager class directly depends on the Networking class. Any changes in Networking would require modifications in UserManager as well. This violates the Dependency Inversion Principle because high-level modules like UserManager should not depend on low-level modules like Networking. This tight coupling makes the code harder to maintain and extend.

class Networking {
func fetchData() {
print("Fetching data from the server")
}
}

class UserManager {
let networking = Networking()

func fetchUserData() {
networking.fetchData()
}
}

let userManager = UserManager()
userManager.fetchUserData()

In this example, we use a NetworkingProtocol to decouple the UserManager and Networking classes. UserManager now depends on the protocol instead of the concrete Networking class. This makes the code more flexible and easier to maintain since changes in Networking won’t affect UserManager.

protocol NetworkingProtocol {
func fetchData()
}

class Networking: NetworkingProtocol {
func fetchData() {
print("Fetching data from the server")
}
}

class UserManager {
let networking: NetworkingProtocol

init(networking: NetworkingProtocol) {
self.networking = networking
}

func fetchUserData() {
networking.fetchData()
}
}

let networking = Networking()
let userManager = UserManager(networking: networking)
userManager.fetchUserData()

How to extract filename from Uri?

Now, we can extract filename with and without extension :) You will convert your bitmap to uri and get the real path of your file. Now w...