BLoC Pattern pada Flutter

Nurul Faza S.
LEARNFAZZ
Published in
6 min readApr 15, 2019

Dalam programming, terkadang programmer menerapkan suatu teknik desain yang hanya bisa memenuhi suatu permasalahan. Namun, ketika teknik desain tersebut diterapkan pada permasalahan yang lebih luas, maka teknik tersbut menjadi sulit untuk diaplikasikan. Oleh karena itu, dibutuhkan sebuah solusi umum yang dapat diaplikasikan sebagai teknik desain pada berbagai permasalahan. Solusi umum tersebut biasa disebut sebagai design pattern.

Dalam software engineering, design pattern adalah solusi berulang yang umum untuk masalah yang biasa terjadi dalam desain perangkat lunak[1]. Design pattern merupakan deskripsi atau template untuk menyelesaikan masalah yang dapat digunakan dalam berbagai situasi.

sumber : https://dev.jimdo.com/bloc-architecture-in-flutter/

BLoC

Bloc dirancang dengan tiga nilai inti sebagai berikut:

  • Simple : Mudah dipahami dan bisa digunakan oleh developer dengan berbagai tingkat keterampilan
  • Powerful : Dapat membantu pembuatan aplikasi kompleks yang tersusun dari komponen-komponen yang lebih kecil
  • Testable : Mudah ditest

BLoC atau Business Logic Component design pattern adalah design pattern yang membantu untuk memisahkan presentation dengan business logic. Sehingga component pada project terbagi menjadi presentational component, BLoC, dan backend. Pattern ini membolehkan developer untuk fokus dalam mengkonversikan event menjadi state.

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

Terdapat beberapa istilah yang digunakan dalam penerapan BLoC, yaitu:

  • Events adalah input untuk Bloc. Biasanya, event ini merupakan UI events seperti tombol yang ditekan atau load halaman.
  • States adalah output dari Bloc. Komponen dari UI dapat didefinisikan sebagai state dan bisa digambar ulang sesuai dengan state mereka saat ini.
  • Transitions adalah perubahan dari satu state ke state lain. Transition terjadi ketika suatu event dikirim setelah mapEventToState dipanggil, tetapi sebelum state dari Bloc diupdate. Transition terdiri atas current state, event, dan next state.
  • Blocs adalah komponen yang mengonversi Stream dari event ke Stream yang mengubah state

Penerapan BLoC pada LEARNFAZZ

Berikut ini merupakan salah satu contoh penerapan BLoC pattern pada pengambilan Post List untuk ditampilkan pada Course Page.

Event

Event yang ada pada penerapan ini adalah GetPostList.

import 'package:equatable/equatable.dart';
import 'package:meta/meta.dart';

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

class GetPostList extends PostListEvent {
GetPostList({@required this.offset, @required this.courseId}) : super([offset, courseId]);

final int offset;

final int courseId;

@override
String toString() =>
'GetPostList {courseId: $courseId, offset: $offset }';
}

State

State yang ada pada penerapan ini adalah

  • GetPostListLoading : state yang menyatakan bahwa pengambilan post list masih dalam proses pemuatan
  • GetPostListSuccess : state yang menyatakan bahwa pengambilan post list sudah sukses
  • GetPostListFailure : state yang menyatakan bahwa pengambilan post list gagal
import 'package:equatable/equatable.dart';
import 'package:learn_fazz/models/post_list_result.dart';

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

class GetPostListLoading extends PostListState {
@override
String toString() => 'GetPostListLoading';
}

class GetPostListSuccess extends PostListState {
GetPostListSuccess(this.result) : super([result]);

final PostListResult result;

@override
String toString() =>
'GetPostListSuccess { result: $result }';
}

class GetPostListFailure extends PostListState {
GetPostListFailure(this.error) : super([error]);

final String error;

@override
String toString() => 'GetPostListFailure { error: $error }';
}

