Created with to build something
Version: 5.0.0
cover-tutorial-awal-menggunakan-bloc-library-pada-flutter

Codding

Tutorial Awal Menggunakan Bloc Library Pada Flutter

Tutorial awal menggunakan BLoC Library pada flutter untuk pemula yang baru saja menggunakan bloc library untuk membuat aplikasi flutter.

Oct 21, 2020

Mari kita berbicara sedikit mengenai state management pada flutter, pada awal perilisannya flutter sebenarnya sudah memiliki state managementnya sendiri sehingga bagian sebagian pengguna flutter mungkin akan merasa cukup untuk menggunakan state management bawaan flutter. Ok, mungkin hal itu tidak masalah namun karena kelebihan flutter yang tidak menerapkan standararisasi gaya penulisan beberapa pengguna awam justru akan sedikit kesulitan dalam mempelajari beberapa architecture design pattern tersebut.

Saat artikel ini dibuat, berdasarkan hasil penelurusan yang saya cari di google. Dengan menggunakan flutter anda dapat menerapkan beberapa architecture design pattern seperti : MVC (Model - View Countroller), MVP (Model - View - Presenter), MVVM (Model - View - View Model) dan BLoC (Bussiness Logic Component). Ke empat design pattern tersebut memeiliki keunikan masing - masing sehingga untuk beberapa kasus, BLoC cenderung lebih dipahami pengguna awam ketimbang ketiga model berikut akan tetapi hal ini tidak mengurangi tujuan untuk mempermudah pengembang dalam melakukan maintan aplikasi ketika terjadi penambalan bug ataupun penambahan fitur baru.

Apa itu BLoC ?

BLoC merupakan kepanjangan dari Bussiness Logic Component yang mengatur penulisan kode agar setiap logik bisnis dipisahkan dari komponen. Gambaran kasarnya adalah ketika kamu ingin membuat sebuah modul kalkulator dalam aplikasi, kode yang mempresentasikan UI (User Interface) akan dipisahkan dari logik sehingga nantinya terdapat satu file view yang hanya akan digunakan untuk menampilkan sedangkan modul BLoC akan mengelola data tersebut.

Pada dasarnya hal ini serupa dengan beberapa arsitektur yang saya sebutkan tadi, akan tetapi anda pun bisa secara tidak sengaja menerapkan arsitektur tersebut karena kebutuhan yang cenderung rumit.

Sebelum anda ingin membaca tutorial ini lebih jauh, tidak salahnya untuk memahami dulu konsep BLoC dan sebagai referensi anda dapat menonton video berikut yang membahas bagaimana BLoC pada flutter bekerja.

Apa itu BLoC Library

Di flutter terdapat sebuah salah satu packages yang bernama BLoC library yang digunakan untuk membantu pengguna flutter dalam mengembangkan aplikasi flutter dengan menggunakan design arsitektur BLoC, BLoC Library juga bisa disebut sebagai state management karena juga dapat digunakan untuk memanage state yang terdapat pada flutter.

Jika anda melirik sedikit kebelakang tentang library yang satu ini di google ataupun youtube, sebenarnya library ini memiliki pertumbuhan yang sangat begitu cepat bahkan tanpa saya sadari BLoC library juga sudah menjadi satu dengan cubit. Anda dapat membaca beberapa dokumentasinya disini.

Saya sebagai pengguna flutter yang baru sekali menggunakan BLoC library cukup kesulitan dalam membaca dokumentasi penggunaan yang diberikan, justru saya lebih senang membaca dokumentasi API dan contoh penggunaannya dan sebagai pengingat mungkin anda perlu mematangkan pemahaman dasar mengenai BLoC terlebih dahulu sebelum menggunakan ini.

Ekstensi BLoC Library

Ada sedikit keunggulan yang ditawarkan oleh BLoC library ini, yaitu tersedianya ekstensi BLoC Library yang memungkinkan anda untuk membuat file bloc tanpa membuatnya secara manual. Ok mungkin ini tidak se - magic apa yang anda bayangkan, akan tetapi anda setidaknya akan mengetahui bagaimana dasar penggunaannya.

