Retrofitのコルーチン関数にwithContext(Dispatchers.IO)は必要ない

Androidのネットワーク通信で頻出ライブラリであるRetrofitは、Version 2.6.0からコルーチンに対応しています。

そして、ネットワーク通信を行うコルーチンの実行はメインセーフティにすることが推奨されています。

では、Retrofitのコルーチン関数を呼ぶ際、メインセーフティにするために withContext(Dispatchers.IO) によってスレッドを切り替える必要があるのしょうか?

どこでスレッドの切り替えが起きているのか?

まず軽くググってみると、例えばこちらの記事には、Networking libraries such as Retrofit and Volley manage their own threads and do not require explicit main-safety in your code when used with Kotlin coroutines. とあります。よさそう。。。他にも、スレッド管理はRetrofitが独自管理しているという記事はいくつか見受けられました。

しかし、肝心のRetrofit公式ページGitHubではこの辺りの記述は見つけられません。 仕方がないのでコードを追うと、RetrofitがラップしているOkHttpの中でスレッド管理されていることがわかりました。

そこだけ抜き出してもわからないけど、具体的にはこの部分。 https://github.com/square/okhttp/blob/4677beea96f0afb1e061f119e52e0203d4cd3738/okhttp/src/main/kotlin/okhttp3/internal/connection/RealCall.kt#L160-L165)\

  override fun enqueue(responseCallback: Callback) {
    check(executed.compareAndSet(false, true)) { "Already Executed" }

    callStart()
    client.dispatcher.enqueue(AsyncCall(responseCallback))  ## ここでOkHttpが管理しているスレッドに処理が渡される
  }

結論

Retrofitのコルーチン関数を利用する際に、敢えて withContext(Dispatchers.IO) を書く必要は必要ありません。

また、コルーチン関数が完了した後にはメインスレッドへ処理が戻されるため安心してUIに触れます。楽ちんで助かりますね。

参考にした記事

developer.android.com

medium.com

stackoverflow.com

Androidアプリエンジニアになって気づいたiOSとの違い

この記事はフラー Advent Calendar 2020 の 2 日目の記事です。

フラーには今年の10月にAndroid アプリエンジニアとして入社しました。ちょうど2ヶ月が経ち、まだまだ不慣れながらも日々楽しくKotlinを書いています。 今回は前職でやってたiOSアプリ開発とのギャップに改めて感じたことを思い出として残しておこうかと思います。

Activityのライフサイクル管理が複雑

もちろん iOSのViewController/Viewにもライフサイクルの考慮は必要なんですが、AndroidのActivity/Fragmentのそれはより難しいと感じます。 特にActivityは割とあっさり捨てられ、再生成されます。なのでそんな場合にもアプリの状態を保持できるよう気を配る必要があります。

自分が関わっていたプロジェクトでも、 プロセス起動中に設定で位置情報パーミッションの設定を変えるだけでActivityが再生成される という現象による不具合がありました。Activity死に過ぎ...

その際に他のエンジニアから教えて頂いたのが、開発者向けオプションのアクティビティを保持しない 機能です。

f:id:gibachan03:20201129202854p:plain:w200

これを有効にするとアクティビティが破棄される挙動を再現しやすいので、この手の問題を確認するのに便利です。

サポートする OS/デバイスのバラエティが広い

iOSでは、デバイスの種類が限定的で、比較的ユーザーが新しいOSへ移行しやすい傾向があるのに対し、Android ではデバイスの種類が多彩で、そのためか利用されているOSのバージョンの幅も広いです。実際に古いOSだkでは使えない機能にもぶつかりもしました。

ただ、Googleが提供するAndroid Jetpackのおかげで、AndroidAPIを扱う際には古いOSのことを基本的には気にせずに済むようになっています。とはいえ、Jetpackの内容を把握するのも結構大変。。。

バックボタン対応

はい大変。Androidではアプリの状態によって常に「今バックボタン押されたらどう振る舞うべきか?」を考えなくてはいけません。特に画面遷移フローにおいて困ります。ログイン画面でログインした後にバックボタン押したらログイン画面には戻したくないとか。

