未熟学生エンジニアのブログ

TetsuFeの個人開発ブログ

TetsuFeはテツエフイー と読みます。FlutterやWeb周り全般、チーム開発について語るブログ

Flutter Webのテストコードを書く際のトラブルシューティング集

$ flutter test --platform chrome$ flutter test でFlutter web用のテストをする際のトラブルシューティングとして、自分が遭遇・解決したものをまとめました。

FlutterやFirebaseの公式IssueやStack Overflowに散らばっている情報を色々試してみてうまくいったものを書いていますが、環境によっては動かないものもあるかもしれません。まだまだFlutter webの開発は大変だと感じていますが、Issueを追うのは勉強になりますね。

環境

  • Flutter channel dev 1.20.0-2.0.pre
  • PC: macOS 10.15.5
  • CI: subosito/flutter-action@v1 (最後にworkflowファイルあり)

1. --platform chrome を使う場合のエラー

Unable to find modules for some sources

Please check the following imports: import 'generated_plugin_registrant.dart'; from flutter_novel|lib/main_web_entrypoint.dart at 5:1

もう少し詳細なエラー出力

Unable to find modules for some sources, this is usually the result of either a
bad import, a missing dependency in a package (or possibly a dev_dependency
needs to move to a real dependency), or a build failure (if importing a
generated file).

Please check the following imports:

import 'generated_plugin_registrant.dart'; from flutter_novel|lib/main_web_entrypoint.dart at 5:1

Failed after 45.5s
Failed to compile tests
##[error]Process completed with exit code 1.

原因

lib/generated_plugin_registrant.dart は、以下の例のように自動生成されるようです。

//
// Generated file. Do not edit.
//

// ignore: unused_import
import 'dart:ui';

import 'package:url_launcher_web/url_launcher_web.dart';

import 'package:flutter_web_plugins/flutter_web_plugins.dart';

// ignore: public_member_api_docs
void registerPlugins(PluginRegistry registry) {
  UrlLauncherPlugin.registerWith(registry.registrarFor(UrlLauncherPlugin));
  registry.registerMessageHandler();
}

しかし、エラーになる場合は、このファイルの中身が開発環境でビルドした場合とCIでビルドした場合で異なることがあるようです。開発環境でのテストが通る場合は、これが原因です。

その理由は、Flutterプロジェクトを作る際のデフォルトの.gitignore では lib/generated_plugin_registrant.dart は ignore対象となっており、開発環境のファイルをCIで使っていないためです。ない場合は自動生成されるはずです。

.gitignore(一部)

# Web related
lib/generated_plugin_registrant.dart

解決法

開発環境でビルドした場合とCIでビルドした場合で異ならないように、これを ignore対象から外し、lib/generated_plugin_registrant.dart をコミットします。

.gitignoreの一部

# Web related
# lib/generated_plugin_registrant.dart
$ git add lib/generated_plugin_registrant.dart
$ git commit -m "<任意のコミットメッセージ>"
$ git push 

2. --platform chrome を使わない場合のエラー

No such file or directory import 'package:analyzer/dart/analysis/results.dart';

または、

No such file or directory import 'package:analyzer/dart/analysis/utilities.dart';

もう少し詳細なエラー出力

/opt/hostedtoolcache/flutter/1.20.0-0.0.pre-dev/x64/packages/_flutter_web_build_script/lib/build_script.dart:9:8: Error: Error when reading '/b/s/w/ir/k/archive/flutter/.pub-cache/hosted/pub.dartlang.org/analyzer-0.39.8/lib/dart/analysis/results.dart': No such file or directory
import 'package:analyzer/dart/analysis/results.dart';
       ^
/opt/hostedtoolcache/flutter/1.20.0-0.0.pre-dev/x64/packages/_flutter_web_build_script/lib/build_script.dart:10:8: Error: Error when reading '/b/s/w/ir/k/archive/flutter/.pub-cache/hosted/pub.dartlang.org/analyzer-0.39.8/lib/dart/analysis/utilities.dart': No such file or directory
import 'package:analyzer/dart/analysis/utilities.dart';

原因

原因はよくわからなかったです。パッケージのバージョンの問題?のようですが・・

参考: Swapping Flutter SDKs via IDE doesn't work with web builds. · Issue #2012 · Dart-Code/Dart-Code · GitHub

解決策

$ flutter update-packages を実行すると動きます。

3. Firebase関連

top_level.dart:1:8: Error: Not found: 'dart:html' import 'dart:html';

Error: Not found: 'dart:js' export 'dart:js' show allowInterop, allowInteropCaptureThis;

もう少し詳細なエラー出力

/opt/hostedtoolcache/flutter/1.20.0-2.0.pre-dev/x64/.pub-cache/hosted/pub.dartlang.org/firebase-7.3.0/lib/src/top_level.dart:1:8: Error: Not found: 'dart:html'
import 'dart:html';
       ^
/opt/hostedtoolcache/flutter/1.20.0-2.0.pre-dev/x64/.pub-cache/hosted/pub.dartlang.org/js-0.6.2/lib/js.dart:8:1: Error: Not found: 'dart:js'
export 'dart:js' show allowInterop, allowInteropCaptureThis;

原因と解決策

firebaseなどをpubspec.yamlに書いてmobile版をビルドする際に、dart:html系のエラーが出るらしいです。参考: Add Firebase to Flutter mobile and web | by Praharsh Bhatt | Multiverse Software | Medium

