フラッターアプリケーションにおける懸念の分離
Engineering at ClickUp

フラッターアプリケーションにおける懸念の分離

最近、私はオンボーディングのウォークスルーを実施しなければならなかった。

/参照 https://clickup.com/ ClickUp /を実装する必要があった。

新規ユーザー!これは本当に重要なタスクだった。

/を発見しようとしていたからだ。 https://www.youtube.com/watch?v=AirnL1NxOAw スーパーボウルで初公開した信じられないほど面白い広告 /%href/

!✨

クリックUp経由

このチュートリアルでは、ClickUpをまだ知らない多くの新規ユーザーが、アプリケーションのいくつかの機能の使い方をすぐに理解できるようになっています。これは、新しいアプリケーションのように、継続的な努力です。

/参照 https://university.clickup.com/。 クリックUp大学 /クリックアップ大学

私たちが追い求めているリソース🚀

幸いなことに、ClickUp Flutterモバイルアプリケーションの背後にあるソフトウェアアーキテクチャのおかげで、アプリケーションの実際のウィジェットを再利用しながらも、この機能を非常に素早く実装することができた!これは、ウォークスルーがダイナミックで、レスポンシブで、アプリの実際のアプリケーション画面と完全に一致することを意味します。

また、懸念事項を適切に分離することで、機能を実装することができました。

どういうことか見てみよう。🤔

関心の分離

ソフトウェアアーキテクチャの設計は、エンジニアリングチームにとって最も複雑なトピックの1つである。全ての責任の中で、将来のソフトウェアの進化を予測することは常に難しい。だからこそ、うまく階層化され、分離されたアーキテクチャを作成することは、あなたやあなたのチームメイトにとって多くの助けとなるのです!

小さな非結合システムを作成する主な利点は、間違いなくテスト可能性です!そしてこれが、アプリの既存画面の代替デモを作成するのに役立った!

ステップバイステップのガイド

さて、これらの原則をFlutterアプリケーションにどのように適用できるだろうか?

私たちがClickUpを作るために使っているいくつかのテクニックを、簡単なウォークスルー例とともに共有しよう。

この例はとてもシンプルなので、その裏にある利点のすべてを悟ることはできないかもしれませんが、複雑なコードベースを持つFlutterアプリケーションをよりメンテナーなものにするのに役立つはずです。💡

アプリケーション

例として、アメリカの人口を年ごとに表示するアプリケーションを作ります。

ClickUp経由

ここには2つの画面があります:

  • HomeScreen:2000年から現在までのすべての年をリストアップ。ユーザーが年のタイルを設定すると、選択された年に設定されたナビゲーション引数でDetailScreen`に移動します。
  • DetailScreen` : ナビゲーション引数から年を取得して /を呼び出す。 https://datausa.io/api/data?drilldowns=Nation&measures=Population&year=2018 を呼び出す。 datausa.io APIを呼び出す。 /%href/ を呼び出し、JSONデータを解析して関連する人口の価値を抽出する。データが利用可能な場合は、人口とともにラベルが表示されます。

