Open38

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

ピン留めされたアイテム
村松龍之介村松龍之介

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

Hidden comment
村松龍之介村松龍之介

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を非同期で取得している。

.data

loading, error状態で呼び出すと null

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

when

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

whenData

when の loading, error ハンドリングを省略したバージョン

.maybeWhen

useProvider(packageInfoProvider).maybeWhen(
  data: (data) => Text(data.version),
  orElse: () => const SizedBox(),
),
村松龍之介村松龍之介

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年7月26日更新:Null Safety
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.2.0 --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'),
],
村松龍之介村松龍之介

REST APIなどのレスポンスの値を enum にパースするとき

enum のプロパティとレスポンスの値が異なる場合は @JsonValue で指定できる。
文字列に限らず、int などでも良い。

enum Board {
  ('Todo')
  todo,
  ('In Progress')
  inProgress,
  ('Done')
  done,
}

想定外の値が入ってきたときに代わりに使用する値を unknownEnumValue で指定できる。

(unknownEnumValue: Board.todo)
final Board board,
村松龍之介村松龍之介

Firebase CLI

  • nvm (Node Version Manager)

[推奨] nvmを使用してNode.jsとnpmをインストールする

nvmのインストール

https://github.com/nvm-sh/nvm#install-script

curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.38.0/install.sh | bash

nvmのアップデート

# 確認
nvm --version
# 移動
cd ~/.nvm
# 最新を取得
git pull origin master
# nvm再起動
source ~/.nvm/nvm.sh
# 確認
nvm --version

Node.js と npm のインストール

安定板をインストールする場合

nvm install --lts
nvm use --lts
nvm alias default v14.17.0

またはバージョンを指定してインストールする。

Firebase CLIのインストール

# 更新も同じコマンド
npm install -g firebase-tools
firebase --version

初期化

Firebaseコンソールで有効化しておく
Firestore, Functionsなど

firebase init

使用するFirebase CLIの機能を選択

  • Database: Firebase Realtime Databaseの設定とルールのデプロイ
  • Firestore: Firestoreのルールのデプロイとインデックスの作成
  • Functions: Cloud Functionsの設定とデプロイ
  • Hosting: Firebase Hostingの設定とデプロイ
  • Storage: Cloud Storageのセキュリティルールのデプロイ
  • Emulators: Firebase機能群のローカルエミュレータのセットアップ
  • Remote Config: Remote Config設定の取得・デプロイ・ロールバック

プロジェクトのセットアップ

❯ Use an existing project 
  Create a new project 
  Add Firebase to an existing Google Cloud Platform project 
  Don't set up a default project 
firebase use --add

機能ごとのセットアップ

機能ごとのセットアップ

Firestore

ルールファイル名: firestore.rules
インデックスファイル名: firestore.indexes.json

Functions

言語: javaScript or TypeScript
ESLintを使うか: Y/n

Storage

ルールファイル名: storage.rules

Emulators

Authentication
Functions
Firestore
Database
Hosting
Pub/Sub
Storage

Auth port: 9099
Functions port: 5001
Firestore port: 8080
PubSub port: 8085
Storage port: 9199
Emulator UIを有効化するか

? Which port do you want to use for the Emulator UI (leave empty to use any available port)?
? Would you like to download the emulators now? Yes

Remote Config template? (remoteconfig.template.json)

✔ Firebase initialization complete!

村松龍之介村松龍之介

Firebase Emulators

firebase emulators:start

firestoreで止まった。

→Javaのインストールが必要だった。
https://www.java.com/ja/

functionsで止まった。
→TypeScriptを選択したのでコンパイルしておく必要があった
→functions ディレクトリで npm run build を実行した

✔  All emulators ready! It is now safe to connect your app.
i  View Emulator UI at http://localhost:4000
村松龍之介村松龍之介

Sliver’s Widgets

  • SliverToBoxAdapter: 要素が1つのWidget
  • SliverList: ListViewなど、複数の要素を縦に並べる
    • SliverChildListDelegate: 全ての要素を事前に構築する。(確実に要素の少ないリストなど)
    • SliverChildBuilderDelegate: 表示されるまで構築されない。(要素の多いリストなど)
  • SliverGrid: GridViewなど、複数の要素をグリッド形式で並べる
  • SliverPadding: Padding
  • SliverFixedExtentList: アイテム数の決まっている要素
  • SliverFillViewport: 要素1つ1つを画面いっぱいに広げる(PageViewで使われている)
  • SliverFillRemaining: Viewport(1画面の表示範囲)の残り部分を埋める。(リストの背景色にも使える)

https://www.youtube.com/watch?v=_NZc23KQChI