I have a fragment with a RecyclerView and FloatingActionButton. Pressing the fab opens a dialog with the settings for the new item. By clicking on the positive button, I insert a new element into the database, doing the following steps:
1) Call from dialog
viewModel.createItem()
2) In the ViewModel
fun createItem() {
return repository.insertItem(Item("${name.value}"))
}
3) Repository looks like
#Singleton
class Repository #Inject constructor(
private val appExecutors: AppExecutors,
private val itemDao: ItemDao
) {
fun insertItem(item: Item) {
appExecutors.diskIO().execute{ itemDao.insert(item) }
}
fun loadItemList() = itemDao.getItemList()
}
4) Dao
#Dao
interface ItemDao {
#Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(item: Item) : Long
#Query("SELECT * FROM item ORDER BY name")
fun getItemList() : LiveData<List<Item>>
}
5) Item
#Entity (tableName = "item")
data class Item(
#PrimaryKey(autoGenerate = true) #ColumnInfo(name = "_id") val id: Long,
#ColumnInfo(name = "name") val name: String
) {
#Ignore
constructor(name: String) : this(0, name)
}
And then I want to navigate to the detail fragment for the inserted item. So i need the id of this item, which is returned by Room. But how can I return id from execute method in the repository? Or maybe you know another way to do this.
P.s. I use Dagger and all of the Architecture Components libraries.
itemDao.insert(item) already returns the id of the new element. What you need to do now is to send this id back to your frontend (or whereever you want to use the new id) via a callback.
You could create a listener:
interface ItemInsertedListener {
fun onItemInserted(itemId: Long)
}
Add the interface as optional argument for your insertItem method:
fun insertItem(item: Item, callback: ItemInsertedListener?) {
appExecutors.diskIO().execute {
val id = itemDao.insert(item)
callback?.onItemInserted(id)
}
}
And finally pass the ItemInsertedListener to your repo in your createItem method. When the callback returns you can redirect the user to the Fragment, given that the activity is still visible. Also bear in mind that the callback might run on a different thread, so explicitly switch to the UI thread before making UI changes.
Related
I am learning Android and I am stuck with making all those components work together. I am talking about ViewModel, LiveData, Room, Retrofit and coroutines.
So, what I want to achieve is the following:
When the application starts up, I want to check whether the user is logged in or not. If it is not logged I do nothing, if he is logged in , i log his name in the main activity. The user can authenticate with another api call. Then, when the user is updated, the main activity should reflect the change to user.
I partially followed the google codelab here https://developer.android.com/jetpack/guide , but it's incomplete and I get lost in it.
So following are the code blocks:
UserDao.kt
Database queries using Room DAO
#Dao
interface UserDao {
#Query("SELECT * FROM user WHERE id = :userId")
fun get(userId: Long): LiveData<UserEntity?>
#Insert(onConflict = REPLACE)
suspend fun insert(userEntity: UserEntity)
#Query("SELECT COUNT(*) FROM user WHERE last_update >= :timeout")
suspend fun hasUser(timeout: Long): Boolean
}
UserService.kt
This is a simply api call using Retrofit
interface UserService {
#GET("users/current-user")
suspend fun getUser(): Response<User>
}
UserRepository.kt
main activity should call get on load and refresh the user (call the api and retrieve user id), save the user id the in the savedStateHandler, and retrieve the user from the database where it's just been saved. But at the very first startup, savedStateHandler is empty. When I get the user from the api, I store it in the database, but the userId variable passed to userDao.get(userId) is still null so nothing is loaded from the database. Where and when should I save the id of the user in the savedStateHandler? I have no reference of it in the Repository.
Also when the application starts up the call is not fired. How can I fire it from the activity?
Not sure where to set the user in the mutable live data variable in order to trigger the observer in the activity.
suspend fun insert(userEntity: UserEntity) {
userDao.insert(userEntity)
}
suspend fun get(userId: Long): LiveData<UserEntity?> {
refreshUser()
return userDao.get(userId)
}
private suspend fun refreshUser() = withContext(Dispatchers.IO) {
launch {
// Check if user data was fetched recently.
val userExists = userDao.hasUser(FRESH_TIMEOUT)
if (!userExists) {
// Refreshes the data.
val response = userService.getUser()
try {
if (response.isSuccessful) {
Log.i("user", "Success ${response.code()} - ${response.message()}")
// Updates the database. The LiveData object automatically
// refreshes, so we don't need to do anything else here.
val body: User? = response.body()
val user = UserEntity(
body!!.id,
body.email,
body.name.firstName,
body.name.lastName,
body.telephone,
body.dateOfBirth,
System.currentTimeMillis() / 1000
)
userDao.insert(user)
} else {
Log.w(
"user",
"Not successful ${response.code()} - ${response.message()}"
)
}
} catch (e: HttpException) {
Log.e("user", "Exception ${response.code()} - ${response.message()}")
}
}
}
}
UserViewModel.kt
class UserViewModel #ViewModelInject constructor(
private val repository: UserRepository,
#Assisted private val savedStateHandle: SavedStateHandle) : ViewModel() {
fun insert(userEntity: UserEntity) = viewModelScope.launch {
repository.insert(userEntity)
}
private val userId: Long? = savedStateHandle["uid"]
val user: LiveData<UserEntity?>
get() = liveData { repository.get(userId) }
}
MainActivity.kt
viewModel.user.observe(this) { user ->
Log.i("USER IS LOGGED", user.toString())
Toast.makeText(this, "HELLO ${user?.email}!!", Toast.LENGTH_SHORT).show()
}
private val userId: Long? = savedStateHandle["uid"]
val user: LiveData<UserEntity?>
get() = liveData { repository.get(userId) }
Should be
private val userId: MutableLiveData<Long> = savedStateHandle.getLiveData("uid")
val user: LiveData<UserEntity?>
get() = userId.switchMap { userId -> liveData { repository.get(userId) } }
Working with Androind and Room for the first time, and i was able to follow a few codelabs and tutorials to achieve inserting and showing a list of my entities, but i cant seem to be able to use my other Repository methods in my ViewModel due to a type mismatch, here is my ViewModel file
class CustomerViewModel(application: Application) : AndroidViewModel(application) {
// The ViewModel maintains a reference to the repository to get data.
private val repository: CustomerRepository
// LiveData gives us updated words when they change.
val allCustomers: LiveData<List<Customer>>
init {
// Gets reference to Dao from db to construct
// the correct repo.
val customerDao = AppDatabase.getInstance(application).customerDao()
repository = CustomerRepository(customerDao)
allCustomers = repository.getCustomers()
}
fun insert(customer: Customer) = viewModelScope.launch {
repository.insert(customer)
}
}
and im trying to add a method like
fun find(id: Int) = viewModelScope.launch {
return repository.getCustomerByLocalId(id)
}
but the ide says there's a type mismatch here? Required: Customer, Found: Job
here is my repository:
class CustomerRepository(private val customerDao: CustomerDao) {
fun getCustomers(): LiveData<List<Customer>> = customerDao.getAlphabetizedCustomers()
suspend fun getCustomerByLocalId(local_Id: Int): Customer =
customerDao.customerByLocalId(local_Id)
suspend fun insert(customer: Customer) {
customerDao.insert(customer)
}
companion object {
// For Singleton instantiation
#Volatile
private var instance: CustomerRepository? = null
fun getInstance(customerDao: CustomerDao) =
instance ?: synchronized(this) {
instance ?: CustomerRepository(customerDao).also { instance = it }
}
}
}
methods in CustomerDao
#Query("SELECT * from customers ORDER BY name ASC")
fun getAlphabetizedCustomers(): LiveData<List<Customer>>
#Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insert(customer: Customer)
#Query("SELECT * FROM customers WHERE local_id = :localId")
suspend fun customerByLocalId(localId: Int): Customer
EDIT
I tried #lena-bru 's suggestion but the error is still there, there appears to be 2 different ones, the type mismatch and that there should not be a return. are you supposed to create this method in a different location?
The IDE error
change this:
fun find(id: Int) = viewModelScope.launch {
return repository.getCustomerByLocalId(id)
}
to this:
fun find(id: Int): Customer = viewModelScope.launch {
withContext(Dispatchers.IO){
repository.getCustomerByLocalId(id)
}
}
Your find method as defined above is void, it needs to return type Customer
Also you need to provide a context, and remove the return keyword
Room , Mvvm Created , Kotlin - Add , Delete are working
but when i run update nothing changes
i had debugged the app , dao , repo and viewModel class are showing the changes but no changes reflect on my recyclerView or when i destroy and again open my app it does not changes
My Model/Entity Classs :
data class IceBreakerModel(val question:String,
val date:String,
val option1:String,
val option2:String,
val option3:String){
#PrimaryKey(autoGenerate = true)
var id: Int = 0
}
Dao : -
interface IcebreakerDao {
#Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertQuestion(question:IceBreakerModel)
#Query("DELETE FROM ice_breaker_questions WHERE id=:ID")
fun deleteQuestion(ID:Int)
#Query("SELECT * FROM ice_breaker_questions")
fun getAllIcebreakerQuestions():LiveData<List<IceBreakerModel>>
#Update(onConflict = OnConflictStrategy.REPLACE)
fun updateIcebreakerQuestion(question:IceBreakerModel)
// #Query("UPDATE ice_breaker_questions SET question =:newQuestion ,date=:newDate,option1=:newOption1,option2=:newOption2,option3=:newOption3 WHERE id=:ID")
// fun updateQuestion(newQuestion:String,newDate:String,newOption1:String,newOption2:String,newOption3:String,ID:Int)
}
Repo :-
private class UpdateQuestion(dao:IcebreakerDao):AsyncTask<IceBreakerModel,Unit,Unit>(){
val questionDao = dao
override fun doInBackground(vararg params: IceBreakerModel?) {
val model = params[0]!!
questionDao.updateIcebreakerQuestion(model)
// questionDao.updateQuestion(model.question,model.date,model.option1,model.option2,model.option3,model.id)
}
}
and Finally Main Activity where i am updating
iceBreakerViewModel.updateQuestion(IceBreakerModel(question,Date().toString(),option1,option2,option3))
adapter.notifyDataSetChanged()
The method you commented:
#Query("UPDATE ice_breaker_questions SET question =:newQuestion ,date=:newDate,option1=:newOption1,option2=:newOption2,option3=:newOption3 WHERE id=:ID")
fun updateQuestion(newQuestion:String,newDate:String,newOption1:String,newOption2:String,newOption3:String,ID:Int)
Is the way to go. At least it's how I'm used to update my columns.
Also, I think you need to adapter.notifyDataSetChanged() inside the observer of the ViewModel in your activity and in your ViewModel, assuming you are using LiveData.
Let me know if you need anything else, as I might missed the point somewhere
I'm having trouble figuring out how I'm supposed to update data in a Room Database after a change in RecyclerView item order. How am I supposed to update LiveData items in Room based on user action?
Using ItemTouchHelper.Callback I've set up an onMove callback that can make changes to the order of items presented to the user (on drag and drop), but when I make a call to update the order of items in the Room Database, using a ViewModel object, the user can then only move items one at a time. So if you drag an item, it will only move one space.
This is the onMove function I have defined in the ListAdapter, which implements ItemTouchHelperAdapter for the callback.
override fun onMove(
recyclerView: RecyclerView,
fromViewHolder: RecyclerView.ViewHolder,
toViewHolder: RecyclerView.ViewHolder
): Boolean {
d(this.TAG, "swap viewHolders: " + fromViewHolder.adapterPosition + " to " + toViewHolder.adapterPosition)
val workoutRoutine1 = workoutRoutines[fromViewHolder.adapterPosition]
val workoutRoutine2 = workoutRoutines[toViewHolder.adapterPosition]
workoutRoutine1.orderNumber = toViewHolder.adapterPosition.toLong()
workoutRoutine2.orderNumber = fromViewHolder.adapterPosition.toLong()
//this.workoutRoutinesViewModel.update(workoutRoutine1)
//this.workoutRoutinesViewModel.update(workoutRoutine2)
notifyItemMoved(fromViewHolder.adapterPosition, toViewHolder.adapterPosition)
return true
}
This is my DAO object
#Dao
interface WorkoutRoutineDAO {
#Insert
suspend fun insert(workoutRoutine: WorkoutRoutine)
#Update(onConflict = OnConflictStrategy.REPLACE)
suspend fun update(workoutRoutine: WorkoutRoutine)
#Delete
suspend fun delete(workoutRoutine: WorkoutRoutine)
#Query("DELETE FROM workout_routine_table")
fun deleteAll()
#Query("SELECT * FROM workout_routine_table ORDER BY order_number ASC")
fun getAllWorkoutRoutines(): LiveData<List<WorkoutRoutine>>
#Query("SELECT COALESCE(MAX(order_number), -1) FROM workout_routine_table")
fun getLargestOrderNumber(): Long
}
This is my RoomDatabase object
#Database(entities = [WorkoutRoutine::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
abstract fun workoutRoutineDAO(): WorkoutRoutineDAO
companion object {
#Volatile
private var INSTANCE: AppDatabase? = null
fun getDatabase(
context: Context,
scope: CoroutineScope
): AppDatabase {
return INSTANCE ?: synchronized(this) {
val instance =
Room.databaseBuilder(context.applicationContext, AppDatabase::class.java, "app_database")
.addCallback(WorkoutRoutineDatabaseCallback(scope))
.build()
INSTANCE = instance
instance
}
}
}
private class WorkoutRoutineDatabaseCallback(
private val scope: CoroutineScope
) : RoomDatabase.Callback() {
}
}
This is the ViewModel object I implemented.
class WorkoutRoutinesViewModel(application: Application) : AndroidViewModel(application) {
private val workoutRoutinesRepository: WorkoutRoutineRepository
val allWorkoutRoutines: LiveData<List<WorkoutRoutine>>
init {
// Get the DAO
val workoutRoutineDAO = AppDatabase.getDatabase(application, viewModelScope).workoutRoutineDAO()
// Build a new data repository for workout routines
workoutRoutinesRepository = WorkoutRoutineRepository(workoutRoutineDAO)
// Get a live view of the workout routines database
allWorkoutRoutines = workoutRoutinesRepository.allRoutines
}
fun insert(workoutRoutine: WorkoutRoutine) = viewModelScope.launch(Dispatchers.IO) {
workoutRoutinesRepository.insert(workoutRoutine)
}
fun update(workoutRoutine: WorkoutRoutine) = viewModelScope.launch(Dispatchers.IO) {
workoutRoutinesRepository.update(workoutRoutine)
}
fun delete(workoutRoutine: WorkoutRoutine) = viewModelScope.launch(Dispatchers.IO) {
workoutRoutinesRepository.delete(workoutRoutine)
}
}
I expect the user to be able to move the item n spaces, then drop, and have the update to Room database execute when the user drops the item, but if I put the Room update in the onMove method, the user can only move the item once.
I'm trying to understand the right way to update the Room data when the order of objects change in the recycler view. I'm trying to get the order of those objects to persist even when the user exits the app or changes activities or whatever. How am I supposed to echo those changes back to the Room database, using LiveData?
You can follow this guide on Udacity. It is free, made by Google and uses Kotlin.
Hi i have a list of items that i retrieve from a data source and then i apply a map on the observable to store the data into ROOM.
It manages to add it to the table but when i try to retrieve a LiveData of it, It doesnt seem to notify my observer of the results.
There is definetly data in the table and my query works as i changed the return time from LiveData to simple a List and that worked perfectly fine
Here is my data class
#Entity
data class Item(
#PrimaryKey
val id:String,
val title: String,
val description: String,
val imgUrl: String,
val usageRules : List<String>,
)
Here is my DAO that exposes a func that add all the list of Items
#Dao
interface MyDao {
#Insert(onConflict = OnConflictStrategy.IGNORE)
fun saveAllItems(itemList: MutableList<Item>)
#Query("SELECT * FROM items")
fun getAllItems(): LiveData<MutableList<Item>>
}
My DB class
#Database(entities = arrayOf(Item::class), version = 1, exportSchema = false)
#TypeConverters(UsageRulesTypeConverter::class)
abstract class MyDatabase : RoomDatabase() {
abstract fun getProductDao(): MyDao
}
Below is how i insert data into the database:
#Inject
lateInt val database : MyDatabase
override fun getAllItems(): Single<LiveData<MutableList<Item>>> {
//retrieve new data using rertrofit
networkController.getItems().map { responseItems ->
saveAllItems(responseItems )
getItems()
}
#Transaction
fun saveAllItems(allItems: ItemsDataSource) {
database.getProductDao().saveAllItems(allItems.loadedItems)
database.getProductDao().saveAllItems(allItems.savedItems)
database.getProductDao().saveAllItems(allItems.expiredItems)
database.getProductDao().saveAllItems(allItems.unloadedItems)
}
fun getItems() : LiveData<MutableList<Item>>{
return database.getProductDao().getAllItems()
}
My data source retrieved 4 lists of Items that i then save all of them in one entity/table but LiveData doesnt notify my UI about it?
ViewModel:
override fun getUnloadedOffers(): LiveData<MutableList<ProductOffer>> {
if (!this::itemsLiveData.isInitialized) {
itemsLiveData= MutableLiveData()
//get data from network or database
itemDelegator.getItems()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({ items->
itemsLiveData= items
})
}
return itemsLiveData
}
UI
viewModel = ViewModelProviders.of(this, viewModelFactory).get(MyViewModel::class.java)
viewModel.getItems().observe(this, Observer {
items->
items?.let {
adapter = SomeAdapter(items)
itemsRecyclerView.adapter = adapter
adapter.notifyDataSetChanged()
}
})
Does your function getUnloadedOffers() return empty mutableList? I'm not sure if passing items to itemsLiveData is going to work, since observing LiveData is running on bg thread (if I'm not mistaken)
Try to use vararg instead of List when saving collection of data.
#Insert(onConflict = OnConflictStrategy.IGNORE)
fun saveAllItems(vararg itemList: Item)
And then you can call this method using * (also known as Spread Operator) on your list like this
saveAllItems(*responseItems.toTypedArray())