#android #kotlin #android-room #repository-pattern #kotlin-coroutines
#Android #kotlin #android-room #репозиторий-шаблон #kotlin-сопрограммы
Вопрос:
Я действительно борюсь с этим и был бы признателен за помощь, пожалуйста. Я изучаю Android Kotlin и создаю приложение, которое отображает список пешеходных маршрутов (загруженных из облака) в RecyclerView, и при выборе маршрута я хочу отобразить все детали маршрута — простое приложение с основными деталями. Поскольку я учусь, я также хочу попробовать и использовать лучшие практики. У меня большая часть этого работает нормально, используя базу данных комнат и репозиторий. База данных заполнена правильно, и RecyclerView отображает список маршрутов. Когда выбран маршрут routeID
, и другие сведения правильно передаются в действие (TwalksRouteActivity.kt)
для отображения сведений, и это работает нормально.
Однако мне нужно использовать routeId для поиска маршрута из базы данных (репозитория?) итак, все детали доступны в операции detail, но я не могу заставить это работать. Я не хочу передавать все детали в пакете, потому что мне нужно будет выполнять другие поисковые запросы в базе данных из действия detail, как только это заработает. Я пробовал всевозможные решения вокруг сопрограмм, чтобы избежать блокировки потоков, но потерпел полный провал. Итак, мой вопрос в том, как мне правильно получить строку из моей базы данных / репозитория из операции detail.
Вот детальная активность (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 возвращает kotlin.Единица измерения в журнале, поэтому запись не возвращается. Вот где мне нужна помощь!
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()
}
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:
Ваш код не работает, потому что вы ничего не возвращаете из getRoute
своего RoutesRepository
класса. Укажите возвращаемый тип, и вы его увидите.
Вы можете решить эту проблему, вернув withContext
блок, но я хотел бы предложить вам некоторые изменения, поскольку вы сказали, что учитесь, а также хотите попробовать применить лучшие практики.
RouteDao
Room поддерживает сопрограммы начиная с версии 2.1. Все, что вам нужно сделать, это пометить ваши методы DAO ключевым suspend
словом. Вам не нужно беспокоиться о вызове suspend
метода DAO в вашем основном потоке, поскольку он приостанавливается, и Room удается выполнить запрос в фоновом потоке.
- Узнайте больше об этой теме здесь.
Итак, ваш getRoute
метод DAO будет выглядеть следующим образом:
@Query("select * from databaseroute where routeID = :routeID")
suspend fun getRoute(routeID: Int): Route
Примечание 1: я изменил возвращаемый тип с LiveData<Route>
на Route
, поскольку я предполагаю, что вы не ожидаете его изменения.
Примечание 2: я не вижу смысла в том, чтобы иметь значение null routeID
в качестве аргумента, поэтому я удалил ?
.
RoutesRepository
С предыдущим изменением ваш getRoute
метод в вашем RoutesRepository
классе будет выглядеть следующим образом:
suspend fun getRoute(id: Int) = database.routeDao.getRoute(id)
Примечание 1: Как я упоминал ранее, вам не нужно беспокоиться о переходе в фоновый поток, поскольку Room сделает это за вас.
Примечание 2: Опять же, аргумент not nullable.
TwalksRouteActivity
Вы вызываете свой репозиторий непосредственно из своего действия. Я не уверен в архитектуре, которую вы применяете, но я бы ожидал увидеть Presenter или ViewModel в середине. Опуская эту деталь, я предлагаю вам избегать запуска сопрограммы с GlobalScope
почти всегда. Используйте GlobalScope
только тогда, когда вы знаете, как GlobalScope
это работает, и вы полностью уверены в том, что делаете.
- Узнайте больше об этой теме здесь.
Вместо 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
в свое новое действие.
Комментарии:
1. Большое вам спасибо, Гленн, ваш ответ сработал отлично, был ясен, и ваше объяснение действительно помогло мне понять, что происходит
![]()