パルカワ2

最近はFlutterをやっています

Flutter/Dartでcustom lint ruleを作りたい

推奨されないコードを機械的に発見出来る部分は人間がコードレビューなどで指摘するのではなくLinterで気づきたい。 なので、プロジェクトごとにLinterのCustom Ruleを作りたいのだが、DartのLinterはCustom Ruleに対応していない。

github.com

このイシューに書かれてある通り、linterコマンドを自分で作る方法とanalyzer_pluginを利用する方法がある。

linterコマンドを自分で作る

この方法は実装が簡単というメリットがあり、IDE上ではエラーや警告が表示されないというデメリットがある。

linter/linter.dart at master · dart-lang/linter · GitHub

linter.dart の中で自分で作ったRuleを登録してあげて実行する。まあこういう感じで出来る

Future main(List<String> args) async {
  final enableRules = [
    MyCustomRule(),
  ];
  for (final rule in enableRules) {
    Analyzer.facade.register(rule);
  }
  await cli.run([
    '--packages=.packages',
    '--rules=${enableRules.map((r) => r.name).join(",")}',
    'lib',
    'test',
  ]);
}

analyzer_pluginを利用する方法

この方法はIDE上でもエラーや警告が表示されるというメリットがあり、実装やデバッグがめんどくさいというデメリットがある。

analyzer_plugin | Dart Packageを利用する。

めんどくさい実装をメンテし続けるのはつらいので、dart_code_metricsというライブラリをforkして自前のルールを追加するのはどうかと思ってやってみた。

試しに追加したのは、 DateTime.utc() とかを使うとエラーにするRule

github.com

これでDateTime.utc(2020) とかするとIDE上でエラーになる。ただまあ、forkなのであんまりやりたくないなと思っている。

RiverpodでModel-View-Updateを試す

ここ1年くらいFlutterで開発していて、Model-View-ViewModel(MVVM)を採用したけどViewModelの役割が大きいなど色々課題を感じていて、より良い形はないかなとFlux, Reduxなどのドキュメントを見ている。 とは言ってもFluxやReduxなどドキュメント見る限り結構めんどくさい事が多い印象で実際に開発してると大変そうだな〜と感じている。

なので、Stateの管理をRiverpodに任せてそれを薄くラップするくらいでよりシンプルな形に出来ないか考えている。

前提

  • The Elm ArchitectureをTEAと略します。
  • Model-View-UpdateをMVUと略します。
  • MVUというかThe Elm Architectureはこのあたりがわかりやすいです
  • Riverpod自体がStateを更新する前に処理を行ったり、更新したあとに処理を行うことは出来ないので自前で行う必要がある
  • Reduxでめんどくさそうだなと思っているのは1つのStoreを使い回すこと・Action objectを作ってdispatchしてStateを更新するあたりです。
    • 1つのStoreを使い回さないのはRiverpodを使えば解決できます

自分なりの理解

アプリには以下のようなフェーズがあると思っている。

  • 最初に値を取得しStateに反映させる「初期化表示」フェーズ
  • ユーザーの入力によって処理を行ってStateを更新しViewに反映させる「更新表示」フェーズ

それをMVUに当てはめて図にするとこんな感じ f:id:hisaichi5518:20210729234740p:plain

Flutter WidgetがView、StateがModel、ActionsがUpdateを行うくんです。

初期化表示のときもUpdateを経由するのでは?と思っていたのだけど、TEAのドキュメントを見る感じそういうわけではなさそうだった。

またTEAにはCmd(Command)という概念があって、そこで副作用のある処理を行ってupdateにstateを更新させる形になっていそうだけど、Actionsという名前を使うことにした。

さっそく実装する

カウンターアプリを作るのだが、ただlocal stateの数字を増やすだけのアプリだと簡単すぎるので、以下の仕様があるアプリをつくる。

  • カウント数はSharedPreferenceに保存される
  • アプリを立ち上げた時は、保存されたカウント数を非同期で取得し表示する
  • カウント数を取得している間はLoadingを表示する
  • カウントはユーザーの入力により増加し、同時にSharedPreferenceに保存される

図にするとこんな感じ f:id:hisaichi5518:20210730000502p:plain

図だとわかりにくい気がしたのでコードにコメントしていく

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:shared_preferences/shared_preferences.dart';

