I don't understand why my MVVM with LiveData takes a lot of time to respond within a BottomSheetFragment, but just the first time I open the fragment.
Here is my code:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
Log.d("HERE", "onViewCreated")
abilityViewModel = ViewModelProvider(this)[AbilityViewModel::class.java]
setupViewModel()
Log.d("HERE", "viewModelCreated")
abilityViewModel.getAbilityByName(abilityName)
private fun setupViewModel() {
abilityViewModel.abilityName.observe(viewLifecycleOwner, Observer { ability ->
Log.d("HERE", ability.name)
fillInfo(ability)
})
}
Here is the ViewModel:
class AbilityViewModel(application: Application) : AndroidViewModel(application) {
private val repository: AbilityRepository
private val _abilityName: MutableLiveData<String> = MutableLiveData()
val abilityName: LiveData<Ability>
fun getAbilityByName(name: String) {
_abilityName.value = name
}
init {
val abilityDao: AbilityDao = MainDatabase(application, viewModelScope).abilityDao()
repository = AbilityRepository(abilityDao)
abilityName = Transformations.switchMap(_abilityName){abilityName ->
repository.getAbilityByName(abilityName)
}
}
}
This is the log for the first time I open the fragment:
After the first time, both the same fragment and different fragments of the same type take zero delay in loading:
Help would be greatly appreciated, thank you
EDIT
In order to get the time in places suggested by #commonsware I did this:
class AbilityViewModel(application: Application) : AndroidViewModel(application) {
private val repository: AbilityRepository
private val _abilityName: MutableLiveData<String> = MutableLiveData()
val abilityName: LiveData<Ability>
fun getAbilityByName(name: String) {
Log.d("HERE", "getAbilityByName called")
_abilityName.value = name
Log.d("HERE", "getAbilityByName value assigned")
}
init {
val abilityDao: AbilityDao = MainDatabase(application, viewModelScope).abilityDao()
repository = AbilityRepository(abilityDao)
abilityName = Transformations.switchMap(_abilityName){abilityName ->
Log.d("HERE", "switchMap called")
repository.getAbilityByName(abilityName)
}
}
}
However the problem does not seem to be in any of those two methods as you can see here:
In addition the database is initialised when the app starts, which is before this point
EDIT 2
Here is the repository modified to accomodate the log:
class AbilityRepository(private val abilityDao: AbilityDao) {
//fun getAbilityByName(name: String) = abilityDao.getAbilityByName(name)
fun getAbilityByName(name: String): LiveData<Ability> {
Log.d("HERE", "getAbilityByName called from repo")
return abilityDao.getAbilityByName(name)
}
}
However the time seems to be fine here as well
Related
I have a fragment that contains a current user data like firstName, lastName and address
class EditUserFragment :Fragment() {
val viewModel: EditUserAddressViewModel by viewModels()
...
#SuppressLint("ClickableViewAccessibility")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
}
fun observeData(){
viewLifecycleOwner.lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.uiState.collect {
// here is the problem
}
}
}
}
}
ViewModel
class EditUserAddressViewModel : ViewModel() {
data class UiState(
val firstName: String? = null,
val lastName: String? = null,
val address: Address? = null
)
private val _uiState = MutableStateFlow(UiState())
val uiState = _uiState.asStateFlow()
fun getAddress(){
viewModelScope.launch{
_uiState.value = userRepo.getUserData()
}
}
fun updateUserFirstName(firstName:String ){
userRepo.updateUserFirstName(firstName)
}
fun updateUserLastNanme(lastName:String){
userRepo.updateUserFirstName(lastName)
}
}
so our repo will update the same flow that I'm observing for user data.
the problem here is that in compose the compose already handles to check what's the data already changed and what's not so it already updates the changed data only in the view.
how do I achieve this in fragments inside the observer?.
the code is just to show the case I have a more complicated screen and have more data on the screen
I've been reading some questions, answers and blogs about MVVM pattern in Android, and I've implemented it in my application.
My application consists of a MainActivity with 3 Tabs. Content of each tab is a fragment.
One of these fragments, is a List of Users stored on Room DB, which is where I've implemented the MVVM (implementing User object, ViewModel, Repository and Adapter with RecycleView).
In this same fragment, I have an "add User" button at the end that leads to a new activity where a formulary is presented to add a new user. In this activity I want to be sure that the full name of user not exists in my DB before saving it.
I was trying to use the same ViewModel to get full UserNames full name, but it seems that ViewModel is never initialized and I dont' know why.
I've read some questions about that viewmodel can't be used in different activities (I use it in MainActivity also in AddUser activity
This is my ViewModel:
class UserViewModel : ViewModel() {
val allUsersLiveData: LiveData<List<User>>
private val repository: UserRepository
init {
Timber.i("Initializing UserViewModel")
repository = UserRepository(UserTrackerApplication.database!!.databaseDao())
allUsersLiveData = repository.getAllUsers()
}
fun getAllUsersFullName(): List<String> {
return allUsersLiveData.value!!.map { it.fullname}
}
And my AddUser activity:
class AddUser : AppCompatActivity() {
private lateinit var userList:List<String>
private lateinit var binding: ActivityAddUserBinding
private val userViewModel: UserViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_add_user)
Timber.i("Add User OnCreate")
binding = ActivityAddUserBinding.inflate(layoutInflater)
setContentView(binding.root)
}
fun addUserClick(v : View){
//someCode
val userName = binding.constraintLayoutAddUser.etUserName!!.text.toString()
if(checkUserExistance(userName)) {
val text: String = String.format(
resources.getString(R.string.repeated_user_name),
userName
Snackbar.make(v, text, Snackbar.LENGTH_LONG).show()
{
else
{
lifecycleScope.launch {
UserTrackerApplication.database!!.databaseDao()
.insertUser(user)
Timber.i("User added!")
}
finish()
}
}
Debugging, I see the log "Initializing UserViewModel" when the fragment of MainActivity is started, but I can't see it when AddUser activity is called. So it seems it's not initializing correctly.
So the questions:
Is this a good approach? I'm making some design mistake?
Why the VM isn't initializing?
EDIT
I forgot to add this function. Calling userViewModel here is where I get the error:
private fun checkUserExistance(userName: String): Boolean {
var result = false
userList = userViewModel.getAllUsersNames()
for (usr in userList)
{
if(usr.uppercase() == userName.uppercase())
{
result = true
break
}
}
return result
}
EDIT 2
I added this on my "onCreate" function and started to work:
userViewModel.allUsersLiveData.observe(this, Observer<List<User>>{
it?.let {
// updates the list.
Timber.i("Updating User Names")
userList =userViewModel.getAllUsersNames()
}
})
if you take a look at by viewModels delegate you will see it's lazy it means it will initialize when it is first time accessed
#MainThread
public inline fun <reified VM : ViewModel> ComponentActivity.viewModels(
noinline factoryProducer: (() -> Factory)? = null
): Lazy<VM> {
val factoryPromise = factoryProducer ?: {
defaultViewModelProviderFactory
}
return ViewModelLazy(VM::class, { viewModelStore }, factoryPromise)
}
How to pass value from Activity to View Model? I try to find anything on web but I failed. What I want is this: I have two recyclerviews in one activity. If user click on item A in recyclerview 1 I want to send ID of this item to View Model and return something by this ID. There is an error with dokladId parameter in testToShow variable.
What is the easy way to handle it?
This is my ViewModel:
#HiltViewModel
class SkladViewModel #Inject constructor(
repository: SybaseRepository
): ViewModel(){
val skladyPolozky = repository.getAllSkladFromPolozka().asLiveData()
val dokladyPolozky = repository.getAllHlavickyToDoklad().asLiveData()
val testToShow = repository.getSelectedDokladyBySklad(dokladId).asLiveData()
}
This is the activity
#AndroidEntryPoint
class DokladActivity : AppCompatActivity(), SkladAdapter.OnItemClickListener, DokladAdapter.OnItemClickListener {
private val skladViewModel: SkladViewModel by viewModels()
//private val dokladViewModel: DokladViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding = ActivityDokladBinding.inflate(layoutInflater)
//setContentView(R.layout.activity_doklad)
setContentView(binding.root)
binding.btVybratDoklad.setOnClickListener{
openActivity(binding.root)
}
val skladAdapter = SkladAdapter (this)
val dokladAdapter = DokladAdapter(this)
binding.apply {
recyclerViewSklady.apply {
adapter = skladAdapter
layoutManager = LinearLayoutManager(this#DokladActivity)
}
skladViewModel.skladyPolozky.observe(this#DokladActivity) {
skladAdapter.submitList(it)
Log.d("Doklad", skladAdapter.currentList.toString())
}
recyclerViewDoklady.apply {
adapter = dokladAdapter
layoutManager = LinearLayoutManager(this#DokladActivity)
}
skladViewModel.dokladyPolozky.observe(this#DokladActivity){
dokladAdapter.submitList(it)
Log.d("Doklad", dokladAdapter.currentList.toString())
}
}
}
fun openActivity(view: View){
val intent = Intent(this,PolozkaActivity::class.java )
startActivity(intent)
}
override fun onItemClick(polozkaSklad: SkladTuple) {
val action = polozkaSklad.reg
}
override fun onItemClick(polozkaHlavicka: DokladTuple) {
val intent = Intent(this, PolozkaActivity::class.java)
intent.putExtra("doklad", polozkaHlavicka.doklad)
//intent.putExtra("polozkaHlavicka", polozkaHlavicka as Serializable)
startActivity(intent)
}
}
Repository with some function:
fun getSelectedDokladyBySklad(sklad: Int) : Flow<List<SkladDokladTuple>>{
return sybaseDao.getAllDokladFromPolozkaBySklad(sklad)
}
and DAO:
#Query("SELECT distinct doklad FROM cis06zebrap where sklad=:skladId")
fun getAllDokladFromPolozkaBySklad(skladId:Int?=null): Flow<List<SkladDokladTuple>>
#HiltViewModel
class SkladViewModel #Inject constructor(
repository: SybaseRepository
): ViewModel(){
val skladyPolozky = repository.getAllSkladFromPolozka().asLiveData()
val dokladyPolozky = repository.getAllHlavickyToDoklad().asLiveData()
val testToShow = repository.getSelectedDokladyBySklad(dokladId).asLiveData()
fun someNameYouFindUseful(id: String) {
// do something with the id
...
// notify the UI
someLiveDataYouShoudlBeObservingFromTheUI.value = SomeSealedClassWrappingTheStates.SomeStateYouFindDescriptive
}
}
Then in your Activity/Fragment you'd do:
viewModel.someNameYouFindUseful("the Id")
Since you will likely have a viewModel reference there.
To complete the missing pieces, please take a look at the Google official documentation about ViewModels including how to expose a "state" and react to it.
I am trying to use the Firebase API in my project but Transformations.map for the variable authenticationState in the View Model does not run. I have been following Google's tutorial here (link goes to the ViewModel of that project).
I want to be able to add the Transformations.map code to the FirebaseUserLiveData file later but I cant seem to figure out why it doesn't run.
FirebaseUserLiveData
class FirebaseUserLiveData: LiveData<FirebaseUser?>() {
private val firebaseAuth = FirebaseAuth.getInstance()
private val authStateListener = FirebaseAuth.AuthStateListener { firebaseAuth ->
value = firebaseAuth.currentUser
}
override fun onActive() {
firebaseAuth.addAuthStateListener { authStateListener }
}
override fun onInactive() {
firebaseAuth.removeAuthStateListener(authStateListener)
}
}
SearchMovieFragmentViewModel
class SearchMovieFragmentViewModel : ViewModel() {
enum class AuthenticationState {
AUTHENTICATED, UNAUTHENTICATED, INVALID_AUTHENTICATION
}
var authenticationState = Transformations.map(FirebaseUserLiveData()) { user ->
Log.d("TEST", "in the state function")
if (user != null) {
AuthenticationState.AUTHENTICATED
} else {
AuthenticationState.UNAUTHENTICATED
}
}
SearchMovieFragment
class SearchMovieFragment : Fragment(), MovieSearchItemViewModel {
companion object {
fun newInstance() = SearchMovieFragment()
}
private lateinit var searchMovieFragmentViewModel: SearchMovieFragmentViewModel
private lateinit var binding: SearchMovieFragmentBinding
private lateinit var movieRecyclerView: RecyclerView
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
binding = DataBindingUtil.inflate(inflater, R.layout.search_movie_fragment, container, false)
searchMovieFragmentViewModel = ViewModelProvider(this).get(SearchMovieFragmentViewModel::class.java)
binding.lifecycleOwner = this
binding.viewmodel = searchMovieFragmentViewModel
binding.signOutButton.setOnClickListener {
AuthUI.getInstance().signOut(requireContext())
}
searchMovieFragmentViewModel.authenticationState.observe(viewLifecycleOwner, Observer { state ->
when (state) {
AUTHENTICATED -> searchMovieFragmentViewModel.signedIn = View.VISIBLE
UNAUTHENTICATED -> searchMovieFragmentViewModel.signedIn = View.GONE
}
})
return binding.root
}
}
Should be .addAuthStateListener(authStateListener) instead of { authStateListener }
That is because you are not keeping the reference of FirebaseUserLiveData() once you start observing it like Transformations.map(FirebaseUserLiveData()) { user ->.
You have to have the reference of the Livedata you are mapping or transferring to another form of Livedata.
It is like a chain of observation, All LiveData in the chain should be observed or should have some kind of observer down the line, The main use-case is to transform some form of livedata to something you want, For Example:
class YourRepository{ // your repo, that connected to a network that keeps up to date some data
val IntegerResource: LiveData<Int> = SomeRetrofitInstance.fetchFromNetwork() //updating some resource from network
}
class YourViewModel{
val repository = YourRepository()
//this will start observe the repository livedata and map it to string resource
var StringResource: Livedata<String> = Transformations.map( repository.IntegerResource ) { integerValue ->
integerValue.toString()
}
My Point is you have to keep alive the LiveData you are transforming. Hope helped.
I Am using MVVM architecture to simple project. Then i stack in this case, when i have to return value from Model DataSource (Lambda function) to Repository then ViewModel will observe this repository. Please correct me if this not ideally and give me some advise for the true MVVM in android. i want to use LiveData only instead of RxJava in this case, because many sample in Github using RxJava.
In my Model i have class UserDaoImpl, code snippet like below
class UserDaoImpl : UserDao {
private val resultCreateUser = MutableLiveData<AppResponse>()
private val mAuth : FirebaseAuth by lazy {
FirebaseAuth.getInstance()
}
override fun createUser(user: User) {
mAuth.createUserWithEmailAndPassword(user.email, user.password)
.addOnCompleteListener {
//I DID NOT REACH THIS LINE
println("hasilnya ${it.isSuccessful} ")
if(it.isSuccessful){
val appResponse = AppResponse(true, "oke")
resultCreateUser.postValue(appResponse)
}else{
val appResponse = AppResponse(false, "not oke -> ${it.result.toString()}")
resultCreateUser.postValue(appResponse)
}
}
.addOnFailureListener {
println("hasilnya ${it.message}")
val appResponse = AppResponse(false, "not oke -> ${it.message}")
resultCreateUser.postValue(appResponse)
}
}
override fun getResultCreateUser() = resultCreateUser
}
And this is my Repository snippet code
class RegisterRepositoryImpl private constructor(private val userDao: UserDao) : RegisterRepository{
companion object{
#Volatile private var instance : RegisterRepositoryImpl? = null
fun getInstance(userDao: UserDao) = instance ?: synchronized(this){
instance ?: RegisterRepositoryImpl(userDao).also {
instance = it
}
}
}
override fun registerUser(user: User) : LiveData<AppResponse> {
userDao.createUser(user)
return userDao.getResultCreateUser() as LiveData<AppResponse>
}
}
Then this is my ViewModel
class RegisterViewModel (private val registerRepository: RegisterRepository) : ViewModel() {
val signUpResult = MutableLiveData<AppResponse>()
fun registerUser(user: User){
println(user.toString())
val response = registerRepository.registerUser(user)
signUpResult.value = response.value
}
}
If i execute the snippet code above, the result always nullpointer in signUpResult
This is my Activity
lateinit var viewModel: RegisterViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_register)
initializeUI()
}
private fun initializeUI() {
val factory = InjectorUtils.provideRegisterViewModelFactory()
viewModel = ViewModelProviders.of(this, factory).get(RegisterViewModel::class.java)
viewModel.signUpResult.observe(this, Observer {
//IT always null
if(it.success){
// to HomeActivity
Toast.makeText(this, "Success! ${it.msg}", Toast.LENGTH_SHORT).show()
}else{
Toast.makeText(this, "FALSE! ${it.msg}", Toast.LENGTH_SHORT).show()
}
})
register_btn.setOnClickListener {
val username = name.text.toString()
val email = email.text.toString()
val password = password.text.toString()
val phone = number.text.toString()
val user = User(0, username,"disana", email, password, "disana")
viewModel.registerUser(user)
}
}
Crash occured when i press register button
I'm not 100% sure, but I think the problem is in your ViewModel, where you are trying to pass by reference MutableLiveData. Your Activity is observing signUpResult MutableLiveData, but you are never posting new value, you are trying to change reference of that LiveData to one in Repository.
val signUpResult = MutableLiveData<AppResponse>()
fun registerUser(user: User){
println(user.toString())
val response = registerRepository.registerUser(user)
signUpResult.value = response.value
}
I think that the solution here is to let your ViewModel return LiveData, which is returned from Repository.
fun registerUser(user: User): MutableLiveData<AppResponse> {
println(user.toString())
return registerRepository.registerUser(user)
}
And you need to observe function registerUser(user) in your Activity.
viewModel.registerUser(user).observe(this, Observer {
But now you encountered another problem. By this example you will trigger observe method every time your button is clicked. So you need to split in repository your function, you need to make one only for returning userDao.getResultCreateUser() as LiveData<AppResponse>, and the other to trigger userDao.create(user) .
So you can make two functions in your repository
override fun observeRegistrationResponse() : LiveData<AppResponse> {
return userDao.getResultCreateUser() as LiveData<AppResponse>
}
override fun registerUser(user: User) {
userDao.createUser(user)
}
Now also in ViewModel you need to make separate function for observing result and for sending request for registration.
fun observeRegistrationResponse(): LiveData<AppResponse> {
return registerRepository.observeRegistrationResponse()
}
fun registerUser(user: User){
println(user.toString())
registerRepository.registerUser(user)
}
And finally you can observe in your function initializeUI
viewModel.observeRegistrationResponse().observe(this, Observer {
And send registration request on button click
viewModel.registerUser(user)
Sorry for long response, but I tried to explain why you need to change your approach. I hope I helped you a bit to understand how LiveData works.