ややめも

アプリ作りたいエンジニアのめも💁‍♀️

技術メモ
日記
つくったもの
就活の話

【Flutter】チーム開発でflutter_blocを導入したらめちゃめちゃ良かった

はじめに

研修でのチーム開発にて、某画像検索アプリのクローンのクライアント側をFlutterで実装しました。

状態管理手法としてBLoCパターンを採用し、その際にflutter_bloc というパッケージを使用しました。

使用感として

  • 誰が書いても同じコードスタイルになる
  • テストがしやすい

という印象をもち、大人数で開発する上では非常に使いやすいと感じたのでご紹介したいと思います。

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の定義

// home_event.dart

import 'package:equatable/equatable.dart';

abstract class HomeEvent extends Equatable {
  @override
  List<Object> get props => [];
}

class LoadData extends HomeEvent {}

データを変更させるためのイベントを定義します。

stateの定義

// home_state.dart

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は流れないようにすることができるからです。(間違っていたら指摘してください。)

blocの定義

// home_bloc.dart

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にてどのイベントがきたら、どのような状態の変化をするのかを記述します。

また、以下のようなファイルを準備しておくことで

// bloc.dart

export 'home_bloc.dart';
export 'home_event.dart';
export 'home_state.dart';

widgetでblocをimportする際は、以下のような記述で済むようになります。

import 'package:path/home_widget/bloc.dart';

widgetでのBlocの使用方法

BlocProviderwidgetを用いることで、BlocクラスをDIします。

// home_widget.dart

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を呼び出すことが可能です。

BlocBuilderwidgetを用いると、状態によってwidgetを変更することが出来ます。

Widget _buildStaggeredGridView() {
    return BlocBuilder<HomeBloc, HomeState>(builder: (context, state) {
      bloc = BlocProvider.of<HomeBloc>(context);
      if (state is LoadedState) {
        final books = state.books;
        return // booksを使ったwidget
      } else if (state is NoDataState) {
        return // データが無いときの表示処理
     } else if (state is LoadingState) {
        return // ローディング中の表示処理
     }
      return Container();
    });
  }
}

BlocConsumerwidgetを用いると、状態によって処理を分けることができます。

Widget _buildScreen(BuildContext context) {
    return BlocConsumer<HomeBloc, HomeState>(
        listener: (context, state) {
      if (state is LoadedState) {
        Navigator.pop(context); // データ取得処理が成功した場合のみpopする
      }
    }, builder: (context, state) {
      return // widgetを生成
    });

詳しくはflutter_blocをご覧ください。

テスト

テストに関するサポートもばっちりです。

bloclibrary.dev

詳しい説明は割愛しますが、次のようにStateをテストすることができます。

 blocTest<HomeBloc, HomeEvent, HomeState>(
        'テストの説明',
        build: () async {
        // blocを生成
        // モックの作成等はここで行う
      return HomeBloc(mockPinsRepository);

    }, act: (bloc) {
        // イベントの発火
      bloc.add(LoadData());
    }, expect: <HomeState>[LoadingState(), LoadedState(books)]); //期待されるState
}

気になるポイント🙋‍♀️

  • 小規模なアプリや個人の開発だと冗長な処理になりがち

この場合は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だと思うので、是非使ってみてください。

「リファクタリング - 既存のコードを安全に改善する」を読んでみた

おすすめされていた本の、「リファクタリング - 既存のコードを安全に改善する」を読んでみました!

本の内容

前半はJavaScriptで書かれたコードを題材に、この本で紹介されている原則に則ってリファクタリングをしていきます。

後半では前半のリファクタリングの際に用いた、様々な原則を丁寧なリファクタリング手法と共に説明されています。

より良いコードを書くためのテクニックをまとめた本といえば、「リーダブルコード」は名著として有名ですよね。

私は「リーダブルコード」も何度か読んでいますが、この本と比較すると「リファクタリング」はより具体例が豊富で、辞書的にも使いやすそうな書籍だなあという印象を持ちました。

参考になった具体例

確かに読みやすさが変わるなと思った項目を適当に2つ紹介していきます。

ファクトリ関数によるコンストラクタの置き換え

コンストラクタだけで初期化すると少し戸惑ってしまいそうな処理を、ファクトリ関数として置き換えることでよりわかりやすくできる原則のこと。 私自身はわりとシンプルにコンストラクタを使いがちだったので、これだけで扱いやすさがだいぶ変わるなという印象。

リファクタリング適用前
// sample.js

leadEngineer = new Employee(document.leadEngineer, 'E');
リファクタリング適用後
// sample.js

leadEngineer = createEngineer(document.leadEngineer);
     
func createEngineer(name) {
     return new Employee(name, 'E');
}

委譲によるスーパークラスの置き換え

継承は既存の機能を再利用するのに強力な手法ですが、 何でもかんでも継承をしてサブクラス化してしまうと、ただ処理が複雑になってしまったり、使われない操作までもが継承されてしまう場合がありますよね。

古典的な継承の失敗例として、StackをListのサブクラスにしていることが挙げられていました。(Javaを例にすると、実際に継承されているのはVectorで、VectorがListのinterfaceを実装しているもよう。) Stackを実装するために、Listのデータ構造や処理を再利用するために継承する設計になっていたようですが、Listの操作はStackにあまり適合していないイメージ。 docs.oracle.com

