Search code examples
androidkotlinandroid-roomkotlin-coroutinesandroid-livedata

Is it possible to write a "suspend" function in a Room @DAO which returns LiveData?


I'm trying to convert all methods of my @Dao objects to be suspendable for use from Kotlin coroutines.

When I add the suspend modifier to working function definitions and try to Build in Android Studio it fails to generate the XXXXDao_Impl.java class

A very stripped down example:

import androidx.lifecycle.LiveData
import androidx.room.*

@Database(entities = [SomeData::class], version = 1)
abstract class SomeDatabase : RoomDatabase() {
    abstract fun getDao(): SomeDataDao
}

@Entity
data class SomeData(@PrimaryKey val id: Int, val name: String)

@Dao
interface SomeDataDao {
    @Query("SELECT * FROM SomeData") // Works
    fun getAllSync(): List<SomeData>

    @Query("SELECT * FROM SomeData") // Works
    suspend fun getAllSuspend(): List<SomeData>

    @Query("SELECT * FROM SomeData") // Works
    fun getAllSyncLiveData(): LiveData<List<SomeData>>

    @Query("SELECT * FROM SomeData") // Fails to generate code
    suspend fun getAllSuspendLiveData(): LiveData<List<SomeData>>
}

The generated code which is failing:

package com.example.android.rawquerysuspendtest;

import android.database.Cursor;
import androidx.lifecycle.LiveData;
import androidx.room.CoroutinesRoom;
import androidx.room.RoomDatabase;
import androidx.room.RoomSQLiteQuery;
import androidx.room.util.CursorUtil;
import androidx.room.util.DBUtil;
import java.lang.Exception;
import java.lang.Object;
import java.lang.Override;
import java.lang.String;
import java.lang.SuppressWarnings;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import kotlin.coroutines.Continuation;

@SuppressWarnings({"unchecked", "deprecation"})
public final class SomeDataDao_Impl implements SomeDataDao {
  private final RoomDatabase __db;

  public SomeDataDao_Impl(RoomDatabase __db) {
    this.__db = __db;
  }

  @Override
  public List<SomeData> getAllSync() {
    final String _sql = "SELECT * FROM SomeData";
    final RoomSQLiteQuery _statement = RoomSQLiteQuery.acquire(_sql, 0);
    __db.assertNotSuspendingTransaction();
    final Cursor _cursor = DBUtil.query(__db, _statement, false);
    try {
      final int _cursorIndexOfId = CursorUtil.getColumnIndexOrThrow(_cursor, "id");
      final int _cursorIndexOfName = CursorUtil.getColumnIndexOrThrow(_cursor, "name");
      final List<SomeData> _result = new ArrayList<SomeData>(_cursor.getCount());
      while(_cursor.moveToNext()) {
        final SomeData _item;
        final int _tmpId;
        _tmpId = _cursor.getInt(_cursorIndexOfId);
        final String _tmpName;
        _tmpName = _cursor.getString(_cursorIndexOfName);
        _item = new SomeData(_tmpId,_tmpName);
        _result.add(_item);
      }
      return _result;
    } finally {
      _cursor.close();
      _statement.release();
    }
  }

  @Override
  public Object getAllSuspend(final Continuation<? super List<SomeData>> p0) {
    final String _sql = "SELECT * FROM SomeData";
    final RoomSQLiteQuery _statement = RoomSQLiteQuery.acquire(_sql, 0);
    return CoroutinesRoom.execute(__db, false, new Callable<List<SomeData>>() {
      @Override
      public List<SomeData> call() throws Exception {
        final Cursor _cursor = DBUtil.query(__db, _statement, false);
        try {
          final int _cursorIndexOfId = CursorUtil.getColumnIndexOrThrow(_cursor, "id");
          final int _cursorIndexOfName = CursorUtil.getColumnIndexOrThrow(_cursor, "name");
          final List<SomeData> _result = new ArrayList<SomeData>(_cursor.getCount());
          while(_cursor.moveToNext()) {
            final SomeData _item;
            final int _tmpId;
            _tmpId = _cursor.getInt(_cursorIndexOfId);
            final String _tmpName;
            _tmpName = _cursor.getString(_cursorIndexOfName);
            _item = new SomeData(_tmpId,_tmpName);
            _result.add(_item);
          }
          return _result;
        } finally {
          _cursor.close();
          _statement.release();
        }
      }
    }, p0);
  }