Ekstensi ini tersedia pada dua lingkungan yaitu pada lingkungan IDE IntelliJ mirip Jetbrains sehingga apapun yang berhubungan dengan produk tersebut anda bisa menggunakannya dan lingkungan Teks Editor yaitu Visual Studio Code.

Sebelum Menggunakan BLoC Library

Sebelum anda menggunakan BLoC library dalam beberapa kasus setidaknya membutuhkan dua modul tambahan untuk menghandling proses yang ada di BLoC library.

1. Repository

Sebelum anda bermain lebih lanjut menggunakan BLoC Library memasukan seluruh proses pada file utama BLoC membuat baris kode yang terdapat pada file tersebut menjadi kurang begitu rapi, sehingga hal tersebut sangat tidak disarankan. Kadang kala anda perlu membuat sebuah file repository yang digunakan untuk memproses sesuatu yang sifatnya lebih kompleks atau lebih mengarah ke pemrosesan data dari atau ke external resources seperti API Endpoint.

2. Model

Jika anda datang dari latar belakang bahasa pemrograman javascript, silahkan hilangkan sedikit kebiasaan yang pernah anda lakukan disana meskipun beberapa cara disana masih bisa digunakan akan tetapi ada beberapa hal yang tidak bisa diterapkan disini. Model digunakan untuk mendeklarasikan suatu object, pada kasus tertentu mungkin anda akan membutuhkan suatu variable yang berisikan suatu objek.

Catatan Penting

Sebelum anda memulai tutorial ini pastikan versi flutter_bloc yang anda gunakan sesuai dengan tutorial yang ada, perbedaan versi flutter_bloc mungkin akan memiliki perbedaan sehingga tutorial ini tidak akan relevan dalam waktu tertentu.

Sebelum anda ingin memulai tutoral ini pastikan project sudah terbuka dan project tersebut telah menggunakan packages flutter_bloc: ^6.1.1 dan equatable yang dapat anda lihat pada file pubspec.yaml bagian depedencies.

dependencies:
  flutter_bloc: ^6.1.1

Diperbarui pada : 25 Januari 2021

Persiapan Membuat Simulasi Login

Dalam tutorial ini akan membahas mengenai bagaimana cara membuat simulasi ketika pengguna ingin melakukan login, harap dicatat ini hanya simulasi sehingga kedepannya anda mungkin dapat menggunakan tutorial ini untuk project nyata.

Pada tutorial ini saya akan membuat 5 file yang saya letakkan pada direktori lib/login/bloc yaitu :

  1. login_model.dart
  2. login_repository.dart
  3. login_state.dart
  4. login_event.dart
  5. login_bloc.dart

File yang terdapat pada nomor 1 dan 2 hanya dibuat secara manual sedangkan file nomor 3 sampai nomor 5 dapat dibuat secara otomatis dengan menggunakan bantuan ekstensi bloc_library.

login_model.dart

Untuk kasus ini saya hanya mendeklarasikan class LoginErrorModel untuk membuat object yang berisikan status dan value untuk menerapkan error handling.

class LoginErrorModel {
  bool status;
  String value;

  LoginErrorModel({this.status, this.value});
}

login_state.dart

Sebelum kita masuk ke repository pastikan state dari bloc sudah dibuatkan, pada BLoC Library kita dapat menggunakan state hingga lebih dari dua kondisi seperti contohnya ketika state dalam keadaan unintialize dan initialize seperti contoh berikut :

abstract class CounterState {}

class CounterUninitialized extends CounterState {}

class CounterLoaded extends CounterState {
  int counter;
  CounterLoaded({this.counter});

  CounterLoaded copyWith({int counter}) {
    return CounterLoaded(
      counter: counter
    );
  }
}

Namun karena pada kasus ini hanya terdapat satu jenis kondisi maka anda hanya cukup mendeklarasikan satu state utama.

part of 'login_bloc.dart';

