Android Kotlin Room Repository unable to retrieve row from within detail activity

梦想的初衷 提交于 2021-02-20 01:00:08

问题


I'm really struggling with this & would appreciate some help please. I'm learning Android Kotlin & building an app that displays a list of walking routes (downloaded from the cloud) in a RecyclerView &, when a route is selected I want to display all details of the route - a simple Master-Detail app. Since, I'm learning I also want to try and use best practice. I have most of it working fine using a Room database & a Repository. The database is correctly populated and the RecyclerView displays the list of routes. When a route is selected the routeID and other details are correctly passed to an activity (TwalksRouteActivity.kt) to display the details & this works fine.

However, I need to use the routeID to looks up the route from the database (Repository?) so all the details are available in the detail activity but I can't get this to work. I don't want to pass all of the details in a bundle because I will need to do other database look ups from the detail activity once this is working. I have tried all sorts of solutions around Coroutines to avoid thread blocking but have failed completely. So my question is, how do I correctly get a row from my database/repository from the detail activity.

Here's the detail activity (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)
    }
}
DatabaseEntities.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)
        }
}
Note that the GlobalScope block returns a kotlin.Unit in the log so no record is being returned. This is where I need help!

Room.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 (Domain Object):

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 routeName: String,
                 val routeImageFile: String,
                 val routeCategoryName: String,
                 val routeCategory: String,
                 val routeDistance: String,
                 val routeTime: String,
                 val routeStatus: String)
DataTransferObjects.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 routeName: String,
        val routeImageFile: String,
        val routeCategoryName: String,
        val routeCategory: String,
        val routeDistance: String,
        val routeTime: 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,
                routeName = it.routeName,
                routeImageFile = it.routeImageFile,
                routeCategoryName = it.routeCategoryName,
                routeCategory = it.routeCategory,
                routeDistance = it.routeDistance,
                routeTime = it.routeTime,
                routeStatus = it.routeStatus)
    }
}

fun NetworkRouteContainer.asDatabaseModel(): Array<DatabaseRoute> {
    return routes.map {
        DatabaseRoute(
                routeID = it.routeID,
                routeName = it.routeName,
                routeImageFile = it.routeImageFile,
                routeCategoryName = it.routeCategoryName,
                routeCategory = it.routeCategory,
                routeDistance = it.routeDistance,
                routeTime = it.routeTime,
                routeStatus = it.routeStatus
        )
    }.toTypedArray()
}
RoutesRepository:

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
            }
    }
}

回答1:


Your code is not working because you're not returning anything from getRoute in your RoutesRepository class. Specify the return type and you'll see it.

You can solve it by returning the withContext block, but I'd like to suggest you some changes since you said you're learning and also want to try and apply best practices.

RouteDao

Room supports coroutines since version 2.1. All you have to do is marking your DAO methods with the keyword suspend. You don't have to worry about calling a suspend DAO method on your Main thread since it gets suspended and Room manages to execute the query on a background thread.

  • Learn more about this subject here.

So your getRoute DAO method would look like this:

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

Note 1: I changed the return type from LiveData<Route> to Route since I assume you don't expect it to change.

Note 2: I don't see the point in having a nullable routeID as argument so I removed the ?.


RoutesRepository

With the previous change your getRoute method on your RoutesRepository class would look like this:

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

Note 1: As I mentioned before, you don't have to worry about moving to a background thread since Room will do it for you.

Note 2: Again, not nullable argument.


TwalksRouteActivity

You're calling your repository directly from your activity. I'm not sure about the architecture you're applying but I would expect to see a Presenter or a ViewModel in the middle. Omitting that detail, I suggest you to avoid starting a coroutine with GlobalScope almost always. Use GlobalScope only when you know how GlobalScope works and you're totally sure about what you're doing.

  • Learn more about this subject here.

Instead of GlobalScope you can use lifecycleScope which runs on the main thread and it's lifecycle aware.

Change your GlobalScope.launch {...} to this:

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

Note 1: You need androidx.lifecycle:lifecycle-runtime-ktx:2.2.0 or higher.

Note 2: If you're getting all the Route data in your request, you could pass only its routeID to your new activity.



来源:https://stackoverflow.com/questions/65477865/android-kotlin-room-repository-unable-to-retrieve-row-from-within-detail-activit

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!