Flutter アプリを
BLoC パターンでリファクタリングし、
BLoC を DI してみた
2018/07/19
Flutter Meetup Tokyo #3
About me
- 桐生 達嗣(きりゅう たつし)
- Technical Consulting Engineer
- Front End Engineer
- Social
About me
Agenda
- BLoC パターンおさらい
- BLoC パターンでリファクタリング
- Flutter の DI の現状
- BLoC を DI してみた
- 所感
BLoC パターン
おさらい
Business Logic Component
- Item1
- Item2
- Item3
Business Logic Component
- Item1
- Item2
- Item3
BLoC パターンで
リファクタリング
作っているアプリ
- TextField に英単語を入力
- WebAPI を使って、その英単語を使っている例文を検索
- 検索結果を ListView に表示
作っているアプリ
- 英単語を WebAPI で問い合わせ
- レスポンスを 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 パターン
- InheritedWidget クラスを継承する
- 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 の仕組みを持ってよしなにやってくれることを期待したい