final counterNotifierProvider =
    StateNotifierProvider<CounterNotifier, AsyncValue<int>>(
  (ref) => CounterNotifier(ref.read)..initialize(),
);

final sharedPreferenceProvider = FutureProvider(
  (ref) async => SharedPreferences.getInstance(),
);

final actionsProvider = Provider((ref) => CounterActions(ref.read));

/// CounterのStateをクラスで定義している。
/// 読み書きを行うStateで初期値をasync/awaitで読み込む時はStateNotifierを使う。
class CounterNotifier extends StateNotifier<AsyncValue<int>> {
  CounterNotifier(this.read) : super(const AsyncValue<int>.loading());

  final Reader read;

  /// ここで初期値を取得している
  /// AsyncValueを利用することで、1つのStateで読み込み中, エラー, データを表現できる
  Future<void> initialize() async {
    await Future<void>.delayed(const Duration(seconds: 1));
    final sharedPreference = await read(sharedPreferenceProvider.future);
    state = AsyncValue.data(sharedPreference.getInt('counter') ?? 0);
  }

  /// incrementするメソッド
  /// このクラスはStateなので自分のStateしか更新しない
  /// 本来ここは副作用がない(入力が同じなら何度実行してもStateが同じ結果になる)のがテストをしやすいと思うのだが、メソッドである以上そんな制約持たせるの無理じゃない?と思うので気にしないことにした。
  /// Elmは言語仕様でそういう制約があるみたい。
  Future<void> increment() async {
    final data = state.data;
    if (data == null) {
      // loading or error
      return;
    }
    final newCount = data.value + 1;
    state = AsyncValue.data(newCount);
  }
}

/// ユーザーの入力を受け取り、Stateの更新やBackend APIとやり取りを行うクラス
class CounterActions {
  CounterActions(this.read);

  final Reader read;

  Future<void> increment() async {
    /// CounterNotifierのincrementを呼び出してStateを更新
    await read(counterNotifierProvider.notifier).increment();

    /// LocalStorageのデータも新しいデータで更新
    final newCount = read(counterNotifierProvider).data?.value ?? 0;
    final sharedPreference = await read(sharedPreferenceProvider.future);
    await sharedPreference.setInt('counter', newCount);
  }
}

Future<void> main() async {
  runApp(
    const ProviderScope(child: MyApp()),
  );
}

class MyApp extends ConsumerWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    return MaterialApp(
      home: Home(),
    );
  }
}

class Home extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    return Scaffold(
      appBar: AppBar(title: const Text('Counter example')),
      body: Center(
        child: Consumer(builder: (context, ref, _) {
          /// 初期値の読み込みやStateが変更されたらrebuildなどwatchすれば行われる
          final count = ref.watch(counterNotifierProvider);
          /// 非同期でカウント数を読み込むので、loading, errorなどをちゃんと表現する
          return count.map(
            data: (data) => Text('${data.value}'),
            loading: (_) => const CircularProgressIndicator(),
            error: (_) => const Text('On Error'),
          );
        }),
      ),
      floatingActionButton: FloatingActionButton(
        /// ここでStateを更新するのではなくActionsを経由する
        onPressed: () => ref.read(actionsProvider).increment(),
        child: const Icon(Icons.add),
      ),
    );
  }
}

まとめ

このようにStateの初期化の処理を行い表示するフェーズ・ユーザーの入力に起因して処理を行いStateを更新して表示するフェーズがある。

ViewModelのようにすべてのことをViewModelが行うのではなくStateに初期化やActionsに更新の役割を与えることで責務の分離がより出来るのではないかと思っている。

FlutterのStatefulWidgetの課題と解決方法

FlutterでStateをもたせるには色々あるが、基本はStatefulWidgetを利用する。

StatefulWidgetは以下のようにStateを持ち、Stateの中で値を持ったりsetしたりできる。

StatefulWidget class - widgets library - Dart API

class Bird extends StatefulWidget {
  const Bird({
    Key? key,
    this.color = const Color(0xFFFFE306),
    this.child,
  }) : super(key: key);

  final Color color;
  final Widget? child;

  @override
  _BirdState createState() => _BirdState();
}

class _BirdState extends State<Bird> {
  double _size = 1.0;

  void grow() {
    setState(() { _size += 0.1; });
  }

  @override
  Widget build(BuildContext context) {
    return Container(
      color: widget.color,
      transform: Matrix4.diagonal3Values(_size, _size, 1.0),
      child: widget.child,
    );
  }
}

