I created an Observer in a Fragment which works perfectly (it fires a toast when an Int increases), but when I try to move this code into the Activity, the observer doesn't seem to connect and it does not update when the LiveData changes.
Fragment (this works!):
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
loginViewModel = ViewModelProviders.of(this).get(LoginViewModel::class.java)
loginViewModel.getLoginAttemptCount().observe(this, Observer { count ->
if (count > 0) makeToast("Authentication failed")
})
}
Activity (when I put the observer in the Activity it doesn't!):
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.login_activity)
loginViewModel = ViewModelProviders.of(this).get(LoginViewModel::class.java)
loginViewModel.getLoginAttemptCount().observe(this, Observer { count ->
if (count > 0) makeToast("Authentication failed")
})
}
ViewModel (both call same function in VM):
fun getLoginAttemptCount(): MutableLiveData<Int> {
Log.d(TAG, "getLoginAttemptCount()")
return firestoreRepository.getLoginAttemptCount() }
Repo (called from VM):
fun getLoginAttemptCount(): MutableLiveData<Int>{
Log.d(TAG, "getLoginAttemptCount()")
return loginAttempt
}
loginAttempt.value is increased everytime there is a login attempt and I have verified this works in Logcat..
For info, makeToast is simply a function to create a justified Toast (text and position):
private fun makeToast(message: String) {
val centeredText: Spannable = SpannableString(message)
centeredText.setSpan(
AlignmentSpan.Standard(Layout.Alignment.ALIGN_CENTER),
0, message.length - 1,
Spannable.SPAN_INCLUSIVE_INCLUSIVE
)
val toast = Toast.makeText(this, centeredText, Toast.LENGTH_LONG)
toast.setGravity(Gravity.CENTER,0,0)
toast.show()
Log.d(TAG, "Toast message: $message")
}
I'm assuming it is to do with the lifeCycleOwner but I am at a loss!
loginViewModel = ViewModelProviders.of(this).get(LoginViewModel::class.java)
In Fragment
you are using above line to create loginviewmodel passing the context of fragment to viewmodel
so,the first thing android does is that it check ,if it contain's any other viewmodel associated with this fragment, if it contains it will not create new Viewmodel it will return the old one
if it does not contain it create a new one.Viewmodel are created using key value pair.
So in your case
you are creating total two viewmodel each of fragment and activity you are changing the live data of fragment but you are trying to observe it in activity using activity viewmodel.
If you want to acheive that you need to create shared viewmodel among activity and fragment.How to create shared viewmodel
Related
Hey I am working in kotlin flow in android. I noticed that my kotlin flow collectLatest is calling twice and sometimes even more. I tried this answer but it didn't work for me. I printed the log inside my collectLatest function it print the log. I am adding the code
MainActivity.kt
class MainActivity : AppCompatActivity(), CustomManager {
private val viewModel by viewModels<ActivityViewModel>()
private lateinit var binding: ActivityMainBinding
private var time = 0
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
setupView()
}
private fun setupView() {
viewModel.fetchData()
lifecycleScope.launchWhenStarted {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.conversationMutableStateFlow.collectLatest { data ->
Log.e("time", "${time++}")
....
}
}
}
}
}
ActivityViewModel.kt
class ActivityViewModel(app: Application) : AndroidViewModel(app) {
var conversationMutableStateFlow = MutableStateFlow<List<ConversationDate>>(emptyList())
fun fetchData() {
viewModelScope.launch {
val response = ApiInterface.create().getResponse()
conversationMutableStateFlow.value = response.items
}
}
.....
}
I don't understand why this is calling two times. I am attaching logs
2022-01-17 22:02:15.369 8248-8248/com.example.fragmentexample E/time: 0
2022-01-17 22:02:15.629 8248-8248/com.example.fragmentexample E/time: 1
As you can see it call two times. But I load more data than it call more than twice. I don't understand why it is calling more than once. Can someone please guide me what I am doing wrong. If you need whole code, I am adding my project link.
You are using a MutableStateFlow which derives from StateFlow, StateFlow has initial value, you are specifying it as an emptyList:
var conversationMutableStateFlow = MutableStateFlow<List<String>>(emptyList())
So the first time you get data in collectLatest block, it is an empty list. The second time it is a list from the response.
When you call collectLatest the conversationMutableStateFlow has only initial value, which is an empty list, that's why you are receiving it first.
You can change your StateFlow to SharedFlow, it doesn't have an initial value, so you will get only one call in collectLatest block. In ActivityViewModel class:
var conversationMutableStateFlow = MutableSharedFlow<List<String>>()
fun fetchData() {
viewModelScope.launch {
val response = ApiInterface.create().getResponse()
conversationMutableStateFlow.emit(response.items)
}
}
Or if you want to stick to StateFlow you can filter your data:
viewModel.conversationMutableStateFlow.filter { data ->
data.isNotEmpty()
}.collectLatest { data ->
// ...
}
The reason is collectLatest like backpressure. If you pass multiple items at once, flow will collect latest only, but if there are some time between emits, flow will collect each like latest
EDITED:
You really need read about MVVM architecture.
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
setupView()
}
private fun setupView() {
if (supportFragmentManager.findFragmentById(R.id.fragmentView) != null)
return
supportFragmentManager
.beginTransaction()
.add(R.id.fragmentView, ConversationFragment())
.commit()
}
}
Delele ActivityViewModel and add that logic to FragmentViewModel.
Also notice you don't need use AndroidViewModel, if you can use plain ViewModel. Use AndroidViewModel only when you need access to Application or its Context
I am completely new to Kotlin, Coroutines and API calls, and I am trying to make an app based on this API.
My intention is to display the information of a game in my MainActivity, so I need ot fill some TextView for that purpose.
My API call and response system works perfectly well: the responses are OK and there are no errors, but the call is made using Kotlin's coroutines which won't let me update my UI after getting the response.
For the sake of simplicity, I am only attaching my MainActivity code, which is the source of the problem.
class MainActivity : AppCompatActivity() {
private lateinit var b: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
b = ActivityMainBinding.inflate(layoutInflater)
setContentView(R.layout.activity_main)
listAllGames()
}
private fun listAllGames() {
CoroutineScope(Dispatchers.IO).launch {
val call = getRetrofit().create(APIService::class.java).listAllGames("")
val games = call.body()
runOnUiThread {
if (call.isSuccessful) {
b.gameTitle.text = games?.get(0)?.title ?: "Dummy"
b.gameDesc.text = games?.get(0)?.short_description ?: "Dummy"
b.gameGenre.text = games?.get(0)?.genre ?: "Dummy"
}
else {
Toast.makeText(applicationContext, "ERROR", Toast.LENGTH_SHORT).show()
Log.d("mydebug", "call unsuccessful")
}
}
}
}
private fun getRetrofit(): Retrofit {
return Retrofit.Builder()
.baseUrl("https://www.freetogame.com/api/games/")
.addConverterFactory(GsonConverterFactory.create())
.build()
}
}
Concretely, the listAllGames() method is the problem here: the app will build successfully, if a breakpoint is added inside the if (call.isSuccessful) block, it'll show the data correctly; but when running the app the display will be left blank forever.
Thanks to all in advance!
In order to use view binding you need to pass the inflated view from the binding to the setContentView method. Otherwise you inflate the view with the binding but display the XML layout without the binding.
Check the documentation here:
private lateinit var binding: ResultProfileBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ResultProfileBinding.inflate(layoutInflater)
val view = binding.root
setContentView(view)
}
(Source: Android Developer documentation - " View Binding Part of Android Jetpack.")
Change your onCreate method as followed:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
b = ActivityMainBinding.inflate(layoutInflater)
setContentView(b.root)
listAllGames()
}
The other answer explains your problems with your view, but you also have some issues with the coroutine.
You should use lifecycleScope instead of creating a new scope. lifecycleScope will automatically cancel itself to avoid leaking the Activity if the activity is destroyed, such as during a screen rotation.
You should use Retrofit's call.await() suspend function instead of directly blocking a thread and having to specify a dispatcher. This also lets you leave things on the main dispatcher and directly update UI without having to use runOnUiThread.
private fun listAllGames() {
lifecycleScope.launch {
val call = getRetrofit().create(APIService::class.java).listAllGames("")
try {
val games = call.await()
b.gameTitle.text = games?.get(0)?.title ?: "Dummy"
b.gameDesc.text = games?.get(0)?.short_description ?: "Dummy"
b.gameGenre.text = games?.get(0)?.genre ?: "Dummy"
} catch (e: Exception) {
Toast.makeText(applicationContext, "ERROR", Toast.LENGTH_SHORT).show()
Log.d("mydebug", "call unsuccessful", e)
}
}
}
And actually, you should move API calls like this into a ViewModel so they can keep running during a screen rotation. If you do that, the function in the ViewModel should update a LiveData or SharedFlow instead of UI elements. Then your Activity can observe for changes. If the call is started and the screen is rotated, the API call will keep running and still publish its changes to the new Activity's UI without having to restart.
The problem: I'm facing a problem with fragments and shared viewmodel LiveData ... The problem that is there is a FragmentA that update data in shared viewmodel and observe for it's changes then display the result for the user inside FragmentA, FragmentA can launch new instance of FragmentA to get new data and display it so the fragment launch it self and old instance gets added to the back stack, until here nothing is wrong the new instance updates LiveData in viewModel and displays the new data perfectly the problem that is when i popUpBackstack() return to FragmentA old instance the data displays in it is the data that new FragmentA instance gets it which means that old FragmentA instance still observing the data even if i remove the observers ... this is general overview about the problem now i will show you fragments structure, the code and what the solution's that i'v tried.
Expected behavior: what i want to achieve is that when FragmentA launch it self and gets added to back stack stop observing the data in viewModel and when i back to it displays the old data that's all.
Fragment structure: i use one activity to hold all the fragments ... MainActivity have FindMoviesFragment inside it there is a viewPager which holds FragmentMovies which launches MovieDetailsFragment and inside it there is ViewPager also which holds the fragments that displays the data from MoviesViewModel it will get clear when you see the code below.
This code shows how MovieDetailsFragment initialize MoviesViewModel and updates data in viewmodel:
class MovieDetailsFragment:Fragment(R.layout.movie_details_fragment) {
private val args: MovieDetailsFragmentArgs by navArgs()
private val fragmentList:ArrayList<Fragment> by lazy {
arrayListOf(MovieDetailsOneFragment(args.movieId), MovieDetailsTwoFragment(),MovieDetailsThreeFragment())
}
private lateinit var pagerAdapter: ViewPagerAdapter
private lateinit var moviesViewModel: MoviesViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
moviesViewModel = ViewModelProvider(requireActivity()).get(MoviesViewModel::class.java)
//these two lines updates the data in viewmodel
moviesViewModel.getMovieDetails(args.movieId)
moviesViewModel.changeMovieID(args.movieId)
}
}
//---------------------MoviesViewModel------------------//
class MoviesViewModel constructor(private val repo:Repository):ViewModel() {
private val _currentMovieDetails = MutableLiveData<Resource<MovieDetails>>()
val currentMovieDetails :LiveData<Resource<MovieDetails>>
get() = _currentMovieDetails
fun getMovieDetails(movieID:Int) = viewModelScope.launch {
_currentMovieDetails.postValue(Resource.loading(null))
val result = repo.getMovieDetails(movieID)
if(result.isSuccessful){
_currentMovieDetails.postValue(Resource.success(result.body()))
}
else{
_currentMovieDetails.postValue(Resource.error(result.errorBody().toString(),null))
}
}
}
And inside MovieDetailsOne which is inside viewpager in MovieDetailsFragment i observe the data like this:
class MovieDetailsOneFragment(private val movieId: Int):Fragment(R.layout.movie_details_one) {
private lateinit var moviesViewModel:MoviesViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
//this is how i define viewModel(global scope)
moviesViewModel = ViewModelProvider(requireActivity()).get(MoviesViewModel::class.java)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
//in this method i observe on changes in currentMovieDetils liveData
subscribeToObservers()
//i try to call this from on create and nothing changes too
}
}
Now What i tried is the following:
-Loacal scope for fragments
//Define viewModel like this in MovieDetilsFragment
moviesViewModel = ViewModelProvider(this).get(MoviesViewModel::class.java)
//And in MovieDetailsOne like this
moviesViewModel = ViewModelProvider(this).get(MoviesViewModel::class.java)
//and this doesn't work MovieDetailsOne don't observe any changes
-Observe once extinction function:
fun <T> LiveData<T>.observeOnce(lifecycleOwner: LifecycleOwner, observer: Observer<T>) {
observe(lifecycleOwner, object : Observer<T> {
override fun onChanged(t: T?) {
observer.onChanged(t)
removeObserver(this)
}
})
}
//and this doesn't work MovieDetailsOne don't observe first time it's lanches
I know i took so long to explain :D but I'm trying to give you clear idea and sorry for you time <3 ... if you want any additional information about the code or the problem comment down below.
Finally i achieve the behavior that i want by doing some steps which is:
Attach ViewPager childs using childFragmentManager which means that android will treat viewpager fragments as childs for viewPager holder:
//instead of doing this
pagerAdapter = ViewPagerAdapter(activity?.supportragmentManager, lifecycle, fragmentList)
//do this
pagerAdapter = ViewPagerAdapter(childFragmentManager, lifecycle, fragmentList)
Define local viewModel for parent fragment like this:
//instead of doing this
moviesViewModel = ViewModelProvider(requireActivity()).get(MoviesViewModel::class.java)
//do this
moviesViewModel = ViewModelProvider(this).get(MoviesViewModel::class.java)
Define parent fragment scope viewModel in childs fragment:
//instead of doing this
moviesViewModel = ViewModelProvider(requireActivity()).get(MoviesViewModel::class.java)
//do this
private val moviesViewModel:MoviesViewModel by viewModels(
{requireParentFragment()}
)
by doing these steps parent fragment will stop observing data when it's destroyed and child fragments also.
I am updating a LiveData value from a DialogFragment in the ViewModel, but not able to get the value in Fragment.
The ViewModel:
class OtpViewModel(private val otpUseCase: OtpUseCase, analyticsModel: IAnalyticsModel) : BaseViewModel(analyticsModel) {
override val globalNavModel = GlobalNavModel(titleId = R.string.otp_contact_title, hasGlobalNavBar = false)
private val _contactListLiveData = MutableLiveData<List<Contact>>()
val contactListLiveData: LiveData<List<Contact>>
get() = _contactListLiveData
private lateinit var cachedContactList: LiveData<List<Contact>>
private val contactListObserver = Observer<List<Contact>> {
_contactListLiveData.value = it
}
private lateinit var cachedResendOtpResponse: LiveData<LogonModel>
private val resendOTPResponseObserver = Observer<LogonModel> {
_resendOTPResponse.value = it
}
private var _resendOTPResponse = MutableLiveData<LogonModel>()
val resendOTPResponseLiveData: LiveData<LogonModel>
get() = _resendOTPResponse
var userSelectedIndex : Int = 0 //First otp contact selected by default
val selectedContact : LiveData<Contact>
get() = MutableLiveData(contactListLiveData.value?.get(userSelectedIndex))
override fun onCleared() {
if (::cachedContactList.isInitialized) {
cachedContactList.removeObserver(contactListObserver)
}
if (::cachedOtpResponse.isInitialized) {
cachedOtpResponse.removeObserver(otpResponseObserver)
}
super.onCleared()
}
fun updateIndex(pos: Int){
userSelectedIndex = pos
}
fun onChangeDeliveryMethod() {
navigate(
OtpVerificationHelpCodeSentBottomSheetFragmentDirections
.actionOtpContactVerificationBottomSheetToOtpChooseContactFragment()
)
}
fun onClickContactCancel() {
navigateBackTo(R.id.logonFragment, true)
}
fun retrieveContactList() {
cachedContactList = otpUseCase.fetchContactList()
cachedContactList.observeForever(contactListObserver)
}
fun resendOTP(contactId : String){
navigateBack()
cachedResendOtpResponse = otpUseCase.resendOTP(contactId)
cachedResendOtpResponse.observeForever(resendOTPResponseObserver)
}
}
The BaseViewModel:
abstract class BaseViewModel(val analyticsModel: IAnalyticsModel) : ViewModel() {
protected val _navigationCommands: SingleLiveEvent<NavigationCommand> = SingleLiveEvent()
val navigationCommands: LiveData<NavigationCommand> = _navigationCommands
abstract val globalNavModel: GlobalNavModel
/**
* Posts a navigation event to the navigationsCommands LiveData observable for retrieval by the view
*/
fun navigate(directions: NavDirections) {
_navigationCommands.postValue(NavigationCommand.ToDirections(directions))
}
fun navigate(destinationId: Int) {
_navigationCommands.postValue(NavigationCommand.ToDestinationId(destinationId))
}
fun navigateBack() {
_navigationCommands.postValue(NavigationCommand.Back)
}
fun navigateBackTo(destinationId: Int, isInclusive: Boolean) {
_navigationCommands.postValue(NavigationCommand.BackTo(destinationId, isInclusive))
}
open fun init() {
// DEFAULT IMPLEMENTATION - override to initialize your view model
}
/**
* Called from base fragment when the view has been created.
*/
fun onViewCreated() {
analyticsModel.onNewState(getAnalyticsPathCrumb())
}
/**
* gets the Path for the current page to be used for the trackstate call
*
* Override this method if you need to modify the path
*
* the page id for the track state call will be calculated in the following manner
* 1) analyticsPageId
* 2) titleId
* 3) the page title string
*/
protected fun getAnalyticsPathCrumb() : AnalyticsBreadCrumb {
return analyticsBreadCrumb {
pathElements {
if (globalNavModel.analyticsPageId != null) {
waPath {
path = PathElement(globalNavModel.analyticsPageId as Int)
}
} else if (globalNavModel.titleId != null) {
waPath {
path = PathElement(globalNavModel.titleId as Int)
}
} else {
waPath {
path = PathElement(globalNavModel.title ?: "")
}
}
}
}
}
}
The DialogFragment:
class OtpVerificationHelpCodeSentBottomSheetFragment : BaseBottomSheetDialogFragment(){
private lateinit var rootView: View
lateinit var binding: BottomSheetFragmentOtpVerificationHelpCodeSentBinding
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
viewModel = getViewModel<OtpViewModel>()
binding = DataBindingUtil.inflate(inflater, R.layout.bottom_sheet_fragment_otp_verification_help_code_sent, container, false)
rootView = binding.root
return rootView
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val otpViewModel = (viewModel as OtpViewModel)
binding.viewmodel = otpViewModel
otpViewModel.resendOTPResponseLiveData.observe(viewLifecycleOwner, Observer {
it?.let { resendOtpResponse ->
if(resendOtpResponse.statusCode.equals("000")){
//valid status code
requireActivity().toastMessageOtp(getString(R.string.otp_code_verification_sent))
}else{
//show the error model
//it?.errorModel?.let { it1 -> handleDiasNetworkError(it1) }
}
}
})
}
}
I am calling the resendOTP(contactId : String) method of the viewmodel from the xml file of the DialogFragment:
<TextView
android:id="#+id/verification_help_code_sent_resend_code"
style="#style/TruTextView.SubText2.BottomActions"
android:layout_height="#dimen/spaceXl"
android:gravity="center_vertical"
android:text="#string/verification_help_resend_code"
android:onClick="#{() -> viewmodel.resendOTP(Integer.toString(viewmodel.userSelectedIndex))}"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#+id/top_guideline" />
Now whenever I try to call resendOTPResponseLiveData from the Fragment it does not gets called:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
Log.d("OtpVerify" , "OnViewCreatedCalled")
viewModel.onViewCreated()
val otpViewModel = (viewModel as OtpViewModel)
binding.lifecycleOwner = this
binding.viewmodel = otpViewModel
binding.toAuthenticated = OtpVerifyFragmentDirections.actionOtpVerifyFragmentToAuthenticatedActivity()
binding.toVerificationBtmSheet = OtpVerifyFragmentDirections.actionOtpVerifyFragmentToOtpContactVerificationCodeSentBottomSheet()
otpViewModel.resendOTPResponseLiveData.observe(viewLifecycleOwner, Observer {
if(it?.statusCode.equals("000")){
//valid status code
requireActivity().toastMessageOtp(getString(R.string.otp_code_verification_sent))
}else{
//show the error model
it?.errorModel?.let { it1 -> handleDiasNetworkError(it1) }
}
})
}
So what wrong I am doing here.
EDIT
Basically I need clicklistener(resend button click) in dialogfragment, and need to read it in the fragment. So I used the concept of SharedViewModel.
So I make necessary changes in the ViewModel:
private val selected = MutableLiveData<LogonModel>()
fun select(logonModel: LogonModel) {
selected.value = logonModel
}
fun getSelected(): LiveData<LogonModel> {
return selected
}
In the DialogFragment:
otpViewModel.resendOTPResponseLiveData.observe(viewLifecycleOwner, Observer{
otpViewModel.select(it);
})
And in the fragment where I want to read the value:
otpViewModel.getSelected().observe(viewLifecycleOwner, Observer {
Log.d("OtpVerify" , "ResendCalled")
// Update the UI.
if(it?.statusCode.equals("000")){
//valid status code
requireActivity().toastMessageOtp(getString(R.string.otp_code_verification_sent))
}else{
//show the error model
it?.errorModel?.let { it1 -> handleDiasNetworkError(it1) }
}
})
But it is still not working.
Edit:
ViewModel Source for fragment:
viewModel = getSharedViewModel<OtpViewModel>(from = {
Navigation.findNavController(container as View).getViewModelStoreOwner(R.id.two_step_authentication_graph)
})
ViewModel Source for dialogfragment:
viewModel = getViewModel<OtpViewModel>()
Being new-ish to the Jetpack library and Kotlin a few months back I ran into a similar issue, if I understand you correctly.
I think the issue here is that you are retrieving you ViewModel using the by viewModels which means the ViewModel you get back will only be scoped to the current fragments context... If you would like to share a view model across multiple parts of your application they have to be activity scoped.
So for example:
//this will only work for the current fragment, using this declaration here and anywhere else and observing changes wont work, the observer will never fire, except if the method is called within the same fragment that this is declared
private val viewModel: AddPatientViewModel by viewModels {
InjectorUtils.provideAddPatientViewModelFactory(requireContext())
}
//this will work for the ANY fragment in the current activies scope, using this code and observing anywhere else should work, the observer will fire, except if the method is called fro another activity
private val patientViewModel: PatientViewModel by activityViewModels {
InjectorUtils.providePatientViewModelFactory(requireContext())
}
Notice my viewModel of type AddPatientViewModel is scoped to the current fragments context only via viewModel: XXX by viewModels, any changes etc made to that particular ViewModel will only be propagated in my current fragment.
Where as patientViewModel of type PatientViewModel is scoped to the activities context via patientViewModel: XXX by activityViewModels.
This means that as long as both fragments belong to the same activity, and you get the ViewModel via ... by activityViewModels you should be able to observe any changes made to the ViewModel on a global scope (global meaning any fragment within the same activity where it was declared).
With all the above in mind if your viewModel is correctly scoped to your activity and in both fragments you retrieve the viewModel using the by activityViewModels and updating the value being observed via XXX.postValue(YYY) or XXX.value = YYY you should be able to observe any changes made to the ViewModel from anywhere within the same activity context.
Hope that makes sense, it's late here, and I saw this question just before I hit the sack!
The problem is that you are actually not sharing the ViewModel between the Fragment and the Dialog. To share instances of a ViewModel they must be retrieved from the same ViewModelStore.
The syntax you are using to retrieve the ViewModels seems to be from a third party framework. I feel like probably Koin.
If that is the case, note that in Koin, getViewModel retrieves the ViewModel from the Fragment's own ViewModelStore. So, you are retrieving the ViewModel in your DialogFragment from its own ViewModelStore. On the other hand, in your Fragment, you are retrieving it using getSharedViewModel, in which you can specify which ViewModelStore it should retrieve the ViewModel from. So you are retrieving the ViewModel from two different ViewModelStores, and so, getting two different ViewModel. Interacting with one of those does not affect the other, as they are not the same instance.
To solve it, you should retrieve the ViewModel in both your Fragment and DialogFragment from the same ViewModelStore. For example, you could use getSharedViewModel in both, maybe specifying the same ViewModelStore manually at each, or even, without even specifying, which Koin will default to their Activity's one.
You could also even just use getViewModel in your Fragment, then pass its own specific ViewModelStore to the DialogFragment, in which you could then use getSharedViewModel, specifying the passed Fragment's ViewModelStore.
In short: when Observe is active it works correctly when I do notify, but when I go back to the previous fragment (I use the navigation component) and again navigate to the current fragment, there is a creation of the fragment, and for some reason the Observe is called.
Why is the Observe not deleted when going back? It should behave according to the fragment's lifecycle.
I tried removing on onStop and still the observe called.
More detail:
Each of my project fragments is divided into 3 parts: model, viewModel, view
In the view section, I first set the viewModel.
class EmergencyFragment : Fragment() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
emergencyFragmentViewModel = ViewModelProviders.of(this).get(EmergencyFragmentViewModel::class.java)
}
And in onViewCreated I set the Observer object so that any changes made in LiveData I get a change notification here:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
emergencyFragmentViewModel.isEmergencyEventMediaLDSuccess.observe(viewLifecycleOwner, Observer {
Log.d("sendEmergencyEvent", "isEmergencyEventMediaLDSuccess observer called")
}
})
}
In the ViewModel class, I set the LiveData parameter as follows:
EmergencyFragmentViewModel: ViewModel() {
var isEmergencyEventMediaLDSuccess: LiveData<Boolean> = MutableLiveData()
private set
private val observerEventMedia = Observer<Boolean> { (isEmergencyEventMediaLDSuccess as MutableLiveData).value = it}
And in the init I set an observer:
init {
EmergencyFragmentModel.isEmergencyEventMediaLDSuccessModel.observeForever(observerEventMedia)
}
And of course removes when needed
override fun onCleared() {
super.onCleared()
EmergencyFragmentModel.isEmergencyEventMediaLDSuccessModel.removeObserver(observerEventMedia)
}
The part of the model is defined as follows:
class EmergencyFragmentModel {
companion object{
val isEmergencyEventMediaLDSuccessModel: LiveData<Boolean> = MutableLiveData()
And I do request network and when a reply comes back I perform a notify
override fun onResponse(call: Call<Int>, response: Response<Int>) {
if(response.isSuccessful) {
(isEmergencyEventLDModelSuccess as MutableLiveData).postValue(true)
Log.d("succeed", "sendEmergencyEvent success: ${response.body().toString()}")
}
Can anyone say what I'm missing? Why when there is an active Observe and I go back to the previous fragment (I use the navigation component) and navigate to the current fragment again, the Observe is called? I can understand that when a ViewModel instance is created and it executes setValue for the LiveData parameter, then it is notified. But Why is the observe not removed when I go back? I tried removing the Observe on the onStop and it keeps happening.
override fun onStop() {
super.onStop()
emergencyFragmentViewModel.isEmergencyEventMediaLDSuccess.removeObservers(viewLifecycleOwner)
emergencyFragmentViewModel.isEmergencyEventMediaLDSuccess.removeObserver(observeEmergencyEventLDSuccess)
}
#Pawel is right. LiveData stores the value and everytime you observe it (in your onViewCreated, in this case), it'll emit the last value stored.
Maybe you want something like SingleLiveEvent, which clean its value after someone reads it.
So when you go back and forth, it won't emit that last value (once it was cleaned).
As I understand your question, you only want to run the observer, when the new value differs from the old one. That can be done by retaining the value in another variable in the viewModel.
if (newValue == viewModel.retainedValue) return#observe
viewModel.retainedValue = newValue
I fixed this by creating an extension in kotlin by checkin the lifecycle state.
fun <T> LiveData<T>.observeOnResumedState(viewLifecycleOwner: LifecycleOwner, observer: Observer<T>) {
this.observe(viewLifecycleOwner) {
if (viewLifecycleOwner.lifecycle.currentState == Lifecycle.State.RESUMED) {
observer.onChanged(it)
}
}
}
And here is how i observe
viewModel.result.observeOnResumedState(viewLifecycleOwner) {
// TODO
}