20210302のAndroidに関する記事は10件です。

ロガーライブラリ「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.gradle
dependencies {
+     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.kt
import 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

おわりに

かんたんにログ出力を実現できました!
どのようなアプリの開発でも有用だと思います。

参考リンク

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

メモリリーク検出ライブラリ「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.gradle
android {
     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

メモリリーク検出の確認

LeakCanaryは別アプリになっているので起動します。
Screenshot_1614686735.png

[Leaks]タブでメモリリークの検出を確認できます。
Screenshot_1614686711.png

私が開発しているアプリではメモリリークが検出されませんでした。

おわりに

かんたんにAndroidアプリのメモリリークを検出できました!
どのようなアプリの開発でも有用だと思います。

参考リンク

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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()など
ライフサイクルの項でまとめる

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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 にして ペイロードを取り出す

image.png

あとは,CyberChefで料理するだけ
image.png

※なお,もしこれが本物の Android マルウェアならば,ここからが本番。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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です。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Wi-Fi経由でadb接続したときのエラーへの対処

経緯

WiFi経由でスマホ(HUAWEI P20 Lite)とパソコン(Windows)をadb接続しようとしたものの、なかなかうまくいかなかったので、僕が直面したエラーとその解決方法について書いていきます。
以下、開発者向けオプションは有効化してあるものとして話を進めていきます。

基本的な手順

  1. スマホとパソコンを同じWi-Fiネットワークに接続
  2. USBケーブルでスマホをパソコンを接続
  3. adb tcpip 5555をコマンドプロンプトで実行
  4. USBを切り離す
  5. スマホのIPアドレスを調べ、adb connect [IPアドレス]を実行。
  6. adb devicesでadb接続できていることを確認

Android Debug Bridge(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接続することができました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

公開しているAndroidアプリのアップデートの手順

公開しているAndroidアプリのアップデートの手順

自作のアプリをアップデートする機会があったので、2021年2月時点でのアップデートの手順を公開します。

最初はAndroidStudioでの手順です。
まず、プロジェクトウィンドウ内の「Gradle Script」をクリックして、「buile.gradle(Module:アプリ名.app」を開きます。
androidアップデート1.png

仮に初めてアップデートするアプリであれば、「versionCode」が"1"、「versionName」が"1.0"になっているので、これらを変更します。
(今回の場合では「versionCode」を"2", 「versionName」を"1.1"などが適当だと思います。次回は「versionCode」を"3",「versionName」を"1.2"にしようと思います。)
androidアップデート2.png
androidアップデート3.png

変更したら、画面右上の「Sync Now」をクリックします。
androidアップデート4.png

次にaabファイルの更新をします。
メニューバーの「Build」をクリックして、「Generate Signed Bundle / APK」をクリックします。
androidアップデート5.png

「Generate Signed Bundle or APK」ウィンドウで「Android App Bundle」を選択して、「Next」をクリックします。
(apkファイルを使用する方は「APK」を選択してください)
androidアップデート6.png

keyの場所を前回のリリース時から変更していなければ、「Key store password」と「Key password」を入力してから、「Next」をクリックします。
変更していれば、「Key store path」の「Choose existing」をクリックして、keyの場所を選択した上で、「Key store password」と「Key password」を入力してから、「Next」をクリックします。
androidアップデート7.png
androidアップデート8.png

最後に、前回リリース後にプロジェクトの場所を変更していなければ、「Finish」をクリックします。
変更していれば、「Destination Folder」のフォルダアイコンをクリックして、場所を選択した上で、「Finish」をクリックします。
androidアップデート9.png

次はGoogle Play Consoleでの手順です。
Google Play Consoleの「すべてのアプリ」を選択して、アップデートしたいアプリを選びます。
androidアップデート10.png

そうすると、そのアプリのダッシュボードに画面が遷移します。
「リリース」の「製品版」をクリックします。
androidアップデート11.png
androidアップデート12.png

画面右上の「新しいリリースを作成」をクリックして、aabファイルをアップロードします。
アップロードは、画面中心部の「App BundleとAPK」の枠内にある「アップロード」をクリックして行います。(ファイルをドラッグアンドドロップでも可)
androidアップデート13.png

アップロードすると、ファイル名やバージョン名が表示されます。
ページの下側に進むと、「リリースノート」があります。「リリースノート」には、このバージョンについての変更点を記載できます。
androidアップデート14.png
androidアップデート15.png

画面右下の「保存」をクリックして、変更を保存します。
androidアップデート16.png

画面右下の「リリースのレビュー」をクリックして、変更内容を確認します。
変更内容に問題がなければ、画面右下の「製品版としての公開を開始」をクリックします。
androidアップデート17.png
androidアップデート18.png

以上で、Androidアプリのアップデートは完了です。
アップデートの審査で数日かかります。(筆者は2日で審査完了しました。)

もしよろしければ、今回アップデートしたアプリのURLを載せるので、買ってください?(110円です。)
https://play.google.com/store/apps/details?id=com.konno_siki.drawing2
子供向けのお絵かきアプリですが、端末に保存している画像を画面に表示して、マーキングする使い方もできます。
今回の投稿で使用した画像も、PCでスクショした画像を端末にメールで送って、このアプリで個人情報にあたる箇所を塗りつぶしてから、端末からPCにメールで画像を送り返しました。広告はありません。
よろしくお願いします?

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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)

Dispatchersの種類

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:1

emit()が起動した回数分呼ばれる。
そのため、以下のように重たい処理をしたい場合には不向き。

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
        }
    }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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)