他にもinitStateやdisposeなどWidgetが生成されるときや破棄されるタイミングに呼び出されるCallbackなどがある。

詳しくはドキュメントを読んでもらうとしてそんなのがある。

ちなみに逆にStatelessWidgetはStateを持たずinitStateなどもない。

StatefulWidgetの課題とは

initStateなどがあると言ったが、例えばテキストを入力できる画面を作りたいときにTextEditingControllerをinitStateで作って、disposeで破棄する処理を書いたりする。

TextEditingController class - widgets library - Dart API

/// Flutter code sample for TextEditingController

// This example creates a [TextField] with a [TextEditingController] whose
// change listener forces the entered text to be lower case and keeps the
// cursor at the end of the input.

import 'package:flutter/material.dart';

void main() => runApp(const MyApp());

/// This is the main application widget.
class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  static const String _title = 'Flutter Code Sample';

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      title: _title,
      home: MyStatefulWidget(),
    );
  }
}

/// This is the stateful widget that the main application instantiates.
class MyStatefulWidget extends StatefulWidget {
  const MyStatefulWidget({Key? key}) : super(key: key);

  @override
  State<MyStatefulWidget> createState() => _MyStatefulWidgetState();
}

/// This is the private State class that goes with MyStatefulWidget.
class _MyStatefulWidgetState extends State<MyStatefulWidget> {
  final TextEditingController _controller = TextEditingController();

  @override
  void initState() {
    super.initState();
    _controller.addListener(() {
      final String text = _controller.text.toLowerCase();
      _controller.value = _controller.value.copyWith(
        text: text,
        selection:
            TextSelection(baseOffset: text.length, extentOffset: text.length),
        composing: TextRange.empty,
      );
    });
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Container(
        alignment: Alignment.center,
        padding: const EdgeInsets.all(6),
        child: TextFormField(
          controller: _controller,
          decoration: const InputDecoration(border: OutlineInputBorder()),
        ),
      ),
    );
  }
}

他にもScrollControllerだったりなんとかControllerがよくある。

そのたびに生成して破棄する処理を書く必要がある。

つまりTextEditingControllerに関する処理をinitStateやdisposeに書く必要があり関連するコードが一箇所にまとまらず分散してしまったり、何度も同じコードを書く必要がある。

解決方法1: mixinする

mixinする方法がある。

ただしmixinはTextEditingControllerが複数ある場合に対応出来ない。

解決方法2: Builderを用意する

TextEditingControllerBuilderみたいなWidgetを作って利用すればmixinの問題は解決するが、この方法はBuilderを使うのでbuilder外でControllerを使うことが難しくなる。

解決方法3: flutter_hooks

このように use*Controller すればいいだけになる。解決方法1, 2で起きるような問題は起きない。

flutter_hooks | Flutter Package

class Example extends HookWidget {
  const Example({Key key, required this.duration})
      : super(key: key);

  final Duration duration;

  @override
  Widget build(BuildContext context) {
    final controller = useAnimationController(duration: duration);
    return Container();
  }
}

ただし、flutter_hooksはそれなりにデメリットもあると感じている。

  • HookWidgetを継承する形になり一般的なStatelessWidgetを継承する形ではない
  • useEffectなどを多用するとbuildメソッドが大きくなってしまう
  • use* は if の中で使う事はできないが今の所機械的に発見しにくい
  • Flutter公式が提供している機能ではない

イシューにはなっていないのか

イシューはある。

Static Metaprogrammingがあればいいみたいな話になっているがどうか

Reusing state logic is either too verbose or too difficult · Issue #51752 · flutter/flutter · GitHub

static metaprogramming自体導入されるかはまだ未定

language/intro.md at master · dart-lang/language · GitHub

hooksはFlutterに入らないのか

このイシューで色々と議論されたようですが今の所はなさそう。

Widget hooks · Issue #25280 · flutter/flutter · GitHub

まとめ

flutterh_ooksを使うのは良さげではあるのだが使う判断はまだ出来ておらずどうしたものかなーと思いながらStatefulWidgetを作っている。

なぜRiverpodのFutureProviderを推すのか

RiverpodにはFutureProviderというのがある。

FutureProvider class - riverpod library - Dart API