いま携わっているプロジェクトでは Single Activity の構成を採用しており、画面遷移はAndroidX Navigationを利用しています。
Navigationでは画面遷移をXMLファイルで定義します。これが結構良くて、XMLで画面遷移の遷移先や一緒に渡すパラメータ、アニメーションなんかも定義できます。その際にもバックボタンで戻る挙動(Fragmentのスタック)も考慮します。考えることは多いですが、XMLのなかに画面遷移に関わる情報が集約されるでわかりやすく感じました。(あとAndroidXMLは人間に優しいのが嬉しい!!普通に読めるし、キーワードから定義元へのジャンプとかもできるとは)

(画面遷移にはAndroidX Navigationを使わず、ActivityからActivityへ遷移する、またはActivityからFragmentを呼び出したりする方法もまたあります。)

ビルドシステムが Gradle

iOS で外部ライブラリを利用するのにCocoaPods/Carthageなどを利用しますが、それらは最初のセットアップがちょっと面倒でした。

Androidでは、アプリの依存モジュールやビルド設定などは build.gradle に記述します。これがプロジェクトに統合されてる分、iOSと比べて扱いやすいなと感じました。ボタンポチだけでライブラリのインストールが済むのは楽ちんです。

まあ、iOSでも最近はSwift Package Managerがサポートされてきて、そこまで差はないかもです。
(余談ですが、Xcode 11からswift packageを直接開けるようになったので SPMの generate-xcodeproj オプションがひっそりとdeprecatedになったのを最近知って時の流れを感じました)

build.gradle はGroovyで記述するので、慣れないうちはちょっと抵抗がありましたが、今ではkotlin でも記述できるようなので、移行しておけば楽そうです。

コードのディレクトリが深くなりがち

Androidではディレクトリ構造がより深くなりがちで、ファイルを行ったり来たりするのが少し辛いです。 特に、マルチモジュール構成のプロジェクトではディレクトリ間の移動が多くなるのでさらに。。。

f:id:gibachan03:20201201215443p:plain:w180 (目的のファイルまでめっちゃ深い)

個人的にファイル間の移動でよく使っているのが、Android Studioショートカット の「shift キーを 2 回押す」で使える すべてを検索(コードとメニューを含む) 機能です。開きたいファイルの名前やキーワードからファイルに飛べるので重宝しています。

あとAndroid StudioのProject tool windowの中にある Always Select Opened File にチェックを入れています。

f:id:gibachan03:20201129211640p:plain:w300

これによってファイルを開いた時に、Project tool windowの該当ファイルも一緒に開いてくれるのでなんとか迷子にならずに済んでいます。 (Xcodeのshift+command+Jを自動的にやってくれる)

XcodeAndroid Studio でショートカットが違う

これはもうIDEが違うので仕方がないです。仕方がないのでAndroid Studioの設定を変えて、できるだけXcodeの時と同じようにしています。 (デフォルトのCtrl + Hで変なウィンドウ開いて困惑しました)

設定を変えると、今度は私用PCと仕事用PCなどの複数の端末で設定し直すのが苦痛になります。 そこで便利だったのが、Android StudioSettings Repository 機能でした。 私用PCと仕事用PCなど、複数の端末で設定し直すのが苦痛でしたが、これによってGitHubで一元管理できるので助かりました。

f:id:gibachan03:20201201220600p:plain:w500

まとめ

Swift/Kotlinの比較は良く見るので、それ以外の部分で実感したものを挙げてみました。
なんかAndroid辛い。。みたいな感じになっちゃたかも。でもそれよりも日々学びがあり、楽しくAndroidと格闘できているので、今後も頑張っていく所存。

React Nativeとプッシュ通知 (iOS編)

前回の記事Androidに対応したので、今回はiOSの対応を行なっていきます。

iOSのセットアップ

Xcode上でプロジェクトの設定を変更したいので /ios/RNPushNotification.xcworkspace を開きます。

最初に、前回にも設定したようにBundle Identifierが com.example.rn-push-notification.ios となっていることを確認します。 さらに通知を受信するためにCapabilityの中からBackground ModesPush Notifications を追加しておきます。 また証明書もまだであれば設定しておきましょう。

f:id:gibachan03:20200811081827p:plain:w400

Firebaseプロジェクトの設定

FirebaseコンソールでプロジェクトにiOSアプリを追加します。 ここで先ほど設定していたバンドルIDを指定しておきます。

f:id:gibachan03:20200811054136p:plain