Flutter webだけで使えるようなパッケージを使うテストでは、必ず $ flutter test --platform chrome を使いましょう。$ flutter test --platform chrome test/x_test.dart$ flutter test --platform chrome test/foo_directory のように、特定のファイルやフォルダだけを対象にすることもできます。以下のように、フォルダによって--platform chromeをつけるテストとつけないテストを分けて両方実行するようにすると良いと思います。

$ flutter test --platform chrome test/web
$ flutter test --coverage test/not_web

おまけ:自分が使っているGitHub Actions 用 workflow

name: Flutter CI

on:
  push:
    branches: [ master ]
  pull_request:
    branches: [ master ]

jobs:
  my-job:
    name: Flutter analyze and test
    runs-on: macos-10.15
    steps:
    - uses: actions/checkout@v1
    - uses: actions/setup-java@v1
      with:
        java-version: '12.x'
    - uses: subosito/flutter-action@v1
      with:
        flutter-version: '1.20.0-2.0.pre'
        channel: 'dev'

    - name: Flutter analyze and test
      run: |
        flutter update-packages
        flutter pub get
        flutter analyze
        flutter test --platform chrome test/web
        flutter test --coverage test/not_web 

My Twitter

https://twitter.com/tetsufe_twitter.com

自分にとってのソフトウェアエンジニアリング

自分にとってのソフトウェアエンジニアリングとは、楽しいものであり、必要なスキルであり、希望であると思う

楽しいもの

  • ソフトウェアを作る技術について学ぶことは楽しい。(時に理解が難しく、苦しく回避したいと思うときもあるが)
  • ソフトウェアを作ることが楽しい。
  • ソフトウェアを作る仲間との会話や、良いチームを作ろうとする取り組みが楽しい。

必要なスキル

  • ソフトウェアを作る技術は、お金を稼ぐ手段であり、生きるために必要
  • 堂々と表明可能なスキルであり、承認欲求を満たし心理的な幸福をもたらす

希望

  • スキルの向上によって、自分がこれまでできなかったことができるようになる
  • 社会にこれまでなかったものを自分だけが作り出せるかもしれない
  • 上記に十分な現実味を感じさせてくれる

Flutterアプリの主流な状態管理パターンと導入事例まとめ(2020年版)

はじめに

Flutterアプリを作る際、状態管理の方法/設計/アーキテクチャは必ず気になるポイントだと思います。この記事では2020年6月現在で主流とされているパターンと実際の導入事例を説明します。

この記事では、詳しい話は省略して概要のみを説明します。代わりにオススメの解説記事のリンクを載せてありますので、詳しい話はそちらから学ぶことができるようになっております。

状態管理パターンとは?

状態管理パターンとはなんなのか、という話ですが、単純にいうと以下の2つのことをうまくやるためのパターンです。

  • アプリの状態を持つ変数を1つまたは2つ以上の箇所(クラス)で使えるようにする
  • 変数の変更に応じて、UIを更新(リビルド)する

状態管理とは、基本的にはこの2つのことだと思えば良いと思います。この2つをうまくやるスキルを身につけることで、どんなアプリに対しても開発効率を大きく改善できると思っています。

Flutterの主流な状態管理パターン

この記事で紹介する状態管理パターンは以下です。

  • StatefulWidget
  • InheritedWidget / InheritedModel
  • ChangeNotifier/ValueNotifier + Provider
  • BLoC + Provider
  • state_notifier + freezed + Provider
  • ? + Riverpod
  • Redux
  • mobX

どれを選ぶべき?

結論: ChangeNotifier + Provider が一番無難

どの状態管理手法を選ぶべきか、迷いますよね・・。そこで、最近指針となるようなツイートを発見したのでご紹介します。

繰り返しますが、こちらで説明されている通り、 ChangeNotifier と Providerの組み合わせが一番無難なパターンと言えそうです。 ただし、今回この記事で紹介するパターンは、組み合わせが可能なものもありますし、混在が悪というわけではありませんので、ご注意ください。

ChangeNotifer + Providerに不安な点があるとすれば、package:providerが2019年5月のGoogle I/O以降にという比較的新しいパッケージであるためか、企業がリリースしているアプリでChangeNotifierをメインで使っている例をネット上で見つけることはできませんでした(パターン3にて後述)。しかし、これも後にご紹介しますが、時期的な問題で採用したBLoCをChangeNotifierに置き換えようという企業も実際にあるようですので、それほど大きな不安を抱く必要はないと思います。

2020/07/17 追記: 同じ方による、新しいライブラリRiverpod(まだ安定版ではないので注意)も含めた以下の投稿がありました。

2020/07/17 追記2: Flutter公式からもpackage:providerが推奨となりました。

https://flutter.dev/docs/development/data-and-backend/state-mgmt/options#provider

2020/8/29 追記3: こちらにもChangeNotifier + Riverpod構成のテンプレートがあります。

medium.com

では、前置きはこのあたりにして、各パターンについて説明していきます。

パターン1. StatefulWidget

StatefulWidgetは、基本的にはある状態変数fooを1つのWidgetだけが変更し、その変更を子Widgetに伝えたい場合に使います。

例として、Adding interactivity to your Flutter app - Flutter よりコードと画像を引用します。

f:id:swiftfe:20200624113849p:plain

class TapboxA extends StatefulWidget {
  TapboxA({Key key}) : super(key: key);

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

class _TapboxAState extends State<TapboxA> {
  bool _active = false;

  void _handleTap() {
    setState(() {
      _active = !_active;
    });
  }

  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: _handleTap,
      child: Container(
        child: Center(
          child: Text(
            _active ? 'Active' : 'Inactive',
            style: TextStyle(fontSize: 32.0, color: Colors.white),
          ),
        ),
        width: 200.0,
        height: 200.0,
        decoration: BoxDecoration(
          color: _active ? Colors.lightGreen[700] : Colors.grey[600],
        ),
      ),
    );
  }
}

