AndroidでMiAuth実装してみた
リポジトリ
AndroidでMiAuthを実装しました。
手っ取り早くソース寄こせって人はこちらから。 developmentブランチです。
あと、かなりクラスを行ったり来たりするので、コード表示しながら読んでいただければ幸いです。
Misskey Hub
解説では明示されていませんが、カスタムスキーマは現在受け付けておらず、httpsに統一しているそうです(しゅいろママが言ってた)。
トークン取得フロー
MiAuthは、次のようなイメージでアクセストークンを取得します。 OAuth2は未実装なので、詳細については言及しません。
さらに詳細を文字起こしにするとこんな感じ。
(一応MiAuth想定のフロー) 認証開始 -> ViewModel#startAuth -> AuthUseCase#startAuth -> ユーザによりアプリ連携が許可される -> Activity#onNewIntentでコールバックURLのキャッチ -> RegisterAppCallback#onRegistered で受信する -> Interactor#requestToken -> トークンが返ってくる(成功の場合) -> ViewModel で実装し Interactor に渡されている AuthResultCallback を叩いてトークンを ViewModel に渡す -> ViewModel から PreferenceUseCase 等を通じてトークンを保存する
インターフェース定義
AuthActivity
とAuthActivityViewModel
は認証フローの詳細を知らず、認証サーバとのやりとりはすべてAuthUseCase
を実装した各種インタラクタが実行します。
ViewModel はほとんどの場合、Activityからのイベントをインタラクタに通知するだけです。
また、MVVM を採用しているので、ViewModel は各種 UseCase を集約しています。
AuthUseCase
MiAuth以外にも対応できるようにsealed interface
で定義しました。
sealed interface
とすることで、AuthUseCase
を次のようにobject
句で実装しようとしてもエラーになります。
また、どんな認証フローでもstartAuth
を叩くことでフローを開始するように縛ります。
fun foo() { val XxxAuthInteractor = object : AuthUseCase { ... } } //-> This type is sealed, so it can be inherited by only its own nested classes or objects
AuthUseCase
は次のとおり定義します。
sealed interface AuthUseCase { fun startAuth(context: Context, ticket: AuthTicket, resultCallback: AuthResultCallback): RegisterAppCallback fun onDismiss() interface MiAuthUseCase: AuthUseCase interface OAuth2UseCase: AuthUseCase } //ユーザがアプリ連携を許可した際に必要なコールバック //フローにActivity#onNewIntentが絡むため、何かしらの形でインタラクタに通知する仕組みが必要 interface RegisterAppCallback { suspend fun onRegistered(callbackIntent: Intent) suspend fun onFailed(err: Exception) } //トークンをリクエストした際のサーバのレスポンス //取得に成功した場合、tokenが格納される sealed interface AuthResultCallback { fun onAuthFinish(ticket: AuthTicket, token: Token) fun onFailed(err: Exception) interface MiAuthResultCallback: AuthResultCallback interface OAuth2ResultCallback: AuthResultCallback }
AuthTicket
は各種パーミッションやホスト情報等をまとめた単なるデータクラスです。
こちらもsealed
で定義します。
MiAuthInteractor と OAuth2Interactor
AuthUseCase
を実装した具象クラス。
このように認証方式ごとにAuthUseCase
を実装したクラスを作ることで、startAuth
を叩くだけで異なる認証フローを起動できるようにしています(たぶん)。
また、RegisnterAppCallback
は Interector#startAuth
にて、AuthResultCallback
は ViewModel にて実装しています。
MiAuthInteractor github.com
AuthActivityViewModel github.com
雑なまとめ
- 一部甘い部分(クラス変数にしている部分とか)があるけどそこはスルーで。
- 別にMiAuthでトークン取得するテストなだけならここまでやる必要はない。。。
まとメモ プライバシーポリシー
まとメモ プライバシーポリシー
第三者に個人を特定できる情報を提供することはありません。
個人情報の管理には最新の注意を払い、以下に掲げた通りに扱います。
サポート時
サポートメールに、問題解決のための端末種類、OSバージョン等が本文として記述されます。
個人を特定できる情報は一切送信されません。
データ解析
アプリの利便性向上のため、匿名で個人を特定できない範囲で最新の注意を払い、アクセス解析をしております。
例えばアプリのクラッシュ時にどんな原因でクラッシュしたかを匿名で送信して、バグの素早い修正に役立たせております。
免責事項
利用上の不具合・不都合に対して可能な限りサポートを行っておりますが、利用者が本アプリを利用して生じた損害に関して、開発元は責任を負わないものとします。
CoordinatorLayout+BottomNavigationView+NestedScrollView+その他諸々で画面構築してみた
次のぎじゅつを使って画面構築してみたのでメモ
- 見た目はToolbar+コンテンツ表示Fragment+BottomNavigationViewの画面構成
- コンテンツ表示Fragmentは、BottomNavigationViewの選択により入れ替える
- Toolbarはコンテンツ表示Fragmentのスクロール動作により隠れるようにする
- Toolbarのタイトルを、Navigationを使ってBottomNavigationViewに連動させる
- BottomNavigationViewは固定する
BottomNavigationView & Toolbar & Navigation 間の連携に微妙なコツがあったり、 そもFrameLayoutであることを考慮したり、いろいろハマりまくった。
スクロールでBottomNavigationViewも隠したい場合は、カスタムBehavior定義して設定してやればおけ。 個人的には画面切り替えにスクロール動作が増えてしまうので、あえて固定する方法をとった。
- 動作プレビュー
- レイアウトXML
- 実装編
- NavigationとToolbar/BottomNavigationViewの連携の実装
- コンテンツのレイアウトを作成する
- BottomNavigationViewのMenuを作成する
- Navigation graphを作成する
- FragmentContainerViewにnav_graphをセット
- スクロール時にToolbarを隠す動作の実装
- 基本となる画面構造
- NestedScrollView内にRecyclerViewを配置する場合の注意
- NavigationとToolbar/BottomNavigationViewの連携の実装
- 参考
時刻をパラメータに使用する場合の実装メモ
時刻を扱う場合に、テスト性を持たせるための実装メモ。
インターフェースを介して時刻を実装してやる。
// Kotlin import java.util.* interface SystemClock { fun getTimeMillis(): Long fun getLocale(): Locale fun getTimeZone(): TimeZone } // 使う側の例 import your.package.SystemClock fun setTime(d: SystemClock) { ... } setTime(object : SystemClock { override fun getTimeMillis(): Long { TODO("Not yet implemented") } override fun getLocale(): Locale { TODO("Not yet implemented") } override fun getTimeZone(): TimeZone { TODO("Not yet implemented") } }
テストのときはエッジケースや定数を実装し、本番ではDate()
等を実装すれば( ΦωΦ)σヨシッ!