続けてGoogleService-Info.plistをダウンロードしておき、残りの作業はスキップして大丈夫です。

そして次にプロジェクトにプッシュ通知を送るためのAPNs 認証キーを設定します。 まず証明書を作成するためにApple Developerへ行き、新たなKeyを作成してApple Push Notifications service (APNs)を有効にします。

f:id:gibachan03:20200811054651p:plain:w400

Keyをダウンロードし、画面に表示されているTeam IDとKey IDをメモしておきます。

Firebaseプロジェクトの設定からクラウドメッセージングに入ります。

f:id:gibachan03:20200811055015p:plain:w300

先ほど作成したKeyの情報を設定します。

f:id:gibachan03:20200811055238p:plain:w300

Firebaseの初期化

Xcodeの作業に戻ります。(ここの作業はドキュメントに沿っています)

もし前回実行していなければ下記コマンドでPodsを更新します。 cd ios/ && pod install

そして先ほどダウンロードしたGoogleService-Info.plistをプロジェクトに追加します。

f:id:gibachan03:20200811054451p:plain

Firebaseを初期化するコードを追加します。 AppDelegate.mを開き、ファイル先頭付近に下記コードを追加し、

#import <Firebase.h>

同じファイルのdidFinishLaunchingWithOptionsメソッドの中に下記コードを追加します。

if ([FIRApp defaultApp] == nil) {
  [FIRApp configure];
}

プッシュ通知の受信を許可

React Nativeの世界に戻ります。 iOSでは予め通知を受信する許可を得る必要があるため、その処理を追加します。

App.tsxを開き下記のようにコードを編集します

const App = () => {
  ...
  // 通知の許可をリクエストする
  async function requestUserPermission() {
    const authStatus = await messaging().requestPermission();
    const enabled =
      authStatus === messaging.AuthorizationStatus.AUTHORIZED ||
      authStatus === messaging.AuthorizationStatus.PROVISIONAL;

    if (enabled) {
      console.log('通知が許可されました');
    }
  }

  useEffect(() => {
    // 最初に通知の許可をリクエストする
    requestUserPermission();

    ...
  }, []);

  ...
};

ここまででプッシュ通知を受信する最低限の準備ができました。

プッシュ通知を送信

iOSシミュレータでは通知を受信できないので、Xcodeから実機でアプリを実行します。

最初に通知許可のダイアログが表示されるので許可し、アプリをバックグラウンドに入れます。

f:id:gibachan03:20200811060725p:plain:w300

前回と同様にFirebaseコンソールから通知を送信します。

f:id:gibachan03:20200811082026j:plain:w300

しかし、通知は受信できるかと思いますがAndroidで表示されてたような画像が表示されていないことがわかります。