この例の場合、状態変数 _active を子WidgetであるTextBoxDecorationで使い、_activeが変化するタイミングでsetState()を実行し、リビルドして状態を反映させます。

逆に、ある状態変数を2つ以上のWIdgetが変更したり、子Widgetで状態変数が変更されて親Widgetでその状態変数を利用するような場合は、package:provider + ChangeNotifier、あるいはpackage:provider + BLoCなどを使った方が管理しやすいです。

パターン2. InheritedWidget / InheritedModel

InheritedWidgetはわかりにくくコード量も無駄に多くなるということで、package:providerが登場しました。現状、package:providerでできるような用途の場合、package:providerを使いましょう。 その方がコードは劇的に短くかけてわかりやすくなります。そのため、 どうしてもpackage:providerでできないようなことをしたいときのみInheritedWidgetを使うべき だと思います。

そのため、ここでの説明も最低限にします。

InheritedWidgetは、状態変数をWidget間で共有する際に使うWidgetです。InheritedWidgetは状態変数と子Widgetを持ちます。

https://aimana-it.com/wp-content/uploads/2019/06/%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88-2019-06-09-12.46.00.pnghttps://aimana-it.com/wp-content/uploads/2019/06/%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88-2019-06-09-12.46.00.png:image=https://aimana-it.com/wp-content/uploads/2019/06/%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88-2019-06-09-12.46.00.pngより引用)

InheritedWidgetを使うと、以下のことができます。

  • 数の子Widgetから状態変数を取得、変更できる
  • その変更を検知して子Widget部分的に(StatefulWidgetではできなかった) リビルドして変更を反映させることができる
  • setState()のバケツリレーをせずに親Widgetをリビルドできる

もっと詳しく

以下の記事が詳しいです。 qiita.com

medium.com

パターン3. ChangeNotifier/ValueNotifier + Provider

これもInheritedWidgetを使ったパターンと同じく、状態変数を複数Widget間で共有する際に使うパターン です。また、冒頭で述べた通り、Flutter開発をする大多数の方にお勧めできるパターンです。

pub.dev

providerパッケージに含まれるProvider.of()やConsumerなどを使うことで、Provider系モジュールにより注入した値の変更を検知し、Widgetをリビルドすることができます。このリビルドの際には状態変数に関わる部分だけを対象にすることができ、パフォーマンス面でも最適になりやすいです。

現在はFlutter公式ガイドでもリストの一番上で紹介されており、シンプルな場合にはこれを使うのが良いとされているのだと思います。とりあえず迷ったらこれ、と考えて良いと思います。

ChangeNotifierの作成

class Counter with ChangeNotifier {
  int _count = 0;
  int get count => _count;

  void increment() {
    _count++;
    notifyListeners();
  }
}

Provider(ChangeNotifierProvider)のchildにWidgetを設定

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

void main() {
  runApp(
    MultiProvider(
      providers: [
        ChangeNotifierProvider(create: (_) => Counter()),
      ],
      child: MyHomePage(),
    ),
  );
}

Widget

class MyHomePage extends StatelessWidget {
  const MyHomePage({Key key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: const Text('Example'),
        ),
        body: Center(
          child: Column(
            mainAxisSize: MainAxisSize.min,
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              const Text('You have pushed the button this many times:'),
              const Count(),
            ],
          ),
        ),
        floatingActionButton: FloatingActionButton(
          onPressed: () => context.read<Counter>().increment(),
          tooltip: 'Increment',
          child: const Icon(Icons.add),
        ),
      ),
    );
  }
}

Widget

class Count extends StatelessWidget {
  const Count({Key key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Text(
        '${context.watch<Counter>().count}',
        style: Theme.of(context).textTheme.headline4);
  }
}

導入事例

日本企業での導入事例はネット上では見つけられませんでしたが、既にリリース済みのアプリを後述のBLoCパターンからprovider+ChangeNotifierの実装に置き換えようとしているという記事がありました。

https://qiita.com/hukusuke1007/items/318875bb11c23572d46f

また、個人開発アプリでは導入事例がありました。この記事では局所的にRx(≒Streamの拡張ライブラリ)を使っているとも書かれています。 https://qiita.com/takashi-i/items/d364b4547db14d5c31de

以下の動画の12:10付近から、2019年5月当時のFlutterにおける状態管理の歴史について少し説明があります。

ここでは、それまで主流だったScopedModelとBLoCについて触れられており(BLoCは現在も主流の一つですが)、新しい方法として、providerパッケージが紹介されています。

youtu.be

もっと詳しく

providerは種類が多いのですが、この記事が網羅的でかつ情報も随時更新されていて新しいです。 qiita.com

Flutter公式サンプルとしてもproviderが採用されています。 flutter.dev

パターン4. BLoC + Provider

BLoCパターンは、簡単に言うとStream/Observableを入出力役として状態管理やロジックの実行を行うパターンです。ChangeNotifierの場合は普通にメンバを直接取得/変更していましたが、それをStream経由に限定するのが大きな特徴 です。

参考図(リンク元より引用) BLoCの参考図

BLoCのメリット、デメリット

Streamを使った状態管理がしたい場合、現在でもBLoCパターンを使うのが一般的でしょう。StreamBuilderとの組み合わせにより、局所的なリビルドが簡単に行えます。インクリメンタルサーチのようなRxDartを使った方が書きやすい場合や、Firestoreのsnapshots系のメソッドなど、Stream系の値を返すライブラリを使う場合に相性が良いです。

一方で、BLoCの実装に必要なStream/RxDartの学習コストが高いことや、ChangeNotiferなどに比べてボイラープレートコードが多くなることがデメリットと言われています。

コード例

Stream/RxDart初心者のためのBLoC入門(Flutter) - Qiita より引用

counter_bloc.dart

import 'dart:async';

class CounterBloc {
  final _actionController = StreamController<void>();
  Sink<void> get increment => _actionController.sink;

