Open6

Flutter Hooksあれこれ

わいすけわいすけ

実環境でFlutter Hooksを使用してだいぶ知見など溜まってきたので、書いていく〜🐶(時間があるときに)

わいすけわいすけ

useEffectでよく使うやつ


// keysの指定を忘れないようにできる
void useEffectOnce(Dispose? Function() effect) => useEffect(effect, const []);

// StatefulWidgetの`initState`のように使用できる
void useInitState(void Function() initState) => useEffectOnce(() {
      initState();
      return null;
    });

// StatefulWidgetの`dispose`のように使用できる
void useDispose(void Function() dispose) => useEffectOnce(() => dispose);

useEffectはHookの中でも癖?がある?というか慣れてない人(特にStatefulWidgetのライフサイクルメソッドが理解できてない人)には難しいかもしれないけど、慣れるといい感じ🐶

わいすけわいすけ

useMemoizedは大切!(だと思ってる)

// `GlobalKey<FormState>`
GlobalKey<FormState> useFormKey() => useMemoized(GlobalKey<FormState>.new);

StatefulWidgetならStateに保持しておくようなもの(buildのたびに生成されなおされなくてもいいもの)をuseMemoizedを使用して簡単に書ける。

わいすけわいすけ

useStateuseRefの違いも大事。

useStateValueNotifier<T>
useRefObjectRef<T>

どちらも使い方は似てる

// 宣言
final index = useState(0);
// useStateの値を変更する
index.value = index.value + 1;

// 宣言
final index2 = useRef(0);
// useRefの値を変更する
index2.value = index2.value + 1;

似てるというか、書き方は一緒。
違いは、stateの変更でrebuildをさせるかさせないか。
useStateValueNotifierなので値が変更されればChangeNotifiernotifyListeners()が呼ばれてrebuildが起きる。

それに対してuseRefObjectRefになっている。
ObjectRefって何??っていうと、ただの値を持ってるクラス。当然値の変更が起きてもrebuildは発生しない。

/// Creates an object that contains a single mutable property.
///
/// Mutating the object's property has no effect.
/// This is useful for sharing state across `build` calls, without causing
/// unnecessary rebuilds.
ObjectRef<T> useRef<T>(T initialValue) {
  return useMemoized(() => ObjectRef<T>(initialValue));
}

/// A class that stores a single value.
///
/// It is typically created by [useRef].
class ObjectRef<T> {
  /// A class that stores a single value.
  ///
  /// It is typically created by [useRef].
  ObjectRef(this.value);

  /// A mutable property that will be preserved across rebuilds.
  ///
  /// Updating this property will not cause widgets to rebuild.
  T value;
}

useRefの使い道としては、rebuildを起こさなくていい時。
例えば、useEffectでしか値の変更が発生しない場合とか。
(つまりその後にbuild()が呼ばれることがFlutterのライフサイクル上、決まっているので、rebuildさせる必要がない時)。

StatefulWidgetで言えば、initState()didUpdateWidget()でしか値の変更をしない場合で、initState()didUpdateWidget()の中でsetState()を呼ぶ必要がないのと同じイメージ。

わいすけわいすけ

Dart3.0から導入されたRecord型の恩恵!!

/// TabControllerとTabControllerの変化に追従したindexを返す
///
/// `initialLength`にはTabのlengthを入れる
(TabController, int) useTabControllerAndIndex(int initialLength) {
  final controller = useTabController(initialLength: initialLength);
  final index = useState(controller.index);
  // useInitStateがここで登場
  useInitState(() {
    controller.addListener(() => index.value = controller.index);
  });

  return (controller, index.value);
}

TabControllerのindexに応じて出し分けたいWidgetがある時とかに使用するかな?
Record型のおかげでカスタムフックが捗る!!

わいすけわいすけ

Hookをどこに置くか?問題。

ベーシックにbuild内に書く

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

  
  Widget build(BuildContext context) {
    final controller = useScrollController();
    useInitState(() {
      controller.addListener(() {
        someFunction();
      });
    });
    return ...
  }

クラスのプライベートメソッドにする

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

  
  Widget build(BuildContext context) {
    final controller = _useCustomScrollController();
    return ...
  }

  ScrollController _useCustomScrollController() {
    final controller = useScrollController();
    useInitState(() {
      controller.addListener(() {
        someFunction();
      });
    });
    return controller;
  }
}

グローバルでプライベートな関数にする

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

  
  Widget build(BuildContext context) {
    return const Placeholder();
  }
}

ScrollController _useCustomScrollController() {
  final controller = useScrollController();
  useInitState(() {
    controller.addListener(() {
      someFunction();
    });
  });
  return controller;
}

どれも問題ないと思うんだけど、どれが良いか悩む。
可読性が悪くない場合はbuild内に直接書いているけど、
長くなって、読みにくい場合に、どうするか...???