Bloc

Pada Bloc dilakukan pemanggilan initial state yaitu GetPostListSuccess dengan PostListResult kosong. Setiap Bloc harus mengimplementasikan fungsi mapEventToState. Fungsi ini mengambil event yang masuk sebagai argumen dan harus mengembalikan Stream dari state baru yang akan ditampilkan pada UI. Fungsi mapEventToState pada Bloc ini diterapkan ketika ada event GetPostList dengan pertama-tama menjalankan loading state. Kemudian dilakukan try untuk pengambilan post list dengan memanggil fungsi getPostList pada post repository. Jika berhasil, maka akan dikembalikan state GetPostListSuccess. Namun, apabila pengambilan post list gagal, maka akan dikembalikan state GetPostListFailure.

import 'dart:async';
import 'package:bloc/bloc.dart';

class PostListBloc extends Bloc<PostListEvent, PostListState> {
PostListBloc({@required PostRepository postRepository})
: _postRepository = postRepository;

final PostRepository _postRepository;

PostListResult feedListResultAggregate = PostListResult.empty();

@override
PostListState get initialState {
return GetPostListSuccess(PostListResult.empty());
}

@override
Stream<PostListState> mapEventToState(PostListEvent event) async* {
print('PostListBloc: Event $event started');

if (event is GetPostList) {
yield GetPostListLoading();
try {
PostListResult result =
await _postRepository.getPostList(event.courseId, event.offset);
feedListResultAggregate.objects.addAll(result.objects);
feedListResultAggregate.count += result.count;
yield GetPostListSuccess(feedListResultAggregate);
} catch (e) {
yield GetPostListFailure(e.toString());
}
}

print('PostListBloc: Event $event completed');
}
}

Course Page UI

Setiap Bloc memiliki metode dispatch. Dispatch akan mengambil event dan memicu pemanggilan mapEventToState. Dispatch dipanggil di Course Page UI pada fungsi didChangeDependencies pada penerapan ini.

@override
void didChangeDependencies() {
super.didChangeDependencies();
if (postListBloc == null) {
postListBloc = widget.postListBloc ??
PostListBloc(
postRepository: AppConfig.of(context).postRepository,
);
postListBloc
.dispatch(GetPostList(courseId: widget.course.id, offset: 0));
}
}

Pada course page UI, Bloc diterapkan pada PostListWidget. Widget ini akan menampilkan post list sesuai dengan state yang sedang terjadi.

  • Pada saat state adalah GetPostListLoading, maka akan ditampilkan circulat progess indicator
  • Pada saat state adalah GetPostListFailure, maka akan ditampilkan snack bar yang menujukkan pesan error
  • Pada saat state adalah GetPostListSuccess, maka akan ditampilkan list post dari sebuah course dalam bentuk cards.
class PostListWidget extends StatelessWidget {
const PostListWidget({
Key key,
@required this.postListBloc,
@required this.courseId,
}) : super(key: key);

final PostListBloc postListBloc;
final int courseId;

Widget build(BuildContext context) {
return BlocBuilder(
bloc: postListBloc,
builder: (BuildContext context, PostListState state) {
print('State changed to: ' + state.toString());

Widget child = Container();
if (state is GetPostListLoading) {
child = Center(child: CircularProgressIndicator());
} else if (state is GetPostListFailure) {
SchedulerBinding.instance.addPostFrameCallback((_) {
Scaffold.of(context).showSnackBar(SnackBar(
content: Text(state.error),
));
});
} else if (state is GetPostListSuccess) {
if (state.result.count == 0) {
return Center(child: Text('This course has no feed yet.'));
}
List<Widget> listOfFeedCard = state.result.objects
?.map((feedDetailResult) => PostFeedCard(
feedDetailResult: feedDetailResult,
))
?.toList();
child = Column(
children: listOfFeedCard,
);
}
return child;
},
);
}
}

Mengapa Bloc?

