今のところのWidgetの書き方まとめ

2022年の3月あたりからFlutter製アプリを本格的に作り始めて1年半過ぎて、Widgetの書き方の今のところの型をまとめておく。

Flutter製アプリを作り始めていたころ、"Everythins is a Widget"という考え方のWidgetはとても強力でKotlinでAndroidアプリを書いていた私にとってはとても衝撃だった。 とても自由だったし、Kotlinで書いてた頃は面倒だったリストの実装もサクサクできたのはとても魅力で、Widgetの思想の強力さを実感していた。

ただ、何も考えずにWidgetを書いていくとネスト地獄になっていく。 一人で個人開発で書いてる分ではいいかもしれないけど、仕事でチーム開発してると他人が書いたコードをレビューしたり、自分のコードをレビューしてもらったりするときにネスト地獄のWidgetのコードを見るのは苦痛だし、見てもらうのは申し訳ない気持ちになるし、効率的ではない。 そこから、いろいろ型を作っていくことにした。

PageとWidget

画面全体をHogePage、画面上のある要素のWidgetをHogeWidgetと命名している。 確かFlutter公式のサンプルコードでも画面のコードファイル名のSuffixにPageと使っていたのでそうするようにした。 なんでもない命名規約だけど、コードを読む時にとりあえず、HogePageから読めば画面の理解ができるの目印になるので大事かなと思う。

1画面、1データクラス

基本的に1画面につき、表示するデータクラスを定義して画面表示必要なデータをそのデータクラスに変換して表示させる。 状態管理にRiverpodを使うことが多くて、Riverpodを使って書くとこんな感じになる。

画面クラス

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pareto/presentations/sample/sample_view_state_provider.dart';

class SamplePage extends ConsumerWidget {
  const SamplePage({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final sampleViewState = ref.watch(sampleViewStateFutureProvider);
    return sampleViewState.when(data: (data) {
      // データ取得時の画面
      return Scaffold(
        appBar: AppBar(
          title: Text(data.title),
        ),
        body: Center(
          child: Text(data.body),
        ),
      );
    }, error: (error, stackTrace) {
      // データ取得失敗時の画面
      return Scaffold(
        appBar: AppBar(
          title: const Text('Error'),
        ),
        body: Center(
          child: Text(error.toString()),
        ),
      );
    }, loading: () {
      // データ取得中の画面
      return Scaffold(
        appBar: AppBar(
          title: const Text('Loading'),
        ),
        body: const Center(
          child: CircularProgressIndicator(),
        ),
      );
    });
  }
}

データクラス

class SampleViewState {
  final String title;
  final String body;

  SampleViewState({required this.title, required this.body});
}

データプロバイダークラス

import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pareto/presentations/sample/sample_view_state.dart';

final sampleViewStateFutureProvider =
    FutureProvider<SampleViewState>((ref) async {
  // ここで何かしらの通信をしてデータを取得し、データクラスにデータ入れて、呼び出し先に返却する
  return SampleViewState(title: 'Sample', body: 'Hello, World!');
});

モバイルアプリ開発だとサーバサイドのWebAPIを叩いて、データを取得し画面上にデータを表示することが多いと思う。 その際、非同期でデータ取得してから表示するまでをRiverpodで繋ぐとデータ取得時、データ取得失敗時、データ取得中と分けて画面を書き分けることができてとても便利だ。

riverpod.dev

Widgetの分離

ただ、このままだとbuildメソッドの中が見にくくなる。サンプルコードなのにすでにちょっと見にくい。 なので、データ取得のフェーズ毎にWidgetを生成するメソッドとして分離する

class SamplePage extends ConsumerWidget {
  const SamplePage({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final sampleViewState = ref.watch(sampleViewStateFutureProvider);
    return sampleViewState.when(data: (data) {
      return createContentWidget(data);
    }, error: (error, stackTrace) {
      return createErrorWidget(error);
    }, loading: () {
      return createLoadingWidget();
    });
  }

  Widget createContentWidget(SampleViewState data) {
    return Scaffold(
      appBar: AppBar(
        title: Text(data.title),
      ),
      body: Center(
        child: Text(data.body),
      ),
    );
  }

  Widget createErrorWidget(Object error) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Error'),
      ),
      body: Center(
        child: Text(error.toString()),
      ),
    );
  }

  Widget createLoadingWidget() {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Loading'),
      ),
      body: const Center(
        child: CircularProgressIndicator(),
      ),
    );
  }
}

これだけでもだいぶ見やすくなったと思う。 メソッドも並びもwhenメソッドのパラメータ毎に並べると有効だと思う。 些細なことだけどこういうのでもコードを読む際の認知負荷が下がって読みやすいコードになる。

CustomWidgetクラス

画面設計の段階で画面の構成毎や複数画面で共通化された部分をCustomWidgetとして定義して置くことでコードの見通しが良くなる。 また、クラスかすることでパフォーマンスの向上にもつながる。

www.youtube.com

個別で動作するWidget

一つの画面内で複数の項目が個別でデータを読み込んで画面表示する場合は個別で動作するWidgetを作る 基本的には1画面に一つのデータクラスを定義して、表示データを管理しているが画面表示して、画面内のWidget毎にデータを非同期で表示する場合はそのWidget毎に表示用のデータクラスを定義して実装している。 これもスコープが狭くなってコードが見やすいし、使い勝手がいい。