iOSでは通知に画像を表示するにはNotification Service Extensionを実装する必要があります。 (ドキュメント: Modifying Content in Newly Delivered Notifications

Notification Service Extension

Notification Service Extension実装方法なのですが、React Nativeであっても特別なことはなさそうで、通常のネイティブ実装と同じ方法で実装することになるようです。つまり、/ios/RNPushNotification.xcworkspaceをXcodeで開いてSwift/Obj-Cを書いていくことになります。

実装についてはReact Nativeとも関係なく一般的なものになり、長くなってしまうので省略します。 (詳細はこちらのドキュメント

また、この場合の通知の送信方法も変わります。 プッシュ通知を受信した時にNotification Service Extensionを起動するためには、通知のペイロード“mutable-content” : 1 を含める必要があります。 (上記のドキュメント参照) そしてFirebaseコンソールからはこの設定が現在ではできないようです。 従って別の方法でプッシュ通知を送ります。

ここではレガシーなFCM HTTP APIを使って通知を送ってみます。 このAPIは手軽に使えて便利なのですが、実際にはよりセキュアなHTTP v1 APIを利用するのが良さそうです。

まず、APIの実行に必要なサーバーキーと送信者IDをFirebaseコンソールのSettings > クラウドメッセージングから確認します。

f:id:gibachan03:20200813081938p:plain:w300

コンソールからcurlを使ってAPIを実行します。

curl -X POST -H "Authorization: key=ここにサーバーキー" -H "project_id: key=ここに送信者ID" -H "Content-Type: application/json" -d '{
"to": "/topics/weather",
"notification": {
    "title": "通知テスト",
    "body": "Hello world",
    "image": "https://picsum.photos/id/1015/300/200" },
"mutable_content": true,
"data": {
    "imageURL": "https://picsum.photos/id/1015/300/200" }
}' https://fcm.googleapis.com/fcm/send

mutable_content を設定しておき、data.imageURL の画像をNotification Service Extensionで表示させます。 これで画像が表示されるはずです。

f:id:gibachan03:20200815094934p:plain:w200

以上です。

まとめ

今回この辺を調べたのはReact NativeでiOSのApp Extension(Notification Service Extensionなど)を実装するのはどうやるんだろう?との疑問が発端でした。結局それは一般的なネイティブ実装でやるっぽく、ちょっと残念でした。。。JS/TSで書ければ良かったな。

コードはここに置いておきます。 github.com

React Nativeとプッシュ通知 (Android編)

React Nativeでプッシュ通知を受信する実装について調べたのでまとめておきます。

ここでは下図のような通知を送ることを想定します。

f:id:gibachan03:20200813200009p:plain:w200

環境

  • node: 14.5.0
  • yarn: 1.22.4
  • react-native 0.63.2
  • @react-native-firebase/app 8.3.0
  • @react-native-firebase/messaging 7.6.1

プッシュ通知はFirebase Cloud Messagingから送信することを前提としています。Expoは利用しません。 基本的にはこのドキュメントに従って実装すれば大丈夫です。

React Nativeプロジェクトの作成

ここでは最初にAndroidを実装し、その後でiOSに対応していきます。 それぞれのアプリのパッケージ名/バンドルIDを以下のように決めておきます。

  • Android パッケージ名: com.example.rn_push_notification.android
  • iOS バンドルID: com.example.rn-push-notification.ios

まず最初に下記のコマンドでプロジェクトを新規作成します。(--template を指定していますがそこはお好みで)

npx react-native init RNPushNotification --template react-native-template-typescript

作成されるプロジェクトのパッケージ名/バンドルIDは、デフォルトでcom.rnpushnotification のようになっているので、それらを上記に変更します。

iOSの場合は open ios/RNPushNotification.xcworkspace/ とし、Xcode上で簡単に変更できます。 一方、Androidの場合はいろいろ変更する場所があるのでこちらの記事を参考にガンバリます。

それぞれ変更した後にビルドが通るか確認しておくと安心だと思います。

Firebaseプロジェクトの準備

最初にFirebaseコンソール で新しいプロジェクトを作成します。

f:id:gibachan03:20200810203343p:plain:w300

プロジェクトにAndroidアプリを追加します。パッケージ名には先ほど設定したものを入力しておきます。

f:id:gibachan03:20200810210712p:plain:w300

その後google-services.jsonをダウンロードしておき、その他の画面に表示される指示(ネイティブ実装用の作業)はパスして大丈夫です。

Androidのセットアップ

以下の設定はドキュメントに従ってセットアップしていきます。

先ほどダウンロードしたgoogle-services.json/android/appに配置します。

そして/android/build.gradleに下記を追加し、

buildscript {
    ...
    dependencies {
        ...
        classpath 'com.google.gms:google-services:4.3.3' // これを追加
    }
}

/android/app/build.gradleに下記を追加します。

apply plugin: 'com.google.gms.google-services' // これを追加

Firebaseモジュールをインストール

コンソール上で下記のコマンドを実行します。

yarn add @react-native-firebase/app
yarn add @react-native-firebase/messaging

cd ios/ && pod install    # これはiOSのために必要

通知のトピックを購読する

通知を受信するために特定のトピックを購読します。 App.tsxを開き、簡略化のために不要なコードを削除しつつ次のように編集します。

import React, {useEffect} from 'react';
import {View, Text, StyleSheet} from 'react-native';
import messaging from '@react-native-firebase/messaging'; // これを追加する

const App = () => {
  useEffect(() => {
    // `wether`トピックを購読
    messaging()
      .subscribeToTopic('weather')
      .then(() => console.log('Subscribed to topic!'));
  }, []);

  return (
    <View style={styles.container}>
      <Text>Hello world</Text>
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
  },
});

export default App;

これでプッシュ通知を受信できるようになりました。