DetailScreen`の実装に焦点を当てよう。非同期呼び出しで最も利息があるからだ。

ステップ1.ナイーブアプローチ

ナイーブアプローチ ステートフルウィジェット

ClickUp経由

この原則は、新しい開発者には理解しにくいかもしれません(また、説明するのも難しいです)が、全体的な考え方は、コンポーネントの外に関心事を抽出し、コンポーネントが振る舞いを選択する責任を負わないようにし、代わりにそれをデリゲートすることです。

より一般的な状況では、単に抽象化を作成し、コンポーネントに実装を注入します。

しかし心配しないでください、次の例を見ればもっと理解できるようになります!👀 Dartソースコードとデモ

API クライアントオブジェクトの作成

API への HTTP 呼び出しを制御するために、実装を専用の DataUsaApiClient クラスに分離した。また、データの操作とメンテナンスを容易にするために Measure クラスも作成した。

クラス DataUsaApiClient {
  const DataUsaApiClient({)
    this.endpoint = 'https://datausa.io/api/data'、
  });


  final String endpoint;


  Future<メジャー?> getMeasure(int year) async { {.
    final uri =
        Uri.parse('$endpoint?drilldowns=Nation&measures=Population&year=$year');
    final result = await get(uri);
    final body = jsonDecode(result.body);
    final data = body['data'] as List<dynamic>;
    if (data.isNotEmpty) { 以下のようにします。
      return Measure.fromJson(data.first as Map<String, Object?>);
    }
    return null;
  }

APIクライアントの提供

この例では、よく知られている

/を使用している。 https://file+.vscode-resource.vscode-webview.net/Users/alois/Desktop/article/clickup-separation_of_concern.md プロバイダー /%href/

パッケージを使って、ツリーのルートに DataUsaApiClient インスタンスを注入する。

プロバイダー<DataUsaApiClient>(
    create: (context) => const DataUsaApiClient()、
        child: const MaterialApp()
        ホーム:HomePage()、
    ),
)

APIクライアントの使用

プロバイダーは、任意の子孫ウィジェット ( 私たちの _ _DetailScreen_ のような )_ が、ツリーの一番近い DataUsaApiClient の上位を読み取ることを可能にします。そして、実際の HTTP 実装の代わりに、その getMeasure メソッドを使用して Future を開始することができます。

overridevoid didChangeDependencies() { を実行します。
    super.didChangeDependencies();
    final year = ModalRoute.of(context)!.settings.arguments as int;
    if (_year != year) { _year = year; if (_year != year) { _year !
      if (_year != year.) { _year = year;
      final api = context.read<DataUsaApiClient>();
      future = api.getMeasure(year);
    }
}

デモ API クライアント

さて、これを利用しよう!

ご存じない方のために補足しておきます。 も暗黙的に関連するインターフェイスを定義します。 または

/または https://pub.dev/packages/riverpod リバーポッド /リバーポッド

(または他の解決策)_、それも素晴らしい仕事だっただろう。原則は同じで、ステートとロジックを分離している限り、他のソリューションからコードベースを移植することも可能です。

この分離は、ウィジェットのあらゆるステートを制御するのにも役立つので、ありとあらゆる方法でモックすることができます!

/参照 https://dartpad.dev/embed-flutter.html?id=e148980534dae261ef1174a15f7ac5cf&split=80&theme=dark Dartソースコードとデモ /%href/

専用ステートの作成

フレームワークの AsyncSnapshot に依存する代わりに、画面の状態を DetailState オブジェクトとして表現します。

オブジェクトを値で比較できるようにするために、 hashCode メソッドと operator == メソッドを実装することも重要である。これにより、2つのインスタンスが異なると見なされるべきかどうかを検出することができる。

💡 その

href/ https://pub.dev/packages/equatable 等価 /%href/

または

または https://pub.dev/packages/freezed フリーズ フリーズした

パッケージは hashCodeoperator == メソッドを実装するのに役立つ素晴らしいオプションだ!

抽象クラス DetailState {
  const DetailState(this.year);
  final int year;


  オーバーライドブール operator ==(Object other) => 同一(this, other)
      identical(this, other) || (other is DetailState &&)
      (他がDetailState &&である
          runtimeType == other.runtimeType && 年
          year == other.year);


  overrideint get hashCode => runtimeType.hashCode ^ year;

状態は常にyearに関連付けられているが、ユーザーに見せたいものに関しても4つの異なる状態が考えられる:

  • NotLoadedDetailState`:データの更新がまだ開始されていない。
  • LoadingDetailState`:データは現在ロード中です。
  • LoadedDetailState: データは関連するmeasure` と共にロードに成功した。
  • NoDataDetailState`: データはロードされたが、利用可能なデータはない。
  • UnknownErrorDetailState`: 操作が失敗した。
class NotLoadedDetailState extends DetailState {: データはロードされたが、利用可能なデータがない。
  const NotLoadedDetailState(int year) : super(year);
}


class LoadedDetailState extends DetailState { ロードされた詳細状態
  const LoadedDetailState({)
    必須 int year、
    required this.measure、
  }) : super(year);


  Final Measureメジャー;


  オーバーライドブール operator ==(Object other) => 同一(this, other)
      identical(this, other) || (other is LoadDetail State && measure == other.measure)
      (other is LoadedDetailState && measure == other.measure);


  この関数を使用すると、この関数を使用する必要があります;
}


class NoDataDetailState extends DetailState { 以下のようになります。
  const NoDataDetailState(int year) : super(year);
}


class LoadingDetailState extends DetailState { ローディングディテールステート(int年) : super(年); }.
  const LoadingDetailState(int year) : super(year);
}


