20220201のAndroidに関する記事は6件です。

スマホからSSH

はじめに ~ SSH connection from your Smart phone ~ スマホからサーバーコンソール、またはラズパイのようなデバイスへのSSH、思ってるより使えるよ!しかもremote.itでインターネット越しにつないじゃおうという記事です。 やりたいこと AndroidまたはiPhoneを小さいながらもSSHコンソールとして使ってみたいと思います。remote.itを経由すれば携帯電波網でも、しかもポートフォワーディング無しでリモートホストに接続できます。 キーボード/マウスをつなぐとなお一層使いやすいですね。Androidならテキトーなキーボード・マウスでも認識するのでハードルは低くかなり使える環境になります。 おすすめSSHクライアント まずはスマホにSSHクライアントをインストールしましょう。個人的おすすめアプリ(2022/1現在)は以下の通り。 Androidのおすすめ:「JuiceSSH」  https://play.google.com/store/apps/details?id=com.sonelli.juicessh&hl=ja&gl=US iPhoneのおすすめ:「ShellFish」  https://apps.apple.com/jp/app/secure-shellfish-ssh-sftp/id1336634154 remote.it のセットアップ(接続先・サーバー側) リモート環境に接続するにはremote.itが必要です。 まずは接続したい装置側にremote.itをセットアップしましょう。ここでは詳細は省略しますが参考になる記事をいくつかリンクします。 remote.it mobileアプリケーションのインストール(接続元・スマホ側) 同じく手元のスマホにもremote.itをインストールするとSSHクライアントと自動連携して(カスタムURLスキーム)快適に接続できます。 Google Play/App Store で "remote.it" を検索してみてください!念のために以下にリンクを。 Android - Google Play "remote.it"  https://play.google.com/store/apps/details?id=com.remoteit&hl=ja&gl=US iPhone - App Store "remote.it"  https://apps.apple.com/jp/app/remote-it/id1437569166 スマホにキーボード・マウスを接続 Androidの場合 Androidは割とどんなキーボード・マウスでも認識するようです。試しに手元にあった無線キーボードタッチパッド付き(非Bluetooth)を接続してみました。こんなかんじ。特に設定はせずともキーボード・マウス共反応しました。 IMEがGboardの場合 [Shift]+[Space] で日本語/英語入力を切り替えられます。少し癖があって2回押さないと切り替わらないことがあります。 下の写真のこの赤丸部分に「あ」の表示が現れたり消えたりするので見分けられます。(ガラスフィルムがバキバキですみません) iphoneの場合 手元にあったBluetoothキーボードとマウスを接続してみました。マウスの接続は少し癖があります。ここでは割愛しますがアクセシビリティ設定あたりを操作します。 [Ctrl]+[Space] で日本語/英語入力を切り替えられます。macと同じなのでわかりやすいですね。 リモートホストにremoteitしてSSHを開始する いよいよ接続の手順です。Androidもiphoneも手順はさほど変わらないのでここではiphoneを例に説明します。 下部「Devices」タブから接続したいデバイス(ターゲット)の一覧を表示します。 ここでは「develop_JPN_04」というラズパイに接続してみます。 上記画面遷移のように、ターゲットのSSHサービス詳細画面まで移動したら起動アプリケーション設定(Launch default)にオススメの「ShellFish」を設定し「Connect」ボタンをタップします。 接続が完了する上記画面に繊維、外部アプリ呼び出し(URLスキーム)ボタン「↗」をタップすると… SSHラズパイのコンソールに繋がりました!なれると意外と快適に操作できると思います。 終了時はexitしてremoteitモバイルアプリまで戻り… 「Close Connection」をタップして接続を終了します。 まとめ スマートフォンは常に持っているのでちょっとした作業ぐらいであれば軽くてどこでも使えて便利ということがわかりました。 とはいえ小さいよ… という方は、iPadがいいかもですね。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

AppAuth for Androidを用いてGoogleログインを実装する

