はじめに
研修でのチーム開発にて、某画像検索アプリのクローンのクライアント側をFlutterで実装しました。
状態管理手法としてBLoCパターンを採用し、その際にflutter_bloc というパッケージを使用しました。
使用感として
- 誰が書いても同じコードスタイルになる
- テストがしやすい
という印象をもち、大人数で開発する上では非常に使いやすいと感じたのでご紹介したいと思います。
BLoCとは Business Logic Component の略で、簡潔に説明するとUIからビジネスロジックを分離する設計パターンです。
2018年に開かれたDart Conferenceで発表されています。
BLoCの全体像やガイドラインに関しては、以下の記事が非常にわかりやすいのでぜひご覧ください。
【Dart/Flutter】導入したBLoCパターンアーキテクチャについて全体像をまとめてみた
このBLoCパターンを実現するには、DartのStreamを用いてデータを送り、providerを用いて、必要なWidgetにDIする手法がよく使われるかと思います。
以下参考記事。
長めだけどたぶんわかりやすいBLoCパターンの解説
flutter_bloc✨
flutter_blocは、上記で説明したBLoCデザインパターンの実装を容易にすることができるpackageです。
手法としては、UI側から何かしらのeventを送ることで、blocを通じてstateを変更するという方法で状態を変更しています。
[引用: https://pub.dev/packages/bloc]
lib/
├ api/
├ data/
├ model/
├ util/
├ values/
├ view/
└ main/
└ home/
├ home_widget.dart
└ bloc/
└ bloc.dart
└ home_bloc.dart
└ home_event.dart
└ home_state.dart
eventの定義
import 'package:equatable/equatable.dart';
abstract class HomeEvent extends Equatable {
@override
List<Object> get props => [];
}
class LoadData extends HomeEvent {}
データを変更させるためのイベントを定義します。
stateの定義
import 'package:equatable/equatable.dart';
import 'package:path/model/book_model.dart';
abstract class HomeState extends Equatable {
@override
List<Object> get props => [];
}
class LoadingState extends HomeState {}
class LoadedState extends HomeState {
LoadedState(this.books);
final List<BookModel> books;
@override
List<Object> get props => [books];
}
class NoDataState extends HomeState {}
class ErrorState extends HomeState {
ErrorState(this.exception);
final Exception exception;
}
アプリの取りうるState(状態)を記述します。
StateにてEquatable packageを使う理由としては、
propsに入っているプロパティが一致しているかを判断することができ、これらの値が全く一緒であればイベントをaddしたとしても、Streamは流れないようにすることができるからです。(間違っていたら指摘してください。)
import 'package:bloc/bloc.dart';
import 'package:path/data/books_repository.dart';
import 'package:path/model/book_model.dart';
import 'bloc.dart';
class HomeBloc extends Bloc<HomeEvent, HomeState> {
HomeBloc(this._booksRepository);
final BooksRepository _booksRepository;
@override
HomeState get initialState => LoadingState();
@override
Stream<HomeState> mapEventToState(HomeEvent event) async* {
if (event is LoadData) {
try {
final books = await _booksRepository.getBooks();
if (books.isEmpty) {
yield NoDataState();
return;
}
yield LoadedState(books);
} on Exception catch (e) {
yield ErrorState(e);
}
}
}
}
mapEventToState
にてどのイベントがきたら、どのような状態の変化をするのかを記述します。
また、以下のようなファイルを準備しておくことで
export 'home_bloc.dart';
export 'home_event.dart';
export 'home_state.dart';
widgetでblocをimportする際は、以下のような記述で済むようになります。
import 'package:path/home_widget/bloc.dart';
BlocProvider
widgetを用いることで、BlocクラスをDIします。
Widget build(BuildContext context) {
return BlocProvider<HomeBloc>(
create: (context) =>
HomeBloc(context.repository<BooksRepository>())..add(LoadData()),
child: : _buildScreen(context),
);
}
add(EventClass())
メソッドを使うことによってイベントを発火することができます。
またDIした後は、BlocProvider.of<HomeBloc>(context);
のようにblocを呼び出すことが可能です。
BlocBuilder
widgetを用いると、状態によってwidgetを変更することが出来ます。
Widget _buildStaggeredGridView() {
return BlocBuilder<HomeBloc, HomeState>(builder: (context, state) {
bloc = BlocProvider.of<HomeBloc>(context);
if (state is LoadedState) {
final books = state.books;
return
} else if (state is NoDataState) {
return
} else if (state is LoadingState) {
return
}
return Container();
});
}
}
BlocConsumer
widgetを用いると、状態によって処理を分けることができます。
Widget _buildScreen(BuildContext context) {
return BlocConsumer<HomeBloc, HomeState>(
listener: (context, state) {
if (state is LoadedState) {
Navigator.pop(context);
}
}, builder: (context, state) {
return
});
詳しくはflutter_blocをご覧ください。
テスト
テストに関するサポートもばっちりです。
bloclibrary.dev
詳しい説明は割愛しますが、次のようにStateをテストすることができます。
blocTest<HomeBloc, HomeEvent, HomeState>(
'テストの説明',
build: () async {
return HomeBloc(mockPinsRepository);
}, act: (bloc) {
bloc.add(LoadData());
}, expect: <HomeState>[LoadingState(), LoadedState(books)]);
}
気になるポイント🙋♀️
- 小規模なアプリや個人の開発だと冗長な処理になりがち
この場合はsetState()
やStateNotifier
パターンなど、もっとシンプルなものを選べば良いと思います。
bloc、event、state用のクラスをわざわざ作成する必要があり、記述量がどうしても増えてしまいます。
コード生成ライブラリであるhttps://pub.dev/packages/freezed freezed packageを活用するのも一つの手です。
以下のサイトにはflutter_blocにてfreezedを使用した例が書かれています。
Flutter - bloc with freezed
まとめ
flutter_blocではbloc、event、statteを定義し、そこからデータをwidgetに流すという方針をとっているため、誰が書いても似たようなコードスタイルになりました。
blocのUIガイドラインには、
Each "complex enough" component has a corresponding BLoC.
という記述があります。
今回は1widgetに対して1blocを用意するという原則を守ることで、どこに何の処理が書いているかを判別しやすくなりました。
複数人で開発する際には非常に役に立つpackageだと思うので、是非使ってみてください。