  final _countController = StreamController<int>();
  Stream<int> get count => _countController.stream;

  int _count = 0;

  CounterBloc() {
    _actionController.stream.listen((_) {
      _count++;
      _countController.sink.add(_count);
    });
  }

  void dispose() {
    _actionController.close();
    _countController.close();
  }
}

main.dart

import 'package:counter_bloc/counter_bloc.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
        title: 'Flutter Demo',
        theme: ThemeData(
          primarySwatch: Colors.blue,
        ),
        home: Provider<CounterBloc>(
          builder: (context) => CounterBloc(),
          dispose: (context, bloc) => bloc.dispose(),
          child: MyHomePage(title: 'Flutter Demo Home Page'),
        ));
  }
}



class MyHomePage extends StatelessWidget {
  MyHomePage({this.title});

  final String title;

  @override
  Widget build(BuildContext context) {
    final counterBloc = Provider.of<CounterBloc>(context);
    return Scaffold(
      appBar: AppBar(
        title: Text(title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              'You have pushed the button this many times:',
            ),
            StreamBuilder(
              initialData: 0,
              stream: counterBloc.count,
              builder: (context, snapshot) {
                return Text(
                  '${snapshot.data}',
                  style: Theme.of(context).textTheme.display1,
                );
              },
            )
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          counterBloc.increment.add(null);
        },
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }
}

導入事例

https://qiita.com/navitime_tech/items/21a6111b36b98cac3100

https://note.com/yamarkz/n/nd9716541d8ad

https://speakerdeck.com/yamarkz/division-plan-and-practical-example-in-bloc-pattern

flutter_blocBLoC

package:flutter_blocは、event、stateなどの制約も同梱しており、blocの本来の制約よりもきつい制約が設定されたライブラリです。

pub.dev

flutter_blocBLoCパターンの実装に必須なパッケージではありません。 それどころか、flutter_blocの制約が強いことを嫌うエンジニアをネット上で見かけることもあります。

しかし一方でより強い型付け/制約付けによりチーム開発でのコードの可読性や統一性を高める効果があるのだと思います。

BLoCの課題

Provider のススメ | Unselfish Meme

(一時、「まだBLoCで消耗してるの?」というタイトルでした。いつの間にか変わっていたのですね。[B! flutter] まだ BLoC で消耗してるの? | Unselfish Meme

BLoCはスタンダードなパターンとして考えられていましたが、BLoCに含まれるStream/Rxという概念に馴染みがないエンジニアにとっては敷居が少し高く、シンプルな状態管理の場合にはBLoCパターンはオーバーなのではないかという意見も出始め、ちょうどそのころ package:provider が登場したことで、次で説明する「Provider + ChangeNotifier/ValueNotifier」パターンに流れる人も多く現れました。

(2019年のGoogle I/Oでも同様の内容が語られています)

Stream=BLoCではない

状態変数の管理にStreamを使うからといって、BLoCパターンを無理に使う必要はありません。Streamを使った状態管理パターンの一つとしてBLoCパターンがあるというだけです。

おまけ:BLoCの歴史

BLoCは、Google I/O 2018 で紹介されました。この際には、ScopedModelのminimal rebuildができないという欠点を補うという点でScopedModelに優っているとされていました。また、 このタイミングではInheritedWidgetを使った自前実装のCartProviderというWidgetを使って複数WidgetでのBLoCの共有を実現していました (動画では 23:40 付近で言及)。

youtu.be 23:11 付近

cubit

flutter_bloc | Flutter Package の軽量版パッケージとして、cubitというパッケージも公開され、海外のエンジニアに好評のようです。ただし、おそらくflutter_blocを使っていた人が軽量板を求めて移行している場合が多いと考えられますが、そうでない方でも使いやすいものになっているようです。

pub.dev

注:現在は packages:flutter_bloc に含まれるようになりました。

実際のコードは以下のようになっており、とてもシンプルです。

https://github.com/felangel/cubit より引用。

class CounterCubit extends Cubit<int> {
  CounterCubit() : super(0);

  void increment() => emit(state + 1);
  void decrement() => emit(state - 1);
}

利用側はこのようになります。以下のクラスやメソッドを用いてCubitを使います。

  • CubitProvider
  • context.cubit()
  • CubitBuilder<CounterCubit, int>

https://pub.dev/documentation/flutter_cubit/latest/ より引用

class CubitCounter extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: CubitProvider(
        create: (_) => CounterCubit(),
        child: CounterPage(),
      ),
    );
  }
}

class CounterPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Cubit Counter')),
      body: CubitBuilder<CounterCubit, int>(
        builder: (_, count) {
          return Center(
            child: Text('$count'),
          );
        },
      ),
      floatingActionButton: Column(
        crossAxisAlignment: CrossAxisAlignment.end,
        mainAxisAlignment: MainAxisAlignment.end,
        children: <Widget>[
          Padding(
            padding: const EdgeInsets.symmetric(vertical: 4.0),
            child: FloatingActionButton(
              child: const Icon(Icons.add),
              onPressed: context.cubit<CounterCubit>().increment,
            ),
          ),
・・・

CubitはStreamを継承しており、Streamのように使えつつ、emit()で簡単に状態更新ができるようになっています。

Cubit class - bloc library - Dart API

cubitは、FlutterのValueNotifierや後に紹介するpackage:state_notifierのStream版ともいえるかと思いますので、ValueNotifierやStateNotifier、ChangeNotifierを使っていて同じようにStreamを使いたいという場合は、選択肢に入ってくるかと思います。

(7/29日追記)一方で、package:riverpodの作者のremiさんも以下のような発言をしており、StateNotifierでもstreamを扱いやすくなるようです。こちらも注目ですね。

もっと詳しく

より詳しく知りたい方は、こちらの記事がオススメです。

note.com

パターン5. state_notifier + freezed + Provider

state_notifierは、ValueNotifier/ChangeNotifierに相当するパッケージです。ほとんどValueNotifierと同じです。

pub.dev

state_notifierを使うと、状態変数をimmutableに扱うことが強制されます。 StateNotiferは、メンバ T stateを持ち、このstateを変更する際には、新しい値で置き換えることになります(state = state.copyWith(count: count+1))。 対して、ChangeNotifierの場合では、すでにある変数の一部(例えば、state.count +=1のような感じ)を変更するという形を取っています。

class Counter extends StateNotifier<int> {
  Counter(): super(0)

  void increment() {
    state++;
  }
}

state_notifierを使うと、現在の状態と新しい状態が変更後も両方存在することになる ため、履歴の管理やがしやすいです。そのため、「元に戻す」といった操作の実装が簡単になります。

一方、ChangeNotifierを使う場合、状態変数がクラスオブジェクトだった場合などに、参照をコピーして保存した履歴は最終的に全て同じものになってしまいます。state_notifierの場合、このようなことは起こりません。

また、 state_notifierを使うと、以下の例のようにStateとNotifierを自然と別々に取得できるようになるので、ChangeNotifierよりも明確に取得役(State)と更新役(Notifier)が分かれているのもポイントです。

      Text(
              context.select((MyState value) => value.count).toString(), // State
              style: Theme.of(context).textTheme.headline4,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: context.watch<MyStateNotifier>().increment, // Notifier
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ),

state_notifierでは、状態変数を一つの値として持ちます。ChangeNotifierでは状態変数を複数もつことができますが、state_notifierではそれはできません。その不便さを解消するため、freezedライブラリをセットで使うことが多いです。

また、LocatorMixinによるDI(以下のread()メソッド)が可能となっています。これは、state_notfierクラス内でproviderから上位widgetで設定されたインスタンスを取得することができるというものです。

class Counter extends StateNotifier<int> with LocatorMixin {
  Counter(): super(0)

  void increment() {
    state++;
    read<LocalStorage>().writeInt('count', state);
  }
}

また、notifieyListeners()を呼ぶ必要がなくなることや、ValueNotifierよりもパフォーマンス上の改善が行われています。(これはValueNotifierも同じですが)

flutter_state_notifier | Flutter Package より引用

import 'package:example/my_state_notifier.dart';
import 'package:flutter/material.dart';
import 'package:flutter_state_notifier/flutter_state_notifier.dart';
import 'package:provider/provider.dart';

// This is the default counter example reimplemented using StateNotifier + provider
// It will print in the console the counter whenever it changes
// The state change is also animated.

// The "print to console" feature is abstracted through a "Logger" class like
// we would do in production.

// This showcase how our custom MyStateNotifier does not depend on Flutter,
// but is still able to read providers and be used as usual in a Flutter app.

void main() {
  runApp(
    MultiProvider(
      providers: [
        Provider<Logger>(create: (_) => ConsoleLogger()),
        StateNotifierProvider<MyStateNotifier, MyState>(
          create: (_) => MyStateNotifier(),
          // Override MyState to make it animated
          builder: (context, child) {
            return TweenAnimationBuilder<MyState>(
              duration: const Duration(milliseconds: 500),
              tween: MyStateTween(end: context.watch<MyState>()),
              builder: (context, state, _) {
                return Provider.value(value: state, child: child);
              },
            );
          },
        ),
      ],
      child: MyApp(),
    ),
  );
}

/// A [MyStateTween].
///
/// This will apply an [IntTween] on [MyState.count].
class MyStateTween extends Tween<MyState> {
  MyStateTween({MyState begin, MyState end}) : super(begin: begin, end: end);

  @override
  MyState lerp(double t) {
    final countTween = IntTween(begin: begin.count, end: end.count);
    // Tween the count
    return MyState(
      countTween.lerp(t),
    );
  }
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      title: 'Flutter Demo',
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatelessWidget {
  const MyHomePage({Key key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Counter example'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text(
              'You have pushed the button this many times:',
            ),
            Text(
              context.select((MyState value) => value.count).toString(),
              style: Theme.of(context).textTheme.headline4,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: context.watch<MyStateNotifier>().increment,
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ),
    );
  }
}

もっと詳しく

itome.team

2020/8/29 追記4:

medium.com

新パターン. Riverpod + ?

また、package:providerの作者のRemiさんが、Riverpodという実験的プロジェクト(ライブラリ)を進行中(2020/6/24現在)のようです。

まだstable(安定版)ではないため、破壊的変更に注意してください。stableリリースまでは、package:providerを使う方が無難です。

github.com

package:providerとの比較

基本的にはpackage:providerと同じようなライブラリで、package:providerで使えていたProviderやChangeNotifierProvider、Consumerなどは今まで通り使えます。

それに加えたメリットとしては、以下が挙げられます。

  • package:riverpodはFlutter非依存
  • 実行時ではなくコンパイル時にエラーを出してくれる
  • Computed、Family、AutoDisposeProviderなどの新機能
  • hooks的な使い方も可能

まだ試行段階ということなので一般的に使われるのは先になりそうですが、個人的には従来のProvider + オプションでhooksにも対応している(package:hooks_riverpod)ことから、将来的にはproviderよりもRiverpodの方がより多くのユーザを取り込めそうかなと思っています。

ただし、Flutter公式はhooksを肯定的には捉えておらず、まずはpackage:flutter_riverpodの方を使う方が無難かと思います。

2020/8/29 追記3: こちらの記事でも、Providerとの比較が載っているのでぜひ参考にしてください。

medium.com

もっと詳しく

以下の公式ドキュメントがわかりやすいので、気になった方はぜひ読んでみてください。

riverpod.dev

アーキテクチャパターン

ここまでで挙げたパターンは、基本的にViewModelを活用したMVVMパターンの一部として使われることが多いかと思います。具体的には、Value/ChangeNotifier、BLoC、StateNotifier(とState)がViewModelとして用いられます(ViewはWidget)。

以下ではMVVM以外のアーキテクチャパターンを説明します。

Redux

Reduxは、「Single source of truth」、「State is read only」、「Changes are made with pure function」の3つの原則によってよりわかりやすく安全に状態変数を扱うためのアーキテクチャ です。以下の図のような要素・フローによってこれらの原則が実現されます(正確には、原則を守りやすくなるということだと思います)。また、Fluxに見られる「単一方向の状態変更フロー」も満たすことができます。

引用: Redux. From twitter hype to production

Reduxの利点(ChangeNotifierやBLoCと比較)

  • 厳し目のルールのため、意図しない状態変数の変更が起こりにくい
  • 書き方が統一されやすく、複数人開発でもコードがわかりやすい
  • 「Single source of truth」により、状態変数がいろんな箇所に散らばるということがない

Reduxの欠点(ChangeNotifierやBLoCと比較)

  • 学習コストが高い
  • boiler plate(定型文)の量が多くなってしまう

導入事例

https://note.com/kitoko552/n/nd25e46daf2d4

もっと詳しく

Reduxのより詳しい解説は以下の記事をお読みください。

itome.team

FlutterにおけるReduxは、以下のpackage:flutter_reduxを使うのが定番のようです。

pub.dev

List of state management approaches - Flutter

サンプル

以下にflutter_reduxに載っているサンプルコードを引用します。

import 'package:flutter/material.dart';
import 'package:flutter_redux/flutter_redux.dart';
import 'package:redux/redux.dart';

// One simple action: Increment
enum Actions { Increment }

// The reducer, which takes the previous count and increments it in response
// to an Increment action.
int counterReducer(int state, dynamic action) {
  if (action == Actions.Increment) {
    return state + 1;
  }

  return state;
}

void main() {
  // Create your store as a final variable in a base Widget. This works better
  // with Hot Reload than creating it directly in the `build` function.
  final store = new Store<int>(counterReducer, initialState: 0);

  runApp(new FlutterReduxApp(
    title: 'Flutter Redux Demo',
    store: store,
  ));
}

class FlutterReduxApp extends StatelessWidget {
  final Store<int> store;
  final String title;

  FlutterReduxApp({Key key, this.store, this.title}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    // The StoreProvider should wrap your MaterialApp or WidgetsApp. This will
    // ensure all routes have access to the store.
    return new StoreProvider<int>(
      // Pass the store to the StoreProvider. Any ancestor `StoreConnector`
      // Widgets will find and use this value as the `Store`.
      store: store,
      child: new MaterialApp(
        theme: new ThemeData.dark(),
        title: title,
        home: new Scaffold(
          appBar: new AppBar(
            title: new Text(title),
          ),
          body: new Center(
            child: new Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                new Text(
                  'You have pushed the button this many times:',
                ),
                // Connect the Store to a Text Widget that renders the current
                // count.
                //
                // We'll wrap the Text Widget in a `StoreConnector` Widget. The
                // `StoreConnector` will find the `Store` from the nearest
                // `StoreProvider` ancestor, convert it into a String of the
                // latest count, and pass that String  to the `builder` function
                // as the `count`.
                //
                // Every time the button is tapped, an action is dispatched and
                // run through the reducer. After the reducer updates the state,
                // the Widget will be automatically rebuilt with the latest
                // count. No need to manually manage subscriptions or Streams!
                new StoreConnector<int, String>(
                  converter: (store) => store.state.toString(),
                  builder: (context, count) {
                    return new Text(
                      count,
                      style: Theme.of(context).textTheme.display1,
                    );
                  },
                )
              ],
            ),
          ),
          // Connect the Store to a FloatingActionButton. In this case, we'll
          // use the Store to build a callback that with dispatch an Increment
          // Action.
          //
          // Then, we'll pass this callback to the button's `onPressed` handler.
          floatingActionButton: new StoreConnector<int, VoidCallback>(
            converter: (store) {
              // Return a `VoidCallback`, which is a fancy name for a function
              // with no parameters. It only dispatches an Increment action.
              return () => store.dispatch(Actions.Increment);
            },
            builder: (context, callback) {
              return new FloatingActionButton(
                // Attach the `callback` to the `onPressed` attribute
                onPressed: callback,
                tooltip: 'Increment',
                child: new Icon(Icons.add),
              );
            },
          ),
        ),
      ),
    );
  }
}