class LoginState extends Equatable {
  final String username;
  final String password;
  final bool loading;
  final LoginErrorModel error;

  LoginState({this.username, this.password, this.loading, this.error});


  LoginState copyWith({
    username,
    password,
    loading,
    error
  }) {
    return LoginState(
        username: username ?? this.username,
        password: password ?? this.password,
        loading: loading ?? this.loading ?? false,
        error : error ?? this.error
    );
  }

  @override
  List<Object> get props => [username, password, loading, error];

}

Ada beberapa hal yang harus diperhatikan dalam membuat bloc state menggunakan pada kode berikut yaitu :

  1. LoginState memiliki method bernama copyWith() yang digunakan untuk menambah dan mengubah state yang sudah ada, perlu diketahui bahwa penggunaan method copyWith tanpa membuat object login state dapat menyebabkan pembuatan state baru sehingga state yang lama tidak dapat diperbarui.
  2. Dalam method copyWith parameter dari copyWith akan diletakkan paling depan sebelum state yang telah anda buat, ketika anda menempatkan parameter class diurutan pertama maka sangat dimungkinkan perubahan state tidak dapat dilakukan.
  3. Karena object login state menggunakan extend equatable, maka pastikan kamu sudah melakukan override pada get props sehingga nantinya state dapat direturn sesuai dengan properties yang ingin direturn dan pastikan beberapa variable dari perintah copyWith sama dengan yang ada pada array get props, karena ketika salah satu tidak ada maka variable tersebut sangat memungkinkan tidak dapat dibaca pada file utama bloc.

login_repository.dart

Simulasi berawal di file ini, repositori biasanya digunakan untuk meminta atau mengirim data ke external resources seperti API endpoint. Jadi beberapa proses yang terkadang melibatkan external resources ada baiknya untuk membuat sebuah repository, pada file repository pastikan untuk mengimport file utama login_bloc karena nantinya kita akan membutuhkan LoginState untuk membaca value yang dikirimkan pengguna.

import 'login_bloc.dart';

class LoginRepository {
  Future<bool> submitLogin(LoginState value) async {
    if (value.username == 'admin' && value.password == 'admin') {
      return true;
    } else {
      return false;
    }
  }
}

Pada file ini saya membuat satu buah method SubmitLogin yang menghasilkan kembalian data boolean, di dalamnya saya hanya menggunakan percabangan sederhana apakah inputan yang dimasukkan telah sesuai dengan kondisi atau tidak.

login_event.dart

File ini digunakan untuk menghandle event yang dikirimkan oleh UI untuk diolah ke file bloc utama, di react redux anda dapat menganggap bahwa login_event ini merupakan sebuah actions sebelum masuk ke reducers.

part of 'login_bloc.dart';

abstract class LoginEvent{
  const LoginEvent();
}

class LoginUsernameChanged extends LoginEvent {
  LoginUsernameChanged(this.username);
  final String username;
}

class LoginPasswordChanged extends LoginEvent {
  LoginPasswordChanged(this.password);
  final String password;
}

class LoginIsLoading extends LoginEvent {
  LoginIsLoading();
}

class LoginIsLoaded extends LoginEvent {
  LoginIsLoaded();
}

class LoginSubmitted extends LoginEvent {
  LoginSubmitted();
}

class LoginErrorHasRetrive extends LoginEvent {
  LoginErrorHasRetrive();
}

Setiap operasi yang dilakukan pada bloc_library (bukan cubit provider) berjalan berdasarkan event yang telah ditentukan, sehingga ketika aplikasi ingin memproses, membuat ataupun mengubah state dengan bloc library maka event harus di deklarasikan terlebih dahulu. Selain sebagai penanda untuk mengeksekusi sebuah proses, kalian dapat mengirimkan parameter pada event tersebut untuk di eksekusi ke dalam proses utama bloc.

login_bloc.dart

Pada file utama bloc, saya akan melakukan pengecekan kondisi terlebih dahulu terhadap event yang masuk sebelum melakukan pemrosesan data yang sebenarnya.

