I've a Room database of Assignment and want to get loadAllByIds so I wrote the below codes, in the last one I'm trying to print the returned result but it fails.
#Entity
data class Assignment(
#PrimaryKey(autoGenerate = true) val uid: Int,
#ColumnInfo(name = "name") val name: String?,
#ColumnInfo(name = "title") val title: String? // ,
)
#Dao
interface AssignmentDao {
#Query("SELECT * FROM assignment WHERE uid IN (:userIds)")
fun loadAllByIds(userIds: IntArray): List<Assignment>
}
And
class AppRepository(private val assignmentDao: AssignmentDao) {
#WorkerThread
fun loadAllByIds(userIds: IntArray) {
assignmentDao.loadAllByIds(userIds)
}
}
And
class AppViewModel(application: Application) : AndroidViewModel(application) {
#WorkerThread
fun loadAllByIds(userIds: IntArray) = viewModelScope.launch(Dispatchers.IO) {
repository.loadAllByIds(userIds)
}
}
And
class AssignmentsAdapter(private val context: Context, private val chaptersList: ArrayList<Assignment>) :
RecyclerView.Adapter<AssignmentsAdapter.ViewHolder>() {
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.chapterName.setOnClickListener {
printAll(appViewModel.loadAllByIds(intArrayOf(position)))
}
}
fun printAll(strings: Collection<String>) {
for(s in strings) print("$s ")
println()
}
}
For printAll(appViewModel.loadAllByIds(intArrayOf(position))) I get an error:
Required: Collection
Found: Job
How can I fix it?
Definitely it won't gonna work and give you the compilation error as you are using coroutines viewModelScope.launch which returns Job.
What would I suggest you to add one more method in view model class which just returns the LiveData
Step 1: Modify your view model class like below
class AppViewModel(application: Application) : AndroidViewModel(application) {
private val assignments = MutableLiveData<List<Assignment>>()
#WorkerThread
fun loadAllByIds(userIds: IntArray) = viewModelScope.launch(Dispatchers.IO) {
assignments.postValue(repository.loadAllByIds(userIds))
}
fun getAssignments(): MutableLiveData<List<Assignment>> {
return assignments
}
}
Step 2: Then call below methods from fragment/activity.
for example calling from onCreate method
appViewModel.getAssignments().observe(this, Observer { assignments ->
printAll(assignments) // You can pass assignments object to your AssignmentsAdapter
})
appViewModel.loadAllByIds(intArrayOf(0,1,2))
Modified repository class
class AppRepository(private val assignmentDao: AssignmentDao) {
#WorkerThread
fun loadAllByIds(userIds: IntArray): List<Assingment> {
return assignmentDao.loadAllByIds(userIds)
}
}
Related
I am not sure what is exactly happening but when ApiService.apiService.getPokemon(name) in fun getPokemon in PokemonRepository.kt is called then the function getPokemon stops executing and emited livedata are then observed as null in DetailAktivity.kt instead of a valid Pokemon class.
I have checked the API call and it is working in other cases. I am new to Android programming, so I would appreciate some detailed explanation.
Here are the classes:
PokemonRepository.kt
class PokemonRepository(context: Context) {
companion object {
private val TAG = PokemonRepository::class.java.simpleName
}
private val pekemonDao = PokemonDatabase.getInstance(context).pokemonDao()
fun getPokemon(name: String) = liveData {
val disposable = emitSource(
pekemonDao.getOne(name).map {
it
}
)
val pokemon = ApiService.apiService.getPokemon(name)
try {
disposable.dispose()
pekemonDao.insertAllPokemons(pokemon)
pekemonDao.getOne(name).map {
it
}
} catch (e: Exception) {
Log.e(TAG, "Getting data from the Internet failed", e)
pekemonDao.getOne(name).map {
e
}
}
}
DetailActivity.kt
class DetailActivity : AppCompatActivity() {
companion object {
const val ITEM = "item"
}
private lateinit var binding: ActivityDetailBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityDetailBinding.inflate(layoutInflater)
setContentView(binding.root)
val vm: DetailViewModel by viewModels()
vm.pokemon.observe(
this,
{
binding.name.text = it.name
supportActionBar?.apply {
setDisplayShowTitleEnabled(true)
title = it.name
}
}
)
intent.extras?.apply {
vm.setCharacterId(getString(ITEM)!!)
}
}
}
DetailViewModel
class DetailViewModel(application: Application) : AndroidViewModel(application) {
private val repository = PokemonRepository(application)
private val name: MutableLiveData<String> = MutableLiveData()
val pokemon = name.switchMap { name ->
repository.getPokemon(name)
}
fun setCharacterId(characterId: String) {
name.value = characterId
}
}
ApiService.kt
interface ApiService {
#GET("pokemon?offset=0&limit=151")
suspend fun getPokemons(#Query("page") page: Int): NamedApiResourceList
#GET("pokemon/{name}")
suspend fun getPokemon(#Path("name") name: String): Pokemon
companion object {
private const val API_ENDPOINT = "https://pokeapi.co/api/v2/"
val apiService by lazy { create() }
private fun create(): ApiService = Retrofit.Builder()
.baseUrl(API_ENDPOINT)
.addConverterFactory(MoshiConverterFactory.create())
.client(OkHttpClient())
.build()
.create(ApiService::class.java)
}
}
Pokemon data class
#Parcelize
#JsonClass(generateAdapter = true)
#Entity
data class Pokemon(
#PrimaryKey val id: Int,
val name: String,
#ColumnInfo(name = "base_experience") val baseExperience: Int,
val height: Int,
#ColumnInfo(name = "is_default") val isDefault: Boolean,
val order: Int,
val weight: Int,
val sprites: PokemonSprites,
) : Parcelable
PokemonDao.kt
#Dao
interface PokemonDao {
#Query("SELECT * FROM namedapiresource")
fun getAll(): LiveData<List<NamedApiResource>>
#Query("SELECT * FROM pokemon WHERE name=:name")
fun getOne(name: String): LiveData<Pokemon>
#Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insertAllNamedApiResources(vararg characters: NamedApiResource)
#Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insertAllPokemons(vararg characters: Pokemon)
}
I would guess because of how your getPokemon is defined:
val disposable = emitSource(
// Attempts to get a pokemon from the database - presumably this does
// not exist at first so would return null first
pekemonDao.getOne(name).map {
it
}
)
// AFTER NULL IS RETURNED this tries to fetch from the API
val pokemon = ApiService.apiService.getPokemon(name)
try {
disposable.dispose()
pekemonDao.insertAllPokemons(pokemon)
// After fetching from API, finally returns a non-null
pekemonDao.getOne(name).map {
it
}
So maybe just get ride of the initial block?
val disposable = emitSource(
pekemonDao.getOne(name).map {
it
}
)
I'm new to android and room. I'm trying to make a local db but I'm struggling at this point where the database is not empty as the image below proves. But when I try to select any data it returns an empty list or null values. Note that the insert query works fine.
Code:
Entity:
#Entity(tableName = "product_table")
#Parcelize
data class Product(
#PrimaryKey
#SerializedName("id")
val id : String,
#SerializedName("title")
val title : String,
#SerializedName("price")
val price : String,
#SerializedName("category")
val category : String,
#SerializedName("description")
val description : String,
#SerializedName("image")
val image : String,
val color : String,
val size : String
): Parcelable
Dao:
#Dao
interface CartDao {
#Query("SELECT * FROM product_table")
fun get_all_carts(): LiveData<List<Product>>
#Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert_item_to_cart(product: Product) : Long
#Delete
suspend fun delete_item_from_cart(product: Product)
#Query("Delete FROM product_table")
fun delete_all_cart()
}
Database:
#Database(entities = [Product::class], version = 1)
abstract class ProductDatabase : RoomDatabase() {
abstract fun productDao(): CartDao
companion object{
#Volatile
private var INSTANCE: ProductDatabase? = null
fun getDataseClient(context: Context) : ProductDatabase {
if (INSTANCE != null) return INSTANCE!!
synchronized(this) {
INSTANCE = Room
.databaseBuilder(context, ProductDatabase::class.java, "product_database")
.fallbackToDestructiveMigration()
.build()
return INSTANCE!!
}
}
}
}
Repository:
class CartRepository {
companion object {
var productDatabase: ProductDatabase? = null
var product: LiveData<Product>? = null
fun initializeDB(context: Context) : ProductDatabase {
return ProductDatabase.getDataseClient(context)
}
fun get_all_data(context: Context) : List<Product> {
productDatabase = initializeDB(context)
var temp_list = emptyList<Product>()
CoroutineScope(Dispatchers.IO).launch {
temp_list = productDatabase!!.productDao().get_all_carts()
}
return temp_list
}
fun get_first_item(context: Context, input_id : String) : Product {
productDatabase = initializeDB(context)
var temp_item : Product = Product("","","","","","","","")
CoroutineScope(Dispatchers.IO).launch {
temp_item = productDatabase!!.productDao().get_item(input_id)
}
return temp_item
}
}
}
View Model:
#HiltViewModel
class CartFragmentViewModel #Inject constructor(
private val productDao : CartDao
) : ViewModel() {
var products_list : MutableLiveData<List<Product>>
init {
products_list = MutableLiveData()
}
fun get_all_products(context: Context) : List<Product>{
return CartRepository.get_all_data(context)
}
fun get_first_item(context: Context, input_id : String) : Product{
return CartRepository.get_first_item(context, input_id)
}
}
Fragment:
class CartFragment #Inject constructor(
) : Fragment(R.layout.cart_fragment_layout) {
lateinit var cart_list : List<Product>
val cart_adapter = CartRecyclerViewAdapter()
val viewModel by activityViewModels<CartFragmentViewModel>()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
rv_cart.adapter = cart_adapter
rv_cart.layoutManager = LinearLayoutManager(
activity?.applicationContext,
LinearLayoutManager.VERTICAL,
false
)
rv_cart.setHasFixedSize(true)
/*cart_list = viewModel.get_all_products(activity?.applicationContext!!)
cart_adapter.submitList(
cart_list
)
cart_adapter.notifyDataSetChanged()
Log.d(TAG, "Fetched Data: {${cart_list.get(1).title}}")*/
var p = viewModel.get_first_item(activity?.applicationContext!!, "1")
cart_adapter.submitList(
listOf(
p
)
)
cart_adapter.notifyDataSetChanged()
var s = p.title
Log.d(TAG, "Fetched Data: {$s}")
/*viewModel.get_first_item(activity?.applicationContext!!).observe(viewLifecycleOwner, Observer {
cart_adapter.submitList(listOf(it))
cart_adapter.notifyDataSetChanged()
})*/
//viewModel.get_first_item(activity?.applicationContext!!)
}
}
There are many comments and logs in the Fragment Class for the sake of trying to figure what the problem is. I can't really know what is happening and when I use LiveData as return type of dao get functions the app crashes. Hope someone can help me out and thanks for your attention.
The problem with your CartRepository methods.
fun get_first_item(context: Context, input_id : String) : Product {
productDatabase = initializeDB(context)
var temp_item : Product = Product("","","","","","","","")
CoroutineScope(Dispatchers.IO).launch {
temp_item = productDatabase!!.productDao().get_item(input_id)
}
return temp_item
}
In the above method, you are fetching an item in a background thread which means it goes into another thread and allows the return temp_item to always returns immediately, without blocking the code to wait for a result so that's why you are getting null and or empty list.
Solution is :
Make all database operation methods in CartRepository as suspended, see below:
Note: I use an object instead of class
object CartRepository {
var productDatabase: ProductDatabase? = null
fun initializeDB(context: Context) : ProductDatabase {
return ProductDatabase.getDataseClient(context)
}
suspend fun get_all_data(context: Context) : List<Product> {
productDatabase = initializeDB(context)
return productDatabase!!.productDao().get_all_carts()
}
suspend fun get_first_item(context: Context, input_id : String) : Product {
productDatabase = initializeDB(context)
return productDatabase!!.productDao().get_item(input_id)
}
}
And in your viewModel call this suspend function in viewModelScope like below:
#HiltViewModel
class CartFragmentViewModel #Inject constructor(
private val productDao : CartDao
) : ViewModel() {
.....
var productData = MutableLiveData<Product>()
fun get_first_item(context: Context, input_id: String) {
viewModelScope.lauch(Dispatchers.IO){
val data = CartRepository.get_first_item(context, input_id)
withContext(Dispatchers.Main){
productData.value = data
}
}
}
....
In your fragment call get_first_item first then observe you data productData and you can do the same things for other database operations also by following all steps.
I hope this will help you if you dont understand any code just let me know in the comments and please ignore the typos
To use a LiveData with room, firstly, you should attach an observer to the live data.
So instead of returning the value of LiveData from the Repository methods, you should return the Live Data object itself,
And then observe that Livedata in your Viewmodel class.
If you need the code sample, you can ask me in the comment section
Happy Coding :D
I have one entity "drinks" which have [id;name;thumb] and I`m using these entities for 2 response calls. One response returns me a NonAlcohol list of drinks, another AlcoholList, I'm using Room for caching the data. But when I run the app, I saw that my lists merged, after some thought, I found a solution to this problem, I added one Boolean field to my entity "alcoholStatus".But I can't understand how to set the data into this variable correctly using this AccessDataStrategy. I'm new to Android, and this is my learning project. Please give me the right way how to solve this problem.
https://github.com/YaroslavSulyma/LetsDrink/tree/master/app/src/main/java/com/example/letsdrink
Thanks a lot!
Entity
#Entity(tableName = "drinks")
data class DrinksModel(
#SerializedName("strDrink")
val strDrink: String,
#SerializedName("strDrinkThumb")
val strDrinkThumb: String?,
#SerializedName("idDrink")
#PrimaryKey
val idDrink: Int,
var alcohol: Boolean
)
DataAccessStrategyCode
fun <T, A> performGetOperation(
databaseQuery: () -> LiveData<T>,
networkCall: suspend () -> Resource<A>,
saveCallResult: suspend (A) -> Unit
): LiveData<Resource<T>> =
liveData(Dispatchers.IO) {
emit(Resource.loading())
val source = databaseQuery.invoke().map { Resource.success(it) }
emitSource(source)
val responseStatus = networkCall.invoke()
if (responseStatus.status == SUCCESS) {
saveCallResult(responseStatus.data!!)
} else if (responseStatus.status == ERROR) {
emit(Resource.error(responseStatus.message!!))
emitSource(source)
}
}
Resource
data class Resource<out T>(val status: Status, val data: T?, val message: String?) {
enum class Status {
SUCCESS,
ERROR,
LOADING
}
companion object {
fun <T> success(data: T): Resource<T> {
return Resource(Status.SUCCESS, data, null)
}
fun <T> error(message: String, data: T? = null): Resource<T> {
return Resource(Status.ERROR, data, message)
}
fun <T> loading(data: T? = null): Resource<T> {
return Resource(Status.LOADING, data, null)
}
}
}
Repository
class CocktailsRepository #Inject constructor(
private val remoteDataSource: CocktailsRemoteDataSource,
private val localDataSource: CocktailsDao
) {
fun getAlcoholicCocktails() = performGetOperation(
databaseQuery = { localDataSource.getAlcoholicCocktails() },
networkCall = { remoteDataSource.getAllAlcoholicCocktails()},
saveCallResult = { localDataSource.insertAllDrinks(it.drinks) }
)
fun getNonAlcoholicCocktails() = performGetOperation(
databaseQuery = { localDataSource.getNonAlcoholicCocktails() },
networkCall = { remoteDataSource.getAllNonAlcoholicCocktails() },
saveCallResult = { localDataSource.insertAllDrinks(it.drinks) }
)
}
DAO
#Dao
interface CocktailsDao {
#Query("SELECT * FROM drinks WHERE alcohol = 'true'")
fun getAlcoholicCocktails(): LiveData<List<DrinksModel>>
#Query("SELECT * FROM drinks WHERE alcohol = 'false'")
fun getNonAlcoholicCocktails(): LiveData<List<DrinksModel>>
#Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertAllDrinks(drinks: List<DrinksModel>)
}
RemoteDataSource
class CocktailsRemoteDataSource #Inject constructor(private val iCocktailApisService: ICocktailApisService) :
BaseDataSource() {
suspend fun getAllAlcoholicCocktails() =
getResult { iCocktailApisService.allAlcoholicAndNonAlcoholicCocktails("Alcoholic") }
suspend fun getAllNonAlcoholicCocktails() =
getResult { iCocktailApisService.allAlcoholicAndNonAlcoholicCocktails("Non_Alcoholic") }
}
First: I strongly recommend that you define separate data classes for your remote and local model classes and do the mapping between them when needed, for example:
Remote data model:
data class DrinkRemoteModel(
#SerializedName("idDrink")
val idDrink: Int,
#SerializedName("strDrink")
val strDrink: String,
#SerializedName("strDrinkThumb")
val strDrinkThumb: String?,
#SerializedName("alcohol")
var alcohol: Boolean
)
Local data model:
#Entity(tableName = "drinks")
data class DrinkLocalModel(
#PrimaryKey
#ColumnInfo(name = "idDrink")
val idDrink: Int,
#ColumnInfo(name = "strDrink")
val strDrink: String,
#ColumnInfo(name = "strDrinkThumb")
val strDrinkThumb: String?,
#ColumnInfo(name = "alcohol")
var alcohol: Boolean
)
Back to your implementation: I think what causing the problem is that Room maps Boolean fields in your entity to an integer column, 1 for true, and 0 for false, so try changing your querys in your DAO like following:
#Dao
interface CocktailsDao {
#Query("SELECT * FROM drinks WHERE alcohol = 1")
fun getAlcoholicCocktails(): LiveData<List<DrinksModel>>
#Query("SELECT * FROM drinks WHERE alcohol = 0")
fun getNonAlcoholicCocktails(): LiveData<List<DrinksModel>>
#Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertAllDrinks(drinks: List<DrinksModel>)
}
Alternatively: you can substitute your getAlcoholicCocktails and getNonAlcoholicCocktails with one DAO function, like this:
#Query("SELECT * FROM drinks WHERE alcohol = :isAlcoholic")
fun getCocktails(isAlcoholic : Boolean = true): LiveData<List<DrinksModel>>
I do not know how to implement a filter query properly inside the Repository and the ViewModel to use it to display the filtered string in a Textview or anything else really. My Entity, Dao, Repository, and ViewModel are as follows:
User.kt
#Entity(tableName = "user_data")
data class User (
#PrimaryKey(autoGenerate = true) val id: Int,
#ColumnInfo(name = "name") val name: String
)
UserDao.kt
#Dao
interface UserDao {
#Insert
fun addUser(user: User)
#Query("SELECT name FROM user_data WHERE name LIKE :filter LIMIT 1")
fun getFilteredUser(filter: String): LiveData<String>
}
UserRepository.kt
class UserRepository(private val userDao: UserDao) {
fun addUser(user: User) { userDao.addUser(User) }
fun getFilteredUser(filter: String) {userDao.getFilteredUser(filter)}
}
UserViewModel.kt
class UserViewModel(application: Application): AndroidViewModel(application) {
private val repository: UserRepository
init {
val userDao = UserDatabase.getDatabase(application).userDao()
repository = UserRepository(userDao )
}
fun addUser(user: User) {
viewModelScope.launch(Dispatchers.IO){
repository.addUser(user)
}
}
fun getFilteredUser(filter: String){
return repository.getFilteredUser(filter)
}
}
How would I proceed from here to make it possible to e.g. display the filtered User String in a textview or anything like that and how do I write the method correctly inside the repository and the viewmodel?
Thank you for your help!
Try the following
UserDao
change getFilteredUser as follows
#Query("SELECT name FROM user_data WHERE name LIKE '%' || :filter || '%' LIMIT 1")
fun getFilteredUser(filter: String): Stringl̥
UserRepo
use coroutines to perform the database I/O operations
suspend fun addUser(user: User) {
withContext(Dispatchers.IO) {
userDao.addUser(user)
}
}
suspend fun getFilteredUser(filter: String): String {
return withContext(Dispatchers.IO) {
userDao.getFilteredUser(filter)
}
}
ViewModel
fun addUser(user: User) {
viewModelScope.launch {
repository.addUser(user)
}
}
private val _dataToUi = MutableLiveData<String>()
val dataToUi: LiveData<String>
get() = _dataToUi
suspend fun getFilteredUser(filter: String): String? {
return withContext(Dispatchers.IO) {
repository.getFilteredUser(filter)
}
}
// to set the filterquery from the fragment/activity
fun setFliterQuery(query: String) {
viewModelScope.launch {
_dataToUi.value = getFilteredUser(query)
}
}
Activity
binding.button.setOnClickListener {
val queryKey = binding.queryKey.text.toString()
Log.i("activity", queryKey)
userViewModel.setFliterQuery(queryKey)
}
userViewModel.dataToUi.observe(this) { result ->
result?.apply {
Log.i("activity", result)
binding.resultText.text = result
}
}
I have allRecords - Live Data values obtained from the Room, through the Repository.
I want the handleSelectedItem method to change the values of one item in the LiveData<List<...>> if it is matched by id.
I tried to do this with Transformation.map (), but this code does not work
class RecordListViewModel #Inject constructor(val repository: RecordRepository): ViewModel() {
private var allRecords : LiveData<List<RecordItem>> = Transformations.map(repository.getAllRecording()){
records -> return#map records.map{ it.toItem()}
}
fun getAllRecords() : LiveData<List<RecordItem>>{
return allRecords
}
fun handleSelectedItem(id : Int) {
Log.d("HandleSelectedItem", "Test1")
allRecords = Transformations.map(allRecords) { records ->
return#map records.map {
if (it.id == id){
Log.d("HandleSelectedItem", "Test2")
it.copy(isSelected = true)
}
else{
Log.d("HandleSelectedItem", "Test3")
it.copy(isSelected = false)
}
}
}
}
}
Help to solve this problem
Update
Here offered MutableLiveData instead of LiveData.
Then both Repository and Dao should return MutableLiveData
Repository
fun getAllRecording(): MutableLiveData<List<RecordEntity>> =
appDatabase.getRecordDao().getAllRecording()
Dao
#Query("SELECT * FROM record")
fun getAllRecording() : MutableLiveData<List<RecordEntity>>
But Room database cannot return MutableLiveData
Error
D:\Project\VoiceRecording\app\build\tmp\kapt3\stubs\debug\ru\ddstudio\voicerecording\data\database\daos\RecordDao.java:17: error: Not sure how to convert a Cursor to this method's return type (androidx.lifecycle.MutableLiveData<java.util.List<ru.ddstudio.voicerecording.data.database.entities.RecordEntity>>).
public abstract androidx.lifecycle.MutableLiveData<java.util.List<ru.ddstudio.voicerecording.data.database.entities.RecordEntity>> getAllRecording();
Update2
private val allRecords = MediatorLiveData<List<RecordItem>>().apply {
val recordsRepository = repository.getAllRecording().map { records -> records.map { it.toItem() } }
addSource(recordsRepository)
}
Error addSource()
None of the following functions can be called with the arguments supplied:
#MainThread public final fun <S : Any!> addSource(#NonNull p0: LiveData<List<RecordItem>!>, #NonNull p1: (List<RecordItem>!) -> Unit): Unit defined in androidx.lifecycle.MediatorLiveData
#MainThread public open fun <S : Any!> addSource(#NonNull p0: LiveData<List<RecordItem>!>, #NonNull p1: Observer<in List<RecordItem>!>): Unit defined in androidx.lifecycle.MediatorLiveData
Your LiveData objects should be val. Use MutableLiveData/ MediatorLiveData and setValue or postValue to change the value in LiveData
class RecordListViewModel #Inject constructor(val repository: RecordRepository): ViewModel() {
private val allRecords = MediatorLiveData<List<RecordItem>>().apply {
val recordsLiveData = repository.getAllRecording().map { records -> records.map { it.toItem() } }
addSource(recordsLiveData) { records ->
value = records
}
}
fun getAllRecords() : LiveData<List<RecordItem>> {
return allRecords
}
fun handleSelectedItem(id : Int) {
Log.d("HandleSelectedItem", "Test1")
allRecords.value?.let { records ->
allRecords.value = records.map { it.copy(isSelected = it.id == id) }
}
}
}
Then both Repository and Dao should return MutableLiveData
No, it shouldn't. Use LiveData in your DAO and Repository