次にアプリがバックグラウンドにある時に通知を受信した際に呼ばれるコールバックを設定します。 index.tsxを編集します。

import {AppRegistry} from 'react-native';
import App from './App';
import {name as appName} from './app.json';
import messaging from '@react-native-firebase/messaging'; // これを追加する

// バックグラウンドで通知を受信した時のコールバック
messaging().setBackgroundMessageHandler(async (remoteMessage) => {
  `[${remoteMessage.notification?.title}]を受信しました`;
});

AppRegistry.registerComponent(appName, () => App);

プッシュ通知を送信

コンソールで下記コマンドからandroidアプリを起動します。

yarn run android

アプリをバックグランドに隠しておきます。 FirebaseコンソールにてCloud Messagingを開き、新しい通知を作成します。

下図のようにタイトルなどを入力して[次へ]進みます。

f:id:gibachan03:20200810220716p:plain:w300

送信先のターゲットとして先ほど設定したトピックを指定します。

f:id:gibachan03:20200810220403p:plain:w300

そのまま送信すると、通知を受信できます。

f:id:gibachan03:20200810220817p:plain:w300

ここまでで通知を受信するための最低限の実装ができました。

処理を追加

これだけだと寂しいので少し処理を追加します。

受信した通知をタップするとアプリが開かれますが、それを検知するためのコードを追加します。 App.tsxを編集し、下記のコールバックを追加します。(アプリがバックグラウンドで実行中か終了しているかによって分かれています)

...
const App = () => {
  const [message, setMessage] = useState('まだ通知を受信していません');

  useEffect(() => {
    // `wether`トピックを購読
    ...

    // アプリがバックグラウンドで実行中に通知をタップされたときのコールバック
    messaging().onNotificationOpenedApp((remoteMessage) => {
      setMessage(
        `[${remoteMessage.notification?.title}]によってバックグラウンドから復帰しました`,
      );
    });

    // 通知をタップしてアプリが起動されたときのコールバック
    messaging()
      .getInitialNotification()
      .then((remoteMessage) => {
        if (remoteMessage) {
          setMessage(
            `[${remoteMessage.notification?.title}]によって起動されました`,
          );
        }
      });
  }, []);

  return (
    <View style={styles.container}>
      <Text>{message}</Text>
    </View>
  );
};

f:id:gibachan03:20200813200539p:plain:w300

まとめ

Androidの場合はFCMと相性が良く、特に面倒な事なく実装できました。 次回はiOSでの実装について書く予定です。

ここまでの実装についてはGitHubに上げています。

github.com

React Native for Web + TypeScript + Firebase + PWAを調べたのでまとめておく

動機

きっかけはこの記事。ちょっと前にこれを読んで、 「モバイルネイティブアプリに慣れた開発者が、React Native for Webを利用することで手軽にWebアプリ開発できないか?」と思ってたので調べてみました。

iOSについて言えばSwiftUI登場後、宣言的にUIを構築するということが自然となり、Webアプリの各種FW(Reactなど)との距離感が縮まっていること。前述の記事にあるように、ネイティブ開発で馴染みのあるコンポーネントをReact Nativeを経由してWebの世界に持っていけるなら、Webアプリ開発の敷居が下げられるんじゃないかと思って調べてみました。

先に結論

React Native for Webを使って簡単なTODOアプリを作ってみました。 一応Firebase Hostingに置いてPWAとしてモバイルにインストールするところまで試しました。

github.com

感想としては、

  • TypeScriptのありがたみ半端なかった。普段コンパイル言語触っててからの生JSは不安しかないのでほぼ必須に感じた。
  • UIコンポーネントのスタイル(サイズや色など)はCSS風な記述なので、普段CSSを触ってない人的にはちょい辛かった。とはいえ、for WebだけでなくReact Nativeでのネイティブアプリでもそのまま流用できそうなのは良さそう。
  • HTML/CSS/JSの知識はやはり多少は必要。あとReactなので当然そこの知識も。だけどあまりDOMを意識しないでアプリを書ける嬉しさはあった。
  • 自分が今Webアプリ作るとなったら選択肢としてはありかも。だけど、React Native for Webは個人プロジェクト?ぽく、Facebookで開発されているReact Nativeと立ち位置が異なるので今後は気になる。(追記:と書いてたら@Nkznさんに↓と教えて頂けた!感謝!!)