import 'dart:async';

import 'package:your_packages/login/bloc/login_model.dart';
import 'package:your_packages/login/bloc/login_repository.dart';
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';


part 'login_event.dart';
part 'login_state.dart';

class LoginBloc extends Bloc<LoginEvent, LoginState> {
  LoginBloc() : super(LoginState());

  @override
  Stream<LoginState> mapEventToState(
    LoginEvent event,
  ) async* {
    if(event is LoginUsernameChanged) {
      yield _mapUsernameChangedToState(event, state);
    } else if(event is LoginPasswordChanged) {
      yield _mapPasswordChangedToState(event, state);
    } else if (event is LoginSubmitted) {
      yield* _mapLoginSubmittedToState(event, state);
    } else if (event is LoginErrorHasRetrive) {
      yield* _mapLoginErrorHasRetrieveToState(event, state);
    }
  }
  
  .........
}

Jika dilihat dari isi percabangan saya akan mengecek terlebih dahulu event apa yang masuk pada bloc, ketika kondisi dari percabangan tersebut terpenuhi maka langkah selanjutnya adalah mengeksekusi method yang mengarah ke salah satu perubahan state. Jadi ketika event yang diterima adalah LoginUsernameChanged maka method_mapUsernameChangedToState akan dieksekusi dengan membawa parameter event dan state.

  LoginState _mapUsernameChangedToState(LoginUsernameChanged event, LoginState state) {
    final String username = event.username;
    return state.copyWith(
      username: username
    );
  }

Keterangan :

  • Parameter event yang terdapat pada method _mapUsernameChangedToState berisi mengenai data event yang telah dibuat berdasarkan parameter yang telah dikirimkan, jika event yang bersangkutan tidak memiliki variable maka kalian tidak perlu memasukkan event ke dalam parameter _mapUsernameChangedToState.
  • Parameter state yang terdapat pada method _mapUsernameChangedToState merupakan state dari login bloc, ini wajib dimasukkan karena nantinya nilai state akan diubah dan akan dikembalikan ke login bloc.

Beberapa proses yang harusnya bisa langsung dieksekusi pada percabangan saya sengaja pisah agar proses maintain aplikasi dan penambahan aplikasi dapat terfokus pada salah satu method sehingga error trace dapat dilakukan secara mudah.

Untuk beberapa method yang berjalan secara async*, pastikan untuk menggunakan Stream untuk memproses event tersebut sehingga anda dapat menunggu proses tersebut selesai sehingga tidak ada kode yang berjalan secara paralel.

  Stream<LoginState> _mapLoginSubmittedToState(LoginSubmitted event, LoginState state) async* {
    yield state.copyWith(
        loading: true
    );
    final bool = await LoginRepository().submitLogin(state);

    if(bool == true) {
      yield state.copyWith(
        loading: false
      );
    } else {
      try {
        var error = new LoginErrorModel();
        error.status = true;
        error.value = 'Username atau password yang anda masukkan salah';

        yield state.copyWith(
            loading: false,
            error: error
        );
      } catch(err) {
        print(err);
      }
    }
  }

Hasil akhir dari file ini dapat anda lihat dibawah ini :

import 'dart:async';

import 'package:bbd_client/login/bloc/login_model.dart';
import 'package:bbd_client/login/bloc/login_repository.dart';
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';


part 'login_event.dart';
part 'login_state.dart';

class LoginBloc extends Bloc<LoginEvent, LoginState> {
  LoginBloc() : super(LoginState());

  @override
  Stream<LoginState> mapEventToState(
    LoginEvent event,
  ) async* {
    if(event is LoginUsernameChanged) {
      yield _mapUsernameChangedToState(event, state);
    } else if(event is LoginPasswordChanged) {
      yield _mapPasswordChangedToState(event, state);
    } else if (event is LoginSubmitted) {
      yield* _mapLoginSubmittedToState(event, state);
    } else if (event is LoginErrorHasRetrive) {
      yield* _mapLoginErrorHasRetrieveToState(event, state);
    }
  }

