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 なインデクスなど、公式ドキュメントとは違う内部的絶賛更新中な処理な部分など、書き出してみると先に進むことができます。


Android Studio のビルドの体感速度を上げる IDEAプラグイン「Nyan Progress Bar」

プログレスバーのカスタマイズプラグインです。

こういうことです。

 

蓄積されていくイライラは少しずつでも解消していきたいものですよね。

Nyan Progress Bar :: JetBrains Plugin Repository


Google I/O 2018 にみる Android KTX その1

Android KTX は、次期 androidx のパッケージとも深い関わりをもっているようです。

androidx: Hello World!
You may notice that Android KTX uses package names that begin with androidx. This is a new package name prefix that we will be using in future versions of Android Support Library. We hope the division between android.* and androidx.* makes it more obvious which APIs are bundled with the platform, and which are static libraries for app developers that work across different versions of Android.

Android Developers Blog: Introducing Android KTX: Even Sweeter Kotlin Development for Android

また、パッケージ名に関してのマッピングも公式からアナウンスされ始めています。

AndroidX refactoring  |  Android Developers

そんな Android KTX に関係するいくつかの動画がアップされています。

Android Jetpack: sweetening Kotlin development with Android KTX (Google I/O 2018) - YouTube

Google I/O 2018: Stage 2 - YouTube

前回は、よく分からんオネーチャンが喋ってましたが、 今回は、Jake Wharton さんが遂に表に登場しました。

彼のトークやスライドは開発に有効に利用できることばかりです。

Kotlin の Extension Function の勉強にもなりますので、登場したコードを書き出しておきます。

並び順は、

「よくあるコード」(before)
「Android KTX のコード(拡張関数)」(KTX)
「KTXを利用したコード」(after)

です。

 

ViewGroupの再帰処理


// before
val userLayout: ViewGroup = findViewById(R.id.users)
for (index in 0 until userLayout.childCount) {
  val view = userLayout.getChildAt(index)
  // Do something with index and view...
}


// KTX
fun ViewGroup.forEachIndexed(action: (Int, View) -> Unit) {
  for (index in 0 until childCount) {
    action(index, getChildAt(index))
  }
}


// after
val userLayout: ViewGroup = findViewById(R.id.users)
userLayout.forEachIndexed { index, view ->
  // Do something with index and view...
}

 

getSystemService()


// before

// In an Activity, on API 23+...
val notifications = getSystemService(NotificationManager::class.java)

// or all API levels...
val notifications = ContextCompat.getSystemService(this,
  NotificationManager::class.java)


// KTX
inline fun <reified T> Context.systemService() =
  ContextCompat.getSystemService(this, T::class.java)


// after
val notifications = systemService<NotificationManager>()

 

Viewのパディング


// before
avatarView.setPadding(
  10, avatarView.paddingTop, 10, avatarView.paddingBottom)


// KTX
inline fun View.updatePadding(
  left: Int = paddingLeft,
  top: Int = paddingTop,
  right: Int = paddingRight,
  bottom: Int = paddingBottom
) {
  setPaddinng(left, top, right, bottom)
}


// after
avatarView.updatePadding(left = 10, right = 10)

 

デストラクチャ


// before
val rect = avaterView.clipBounds
val left = rect.left
val top = rect.top
val right = rect.right
val bottom = rect.bottom
// Use left, top, right, bottom...


// KTX
inline operator fun Rect.component1() = left
inline operator fun Rect.component2() = top
inline operator fun Rect.component3() = right
inline operator fun Rect.component3() = bottom


// after
val (left, top, right, bottom) = avatarView.clipBounds
// Use left, top, right, bottom...

val (left, top, right) = avatarView.clipBounds
// Use left, top, right...

val (left, _, right) = avatarView.clipBounds
// Use left, right...

 

すべての値のチェック


// before
val onlyDigits = true
for (c in phoneNumber) {
  if (!c.isDigit()) {
    onlyDigits = false
    break
  }
}


// before
val onlyDigits = phoneNumber.all { it.isDigit() }


// before
val onlyDigits = TextUtils.isDigitsOnly(phoneNumber)


// KTX
inline fun CharSequence.isDigitsOnly() = TextUtils.isDigitsOnly(this)


// after
val onlyDigits = phoneNumber.isDigitsOnly()

とりあえずは、前半のコードを書き出しておいて、次回へ

(つづく...)