Flutter アプリを

BLoC パターンでリファクタリングし、

BLoC を DI してみた

2018/07/19

Flutter Meetup Tokyo #3

About me

About me

Agenda

  • BLoC パターンおさらい
  • BLoC パターンでリファクタリング
  • Flutter の DI の現状
  • BLoC を DI してみた
  • 所感

BLoC パターン

おさらい

Business Logic Component

  • Item1
  • Item2
  • Item3

Business Logic Component

  • Item1
  • Item2
  • Item3

BLoC パターンで

リファクタリング

作っているアプリ

  1. TextField に英単語を入力
  2. WebAPI を使って、その英単語を使っている例文を検索
  3. 検索結果を ListView に表示

作っているアプリ

  1. 英単語を WebAPI で問い合わせ
  2. レスポンスを setState で反映
new TextField(
    controller: _controller,
    decoration: new InputDecoration(hintText: 'Type word'),
    onSubmitted: (String inputValue) {
        if (inputValue.isEmpty) {
        return;
        }

        final String query = inputValue.toLowerCase();
        this.fetchSentences(query).then((_list) {
            setState(() {
                _searchResults = _list;
            });
        });
    }
)

作っているアプリ

setState で反映された値で ListView を生成

new Expanded(
    child: new ListView(
        children: ListTile.divideTiles(
            context: context,
            tiles: _searchResults.map(
              (SearchResult searchResult) {
                return new ListTile(
                  title: formatString(searchResult.word),
                  trailing: new Row(...

Let's refactoring

1.BLoC の定義

  • Item1
  • Item2
  • Item3
class SearchBloc {
  // Inputs
  final _query = new BehaviorSubject<String>();
  Sink<String> get query => _query.sink;

  // Outputs
  Stream<List<SearchResult>> _results = Stream.empty();
  Stream<List<SearchResult>> get results => _results;

  SearchBloc() {
    _results = _query
                    .distinct()
                    .switchMap((query) => new Observable.fromFuture(_search(query)))
                    .map(_xmlToSearchResults)
                    .asBroadcastStream();
  }
...
}

2. Sink の処理

入力値を Sink に渡すだけ

new Padding(
  padding: const EdgeInsets.all(16.0),
  child: new TextField(
    controller: _controller,
    decoration: new InputDecoration(hintText: 'Type word'),
    onSubmitted: (String query) {
      _searchBloc.query.add(query);
    },
  ),
),

3.Stream の処理

  • ListView を StreamBuilder でラップ
  • 引数の snapshot.data からデータを取得
  StreamBuilder<List<SearchResult>>(
    stream: _searchBloc.results,
    builder: (context, snapshot) {
      return new ListView(
        children: ListTile
            .divideTiles(
              context: context,
              tiles: snapshot.data.map((SearchResult searchResult) {
                return new ListTile(
                  title: _formatString(context, searchResult.word),
                  trailing: new Row(...

4. BLoC はどう生成する?

InheritedWidget パターンを使う

InheritedWidget パターン

  1. InheritedWidget クラスを継承する
  2. of メソッドの実装
class SearchProvider extends InheritedWidget {
  final SearchBloc searchBloc;

  SearchProvider({Key key, SearchBloc searchBloc, Widget child})
      : searchBloc = searchBloc ?? new SearchBloc(),
        super(key: key, child: child);

  @override
  bool updateShouldNotify(InheritedWidget oldWidget) => true;

  static SearchBloc of(BuildContext context) =>
      (context.inheritFromWidgetOfExactType(SearchProvider) as SearchProvider)
          .searchBloc;
}

InheritedWidget パターン

3. 対象ウィジェットをラップ
    ラップしたときに BLoC インスタンスを生成

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return SearchProvider(
      searchBloc: SearchBloc(),
      child: new MaterialApp(
        title: 'Flutter Demo',
        theme: new ThemeData(primarySwatch: Colors.pink),
        home: new MyHomePage(title: 'Flutter Demo Home Page'),
      )
    );
  }
}

InheritedWidget パターン

4. build メソッドの中で of メソッド経由で   
    BLoC インスタンスを取得

class Search extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final _searchBloc = SearchProvider.of(context);
    final _controller = new TextEditingController();
    return new Column(
      children: <Widget>[
          new TextField(
            controller: _controller,
            decoration: new InputDecoration(hintText: 'Type word'),
            onSubmitted: (String query) {
              _searchBloc.query.add(query);
            },
         ),
        new Expanded(
          child: StreamBuilder<List<SearchResult>>(
            stream: _searchBloc.results,
            builder: (context, snapshot) {
              return new ListView(
                children: ListTile
                    .divideTiles(
                      context: context,
                      tiles: snapshot.data.map((SearchResult searchResult) {
                        return new ListTile(
                          title: _formatString(context, searchResult.word),
                          trailing: new Row(...

うーん、InheritedWidget 作るのめんどくさい

BLoC を使う Widget に DI できないか?

Flutter の DI の現状

Flutter の DI の現状

Does Flutter come with a dependency injection framework or solution?

Not at this time. Please share your ideas at flutter-dev@googlegroups.com.

 

現状、Flutter には DI の仕組みは存在しない

なら、Package はどうか?

あるにはある

けっこう、ある

どれ使おう??

Google からはどうか?

公式ではないがある

がしかし Latest commit 4 months ago...

https://github.com/google/inject.dart

flutter_simple_dependency_injection

  • 一番スコア高い
  • Flutter 専用っぽい
  • Dart 2 に対応

 

  • 名前長い・・・

BLoC を DI してみた

Widget のコンストラクタ変更

class Search extends StatelessWidget {
  final SearchBloc _searchBloc;

  Search(this._searchBloc);

  @override
  Widget build(BuildContext context) {
    final _controller = new TextEditingController();
    return new Column(
      children: <Widget>[
          new TextField(
            controller: _controller,
            decoration: new InputDecoration(hintText: 'Type word'),
            onSubmitted: (String query) {
              _searchBloc.query.add(query);
            },
         ),
        new Expanded(
          child: StreamBuilder<List<SearchResult>>(
            stream: _searchBloc.results,
            builder: (context, snapshot) {
              return new ListView(
                children: ListTile
                    .divideTiles(
                      context: context,
                      tiles: snapshot.data.map((SearchResult searchResult) {
                        return new ListTile(
                          title: _formatString(context, searchResult.word),
                          trailing: new Row(...

BLoC を DI コンテナに登録

import 'package:flutter/material.dart';

import 'package:flutter_simple_dependency_injection/injector.dart';

import 'package:doughnut/search/search.dart';
import 'package:doughnut/search/search_bloc.dart';

void main() {
  final injector = Injector.getInjector();

  injector.map<SearchBloc>((i) => new SearchBloc(), isSingleton: true);

  injector.map<Search>((i) => new Search(injector.get<SearchBloc>()));

  runApp(new MyApp());
}

DI 済みのWidget を生成

  @override
  Widget build(BuildContext context) {

    final injector = Injector.getInjector();

    return new Scaffold(
      appBar: new AppBar(
        title: new Text('doughnut'),
        actions: <Widget>[
          new IconButton(
              icon: new Icon(Icons.info),
              onPressed: () {
                ...
              })
        ]
      ),
      body: injector.get<Search>(),
...

所感

BLoC パターン

  • StatefulWidget にして State を作るのいまいちしっくり来てなかったので、
    • BLoC に State もビジネスロジックも分離できて良い
    • StatelessWidget + BLoC だけでやっていけそう

BLoC の DI

  • BLoC をホストするために InheritedWidget をつくるのは正直めんどうだと思っているので、
    • DI コンテナに登録するほうがまだ簡単(それでもまだイマイチ感はある)
    • Google の package:inject に期待したい
    • 将来的にFlutter が DI の仕組みを持ってよしなにやってくれることを期待したい