Android Kotlin Room Repository 无法从详细信息中检索行 activity
Android Kotlin Room Repository unable to retrieve row from within detail activity
我真的为此苦苦挣扎,希望能提供一些帮助。我正在学习 Android Kotlin 并构建一个应用程序,在 步行路线 (从云端下载)列表中显示 RecyclerView &, when a route is selected I want to display all details of the route - a simple Master-Detail app.因为,我正在学习,我也想尝试使用最佳实践。我使用 Room 数据库和存储库大部分工作正常。数据库已正确填充且 RecyclerView 显示路线列表。选择路线后,routeID
和其他详细信息会正确传递给 activity (TwalksRouteActivity.kt)
以显示详细信息,这工作正常。
但是,我需要使用 routeID 从数据库(存储库?)中查找路线,因此所有详细信息都可以在详细信息 activity 中找到,但我无法让它工作。我不想在一个包中传递所有详细信息,因为一旦它起作用,我将需要从详细信息 activity 进行其他数据库查找。我已经尝试了各种围绕协程的解决方案来避免线程阻塞,但都完全失败了。所以我的问题是,如何从细节 activity.
中正确地从我的 database/repository 中获取一行
详情如下 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)
}
}
请注意,日志中的 GlobalScope 块 returns a kotlin.Unit 因此不会返回任何记录。这是我需要帮助的地方!
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(域对象):
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()
}
路线库:
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
}
}
}
您的代码无法正常工作,因为您没有在 RoutesRepository
class 中 return 使用 getRoute
中的任何内容。指定 return 类型,您将看到它。
您可以通过 returning 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: 我将 return 类型从 LiveData<Route>
更改为 Route
因为我假设您不希望它发生变化.
注 2: 我看不出将可为 null 的 routeID
作为参数有什么意义,所以我删除了 ?
.
路由库
通过之前的更改,您的 RoutesRepository
class 上的 getRoute
方法将如下所示:
suspend fun getRoute(id: Int) = database.routeDao.getRoute(id)
注意 1: 正如我之前提到的,您不必担心转移到后台线程,因为 Room 会为您完成。
注 2: 同样,不可为 null 的参数。
TwalksRouteActivity
您直接从 activity 调用您的存储库。我不确定您正在应用的架构,但我希望在中间看到 Presenter 或 ViewModel。省略该细节,我建议您几乎总是避免使用 GlobalScope
启动协程。仅当您了解 GlobalScope
的工作原理并且完全确定自己在做什么时才使用 GlobalScope
。
- 了解有关此主题的更多信息here。
您可以使用 lifecycleScope
而不是 GlobalScope
,它在主线程上运行并且它具有生命周期意识。
将您的 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
传递给新的 activity。
我真的为此苦苦挣扎,希望能提供一些帮助。我正在学习 Android Kotlin 并构建一个应用程序,在 步行路线 (从云端下载)列表中显示 RecyclerView &, when a route is selected I want to display all details of the route - a simple Master-Detail app.因为,我正在学习,我也想尝试使用最佳实践。我使用 Room 数据库和存储库大部分工作正常。数据库已正确填充且 RecyclerView 显示路线列表。选择路线后,routeID
和其他详细信息会正确传递给 activity (TwalksRouteActivity.kt)
以显示详细信息,这工作正常。
但是,我需要使用 routeID 从数据库(存储库?)中查找路线,因此所有详细信息都可以在详细信息 activity 中找到,但我无法让它工作。我不想在一个包中传递所有详细信息,因为一旦它起作用,我将需要从详细信息 activity 进行其他数据库查找。我已经尝试了各种围绕协程的解决方案来避免线程阻塞,但都完全失败了。所以我的问题是,如何从细节 activity.
中正确地从我的 database/repository 中获取一行详情如下 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
}
}
}
您的代码无法正常工作,因为您没有在 RoutesRepository
class 中 return 使用 getRoute
中的任何内容。指定 return 类型,您将看到它。
您可以通过 returning 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: 我将 return 类型从 LiveData<Route>
更改为 Route
因为我假设您不希望它发生变化.
注 2: 我看不出将可为 null 的 routeID
作为参数有什么意义,所以我删除了 ?
.
路由库
通过之前的更改,您的 RoutesRepository
class 上的 getRoute
方法将如下所示:
suspend fun getRoute(id: Int) = database.routeDao.getRoute(id)
注意 1: 正如我之前提到的,您不必担心转移到后台线程,因为 Room 会为您完成。
注 2: 同样,不可为 null 的参数。
TwalksRouteActivity
您直接从 activity 调用您的存储库。我不确定您正在应用的架构,但我希望在中间看到 Presenter 或 ViewModel。省略该细节,我建议您几乎总是避免使用 GlobalScope
启动协程。仅当您了解 GlobalScope
的工作原理并且完全确定自己在做什么时才使用 GlobalScope
。
- 了解有关此主题的更多信息here。
您可以使用 lifecycleScope
而不是 GlobalScope
,它在主线程上运行并且它具有生命周期意识。
将您的 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
传递给新的 activity。