Flutter + Riverpod + FlutterHooks + Firebase でアプリ開発するスクラップ
Flutter + Riverpod + Firebase でアプリ開発する上で、記事にする前のメモや小ネタを書き溜めて行きたい✍️
Provider使い分け
Provider
状態を持たない値やクラスで使う。
基本的に値のoverrideはProviderScopeでのみ可能。
StateProvider
状態を持つ値で使う。
watch(stateProvider).state = newValue
で状態を更新できる。
StateNotifierProvider
状態を持たせたい、かつ値を操作するビジネスロジックを持たせたい時に使う
intやboolなど単純な値でも良いし、いくつかの要素をまとめたオブジェクトでも良い。
.autoDispose
参照がなくなったら、自動的に破棄されても良い
(画面単位のProviderなど)
.family
Providerに直接引数を渡して使いたい。
値によって複数作る可能性があるProviderなどで使う。
背景は同じだけど全面のViewを出し分けしたい時、
三項演算子だけだと味気なくてアニメーションを追加。
AnimatedSwitcher が簡単かつカスタマイズできそうで良かった。
// フェードアニメーションで、ログイン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 の方が簡単かと思いきや、自分の環境だと思った挙動にならなかったため、今回は仕様を見送った。
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を使う
# 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
authStateChanges
と idTokenChanges
のスーパーセット。
ユーザーの変更に関するすべてのイベントを提供する。
- サインイン
- サインアウト
- トークンの更新
- 認証情報がリンク・リンク解除
- ユーザープロフィールの更新
ドキュメント
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
空白の各シーケンス(つまり、各単語の間)で追加するスペースの量(論理ピクセル単位)。負の値を使用して、単語を近づけることができます。
context.refresh
Riverpod の Pull-to-refreshなど、Futureを再施行したい時などに使うやつ
FlutterHooks
useXxx
の使い所
useEffect
HookWidget内で一度きりの処理をしたい時に使う。
statefulWidget
の initeState()
内で行いたいようなことを代替できる。
useMemoized
結果をキャッシュする。
通常、 build
メソッド内に処理を書くとリビルドの度に実行されてしまうが、 useMemoized
を使えばキャッシュできる。
useContext
build
メソッドの名で BuildContext が使える。
Widget _buildXxx(){ /* something */ }
のような、ウィジェットを生成するメソッドへBuildContextをバケツリレーする必要がなくなる。
メソッドでWidgetを返す場面がほとんどないので、使わないと思う(StatelessWidgetなりを使う)
useFuture / useStream
ネストが浅くなり流ので、FutureBuilder, StreamBuilderを使うなら、代わりにこちらを使いたい。
参考
[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 以上指定にする。
environment:
sdk: ">=2.12.0 <3.0.0"
マイグレーションを実行する
dart migrate --apply-changes --no-web-preview
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的にはどうなるんだっけ?
参考
DateTime 'ja' パターン表
yMd('ja')は月日が0埋めされない✍️
[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通り
- Builderウィジェットを使ったり、StatelessWidgetとして書き出して、その
context
を使用する -
GlobalKey<ScaffoldState>
を定義し、Scaffoldのkey
に指定。_scaffoldKey.currentState.showSnackBar()
を使う
HookWidgetにて、画面を開いたら、さらに自動で他の画面に遷移させたい
ボタン押した時とかではなく、その画面を開いた時など(オンボーディングなどを想定)
useEffect
と WidgetsBinding.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
# 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
adb とFirebase Analytics
パスを通す
$ vi ~/.zshrc
# Android Debug Bridge
export PATH="$PATH:$HOME/Library/Android/sdk/platform-tools"
使える
$ adb shell setprop debug.firebase.analytics.app {Bundle ID (Package)}
参考
Badge
アプリのアイコンにバッジを表示(ホーム・ドック)
アプリ内でバッジを表示
参考
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'),
],
チュートリアルリスト
FlutterのFirebaseについて知る
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のインストール
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のインストールが必要だった。
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画面の表示範囲)の残り部分を埋める。(リストの背景色にも使える)