Open
33

Flutter + Riverpod + FlutterHooks + Firebase でアプリ開発するスクラップ

ピン留めされたアイテム

Flutter + Riverpod + Firebase でアプリ開発する上で、記事にする前のメモや小ネタを書き溜めて行きたい✍️

Riverpod - ScopedProvider

https://pub.dev/documentation/riverpod/latest/all/ScopedProvider-class.html

例えば、 ListView.builderchild に指定するWidgetに、引数で index 等の引数を渡したいとき、代わりに ScopedProvider が使える。

メリットは、

  1. const でインスタンス化できるので、すべてのWidgetではなく、影響を受けたウィジェットのみが再構築されるようになる
  2. ProviderScope(overrides: child:) で囲むことで、値の指定がその場で必要なことが明示的になること

使用例

// Providerをグローバルに定義
final currentProductIndex = ScopedProvider<int>(null);
---
// 画面等で使うListViewでの使用例
ListView.builder(
  itemBuilder: (context, index) {
    return ProviderScope(
      overrides: [
        currentProductIndex.overrideWithValue(index),
      ],
      child: const ProductItem(),
    );
  }
)
---
// ProductItemで `watch` して使う。
// Hooksなら `HookWidget` + `useProvider(currentProductIndex)`
class ProductItem extends ConsumerWidget {
  const ProductItem({Key key}): super(key: key);
  Widget build(BuildContext context, ScopedReader watch) {
    final index = watch(currentProductIndex);
    // do something with the index
  }
}

Official Examples の一つ、 "TODO List" でも使われている。

https://github.com/rrousselGit/river_pod/blob/master/examples/todos/lib/main.dart#L126

ScopedProviderは、ProviderReference 経由では使用できないので注意。

Provider使い分け

Provider

状態を持たない値やクラスで使う。
基本的に値のoverrideはProviderScopeでのみ可能。

StateProvider

状態を持つ値で使う。
watch(stateProvider).state = newValue で状態を更新できる。

StateNotifierProvider

状態を持たせたい、かつ値を操作するビジネスロジックを持たせたい時に使う
intやboolなど単純な値でも良いし、いくつかの要素をまとめたオブジェクトでも良い。

.autoDispose

参照がなくなったら、自動的に破棄されても良い
(画面単位のProviderなど)

.family

Providerに直接引数を渡して使いたい。
値によって複数作る可能性があるProviderなどで使う。

ありがとうございます。Providerの数が多すぎて、どれを使えばいいのか迷っていましたが、上記の説明で良く分かりました。

そう言っていただけて嬉しいです☺️

背景は同じだけど全面のViewを出し分けしたい時、
三項演算子だけだと味気なくてアニメーションを追加。

AnimatedSwitcher が簡単かつカスタマイズできそうで良かった。

https://api.flutter.dev/flutter/widgets/AnimatedSwitcher-class.html
// フェードアニメーションで、ログインViewと新規登録Viewを出し分ける
child: AnimatedSwitcher(
  duration: const Duration(milliseconds: 400),
  transitionBuilder: (child, animation) => FadeTransition(
    child: child,
    opacity: animation,
  ),
  child: pageMode == PageMode.login ? LoginView() : RegisterView(),
),

Fadeさせるだけなら AnimatedCrossFade の方が簡単かと思いきや、自分の環境だと思った挙動にならなかったため、今回は仕様を見送った。

https://api.flutter.dev/flutter/widgets/AnimatedCrossFade-class.html

ListViewの中のButtonの幅はデフォルトで引き伸ばされる。
ボタンテキストに応じた幅にしたい場合は Align で囲めばOK。

Align(
  child: OutlinedButton(
    onPressed: null,
    child: const Text('ログアウト'),
  ),
),

WIP

AsyncValue 3通りの使い方

例に使うFutureProvider
PackageInfoを非同期で取得している。

final packageInfoProvider = FutureProvider((ref) => PackageInfo.fromPlatform());

.when

