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.
Related
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.
im really new on AAC and repository.
I have made an app with MVVM and repository.
Activity
class UserTestActivity : AppCompatActivity() {
private val userViewModel : UserViewModel by viewModel<UserViewModel>()
private lateinit var button : AppCompatButton
private var count : Int =0
override fun onCreate(savedInstanceState: Bundle?)
{
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_user_test
button = findViewById(R.id.testButton)
val userObserver = Observer<MutableList<UserModel>> { newUserList ->
Log.d("room-db-status", "size: "+newUserList.size)
}
userViewModel._user.observe(this, userObserver)
button.setOnClickListener(View.OnClickListener {
count++
Toast.makeText(this, "updated: "+count, Toast.LENGTH_SHORT).show()
userViewModel.insertUser(UserModel(
uid = count.toString(),
nickName = "Alexar",
gender ="female",
age = 22,
birth ="19990901",
mainResidence= "Seoul",
subResidence = "???",
tripWish = mutableListOf("!!!","!!?"),
tripStyle = mutableListOf("!!!","!!?"),
selfIntroduction = "hi -_-",
uriList = mutableListOf("!!!","!!?"),
geohash = "none",
latitude = 37.455,
longitude = 124.890,
mannerScore = 4.5,
premiumOrNot = false,
knock = 0
))
})
}
}
this is ViewModel
class UserViewModel (
private val userRepository : UserRepository): ViewModel() {
val _user : LiveData<MutableList<UserModel>> = liveData(Dispatchers.IO) {
val data = userRepository.getAllUser()
emit(data)
}
fun insertUser (userModel: UserModel) {
viewModelScope.launch(Dispatchers.IO) {
userRepository.insertUser(userModel)
}
}
}
Repositoty
interface UserRepository {
suspend fun getAllUser () : MutableList<UserModel>
suspend fun insertUser (userModel: UserModel)
}
RepositoryImpl
class UserRepositoryImpl (
private val localDataSource : UserLocalDataSource,
private val userMapper: UserMapper) :UserRepository{
override suspend fun getAllUser() : MutableList<UserModel> {
val userList : List<UserEntity> = localDataSource.getAllUser()
var temp = mutableListOf<UserModel>()
for (user in userList)
temp.add(userMapper.entityToModel(user))
return temp
}
override suspend fun insertUser(userModel: UserModel) {
return localDataSource.insertUser(userMapper.modelToEntity(userModel))
}
}
UserLocalDataSource
interface UserLocalDataSource {
suspend fun getAllUser () : MutableList<UserEntity>
suspend fun insertUser (userEntity: UserEntity)
}
UserLocalDataSourceImpl
class UserLocalDataSourceImpl(
private val appDatabase: AppDatabase) : UserLocalDataSource {
override suspend fun getAllUser() : MutableList<UserEntity> {
return appDatabase.userEntityDao().getAllUser()
}
override suspend fun insertUser(userEntity: UserEntity) {
appDatabase.userEntityDao().insertUser(userEntity)
}
}
UserEntityDao
interface UserEntityDAO {
#Query ("SELECT * FROM user " )
suspend fun getAllUser() : MutableList<UserEntity>
#Query ("SELECT * FROM user WHERE uid = (:uid) ")
suspend fun getUser(uid: String) :UserEntity
#Insert (onConflict = REPLACE)
suspend fun insertUser (user : UserEntity)
#Query("DELETE FROM user WHERE uid = (:uid)")
suspend fun delete(uid : String)
}
there are also Mapper and Koin injection.
when trying to insert user data to room, it was successful. but
after that, liveData Builder
val _user : LiveData<MutableList<UserModel>> = liveData(Dispatchers.IO) {
val data = userRepository.getAllUser()
emit(data)
}
not invoked...
Of course, that builder is invoked only once when app started haha
who knows why??
I do not know.
You shouldn't rely on the liveData builder to be invoked when data is inserted/updated in DB. Please refer to this doc on how to work with liveData builder.
To achieve the case when an observer of LiveData is invoked when data is inserted/updated in DB, methods must return LiveData object in UserEntityDao:
interface UserEntityDAO {
#Query ("SELECT * FROM user " )
suspend fun getAllUser() : LiveData<List<UserEntity>>
#Query ("SELECT * FROM user WHERE uid = (:uid) ")
suspend fun getUser(uid: String) : LiveData<UserEntity>
// ...
}
I am trying to test my app with firestore and I don't know how will I fake the datasource with a Flow to test my repository. I'm trying to follow the pattern from google codelabs but I only convert toObject in the composable. How can I write the repository test?
below is my FirestoreDatasource and my Repository and the FakeDatasource I am trying to write
#Singleton
class FirestoreDatasourceImpl #Inject constructor(
private val firestore: FirebaseFirestore
) : FirestoreDatasource {
override suspend fun addUser(user: UserModel) {
firestore.collection(USERS)
.document(user.id)
.set(user, SetOptions.merge())
}
#ExperimentalCoroutinesApi
override fun getAllUsers(userId: String) = callbackFlow {
val collection = firestore.collection(USERS).whereNotEqualTo("id", userId)
val snapshotListener = collection.addSnapshotListener() { snapshot, e ->
this.trySend(snapshot).isSuccess
}
awaitClose {
snapshotListener.remove()
}
}
}
#Singleton
class FirestoreRepositoryImpl #Inject constructor(
private val firestoreClass: FirestoreDatasourceImpl
) : FirestoreRepository{
override suspend fun addUser(user: UserModel){
firestoreClass.addUser(user)
}
#ExperimentalCoroutinesApi
override fun getAllUsers(userId: String) : Flow<QuerySnapshot?> {
return firestoreClass.getAllUsers(userId)
}
}
internal class FakeFirestoreDatasourceImplTest constructor(
private var userList: MutableList<UserModel>? = mutableListOf()
) : FirestoreDatasource{
override suspend fun addUser(user: UserModel) {
userList?.add(user)
}
override fun getAllUsers(userId: String): Flow<QuerySnapshot?> {
}
}
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 am implementing Room with a vIewModel, my structure is the following
#DAO,#Entity,#Database,#Repository
#Entity(tableName="dx_table")
class dx_table(
#ColumnInfo(name = "name")
val naxme: String,
#PrimaryKey
#ColumnInfo(name = "phone")
val phone: String,
#ColumnInfo(name = "passx")
val passx: String,
#ColumnInfo(name = "login_fx")
val login_fx: String
)
#Dao
interface dx_dao{
#Query("SELECT * FROM dx_table")
fun get_all():LiveData<List<dx_table>>
#Insert
suspend fun insertTrx(dx_table:dx_table)
#Query("UPDATE dx_table SET login_fx =:login_fx where phone=:phonex")
suspend fun insertFx(login_fx: String,phonex: String)
#Query("SELECT * from dx_table where phone=:phonex")
suspend fun get_name_px(phonex: String):List<dx_table>
#Query("Delete from dx_table")
suspend fun deleteAll()
#Query("Select * from dx_table where login_fx=1")
suspend fun selectFx():List<dx_table>
}
#Database(entities = arrayOf(dx_table::class), version = 1, exportSchema = false)
public abstract class DxDatabase : RoomDatabase() {
abstract fun dxDao(): dx_dao
companion object {
// Singleton prevents multiple instances of database opening at the
// same time.
#Volatile
private var INSTANCE: DxDatabase? = null
fun getDatabase(context: Context): DxDatabase {
val tempInstance = INSTANCE
if (tempInstance != null) {
return tempInstance
}
synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
DxDatabase::class.java,
"dx_database"
).build()
INSTANCE = instance
return instance
}
}
}
}
class dxRepository(private val dxDao: dx_dao ){
val k_d:LiveData<List<dx_table>> = dxDao.get_all()
suspend fun insert_trx(dx_table: dx_table){
dxDao.insertTrx(dx_table)
}
suspend fun insert_fx(login_fx: String,phonex: String) {
dxDao.insertFx(login_fx,phonex)
}
suspend fun select_fx() {
dxDao.selectFx()
}
suspend fun get_name_px(phonex: String) :List<dx_table> {
return dxDao.get_name_px(phonex) as List<dx_table>
}
}
The viewmodel is
class DxViewModel (application: Application) : AndroidViewModel(application) {
var repository: dxRepository
var k_d: LiveData<List<dx_table>>
init {
// Gets reference to WordDao from WordRoomDatabase to construct
// the correct WordRepository.
val dxDao = DxDatabase.getDatabase(application).dxDao()
repository = dxRepository(dxDao)
k_d = repository.k_d
}
fun insert_trx(dxTable: dx_table) = viewModelScope.launch {
repository.insert_trx(dxTable)
}
fun insert_fx(login_fx: String, phonex: String) = viewModelScope.launch {
repository.insert_fx(login_fx, phonex)
}
fun select_fx() = viewModelScope.launch {
repository.select_fx()
}
fun get_name_px(phonex: String) = viewModelScope.launch {
repository.get_name_px(phonex)
}
}
I can track the live data using observe,its not an issue, the problem i am facing is with the get_name_px(phone)
var mView = ViewModelProviders.of(this).get(DxViewModel::class.java)
var lm = mView.get_name_px(phone)
here lm seems to be job type , i need the return List , how do i get it .
In your viewModel function select_fx() return a job, because launch does not return result, so you have to either:
1) Use async and await
fun get_name_px(phonex: String) = viewModelScope.async {
repository.get_name_px(phonex)
}.await()
2) Not use launch viewModel, use it in Activity/Fragment
suspend fun get_name_px(phonex: String) = repository.get_name_px(phonex)
class CardFragment : Fragment() {
fun get() {
// launch in Dispatchers.Main
lifecycleScope.launch {
var lm = mView.get_name_px(phone)
}
// launch in background thread
lifecycleScope.launch(Dispatchers.Default) {
var lm = mView.get_name_px(phone)
}
}
}