非常によく出来たAPIだなぁと思ったのでそれのメリット・デメリットと所感をまとめたい。間違えているとかなにかあれば教えてください。

まずFutureProviderの例

コードは、公式ドキュメントから引用している。

FutureProvider class - riverpod library - Dart API

設定ファイルからJSONを取得してDartのObjectに変換して返すという処理があるとする。設定ファイルからJSONを取得部分がasync/awaitに依存している。

Provider

FutureProviderは値を取得して返す処理が書かれている。 Configurationは1度取得したら明示的にconfigProvider に対して reloadしたりするまで設定ファイルが再度読まれることはない。

final configProvider = FutureProvider<Configuration>((ref) async {
  final content = json.decode(
    await rootBundle.loadString('assets/configurations.json'),
  ) as Map<String, Object?>;

  return Configuration.fromJson(content);
});

Widget

FutureProviderをwatchするとAsyncValueが返ってくる。Futureを使うということは、loading, error, data(データ取得成功)の状態があるので、それぞれの状態にあったWidgetを返す。

Widget build(BuildContext, ScopedReader watch) {
  AsyncValue<Configuration> config = watch(configProvider);

  return config.when(
    loading: () => const CircularProgressIndicator(),
    error: (err, stack) => Text('Error: $err'),
    data: (config) {
      return Text(config.host);
    },
  );
}

FutureProviderはどういうことができるのか

アプリを作るときによくある「値をアプリ外から取得する」「取得中などの状態管理をする」「取得した値をStateとして持つ」ことができる。もちろんRiverpodのProviderとして持つので、値が更新されたら再描画などができる。

具体的にはこういうの

  • 設定ファイルから読み込んでStateとして持ちたい
  • Web APIを叩いて値をStateとして持ちたい

ちなみにReact界隈では"Application Stateと呼んでいるものの多くは、実際にはサーバーにあるStateをクライアント側でキャッシュしているに過ぎない"みたいな話があるらしくまぁ確かにと思いながらこのツイートのツリーを読んでいた。

FutureProviderのメリット

  • initStateなどで最初の読み込み処理を呼ぶ必要がない
  • 依存するデータが更新されたときに値の再生成が自動的に行われる
  • AsyncValueが返ってくる形になっているのでloading, error, dataなどの状態管理が必ず行われる
  • loading, errorなどの状態管理を自分でする必要がない

FutureProviderのデメリット

  • initStateなどで明示的に呼び出す形ではないので、どのタイミングでAPIが呼び出されるか慣れていないのわからないことがある

所感

Riverpodは、値Aが更新されたら値Aに依存する画面を再描画したり、値Aに依存する値の更新が行われる。 同様にWeb APIを叩く時に値Aに依存している場合、値Aが更新されたらWeb APIも再度叩いてほしい場合などがある。具体的には、FutureProviderがAPIClientに依存していてAPIのエンドポイントが変わってAPIClientが更新された時など。

initStateなどで読み込む場合はそういうのに対応するには自前で書く必要があるが、Riverpodにまかせてしまうとそれが不要になる。

明示的にAPIを呼び出さないので混乱のもとになるような気もしたけど、他の値同様に依存している値が更新されたら更新されるという形になるので設計的にも統一感があってシンプルになると思う。

またFutureを使うということは、loading, error, sccuessという状態管理を必ず行う必要がある。FutureProviderを使えば自分たちでisLoadingとかを管理していたのが不要になる。

このような理由から個人的には値を取得する部分ではFutureProviderを使うのがよいと感じている。

関連

この記事を書くにあたって読んだ記事など。SWRやreact-queryは関連している気がしたので読んだ。参考になりました。ありがとうございます!

1つのWidgetの役割をとにかく小さくしたい

第5回 今ならこうするシリーズ。更新を忘れていた。

自分でFlutterのWidgetを作っているときに以下のように値を受け取るWidgetを作りがちだった。

HogeButton(title: "ほげボタン", titleColor: ...);

class HogeButton extends StatelessWidget {
    @override
    Widget build(BuildContext context) {
        return TextButton(
            style: ...,
            child: Padding(
                ...,
                child: Text(title, style: ...)
            ),
        );
    }
}

今ならこうする

Widgetを受け取る形のWidgetを作る。

HogeButton(
    child: HogeButtonRegularText(
        title: "ほげボタン",
        titleColor: ...
    ),
);

