☄️
buildメソッドの中に処理を書くのはよくないらしい?
Overview
この記事を見て、buildメソッドの中に処理を書くのってよくないんだな〜と思いつつも書かないといけない場面がよくあります。
普段は私は、flutter_hooksを使っているのですが、あれもbuildメソッドの中に処理を書いているのでよくないのだろうかと考えたりします???
summary
今回は、ReactのカスタムHookのようなことをして、これ防げないだろうかと考えてStreamコントローラーを制御するロジックを作ってみました。
⚓️HookWidgetを使った例
普段から気に入って使ってるuseなんちゃらを使ってみました。これがReactみたいな書き方になって、メリットになっているかは分からないですが🤔
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'dart:async';
void main() => runApp(MyApp());
class MyApp extends HookWidget {
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Hooks Demo',
theme: ThemeData(primarySwatch: Colors.blue),
home: MyHomePage(),
);
}
}
class MyHomePage extends HookWidget {
Widget build(BuildContext context) {
final counter = useCounter();
final streamValue = useStream(counter.stream, initialData: counter.value).data;
return Scaffold(
appBar: AppBar(title: Text('Flutter Hooks Demo')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text('You have pushed the button this many times:'),
Text(
'$streamValue',
style: Theme.of(context).textTheme.headline4,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: counter.increment,
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
}
}
// Custom Hook
CounterController useCounter() {
final controller = useMemoized(() => CounterController());
useEffect(() {
return controller.dispose; // dispose when widget is removed from tree
}, []);
return controller;
}
class CounterController {
final _counter = StreamController<int>.broadcast();
int _value = 0;
int get value => _value;
Stream<int> get stream => _counter.stream;
void increment() {
_value++;
_counter.sink.add(_value);
}
void dispose() {
_counter.close();
}
}
📡Riverpodを使った例
カウンターしかやらないので、プロバイダーでやる必要あるのかなと思いつつ、StreamコントローラーとonDispose使ったことないので、試してみたいなと思ってこんなの作りました。
import 'package:flutter/material.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'dart:async';
part 'myapp.g.dart';
void main() => runApp(const ProviderScope(child: MyApp()));
class MyApp extends ConsumerWidget {
const MyApp({super.key});
Widget build(BuildContext context, WidgetRef ref) {
final counterStream = ref.watch(streamProvider);
return MaterialApp(
title: 'StreamProvider Demo',
theme: ThemeData(primarySwatch: Colors.blue),
home: Scaffold(
appBar: AppBar(title: const Text('StreamProvider Demo')),
body: Center(
child: counterStream.when(
data: (value) => Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text('You have pushed the button this many times:'),
Text(
'$value',
style: const TextStyle(fontSize: 30),
),
],
),
loading: () => const CircularProgressIndicator(),
error: (error, stack) => Text('Error: $error'),
),
),
floatingActionButton: FloatingActionButton(
onPressed: () => ref.read(counterProvider).increment(),
tooltip: 'Increment',
child: const Icon(Icons.add),
),
),
);
}
}
// StreamCounterControllerを使ってカウントするためのクラス
class CounterController {
// refメソッドを使うために必要
final Ref ref;
// コンストラクター内にコントローラーが破棄された時の処理を記述
CounterController(this.ref) {
ref.onDispose(() => _counter.close());
}
// broadcastは、複数のリスナーに対してイベントを通知するためのもの
final _counter = StreamController<int>.broadcast();
// カウンターの初期値
int _value = 0;
// Streamを公開するためのgetter
Stream<int> get stream => _counter.stream;
// カウントアップするためのメソッド
void increment() {
_value++;
_counter.sink.add(_value);
}
}
// 状態破棄したくないときは、keepAlive: trueをつける
(keepAlive: true)
// このクラスは、CounterControllerを継承している
CounterController counter(CounterRef ref) {
return CounterController(ref);
}
// StreamProviderを使うための関数
Stream<int> stream(StreamRef ref) {
final controller = ref.watch(counterProvider);
return controller.stream;
}
thoughts
buildメソッドの中に処理を書かないように最近は、気をつけようと意識するのですが、いい方ないのかな〜と考えております。
書いたらやばいな〜って処理は、FirestoreとAPIに接続してる処理だと思います。ReactでもuseEffectの中に処理を書いて、無限ループが走ったことがあることをTwitterで以前見かけたことがあります。
怖いですね〜、気をつけないと😅
Discussion
useMemoizedを使って再レンダリングを防ぐ方法もあるそう。以前なしでやった時にデバッグすると、何度も実行されてたが、囲むと1回だけしか実行されていなようだ。