Kelompok kami menggunakan Bloc karena Bloc dapat memisahkan komponen logic dan presentation. Pemisahan komponen logic dan presentation akan memudahkan maintenance kode. Misalnya, pada saat suatu implementasi kode dirubah, maka tidak diperlukan banyak perubahan pada kode yang lainnya. Selain itu, Bloc dapat memudahkan kita untuk mengetahui aplikasi sedang berada di state yang mana.

Refactoring

Refactoring yang sudah saya lakukan adalah pada fitur Create Comment. Pada awalnya fungsi createComment memiliki implementasi sebagai berikut.

Create comment bloc event

class CreateComment extends CreateCommentEvent {
final int courseId;
final int postId;
final String content;

CreateComment({this.courseId, this.postId, this.content}) : super([courseId, postId, content]);

@override
String toString() {
return 'CreateComment { courseId: $courseId, post: $postId, content: $content }';
}
}

Fungsi createComment pada comment repository

#comment repository
Future<CommentDetailResult> createComment(
{@required int courseId, @required int postId, @required String content}) async {
CommentDetailResult createdComment;
try {
final Response<Map<String, dynamic>> response =
await _httpClient.post<Map<String, dynamic>>(
createCommentSiteUrl(courseId, postId),
data: <String, dynamic>{'content': content},
);
createdComment = CommentDetailResult.fromJson(response.data);
} catch (e) {
throw e;
}
return createdComment;
}

Fungsi handle untuk button pada comment page

handleOnPostButtonPressed() {
FormState form = formKey.currentState;
form.save();
createCommentBloc
.dispatch(CreateComment(courseId: widget.courseId, postId: widget.post.id, content: commentPost));
}

Kemudian saya melakukan refactoring agar hanya dibutuhkan satu parameter untuk membuat comment yaitu dengan cara membuat model specification CreateCommentSpec. Tujuan dari refactoring ini adalah untuk membuat kode lebih rapi.

File CreateCommentSpec

import 'package:dio/dio.dart';
import 'package:learn_fazz/models/lib/serializable.dart';
import 'package:meta/meta.dart';

class CreateCommentSpec implements FormDataSerializable {
int courseId;
int postId;
String content;

CreateCommentSpec(
{@required this.courseId,
@required this.postId,
this.content = '',
});

@override
FormData serialize() {
return FormData.from({'content': content});
}

@override
String toString() {
return '{ courseId: $courseId, content: $content, postId: $postId} }';
}
}

CreateComment bloc event

class CreateComment extends CreateCommentEvent {
final CreateCommentSpec spec;

CreateComment({this.spec}) : super([spec]);

@override
String toString() {
return 'CreateComment { spec: ${spec.toString()} }';
}
}

Fungsi createComment pada comment repository

Future<CommentDetailResult> createComment(
CreateCommentSpec spec) async {
CommentDetailResult createdComment;
try {
final Response<Map<String, dynamic>> response =
await _httpClient.post<Map<String, dynamic>>(
createCommentSiteUrl(spec.courseId, spec.postId),
data: spec.serialize(),
);
return CommentDetailResult.fromJson(response.data);
} catch (e) {
throw e;
}
}

Fungsi handle untuk button pada comment page

handleOnPostButtonPressed() {
FormState form = formKey.currentState;
form.save();
createCommentBloc.dispatch(CreateComment(spec: spec));
}

Referensi :

  1. Design Patterns. Source Making. Web. 1 April 2019. https://sourcemaking.com/design_patterns
  2. Bloc. Dart Packages. Web. 15 April 2019. https://pub.dartlang.org/packages/bloc
  3. Reactive Programming — Streams — BLoC. Didier Boelens . Web. 1 April 2019. https://www.didierboelens.com/2018/08/reactive-programming---streams---bloc/
  4. Bloc. Github. Web. 15 April 2019. https://felangel.github.io/bloc/

--

--