class UnknownErrorDetailState extends DetailState { 以下のようになります。
  const UnknownErrorDetailState({)
    必須 int year、
    required this.error、
  }) : super(year);


  最終的な動的エラー;


  オーバーライドブール operator ==(オブジェクト other) => 同一(this, other)
      identical(this, other) || (other is Unknown Error Detail State && other is Unknown Error Detail State)
      (他はUnknownErrorDetailState &&です。
          year == other.year && エラー
          エラー == other.error);


  オーバーライドget hashCode => オブジェクト.ハッシュ(super.hashCode, エラー.has)

これらの状態は、AsyncSnapshotよりも明確である。そしてまた、これはコードをより保守しやすくする!

そしてまた、これによってコードの保守性が向上するのです! > 💡 私たちが強く推奨するのは、次のようなものです。 > > を強くお勧めします。 > https://pub.dev/packages/freezed#unionssealed-classes > フリーズしたパッケージのユニオン型 > /%href/ > > ロジックの状態を表現する!copyWithメソッドやマップ`メソッドのような多くのユーティリティが追加される。

ロジックをノーティファイアに置く

さて、ステートの表現ができたので、そのインスタンスをどこかに保存する必要があります - これが DetailNotifier の目的です。これは現在の DetailState インスタンスを value プロパティに保持し、状態を更新するメソッドを提供する。

初期状態として NotLoadedDetailState を提供し、api からデータをロードして現在の value を更新するために refresh メソッドを提供する。

