💭

DartとFlutterのベストプラクティス #1

2025/03/23に公開

この記事では、DartとFlutterのベストプラクティスについてすべてを共有します。振り返ってみると、このような知識の源があったらとても嬉しかっただろうなと思います。面白いことに、何か興味深いことを思い出すたびに頻繁に更新されています。

Flutter Best Practices

Flutterは、真のクロスプラットフォームアプリケーションを構築するための素晴らしいフレームワークです。デフォルトで強力で整理されていますが、物事を悪化させる方法も多く存在します。このセクションでは、アプリケーションを良い状態に保つ方法について説明します。

Effective build methods & widgets

Flutterでは、UIはWidgetを組み合わせることで作成されます。実際にWidgetを構築する重要な関数である「Build Method」は、最適化する必要があります。ここでは、アプリをスムーズに動作させるためのいくつかのヒントを紹介します:

  • Build Methodは純粋に保ち、SideEffectや重いタスクは避けましょう。Build Methodは1秒間に最大120回実行されるため、スピードが重要です。計算が必要な場合は、initState、didUpdateWidget、didChangeDependenciesなどのライフサイクルイベントを使用してください。
  • 大きなBuild Methodは、小さなWidgetに分割しましょう。これにより、Flutterは必要な部分のみを再構築し、毎回全体のビルドを再計算するのではなく、より効率的に識別できます。
  • Widgetとそのサブツリーにはconstコンストラクタを使用しましょう。これにより、Flutterの内部システムが変更をより効率的に認識できるようになります。また、標準インスタンスがメモリ内で一度だけ作成されます。
  • Widgetは必要に応じて遅延生成しましょう(遅延処理)。たとえば、IndexedStack内ですべてのウィジェットを初期化するのではなく、LazyIndexedStackを選ぶことで、必要な時にだけアイテムが初期化されます。
  • ContainerよりもSizedBox、ColoredBox、またはDecoratedBoxを選びましょう。Containerは内部でこれらのウィジェットを使用していますが、必要な構成を決定するために追加の計算を行います。
  • InheritedWidgetがテーマやロケールなど複数の側面を管理する場合は、InheritedModelを使用しましょう。これにより、変更された情報に関心のあるWidgetだけが更新を受け取ります。
  • InheritedWidgetのupdateShouldNotifyメソッドは慎重に実装しましょう。最適なパフォーマンスのために、必要な場合にのみtrueを返すようにします。
  • Widgetを作成する際には、ヘルパーメソッドよりもクラスを使用することを選びましょう。ヘルパーメソッドは不必要な再構築を引き起こし、constの最適化を活かせなくなる可能性があります。クラスはパフォーマンスとメンテナンス性が向上します。

Optimizing Paints and Repaints

Flutterのレンダリングパイプラインでは、描画ステージが画面に表示するデータを収集します。これを最適化する方法を学びましょう:

  • RepaintBoundaryを活用して、Animation Widgetのように頻繁に再描画が必要な領域を制限しましょう。これにより、ウィジェットが新しいレイヤーでラップされ、頻繁な再描画が周囲の要素に影響を与えません。
  • 不透明度を適用するよりも、組み込みの不透明度を持つ画像や色を描画する方が高速です。不透明度はWidgetのグループに影響を与える可能性があり、リソース集約型のオフスクリーンバッファを使用することにつながります。
  • 画像フィルタを1つのウィジェットにのみ適用する場合は、ImageFilteredを選びましょう。このタスクには、すべての背後にフィルタを適用するBackdropFilterよりもシンプルで効率的です。
  • UIパフォーマンスを向上させるために、debugDisableOpacityLayers、debugDisableClipLayers、debugDisablePhysicalShapeLayersのようなデバッグ変数を使用しましょう。これらのツールは、不透明度やクリッピングのような特定のエフェクトを無効にしてボトルネックを特定しやすくします。
  • 内部ウィジェットは慎重に使用しましょう。これらは推測的なレイアウト計算を必要とし、複数回のレイアウトパスや複雑な操作を引き起こすため、パフォーマンスを大幅に低下させる可能性があります。これらのパフォーマンス問題を避けるためには、ウィジェットの制約を直接定義する方が効率的です。
  • CustomPainterの使用を最適化するために、shouldRepaintメソッドが必要な場合にのみtrueを返すようにしましょう。これにより、不必要な再描画を避けることができます。

StatefulWidget