//sample.java

Stack<Integer> stack = new Stack<Integer>();
stack.push(2);
stack.add(1);

たしかにJavaだとStackにVectorクラスのaddが実装されているのは気持ち悪い…。

C#のStackのドキュメント見てみたら、こっちのStackは割とシンプルで良さそうだった。 docs.microsoft.com

話は逸れたが、スーパークラスの関数をサブクラスで活用出来ないような場合は継承するのではなく、Listをフィールドに保持しておき、必要な操作はStackに任せたほうがスッキリする。

リファクタリング適用前
// sample.js

class List {
// 具体的な処理
}
       
class Stack Extends List {
// 具体的な処理
}
リファクタリング適用後
// sample.js

class Stack {
          constructor() {
              this._storage = new List();
          }
       }
       
class List {
// 具体的な処理
}

まとめ

具体的な改善例が丁寧に書かれていて、納得感が多い事例ばかりでした!

長いけど、読む価値はあると思います!!!

FlutterでMockitoを使うときにレスポンスが返却されない問題

問題

  final Client _client;

  Future<Response> get(String relativeUrl, {Map<String, String> query}) async {
    final token = await _authenticationPreferences.getAccessToken();
    final header = {'token': token};
      _client.get(
        Uri.http(domain, relativeUrl, query),
        headers: header,
      ),
    );
  }

このように定義しているメソッド(headerを付与してGETリクエストを送る)に対して、テストを行う。

  group('ApiClient error handling', () {
    setUp(() async {
      //色々セットアップ
    });

    test(
        'when API return response with status code 401, [get] function should throw [UnauthorizedError]',
        () {
      when(mockHttpClient.get(any, headers: {}))
          .thenAnswer((_) => Future.value(Response('body', 401)));

      final getFuture = apiClient.get('401');
      expect(getFuture, throwsA(isInstanceOf<UnauthorizedError>()));
    });

mockito | Dart Packageを用いてテストを行うと以下のようなエラーが帰ってきていてレスポンスが返却されない。

NoSuchMethodError: The getter 'statusCode' was called on null.
Receiver: null
Tried calling: statusCode

解決策

      when(mockHttpClient.get(any, headers: anyNamed('headers')))
          .thenAnswer((_) => Future.value(Response('body', 401)));

      final getFuture = apiClient.get('401');
      expect(getFuture, throwsA(isInstanceOf<UnauthorizedError>()));


headers: anyNamed('headers')
ここが重要。

dart - Flutter/Mockito. Testing API provider with mockito, problem with api headers - Stack Overflow
こちらのStack Overflowによる実際のリクエストと合う形にしないとレスポンスが返却されないもよう。

最近読んだ本や漫画と積読書

今年は2記事/月は書く!と言っていたのに全く記事を書いていなかったので、
最近読んだ本とかについてゆる〜〜〜〜く書きました。
技術書やエンジニアの方が読むと面白い書籍が多い気がします。

読んだ📖

岩田さん 岩田聡はこんなことを話していた。

 
任天堂の岩田元社長のことばを集めた書籍なのですが、
エンジニアとして人としても岩田元社長の姿勢というか考え方を意識していきたいと思えるので定期的に読み返している一冊!
個人的には「MOTHER2」開発中のエピソードが衝撃的だったーーー。かっこよすぎるよ!!

f:id:yaya-w-1026:20200404205346p:plain:w200
岩田さん 岩田聡はこんなことを話していた。

以下のサイトで書籍の一部分が無償公開されているので、気になる人は是非読んでほしい。
www.1101.com

WEB DB Press vol115/vol110

今回の特集は全部興味がある内容だったから全体的にがっつりよんだ。
iOSの開発最近あんまりしてないから参考なったーー🙄
gihyo.jp

こっちはgRPC周りを使う予定だったのでそのために。よくまとまっていた。
ついでに載っていた名前付け大全も良かった。
名前付け関係の話って言われればわかるわかるーって思うんだけど、いざやるとわりと悩むことがむっちゃ多い。むずい。
gihyo.jp

これだからゲームづくりはやめられない!

ゲーム会社で働くエンジニアとデザイナーのお話!
ものづくりのモチベあがるううう!まじで読んでほしいい。
f:id:yaya-w-1026:20200404205633p:plain:w200
これだからゲーム作りはやめられない!(2)

ランウェイで笑って

ただただ二人を見守りたいから買い続けている。最近アニメ化されたーー。
f:id:yaya-w-1026:20200404210744p:plain:w200
ランウェイで笑って(15)

積読📦

ノンデザイナーズ・デザインブック

個人アプリの開発の参考のために!
色んな人からもおすすめだーーってコメント来たけどまだ途中。

f:id:yaya-w-1026:20200404210239p:plain:w200
ノンデザイナーズ・デザインブック

リファクタリング 既存のコードを安全に改善する

いつも自分のためのコードしか書いてこなかった私がバイト中に大反省した暁に購入したやつ。
ちょこちょこ直していきたい部分を読みすすめてる。もうちょっとしっかり読んだら詳しい記事書くかもしれない。
f:id:yaya-w-1026:20200404210900p:plain:w200
リファクタリング 既存のコードを安全に改善する