class HogeButtonRegularText extends StatelessWidget {
    @override
    Widget build(BuildContext context) {
        return Text(title, style: ...)
    }
}

class HogeButton extends StatelessWidget {
    @override
    Widget build(BuildContext context) {
        return TextButton(
            style: ...,
            child: Padding(..., child: child),
        );
    }
}

ここまで書いてボタンの例はあんま良くないかもなと思いつつめんどくさいのでそのまま進める。

理由

Widgetの役割を分けたい

値だけを受け取るWidgetのHogeButtonは、以下のような役割がある。

  • Buttonのスタイルを決める
  • Buttonのchildを囲むPaddingを決める
  • Buttonのchildの内容を決める
  • Buttonのchildのテキストスタイルを決める

一方でchildを受け取るWidgetのHogeButtonは、以下のような役割がある。

  • Buttonのスタイルを決める
  • Buttonのchildを囲むPaddingを決める

その代わりにHogeButtonRegularTextが以下を担う。

  • Buttonのchildのテキストスタイルを決める

またButtonのchildの内容を決めるのは、HogeButtonを呼び出すところになる。

このように役割を分けることで、密結合が避けられて影響範囲を小さくし変更に対するコストを下げることができる。

具体的には、ボタンにテキストだけではなくIconを表示したい場合、childを受け取るWidgetの場合はHogeButtonIconTextを作ればよいが、値だけを受け取るWidgetはIconがある場合は表示するなど分岐をする必要があり後々大変になってくる。

分岐を避けるためにIconHogeButtonを作ることはできるが、HogeButtonとボタンのスタイルは同じなのにWidgetは分ける必要があるため、再利用性が低い。

懸念点

コードの記述量が多くなるのでは

多くはなる。ただchildを受け取るWidgetのほうが関心の分離は出来ているし、それぞれのクラスは非常にシンプルな役割になっている。逆に値だけを受け取るWidgetだとコードの量自体は少ないが、HogeButtonが複数の責任を持つクラスになり変更に対するコストが大きくなってしまう。

自由度が高すぎるのでは

Widgetであればなんでも受け取れるのでHogeButtonRegularTextがすでにあることに気づかずに自前で作ってしまうみたいなこともありそう。それをどうやって防ぐかは命名規則をしっかり決める、デザインするときに名前をつける以外で今の所いいアイデアがない。

メソッドを分ければ役割が分かれるのでは

メソッドで分ける方法は、影響範囲が明確に分かれていないので役割も分かれていない。

hisaichi5518.hatenablog.jp

まとめ

正直なところ、こういうことは1年で消えてなくなる可能性があるサービスは、あんまり気にする必要がないと思っているけど、少なくとも数年は生き残りそうなソフトウェアは、コードの記述量が少ないといった短期的に楽なコードではなく変化しやすさを重視した中長期的に楽なコードを目指すべきではないかと思っている。

あと当たり前なんだけど値だけを受け取るWidgetを一切作らないのは無理な話なのでそれは考えてない。むずかしいな〜〜

_buildWidgetについての考えと今ならどうするか

第4回 今ならこうするシリーズ

UIをつくるときにStatelessWidget(またはStatefulWidget)を継承してWidgetを作るのではなく、以下のようにWidgetを返すメソッドを作って切り分けることが出来る。メソッドで切り分けるとクラスを作るよりは比較的楽に作れるという理由でそうやってることが多い。僕もそう思ってやっていた。

ここでは、これを_buildWidgetと呼ぶ。今回はこの_buildWidgetについて書く。

class HogeView extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        _buildHoge(context),
        _buildFuga(context),
        _buildPiyo(context),
        _buildNyan(context),
      ],
    );
  }

  Widget _buildHoge() { ... }

  ...
}

考え

高速化・コードの読みやすさ・リファクタリングの観点からやめたほうがよい。

なぜ

無駄なbuildがされやすい

上の例で _buildNyan メソッドの中で context.watch<Config>() していたとする。

Configに変更があったとき本来は _buildNyan の内容だけrebuildされてほしいが、_buildHogeなども再度buildされることになり、無駄なbuildが走る。

詳しくは10Xの社内向けに書いた「FlutterのWidgetを書く時に知ってると便利なこと」というドキュメントで書かれているので入社してください。

open.talentio.com

各メソッドの影響範囲が掴みにくい