  LoginState _mapUsernameChangedToState(LoginUsernameChanged event, LoginState state) {
    final String username = event.username;
    return state.copyWith(
      username: username
    );
  }

  LoginState _mapPasswordChangedToState(LoginPasswordChanged event, LoginState state) {
    final String password = event.password;
    return state.copyWith(
      password: password
    );
  }

  Stream<LoginState> _mapLoginSubmittedToState(LoginSubmitted event, LoginState state) async* {
    yield state.copyWith(
        loading: true
    );
    final bool = await LoginRepository().submitLogin(state);

    if(bool == true) {
      yield state.copyWith(
        loading: false
      );
    } else {
      try {
        var error = new LoginErrorModel();
        error.status = true;
        error.value = 'Username atau password yang anda masukkan salah';

        yield state.copyWith(
            loading: false,
            error: error
        );
      } catch(err) {
        print(err);
      }
    }
  }

  Stream<LoginState> _mapLoginErrorHasRetrieveToState(LoginErrorHasRetrive event, LoginState state) async* {
    var error = new LoginErrorModel();
    error.status = false;
    error.value = null;

    yield state.copyWith(
      error: error
    );
  }
}

Implementasi ke User Interface

Dalam skenario aplikasi real world, penggunaan BLoC tidak selalu atau terpaku pada satu modul saja sehingga setiap BLoC yang dibuat harus terdaftar terlebih dahulu pada file main.dart. Umumnya anda dapat menggunakan BlocProvider untuk menghubungkan bloc ke user interface, akan tetapi penggunaan BlocProvider hanya terpaut pada satu modul BloC sehingga anda mungkin perlu MultiBlocProvider untuk mendaftarkan BLoC yang anda buat.

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

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MultiBlocProvider(
      providers: [
        BlocProvider<LoginBloc>(create: (BuildContext context) => LoginBloc()),
        BlocProvider<CounterBloc>(create: (BuildContext context) => CounterBloc())
      ],
      child: MaterialApp(
          theme: ThemeData(
            primarySwatch: Colors.blue,
            visualDensity: VisualDensity.adaptivePlatformDensity,
          ),
          home: CounterView(),
        )
    );
  }
}

Selanjutnya saya akan membuat satu view baru bernama login pada direktori /lib/view, view ini menggunakan StatefullWidget sehingga meskipun begitu proses yang berhubungan dengan logik bisnis juga akan tetap dipisah.

Untuk pertama - tama saya akan membuat BlocListener untuk mengetahui perubahan state yang terjadi pada BLoC, pada kasus ini saya menggunakan BLoC listener untuk melakukan error handling ketika username & password salah maka saya akan menjalankan perintah print(). Dalam kasus yang lebih spesifik anda dapat menggunakan shackbar untuk menampilkan popup info jika password salah.

import 'package:your_packages/login/bloc/login_bloc.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';

class LoginView extends StatefulWidget {
  @override
  _LoginViewState createState() => _LoginViewState();
}

class _LoginViewState extends State<LoginView> {


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

  @override
  void dispose() {
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return BlocListener<LoginBloc, LoginState>(
      listener: (context, state) {
        if(state.error.status == true) {
          print(state.error.value);
          context.read<LoginBloc>().add(LoginErrorHasRetrive());
        }
      },
      child: Scaffold(
          appBar: AppBar(title: Text('Login')),
      ),
    );
  }
}

Selanjutnya saya akan membuat sebuah form login yang menerima inputan username dan password, beserta dengan tombol login pada body scaffold. Namun sebelum hal itu dilakukan saya akan membuat BlocBuilder untuk membungkus body scaffold tersebut agar dapat terhubung dengan Bloc yang telah dibuat.