ちなみに、パッケージの最終更新日が2019/12 となっており、versionも0.6.0となっていることからあまり開発が活発ではないかのようにも思いましたが、本家のjs版Reduxも最終更新は2019/12だったことから、特にそういうわけではないようです。

Releases · reduxjs/redux · GitHub

mobX

Flutter公式の選択肢として、mobXというパターンも紹介されていますが、個人的に主流には感じられないため、今回は割愛します。以下の記事でわかりやすい説明がされておりますので、興味がある方は読んでみてください。Reduxに近いアーキテクチャのようです。

itome.team

過去に使われたパターン

ScopedModel

ScopedModelは、InheritedWIdget + ChangeNotifierのようなパッケージです。

過去によく使われていたパッケージですが、package:providerの登場後はおそらくほぼ使われなくなりました。

package:provider は ScopedModelでしたかったことである「InheritedWidgetを使いやすくすること」を達成しつつ、より汎用性が高い(ScopedModelはInheritedWIdget + ChangeNotifierのような使い方しかできない)ことから、ScopedModelを使う理由がなくなったのだと思われます。

また、過去にはprovideパッケージというものも存在していましたが、providerパッケージの方が優れているということでproviderパッケージが本流となったようです。 https://ntaoo.hatenablog.com/entry/2019/05/12/141018