Dispatchersの種類

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:1

emit()が起動した回数分呼ばれる。
そのため、以下のように重たい処理をしたい場合には不向き。

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
        }
    }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

新型コロナウイルス接触確認アプリ 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/

VSDownload.png

ファイル名は、vs_community__1674506925.1614572429.exeでした
ミニインストーラなので、サイズは1.4MBほどです

インストール

Xamarinの開発環境のみチェックします
インストールには1時間ほどかかりました
(Celeron T3500のHDDノートパソコンなので、非常に遅いです)

VSInstall.png

サインインは後で行うを選択

VSSignIn.png

COCOAのダウンロード

次にCOCOAのソースファイルを下記よりダウンロードします
https://github.com/cocoa-mhlw/cocoa
ファイル名は、cocoa-master.zipでした
サイズは96MBほどです

CocoaDownload.png

ライセンスの関係で、ソースが公開されているようです

CocoaLicense.png

COCOAを開発環境で開く

適当なフォルダでcocoa-master.zipファイルを展開し、Covid19Radar.slnをダブルクリックすると、VisualStuidioが起動して、ビルドが始まります

VSCocoa.png

セキュリティ警告は何度も表示されるので、チェックを外してOKをクリックします

VSSecurity.png

Android Sdkのライセンスへの同意が求められるので、同意しておきます

SdkLicense.png

マイクロソフトのユーザーアカウント制御の確認が表示されるので、はいを押します

MsAccount.png

次々とソフトウェアをダウンロードしているようですので、終わるまで待ちます

API 29関連のエラーが出ているので、SDKをインストールします
zipファイルを展開したフォルダ内の、cocoa-master\Covid19Radar\Covid19Radar.Android\Properties\AndroidManifest.xmlには、android:targetSdkVersion="29"の記述があったので、sdkのバージョンを29にしてみます
しかし、最終的にこのエラーはなくなりませんでした…

SdkErr.png

ツールメニュー -> Android -> Android SDK マネージャを起動すると、SDKの修復が必要と表示されましたが、今回は修復しないを選択しました

SdkRepair.png

Android 10.0のAndroid SDK Platform 29をチェックし、28の方は外しました

SdkManager.png

ライセンスの同意に同意します

Sdk29License.png

インストールが終わったら、ビルドメニューから、リビルドを実行します
リビルドが良いようです。ちょっと時間がかかります

CocoaBuild.png

タブレットの接続

実機を接続し、パソコンに認識させます
COCOAの要件が、Android6.0以降なので、Android8.1のタブレットを準備しました
タブレットは、開発者向けオプションをオンにしておきます
USBケーブルでパソコンと接続し、許可しておきます

UsbDebug.png

さらに、VisualStudioの画面上部の設定をCovid19Radar.Androidと、タブレットの名前に変更後、実行させてみます

VSConfig.png

タブレットでアプリが起動しました

Screenshot_20210301-155059.png

おわりに

SDK 29をインストールしましたが、android.jar が見つからないエラーは消えませんでした
実機では起動できたのですが、少ししっくりこない部分が残りました

実際に動作確認をするためには、通信環境も構築しないといけないようです
時間があれば、コードの方も見てみたいと思います
後からまとめたので、少し間違っていたら申し訳ありません

あくまで当方のテスト環境での報告です。実際に稼働中の開発環境でのビルドにつきましては、ご自身の責任でお願いいたします

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む