child: Scaffold(
    appBar: AppBar(title: Text('Login')),
    body: BlocBuilder<LoginBloc, LoginState>(
      builder: (context, state) {
        return Padding(
          padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 32),
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              SizedBox(
                width: double.infinity,
                child: Text("Silahkan Masukkan Username dan Password untuk mengakses aplikasi ini."),
              ),
              Form(
                  key: _formKey,
                  child: Column(
                    mainAxisAlignment: MainAxisAlignment.start,
                    children: [
                      TextFormField(
                        textInputAction: TextInputAction.go,
                        keyboardType: TextInputType.text,
                        decoration: InputDecoration(
                          hintText: 'Masukkan Username',
                          labelText: 'Username',
                        ),
                        autocorrect: false,
                        enableSuggestions: false,
                        onEditingComplete: () {
                          FocusScope.of(context).requestFocus(passwordNode);
                        },
                        onChanged: (value) {
                          context.read<LoginBloc>().add(LoginUsernameChanged(value));
                        },
                        validator: (value) {
                          if(value.isEmpty) {
                            return 'Bagian ini wajib di isi !';
                          }
                          return null;
                        },
                      ),
                      TextFormField(
                        focusNode: passwordNode,
                        keyboardType: TextInputType.text,
                        decoration: InputDecoration(
                          hintText: 'Masukkan Password',
                          labelText: 'Password',
                        ),
                        obscureText: true,
                        autocorrect: false,
                        enableSuggestions: false,
                        onChanged: (value) {
                          context.read<LoginBloc>().add(LoginPasswordChanged(value));
                        },
                        validator: (value) {
                          if(value.isEmpty) {
                            return 'Bagian ini wajib di isi !';
                          }
                          return null;
                        },
                      )
                    ],
                  )
              ),
              Padding(
                padding: const EdgeInsets.only(top: 16),
                child: Row(
                  mainAxisAlignment: MainAxisAlignment.spaceBetween,
                  children: [
                    Expanded(
                      child: RaisedButton.icon(
                        icon: Icon(Icons.login),
                        elevation: 0,
                        label: Text('Login'),
                        onPressed: () => context.read<LoginBloc>().add(LoginSubmitted()),
                      ),
                    ),
                    SizedBox(
                      width: 16,
                    ),
                    Expanded(
                      child: RaisedButton.icon(
                        icon: Icon(Icons.history),
                        elevation: 0,
                        label: Text('Reset'),
                      ),
                    ),
                  ],
                ),
              ),
              SizedBox(
                width: double.infinity,
                child: RaisedButton.icon(
                  icon: Icon(Icons.vpn_key),
                  label: Text("Lupa Password"),
                ),
              )
            ],
          ),
        );
      }
    )
),

Sneak Peak Result

Setelah keseluruhan tutorial telah anda lakukan, mungkin saya bisa bagikan bagaimana aplikasi berjalan pada android emulator yang berjalan pada komputer saya. Anda dapat melihat hasil tutorial ini pada video yang terdapat pada link berikut.

Untuk hasil coddingan anda dapat melihatnya pada repositori github saya : https://github.com/ambrizals/flutter_login_simulation_with_bloc

Sedikit Catatan Tambahan

Pada tutorial ini terdapat satu tambahan packages yang dipasang yaitu bernama equatable, jika anda ingin mempelajari lebih lanjut mengenai equatable saya dapat mereferensikan untuk melihat video berikut.

Pembaruan Artikel

Tanggal 07 November 2020 saya melakukan perubahan terkait penjelasan yang terdapat pada bagian login_state dimana terdapat sedikit penjelasan mengenai penggunaan method copyWith().

Tanggal 25 Januari 2021 terdapat perubahan terkait penggunaan flutter_bloc versi 6.1.1, perubahan tersebut meliputi penghapusan method bloc() dan menggantinya dengan method baru seperti select, read dan watch. Anda dapat melihat pembaruan tersebut pada halaman migrations v6.1.0.

Ingin Berkomentar ?

Gunakan fitur komentar dengan bijak demi keamanan dan kenyamanan anda saat berselancar di dunia maya ini, mungkin undang - undang atau peraturan dari sebagian wilayah akan menjerat aktivitas yang ada pada kolom komentar.