Android Kotlin Room Repository 无法从详细活动中检索行

问题描述

我真的很挣扎,希望得到一些帮助。我正在学习 Android Kotlin 并构建一个应用程序,该应用程序在 RecyclerView显示步行路线(从云端下载)列表,当选择了一条路线 我想显示该路线的所有详细信息 - 一个简单的 master-detail 应用程序。因为,我正在学习,我也想尝试使用最佳实践。我使用 Room 数据库和存储库使大部分工作正常。数据库已正确填充并且 RecyclerView 显示路线列表。选择路线后,routeID 和其他详细信息会正确传递给活动 (TwalksRouteActivity.kt)显示详细信息,这可以正常工作。

但是,我需要使用 routeID 从数据库(存储库?)中查找路线,因此所有详细信息都可以在详细信息活动中使用,但我无法使其正常工作。我不想在一个包中传递所有详细信息,因为一旦工作正常,我将需要从详细信息活动中进行其他数据库查找。我已经尝试了围绕协程的各种解决方案来避免线程阻塞,但完全失败了。所以我的问题是,如何从详细活动的数据库/存储库中正确获取一行。

这是详细活动 (TwalksRouteActivity.kt):

package com.example.android.twalks.ui

import android.os.Bundle
import android.util.Log
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import com.example.android.twalks.R
import com.example.android.twalks.database.RouteDao
import com.example.android.twalks.database.getDatabase
import com.example.android.twalks.domain.Route
import com.example.android.twalks.repository.RoutesRepository
import kotlinx.coroutines.dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import timber.log.Timber
import timber.log.Timber.*

class TwalksRouteActivity() : AppCompatActivity()  {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        var bundle: Bundle? = intent.extras
        var routeID = bundle?.getInt("routeID")
        var routeName = bundle?.getString("routeName")
        var routeCategoryName = bundle?.getString("routeCategoryName")
        var routedistance = bundle?.getString("routedistance")
        var routeTime = bundle?.getString("routeTime")
        var routeImageFile = bundle?.getString("routeImageFile")

        GlobalScope.launch (dispatchers.Main) {
            val database = getDatabase(application)
            val routesRepository = RoutesRepository(database)
            val selectedRoute = routesRepository.getRoute(routeID)
            Log.d("CWM",selectedRoute.toString())

        }

        setContentView(R.layout.route_detail)

        val routeName_Text: TextView = findViewById(R.id.routeName_text)
        routeName_Text.text = routeName.toString()
        val routeID_Text: TextView = findViewById(R.id.routeID)
        routeID_Text.text = routeID.toString()

        //Toast.makeText(this,"Here in TwalksRouteActivity",Toast.LENGTH_LONG).show()
        //Toast.makeText(applicationContext,routeName,Toast.LENGTH_LONG)
    }
}
数据库实体.kt

package com.example.android.twalks.database

import androidx.room.Entity
import androidx.room.PrimaryKey
import com.example.android.twalks.domain.Route

/**
 * DataTransferObjects go in this file. These are responsible for parsing responses from the server
 * or formatting objects to send to the server. You should convert these to domain objects before
 * using them.
 */

@Entity
data class DatabaseRoute constructor(
        @PrimaryKey
        val routeID: String,val routeName: String,val routeImageFile: String,val routeCategoryName: String,val routeCategory: String,val routedistance: String,val routeTime: String,val routeStatus:String)

fun List<DatabaseRoute>.asDomainModel(): List<Route> {
        return map {
                Route(
                        routeID = it.routeID,routeName = it.routeName,routeImageFile = it.routeImageFile,routeCategoryName = it.routeCategoryName,routeCategory = it.routeCategory,routedistance = it.routedistance,routeTime = it.routeTime,routeStatus = it.routeStatus)
        }
}
请注意, GlobalScope 块在日志中返回一个 kotlin.Unit,因此没有返回任何记录。这就是我需要帮助的地方!

房间.kt

package com.example.android.twalks.database

import android.content.Context
import androidx.lifecycle.LiveData
import androidx.room.*
import com.example.android.twalks.domain.Route

@Dao
interface RouteDao {
    @Query("select * from databaseroute")
    fun getRoutes(): LiveData<List<DatabaseRoute>>

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    fun insertAll(vararg routes: DatabaseRoute)

    @Query("select * from databaseroute where routeID = :routeID")
    fun getRoute(routeID: Int?): LiveData<Route>
}

@Database(entities = [DatabaseRoute::class],version = 1)
abstract class RoutesDatabase: RoomDatabase() {
    abstract val routeDao: RouteDao
}

private lateinit var INSTANCE: RoutesDatabase

fun getDatabase(context: Context): RoutesDatabase {
    synchronized(RoutesDatabase::class.java) {
        if (!::INSTANCE.isInitialized) {
            INSTANCE = Room.databaseBuilder(context.applicationContext,RoutesDatabase::class.java,"routes").build()
        }
    }
    return INSTANCE
}
Models.kt(域对象):