useProvider(packageInfoProvider).when(
  data: (data) => Text(data.version),
  loading: () => const CircularProgressIndicator(),
  error: (error, stackTrace) => Text('$error, $stackTrace'),
),

.maybeWhen

useProvider(packageInfoProvider).maybeWhen(
  data: (data) => Text(data.version),
  orElse: () => const SizedBox(),
),

.data

Widget build(BuildContext context) {
  // .data= AsyncData<PackageInfo>?, .value= PackageInfo
  final version = useProvider(packageInfoProvider).data?.value?.version;
  // 中略
    if (version != null) Text(version),
  // 後略

Bundle ID (iOS), パッケージ名 (Android)の決まり方

パッケージ名 (Android)には、 "-"(ハイフン)が使えない。
Bundle ID (iOS)には、 "_"(アンダースコア)が使えない。

flutter create 時に、 "_"(アンダースコア)を入れると…

flutter create 時に、 ""(アンダースコア)が入った org や project-name を指定すると、Androidではそのままだが、iOSでは ""(アンダースコア)が削除される。

flutter create 時に、 "-"(ハイフン)を入れると…

flutter create 時に、 "-"(ハイフン)が入った org や project-name を指定すると、iOSではそのままだが、Androidでは "-"(ハイフン)が削除される。

再度 flutter create . するとき

% fvm flutter create .
Ambiguous organization in existing files: {com.example-domain, com.example_domain}. The --org command line argument must be specified to recreate project.

2通りの organization があるから指定してと言われる。

教訓
面倒なので、これからドメイン取得するときはハイフン使わないようにしよう

シングルトンの定義

class Example {
  /// 初めてインスタンス化するときにfactoryコンストラクタから使用する、Privateなコンストラクタ
  Example._();

  /// シングルトンを実現するインスタンスを提供
  factory Example.instance() {
    return _instance ??= Example._();
  }

  /// インスタンスのキャッシュ
  static Example _instance;

2021年2月15日更新:factoryコンストラクタを使用したバージョンに変更
2021年2月20日更新:Privateコンストラクタのコメントを変更

FlutterアプリでFastlaneを使う

https://flutter.dev/docs/deployment/cd
# iOSディレクトリに移動
$ cd ios
# Fastlane を Gemでインストール
$ sudo gem install fastlane
# Fastlane 初期化
$ bundle exec fastlane init -u{Apple ID}
# App Storeのスクリーンショットをダウンロード
$ bundle exec fastlane deliver download_screenshots --use_live_version true
# App Store のメタデータをダウンロード
$ bundle exec fastlane deliver download_metadata --use_live_version true
# Androidディレクトリに移動
$ cd android
# Fastlane 初期化
$ bundle exec fastlane init
# ドキュメントを参考に、サービスアカウントを発行する
# https://docs.fastlane.tools/actions/supply/
# `android/` ディレクトリにJSONファイルを配置
# Appfileにファイル名を追記 `json_key_file("xxx.json")`
# 以下のコマンドでGoogle Play Store への接続を確認できる
$ bundle exec fastlane run validate_play_store_json_key json_key:/path/to/xxx.json
# Play Store のメタデータ(アイコン画像やスクリーンショット含む)をダウンロード
$ bundle exec fastlane supply init
# 
# オプションリスト
$ bundle exec fastlane action supply
# メタデータ、ビルド、画像、スクリーンショットでアプリを更新する
$ bundle exec fastlane supply

direnv でカレントディレクトリでのみ有効な環境変数を設定する手順

# direnvをHomebrewでインストール
$ brew install direnv
# 以下2行を `.zshrc` や `.bashrc` 等に追記
export EDITOR={エディタを指定} # vi など
eval "$(direnv hook $SHELL)"
# アプリのプロジェクトディレクトリ等で `.envrc` ファイルを作成・編集する
$ direnv edit .

.envrc を作成したディレクトリ以下でのみ、その環境変数が有効になる。

# 別途、手動で有効化したい場合
direnv allow
# 無効化
direnv deny

参考

direnvを使おう
https://qiita.com/hummer/items/c320a060cac079f654a2

Firebase Auth のユーザー情報変更検知

FirebaseAuth.instance.xxx() の違い

どれも Stream<User> を提供するが…

authStateChanges

ユーザーのサインイン状態の変更を通知

  • サインイン
  • サインアウト

idTokenChanges

ユーザーのサインイン状態の変更や、トークンの更新イベントを通知

  • サインイン
  • サインアウト
  • トークンの更新

userChanges

authStateChangesidTokenChanges のスーパーセット。
ユーザーの変更に関するすべてのイベントを提供する。

  • サインイン
  • サインアウト
  • トークンの更新
  • 認証情報がリンク・リンク解除
  • ユーザープロフィールの更新

ドキュメント

https://firebase.flutter.dev/docs/auth/usage/#authentication-state

startAtDocument(DocumentSnapshot documentSnapshot)

指定したドキュメント(含む)から開始するクエリを返す。
クエリの orderBy で提供されたすべてのフィールドを含んでいなければならない。
このメソッドを呼び出すと、cursor start がすべて置き換えられる。

startAfterDocument(DocumentSnapshot documentSnapshot)

指定したドキュメント(は含まない)から開始するクエリを返す。

endAtDocument(DocumentSnapshot documentSnapshot)

指定したドキュメント(含む)で終了するクエリを返す。
クエリの orderBy で提供されたすべてのフィールドを含んでいなければならない。
startAxxx となら併用できる。

endBeforeDocument(DocumentSnapshot documentSnapshot)

指定したドキュメント(は含まない)で終了するクエリを返す。
クエリの orderBy で提供されたすべてのフィールドを含んでいなければならない。
このメソッドを呼び出すと、cursor end がすべて置き換えられる。

startAt(List<dynamic> values)
startAfter(List<dynamic> values)
endAt(List<dynamic> values)
endBefore(List<dynamic> values)

Text

height

テキストスパンの高さ(フォントサイズの倍数)

letterSpacing

各文字の間に追加するスペースの量(論理ピクセル単位)。負の値を使用して、文字を近づけることができます。

wordSpacing

空白の各シーケンス(つまり、各単語の間)で追加するスペースの量(論理ピクセル単位)。負の値を使用して、単語を近づけることができます。

FlutterHooks

useXxx の使い所

useEffect

HookWidget内で一度きりの処理をしたい時に使う。
statefulWidgetiniteState() 内で行いたいようなことを代替できる。

useMemoized

結果をキャッシュする。
通常、 buildメソッド内に処理を書くとリビルドの度に実行されてしまうが、 useMemoized を使えばキャッシュできる。

useContext

build メソッドの名で BuildContext が使える。
Widget _buildXxx(){ /* something */ } のような、ウィジェットを生成するメソッドへBuildContextをバケツリレーする必要がなくなる。
メソッドでWidgetを返す場面がほとんどないので、使わないと思う(StatelessWidgetなりを使う)

useFuture / useStream

ネストが浅くなり流ので、FutureBuilder, StreamBuilderを使うなら、代わりにこちらを使いたい。

参考

https://medium.com/flutter-community/flutter-hooks-say-goodbye-to-statefulwidget-and-reduce-boilerplate-code-8573d4720f9a
https://qiita.com/mkosuke/items/f88419d0f4d41ed6d858

[WIP] データの取り出し方比較

    final meEntity = useProvider(meEntityProvider); // nullable

    final meAsyncValue = useProvider(meEntityStreamProvider);
    meAsyncValue.when(
      data: null, // non-null?
      loading: null,
      error: null,
    );

    final meStream =
        useProvider(meRepositoryProvider.select((value) => value.stream()));
    final meAsyncSnapshot = useStream(meStream);
    meAsyncSnapshot.data; // nullable, data != null なら hasData == true

Sound Null Safety

新規アプリ作成

$ fvm use 2.0.2 --force
$ fvm flutter create .

作成されるテンプレートはまだNull Safetyになっていない。

Dart SDKのバージョンを 2.12.0 以上指定にする。

pubspec.yaml
environment:
  sdk: ">=2.12.0 <3.0.0"

マイグレーションを実行する

dart migrate --apply-changes --no-web-preview

Null Safety でコーディングする!

参考

https://dart.dev/null-safety

SliverAppBarとSliverListを使うときのWidgets tree

Scaffold(
  body: SafeArea(
    child: CustomScrollView(
      slivers: [
        SliverAppBar(),
          flexibleSpace: FlexibleSpaceBar(),
        SliverList(
          delegate: SliverChildBuilderDelegate(
            builder,
            itemCount:
          ),
      ],
    ),
  ),
);

Firestore

set

  • ドキュメントが存在しない場合、ドキュメントを新たに作成する
  • ドキュメントがすでに存在する場合、ドキュメントごとすべて上書き

set + merge

  • ドキュメントが存在しない場合、ドキュメントを新たに作成する
  • 全体は上書きせず、フィールドの追加・更新を行う
  • ネストされたフィールドを更新するときに追加の形になる
 {
   "friends": {
     "friend-uid-1": true,
     "friend-uid-2": true,
     "friend-uid-3": true // <- 追加
   }
 }

update(既存ドキュメントのみに使用可能)

  • ドキュメントがない場合はエラー
  • フィールドの更新
  • ネストされたフィールドを更新するときにドット記法を使わないと上書きの形になる
.update(<String, Object>{
    "friends": <String, Object>{
        "friend-uid-3": true
    }
});

{
   "friends": {
     "friend-uid-3": true // <- 追加、1, 2 は削除される
   }
 }

ドット記法

.update(<String, Object>{
    "friends.friend-uid-3": true
});

 {
   "friends": {
     "friend-uid-1": true,
     "friend-uid-2": true,
     "friend-uid-3": true // <- 追加
   }
 }

Security Rules的にはどうなるんだっけ?

参考

https://stackoverflow.com/questions/46597327/difference-between-set-with-merge-true-and-update

[WIP] パラメータで渡すときの型の違いについて

void Function() = VoidCallBack

Function

void Function() Function()

Scaffold.of(context) のエラーについて

Flutter 2 で使いやすくなった新しい仕組みを使用すれば問題ない想定だが、従来の使い方への対処。

Scaffold.of(context) の context がScaffold以下で発行されたものでなければエラーが発生する。
Widget build(BuildContext context)context には Scaffold は含まれていないため。

解決策としては2通り

  1. Builderウィジェットを使ったり、StatelessWidgetとして書き出して、その context を使用する
  2. GlobalKey<ScaffoldState> を定義し、Scaffoldの key に指定。 _scaffoldKey.currentState.showSnackBar() を使う

https://stackoverflow.com/questions/51304568/scaffold-of-called-with-a-context-that-does-not-contain-a-scaffold

HookWidgetにて、画面を開いたら、さらに自動で他の画面に遷移させたい

ボタン押した時とかではなく、その画面を開いた時など(オンボーディングなどを想定)
useEffectWidgetsBinding.instance.addPostFrameCallback を使う

  Widget build(BuildContext context) {
    useEffect(() {
      WidgetsBinding.instance.addPostFrameCallback((_) async {
        if (condition) { // 何らかのフラグなどで遷移するか決められる
          await Navigator.of(context).push(/* route */);
        }
      });
      return;
    }, []);
    return Scaffold(
      ...

GridView

コンストラクの選び方

GridView.count

children に表示したいWidgetのリストを指定する。
横方向に並べる数を指定する方法。(=子の幅は柔軟に変化する)
ListViewでいう ListView() コンストラクタが近い。

GridView.extent

children に表示したいWidgetのリストを指定する。
アイテムの最大幅を指定する方法(=横方向に並ぶ子の数は画面幅によって変化する)

GridView.builder

itemBuilder で、表示するWidgetを生成して返す。
ListViewでいう ``ListView.builder()` コンストラクタが近い。

GridView.custom

SliverChildDelegate childrenDelegate を指定する。
※使ったことがないので詳しくは分からない

children などでの配列内の便利な書き方

children: [
  // 別の配列の中身を挿入したい時
  ...[
    Text('1つ目'),
    Text('2つ目'),
  ],
  // 条件次第で表示非表示を切り替えたい時
  if (isEnabled)
    Text('有効です'),
  else
    Text('無効です'),
  // 繰り返したい時
  for (final e in elements)
    Text('繰り返し:$e'),
  // または `map` で
  ...elements.map((e) => Text('繰り返し:$e')).toList(),
  // 複数の要素をまとめて条件分岐させたい時
  if (isEnabled) ...[
    Text('有効1'),
    Text('有効2'),
  ],
],

Riverpod migrate to v0.14.0

https://riverpod.dev/docs/migration/0.13.0_to_0.14.0/
# Dartが2.12.0以上である必要がある
$ dart --version
# Dartを2.12.0にアップグレード(Homebrew経由でDartを使っている場合)
$ brew upgrade dart
# 移行ツールをインストール
$ dart pub global activate riverpod_cli
# プロジェクトディレクトリ(`pubspec.yaml`を置いてあるところ)に移動して実行
# この時、pubspec.yaml の riverpod パッケージのバージョンを上げる前に実行する
$ riverpod migrate
# 変更内容とともに変更するか尋ねてくれる。
# 既存プロジェクトなら量が多いと思われるので `A = yes to all` が現実的な選択か
$ Accept change (y = yes, n = no [default], A = yes to all, q = quit)? 

いくつか、移行された箇所を抜粋

// StateNotifierProviderにState部分の型を明示
- final provider = StateNotifierProvider<Counter>...
+ final provider = StateNotifierProvider<Counter, int>...

// all でインポートしてしまっていたところを正してくれた
- import 'package:hooks_riverpod/all.dart';
+ import 'package:hooks_riverpod/hooks_riverpod.dart';

// 不要な `.state` を削除
- final user = ref.watch(authControllerProvider.state);
+ final user = ref.watch(authControllerProvider);

- final revenueState = useProvider(revenueControllerProvider.state);
+ final revenueState = useProvider(revenueControllerProvider);

// 必要になった `.notifier` を追加
- context.read(themeColorProvider).change(newTheme);
+ context.read(themeColorProvider.notifier).change(newTheme);

// 最後にpubspec.yaml に記載の riverpod のバージョンを最新に上げてくれる
- hooks_riverpod: ^0.11.0
+ hooks_riverpod: ^0.14.0

Unit Test

https://stackoverflow.com/questions/59027915/accessing-rootbundle-in-flutter-unit-test

setUp(body)

各テストが実行される前に呼び出される。
非同期可能、その場合はFutureを返す必要あり。

setUpAll(body)

すべてのテストの前に一度だけ実行される。
非同期可能、その場合はFutureを返す必要あり。
注意: この関数を使うと、本来は分離されているはずのテスト間の隠れた依存関係を誤って導入してしまうことがあります。一般的には [setUp] を使用し、コールバックが非常に遅い場合にのみ [setUpAll] を使用するようにしましょう。

旧Buttonから新Buttonへの移行メモ

新旧対応

RaisedButton -> ElevatedButton
OutlineButton -> OutlinedButton
FlatButton -> TextButton

単純なスタイル指定なら styleForm() を使用する。

ElevatedButton.styleForm()
OutlinedButton.styleForm()
TextButton.styleFrom()

ボタンの状態によってスタイルの値を変えたい場合は MaterialStateProperty を使って指定する

e.g. ElevatedButtonThemeData

  • MaterialStateProperty.all(value) : すべて状態で単一の値を返したいとき
  • resolveWith((states) => value) : states ごとに値を変えたいとき
  • MaterialStateProperty.resolveAs(value, states) : まだ使ったことない

改善Tips

空Widgetはconstに

Container() より const SizedBox()

配列内条件分岐は if-else を使って

children [
  condition // true or false
    ? Text('Sample')
    : const SizedBox(),
],

より

children [
  if (condition)
    Text('Sample'),
],
ログインするとコメントできます