調べたこと

Webアプリ開発の事前知識としてはHTML/CSS/JavaScriptのほんの基本がわかる程度で、Reactも今回初めてちゃんと触ってみた感じです。ので、若干怪しいところはあるかも。。

あと、React NativeでiOS/Androidのネイティブアプリも開発する場合に、for Webのコードとはある程度共有できる期待はありますが、ここでは一旦考えていません。単純にWebのみでの動作を考えてます。

React Native for Webプロジェクトの作成方法

ドキュメントによるとExpoを利用する方法とCreate React Appを利用する方法の2つがあります。

前者はReactアプリのための様々な便利ツールがパッケージにされたフレームワークらしく、今回はシンプルに最低限で試したかったので後者を使いました。

下記のコマンドでReactプロジェクトを作成し、react-native-webをインストールします。 また、TypeScriptで書きたいので --template typescript を指定してます。

npx create-react-app todo --template typescript
cd todo

この段階で npm start で起動、 npm run build でビルドができます。

そして下記のコマンドでreact-native-webをインストールします。 (TypeScriptなので@types/..の型定義も必要になります。)

npm install react-native-web
npm install @types/react-native --save-dev

これで、プロジェクト内で import { ... } from 'react-native'; とすることでReact Nativeと互換性のあるUIコンポーネントを利用できるようになります。 Webアプリ開発の場合には react-nativeエイリアスによって react-native-web として解釈され、一方、ネイティブアプリ開発の場合は本家の react-native を参照するようにして、コード上は統一的に記述することできるようです。

React Native UIコンポーネント

基本的にはReact Nativeで用意されているコンポーネントに互換性のあるWeb版コンポーネントが用意されていますが、現状では未対応のものもいくつかありました。

利用可能なコンポーネントドキュメントGitHubに一覧があるのでそれを確認します。とりあえず最低限は揃っている印象。

React Nativeでは公式に提供されるもの以外にコミュニティ主導で開発されてるコンポーネントも多いようですが、それらがWeb側にそのまま持ってこれるかは未確認です。

Reactでのコンポーネントの作成