  @Override
  public LiveData<List<SomeData>> getAllSyncLiveData() {
    final String _sql = "SELECT * FROM SomeData";
    final RoomSQLiteQuery _statement = RoomSQLiteQuery.acquire(_sql, 0);
    return __db.getInvalidationTracker().createLiveData(new String[]{"SomeData"}, false, new Callable<List<SomeData>>() {
      @Override
      public List<SomeData> call() throws Exception {
        final Cursor _cursor = DBUtil.query(__db, _statement, false);
        try {
          final int _cursorIndexOfId = CursorUtil.getColumnIndexOrThrow(_cursor, "id");
          final int _cursorIndexOfName = CursorUtil.getColumnIndexOrThrow(_cursor, "name");
          final List<SomeData> _result = new ArrayList<SomeData>(_cursor.getCount());
          while(_cursor.moveToNext()) {
            final SomeData _item;
            final int _tmpId;
            _tmpId = _cursor.getInt(_cursorIndexOfId);
            final String _tmpName;
            _tmpName = _cursor.getString(_cursorIndexOfName);
            _item = new SomeData(_tmpId,_tmpName);
            _result.add(_item);
          }
          return _result;
        } finally {
          _cursor.close();
        }
      }

      @Override
      protected void finalize() {
        _statement.release();
      }
    });
  }

  @Override
  public Object getAllSuspendLiveData(final Continuation<? super LiveData<List<SomeData>>> p0) {
    final String _sql = "SELECT * FROM SomeData";
    final RoomSQLiteQuery _statement = RoomSQLiteQuery.acquire(_sql, 0);
    return CoroutinesRoom.execute(__db, false, new Callable<LiveData<List<SomeData>>>() {
      @Override
      public LiveData<List<SomeData>> call() throws Exception {
        final Cursor _cursor = DBUtil.query(__db, _statement, false);
        try {
          return _result;
        } finally {
          _cursor.close();
          _statement.release();
        }
      }
    }, p0);
  }
}

The build error message:

/Projects/Android/RawQuerySuspendTest/app/build/tmp/kapt3/stubs/debug/com/example/android/rawquerysuspendtest/SomeDataDao.java:24: error: Not sure how to convert a Cursor to this method's return type (androidx.lifecycle.LiveData<java.util.List<com.example.android.rawquerysuspendtest.SomeData>>).
    public abstract java.lang.Object getAllSuspendLiveData(@org.jetbrains.annotations.NotNull()

The build.gradle for the app

apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'

android {
    compileSdkVersion 28
    defaultConfig {
        applicationId "com.example.android.rawquerysuspendtest"
        minSdkVersion 21
        targetSdkVersion 28
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation"org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
    implementation 'androidx.appcompat:appcompat:1.0.2'
    implementation 'androidx.core:core-ktx:1.0.2'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'androidx.test:runner:1.2.0'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'

    kapt "androidx.room:room-compiler:2.1.0-rc01"
    implementation "androidx.room:room-runtime:2.1.0-rc01"
    implementation "androidx.room:room-ktx:2.1.0-rc01"

    implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.2.0-alpha01'
    implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0-alpha01'
    implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.2.0-alpha01'
    implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0-alpha01'
}

Am I missing a dependency or annotation processing step?


Solution

  • Current implementation of Room doesn't support coroutines with LiveData. As a workaround you can implement it like the following:

    @Dao
    interface AccountDao{
    
        @Query("SELECT * FROM account_master")
        suspend fun getAllAccounts(): List<Account>
    }
    

    And in your implementation of ViewModel class you can create LiveData and assign a value to it, retrieved from DB:

    class MainViewModel : ViewModel() {
        private val dao: AccountDao = ...// initialize it somehow
        private var job: Job = Job()
        private val scope = CoroutineScope(job + Dispatchers.Main)
        lateinit var accounts: MutableLiveData<List<Account>>
    
        override fun onCleared() {
            super.onCleared()
            job.cancel()
        }
    
        fun getAccounts(): LiveData<List<Account>> {
            if (!::accounts.isInitialized) {
                accounts = MutableLiveData()
                
                scope.launch {
                    accounts.postValue(dao.getAllAccounts())
                }
                
            }
            return accounts
        }
    }
    

    To use Dispatchers.Main import:

    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.1.1'
    

    If you want to get notified when the changes to the DB occur you can use LiveData<List<Account>> or Flow<List<Account>> as a return type of getAllAccounts method:

    @Dao
    interface AccountDao{
    
        @Query("SELECT * FROM account_master")
        fun getAllAccounts(): Flow<List<Account>>
    }
    
    class MainViewModel : ViewModel() {
        //...
    
        fun loadAllAccounts(): Flow<List<Account>> = dao.getAllAccounts()
    }
    
    // In Activity/Fragment
    
    lifecycleScope.launch {
       viewModel.loadAllAccounts().collect { accounts ->
           // use accounts, for example populate RecyclerView Adapter
       }
    }