https://youtu.be/d_m5csmrf7I 19:34-

導入事例

developer.diverse-inc.com

実装比較用

こちらのGitHubリポジトリにいろんなパターンのサンプルプロジェクトがあるので、同じような構成のアプリでパターンを比較したいという方におすすめです。

github.com

「勇気」と恋愛ソング

最近、Buono! の「初恋サイダー」という曲がマイブームです。

www.youtube.com

女の子が仲の良い男の子に大胆にもキスで気持ちを伝えようとする・・そんないかにもな青春ソングです。

自分は実は普段から(恋愛全然してないのに)恋愛ソングをよく聞くのですが、なにがいいんだろうと思った時に、「勇気」をもらえるからじゃないかなとふと思いました。

恋愛ソングは、「勇気を出して告白する」というような内容であることが多いはずです。

日頃別に告白する予定がなかったとしても、「何かをするために勇気が必要」という場面は時々あるかと思います。そんなとき、勇気をくれるから自分は恋愛ソングが好きなんじゃないかなあ、と。

そんなわけで、勇気がほしいときには「勇気を出して告白する」恋愛ソングを聞いてみるといいのでは?という話でした。

余談

また、「勇気」については書籍「エクストリームプログラミング」で最近見かけたこともあり気になっていて、これからこのブログでも深堀りしていきたいと思っています。

www.amazon.co.jp

そして、Bouno! はYoutubeでMVを配信してくれているので、興味のある人はぜひ聞いてみてください(自分はにわかですが)。

www.youtube.com

Flutterを一年触って、エンジニアとして良かったこと

はじめに

2020年5月現在、Flutterを2019年の3月から初めてから1年と少し経ったので、Flutterを触ってきてエンジニアとしてよかったことについてまとめたいと思います。

Flutterが持つ特徴として、以下があると思っています。

  • 比較的新しいフレームワークである(React Nativeなどと比較)
  • 日本語の情報が少ない

これらは、ちょうど良い制約、またはチャンスとして捉えることができる と思っています。どういった良い制約、チャンスがあったのか、説明していきたいと思います。

GitHubで他の人が書いたコードを読む習慣がついた

特に mono0926 さんntaoo さんkaboc さんrrousselGit さん のコードを読みました。

正直理解が及ばない部分も多かったのですが、 そもそもあまり今までGitHubでコードを参考にすることがなかったので、良い書き方について多くの点を学べました。サンプルコード全体を読むことで、ディレクトリ構成やクラス、Widgetの切り分け方も学ぶことができました。

Flutterの場合、アーキテクチャ関連のトレンドの変化が速い(ScopedModel -> Provider、BLoC -> ChangeNotifierなど)印象なのですが、 解説記事だけでは(英語の場合が多いということもあり)理解できない部分も多かったため、サンプルコードを読む機会が増えました。これらの努力は時間がかかるという意味で制約ともとれますが、 必要に駆られて今まであまりやれなかったことがやれるようになったという意味では成長につながるよい機会となりました。

また、Flutterは時々バグがあることもあるので、Flutter公式のIssueをみることも多く、英語で書かれたIssueを読む機会も大幅に増えました。

APIリファレンスを読むのに抵抗がなくなった

Flutterを触り始めてから、Dart Docs形式のAPIリファレンスを読むことが増えました。例えば、以下のようなものです。

provider - Dart API docs

この手のリファレンスはわかりにくい印象があったので、代わりにわかりやすい日本語の解説があればそちらを読んで、とりあえず使えればOK、というように今までは考えていたのですが、最新のライブラリ(最近ではfreezed, state_notifier)では日本語の解説が少なく、リファレンスを読むことも多々ありました。

しかし慣れてくるとDart Docs形式のAPIリファレンスは割と読みやすく、こちらを読んだ方がわかりやすい場合もあり、かつ一時情報なので安心感もあるという風に思える用になりました。 苦手意識を克服できました。

言語・フレームワーク(Flutter/Dart)の最新の動向について調べる習慣がついた

Flutterは半年に一回ほどのペースでstable版のアップデートがあります。その際にmediumで公式からアナウンスがされるのですが、昨年末に記事が出た時からはこのアナウンス記事をそのまま読むようになりました。

Flutterのアップデートの際には対応するDartのバージョンも上がるため、Dartについても同様のアナウンス記事が出されるのでそれも読んでいます。

今まではこういった言語やフレームワークについての記事は日本語の記事が出てくるまで読んでいなかったのですが、最近では公式の記事をそのまま読むようにしています。