package com.example.android.twalks.domain

/**
 * Domain objects are plain Kotlin data classes that represent the things in our app. These are the
 * objects that should be displayed on screen,or manipulated by the app.
 *
 * @see database for objects that are mapped to the database
 * @see network for objects that parse or prepare network calls
 */

data class Route(val routeID: String,val routeStatus: String)
数据传输对象.kt:

package com.example.android.twalks.network

import android.os.Parcelable
import com.example.android.twalks.database.DatabaseRoute
import com.example.android.twalks.domain.Route
import com.squareup.moshi.JsonClass
import kotlinx.android.parcel.Parcelize

@JsonClass(generateAdapter = true)
data class NetworkRouteContainer(val routes: List<NetworkRoute>)


@JsonClass(generateAdapter = true)
data class NetworkRoute(
        val routeID: String,val routeStatus: String )
/**
 * Convert Network results to com.example.android.twalks.database objects
 */

fun NetworkRouteContainer.asDomainModel(): List<Route> {
    return routes.map {
        Route(
                routeID = it.routeID,routeStatus = it.routeStatus)
    }
}

fun NetworkRouteContainer.asDatabaseModel(): Array<DatabaseRoute> {
    return routes.map {
        DatabaseRoute(
                routeID = it.routeID,routeStatus = it.routeStatus
        )
    }.toTypedArray()
}
路由存储库:

package com.example.android.twalks.repository

import android.util.Log
import androidx.annotation.WorkerThread
import androidx.lifecycle.LiveData
import androidx.lifecycle.Transformations
import com.example.android.twalks.database.RouteDao
import com.example.android.twalks.database.RoutesDatabase
import com.example.android.twalks.database.asDomainModel

import com.example.android.twalks.domain.Route
import com.example.android.twalks.network.Network
import com.example.android.twalks.network.asDatabaseModel


import kotlinx.coroutines.dispatchers
import kotlinx.coroutines.withContext
import timber.log.Timber

/**
 * Repository for fetching routes from the network and storing them on disk
 */
class RoutesRepository(private val database: RoutesDatabase) {

    val routes: LiveData<List<Route>> =
            Transformations.map(database.routeDao.getRoutes()) {
                it.asDomainModel()
            }

    suspend fun refreshRoutes() {
        withContext(dispatchers.IO) {
            val routelist = Network.twalks.getRoutes().await()
            database.routeDao.insertAll(*routelist.asDatabaseModel())
                    }
    }

    suspend fun getRoute(id: Int?) {
        withContext(dispatchers.IO) {
            val route: LiveData<Route> = database.routeDao.getRoute(id)
            //Log.d("CWM2",route.toString())
            return@withContext route
            }
    }
}

解决方法

您的代码不起作用,因为您没有从 getRoute 类中的 RoutesRepository 返回任何内容。指定返回类型,您就会看到它。

您可以通过返回 withContext 块来解决它,但我想建议您进行一些更改,因为您说您正在学习并且还想尝试应用最佳做法。

RouteDao

Room 从 2.1 版开始支持协程。您所要做的就是使用关键字 suspend 标记您的 DAO 方法。您不必担心在主线程上调用 suspend DAO 方法,因为它被挂起并且 Room 设法在后台线程上执行查询。

  • 了解有关此主题的更多信息 here

因此您的 getRoute DAO 方法将如下所示:

@Query("select * from databaseroute where routeID = :routeID")
suspend fun getRoute(routeID: Int): Route

注意 1:我将返回类型从 LiveData<Route> 更改为 Route,因为我假设您不希望它发生变化。

注意 2:我认为将可空 routeID 作为参数没有意义,所以我删除了 ?


路由存储库

通过之前的更改,您的 getRoute 类中的 RoutesRepository 方法将如下所示:

suspend fun getRoute(id: Int) = database.routeDao.getRoute(id)

注意 1:正如我之前提到的,您不必担心移动到后台线程,因为 Room 会为您完成。

注意 2: 同样,不可为空的参数。


TwalksRouteActivity

​​>

您直接从您的活动中调用您的存储库。我不确定您正在应用的架构,但我希望在中间看到 Presenter 或 ViewModel。省略该细节,我建议您几乎总是避免使用 GlobalScope 启动协程。仅当您知道 GlobalScope 的工作原理并且完全确定自己在做什么时才使用 GlobalScope

  • 了解有关此主题的更多信息 here

您可以使用 GlobalScope 代替 lifecycleScope,它在主线程上运行并且具有生命周期感知能力。

将您的 GlobalScope.launch {...} 更改为:

lifecycleScope.launch {
    ...
    val selectedRoute = routesRepository.getRoute(routeID)
    //Do something with selectedRoute here
}

注意 1:您需要 androidx.lifecycle:lifecycle-runtime-ktx:2.2.0 或更高。

注意 2:如果您要获取请求中的所有路由数据,则只能将其 routeID 传递给您的新活动。