Zenn
😱

【Flutter】dynamicはなるべく避けて!

2025/01/25に公開1
1

今まで通りRiverpodを使おうとしていたのに、
いざ動作確認をするとエラーとなった事例があったので共有です😿

今回はたまたまRiverpod関連でエラーが出たのですが、
dynamicは極力避けたほうが良いという話です。

結論

下記のようにdynamicで組まれていた場合の話です。
(今回はWidgetRefで定義すべき箇所がdynamicとなっていました)

  import 'extension.dart';

  Future<void> hogeFunction(
    dynamic ref, // dynamicで定義されている
  ) async {
    final result = await ref.read(useCaseProvider).hogeFunction() as Hoge;
    return result.extension; // 拡張メソッド(extension)を使用!
  }

リファクタ等でasキャスト(上記のas Hogeの部分)を削除してしまった場合、
dynamic定義のため拡張メソッド(extension)が存在していても型が不明確で、
拡張メソッドの存在をIDEが検出できずimportが自動で削除され
(※IDEの設定による)、
気付かぬうちにエラーとなってしまうことがあります。
(ビルドエラーは起こらず、実際に動作させてみないとエラーとなるかわかりません)

  // importは使用されていないwarningが出るため削除
  // import 'extension.dart';

  Future<void> hogeFunction(
    dynamic ref, // dynamicで定義されている
  ) async {
    // `as Hoge`を削除したが、特にビルドエラーは出ない。
    final result = await ref.read(useCaseProvider).hogeFunction();

    // ビルドエラーはでないが、
    // extensionはimportしていないので、実際に動かすとエラーが出る!
    return result.extension;
  }

上記の事例は一例であり、
dynamicは極力避け、型定義をちゃんとしましょう!

  import 'extension.dart'; // 型定義するとimportが必要となる

  Future<void> hogeFunction(
    WidgetRef ref, // 型定義!
  ) async {
    final result = await ref.read(useCaseProvider).hogeFunction();
    return result.extension;
  }

今回起きた事例

Riverpod関連のリファクタをしようとしたときにエラーが起きました。

本題とは少しズレますがリファクタの内容について少し説明します。
下記のように、Riverpodにて特に状態管理をしないクラスでNotifierを使用していました


class UseCase extends _$UseCase {
  
  void build() {
    // UseCaseで特に状態管理をしないのにNotifierが使用されている。
    return;
  }

  Future<Hoge> hogeFunction() async {
    // 処理
  }
}

個人的に、Notifierで管理する理由がない場合は下記の理由から避けたいと思っています。

  • 今後の実装で、状態を管理しないはずのUseCase層にて、状態を管理する実装をしてしまう可能性がある。
    • 別で状態管理用の層は設けている状態です。
  • 実装時に、不必要な記述が増える
    • 呼び出し側で.notifierを余計に付ける必要がある。
    • 毎回Notifierにてbuildメソッドを余計に設ける必要がある。

上記理由から、以下のようにリファクタをしました。


UseCase useCase(Ref ref) {
  return UseCase();
}
class UseCase {
  Future<Hoge> hogeFunction() async {
    // 処理
  }
}

そして、呼び出し側で.notifierがついているコードを削除していったのですが、
下記のようなasキャストをしているコードを見つけました。
(この時点ではrefがdynamic定義がされているとは気づいていませんでした)

final notifier = ref.read(useCaseProvider.notifier) as UseCase;
final hoge = await notifier.hogeFunction();
return hoge.extension;

※上記のextensionでは少し計算する処理を入れているようでした。

extension HogeExtension on Hoge {
    String get extension => // コード割愛;
}

今まで僕はProviderを使用する上でキャストが必要になったことがなかったので、
要らないだろうと下記のように削除をしました。

final notifier = ref.read(useCaseProvider); // `as`を削除!
final hoge = await notifier.hogeFunction();
return hoge.extension;

特にエラーもなくビルドでき、一件落着…

のはずが、動作確認でエラーが出ました(!?)

確認していると、extensionのimportが消えていることを発見。

  // importは使用されていないwarningが出るため削除
  // import 'extension.dart';

  Future<void> hogeFunction(
    dynamic ref, // dynamicで定義されている
  ) async {
    // `as UseCase`を削除したが、特にビルドエラーは出ない。
    final notifier = ref.read(useCaseProvider);
    final hoge = await notifier.hogeFunction();

    // ビルドエラーはでないが、
    // extensionはimportしていないので、実際に動かすとエラーが出る!
    return hoge.extension;
  }

どうやら、WidgetRefを使用するところでdynamic定義されていたことにより、
asによるキャストがないと型が判断できずimportが不必要と判断され、
VSCodeのorganizeImports設定にて自動削除されていた
ことがわかりました。

実際に動かさないと気付けないエラーのため、
予期せぬ不具合の原因となる可能性が高いです。

対応としては、下記の2通りが考えられます。

  1. そもそもdynamic定義する必要がなければしない。
  2. asキャストをもとに戻して、importが機能するようにする。

今回はそもそもdynamic定義をする必要がない箇所だったため、
1の選択をして適切な型に修正しました。

  import 'extension.dart' // 型定義するとimportが必要となる

  Future<void> hogeFunction(
    WidgetRef ref, // 型定義!
  ) async {
    final hoge = await ref.read(useCaseProvider).hogeFunction();
    return hoge.extension;
  }

さいごに

dynamicを定義するコードをほぼ書いたことがなかったので、
「なんでこんな挙動に!?」とかなり焦りました(汗)
本当にdynamic定義が必要か、ちゃんと確認したいですね🏃

1

Discussion

ログインするとコメントできます