☄️

buildメソッドの中に処理を書くのはよくないらしい?

2023/10/23に公開1

Overview

この記事を見て、buildメソッドの中に処理を書くのってよくないんだな〜と思いつつも書かないといけない場面がよくあります。
https://qiita.com/chooyan_eng/items/976adeea8eed1b5ebad4

普段は私は、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で以前見かけたことがあります。

怖いですね〜、気をつけないと😅

Jboy王国メディア

Discussion

JboyHashimotoJboyHashimoto

useMemoizedを使って再レンダリングを防ぐ方法もあるそう。以前なしでやった時にデバッグすると、何度も実行されてたが、囲むと1回だけしか実行されていなようだ。

import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';

class MemoExample extends HookWidget {
  const MemoExample({super.key});

  
  Widget build(BuildContext context) {
    // DatePickerのロジックを実装
    final selectedDate = useState<DateTime>(DateTime.now());

    // useMemoizedを使って再レンダリングを防ぐ
    final selectDate = useMemoized(() {
      return () async {
        Future.delayed(Duration.zero, () async {
          final pickedDate = await showDatePicker(
            context: context,
            initialDate: selectedDate.value,
            firstDate: DateTime(2000),
            lastDate: DateTime(2025),
          );
          if (pickedDate != null) {
            selectedDate.value = pickedDate;
          }
        });
      };
    });

    return Scaffold(
      appBar: AppBar(
        title: const Text('Memo Example'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text('Selected Date: ${selectedDate.value}'),
            ElevatedButton(
              onPressed: selectDate,
              child: const Text('Select Date'),
            ),
            TextButton(onPressed: () {
              debugPrint('click');
            }, child: const Text('click')),
          ],
        ),
      ),
    );
  }
}