Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Kotlin Closable and SQLiteDatabase on Android

I use this code in my project

fun getAllData(): List<Data> = writableDatabase.use { db ->
    db.query(...)
}

It executes successfully on Lollipop devices, but on pre-Lollipop it throws a ClassCastException

FATAL EXCEPTION: main
java.lang.ClassCastException: android.database.sqlite.SQLiteDatabase cannot be cast to java.io.Closeable

Probably because it's Java 1.6 and doesn't have the try-with-resources feature, but I'm not sure.

So why do I have this exception and how can it be fixed?

like image 336
hluhovskyi Avatar asked Sep 10 '16 20:09

hluhovskyi


1 Answers

The issue has nothing to do with try-with-resources and is simply an incompatible dependency issue between the compiled code and the runtime environment.

The problem is at compile time SQLiteDatabase implements the interface Closeable and later you swap this out for another implementation that does not. But the code is compiled already and doesn't know about this change.

When you call a function / method, all parameters are checked to make sure they are the correct types, this usually being handled by the JVM. For inline and extension functions the Kotlin compiler inserts a type check to make sure it is operating on the correct type, and for this specific inline function the receiver of the function is defined as being Closeable. Here is the method signature:

inline fun <T : Closeable, R> T.use(block: (T) -> R): R { ... }

So the compiler inlines a type check on writableDatabase as follows (in bytecode):

CHECKCAST java/io/Closeable

So at this point, the implementation of SQLiteDatabase must forever implement the Closeable interface or the compiled code will fail. By swapping to an older version of Android where this is no longer true, you break the contract and cause the exception. There is no way the compiler can do anything different. It would be the same as me swapping out a JAR in any JVM application to one with radically different implementations after I've already compiled my code. Changing Android versions basically swaps all the JARs.

What you can do to work around this problem is write your own use function specifically for the SQLiteDatabase class if all versions have a close method (copied and modified from the Kotlin stdlib use function):

inline fun <T : SQLiteDatabase, R> T.use(block: (T) -> R): R {
    var closed = false
    try {
        return block(this)
    } catch (e: Exception) {
        closed = true
        try {
            close()
        } catch (closeException: Exception) {
            // eat the closeException as we are already throwing the original cause
            // and we don't want to mask the real exception
       }
        throw e
    } finally {
        if (!closed) {
            close()
        }
    }
}

Now the inline typecheck will be CHECKCAST android/database/sqlite/SQLiteDatabase which will always succeed.

like image 95
4 revs Avatar answered Sep 20 '22 21:09

4 revs