💭

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

2025/03/23に公開

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

Assets

アイコン、画像、ロゴはすべてのアプリケーションにおいて重要であり、しばしばパフォーマンスのボトルネックとなります。スムーズな操作のために、以下の戦略を考慮しましょう:

  • より良いパフォーマンスのために、SVGの代わりにアイコンフォントを選びましょう。アイコンフォントは効率的で、flutter_svgに関連する重いパース処理を避け、バンドルからファイルをロードする必要もありません。
  • ロゴには、SVGやPNGの代わりにCustomPaintを使用しましょう。このベクターグラフィックスアプローチはパフォーマンスを向上させ、アニメーションにも対応できます。Flutter Shape Makerを使用すれば、SVGを簡単にカスタムペインターに変換できます。
  • CachedNetworkImageやその他のツールを使って、ネットワーク画像をキャッシュすることでパフォーマンスを向上させましょう。これにより、繰り返しの画像リクエストが減少し、パフォーマンスが向上します。
  • ネットワーク画像を最適化して、RAMの使用を効果的に管理しましょう。4k画像などの大きな画像(約30MB)は、cacheWidth、cacheHeightパラメータやResizeImageプロバイダーを使用して、必要な表示サイズにダウンスケールできます。Flutterはまた、オーバーサイズの画像を警告するデバッグツールを提供しています。
  • アセットへのパスを自分で書く代わりに、生成されたパスを使用しましょう。これにより、ミスを防ぎ、コードの量を減らすことができます。

Non-categorized

  • ストリームやフューチャーはinitStateメソッドで初期化するようにコードを修正しましょう。これにより、ビルドがトリガーされるたびに不必要に再計算されることを防げます。
  • Flutter DevToolsを活用して、詳細なパフォーマンス分析を行い、弱点やメモリリークを特定することが重要です。また、UIを確認するための非常に有用なツールであるWidget Inspectorを継続的に利用しましょう。
  • 宣言的なナビゲーションを採用し、NavigatorのページAPIを使用しましょう。また、go_routerのようなライブラリの統合も検討できます。ただし、実装前に自分の特定のニーズに合致しているか確認してください。
  • アプリケーションがメモリからアンロードされた際に状態を復元できるよう、復元バケットと識別子を設定しましょう。これにより、リストビュー(位置の保存)、ナビゲーター(スタックの保存)などが便利に使えます。

Dart-Specific Tips

Performance

  • パフォーマンスと柔軟性の向上のために伝統的な「for」ループを使用することを推奨します。forEachはコードを遅くする可能性があり、await、break、returnステートメントをサポートしていません。
  • Dartチームのbenchmark_harnessライブラリを使用して、パフォーマンスベンチマークを作成することを検討しましょう。
  • 再帰の使用は制限しましょう。Dartには末尾再帰最適化がないため、各再帰呼び出しはコールスタックに追加されます。再帰呼び出しが多すぎると、スタックオーバーフローが発生し、メモリが不足することがあります。
  • 集中的な計算は、アプリの遅延やブロックを防ぐためにDartのメインアイソレートから分離しましょう。
  • Dartで等価性を定義する際は、必ず==とhashCodeの両方をオーバーライドしましょう。これらは、ハッシュセットやマップのような構造が依存しているため、非常に重要です。
  • null安全演算子を実装し、nullチェックなしで!演算子を避けて、ランタイムエラーを防ぎ、アプリの安定性を確保しましょう。
  • リストの作成にはList.ofを使用しましょう。これにより、リストの型を保持できます。
  • Future.waitよりも、StreamsやFuturesを選びましょう。Future.waitは、1つのFutureが失敗した場合に全体の失敗を引き起こし、型情報の喪失や個別のイベントやエラー処理の制限を招く可能性があります。

Errors & Exceptions

Flutterでは、ErrorとExceptionという2つのインターフェースが区別されています。名前は似ていますが、機能と目的には大きな違いがあります:

  • Error(StateErrorやOutOfMemoryErrorなど)は、修復不可能な問題を示すため、キャッチしないようにしましょう。キャッチすべきはExceptionであり、これはキャッチされることを前提にしており、失敗に関する貴重な情報を提供します。
  • エラーハンドリングには例外を使用することを検討しましょう。エラーをカスタムタイプでラップする場合、Error.throwWithStackTraceを使用してスタックトレースを保持しましょう。
  • 例外を無視しないようにしましょう。常にキャッチブロック内で処理または再スローし、予期しない問題に対処しましょう。
  • より良いエラーハンドリングのために、コンポーネントに合わせたExceptionの専門的なサブクラスを開発することを検討しましょう。
  • throwはErrorおよびException型のみに制限しましょう。

Architecture

モジュールとシステムを保守しやすく保つための一般的なアーキテクチャのヒント

Design Principles

コンポーネントを良い状態に保つための一般的なガイドライン

  • 単一責任の原則 (Single Responsibility Principle) – モジュールは一つの役割のみを持つべきであり、その役割に変更が必要な唯一の理由となります。
  • 開放-閉鎖の原則 (Open-Closed Principle) – モジュールは拡張には開かれているべきですが、修正には閉じられているべきです。これは、ストラングラー・フィグ・パターンとも関連しています。
  • リスコフ置換の原則 (Liskov Substitution Principle) – 親クラスのオブジェクトは、そのサブクラスのオブジェクトで置き換えても、プログラムの正しさに影響を与えてはいけません。
  • インターフェース分離の原則 (Interface Segregation Principle) – 大きなインターフェースよりも、複数の小さなインターフェースを選びましょう。これにより、クライアントは必要なインターフェースだけを使用することができます。
  • 依存関係逆転の原則 (Dependency Inversion Principle) – 具象的な実装に依存するのではなく、抽象に依存しましょう。これにより、テスト性が向上し、モジュールの再利用性が高まります。
  • 関心の分離 (Separation of concerns) – コードを異なる機能や関心ごとに整理し、それぞれが特定の機能を担当するようにします。これには、より良い整理と明確さを得るためにアーキテクチャを層に分けることが含まれます。
  • 低い結合度と高い凝集度 (Low coupling & high cohesion) – ソフトウェア設計における低い結合度は、コンポーネント間の依存関係を最小限に抑えることで、保守性が向上します。高い凝集度は、特定のタスクセットに焦点を当てたモジュールの設計を意味し、これにより理解と管理が容易になります。
  • KISS (Keep It Simple, Stupid) – 短く、簡潔に保ちましょう。シンプルなものは理解しやすく、管理しやすいです。
  • DRY (Don't Repeat Yourself) – 自分を繰り返さないようにしましょう。
  • YAGNI (You Aren't Gonna Need It) – 必要ないものは作らないようにしましょう。

Design Pattern

  • 依存性注入 (Dependency Injection) – オブジェクトの生成とその使用を分離します。これは、コンストラクタを使用して明示的な依存関係を指定することで実現されます。

Discussion