これも今までは抵抗感があり読んでいなかったのですが、 実際に読んでみるとすぐ使える便利な機能も多かった ので、読むのが楽しみになりました。

これは FlutterがUI系のフレームワークでありすぐに試せるものが多い から、ということもあると思います。例えばiOSのアップデートの場合、自分が使わない機能も多く含まれていることが多い印象です(機械学習関連やSiriなど)。

英語のドキュメントを読む抵抗がなくなった

ここまでで同じようなことを書いていますが、調べ物をする際に 英語を読むことを強制される場面も多いので(英語のリソースは良質で読みやすいものが多い のですが)、次第に英語を読むことにも抵抗がなくなりました。

ただし、抵抗がなくなったというだけで別に英語力が上がるわけではないため、読むスピードはそんなに変わっていない気がします(苦笑

それでも読むまでの抵抗がなくなったのは、特に個人開発の際には大きいと感じています。

iOS/Android両対応のアプリをリリースできた

チーム開発ですが、「スタマチ」というアプリをリリースすることができました。詳細は以下の記事にあります。iOS/Android両方のアプリをリリースすることで、今まで使ったことのなかったGoogle Playへのリリースフローも経験することができましたし、より多くのユーザが利用できるアプリとしてリリースできたのが良かったです。

qiita.com

技術記事をそこそこ見ていただけた

2019年は、Qiitaやこのブログにそこそこまとまった分量の技術記事を複数投稿し、40-300いいね(LGTM)ほどもらえています。

qiita.com

qiita.com

記事自体も頑張って書いたのですが、Flutterが比較的新しいフレームワークであり日本語の情報が少ないため、多くの人にみていただけた のではないかと思います。

今後は?

今後は、Flutter関連の有名な方の真似をしていきたいと思っています。

今後もFlutterを通してエンジニアとして成長していきたいです。

twitter.com

ターミナルのプロンプトにflutterのchannelを表示する

注意: 実用性は低いです

サンプル

f:id:swiftfe:20200503201948p:plain f:id:swiftfe:20200503202305p:plain

モチベーション

  • flutterのweb supportを使うにはbeta channelを使う必要があるが、他のweb supportの必要のないプロジェクトではmasterを使いたい
  • masterとbetaをすぐ判別がつくようにしたい

Building a web application with Flutter - Flutter より

As of 1.12, Flutter has early support for running web applications, but you need to be running the beta channel of Flutter at present. If you experience a problem that hasn’t yet been reported, please file an issue and make sure that “web” appears in the title.

zshrcのカスタマイズ

環境

git-promptを使っている場合

~/.zshrc

# flutterのパスを追加
export PATH=/Users/xxx/development/flutter/bin:$PATH

# flutter channelの結果から、現在のchannel名を取得 (* stable なら、stableを取得)
flutter_channel=$(flutter channel | tr '\n' ':' | sed -E "s/.+\* ([^:]+):.*/\1/")

# gitのプロンプト設定
export GIT_PS1_SHOWCOLORHINTS=1
export GIT_PS1_SHOWDIRTYSTATE=1
git_prompt_sh=/usr/local/etc/bash_completion.d/git-prompt.sh
if [ -e $git_prompt_sh ]; then
 source $git_prompt_sh
 
 # $flutter_channelをプロンプトに表示
 precmd () { __git_ps1 "%F{cyan}(flutter: $flutter_channel)%f " "%F{yellow}%c%f $ " " (%s) " }
fi

git-promptを使わない場合

git-promptを使っているのでわかりにくいですが、内部ではzshのPROMPT変数?を使うことで実現しているため、git-promptを使っていない人はPROMPT(bashの場合はPS1だったはず)を使うことで同様に使うことができます。

~/.zshrc

# flutterのパスを追加
export PATH=/Users/xxx/development/flutter/bin:$PATH

# flutter channelの結果から、現在のchannel名を取得 (* stable なら、stableを取得)
flutter_channel=$(flutter channel | tr '\n' ':' | sed -E "s/.+\* ([^:]+):.*/\1/")

# プロンプト設定
PROMPT="%F{cyan}(flutter: $flutter_channel) $ "

簡単な解説

$ flutter channelの出力は以下のようになります。

Flutter channels:
  beta
  dev
  master
* stable

この * stable のように * がついているのが現在のchannelになるので、sedにより正規表現で「stable」だけを抜き出します。それだけです。

この方法の問題点

  • シェルの起動が重くなる
  • $ flutter channel が重いため、コマンド実行ごとに channel 表示を切り替える用途には待ち時間の問題上、使えない
  • $ flutter channel stable などを使った際、表示は切り替わらず、シェルを再起動する必要あり
    • しかも切り替えたあとすぐは余計な出力が出るためにsedの処理に1分ほど時間がかかる
    • しかし、Android Studioで他のプロジェクトを開いた際にはシェルが再起動されるので実用上あまり問題はない

参考

Google Domainsでドメイン購入しました

Google Domainsでドメイン購入をしてみたのでメモ。

今回買ったのはtetsufe.dev。1540円/年(税込) で、1年+継続契約。54歳になるまでの30年間使うとしても、46200円。

購入理由

何かしら個人でページを作る際に便利かなと思って登録。アプリの登録時にCompany Domainを指定するなどの用途にも使うので、念のためあると安心

Google Domainを使ったのは、今まで使っていたお名前.comはメールが大量に届いて面倒(結局ほとんど迷惑メールに入れている)なのと、別のドメインレジストラを使ってみたいという理由から。

調べたこと

f:id:swiftfe:20200503013320p:plain

f:id:swiftfe:20200503013326p:plain