State Management di Flutter dengan BLoC (Business Logic Component)

Halo temen-temen developer, pasti banyak dari kalian yang sudah menggunakan design pattern seperti MVC, MVP, MVVM dan sebagainya. Nah untuk di Flutter sendiri, ada design pattern yang sedang banyak digunakan, yaitu BLoC [https://bloclibrary.dev/#/whybloc] (Business Logic Component). Kenapa pake BLoC? Karena dengan BLoC proses component bisnis logic dan component presentation dibuat terpisah, sehingga lebih mudah untuk dipahami. BLoC sendiri memiliki 3 point utama, yaitu: 1. Simple -> Mud

Halo temen-temen developer, pasti banyak dari kalian yang sudah menggunakan design pattern seperti MVC, MVP, MVVM dan sebagainya. Nah untuk di Flutter sendiri, ada design pattern yang sedang banyak digunakan, yaitu BLoC (Business Logic Component).

Kenapa pake BLoC? Karena dengan BLoC proses component bisnis logic dan component presentation dibuat terpisah, sehingga lebih mudah untuk dipahami.

BLoC sendiri memiliki 3 point utama, yaitu:

  1. Simple -> Mudah dimengerti dan dapat digunakan oleh developer dari berbagai keterampilan.
  2. Powerful -> Membantu membuat aplikasi yang kompleks dengan membagi kedalam komponen-komponen kecil.
  3. Testable -> Mudah untuk dilakukan proses testing.

Secara garis besar, BLoC dapat digambarkan seperti dibawah ini:

https://raw.githubusercontent.com/felangel/bloc/master/docs/assets/bloc_architecture.png

Okeh langsung aja ke cara bagaimana implementasi dari BLoC itu sendiri kedalam sebuah project Flutter. Pertama perlu meng-install Flutter BLoC, caranya dengan mendaftarkan Flutter BLoC kedalam file pubspec.yaml, seperti dibawah ini:

dependencies:
  flutter:
    sdk: flutter
  flutter_bloc: ^2.0.0
  equatable: ^0.6.0

Selanjutnya install deh dengan cara:

flutter packages get

Jika udah, mari kita lanjutkan dengan membuat Event, State dan Bloc. Pada studi kasus ini gw akan melakukan pengambilan data movies dari TheMoviesDB API (sudah nggak asing lagi dong dengan themoviedb).

  • Diawali dengan membuat State, State itu apasih ? State itu merupakan kondisi-kondisi yang akan dialami oleh aplikasi yang kita buat ketika melakukan suatu pekerjaan.

    Pada studi kasus pengambilan data movies, gw membagi State-nya menjadi 3, yaitu MoviesFetchLoading, MoviesFetchSuccess dan MoviesFetchError seperti dibawah ini:
import 'package:meta/meta.dart';
import 'package:equatable/equatable.dart';
import 'package:my_app/data/movies/movies.dart';

abstract class MoviesState extends Equatable{
  const MoviesState();
}

class MoviesFetchLoading extends MoviesState {
  @override
  List<Object> get props => [];
}

class MoviesFetchSuccess extends MoviesState {
  final Movies movies;

  const MoviesFetchSuccess({this.movies});

  @override
  List<Object> get props => [movies];
}

class MoviesFetchError extends MoviesState {
  final String error;

  const MoviesFetchError({this.error});

  @override
  List<Object> get props => [error];

  @override
  String toString() => 'LoginFailure { error: $error }';
}
  • Kemudian kita membuat yang namanya Event, Event itu apa ? Event itu adalah input yang diberikan oleh Presentation Component kedalam Business Logic Component.

    Contohnya seperti load halaman, button onClick dan sebagainya. Pada studi kasus ini, gw membuat satu buah Event, yaitu MoviesFetching seperti dibawah ini:
import 'package:equatable/equatable.dart';

abstract class MoviesEvent extends Equatable {
  const MoviesEvent();

  @override
  List<Object> get props => [];
  
}

class MoviesFetching extends MoviesEvent {}
  • Yap, sekarang kita akan membuat Bloc, Bloc apaan lagi dah ? Bloc itu yang bertugas untuk mengubah State pada aplikasi berdasarkan dari Event yang sedang dijalankan. Contoh Bloc pada studi kasus ini seperti dibawah ini:
import 'package:bloc/bloc.dart';
import 'package:flutter/material.dart';
import 'package:my_app/bloc/movies_event.dart';
import 'package:my_app/bloc/movies_state.dart';
import 'package:my_app/data/domain/movies_domain.dart';
import 'package:my_app/data/movies/movies.dart';

class MoviesBloc extends Bloc<MoviesEvent, MoviesState>{
  final MoviesDomain moviesDomain;

  MoviesBloc({
    @required this.moviesDomain
  }) : assert(moviesDomain != null);
  
  @override
  MoviesState get initialState => MoviesFetchLoading();

  @override
  Stream<MoviesState> mapEventToState(MoviesEvent event) async* {
    if(event is MoviesFetching) {
      yield MoviesFetchLoading();
      try {
        Movies movies = await moviesDomain.getMoviesPopular();
        yield MoviesFetchSuccess(movies: movies);
      } catch (e) {
        yield MoviesFetchError(error: e.toString());
      }
    }
  }
}

Siap, akhirnya selesai sudah kita membuat Event, State dan Bloc. But wait, masih ada tahapannya lagi, tapi nyantai dululah sambil ngopi atau ngeteh.

https://pbs.twimg.com/media/DLXNXwZVYAEL8W-.jpg

Selanjutnya kita akan membuat Model untuk menampung data dari response API, disini gw menggunakan library json_to_dart yang dapat membantu untuk mengubah JSON menjadi Dart.

Hasilnya akan seperti ini:

class Movies {
  int page;
  int totalResults;
  int totalPages;
  List<Results> results;

  Movies({this.page, this.totalResults, this.totalPages, this.results});

  Movies.fromJson(Map<String, dynamic> json) {
    page = json['page'];
    totalResults = json['total_results'];
    totalPages = json['total_pages'];
    if (json['results'] != null) {
      results = new List<Results>();
      json['results'].forEach((v) {
        results.add(new Results.fromJson(v));
      });
    }
  }

  Map<String, dynamic> toJson() {
    final Map<String, dynamic> data = new Map<String, dynamic>();
    data['page'] = this.page;
    data['total_results'] = this.totalResults;
    data['total_pages'] = this.totalPages;
    if (this.results != null) {
      data['results'] = this.results.map((v) => v.toJson()).toList();
    }
    return data;
  }
}

class Results {
  double popularity;
  int voteCount;
  bool video;
  String posterPath;
  int id;
  bool adult;
  String backdropPath;
  String originalLanguage;
  String originalTitle;
  List<int> genreIds;
  String title;
  double voteAverage;
  String overview;
  String releaseDate;

  Results({
      this.popularity,
      this.voteCount,
      this.video,
      this.posterPath,
      this.id,
      this.adult,
      this.backdropPath,
      this.originalLanguage,
      this.originalTitle,
      this.genreIds,
      this.title,
      this.voteAverage,
      this.overview,
      this.releaseDate
  });

  Results.fromJson(Map<String, dynamic> json) {
    popularity = json['popularity'];
    voteCount = json['vote_count'];
    video = json['video'];
    posterPath = json['poster_path'];
    id = json['id'];
    adult = json['adult'];
    backdropPath = json['backdrop_path'];
    originalLanguage = json['original_language'];
    originalTitle = json['original_title'];
    genreIds = json['genre_ids'].cast<int>();
    title = json['title'];
    //voteAverage = double.parse(json['vote_average']);
    overview = json['overview'];
    releaseDate = json['release_date'];
  }

  Map<String, dynamic> toJson() {
    final Map<String, dynamic> data = new Map<String, dynamic>();
    data['popularity'] = this.popularity;
    data['vote_count'] = this.voteCount;
    data['video'] = this.video;
    data['poster_path'] = this.posterPath;
    data['id'] = this.id;
    data['adult'] = this.adult;
    data['backdrop_path'] = this.backdropPath;
    data['original_language'] = this.originalLanguage;
    data['original_title'] = this.originalTitle;
    data['genre_ids'] = this.genreIds;
    data['title'] = this.title;
    data['vote_average'] = this.voteAverage;
    data['overview'] = this.overview;
    data['release_date'] = this.releaseDate;
    return data;
  }
}

Taraaaaaa, sangat mudah bukan hehehe. Okeh, kemudian kita perlu membuat sebuah Domain Layer, layer ini akan berfungsi untuk menerima dan merubah data sesuai yang dibutuhkan oleh UI atau Presentation Component.

Merubah data yang dimaksudkan adalah, semisalkan response dari API untuk jenis kelamin laki-laki adalah L, sedangkan di UI kita menampilkan Laki-Laki, agar tidak ada proses perubahan data di UI, maka proses perubahan data tersebut dilakukan pada domain layer.

Contoh domain layer seperti dibawah ini:

import 'package:my_app/data/movies/movies.dart';
import 'package:my_app/data/repository/movies_repository.dart';

class MoviesDomain {
  final MoviesRepository moviesRepository;
   
  MoviesDomain(this.moviesRepository);

  Future<Movies> getMoviesPopular() {
    return moviesRepository.getPopularMovies();
  }
}

Untuk contoh diatas, belum dilakukan perubahan data dari response yang diterima. Kemudian kita membuat Repository Layer, layer ini berfungsi untuk melakukan request data ke API:

import 'package:alice/alice.dart';
import 'package:my_app/data/movies/movies.dart';
import 'package:my_app/constants.dart';

class MoviesRepository {
  Dio dio = Dio();
  Future<Movies> getPopularMovies() async {
    try {
      final response = await dio.get("${baseUrl}movie/popular?api_key=${apiKey}");
      return Movies.fromJson(response.data);
    } catch (e) {
      return e;
    }
  }
}

Masih semangat ? gw harap masih, kita lanjutkan pada sisi Presentation Component-nya. Pertama kita harus mendaftarkan Bloc yang telah kita buat, caranya bagaimana ? Seperti ini:

class _State extends State<SecondApp> {
  MoviesBloc _moviesBloc;
  MoviesDomain _moviesDomain;
  @override
  void initState() {
    super.initState();
    _moviesDomain = new MoviesDomain(MoviesRepository());
    _moviesBloc = new MoviesBloc(moviesDomain: _moviesDomain);
  }

Kemudian kita panggil Event untuk melakukan Fetching Movies Data, yaitu MoviesFetching Event. Caranya seperti dibawah ini:

@override
  Widget build(BuildContext context) {
    _moviesBloc.add(MoviesFetching()); // Call Event
    return new Scaffold(
      appBar: new AppBar(
        title: new Text("Movies List"),
      ),
      body: BlocProvider(
        create: (BuildContext context){
          return _moviesBloc; 
        },
        child: BlocListener<MoviesBloc, MoviesState>(
          bloc: _moviesBloc, // set bloc yang akan dipakai
          listener: (context, state){
            // listen the state changed
            if (state is MoviesFetchError) {
              print(state.error.toString());
              Scaffold.of(context).showSnackBar(
                SnackBar(
                  content: Text('${state.error}'),
                  backgroundColor: Colors.red,
                ),
              );
            }
          },
          child: new BlocBuilder<MoviesBloc, MoviesState>(
            bloc: _moviesBloc, // set bloc yang akan dipakai
            builder: (BuildContext context, MoviesState state){
              // listen the state changed
              if (state is MoviesFetchSuccess) { 
                return _MoviesList(movies: state.movies,);
              }else{
                return Center(
                  child: CircularProgressIndicator(),
                );
              }
            },
          ),
        ),
      ),
    );
  }

Ketika state dari Bloc me-response dengan MoviesFetchSuccess, maka akan memanggil Widget _MoviesList:

class _MoviesList extends StatelessWidget {
  final Movies movies;

  _MoviesList({this.movies});
  
  ListTile _listTile(BuildContext context, int index) {
    return ListTile(
      title: new Text(movies.results[index].originalTitle.toString()),
      subtitle: new Text(movies.results[index].overview.toString(),
        overflow: TextOverflow.ellipsis,
        maxLines: 3,
        softWrap: true,),
    );
  }

  @override
  Widget build(BuildContext context) {
    return new ListView.builder(
      itemCount: movies.results.length,
      itemBuilder: _listTile,
    );
  }
}

Hasilnya adalah seperti dibawah ini:

Sekian dulu dari saya, semoga bermanfaat dan mohon maaf jika masih banyak kekurangan. Untuk kritik dan masukan sangat terbuka :), kita sama-sama belajar.

Github Repo : https://github.com/wahyupermadie/latihan-flutter

Reference :