class DetailNotifier extends ValueNotifier<DetailState> { {{DetailNotifier({NotLoadedDetailState`)
  DetailNotifier({
    required int year、
    required this.api、
  }) : super(DetailState.notLoaded(year));


  final DataUsaApiClient api;


  int get year => value.year;


  Future<void> refresh() async { {.
    if (価値は! LoadingDetailState) { { if (価値は! LoadingDetailState)
      値 = DetailState.loading(年);
      try {
        final result = await api.getMeasure(year);
        if (result != null) { { result = getMeasure(year).
          値 = DetailState.loaded(
            year: year、
            measure: 結果、
          );
        } else {
          値 = DetailState.noData(year);
        }
      } catch (エラー) { { value = DetailState.unknownError()
        値 = DetailState.unknownError(
          year: year、
          error: エラー、
        );
      }
    }
  }

ビューの状態を提供する

画面の状態をインスタンス化して観察するために、プロバイダーとその ChangeNotifierProvider にも依存する。この種のプロバイダーは、自動的に作成された ChangeListener を探し、変更が通知されるたびに Consumer から再構築をトリガーします _(ノーティファイアの値が前回と異なる場合)。

class DetailScreen extends StatelessWidget { ステートレスウィジェット
  const DetailScreen({
    キー?
  }) : super(key: key);


  オーバーライド
  ウィジェットbuild(BuildContext context) { ウィジェットbuild(BuildContext context)
    final year = ModalRoute.of(context)!.settings.arguments as int;
    return ChangeNotifierProvider<DetailNotifier>(
      create:(コンテキスト){」を選択します。
        final notifier = DetailNotifier(
          year: 年
          api: context.read<DataUsaApiClient>()、
        );
        notifier.refresh();
        return notifier;
      },
      child:Consumer<DetailNotifier>(
        builder: (context, notifier, child) { }.
             final state = notifier.value;
            // ...
        },
      ),
    );
  }
}

レビュー

素晴らしい!我々のアプリケーション・アーキテクチャはかなり良くなってきている。すべてが明確に定義されたレイヤーに分けられ、特定の懸念事項があります!🤗

しかし、テストしやすくするために、まだ1つ足りないものがあります。

ステップ4.ビジュアル専用レイアウトウィジェット

flutterアプリケーションのビジュアル専用レイアウトウィジェット

クリックUp経由

最後のステップでは、DetailScreenウィジェットに少し責任を持たせすぎました。そして、前に見たように、ビュー層でのロジックの責任を避けるようにします!

DetailScreen`ウィジェットを2つに分割します:

  • DetailScreenウィジェットを2つに分割します。DetailScreen`は、現在のアプリケーションの状態(ナビゲーション、通知、状態、サービス、...)からスクリーンの様々な依存関係の設定を担当します、
  • DetailLayoutは単純にDetailState`をウィジェット専用のツリーに変換します。

この2つを組み合わせることで、単純に DetailLayout のデモ/テストインスタンスを作成し、アプリケーションで実際に使用する場合は DetailScreen を使用することができる。

/参照 https://dartpad.dev/embed-flutter.html?id=6e4075480df18bfe42b932f419a231ac&split=80&theme=dark Dartソースコードとデモ /%href/

専用レイアウト

より良い分離を実現するために、Consumer ウィジェットの下にあるすべてを専用の DetailLayout ウィジェットに移動しました。この新しいウィジェットはデータを消費するだけで、インスタンスの生成は担当しません。読み込んだ状態を特定のウィジェットツリーに変換するだけです。

ModalRoute.ofの呼び出しとChangeNotifierProviderインスタンスはDetailScreenに残り、このウィジェットは単にDetailLayout` を設定済みの依存関係ツリーと一緒に返します!

このマイナーな改善はプロバイダーの使い方に特化したものですが、子孫ウィジェットが DetailState を直接消費できるように ProxyProvider も追加したことにお気づきでしょう。これにより、モック・データが簡単になります。

class DetailScreen extends StatelessWidget { ディテールスクリーン({)
  const DetailScreen({
    key?
  }) : super(key: key);


  オーバーライド
  ウィジェットbuild(BuildContext context) { ウィジェットbuild(BuildContext context)
    final year = ModalRoute.of(context)!.settings.arguments as int;
    return ChangeNotifierProvider<DetailNotifier>(
      create:(コンテキスト){」を参照してください。
        final notifier = DetailNotifier(
          year: 年
          api: context.read<DataUsaApiClient>()、
        );
        notifier.refresh();
        return notifier;
      },
      child: child:ProxyProvider<DetailNotifier, DetailState>(
        update: (context, value, previous) => value.value、
        child: const DetailLayout()、
      ),
    );
  }
}


class DetailLayout extends StatelessWidget { ステートレスウィジェット
  constDetailLayout({キー?
    キー?
  }) : super(key: key);


  オーバーライド
  ウィジェットbuild(BuildContext context) { @override
    return Consumer<DetailState>(
      builder: (コンテキスト, ステート, チャイルド) { }.
        return Scaffold(
          appBar:AppBar(
            タイトルテキスト('年 ${state.year}')、
          ),
          body: () { .
              // ...
          }(),
        );
      },
    );
  }

ウィジェットを専用クラスとして取り出す

ウィジェットツリーを専用クラスに展開することをためらってはいけません!そうすれば

/href/ /ブログ?p=56354 パフォーマンスが向上します。 /を向上させます。

を向上させ、コードをよりメンテナーにする。

この例では、関連するステート・タイプごとに1つのビジュアル・レイアウト・ウィジェットを作成しました:

if (state is NotLoadedDetailState || state is LoadingDetailState) { { (state is NotLoadedDetailState || state is LoadingDetailState)
    return const LoadingDetailLayout();
}
if (state is LoadedDetailState) { { (状態がLoadedDetailStateの場合)
    return LoadedDetailLayout(state: state);
}
if (state is UnknownErrorDetailState) { { (state is UnknownErrorDetailState)
    return UnknownErrorDetailLayout(state: state);
}
return const NoDataDetailLayout();

デモインスタンス

これで、何をモックして画面に表示するかを完全にコントロールできるようになった!

あとは DetailLayoutProvider<DetailState> でラップして、レイアウトの状態をシミュレートするだけだ。

ListTile(
    タイトル: const Text('Open "loaded" demo')、
    onTap: () {
        Navigator.push(
        context、
        MaterialPageRoute(
            builder: (コンテキスト) { { プロバイダ<DetailState>.value()
            return Provider<DetailState>.value()
                    値: const DetailState.loaded(
                    年2022,
                    measure: Measure(
                        年2022,
                        人口425484,
                        nation: 'United States'、
                    ),
                    ),
                    child: const DetailLayout()、
                );
            },
        ),
        );
    },
),
リストタイル(
    タイトル: const Text('Open "loading" demo')、
    onTap: () {
        Navigator.push(
        コンテキスト
        MaterialPageRoute(
            builder: (コンテキスト) { { プロバイダ<DetailState>.value()
                    return Provider<DetailState>.value(
                        値: const DetailState.loading(2022)、
                        child: const DetailLayout()、
                    );
                },
            ),
        );
    },
),

結論

メンテナンス可能なソフトウェアアーキテクチャを作るのは簡単ではありません!将来のシナリオを予測することは多くの努力を必要としますが、私が共有したいくつかのヒントが将来あなたの役に立つことを願っています!

例は単純に見えるかもしれません。過剰にエンジニアリングしているようにさえ見えるかもしれませんが、アプリの複雑さが増すにつれて、このような標準を持つことは大いに役立つでしょう!💪

Flutterを楽しんで、ブログをフォローしてこのような技術記事をもっとゲットしよう!お楽しみに