- 投稿日:2021-03-02T22:02:38+09:00
ロガーライブラリ「Timber」のセットアップ&操作方法(Kotlin)
はじめに
Androidアプリ開発で使えるロガーライブラリを教えてもらったので、導入することにしました。
「Timber」とは?
Android向けのロガーライブラリです。
環境
- OS:macOS Big Sur 11.1
- Android Studio:4.1.2
- Kotlin:1.4.20
- Gradle:6.8
- Gradle plugin:4.1.2
- Timber:4.7.1
セットアップ
Timberのインストール
appフォルダ配下の「build.gradle」に依存関係を追加します。
/app/build.gradledependencies { + implementation 'com.jakewharton.timber:timber:4.7.1' }アプリケーションクラスでの初期化
アプリケーションクラスの
onCreate()
でTree
インスタンスをインストールします。+ import timber.log.Timber class UhooiPicBookApp : Application() { override fun onCreate() { super.onCreate() + configureTimber() } + private fun configureTimber() { + if (BuildConfig.DEBUG) { + Timber.plant(Timber.DebugTree()) + } + } }私はデバッグ時のみTimberを使うようにしています。
これでTimberのセットアップは完了です。
使い方
Timberの各メソッドを呼び出すことで、Logcatにログを出力します。
MainActivity.ktimport timber.log.Timber class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) Timber.v("Verbose") Timber.d("Debug") Timber.i("Info") Timber.e("Error") Timber.w("Warning") } }Logcatの出力結果2021-03-02 21:58:46.736 29451-29451/com.theuhooi.uhooipicbook V/MainActivity: Verbose 2021-03-02 21:58:46.736 29451-29451/com.theuhooi.uhooipicbook D/MainActivity: Debug 2021-03-02 21:58:46.736 29451-29451/com.theuhooi.uhooipicbook I/MainActivity: Info 2021-03-02 21:58:46.736 29451-29451/com.theuhooi.uhooipicbook E/MainActivity: Error 2021-03-02 21:58:46.737 29451-29451/com.theuhooi.uhooipicbook W/MainActivity: Warningおわりに
かんたんにログ出力を実現できました!
どのようなアプリの開発でも有用だと思います。参考リンク
- JakeWharton/timber: A logger with a small, extensible API which provides utility on top of Android's normal Log class.
- timber/ExampleApp.java at master · JakeWharton/timber
- Timber (Timber 3.0.1 API)
- Setup Timber · Issue #80 · uhooi/UhooiPicBook-Android
- Setup Timber #80 by omooooori · Pull Request #85 · uhooi/UhooiPicBook-Android
- Refactor Timber by uhooi · Pull Request #104 · uhooi/UhooiPicBook-Android
- 投稿日:2021-03-02T21:16:23+09:00
メモリリーク検出ライブラリ「LeakCanary」のセットアップ&使い方
はじめに
Androidアプリ開発で使えるメモリリーク検出ライブラリを教えてもらったので、導入することにしました。
「LeakCanary」とは?
Android向けのメモリリーク検出ライブラリです。
環境
- OS:macOS Big Sur 11.1
- Android Studio:4.1.2
- Kotlin:1.4.20
- Gradle:6.8
- Gradle plugin:4.1.2
- LeakCanary:2.6
セットアップ
LeakCanaryのインストール
appフォルダ配下の「build.gradle」に依存関係を追加します。
/app/build.gradleandroid { defaultConfig { + // For LeakCanary in instrumentation tests + testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + testInstrumentationRunnerArgument "listener", "leakcanary.FailTestOnLeakRunListener" } } dependencies { + // LeakCanary + def leakcanary_version = '2.6' + debugImplementation "com.squareup.leakcanary:leakcanary-android:$leakcanary_version" + androidTestImplementation "com.squareup.leakcanary:leakcanary-android-instrumentation:$leakcanary_version" }インストルメンテッドテストでLeakCanaryを実行しない場合は
debugImplementation
の追加のみでOKです。これだけでLeakCanaryのセットアップは完了です。
使い方
メモリリーク検出の開始
Android Studioから開発しているアプリを実行するだけで、メモリリークの検出が始まります。
Logcatに以下の文字列が出力されていたら、無事に実行されています。
D LeakCanary: LeakCanary is running and ready to detect leaksメモリリーク検出の確認
私が開発しているアプリではメモリリークが検出されませんでした。
おわりに
かんたんにAndroidアプリのメモリリークを検出できました!
どのようなアプリの開発でも有用だと思います。参考リンク
- 投稿日:2021-03-02T20:15:19+09:00
Android開発メモ① Activity ドキュメントの自分用まとめ
Android開発メモ① Activity ドキュメントの自分用まとめ
要はアクティビティの概要のまとめ
アプリアーキテクチャガイド
またの機会に
コンセプト
- main() メソッドで起動するアプリをプログラミングする際の枠組みとは異なり、Android システムでは、ライフサイクルのそれぞれの段階に対応するコールバック メソッドを呼び出すことにより、Activity インスタンス内のコードが開始されます。
- 一つのアプリが別のアプリを呼び出しやすくする、モバイル特有の画面遷移を実現
- アクティビティにより、アプリがUIを描画するウィンドウが用意される。
- 通常ウィンドウが画面全体だが、小さくなったり、重なったりする
- アクティビティ関係は緩やかな結びつきで依存関係はほとんどない
- アクティビティを利用するにはマニフェストに登録する必要がある
マニフェストの設定
<manifest ... > <application ... > <activity android:name=".ExampleActivity" /> ... </application ... > ... </manifest >インテントフィルタ
- 明示的なリクエスト
- 「Gmail アプリでのメール送信アクティビティの開始」
- 暗黙的なリクエスト
- 「この作業を行えるアクティビティでのメール送信画面の開始」
<activity android:name=".ExampleActivity" android:icon="@drawable/app_icon"> <intent-filter> <action android:name="android.intent.action.SEND" /> <category android:name="android.intent.category.DEFAULT" /> <data android:mimeType="text/plain" /> </intent-filter> </activity>パーミッション
マニフェスト内で
<uses-permission>
することでアクティビティの呼び出し権限を制御できるライフサイクル
おなじみのonCreate()など
ライフサイクルの項でまとめる
- 投稿日:2021-03-02T19:31:00+09:00
ksnctf Jewel writeup
本物のAndroidマルウェアの解析訓練にもなる良問なので, ksnctf Jewel の writeup を残そうと思う。
ノーヒントで Jewel.apk が与えられるので JADX でデコンパイルする
AndroidManifest.xmlからエントリーポイントをさがす
<activity android:label="@string/app_name" android:name=".JewelActivity">JewelActivityから動きそう
JewelActivity の OnCreate() を見るとすぐに AES とわかる
Cipher instance2 = Cipher.getInstance("AES/CBC/PKCS5Padding");AES鍵は?
String deviceId = ((TelephonyManager) getSystemService("phone")).getDeviceId(); try { MessageDigest instance = MessageDigest.getInstance("SHA-256"); instance.update(deviceId.getBytes("ASCII")); String bigInteger = new BigInteger(instance.digest()).toString(16); if (!deviceId.substring(0, 8).equals("99999991")) { new AlertDialog.Builder(this).setMessage("Your device is not supported").setCancelable(false).setPositiveButton("OK", new C0001b(this)).show(); } else if (!bigInteger.equals("356280a58d3c437a45268a0b226d8cccad7b5dd28f5d1b37abf1873cc426a8a5")) { new AlertDialog.Builder(this).setMessage("You are not a valid user").setCancelable(false).setPositiveButton("OK", new C0000a(this)).show(); } else { InputStream openRawResource = getResources().openRawResource(R.raw.jewel_c); byte[] bArr = new byte[openRawResource.available()]; openRawResource.read(bArr); SecretKeySpec secretKeySpec = new SecretKeySpec(("!" + deviceId).getBytes("ASCII"), "AES");getDeviceId()でIMEIを取得している
!+IMEIがAES鍵だ!ただし,動作に条件があり,
IMEIの先頭8文字 = 99999991
IMEIのSHA-256ハッシュ = 356280a58d3c437a45268a0b226d8cccad7b5dd28f5d1b37abf1873cc426a8a5
IMEIは15文字なので
999999910000000~999999919999999まで総当たりでハッシュ計算してターゲットのIMEIを計算するIVは?
IvParameterSpec ivParameterSpec = new IvParameterSpec("kLwC29iMc4nRMuE5".getBytes());ペイロードは?
InputStream openRawResource = getResources().openRawResource(R.raw.jewel_c);問題データ Jewel.apk の拡張子を zip にして ペイロードを取り出す
※なお,もしこれが本物の Android マルウェアならば,ここからが本番。
- 投稿日:2021-03-02T18:30:48+09:00
Xamarin.Androidで動的に追加したコントロールの文字や背景の色を変える
最近、目標達成スケジュールというアプリを作りました。
これを作るにあたり、調べてもなかなか情報が見つからないことがあったので、同じようなところで困っている人のために共有しようと思います。間違った知識、非効率的な方法を紹介しているかもしれませんが、一時お付き合いください。
環境
windows10
VisualStudio2019
Xamarin.Androidコントロールを追加する
まずはコントロールを追加しましょう。
コントロールを追加する方法は、XMLファイルで定義したコントロールをCSファイルで利用する方法とあまり変わりません。まずレイアウトを作成し、そのレイアウトの中にコントロールを追加します。
var ll = new LinearLayout(this) { Orientation = Orientation.Vertical,//縦並びにする LayoutParameters = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MatchParent, ViewGroup.LayoutParams.MatchParent) }; var title = new TextView(this) { Text = "タイトル", LayoutParameters = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MatchParent, ViewGroup.LayoutParams.WrapContent) }; ll.AddView(title);//これでレイアウト内にtitleテキストが表示されるこれをSetContentViewの引数に指定するだけです。
LayoutParamsは縦横の大きさを指定します。MatchParentとWrapContent以外にも値を直接指定することができます。自分は常に画面サイズの何分の1を維持したいときなどに使っています。
コントロールの色を変える
こっちがこの記事のメインです。プログラムからコントロールを追加する方法はググれば簡単に見つかるのですが、探し方が悪かったのか語る必要もないほど常識的なことなのか、色を指定する方法がなかなか見つかりませんでした。同じところで迷っている人がもしいれば、参考にしてください。
var title = new TextView(this) { Text = "タイトル", LayoutParameters = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MatchParent, ViewGroup.LayoutParams.WrapContent) }; title.SetTextColor(Android.Graphics.Color.Argb(200, 200, 0, 0));//テキストの色 title.SetBackgroundColor(Android.Graphics.Color.Argb(200, 0, 200, 0));//背景の色 ll.AddView(title);これだけでOKです。
- 投稿日:2021-03-02T18:26:14+09:00
Wi-Fi経由でadb接続したときのエラーへの対処
経緯
WiFi経由でスマホ(HUAWEI P20 Lite)とパソコン(Windows)をadb接続しようとしたものの、なかなかうまくいかなかったので、僕が直面したエラーとその解決方法について書いていきます。
以下、開発者向けオプションは有効化してあるものとして話を進めていきます。基本的な手順
- スマホとパソコンを同じWi-Fiネットワークに接続
- USBケーブルでスマホをパソコンを接続
adb tcpip 5555
をコマンドプロンプトで実行- USBを切り離す
- スマホのIPアドレスを調べ、
adb connect [IPアドレス]
を実行。adb devices
でadb接続できていることを確認うまくいかなかったこと
まずは手順通りに進めていったところ、手順の5で
adb connect [IPアドレス]
を実行したときに、対象のコンピューターによって拒否されたため、接続できませんでした。(10061)となり、接続できませんでした。
そこで、手順4でUSBを切り離さずに、USBで接続したままadb connect [IPアドレス]
を実行したところ、その時点ではconnect to [IPアドレス]:5555と接続できたものの、その後USBを切り離してから
adb devices
で確認したところ、List of devices attached [IPアドレス]:5555 offlineと表示され、うまく接続できませんでした。
同様のエラーについて検索したところ色々な情報が見つかったのですが、僕の場合はどれもうまくいきませんでした。
adb device offline with ADB wireless - Stack Overflow
adb devicesでofflineと表示される | 悠久無窮的生活
android — Android StudioワイヤレスADBエラー(10061)
AndroidアプリをWiFi経由でデバッグ - Qiita解決方法
スマホの設定の
設定 > システム > 開発者向けオプション > 充電専用モードでADBデバッグを許可する
をONにして手順通りに実行したところ、無事にWi-Fi経由でadb接続することができました。
- 投稿日:2021-03-02T17:10:32+09:00
公開しているAndroidアプリのアップデートの手順
公開しているAndroidアプリのアップデートの手順
自作のアプリをアップデートする機会があったので、2021年2月時点でのアップデートの手順を公開します。
最初はAndroidStudioでの手順です。
まず、プロジェクトウィンドウ内の「Gradle Script」をクリックして、「buile.gradle(Module:アプリ名.app」を開きます。
仮に初めてアップデートするアプリであれば、「versionCode」が"1"、「versionName」が"1.0"になっているので、これらを変更します。
(今回の場合では「versionCode」を"2", 「versionName」を"1.1"などが適当だと思います。次回は「versionCode」を"3",「versionName」を"1.2"にしようと思います。)
変更したら、画面右上の「Sync Now」をクリックします。
次にaabファイルの更新をします。
メニューバーの「Build」をクリックして、「Generate Signed Bundle / APK」をクリックします。
「Generate Signed Bundle or APK」ウィンドウで「Android App Bundle」を選択して、「Next」をクリックします。
(apkファイルを使用する方は「APK」を選択してください)
keyの場所を前回のリリース時から変更していなければ、「Key store password」と「Key password」を入力してから、「Next」をクリックします。
変更していれば、「Key store path」の「Choose existing」をクリックして、keyの場所を選択した上で、「Key store password」と「Key password」を入力してから、「Next」をクリックします。
最後に、前回リリース後にプロジェクトの場所を変更していなければ、「Finish」をクリックします。
変更していれば、「Destination Folder」のフォルダアイコンをクリックして、場所を選択した上で、「Finish」をクリックします。
次はGoogle Play Consoleでの手順です。
Google Play Consoleの「すべてのアプリ」を選択して、アップデートしたいアプリを選びます。
そうすると、そのアプリのダッシュボードに画面が遷移します。
「リリース」の「製品版」をクリックします。
画面右上の「新しいリリースを作成」をクリックして、aabファイルをアップロードします。
アップロードは、画面中心部の「App BundleとAPK」の枠内にある「アップロード」をクリックして行います。(ファイルをドラッグアンドドロップでも可)
アップロードすると、ファイル名やバージョン名が表示されます。
ページの下側に進むと、「リリースノート」があります。「リリースノート」には、このバージョンについての変更点を記載できます。
画面右下の「リリースのレビュー」をクリックして、変更内容を確認します。
変更内容に問題がなければ、画面右下の「製品版としての公開を開始」をクリックします。
以上で、Androidアプリのアップデートは完了です。
アップデートの審査で数日かかります。(筆者は2日で審査完了しました。)もしよろしければ、今回アップデートしたアプリのURLを載せるので、買ってください?(110円です。)
https://play.google.com/store/apps/details?id=com.konno_siki.drawing2
子供向けのお絵かきアプリですが、端末に保存している画像を画面に表示して、マーキングする使い方もできます。
今回の投稿で使用した画像も、PCでスクショした画像を端末にメールで送って、このアプリで個人情報にあたる箇所を塗りつぶしてから、端末からPCにメールで画像を送り返しました。広告はありません。
よろしくお願いします?
- 投稿日:2021-03-02T11:00:51+09:00
Android Flowを自分用にメモってみた
念のため前書き
この記事は公式のドキュメントと
Mori Atsushiさんの素晴らしい記事を
自分向けに雑にまとめた物です。Flowの超基本的なところ
Flow(Flow,SharedFlow,StateFlowを纏めて呼んでる)には
コールドストリームとホットストリームに分けられる。
普通のFlowはコールドストリーム。
SharedFlow, StateFlowはホットストリーム。Flowのメリット
普通のFlowの使い方
基本の使い方
出力にはemit()を使用する
class NewsRemoteDataSource( private val newsApi: NewsApi, private val refreshIntervalMs: Long = 5000 ) { val latestNews: Flow<List<ArticleHeadline>> = flow { while(true) { val latestNews = newsApi.fetchLatestNews() emit(latestNews) // Emits the result of the request to the flow delay(refreshIntervalMs) // Suspends the coroutine for some time } } } // Interface that provides a way to make network requests with suspend functions interface NewsApi { suspend fun fetchLatestNews(): List<ArticleHeadline> }Repositoryで値を変換することも可能
class NewsRepository( private val newsRemoteDataSource: NewsRemoteDataSource, private val userData: UserData ) { /** * Returns the favorite latest news applying transformations on the flow. * These operations are lazy and don't trigger the flow. They just transform * the current value emitted by the flow at that point in time. */ val favoriteLatestNews: Flow<List<ArticleHeadline>> = newsRemoteDataSource.latestNews // Intermediate operation to filter the list of favorite topics .map { news -> news.filter { userData.isFavoriteTopic(it) } } // Intermediate operation to save the latest news in the cache .onEach { news -> saveInCache(news) } }リッスンの開始にはcollectを使用する。
class LatestNewsViewModel( private val newsRepository: NewsRepository ) : ViewModel() { init { viewModelScope.launch { // Trigger the flow and consume its elements using collect newsRepository.favoriteLatestNews.collect { favoriteNews -> // Update View with the latest favorite news } } } }収集中のコルーチンをキャンセルした場合にデータストリームが終了する。
例外のキャッチにはcatchを使用する
class NewsRepository(...) { val favoriteLatestNews: Flow<List<ArticleHeadline>> = newsRemoteDataSource.latestNews .map { news -> news.filter { userData.isFavoriteTopic(it) } } .onEach { news -> saveInCache(news) } // If an error happens, emit the last cached values .catch { exception -> emit(lastCachedNews()) } }flowOnでCoroutineContextを切り替える
.flowOn(defaultDispatcher)callbackFlowを使用してコールバックベースのAPIをFlowに変換する
class FirestoreUserEventsDataSource( private val firestore: FirebaseFirestore ) { // Method to get user events from the Firestore database fun getUserEvents(): Flow<UserEvents> = callbackFlow { // Reference to use in Firestore var eventsCollection: CollectionReference? = null try { eventsCollection = FirebaseFirestore.getInstance() .collection("collection") .document("app") } catch (e: Throwable) { // If Firebase cannot be initialized, close the stream of data // flow consumers will stop collecting and the coroutine will resume close(e) } // Registers callback to firestore, which will be called on new events val subscription = eventsCollection?.addSnapshotListener { snapshot, _ -> if (snapshot == null) { return@addSnapshotListener } // Sends events to the flow! Consumers will get the new events try { offer(snapshot.getEvents()) } catch (e: Throwable) { // Event couldn't be sent to the flow } } // The callback inside awaitClose will be executed when the flow is // either closed or cancelled. // In this case, remove the callback from Firestore awaitClose { subscription?.remove() } } }send()を利用して別のCoroutineContextから or offer()を利用してCoroutineの外部から値を出力できる。
普通のFlowの注意点
このようなコードがあった場合
val flow = flow { println("emit!") emit(1) } flow.onEach { println("onEach1:$it") }.launchIn(GlobalScope) flow.onEach { println("onEach2:$it") }.launchIn(GlobalScope) runBlocking { delay(100) } // 処理が終わるまでちょっと待つemit! emit! onEach1:1 onEach2:1emit()が起動した回数分呼ばれる。
そのため、以下のように重たい処理をしたい場合には不向き。val flow = flow { // 重たい処理(API call等) }.map { // 重たい処理(API call等) } // 重たい処理が毎回呼ばれる flow.launchIn(GlobalScope)また、コールドFlowのためflowに対して直接emitすることは出来ない。
val flow = flowOf(1) flow.emit(2) // compile error!!普通のFlowの問題点を解決するために出てきたのがSharedFlow
SharedFlowの作り方にはMutableSharedFlowとshareIn()がある。
MutableSharedFlowの使い方
val mutableSharedFlow = MutableSharedFlow<Int>() mutableSharedFlow.onEach { println("onEach1:$it") }.launchIn(GlobalScope) mutableSharedFlow.onEach { println("onEach2:$it") }.launchIn(GlobalScope) runBlocking { mutableSharedFlow.emit(1) mutableSharedFlow.emit(2) delay(100) // 処理が終わるまでちょっと待つ }ホットFlowのため、外部からemit()することが可能。
公式では以下のようなサンプルコードを載せている
// Class that centralizes when the content of the app needs to be refreshed class TickHandler( private val externalScope: CoroutineScope, private val tickIntervalMs: Long = 5000 ) { // Backing property to avoid flow emissions from other classes private val _tickFlow = MutableSharedFlow<Unit>(replay = 0) val tickFlow: SharedFlow<Event<String>> = _tickFlow init { externalScope.launch { while(true) { _tickFlow.emit(Unit) delay(tickIntervalMs) } } } } class NewsRepository( ..., private val tickHandler: TickHandler, private val externalScope: CoroutineScope ) { init { externalScope.launch { // Listen for tick updates tickHandler.tickFlow.collect { refreshLatestNews() } } } suspend fun refreshLatestNews() { ... } ... }replay を使用すると、以前に出力された複数の値を新しいサブスクライバに再送信できる。
onBufferOverflow を使用すると、バッファが送信アイテムでいっぱいになったときのポリシーを指定できる。
デフォルト値は BufferOverflow.SUSPEND で、呼び出し元を停止します。他のオプションには、DROP_LATEST と DROP_OLDEST がある。shareInを使用してコールドFlowをホットFlow化する
shareInは通常のFlowをSharedFlowに変換するoperator。
shareInを使用する場合、次の情報を渡す必要がある。
- Flow の共有に使用する CoroutineScope。このスコープは、共有 Flow を必要な期間存続させるために、どのコンシューマよりも長く存続させる必要がある。
- 新しいコレクタそれぞれに対してリプレイするアイテムの数。
- 開始動作ポリシー。shareInを使用することで、コールドFlowとは違い重たい処理を行いやすくなる。
val flow = flow { println("emit!") emit(1) } val sharedFlow = flow.shareIn(GlobalScope, SharingStarted.Eagerly) sharedFlow.onEach { println("onEach1:$it") }.launchIn(GlobalScope) sharedFlow.onEach { println("onEach2:$it") }.launchIn(GlobalScope) runBlocking { delay(100) } // 処理が終わるまでちょっと待つemit! // emitが一度しか呼ばれていない onEach2:1 onEach1:1開始動作ポリシーには以下の3つがある。
- SharingStarted.WhileSubscribed() 開始ポリシーは、アクティブなサブスクライバが存在する間は、
アップストリーム プロデューサをアクティブに保つ
- SharingStarted.Eagerlyは他の開始ポリシーとして、プロデューサをすぐに開始する
- SharingStarted.Lazilyは最初のサブスクライバが現れたときに共有を開始し、Flow を永続的にアクティブに保つStateFlow
StateFlowもホットストリームです。
状態保持を行うための特別なSharedFlowのイメージ。
MutableStateFlow、stateIn()を使用して作成可能。
SharedFlowと違い、
- 初期値が必須
- launchInしたタイミングで直近の値が1件流れてくる
- 値の設定はvalueで行え、coroutines scopeは必要ない
- 同じ値は流れない
- 連続で値が変更されると最後の値のみ流れてくる基本の使い方
class LatestNewsViewModel( private val newsRepository: NewsRepository ) : ViewModel() { // Backing property to avoid state updates from other classes private val _uiState = MutableStateFlow(LatestNewsUiState.Success(emptyList())) // The UI collects from this StateFlow to get its state updates val uiState: StateFlow<LatestNewsUiState> = _uiState init { viewModelScope.launch { newsRepository.favoriteLatestNews // Update View with the latest favorite news // Writes to the value property of MutableStateFlow, // adding a new element to the flow and updating all // of its collectors .collect { favoriteNews -> _uiState.value = LatestNewsUiState.Success(favoriteNews) } } } } // Represents different states for the LatestNews screen sealed class LatestNewsUiState { data class Success(news: List<ArticleHeadline>): LatestNewsUiState() data class Error(exception: Throwable): LatestNewsUiState() }ViewからStateFlowを参照する場合
class LatestNewsActivity : AppCompatActivity() { private val latestNewsViewModel = // getViewModel() override fun onCreate(savedInstanceState: Bundle?) { ... // This coroutine will run the given block when the lifecycle // is at least in the Started state and will suspend when // the view moves to the Stopped state lifecycleScope.launchWhenStarted { // Triggers the flow and starts listening for values latestNewsViewModel.uiState.collect { uiState -> // New value received when (uiState) { is LatestNewsUiState.Success -> showFavoriteNews(uiState.news) is LatestNewsUiState.Error -> showError(uiState.exception) } } } } }普通のFlowをリッスンする場合と変わらない。
launchWhenって何
ライフサイクルの状態によって動作を開始・終了するcoroutine scope。
launchWhenStartedの場合、クラスが起動した場合に実行される。
詳しくはここFlowをStateFlowに変換するにはstateInを使用する
StateFlowの動作を停止させるには
launchWhenStarted を使用して Flow を収集する上記の例では、View がバックグラウンドに移動して Flow 収集をトリガーするコルーチンが停止しても、下層のプロデューサはアクティブなまま。
手動で停止させるには
class LatestNewsActivity : AppCompatActivity() { ... // Coroutine listening for UI states private var uiStateJob: Job? = null override fun onStart() { super.onStart() // Start collecting when the View is visible uiStateJob = lifecycleScope.launch { latestNewsViewModel.uiState.collect { uiState -> ... } } } override fun onStop() { // Stop collecting when the View goes to the background uiStateJob?.cancel() super.onStop() } }Viewが表示されていないときに停止させるにはasLiveData()でLiveDataに変更するという手もある
class LatestNewsActivity : AppCompatActivity() { ... override fun onCreate(savedInstanceState: Bundle?) { ... latestNewsViewModel.uiState.asLiveData().observe(owner = this) { state -> // Handle UI state } } }
- 投稿日:2021-03-02T11:00:51+09:00
Android Flow, SharedFlow, StateFlowを自分用にメモる
念のため前書き
この記事は公式のドキュメントと
Mori Atsushiさんの素晴らしい記事を
自分向けに雑にまとめた物です。Flowの超基本的なところ
Flow(Flow,SharedFlow,StateFlowを纏めて呼んでる)には
コールドストリームとホットストリームに分けられる。
普通のFlowはコールドストリーム。
SharedFlow, StateFlowはホットストリーム。Flowのメリット
普通のFlowの使い方
基本の使い方
出力にはemit()を使用する
class NewsRemoteDataSource( private val newsApi: NewsApi, private val refreshIntervalMs: Long = 5000 ) { val latestNews: Flow<List<ArticleHeadline>> = flow { while(true) { val latestNews = newsApi.fetchLatestNews() emit(latestNews) // Emits the result of the request to the flow delay(refreshIntervalMs) // Suspends the coroutine for some time } } } // Interface that provides a way to make network requests with suspend functions interface NewsApi { suspend fun fetchLatestNews(): List<ArticleHeadline> }Repositoryで値を変換することも可能
class NewsRepository( private val newsRemoteDataSource: NewsRemoteDataSource, private val userData: UserData ) { /** * Returns the favorite latest news applying transformations on the flow. * These operations are lazy and don't trigger the flow. They just transform * the current value emitted by the flow at that point in time. */ val favoriteLatestNews: Flow<List<ArticleHeadline>> = newsRemoteDataSource.latestNews // Intermediate operation to filter the list of favorite topics .map { news -> news.filter { userData.isFavoriteTopic(it) } } // Intermediate operation to save the latest news in the cache .onEach { news -> saveInCache(news) } }リッスンの開始にはcollectを使用する。
class LatestNewsViewModel( private val newsRepository: NewsRepository ) : ViewModel() { init { viewModelScope.launch { // Trigger the flow and consume its elements using collect newsRepository.favoriteLatestNews.collect { favoriteNews -> // Update View with the latest favorite news } } } }収集中のコルーチンをキャンセルした場合にデータストリームが終了する。
例外のキャッチにはcatchを使用する
class NewsRepository(...) { val favoriteLatestNews: Flow<List<ArticleHeadline>> = newsRemoteDataSource.latestNews .map { news -> news.filter { userData.isFavoriteTopic(it) } } .onEach { news -> saveInCache(news) } // If an error happens, emit the last cached values .catch { exception -> emit(lastCachedNews()) } }flowOnでCoroutineContextを切り替える
.flowOn(defaultDispatcher)callbackFlowを使用してコールバックベースのAPIをFlowに変換する
class FirestoreUserEventsDataSource( private val firestore: FirebaseFirestore ) { // Method to get user events from the Firestore database fun getUserEvents(): Flow<UserEvents> = callbackFlow { // Reference to use in Firestore var eventsCollection: CollectionReference? = null try { eventsCollection = FirebaseFirestore.getInstance() .collection("collection") .document("app") } catch (e: Throwable) { // If Firebase cannot be initialized, close the stream of data // flow consumers will stop collecting and the coroutine will resume close(e) } // Registers callback to firestore, which will be called on new events val subscription = eventsCollection?.addSnapshotListener { snapshot, _ -> if (snapshot == null) { return@addSnapshotListener } // Sends events to the flow! Consumers will get the new events try { offer(snapshot.getEvents()) } catch (e: Throwable) { // Event couldn't be sent to the flow } } // The callback inside awaitClose will be executed when the flow is // either closed or cancelled. // In this case, remove the callback from Firestore awaitClose { subscription?.remove() } } }send()を利用して別のCoroutineContextから or offer()を利用してCoroutineの外部から値を出力できる。
普通のFlowの注意点
このようなコードがあった場合
val flow = flow { println("emit!") emit(1) } flow.onEach { println("onEach1:$it") }.launchIn(GlobalScope) flow.onEach { println("onEach2:$it") }.launchIn(GlobalScope) runBlocking { delay(100) } // 処理が終わるまでちょっと待つemit! emit! onEach1:1 onEach2:1emit()が起動した回数分呼ばれる。
そのため、以下のように重たい処理をしたい場合には不向き。val flow = flow { // 重たい処理(API call等) }.map { // 重たい処理(API call等) } // 重たい処理が毎回呼ばれる flow.launchIn(GlobalScope)また、コールドFlowのためflowに対して直接emitすることは出来ない。
val flow = flowOf(1) flow.emit(2) // compile error!!普通のFlowの問題点を解決するために出てきたのがSharedFlow
SharedFlowの作り方にはMutableSharedFlowとshareIn()がある。
SharedFlowはemit()が一度しか呼ばれないため、
複数箇所で起動することに適している。
だから「Shared」?MutableSharedFlowの使い方
val mutableSharedFlow = MutableSharedFlow<Int>() mutableSharedFlow.onEach { println("onEach1:$it") }.launchIn(GlobalScope) mutableSharedFlow.onEach { println("onEach2:$it") }.launchIn(GlobalScope) runBlocking { mutableSharedFlow.emit(1) mutableSharedFlow.emit(2) delay(100) // 処理が終わるまでちょっと待つ }ホットFlowのため、外部からemit()することが可能。
公式では以下のようなサンプルコードを載せている
// Class that centralizes when the content of the app needs to be refreshed class TickHandler( private val externalScope: CoroutineScope, private val tickIntervalMs: Long = 5000 ) { // Backing property to avoid flow emissions from other classes private val _tickFlow = MutableSharedFlow<Unit>(replay = 0) val tickFlow: SharedFlow<Event<String>> = _tickFlow init { externalScope.launch { while(true) { _tickFlow.emit(Unit) delay(tickIntervalMs) } } } } class NewsRepository( ..., private val tickHandler: TickHandler, private val externalScope: CoroutineScope ) { init { externalScope.launch { // Listen for tick updates tickHandler.tickFlow.collect { refreshLatestNews() } } } suspend fun refreshLatestNews() { ... } ... }replay を使用すると、以前に出力された複数の値を新しいサブスクライバに再送信できる。
onBufferOverflow を使用すると、バッファが送信アイテムでいっぱいになったときのポリシーを指定できる。
デフォルト値は BufferOverflow.SUSPEND で、呼び出し元を停止します。他のオプションには、DROP_LATEST と DROP_OLDEST がある。shareInを使用してコールドFlowをホットFlow化する
shareInは通常のFlowをSharedFlowに変換するoperator。
shareInを使用する場合、次の情報を渡す必要がある。
- Flow の共有に使用する CoroutineScope。このスコープは、共有 Flow を必要な期間存続させるために、どのコンシューマよりも長く存続させる必要がある。
- 新しいコレクタそれぞれに対してリプレイするアイテムの数。
- 開始動作ポリシー。shareInを使用することで、コールドFlowとは違い重たい処理を行いやすくなる。
val flow = flow { println("emit!") emit(1) } val sharedFlow = flow.shareIn(GlobalScope, SharingStarted.Eagerly) sharedFlow.onEach { println("onEach1:$it") }.launchIn(GlobalScope) sharedFlow.onEach { println("onEach2:$it") }.launchIn(GlobalScope) runBlocking { delay(100) } // 処理が終わるまでちょっと待つemit! // emitが一度しか呼ばれていない onEach2:1 onEach1:1開始動作ポリシーには以下の3つがある。
- SharingStarted.WhileSubscribed() 開始ポリシーは、アクティブなサブスクライバが存在する間は、
アップストリーム プロデューサをアクティブに保つ
- SharingStarted.Eagerlyは他の開始ポリシーとして、プロデューサをすぐに開始する
- SharingStarted.Lazilyは最初のサブスクライバが現れたときに共有を開始し、Flow を永続的にアクティブに保つStateFlow
StateFlowもホットストリームです。
状態保持を行うための特別なSharedFlowのイメージ。
MutableStateFlow、stateIn()を使用して作成可能。
SharedFlowと違い、
- 初期値が必須
- launchInしたタイミングで直近の値が1件流れてくる
- 値の設定はvalueで行え、coroutines scopeは必要ない
- 同じ値は流れない
- 連続で値が変更されると最後の値のみ流れてくる基本の使い方
class LatestNewsViewModel( private val newsRepository: NewsRepository ) : ViewModel() { // Backing property to avoid state updates from other classes private val _uiState = MutableStateFlow(LatestNewsUiState.Success(emptyList())) // The UI collects from this StateFlow to get its state updates val uiState: StateFlow<LatestNewsUiState> = _uiState init { viewModelScope.launch { newsRepository.favoriteLatestNews // Update View with the latest favorite news // Writes to the value property of MutableStateFlow, // adding a new element to the flow and updating all // of its collectors .collect { favoriteNews -> _uiState.value = LatestNewsUiState.Success(favoriteNews) } } } } // Represents different states for the LatestNews screen sealed class LatestNewsUiState { data class Success(news: List<ArticleHeadline>): LatestNewsUiState() data class Error(exception: Throwable): LatestNewsUiState() }ViewからStateFlowを参照する場合
class LatestNewsActivity : AppCompatActivity() { private val latestNewsViewModel = // getViewModel() override fun onCreate(savedInstanceState: Bundle?) { ... // This coroutine will run the given block when the lifecycle // is at least in the Started state and will suspend when // the view moves to the Stopped state lifecycleScope.launchWhenStarted { // Triggers the flow and starts listening for values latestNewsViewModel.uiState.collect { uiState -> // New value received when (uiState) { is LatestNewsUiState.Success -> showFavoriteNews(uiState.news) is LatestNewsUiState.Error -> showError(uiState.exception) } } } } }普通のFlowをリッスンする場合と変わらない。
launchWhenって何
ライフサイクルの状態によって動作を開始・終了するcoroutine scope。
launchWhenStartedの場合、クラスが起動した場合に実行される。
詳しくはここFlowをStateFlowに変換するにはstateInを使用する
StateFlowの動作を停止させるには
launchWhenStarted を使用して Flow を収集する上記の例では、View がバックグラウンドに移動して Flow 収集をトリガーするコルーチンが停止しても、下層のプロデューサはアクティブなまま。
手動で停止させるには
class LatestNewsActivity : AppCompatActivity() { ... // Coroutine listening for UI states private var uiStateJob: Job? = null override fun onStart() { super.onStart() // Start collecting when the View is visible uiStateJob = lifecycleScope.launch { latestNewsViewModel.uiState.collect { uiState -> ... } } } override fun onStop() { // Stop collecting when the View goes to the background uiStateJob?.cancel() super.onStop() } }Viewが表示されていないときに停止させるにはasLiveData()でLiveDataに変更するという手もある
class LatestNewsActivity : AppCompatActivity() { ... override fun onCreate(savedInstanceState: Bundle?) { ... latestNewsViewModel.uiState.asLiveData().observe(owner = this) { state -> // Handle UI state } } }
- 投稿日:2021-03-02T10:47:53+09:00
新型コロナウイルス接触確認アプリ COCOAをビルドしてみる
はじめに
厚生労働省 新型コロナウイルス接触確認アプリ COCOAのソースコードが公開されているので、ビルドしてみました
使用した環境
- パソコン:lenovo SL510 Celeron(R) T3500 メモリ4GB HDD Win10
- タブレット:DRAGONTOUCH メモリ2GB Android8.1
- Microsoft VisualStudio 2019 Community
開発環境のインストール
ダウンロード
↓
まず、フリーの開発環境として、Microsoft VisualStudio 2019 Communityを下記よりダウンロードしてインストールします
https://visualstudio.microsoft.com/ja/free-developer-offers/ファイル名は、vs_community__1674506925.1614572429.exeでした
ミニインストーラなので、サイズは1.4MBほどですインストール
↓
Xamarinの開発環境のみチェックします
インストールには1時間ほどかかりました
(Celeron T3500のHDDノートパソコンなので、非常に遅いです)
↓
サインインは後で行うを選択COCOAのダウンロード
↓
次にCOCOAのソースファイルを下記よりダウンロードします
https://github.com/cocoa-mhlw/cocoa
ファイル名は、cocoa-master.zipでした
サイズは96MBほどですライセンスの関係で、ソースが公開されているようです
COCOAを開発環境で開く
↓
適当なフォルダでcocoa-master.zipファイルを展開し、Covid19Radar.slnをダブルクリックすると、VisualStuidioが起動して、ビルドが始まります
↓
セキュリティ警告は何度も表示されるので、チェックを外してOKをクリックします
↓
Android Sdkのライセンスへの同意が求められるので、同意しておきます
↓
マイクロソフトのユーザーアカウント制御の確認が表示されるので、はいを押します次々とソフトウェアをダウンロードしているようですので、終わるまで待ちます
↓
API 29関連のエラーが出ているので、SDKをインストールします
zipファイルを展開したフォルダ内の、cocoa-master\Covid19Radar\Covid19Radar.Android\Properties\AndroidManifest.xmlには、android:targetSdkVersion="29"の記述があったので、sdkのバージョンを29にしてみます
しかし、最終的にこのエラーはなくなりませんでした…
↓
ツールメニュー -> Android -> Android SDK マネージャを起動すると、SDKの修復が必要と表示されましたが、今回は修復しないを選択しました
↓
Android 10.0のAndroid SDK Platform 29をチェックし、28の方は外しました
↓
ライセンスの同意に同意します
↓
インストールが終わったら、ビルドメニューから、リビルドを実行します
リビルドが良いようです。ちょっと時間がかかりますタブレットの接続
↓
実機を接続し、パソコンに認識させます
COCOAの要件が、Android6.0以降なので、Android8.1のタブレットを準備しました
タブレットは、開発者向けオプションをオンにしておきます
USBケーブルでパソコンと接続し、許可しておきます
↓
さらに、VisualStudioの画面上部の設定をCovid19Radar.Androidと、タブレットの名前に変更後、実行させてみます
↓
タブレットでアプリが起動しましたおわりに
SDK 29をインストールしましたが、android.jar が見つからないエラーは消えませんでした
実機では起動できたのですが、少ししっくりこない部分が残りました実際に動作確認をするためには、通信環境も構築しないといけないようです
時間があれば、コードの方も見てみたいと思います
後からまとめたので、少し間違っていたら申し訳ありませんあくまで当方のテスト環境での報告です。実際に稼働中の開発環境でのビルドにつきましては、ご自身の責任でお願いいたします