コンポーネントを作成する時は、クラスコンポーネントと関数コンポーネントの2択あって、関数コンポーネントが後発のようです。(https://ja.reactjs.org/docs/components-and-props.html

コンポーネントがクラスでなく関数になるので状態やライフサイクルを持たないことになるので、 それらの要素にアクセスするためにHooks APIなるものが導入されています。 (関数コンポーネントユニットテストどうするの?と思うけど今回はパス)

関数コンポーネントはまだ新しく過渡期?だけど、今後は関数コンポーネントとHooks APIを組み合わせる方向になる雰囲気のようでした。

アプリの状態管理

ベストな手法は?Reactのステート管理方法まとめ という記事を参考にしました。

ざっくり言うと、アプリが小規模であったりコンポーネント間で状態を共有する必要がなかったりする場合はReactが標準で提供するHooks APIを利用するだけで十分。より複雑な状態管理が必要であればReduxの導入を検討するのが良さそうでした。 今回作成したサンプルアプリでは前者の方法を採っています。

Reduxの場合はこのライブラリを利用することになりそう。このサイトのドキュメントもReduxの考え方の解説など、とてもわかりやすく記述されてて参考になりました。

画面遷移

画面遷移(ルーティング)を扱うライブラリはいくつかあって、どのように実装するのが定石なのかパッとわかりませんでした。

React Nativeの場合はReact Navigationあたりを利用するみたい。ですがReact Navigation on the Webのページには Support for web in React Navigation 5 is experimental と書かれててfor Webで使えるかわからなかったので未検証。react-router-domが一般的なReactアプリで利用されてるようなので今回はそちらを使ってみました。

Firebase

React Native for WebのアプリでFirebaseを利用する方法を調べました。 結局は普通のWebアプリなので、一般的なWebアプリと同じセットアップで導入できます。 つまりこれだけ $ npm install firebase

FirebaseのAPIキーなどの設定は様々なんだと思いますが、create-react-appしたプロジェクトでは.envに記述したキーと値のペアを自動的に環境変数に読み込んでくれるので、今回はそれを利用しています。めっちゃ楽。 (ちゃんと調べてないけどdotenvライブラリが動いてるみたい)

今回のサンプルアプリではFirestoreを利用してますが、それも特別なことはなさそう。 Firebase Hostingにデプロイする際にはビルドが必要になので以下の作業が必要でした。

  • $ npm start build でプロジェクトをビルドする。デフォルトでは build ディレクトリに成果物が生成される
  • firebase.jsonでこのディレクトリを公開設定

PWA

実は今回はPWAについてはあんまり手をだせてないです。

ですが、PWAにする最低条件としてはWeb App ManifestとService Workerを用意する必要があるようですが、 create-react-app で作成したプロジェクトには最初からそれらが用意されているので、取り敢えずシュッと試してみることができます。 (他にHTTPSでの配信が必須ですがFirebase HostingでそれもOK)

この1行unregister() -> register() に変えただけで、iOSAndroidなどのブラウザで「ホーム画面に追加」(A2HS)できます。

PWAの詳細は https://developer.mozilla.org/ja/docs/Web/Progressive_web_apps にあって、特にService Workerは一旦インストールされると後から更新が難しかったりするので、実際に利用する際には気をつけた方がよさそうです。

その他の参考記事

「ドメイン駆動設計入門 ボトムアップでわかる!ドメイン駆動設計の基本」を読んだ

DDDについては軽く表面的な部分を齧った程度の知識だったので、いつかはちゃんと勉強しなきゃなーと思ってた。
けどエヴァンス本とかは難しそうなイメージがあってずっと敬遠しちゃってて。
そんなところに、とっつきやすそうな本書を見つけたので読んでみた。

www.shoeisha.co.jp

結果、読んで良かったと思えたので感想やメモなんかを残しておく。

感想

DDDと言うとまずは「ユビキタス言語」とか「境界づけられたコンテキスト」のような用語から始まって、抽象的な話が続くようなイメージがある。

だけど本書はその辺は後回しで、単純な部分から具体的なコードで示しながら説明が進められる。なので、普段コードを書く時に悩んでたこととリンクさせながら読めたので理解がしやすかった。

あと、様々な設計上の判断の指針(何を判断基準にして利用するパターンを選択するかとか)を示してくれつつ、時には判断の難しいこともあるんだと言うことを明確にしてくれているのがとても参考になった。

メモ

以下は個人的に参考になったことのメモ。長!!

  • 値オブジェクト
    • 値オブジェクトにするかどうかの判断基準は「そこにルールが存在しているか」と「それ単体で取り扱いたいか」。
    • 不変オブジェクトなので、メソッドの結果としては新たなインスタンスとして返却する。
    • 値オブジェクトを使うモチベーション
      1. 表現力を増す
      2. 不正な値を存在させない
      3. 誤った代入を防ぐ
      4. ロジックの散財を防ぐ
  • エンティティ
    • エンティティにするの判断基準は「ライフサイクルを持つかどうか」(生成から破棄までの間に変化するものならエンティティ)。
    • ライフサイクルを表現することが無意味である場合には、ひとまずは値オブジェクトとして取り扱う(その方がシステムはシンプルになる)。
  • ドメインサービス
    • 値オブジェクト・エンティティに記述すると不自然になってしまうふるまいを記述するオブジェクト。
    • 値オブジェクト・エンティティとは異なり、自身の振る舞いを変更するような状態は持たない。
    • ある処理がドメインサービスになるかどうかの判断基準は「それがドメインに基づくものかそうでないか」。
      • ドメインに基づくものであれば、それを実現するサービスはドメインサービス。もしアプリケーションを作成するにあたって必要になったのであれば、それはアプリケーションサービス。
  • ドメインモデル貧血症は避ける
    • ドメインオブジェクトに記述されるべき知識や振る舞いがドメインサービスやアプリケーションサービスに記述され、語るべきことを何も語っていないドメインオブジェクトの状態。
    • 振る舞いをどこに定義するか迷いが生じたら、まずは値オブジェクトやエンティティに定義する。ドメインサービスは可能な限り利用しない。
  • リポジトリ
    • 責務はあくまでもオブジェクトの永続化。ユーザーの重複確認のような処理はドメインのルールに近いのでリポジトリの責務としては相応しくない。
    • 永続化のふるまいは永続化を行うオブジェクトを引数に取る。識別子と更新項目を引数にすることはしない。
    • オブジェクトの作成処理はリポジトリには定義しない。コンストラクタかファクトリを利用する。
  • アプリケーションサービス
    • 処理の結果としてドメインオブジェクトをそのまま戻り値とするか否かの選択は重要。ドメインオブジェクトを返すと、アプリケーションサービスの外側でドメインオブジェクトの振る舞いが呼び出されることにより、ドメインに関わるコードが散逸する可能性がある。
    • ドメインオブジェクトの振る舞いを呼び出すのはアプリケーションサービスの役目。
    • 回避する場合は、データ転送用オブジェクト(DTO)にデータを移し替えて返却する。
  • ファクトリ
    • オブジェクトの生成処理自体も、ドメインを表現する層の責務である。
  • 仕様
    • あるオブジェクトが、ある評価基準に達しているかを判定するオブジェクト。
    • ドメインの重要なルールがアプリケーションサービスに記述されてしまうのを回避するために利用する。(=ドメイン貧血症を避ける)
  • アーキテクチャ

View.onAppear(perform:)が呼ばれないことがある

SwiftUIの View のライフサイクルで少しハマったのでメモしておきます。

  • 環境:

  • 要件:

    • ある画面(View)が表示されたタイミングで何か処理を実行したい

UIKitの場合にはViewControllerの viewWillAppear / viewDidAppear なんかで実行することになりますが、
SwiftUIのViewの場合は onAppear(action:) がそれに相当するかと思います。

ですが、このonAppear(action:)が自分の想定していたタイミングでは呼ばれないということがありましたので紹介します。

例として、次のような単純な親子関係のViewのがあり、その子Viewが表示された時にある処理を実行したいとします。

Parent
  - Child  <== このViewが表示されたらonAppearで処理を実行したい!!

コードだとこうなります。

// 子View
struct Child: View {
    init() { print("Child.init()") }
    var body: some View {
        Text("Child")
            // 子Viewが表示されたら処理を実行したい!!
            .onAppear { print("Child.onAppear(perform:)") }
    }
}

// 親View
struct Parent: View {
    init() { print("Parent.init()") }
    var body: some View {
        VStack {
            Child()
        }
    }
}

そしてこの親View( Parent )を表示すると次のようにログ表示されます。

Parent.init()
Child.init()
Child.onAppear(perform:) // ちゃんと呼ばれてる

ここまでは単純ですね。

問題は次のように 親Viewが状態を持つ場合 です。
親Viewの状態が変更されると、その子Viewは再構築されるのですが、 そのタイミングでは onAppear(perfom:) が呼ばれません。(これが想定と違ってた!!)

// 親View
struct Parent: View {
    @State private var myState = false // なにかの状態を追加
    
    init() { print("Parent.init()") }
    var body: some View {
        VStack {
            Child()
            
            // このボタンをタップするとParentの状態が変更されViewの再構築が発生する
            Button(action: {
                self.myState.toggle()
            }) {
                Text("myState = \(self.myState ? "true" : "false")")
            }
        }
    }
}

ここで最初に親View( Parent )を表示すると先ほどと同じログになるのですが、
ボタンをタップして親Viewの状態が変更されると、

Child.init()

となり、子VIew ( Child )の再構築が発生( init() が呼ばれる)するのですが、 onAppear(perfom:) が呼ばれないことがわかります。

なにが困るか?

当初は「ある画面を複数のViewに分割(コンポーネント化)し、そのViewに関連する初期化処理(データの取得など)は onAppear(perform:) で実行する」としたかったのですが、それだと通用しないことがわかりました。

では、常に呼ばれてる init() で初期化処理を行えば?となるかもしれませんが、 init() は表示する必要のないViewに対しても呼ばれるため、無駄な処理が発生してしまいます。

// 例えばこんな場合、すべての子Viewでinit()が呼ばれてしまう
NavigationView {
    List {
        ForEach(0..<100, id: \.self) { value in
            NavigationLink(destination: Child()) {  // すべての子Viewでinit()が呼ばれる
                Text("value: \(value)")
            }
        }
    }
}

このような挙動を考えると、 局所的なView状態以外のオブジェクト(モデル)のライフサイクルは完全にViewと切り離して管理するのがよさそうです。