问题
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)
}
}
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)
}
}
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
}
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)
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()
}
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