前置き Android端末において、Googleログインの実装方法として、よく使われているのはGoogle Play開発者サービスを利用するやり方です。しかし、Google Play開発者サービスが無効になった場合、Googleログインができなくなります。また、国や地域、メーカーによって、そもそもAndroid端末にGoogle Play開発者サービスが入っていないことがあります。 AppAuth for Androidを利用するメリット GoogleのOAuthでは、webviewをサポートしなくなりました。AppAuth for AndroidはChrome Custom Tabをサポートしているので、GoogleのOAuthを利用できます。(https://developers-jp.googleblog.com/2016/09/modernizing-oauth-interactions-in-native-apps.html) Google Play開発者サービスがいりません。 独自実装より楽です。 AppAuth for Androidの導入方法 AppAuth for Androidの使い方で基本的な使い方を解説しました。ここではGoogleログインに特化した導入方法を説明します。 Google Cloud Platformの登録 Google Cloud Platformでプロジェクトを作成します。 [APIとサービス]→[OAuth同意画面]に入り、OAuth 同意画面の設定をします。スコープもここで設定します。 [APIとサービス]→[認証情報]に入り、[認証情報を作成]→[OAuthクライアントID]をクリックし、[アプリケーションの種類]で“Android”を選び、その他の必要な情報を設定します。 設定が終わったら、クライアントIDが表示されます。このクライアントIDはソースコードで使われます。 ライブラリの導入 appモジュールのbuild.gradleに 1. AppAuth for Android(net.openid:appauth) 2. JWT Decoding library for Android(com.auth0.android:jwtdecode) を追加します。 build.gradle // AppAuth SDK:最新バージョンはhttps://search.maven.org/artifact/net.openid/appauth implementation 'net.openid:appauth:0.11.1' // For decode ID Token:最新バージョンはhttps://mvnrepository.com/artifact/com.auth0.android/jwtdecode implementation 'com.auth0.android:jwtdecode:2.0.0' AndroidManifest.xmlの編集 AndroidManifest.xmlに次のようにリダイレクト用のactivityを追加します。 AndroidManifest.xml ... <activity android:name="net.openid.appauth.RedirectUriReceiverActivity" tools:node="replace"> <intent-filter> <action android:name="android.intent.action.VIEW"/> <category android:name="android.intent.category.DEFAULT"/> <category android:name="android.intent.category.BROWSABLE"/> <data android:scheme="{アプリのパッケージ名}"/> </intent-filter> </activity> ... AuthStateとAuthorizationServiceConfigurationを用意 空のAuthStateとAuthorizationServiceConfigurationを生成します。 private val appAuthState: AuthState = AuthState.jsonDeserialize("{}") private val config = AuthorizationServiceConfiguration( Uri.parse("https://accounts.google.com/o/oauth2/v2/auth"), Uri.parse("https://www.googleapis.com/oauth2/v4/token") ) サインイン サインイン画面を表示 AuthorizationRequestを生成します。 スコープを設定します。 AuthorizationServiceのgetAuthorizationRequestIntentで、サインインのIntentを作ります。 startActivityForResultでサインインのIntentを起動します。 fun signIn() { val authorizationRequest = AuthorizationRequest .Builder( config, {クライアントID}, ResponseTypeValues.CODE, Uri.parse({アプリのパッケージ名}:/{適当なパス}) ) .setScope({Google Cloud Platformで設定したスコープ。例:openid email profile}) .build() val intent = AuthorizationService(context).getAuthorizationRequestIntent(authorizationRequest) startActivityForResult(intent, requestCode) } サインインの認証結果を取得 onActivityResultでサインインの認証結果を受け取ります。 AuthStateを更新します。 認証成功の条件 AuthorizationResponseが空ではなく、かつAuthorizationExceptionが空である 認証失敗の条件 AuthorizationResponseが空、またはAuthorizationExceptionが空ではない fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { val authorizationResponse = AuthorizationResponse.fromIntent(data) val exception = AuthorizationException.fromIntent(data) appAuthState.update(authorizationResponse, exception) if (exception != null || authorizationResponse == null) { // 認証失敗 } else { // 認証成功 } } 各トークンを取得 AuthorizationResponseを使ってトークンを要求します。 (1)のレスポンスを使ってAuthStateを更新します。 AuthStateのaccessTokenにアクセストークンが入っています。 AuthStateのrefreshTokenにリフレッシュトークンが入っています。 AuthStateのidTokenにIDトークンが入っています。 AuthorizationService(context) .performTokenRequest(authorizationResponse.createTokenExchangeRequest()) { tokenResponse, authorizationException -> appAuthState.update(tokenResponse, authorizationException) tokenResponse?.let { // アクセストークン=appAuthState.accessToken // リフレッシュトークン=appAuthState.refreshToken // IDトークン=appAuthState.idToken } } IDトークンをユーザー情報に変換 JWTでIDトークンを解凍したら、次のユーザー情報が取得できます。 val jwt = JWT(idToken) // Open ID val openId = jwt.subject // 姓名 val name = jwt.claims["name"]?.asString() // 姓 val familyName = jwt.claims["family_name"]?.asString() // 名 val givenName = jwt.claims["given_name"]?.asString() // プロフィール画像のURL val pictureUrl = jwt.claims["picture"]?.asString() // メールアドレス val email = jwt.claims["email"]?.asString() // メールアドレスは認証済みかどうか val emailVerified = jwt.claims["email_verified"]?.asBoolean() GitHub AppAuthDemo(Huawei Google) 参考 AppAuth for Android AppAuth for Androidを用いてソーシャルログインを実装する
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

AppAuth for Androidを用いてソーシャルログインを実装する

前置き 通常、ソーシャルログインを実装するのに、そのソーシャルログインのSDKを導入することが一般的です。しかし、この方法ですと、SDKに依存してしまうため、対象プラットフォームのSDKがなければ、導入ができなくなります。たとえば、 HMSのAndroid端末において、Google IDまたはApple IDでログインする場合 HMS Coreが入っていないAndroid端末において、HUAWEI IDでログインする場合 GMSのAndroid端末において、Apple IDでログインする場合 上記の状況ですと、SDK導入によるソーシャルログインの実装では対応が不可能です。 また、SDKはプラットフォームごとに異なるため、ソースコードの管理において手間が増えます。 そこで、解決策としてOauthが出てきます。Oauthを利用すれば、プラットフォームとソーシャルログインSDKに依存せずに、ソーシャルログインを実装できます。 Android端末限定であれば、AppAuth for AndroidというOauthに特化したAndroid専用のSDKあり、それを使えば、OAuth 2.0に対応したソーシャルログインを簡単に実装できます。 AppAuth for Androidの導入方法 対応OS Android 6.0以上 ライブラリの導入 appモジュールのbuild.gradleに 1. AppAuth for Android(net.openid:appauth) 2. JWT Decoding library for Android(com.auth0.android:jwtdecode) を追加します。 AppAuth for Androidを使ってソーシャルログインをし、JWT Decoding library for Androidを使ってログイン後に返ってくるIDトークンを解凍し、ユーザー情報を取得します。 build.gradle // AppAuth SDK implementation 'net.openid:appauth:{バージョン}' // For decode ID Token implementation 'com.auth0.android:jwtdecode:{バージョン}' また、それぞれの最新バージョンはこちらでご確認ください。 AppAuth for Androidの最新バージョン JWT Decoding library for Androidの最新バージョン AndroidManifest.xmlの編集 AndroidManifest.xmlに次のようにリダイレクト用のactivityを追加します。 AndroidManifest.xml ... <activity android:name="net.openid.appauth.RedirectUriReceiverActivity" tools:node="replace"> <intent-filter> <action android:name="android.intent.action.VIEW"/> <category android:name="android.intent.category.DEFAULT"/> <category android:name="android.intent.category.BROWSABLE"/> <data android:scheme="{your scheme}" android:host="{your host}" android:path="{your path}"/> </intent-filter> </activity> ... <data>要素の中身は対象のソーシャルログインによって異なります。たとえば、 Google ID: <data android:scheme="{アプリのパッケージ名}"/> HUAWEI ID: <data android:scheme="com.huawei.apps.{OAuth 2.0 client ID}"/> Apple ID: <data android:scheme="https" android:host="{Developerサイトへ事前に登録したReturnURLsのホスト部分}" android:path="{Developerサイトへ事前に登録したReturnURLsのパス部分}"/> LINE ID: <data android:scheme="https" android:host="{LINE Developersコンソールに登録したコールバックURLのホスト部分}" android:path="{LINE Developersコンソールに登録したコールバックURLのパス部分}"/> Facebook ID: <data android:scheme="https" android:host="{クライアントOAuth設定に登録した有効なOAuthリダイレクトURIのホスト部分}" android:path="{クライアントOAuth設定に登録した有効なOAuthリダイレクトURIのパス部分}"/> AuthStateとAuthorizationServiceConfigurationを用意 空のAuthStateを生成し、ソーシャルログインの認証エンドポイントとトークンエンドポイントを用いてAuthorizationServiceConfigurationを生成します。 private val appAuthState: AuthState = AuthState.jsonDeserialize("{}") private val config = AuthorizationServiceConfiguration( Uri.parse({認証エンドポイント}), Uri.parse({トークンエンドポイント}) ) サインイン サインイン画面を表示 ソーシャルログインのクライアントIDとリダイレクトURIと、前のステップで生成したAuthorizationServiceConfigurationを用いて、AuthorizationRequestを生成します。 スコープを設定します。 AuthorizationServiceのgetAuthorizationRequestIntentで、サインインのIntentを作ります。 startActivityForResultでサインインのIntentを起動します。サインインの結果はonActivityResultで返ってくるので、startActivityではなく、startActivityForResultでIntentを起動しなければなりません。 fun signIn() { val authorizationRequest = AuthorizationRequest .Builder( config, {ソーシャルログインのクライアントID}, ResponseTypeValues.CODE, Uri.parse({ソーシャルログインのクライアントのリダイレクトURI}) ) .setScope({スコープ。例:openid email profile}) .build() val intent = AuthorizationService(context).getAuthorizationRequestIntent(authorizationRequest) startActivityForResult(intent, requestCode) } サインインの認証結果を取得 onActivityResultでサインインの認証結果を受け取ります。 AuthStateを更新します。 認証成功の条件 AuthorizationResponseが空ではなく、かつAuthorizationExceptionが空である 認証失敗の条件 AuthorizationResponseが空、またはAuthorizationExceptionが空ではない fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { val authorizationResponse = AuthorizationResponse.fromIntent(data) val exception = AuthorizationException.fromIntent(data) appAuthState.update(authorizationResponse, exception) if (exception != null || authorizationResponse == null) { // 認証失敗 } else { // 認証成功 } } 各トークンを取得 AuthorizationResponseを使ってトークンを要求します。 (1)のレスポンスを使ってAuthStateを更新します。 AuthStateのaccessTokenにアクセストークンが入っています。 AuthStateのrefreshTokenにリフレッシュトークンが入っています。 AuthStateのidTokenにIDトークンが入っています。 AuthorizationService(context) .performTokenRequest(authorizationResponse.createTokenExchangeRequest()) { tokenResponse, authorizationException -> appAuthState.update(tokenResponse, authorizationException) tokenResponse?.let { // アクセストークン=appAuthState.accessToken // リフレッシュトークン=appAuthState.refreshToken // IDトークン=appAuthState.idToken } } IDトークンをユーザー情報に変換 JWTでIDトークンを解凍します。 OpenID=jwt.subject メールアドレス=jwt.claims["email"]?.asString() val jwt = JWT(idToken) val openId = jwt.subject val email = jwt.claims["email"]?.asString() GitHub AppAuthDemo(Huawei Google) 参考 AppAuth for Android
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

AppAuth for Androidの使い方

前置き 通常、ソーシャルログインを実装するのに、そのソーシャルログインのSDKを導入することが一般的です。しかし、この方法ですと、SDKに依存してしまうため、対象プラットフォームのSDKがなければ、導入ができなくなります。たとえば、 HMSのAndroid端末において、Google IDまたはApple IDでログインする場合 HMS Coreが入っていないAndroid端末において、HUAWEI IDでログインする場合 GMSのAndroid端末において、Apple IDでログインする場合 上記の状況ですと、SDK導入によるソーシャルログインの実装では対応が不可能です。 また、SDKはプラットフォームごとに異なるため、ソースコードの管理において手間が増えます。 そこで、解決策としてOauthが出てきます。Oauthを利用すれば、プラットフォームとソーシャルログインSDKに依存せずに、ソーシャルログインを実装できます。 Android端末限定であれば、AppAuth for AndroidというOauthに特化したAndroid専用のSDKあり、それを使えば、OAuth 2.0に対応したソーシャルログインを簡単に実装できます。 AppAuth for Androidの導入方法 ライブラリの導入 appモジュールのbuild.gradleに 1. AppAuth for Android(net.openid:appauth) 2. JWT Decoding library for Android(com.auth0.android:jwtdecode) を追加します。 AppAuth for Androidを使ってソーシャルログインをし、JWT Decoding library for Androidを使ってログイン後に返ってくるIDトークンを解凍し、ユーザー情報を取得します。 build.gradle // AppAuth SDK implementation 'net.openid:appauth:{バージョン}' // For decode ID Token implementation 'com.auth0.android:jwtdecode:{バージョン}' また、それぞれの最新バージョンはこちらでご確認ください。 AppAuth for Androidの最新バージョン JWT Decoding library for Androidの最新バージョン AndroidManifest.xmlの編集 AndroidManifest.xmlに次のようにリダイレクト用のactivityを追加します。 AndroidManifest.xml ... <activity android:name="net.openid.appauth.RedirectUriReceiverActivity" tools:node="replace"> <intent-filter> <action android:name="android.intent.action.VIEW"/> <category android:name="android.intent.category.DEFAULT"/> <category android:name="android.intent.category.BROWSABLE"/> <data android:scheme="{your scheme}" android:host="{your host}" android:path="{your path}"/> </intent-filter> </activity> ... <data>要素の中身は対象のソーシャルログインによって異なります。たとえば、 Google ID: <data android:scheme="{アプリのパッケージ名}"/> HUAWEI ID: <data android:scheme="com.huawei.apps.{APP ID}"/> Apple ID: <data android:scheme="https" android:host="{Developerサイトへ事前に登録したReturnURLsのホスト部分}" android:path="{Developerサイトへ事前に登録したReturnURLsのパス部分}"/> LINE ID: <data android:scheme="https" android:host="{LINE Developersコンソールに登録したコールバックURLのホスト部分}" android:path="{LINE Developersコンソールに登録したコールバックURLのパス部分}"/> Facebook ID: <data android:scheme="https" android:host="{クライアントOAuth設定に登録した有効なOAuthリダイレクトURIのホスト部分}" android:path="{クライアントOAuth設定に登録した有効なOAuthリダイレクトURIのパス部分}"/> AuthStateとAuthorizationServiceConfigurationを用意 空のAuthStateを生成し、ソーシャルログインの認証エンドポイントとトークンエンドポイントを用いてAuthorizationServiceConfigurationを生成します。 private val appAuthState: AuthState = AuthState.jsonDeserialize("{}") private val config = AuthorizationServiceConfiguration( Uri.parse({認証エンドポイント}), Uri.parse({トークンエンドポイント}) ) サインイン サインイン画面を表示 ソーシャルログインのクライアントIDとリダイレクトURIと、前のステップで生成したAuthorizationServiceConfigurationを用いて、AuthorizationRequestを生成します。 スコープを設定します。 AuthorizationServiceのgetAuthorizationRequestIntentで、サインインのIntentを作ります。 startActivityForResultでサインインのIntentを起動します。サインインの結果はonActivityResultで返ってくるので、startActivityではなく、startActivityForResultでIntentを起動しなければなりません。 fun signIn() { val authorizationRequest = AuthorizationRequest .Builder( config, {ソーシャルログインのクライアントID}, ResponseTypeValues.CODE, Uri.parse({ソーシャルログインのクライアントのリダイレクトURI}) ) .setScope({スコープ。例:openid email profile}) .build() val intent = AuthorizationService(context).getAuthorizationRequestIntent(authorizationRequest) startActivityForResult(intent, requestCode) } サインインの認証結果を取得 onActivityResultでサインインの認証結果を受け取ります。 AuthStateを更新します。 認証成功の条件 AuthorizationResponseが空ではなく、かつAuthorizationExceptionが空である 認証失敗の条件 AuthorizationResponseが空、またはAuthorizationExceptionが空ではない fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { val authorizationResponse = AuthorizationResponse.fromIntent(data) val exception = AuthorizationException.fromIntent(data) appAuthState.update(authorizationResponse, exception) if (exception != null || authorizationResponse == null) { // 認証失敗 } else { // 認証成功 } } IDトークンを取得 AuthorizationResponseを使ってトークンを要求します。 (1)のレスポンスを使ってAuthStateを更新します。 AuthStateのidTokenにIDトークンが入っています。 AuthorizationService(context) .performTokenRequest(authorizationResponse.createTokenExchangeRequest()) { tokenResponse, authorizationException -> appAuthState.update(tokenResponse, authorizationException) tokenResponse?.let { // IDトークン=appAuthState.idToken } } IDトークンをユーザー情報に変換 JWTでIDトークンを解凍します。 OpenID=jwt.subject メールアドレス=jwt.claims["email"]?.asString() val jwt = JWT(idToken) val openId = jwt.subject val email = jwt.claims["email"]?.asString() GitHub AppAuthDemo(Huawei Google) 参考 AppAuth for Android
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Android Studio(Arctic Fox/Bumblebee) + kotlin 1.6.10 でリリースビルドするとアプリの動作がおかしくなる件

みなさんいかがお過ごしでしょうか? 私は寒いのと暑いのが両方嫌いなタイプですので、早くあったかくならないかなあと思っているところです。 でもどうせ寒いなら雪が少しでもふればいいのに、とも思うわけです。 氷じゃだめなんです!雪が必要なんです! そうとう寒いのに全然雪が降らなかったら、何か損した気分になりますよね? (ならないか。おかしいな。 あと、どうでもいいことかもしれませんが、最近のマイブームは公園にいってカモとコイに食パンをあげることです。 (餌あげていい場所です、これ大事。リストラされたサラリーマンみたいだな、って思わないように。 カモとコイを比べると、素早さが武道家と戦士くらい違うので、だいたいカモが先に食べるんですよね。 そんな他人に一切忖度をしないカモが好きになった今日この頃です。 さて、今回のお題はリリースビルドについてです。 Android Studio(Arctic Fox/Bumblebee)、gradle、perf-pluginの組み合わせでリリースビルドすると、 アプリの動作がおかしかったり(、ビルドできなかったり)したときのメモです。 (ハマっているかもしれないどなたかのお役に立てればと。 デバッグ実行すると問題ないのに、リリースビルドするとアプリは起動するけど、正しく動かないんですよね。 関わっているアプリでいうと、jsonのパース周りだということはわかっていたのですが、解決しなくて2か月くらい悩んでいました。 環境 ① Android Studio Arctic Fox | 2020.3.1 Patch 4 kotlin 1.5.31 classpath 'com.android.tools.build:gradle:7.0.4' classpath 'com.google.firebase:perf-plugin:1.4.0' → アプリ動作に問題なし。 ② Android Studio Arctic Fox | 2020.3.1 Patch 4 kotlin 1.6.10 classpath 'com.android.tools.build:gradle:7.0.4' classpath 'com.google.firebase:perf-plugin:1.4.0' → アプリ動作に問題あり。 ③ Android Studio Arctic Fox | 2020.3.1 Patch 4 kotlin 1.6.10 classpath 'com.android.tools.build:gradle:7.1.0' classpath 'com.google.firebase:perf-plugin:1.4.0' → ビルドできない。 Unable to load class 'com.android.build.api.extension.AndroidComponentsExtension'. This is an unexpected error. Please file a bug containing the idea.log file. ④ Android Studio Arctic Fox | 2020.3.1 Patch 4 kotlin 1.6.10 classpath 'com.android.tools.build:gradle:7.1.0' classpath 'com.google.firebase:perf-plugin:1.4.1' → ビルドできない。 This version of the Android Support plugin for IntelliJ IDEA (or Android Studio) cannot open this project, please retry with version 2021.1.1 or newer. ⑤ Android Studio Bumblebee | 2021.1.1 ※最近安定版が出たばかり。 kotlin 1.5.31 classpath 'com.android.tools.build:gradle:7.0.4' classpath 'com.google.firebase:perf-plugin:1.4.0' → アプリ動作に問題なし。 ⑥ Android Studio Bumblebee | 2021.1.1 ※最近安定版が出たばかり。 kotlin 1.5.31 classpath 'com.android.tools.build:gradle:7.1.0' classpath 'com.google.firebase:perf-plugin:1.4.1' → アプリ動作に問題なし。 ⑦ Android Studio Bumblebee | 2021.1.1 ※最近安定版が出たばかり。 kotlin 1.6.10 classpath 'com.android.tools.build:gradle:7.0.4' classpath 'com.google.firebase:perf-plugin:1.4.1' → アプリ動作に問題あり。 ⑧ Android Studio Bumblebee | 2021.1.1 ※最近安定版が出たばかり。 kotlin 1.6.10 classpath 'com.android.tools.build:gradle:7.0.4' classpath 'com.google.firebase:perf-plugin:1.4.0' → アプリ動作に問題あり。 ⑨ Android Studio Bumblebee | 2021.1.1 ※最近安定版が出たばかり。 kotlin 1.6.10 classpath 'com.android.tools.build:gradle:7.1.0' classpath 'com.google.firebase:perf-plugin:1.4.0' → ビルドできない。 Unable to load class 'com.android.build.api.extension.AndroidComponentsExtension'. This is an unexpected error. Please file a bug containing the idea.log file. ⑩ Android Studio Bumblebee | 2021.1.1 ※最近安定版が出たばかり。 kotlin 1.6.10 classpath 'com.android.tools.build:gradle:7.1.0' classpath 'com.google.firebase:perf-plugin:1.4.1' → アプリ動作に問題なし。 解決できなくてしばらく①でリリースビルドをしていたのですが、 ⑩で問題が解決しました。(表にしたらよかった・・。 kotlin 1.6.10 使うには Android Studio Bumblebee を待つしかなかったということなんだろうか・・・。 今回は以上です。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Guide to app architectureからプラクティスを拾うメモ データレイヤー編

の続きです。 用語整理 データレイヤーとは? While the UI layer contains UI-related state and UI logic, the data layer contains application data and business logic. The business logic is what gives value to your app—it's made of real-world business rules that determine how application data must be created, stored, and changed. アプリケーションデータとビジネスロジックを含む。ビジネスロジックはアプリに価値を与えるもので、ビジネスロジックはアプリケーションのデータがどのように作られ保存され変更されるかを決定するルールから作られる。 This separation of concerns allows the data layer to be used on multiple screens, share information between different parts of the app, and reproduce business logic outside of the UI for unit testing. For more information about the benefits of the data layer, check out the Architecture Overview page. この分離によって複数の画面やテストなどで共有でき、ロジックをUI外でテストすることを可能にする。 The data layer is made of repositories that each can contain zero to many data sources. You should create a repository class for each different type of data you handle in your app. For example, you might create a MoviesRepository class for data related to movies, or a PaymentsRepository class for data related to payments. データレイヤーは複数のリポジトリから作られ、リポジトリは0〜複数のデータソースを含む。 https://developer.android.com/jetpack/guide/data-layer?hl=en より class ExampleRepository( private val exampleRemoteDataSource: ExampleRemoteDataSource, // network private val exampleLocalDataSource: ExampleLocalDataSource // database ) { /* ... */ } Repository classes are responsible for the following tasks: Exposing data to the rest of the app. Centralizing changes to the data. Resolving conflicts between multiple data sources. Abstracting sources of data from the rest of the app. Containing business logic. 以下を行う アプリの他の部分にデータを提供する データの変更をここに集中させる 複数のデータソース間の競合を解決する アプリの他の部分にデータソースを抽象化する ビジネスロジックを含む Repositoryクラスをアプリで扱うデータの種類ごとに作らなければならない The data layer is made of repositories that each can contain zero to many data sources. You should create a repository class for each different type of data you handle in your app. For example, you might create a MoviesRepository class for data related to movies, or a PaymentsRepository class for data related to payments. 例えばmovieに関係するものをMoviesRepositoryに、paymentsに関係するものをPaymentsRepositoryに。 なぜ? そのようにRepositoryを定義しているため。 データソースクラスは一つのデータのソースとのみ動作する責務を持つべき Each data source class should have the responsibility of working with only one source of data, which can be a file, a network source, or a local database. Data source classes are the bridge between the application and the system for data operations. ファイルやネットワークやローカルデータベースなどがソースになり得る。データソースはシステムとアプリの架け橋になる。 なぜ? 特に書かれていませんが、単一責任や、関心事の分離のためだと思います。 他のレイヤーのクラスはデータソースに直接アクセスしてはならない。リポジトリーを経由する必要がある。 Other layers in the hierarchy should never access data sources directly; the entry points to the data layer are always the repository classes. State holder classes (see the UI layer guide) or use case classes (see the domain layer guide) should never have a data source as a direct dependency. Using repository classes as entry points allows the different layers of the architecture to scale independently. なぜ? アーキテクチャの異なるレイヤーを独立して拡張することができるため。 (多分データソースを変更したときに使っている箇所で影響を受けないようにできるとかそういうことだと思われる) このレイヤーから排出されるデータはimmutable(変更不可能)でなければならない。 The data exposed by this layer should be immutable so that it cannot be tampered with by other classes, which would risk putting its values in an inconsistent state. Immutable data can also be safely handled by multiple threads. See the threading section for more details. なぜ? データを他のクラスで変更することでデータの不整合が起こる可能性があり、変更を防ぐため。またマルチスレッドの問題もあるため。 1データソースしかないときにリポジトリとデータソースを混ぜている場合は、複数データソースが必要になったときにリファクタリングすること Note: Often, when a repository only contains a single data source and doesn't depend on other repositories, developers merge the responsibilities of repositories and data sources into the repository class. If you do this, don't forget to split functionalities if the repository needs to handle data from another source in a later version of your app. なぜ? データソースクラスは一つのデータのソースとのみ動作する責務を持つべきというのがあるため。 (関心事の分離) 一度行うだけの操作(One-shotな操作)にはKotlinのsuspend関数、JavaであれコールバックやRxJavaのSingleやMaybe、Completableを公開する。変更されるデータにはKotlinのFlowやコールバックやRxJavaのObservableやFlowableを公開する One-shot operations: The data layer should expose suspend functions in Kotlin; and for the Java programming language, the data layer should expose functions that provide a callback to notify the result of the operation, or RxJava Single, Maybe, or Completable types. To be notified of data changes over time: The data layer should expose flows in Kotlin; and for the Java programming language, the data layer should expose a callback that emits the new data, or the RxJava Observable or Flowable type. class ExampleRepository( private val exampleRemoteDataSource: ExampleRemoteDataSource, // network private val exampleLocalDataSource: ExampleLocalDataSource // database ) { val data: Flow<Example> = ... suspend fun modifyData(example: Example) { ... } } なぜ? 特に理由は書いていませんでしたが、おそらく公開するインターフェースを必要最低限にするためそのための仕組みを使おうということだと思います。 RepositoryはRepositoryに依存するものも存在しうる In some cases involving more complex business requirements, a repository might need to depend on other repositories. This could be because the data involved is an aggregation from multiple data sources, or because the responsibility needs to be encapsulated in another repository class. For example, a repository that handles user authentication data, UserRepository, could depend on other repositories such as LoginRepository and RegistrationRepository to fulfill its requirements. https://developer.android.com/jetpack/guide/data-layer?hl=en#multiple-levels より なぜ? 関係するデータが複数のデータソースからできる集合体であったり、責任を別のリポジトリのクラスにカプセル化する必要があったりするため。 Repositoryやデータソースのメソッドはメインセーフ(メインスレッドから呼び出しても安全)であるべき Calling data sources and repositories should be main-safe—safe to call from the main thread. These classes are responsible for moving the execution of their logic to the appropriate thread when performing long-running blocking operations. For example, it should be main-safe for a data source to read from a file, or for a repository to perform expensive filtering on a big list. Note that most data sources already provide main-safe APIs like the suspend method calls provided by Room or Retrofit. Your repository can take advantage of these APIs when they are available. RoomやRetrofitを使っていればすでにメインセーフになっている。 なぜ? Repositoryやデータソースに適切なスレッドで実行する責務があるため。 データレイヤーのクラスは他のクラスから参照される限りメモリに持ち続ける Instances of classes in the data layer remain in memory as long as they are reachable from a garbage collection root—usually by being referenced from other objects in your app. If a class contains in-memory data—for example, a cache—you might want to reuse the same instance of that class for a specific period of time. This is also referred to as the lifecycle of the class instance. If the class's responsibility is crucial for the whole application, you can scope an instance of that class to the Application class. This makes it so the instance follows the application's lifecycle. Alternatively, if you only need to reuse the same instance in a particular flow in your app—for example, the registration or login flow—then you should scope the instance to the class that owns the lifecycle of that flow. For example, you could scope a RegistrationRepository that contains in-memory data to the RegistrationActivity or the navigation graph of the registration flow. アプリ全体で重要であればApplicationクラスで持っても良いし、そうでなければ、アプリのフローの中で持っても良い。 The lifecycle of each instance is a critical factor in deciding how to provide dependencies within your app. It's recommended that you follow dependency injection best practices where the dependencies are managed and can be scoped to dependency containers. To learn more about scoping in Android, see the Scoping in Android and Hilt blog post. DIなどを活用して利用しよう。 なぜ? データレイヤーでキャッシュして使いまわしたくなるデータが有るため。 理想的にはアプリが必要なデータだけをデータソースが返すことが望ましい(サーバーのレスポンスなど) The data models that you want to expose from the data layer might be a subset of the information that you get from the different data sources. Ideally, the different data sources—both network and local—should return only the information your application needs; but that's not often the case. なぜ? メモリの消費を抑えるなど。ただそうなることはあまりないという補足がある。 データソースのクラスとビジネスモデルを分離し、ビジネスモデルではアプリが必要なデータだけをも持とう (ここで初めてビジネスモデル = business models という単語が出てきます。おそらくいわゆるドメインモデルと言われるやつのようです。) For example, imagine a News API server that returns not only the article information, but also edit history, user comments, and some metadata: 必要ないauthorDateOfBirthとかが入っている。 data class ArticleApiModel( val id: Long, val title: String, val content: String, val publicationDate: Date, val modifications: Array<ArticleApiModel>, val comments: Array<CommentApiModel>, val lastModificationDate: Date, val authorId: Long, val authorName: String, val authorDateOfBirth: Date, val readTimeMin: Int ) The app doesn't need that much information about the article because it only displays the content of the article on the screen, along with basic information about its author. It's a good practice to separate model classes and have your repositories expose only the data that the other layers of the hierarchy require. For example, here is how you might trim down the ArticleApiModel from the network in order to expose an Article model class to the domain and UI layers: data class Article( val id: Long, val title: String, val content: String, val publicationDate: Date, val authorName: String, val readTimeMin: Int ) Separating model classes is beneficial in the following ways: It saves app memory by reducing the data to only what's needed. It adapts external data types to data types used by your app—for example, your app might use a different data type to represent dates. It provides better separation of concerns—for example, members of a large team could work individually on the network and UI layers of a feature if the model class is defined beforehand. なぜ? 必要なものを持つことで、アプリのメモリを節約できる。 Data型で別のものを使うなど、別のデータ型をてきおうできる 関心事の分離ができる。例えばモデルクラスが事前に定義されていれば、大規模なチームでUIレイヤーとネットワークで個別に作業できる 他のレイヤーもモデルクラスを同様に定義できるが、追加のクラスとロジック、ドキュメントとテストを追加する必要がある。 You can extend this practice and define separate model classes in other parts of your app architecture as well—for example, in data source classes and ViewModels. However, this requires you to define extra classes and logic that you should properly document and test. なぜ? 実装するクラスが増えるため。 最低でもデータソースが受信するデータとアプリ内のデータを分けよう At minimum, it's recommended that you create new models in any case where a data source receives data that doesn't match with what the rest of your app expects. 分けることによるメリット(前述)と、他のレイヤーでもモデルクラスを増やす場合のデメリットが有るため。 データレイヤーは3つのlifecycleの操作を持つことができる Types of data operations The data layer can deal with types of operations that vary based on how critical they are: UI-oriented, app-oriented, and business-oriented operations. UI-oriented operations UI-oriented operations are only relevant when the user is on a specific screen, and they're canceled when the user moves away from that screen. An example is displaying some data obtained from the database. UI-oriented operations are typically triggered by the UI layer and follow the caller's lifecycle—for example, the lifecycle of the ViewModel. See the Make a network request section for an example of a UI-oriented operation. UI-oriented operations、画面に必要なDBのデータを購読するなど。画面単位のライフサイクル。 App-oriented operations App-oriented operations are relevant as long as the app is open. If the app is closed or the process is killed, these operations are canceled. An example is caching the result of a network request so that it can be used later if needed. See the Implement in-memory data caching section to learn more. These operations typically follow the lifecycle of the Application class or the data layer. For an example, see the Make an operation live longer than the screen section. App-oriented operations、メモリでキャッシュを保持しておくときなど、Applicationクラスのライフサイクル。 Business-oriented operations Business-oriented operations cannot be canceled. They should survive process death. An example is finishing the upload of a photo that the user wants to post to their profile. The recommendation for business-oriented operations is to use WorkManager. See the Schedule tasks using WorkManager section to learn more. Business-oriented operations、プロセスが死んでもキャンセルされないようなタスクで写真のアップロードなど。WorkManagerを使って処理すべき リポジトリやデータレイヤーではCoroutinesとFlow関してはKotlinの仕組みを使ってエラーをthrowし、UIレイヤーでキャッチする。 Interactions with repositories and sources of data can either succeed or throw an exception when a failure occurs. For coroutines and flows, you should use Kotlin's built-in error-handling mechanism. For errors that could be triggered by suspend functions, use try/catch blocks when appropriate; and in flows, use the catch operator. With this approach, the UI layer is expected to handle exceptions when calling the data layer. The data layer can understand and handle different types of errors and expose them using custom exceptions—for example, a UserNotAuthenticatedException. なぜ? おそらくKotlinの仕組みでハンドリングできるからだと思われます。 Kotlin langでこのあたりを質問してGooglerから回答をもらったりしました。ちょっと個人的な意見もまざるんですが、 なぜこういう関数でResultを使いたくなるのかというとKotlinにはチェック例外がなく、なぜチェック例外がないかというと以下のように、versionabilityとscalabilityの問題があるからです。 Googlerの意見によるといろんなレイヤーですべてエラーをチェックし、もう一度投げたりするのは例外に対してばかげており、throwしてコールスタックを脱出してしまう方が良いという意見だと解釈をしました。 例えば .firstOrNull(): R?が毎回チェックしないといけないResult型とかを返してきたらちょっとうざいみたいな感じだと思います。 Rustではもっといいとか、Rustは使う側からは扱いにくいとか、Railway Oriented Programmingとかこのあたりは奥が深いです interfaceを提供して、API実装を交換可能にしよう Key Point: Relying on interfaces makes API implementations swappable in your app. In addition to providing scalability and allowing you to replace dependencies more easily, it also favors testability because you can inject fake data source implementations in tests. なぜ? Fakeを提供して、テストで入れ替え可能にするため。 Repositoryで可変変数を使う場合はMutexを使う方法がある For simplicity, NewsRepository uses a mutable variable to cache the latest news. To protect reads and writes from different threads, a Mutex is used. To learn more about shared mutable state and concurrency, see the Kotlin documentation. The following implementation caches the latest news information to a variable in the repository that is write-protected with a Mutex. If the result of the network request succeeds, the data is assigned to the latestNews variable. 他のスレッドからの書き込みで壊れないようにする方法としてmutexがある。 class NewsRepository( private val newsRemoteDataSource: NewsRemoteDataSource ) { // Mutex to make writes to cached values thread-safe. private val latestNewsMutex = Mutex() // Cache of the latest news got from the network. private var latestNews: List<ArticleHeadline> = emptyList() suspend fun getLatestNews(refresh: Boolean = false): List<ArticleHeadline> { if (refresh || latestNews.isEmpty()) { val networkResult = newsRemoteDataSource.fetchLatestNews() // Thread-safe write to latestNews latestNewsMutex.withLock { this.latestNews = networkResult } } return latestNewsMutex.withLock { this.latestNews } } } なぜ? マルチスレッドで可変な状態を扱うには注意が必要なため。(不整合が起きやすい) アプリのスコープで動かしたい操作を使うときに別のCoroutineScopeを渡して使うことができる class NewsRepository( private val newsRemoteDataSource: NewsRemoteDataSource, private val externalScope: CoroutineScope ) { /* ... */ suspend fun getLatestNews(refresh: Boolean = false): List<ArticleHeadline> { return if (refresh) { externalScope.async { newsRemoteDataSource.fetchLatestNews().also { networkResult -> // Thread-safe write to latestNews. latestNewsMutex.withLock { latestNews = networkResult } } }.await() } else { return latestNewsMutex.withLock { this.latestNews } } } } asyncを使って別のスコープでCoroutiensの処理を行っている。 async is used to start the coroutine in the external scope. await is called on the new coroutine to suspend until the network request comes back and the result is saved to the cache. If by that time the user is still on the screen, then they will see the latest news; if the user moves away from the screen, await is canceled but the logic inside async continues to execute. なぜ?  ユーザーが画面から離れたときに処理をキャンセルせずに処理を続行することができるため。 大きいデータセットはRoomへ、小さいデータセットはDataStoreへ、JSONのようなデータの塊はファイルへ If the data you’re working with needs to survive process death, then you need to store it on disk in one of the following ways: For large datasets that need to be queried, need referential integrity, or need partial updates, save the data in a Room database. In the News app example, the news articles or authors could be saved in the database. For small datasets that only need to be retrieved and set (not queries or updated partially), use DataStore. In the News app example, the user's preferred date format or other display preferences could be saved in DataStore. For chunks of data like a JSON object, use a file. なぜ? 特に言及はなさそうなんですが、仕組み的に適しているからではないかなと思います。 Integration testで外部リソースにアクセスするときは、置き換えて管理できるようにしよう。 Integration tests that access external sources tend to be less deterministic because they need to run on a real device. It's recommended that you execute those tests under a controlled environment to make the integration tests more reliable. For databases, Room allows creating an in-memory database that you can fully control in your tests. To learn more, see the Test and debug your database page. For networking, there are popular libraries such as WireMock or MockWebServer that let you fake HTTP and HTTPS calls and verify that the requests were made as expected. なぜ? 置き換えないと、実デバイスで動かす必要があり、結果が不安定になるため。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む