I have a recycler view implemented in MVVM architecture with room database. Created this query to sort recycler view in DAO file.
#Query("SELECT * FROM task_table ORDER BY " +
"CASE WHEN:choice =1 THEN date END ASC," +
"CASE WHEN:choice=2 THEN title END ASC")
fun readAllData(choice : Int): LiveData<MutableList<Task>>
Repository file
class TaskRepository(private val taskDao: TaskDao) {
val readAllData: LiveData<MutableList<Task>> = taskDao.readAllData(1)
suspend fun addTask(task: Task) {
taskDao.addTask(task)
}
suspend fun deleteTask(task: Task) {
taskDao.deleteTask(task)
}
suspend fun updateTask(task: Task) {
taskDao.updateTask(task)
}
fun deleteAllTasks() {
taskDao.deleteAllTasks()
}
}
ViewModel file
class TaskViewModel(
application: Application,
) : AndroidViewModel(application) {
val readAllData: LiveData<MutableList<Task>>
private val repository: TaskRepository
init {
val taskDao = TaskDatabase.getDatabase(application).taskDao()
repository = TaskRepository(taskDao)
readAllData = repository.readAllData
}
fun addTask(task: Task) {
viewModelScope.launch(Dispatchers.IO) {
repository.addTask(task)
}
}
fun updateTask(task: Task) {
viewModelScope.launch(Dispatchers.IO) {
repository.updateTask(task)
}
}
fun deleteTask(task: Task) {
viewModelScope.launch(Dispatchers.IO) {
repository.deleteTask(task)
}
}
fun deleteAllTask() {
viewModelScope.launch(Dispatchers.IO) {
repository.deleteAllTasks()
}
}
}
I don't know how to implement the sort menu in UI in order to make my room database sort in attributes. Which means I want to pass data from Fragment to Repository file so that DAO can take parameter.
First, you don't want to use MutableList with LiveData. If you would just add/remove from the value that is in the LiveData the underlying object is not changed and you won't get updates from it.
The most common approach(AFAIK) is to have LiveData of the thing that modifies your query. With combination of Transformations.switchMap you can use the output of one LiveData and translate it into another LiveData(your query).
class TaskViewModel(application: Application) : AndroidViewModel(application) {
private val taskDao: TaskDatabase.getDatabase(application).taskDao()
private val repository: TaskRepository = TaskRepository(taskDao)
private val _filter: MutableLiveData<Int> = MutableLiveData(1)
val filter: LiveData<Int>
get() = _filter
private val _data: MutableLiveData<List<Task>> = Transformations.switchMap(filter) { choice ->
repository.readAllData(choice)
}
val readAllData: LiveData<List<Task>>
get() = _data
fun updateFilter(filter: Int) {
_filter.postValue(filter)
}
}
Any change in _filter will trigger re-evaluaing via the switchMap which will get readAllData from your repository.
Related
I wanna create a menu about sorting by Task's attributes but don't know how because I am using database combined with Live Data, View Model and coroutine which is very complicated. Please show me how to handle these logics.
Data file
#Entity(tableName = "task_table")
data class Task(
//id is the PrimaryKey which is auto generated by Android Room Database
#PrimaryKey(autoGenerate = true)
val id: Int,
val date: String,
val content: String,
var timeLeft: String,
var isDone: Boolean
):Parcelable
Dao file
#Dao
interface TaskDao {
// means it will be just ignore if there is a new exactly same task then we're gonna just ignore it
#Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun addTask(task: Task)
#Delete
suspend fun deleteTask(task: Task)
#Update
suspend fun updateTask(task: Task)
#Query("SELECT * FROM task_table order by timeLeft ASC")
fun readAllData(): LiveData<MutableList<Task>>
#Query("SELECT COUNT(id) FROM task_table")
fun getCount(): Int
}
Database file
#Database(entities = [Task::class], version = 1, exportSchema = false)
abstract class TaskDatabase : RoomDatabase() {
abstract fun taskDao(): TaskDao
companion object {
#Volatile
private var INSTANCE: TaskDatabase? = null
fun getDatabase(context: Context): TaskDatabase {
val tempInstance = INSTANCE
if (tempInstance != null) {
return tempInstance
}
synchronized(this)
{
val instance = Room.databaseBuilder(
context.applicationContext,
TaskDatabase::class.java,
"task_database"
).build()
INSTANCE = instance
return instance
}
}
}
}
Repository file
// a repository class abstracts access to multiple data sources
class TaskRepository(private val taskDao: TaskDao) {
val readAllData: LiveData<MutableList<Task>> = taskDao.readAllData()
suspend fun addTask(task: Task) {
taskDao.addTask(task)
}
suspend fun deleteTask(task: Task) {
taskDao.deleteTask(task)
}
suspend fun updateTask(task: Task) {
taskDao.updateTask(task)
}
}
ViewModel file
class TaskViewModel(application: Application) : AndroidViewModel(application) {
val readAllData: LiveData<MutableList<Task>>
private val repository: TaskRepository
init {
val taskDao = TaskDatabase.getDatabase(application).taskDao()
repository = TaskRepository(taskDao)
readAllData = repository.readAllData
}
fun addTask(task: Task) {
viewModelScope.launch(Dispatchers.IO) {
repository.addTask(task)
}
}
fun updateTask(task: Task) {
viewModelScope.launch(Dispatchers.IO) {
repository.updateTask(task)
}
}
fun deleteTask(task: Task) {
viewModelScope.launch(Dispatchers.IO) {
repository.deleteTask(task)
}
}
}
In my fragment these will be a menu to choose how recyclerview items should be sorted. Don't know how to handle these logic. Please help. I appreciate!
I want to get callback when insert data successfully in Roomdb with coroutine
MyDao.kt
#Dao
interface MyDao {
#Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(obj: Task): Long
}
TaskViewModel.kt
class TaskViewModel(var context:Context) : ViewModel{
private var appDao: AppDao
init {
val db = AppDatabase.getInstance(context)
appDao = db.appDao()
}
fun insertTask(tast: Task) {
GlobalScope.launch {
val mID = appDao.insert(task)
}
}
}
How to return mID from insertTask() method ?
Thanks you in advance
Wrap the call with the withContext and mark the function as suspend
suspend insertTask(task: Task) = withContext(Dispatchers.IO) { appDao.insertTask(task) }
And from your view:
fun saveTask(t: Task) = lifecycleScope.launch {
val id = viewModel.insertTask(t)
Toast.makeText(context, "Task $id has been inserted", Toast.LENGTH_SHORT).show()
}
You could also return a LiveData that triggers a callback when it's done:
fun insertTask(task: Task): LiveData<Long> {
val liveData = MutableLiveData<Long>()
viewModelScope.launch {
liveData.value = dao.insertTask(task)
}
return liveData
}
Further advice
Don't keep a reference to a context in your viewmodel. If you need the context use AndroidViewModel and AndroidViewModel#getApplication()
I'd like to get a User from my Room database using the Repository, and then observing it in the OverviewFragment.
What I tried:
using an Observer in the fragment, but I got a NullPointerException.
using #{overviewViewModel.user.email} in fragment_overview.xml: the application runs but nothing is displayed.
Unit Testing and Instrumented Testing with a FakeRepository. The tests pass and the data are displayed.
What I use:
Android Studio (up to date)
Kotlin
MVVM architecture
Repository pattern linked to a Room database
Constructor dependency injection (for the viewmodels)
ServiceLocator pattern for testing
User Flow (Navigation)
The User opens the app, fills a form and clicks the registration button in the RegistrationFragment, then navigates to the OverviewFragment, where his data (Email, Password) are displayed.
RegistrationViewModel:
class RegistrationViewModel(private val repository: IUserRepository) : ViewModel() {
private var viewModelJob = Job()
private val uiScope = CoroutineScope(Dispatchers.Main + viewModelJob)
private val _user = MutableLiveData<User>()
val user: LiveData<User>
get() = _user
val email = MutableLiveData<String>()
val password = MutableLiveData<String>()
[...]
fun onRegister() {
val userEmail = email.value.toString()
val userPassword = password.value.toString()
registerUser(User(userEmail, userPassword))
}
private fun registerUser(newUser: User) = uiScope.launch {
repository.registerUser(newUser)
}
[...]
}
OverviewViewModel
class OverviewViewModel(val database: UserDatabaseDao,
private val userRepository: IUserRepository
) : ViewModel() {
private val _user = MutableLiveData<User>()
val user: LiveData<User>
get() = _user
init {
getUser()
}
private fun getUser() {
runBlocking {_user.value = userRepository.getUser().value }
}
OverviewFragment:
class OverviewFragment : Fragment() {
[...]
overviewViewModel.user.observe(this, Observer {
binding.apply {
textview.text = overviewViewModel.user.value!!.email
textview2.text = overviewViewModel.user.value!!.password
}
})
return binding.root
}
}
Repository:
class UserRepository(private val dataSource: UserDatabaseDao,
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO) :
IUserRepository {
override suspend fun registerUser(user: User) = withContext(ioDispatcher) {
dataSource.insert(user)
}
override suspend fun getUser(): LiveData<User?> = withContext(ioDispatcher) {
return#withContext dataSource.getUser()
}
override suspend fun getUser(email: String): LiveData<User?> = withContext(ioDispatcher) {
return#withContext dataSource.getUser(email)
}
}
DatabaseDao:
#Dao
interface UserDatabaseDao {
#Insert
fun insert(user: User)
#Query("SELECT * from user_table WHERE email = :key")
fun getUser(key: String): LiveData<User?>
#Query("SELECT * from user_table")
fun getUser(): LiveData<User?>
#Query("SELECT * from user_table")
fun getAllUsers(): LiveData<List<User>>
}
Note: there's only one user in the database, the one that fills the form in the RegistrationFragment.
i want to use SearchView for search some element in room db and i have a problem with this because i cant use getFilter in RecyclerViewAdapter because i have ViewModel maybe whoes know how to combine all of this element in one project.
I search one way use Transormations.switchMap. But I couldn’t connect them.
ProductViewModel
class ProductViewModel(application: Application) : AndroidViewModel(application) {
private val repository: ProductRepository
val allProducts: LiveData<List<ProductEntity>>
private val searchStringLiveData = MutableLiveData<String>()
init {
val productDao = ProductsDB.getDatabase(application, viewModelScope).productDao()
repository = ProductRepository(productDao)
allProducts = repository.allProducts
searchStringLiveData.value = ""
}
fun insert(productEntity: ProductEntity) = viewModelScope.launch {
repository.insert(productEntity)
}
val products = Transformations.switchMap(searchStringLiveData) { string ->
repository.getAllListByName(string)
}
fun searchNameChanged(name: String) {
searchStringLiveData.value = name
}
}
ProductDao
interface ProductDao {
#Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insertProduct(productEntity: ProductEntity)
#Query("SELECT * from products")
fun getListAllProducts(): LiveData<List<ProductEntity>>
#Query("DELETE FROM products")
suspend fun deleteAll()
#Query("SELECT * FROM products where product_name_ent LIKE :name or LOWER(product_name_ent) like LOWER(:name)")
fun getListAllByName(name: String):LiveData<List<String>>
}
ProductDao
#Query("SELECT * FROM products where product_name_ent LIKE :name or LOWER(product_name_ent) like LOWER(:name)")
fun getListAllByName(name: String):LiveData<List<ProductEntity>>
This Method in your dao should return LiveData<List<ProductEntity>> and not LiveData<List<String>>, because this query selects everything (*) from the entity and not a specific column.
it is similar to (#Query("SELECT * from products") fun getListAllProducts():LiveData<List<ProductEntity>>)
ProductViewModel
class ProductViewModel(application: Application) : AndroidViewModel(application) {
private val repository: ProductRepository
init {
val productDao = ProductsDB.getDatabase(
application,
viewModelScope
).productDao()
repository = ProductRepository(productDao)
}
private val searchStringLiveData = MutableLiveData<String>("") //we can add initial value directly in the constructor
val allProducts: LiveData<List<ProductEntity>>=Transformations.switchMap(searchStringLiveData)
{
string->
if (TextUtils.isEmpty(string)) {
repository.allProducts()
} else {
repository.allProductsByName(string)
}
}
fun insert(productEntity: ProductEntity) = viewModelScope.launch {
repository.insert(productEntity)
}
fun searchNameChanged(name: String) {
searchStringLiveData.value = name
}
}
Repository
...with the other method that you have, add the following:
fun allProducts():LiveData<List<ProductEntity>>=productDao.getListAllProducts()
fun allProductsByNames(name:String):LiveData<List<ProductEntity>>=productDao.getListAllByName(name)
In Your Activity Or Fragment Where you have the recyclerview adapter
inside onCreate() (if it is an activity)
viewModel.allProducts.observe(this,Observer{products->
//populate your recyclerview here
})
or
onActivityCreated(if it is a fragment)
viewModel.allProducts.observe(viewLifecycleOwner,Observer{products->
//populate your recyclerview here
})
Now set a listener to the searchView, everytime the user submit the query , call viewModel.searchNameChanged(// pass the new value)
Hope this helps
Regards,
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