上記の通り、メソッド内だけではなくメソッド外にもUIの再構築という影響を与えうる。

Widget build(BuildContext context) の中でしか利用していないのならまだよくて、_buildHogeの中で更に_buildFooを呼んでいるみたいなことも多々あり、_buildFooの影響範囲はコードを追わないとちゃんと掴めない。

1つのWidgetクラスにたくさんの役割が与えられてしまいコードの再利用性が低い

最初は1画面でしか利用していなかったが他の画面で利用するようになった場合など、クラスであればprivateからpublicにするだけで基本よい。そのクラスがViewModelに依存していると話は変わってくるけどそれは別の話。

メソッドにしていると再利用するにはクラス化する必要があり、クラス化にあたって依存しているメソッドやパラメータを引き剥がす必要がある。またUIの再構築という影響をメソッド外にも及ぼしていたので、クラス化することでUIの再構築が起きなくなってバグが出ないかなどリファクタリングするときに考慮することが多くなる。

今ならどうするか

Widgetクラスをつくって影響範囲と役割を狭め再利用性を高める。

まとめ

最初は高速化のためにメソッドで切り分けるのをやめたほうがいいなくらいに軽く考えていて、まあ速度面で困ったらクラスに分けるくらいでいいかなと思っていたのだが、変化のしにくさを感じるようになってからはクラスを分けるようにしている。言ってしまえば高速化のところはわりかしどうでもよくてクラスの責務をしっかり分けましょうみたいな話のほうが個人的には重要。

次回はWidgetの設計について書きたいけど、書くことが多すぎるので分けるかも。

TextStyleの共通化についての考えと今ならどうするか

第三回 今ならこうするシリーズ

FlutterはTextにstyleをあてることができる。今回はそのTextStyleの共通化について書いていく。

const Text("文字", style: ...);

TextStyleは以下のように共通化している。こうすることで基本はconstとして呼び出すことが出来るし、画面によって文字色の変更がしたいときは copyWithで上書きすることも出来る。またTextStylesをimportしてしまえば、名前の補完が効く。

class TextStyles {
  static const body = TextStyle(
    fontSize: ...,
    color: ...,
    height: ...,
  );
}

// 呼び出すとき
const Text("文字", style: TextStyles.body);

// 上書きするとき
const Text("文字", style: TextStyles.body.copyWith(color: ...));

考え

比較的いい方法だと思っていてやめる気はないけど気になることもある。

なぜ

ファイル移動しないとデフォルト値がわからない

static constで定義しているので、呼び出し元からはTextStyleが何を受け取っているのかわからない。デフォルトでは、色が何色が設定されているのかファイル移動せずに確認したい。InteliJ固有の問題かも。

BuildContextに依存して文字の大きさを変更できない

static constで定義しているので、変更されうる画面の大きさなどに基づいて文字の大きさのデフォルト値を変更できない。

今ならどうするか

TextStylesのままでいく。

ただ以下のようにWidgetを作成するのもよいかもと思っている。

class TextBody extends StatelessWidget {
  final String data;
  final Color color;

  const TextBody(
    this.data, {
    Key? key,
    this.color = AppColors.black,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    final breakpoint = context.watch<Breakpoint>();

    return Text(
      data,
      style: TextStyle(
        fontSize: breakpoint.map(
          mobile: (_) => 14,
          tablet: (_) => 15,
          desktop: (_) => 16,
        ),
        color: color,
      ),
    );
  }
}

ただしデメリットもあると感じている。なのでTextStylesで定義するのがよいと思っている。

  • 値を変更するためには受け取るパラメータを自分で書く必要がある
  • Widgetを作成する必要があり、書くコード量が多い
  • SelectableTextなどもあるのでそれぞれに対応が必要になる

まとめ

一長一短で悩ましい。例えばクラスにする方法は値を変更するためには自分でパラメータを受け取る必要がある。画面ごとに変わるパラメータが多い場合はめんどくさく感じそう。またWidgetを作成する必要があるので比較的書くコードが多くなる。

しかし、今回あげたデフォルト値がわからない問題はインスタンス作成時の引数として受け取るときにデフォルト引数に設定すればわかるようになるし、画面の大きさなどBuildContextに依存する値によってデフォルトの文字の大きさを変更したい場合には有効だと思う。

余談: Flutter folioでは同様にTextStylesが定義されていた。

github.com

次回は_buildWidgetについて。