【Android】Kotlin でモダンな concurrency その1

現在の Java/Android の concurrency フレームワークはコールバック地獄の原因となります。

それは、スレッドセーフを保証するシンプルな方法がないからです。

kotlin coroutine は、concurrency を管理するための効果的でシンプルなフレームワークです。

Suspending と Blocking

coroutine は、スレッドを置き換えるものではなく、それを管理するフレームワークのようなものです。

元のスレッドをブロックすることなく、バックグラウンド処理完了に対しての wait を可能にする実行コンテキストを定義しています。

コールバックを避けて、簡単に concurrency を行ってみましょう。

基本

最初は非常にシンプルな例です。UIコンテキストで coroutine を起動し、その中でIOコンテキストでイメージを取得します。

その後、UIコンテキストに戻ります。


launch(UI) {
    val image = withContext(IO) { getImage() } 
    imageView.setImageBitmap(image) 
}

シングルスレッドのように直感的なコードです。

getImage() がIOスレッドで実行されている間、メインスレッドは、自由に他の処理を行うことができます。

withContext は、その coroutine を getImage() が実行されている間は中断します。

getImage() のあとすぐに、メインlooperが利用可能になり、coroutine はメインスレッドを再開します。

そして imageView.setImageBitmap(image) が呼ばれます。

 

次の例は、2つのバックグラウンド処理を完了させ、その結果を利用する、というものです。

async/await を使ってパラレルに実行し、それら両方の結果取得のあとメインスレッドでそれを利用した処理を行います。


val job = launch(UI) {
    val deferred1 = async { getFirstValue() }
    val deferred2 = async(IO) { getSecondValue() }
    useValues(deferred1.await(), deferred2.await())
}

job.join() 

async は launch に似ていますが、deferred (Kotlin版 Future) を返します。

そして、await() で結果を取得します。パラメータなしで実行した場合は、CommonPool コンテキストで実行されます。

前の例と同様に、2つの結果を待っている間は、メインスレッドは自由に処理を行うことができます。

例にあるように、launch は処理が完了するのを待つのに利用できる Job を返します。

「スレッドをブロックせず coroutine をブロックする」ということを除けば、他の言語と同様です。

(つづく)


Android Architecture Components: Room の Migration で IllegalStateException

データを移行せずに捨ててしまうのなら、以下でいいのですが。


Room.databaseBuilder(context, RepoDatabase.class, DB_NAME)
    .fallbackToDestructiveMigration()
    .build();

きっと捨てることができませんよね。

テーブル定義を変更しながら、データを移行しますよね。


Room.databaseBuilder(context, RepoDatabase.class, DB_NAME)
    .addMigrations(FROM_1_TO_2)
    .build();

static final Migration FROM_1_TO_2 = new Migration(1, 2) {
    @Override
    public void migrate(final SupportSQLiteDatabase database) {
        database.execSQL("ALTER TABLE Repo
                         ADD COLUMN createdAt TEXT");
        }
    };

database.execSQL()でSQLをベタに実行しながらデータを別テーブルにRENAME後、CREATE→INSERT→DROP というかんじでスキーマを変更していますが。

すると、こんなのに遭遇します。

java.lang.IllegalStateException: Room cannot verify the data integrity. Looks like you’ve changed schema but forgot to update the version number. You can simply fix this by increasing the version number.

データベースのバージョンナンバーは上げることは、まあ上げるとして、それでもインデクスなどうまく意図通りに移行できてない場合があります。

データモデルにアノテーションで記述した「Room が利用しようとしているスキーマ」と、Migration部分にベタ記述した「SQLiteのスキーマ」が合致しないといけません。

また、最近のAndroidでは、.dbファイルが、OS上で取り回しづらく、実態を把握しづらかったりします。

Roomが認識しようとしているテーブルスキーマは以下で書き出すことができます。


android {
    javaCompileOptions {
        annotationProcessorOptions {
            arguments = ["room.schemaLocation":
                         "$projectDir/schemas".toString()]
            }
        }
    }
}


"tableName": "Repo",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT, `url` TEXT, PRIMARY KEY(`id`))"

モデルクラスに記述したアノテーションのRoomが認識している状態(expected)をSQLで書き出してくれます。

これと、MIGRATION部分のベタSQL(found)を比較すると、意味が分かってきます。

複数フィールドに対してのUNIQUE なインデクスなど、公式ドキュメントとは違う内部的絶賛更新中な処理な部分など、書き出してみると先に進むことができます。


プレインストールアプリを削除する

ずっと、Nexus/Pixelシリーズの、素のAndroid OS を使ってきましたが、 今回、ドコモ版のGalaxy S8を周回遅れで購入しましたが、使わないベンダーやキャリアのアプリが頻繁に通知を送ってきます。

後付なおせっかい機能です。

Androidの煩わしさの原因に見えたりします。

以下、自己責任の不具合覚悟の方のみ。

OS OTAアップデート当たらないかもしれません。 キャリア依存。


// パッケージ名のリストをみる。
adb shell pm list packages -s

無効化できないアプリは、もう強制的に消してしまいます。


// パッケージを強制的に消す
adb shell pm uninstall -k --user 0 com.nttdocomo.android.applicationmanager

adb shell pm uninstall -k --user 0 com.nttdocomo.android.dhome

adb shell pm uninstall -k --user 0 com.nttdocomo.android.store

adb shell pm uninstall -k --user 0 jp.co.nttdocomo.lcsapp

adb shell pm uninstall -k --user 0 com.nttdocomo.android.voiceeditor (ドコモ文字編集)

adb shell pm uninstall -k --user 0 com.sec.android.inputmethod.iwnnime.japan

素のAndroid(Nexus/Pixelシリーズ)では表示されてた「パッケージ名」は、それ以外の端末では、表示できないようにされています。

ので、アンインストールしたい「アプリ名」が分かっていても、「パッケージ名」との紐づけができず困ったりします。

adb badge を使ってやるのも面倒なので、簡単にアプリ内から、「アプリ名」-「パッケージ名」のリストを見ましょう。


inline fun Context.listPackages() {
  val packages = packageManager
      .getInstalledApplications(PackageManager.GET_META_DATA)
      .sortedBy { it.packageName }

  packages.forEach {
    println(
        "%s %s %s".format(
            it.packageName,
            it.loadLabel(packageManager),
            it.uid
        )
    )
  }
}

OSアップデートや国内独自機能(NFC/トルカ?)など不具合が起こり始めるのだろうが、それを差し引いても快適で。

格安の素のAndroidを国内キャリアはなぜ出さないのかが謎。

Androidに関しては「付加価値を」とか「他社と違い」てなフレーズを使ったビジネスするしか?

いまどきのAndroid端末ってアプリのパッケージ名見れないのですか?
Package Info - Apps on Google Play