StatefulWidgetは、状態オブジェクトを使用して自分の状態を管理し、ライフサイクルイベントに応じて反応します。

  • 必要な場合にStatefulWidgetを使用しましょう。もしComponentsが状態を必要とするのであれば、進んで使用してください。これは複雑でも負担でもありません。基本的に、StatelessWidgetとStatefulWidgetは、同じライフサイクルを持つ類似の要素です。
  • initStateメソッドで、Controllers、Notifiers、Subscriptions、BLoCなどのリソースを初期化しましょう。そして、disposeメソッドで必ずそれらを閉じてください。Build Methodでの初期化は避け、最適なパフォーマンスを維持しましょう。
  • コード内でsetStateの使用を最小限に抑えましょう。setStateは慎重に使用し、ツリーの小さな部分だけを対象にしましょう。過度に使用すると、大規模な再構築が発生し、レンダリングオブジェクトで不必要なレイアウトや再描画を引き起こす可能性があります。
  • ValueKey、ObjectKey、UniqueKeyなどのキーをコードに実装しましょう。これにより、ウィジェットの識別が可能になり、効率が向上します。特定のケースでは、ウィジェットの再親子化を効果的に行うためにGlobalKeyが必要です。
  • 複雑なプロダクション規模のアプリでは、setStateに頼るのではなく、BLoCのような高度な状態管理ソリューションを使用して、効率的でシームレスな開発を行うことが重要です。

Scrolling optimizations

Flutterでのスクロールをマスターするのは難しいことがあります。まずは、以下の基本的なポイントを理解することから始めましょう:

  • 複雑な動作を管理するためには、ListViewやColumnのネストを避け、CustomScrollViewを選びましょう。これにより効率が向上し、プロセスが簡素化されます。
  • 長いリストには、ListView.builderやListViewを使用しましょう。これらはビューポート内にあるものだけをレンダリングします。特にListView.builderは、ページネーションが必要な場合や多くのアイテムがある場合に適しています。アイテムがビューポートに現れる直前にウィジェットインスタンスが作成されるのに対し、ListViewはあらかじめ初期化されたインスタンスを使用します。
  • 長いリストや常にビューポートを超えるコンテンツには、SingleChildScrollViewの使用は避けましょう。このWidgetはすべての子コンテンツをレンダリングするため、大きなリストでは非効率的です。ビューポートサイズを超えた場合にのみスクロール可能な小さなコンテンツに適しています。
  • ListViewでitemExtentを設定するか、SliverFixedExtentListを使用して効率的なスクロールを実現しましょう。プロトタイプやエクステントビルダーを利用するのも有効です。これらの方法は、レイアウト計算の必要性を減らすことでスクロール処理を支援します。
  • スムーズなスクロールを実現するために、Column、Row、WrapをListViewやSliverに置き換えましょう。この変更により、過剰なウィジェットのレンダリングが減り、効率が向上します。
  • shrinkWrapよりもsliverを選びましょう。shrinkWrapは各アイテムのエクステントを計算するため、長いリストではパフォーマンスが大きく低下します。Sliverはこれらの計算を必要とせず、より良い代替手段です。

Animation Tips

アニメーションは、アプリを動的で視覚的に魅力的にします。以下は、いくつかの重要なポイントです:

  • AnimatedBuilderは、childパラメータを活用して最適化しましょう。これにより、ウィジェットは一度だけ作成され、アニメーション内で効率的に再利用されます。OpacityやClippingのような操作において非常に重要です。
  • 不透明度の変化を伴うアニメーションでは、AnimatedOpacityやFadeTransitionウィジェットを使用することをお勧めします。これらは、Opacity値の直接更新と異なり、サブツリーの再構築や再描画というコストのかかるプロセスを回避するため、より効率的です(詳細はこちら)。
  • チェーンアニメーションには、Intervalカーブを利用しましょう。これにより、アニメーションの値は指定された閾値をアニメーションコントローラーが超えるまで0.0のままになります。
  • スムーズなアニメーションの統合には、FadeTransition、AnimatedOpacity、AnimatedSwitcher、SizeTransition、CrossFadeTransitionなど、SDKの組み込みWidgetを選びましょう。これらはUIの向上に大きく貢献します。
  • 高度でエフェクト集中的なアニメーションには、LottieやRiveの使用を検討しましょう。これらのツールは、手動の方法と比べて優れたパフォーマンスを提供します。

Discussion