I have rigged up a simple login fragment with a view model.
Here is the fragment :
class LoginFragment : Fragment() {
companion object {
fun newInstance() = LoginFragment()
}
private lateinit var viewModel: LoginViewModel
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.login_fragment, container, false)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
viewModel = ViewModelProviders.of(this).get(LoginViewModel::class.java)
viewModel.loginState.observe(this, Observer{
handleState(it)
})
login_button.setOnClickListener {
viewModel.isUserValid(username.text.toString(), pass.toString())
}
}
private fun handleState(status: RegisterState) {
if (status.statusMessage.equals("Good"))
view?.findNavController()?.navigate(R.id.action_registerFragment_to_homeFragment)
else
Snackbar.make(login_container, "Welcome to SwA", Snackbar.LENGTH_LONG).show();
}
}
and here is my view model :
class LoginViewModel : ViewModel() {
lateinit var auth: FirebaseAuth
private var _loginState = MutableLiveData<LoginState>()
val loginState : MutableLiveData<LoginState> get() = _loginState
init {
loginState.value = LoginState()
}
fun isUserValid(email: String, password: String): Boolean {
//Add call to authenticate through firebase
auth.signInWithEmailAndPassword(email, password)
.addOnCompleteListener {
if (it.isSuccessful) {
// Sign in success, update UI with the signed-in user's information
val user = auth.currentUser
//updateUI(user)
} else {
// If sign in fails, display a message to the user.
_loginState.value?.statusMessage = "Authentication Failed"
}
}
return true
}
}
This works and registers a change to the string status when a failed log in is attempted, however it also submits an onChange() when loading the fragment causing the snackbar to appear in the UI before they have actually entered anything when the fragment is created. How can I initialize the view state without triggering an onChange() ?
LiveData class has a method
boolean shouldBeActive() {
return mOwner.getLifecycle().getCurrentState().isAtLeast(STARTED);
}
which checks if owner of the LifeCycle, Activity or Fragment, is on a state after STARTED which means for an Activity if it has called onStart().
So whenever an Activity or Fragment observes a LiveData it gets LiveData's value by setValue or PostValue after onStart is called.
One of the ways to prevent same value to update UI more than once is to use SingleLiveEvent class, example here. Another one is using Event class, example is here.
Related
I am using Room and I need to return id to Fragment which is returned when insert().
However, But I couldn't return the value from viewModelScope.
I saw other similar questions, but the answer was to return LiveData.
But I don't need LiveData. I just want to return values of type Long.
How can I do it?
Repo
class WorkoutListRepository(private val dao: WorkoutDao) {
#RequiresApi(Build.VERSION_CODES.O)
suspend fun createDailyLog(part: BodyPart) : Long {
...
return dao.insertDailyLog(data)
}
}
ViewModel
class WorkoutListViewModel(
private val repository: WorkoutListRepository
) : ViewModel() {
...
#RequiresApi(Build.VERSION_CODES.O)
fun createDailyLog(part: BodyPart) : Long {
viewModelScope.launch(Dispatchers.IO) {
return#launch repository.createDailyLog(part) // can't return
}
}
}
Fragment
class WorkoutListTabPagerFragment : Fragment(), WorkoutListAdapter.OnItemClickListener {
...
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
_binding = FragmentWorkoutListTabPagerBinding.inflate(inflater, container, false)
...
return binding.root
}
#RequiresApi(Build.VERSION_CODES.O)
override fun onItemClick(workout: String) {
when(PageState.curPageState) {
is PageState.startWorkout -> {
val id = vm.createDailyLog(part)
...
}
is PageState.addWorkout -> //TODO:
is PageState.editWorkout -> //TODO:
}
}
}
But I don't need LiveData
You do. You need some kind of observable data holder because the code inside launch is asynchronous. It doesn't run immediately. It is only kind of scheduled for execution. launch function, on the other hand, returns immediately, i.e. your createDailyLog function in ViewModel returns before the call to repository.createDailyLog(part) is made. So you can't return a value synchronously from an asynchronous method.
You could either use LiveData or Kotlin's StateFlow to send this data to the Fragment. Your fragment will observe changes to that state and respond accordingly. I suggest using StateFlow here. The code will look somewhat like this:
// ViewModel
class WorkoutListViewModel(
private val repository: WorkoutListRepository
) : ViewModel() {
private val _logIdFlow = MutableStateFlow<Long?>(null)
val logIdFlow = _logIdFlow.asStateFlow()
...
#RequiresApi(Build.VERSION_CODES.O)
fun createDailyLog(part: BodyPart) : Long {
viewModelScope.launch(Dispatchers.IO) {
_logIdFlow.value = repository.createDailyLog(part)
}
}
}
// Fragment
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
_binding = FragmentWorkoutListTabPagerBinding.inflate(inflater, container, false)
...
viewLifecycleOwner.lifecycleScope.launch {
viewModel.logIdFlow.collect { logId ->
if(logId != null) {
// Do whatever you want with the log Id
}
}
}
return binding.root
}
An alternate solution can be to use Kotlin Channel and send data through that Channel.
If you just need a quick, short solution, you can call the repository function from the Fragment's lifecycle scope directly, like this:
// ViewModel
suspend fun createDailyLog(part: BodyPart) : Long {
return repository.createDailyLog(part)
}
//Fragment
override fun onItemClick(workout: String) {
viewLifecycleOwner.lifecycleScope.launch {
when(PageState.curPageState) {
is PageState.startWorkout -> {
val id = vm.createDailyLog(part) // This will now work
...
}
is PageState.addWorkout -> //TODO:
is PageState.editWorkout -> //TODO:
}
}
}
The only problem with this solution is that, now db operation is tied to fragment's lifecycle. So if there is any event which destroy's fragment's lifecycle (like a config change), the operation will be cancelled. This shouldn't be that big of an issue here as your db operation will only take a few milliseconds. But the first option of using a StateFlow or Channel to send data to Fragment/Activity is a more general and recommended way. You can go with whichever option you like.
I have a view model that is data binded to a fragment. The view model is shared with the main activity.
I've button is binded to the view as follows:
<Button
android:id="#+id/startStopBtn"
android:text="#{dashboardViewModel.startStopText == null ? #string/startBtn : dashboardViewModel.startStopText}"
android:onClick = "#{() -> dashboardViewModel.onStartStopButton(context)}"
android:layout_width="83dp"
android:layout_height="84dp"
android:layout_gravity="center_horizontal|center_vertical"
android:backgroundTint="#{dashboardViewModel.isRecStarted == false ? #color/startYellow : #color/stopRed}"
tools:backgroundTint="#color/startYellow"
android:duplicateParentState="false"
tools:text="START"
android:textColor="#FFFFFF" />
What I expect to happen is that every time I press the button the function onStartStopButton(context) runs. This works fine as long as I don't rotate the device. When I rotate the device the function is run twice, if I rotate again the function is run 3 times and so on. This is not a problem if I go to another fragment and then back to the dashboard fragment. It looks like the live data observer is getting registered every time I rotate my screen, but not every time I detach and reattach the fragment.
This is true for all the elements in that fragment, whether they are data binded or I manually observe them.
Fragment code:
class DashboardFragment : Fragment() {
private var _binding: FragmentDashboardBinding? = null
private val binding get() = _binding!!
private val dashboardViewModel: DashboardViewModel by activityViewModels()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
_binding = FragmentDashboardBinding.inflate(inflater, container, false)
val root: View = binding.root
binding.dashboardViewModel = dashboardViewModel
binding.lifecycleOwner = viewLifecycleOwner
dashboardViewModel.bleSwitchState.observe(viewLifecycleOwner, Observer { switchState -> handleBleSwitch(switchState) })
dashboardViewModel.yLims.observe(viewLifecycleOwner, Observer { yLims ->
updatePlotWithNewData(yLims.first, yLims.second)
})
Timber.i("Dahsboard on create: DashboardViewModel in fragment: $dashboardViewModel")
return root
}
}
The view model:
class DashboardViewModel : ViewModel() {
//region live data
private var _isRecStarted = MutableLiveData<Boolean>()
val isRecStarted: LiveData<Boolean> get() = _isRecStarted
//private var _bleSwitchState = MutableLiveData<Boolean>()
val bleSwitchState = MutableLiveData<Boolean>()
private var _startStopText = MutableLiveData<String>()
val startStopText: LiveData<String> get() = _startStopText
private var _yLims = MutableLiveData<Pair<kotlin.Float,kotlin.Float>>()
val yLims: LiveData<Pair<kotlin.Float,kotlin.Float>> get() = _yLims
//endregion
init {
Timber.d("DashboardViewModel created!")
bleSwitchState.value = true
}
//region start stop button
fun onStartStopButton(context: Context){
Timber.i("Start stop button pressed, recording data size: ${recordingRawData.size}, is started: ${isRecStarted.value}")
isRecStarted.value?.let{ isRecStarted ->
if (!isRecStarted){ // starting recording
_isRecStarted.postValue(true)
_startStopText.postValue(context.getString(R.string.stopBtn))
startDurationTimer()
}else{ // stopping recording
_isRecStarted.postValue(false)
_startStopText.postValue(context.getString(R.string.startBtn))
stopDurationTimer()
}
} ?: run{
Timber.e("Error! Is rec started is not there for some reason")
}
}
}
The view model is created the first time from the MainActivity as follows:
class MainActivity : AppCompatActivity() {
private val dashboardViewModel: DashboardViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
Timber.i("DashboardViewModel in main activity: $dashboardViewModel")
}
}
Edit explaining why the MainActivity is tided to the ViewModel:
The reason why the ViewModel is linked to the main activity is that the main activity handles some Bluetooth stuff for a stream of data, when a new sample arrives then the logic to handle it and update the UI of the dashboard fragment is on the DashboardViewModel. The data still needs to be handled even if the dashboard fragment is not there.
So I need to pass the new sample to the DashboardViewModel from the main activity as that is where I receive it. Any suggestions to make this work?
As you know, when you instantiate the ViewModel of a Fragment with activityViewModels, it means that the ViewModel will follow the lifecycle of the Activity containing that Fragment. Specifically here is MainActivity.
So what does ViewModel tied to Activity lifecycle mean in your case?
When you return to the Fragment, normally LiveData (with ViewModel attached to Fragment lifcycler) will trigger again.
But when that ViewModel is attached to the Activity's lifecycle, the LiveData will not be triggered when returning to the Fragment.
That leads to when you return to the Fragment, your LiveData doesn't trigger again.
And that LiveData only triggers according to the life cycle of the activity. That is, when you rotate the screen, the Activity re-initializes, now your LiveData is triggered.
EDIT:
Here, I will give you one way. Maybe my code below doesn't work completely for your case, but I think it will help you in how to control LiveData and ViewModel when you bind ViewModel to Activity.
First, I recommend that each Fragment should have its own ViewModel and it should not depend on any other Fragment or Activity. Here you should rename the DashboardViewModel initialized by activityViewModels() as ShareViewModel or whatever you feel it is related to this being the ShareViewModel between your Activity and Fragment.
class DashboardFragment : Fragment() {
// Change this `DashboardViewModel` to another class name. Could be `ShareViewModel`.
private val shareViewModel: ShareViewModel by activityViewModels()
// This is the ViewModel attached to the DashboardFragment lifecycle.
private val viewModel: DashboardViewModel by viewModels()
private lateinit var _binding: FragmentDashboardBinding? = null
private val binding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
_binding = FragmentDashboardBinding.inflate(inflater, container, false)
binding.dashboardViewModel = viewModel
binding.lifecycleOwner = viewLifecycleOwner
return binding.root
}
override fun onDestroyView() {
_binding = null
super.onDestroyView()
}
}
Next, when there is data triggered by the ShareViewModel's LiveData, you will set the value for the LiveData in the ViewModel associated with your Fragment. As follows:
DashboardViewModel.kt
class DashboardViewModel: ViewModel() {
private val _blueToothSwitchState = MutableLiveData<YourType>()
val blueToothSwitchState: LiveData<YourType> = _blueToothSwitchState
private val _yLims = MutableLiveData<Pair<YourType, YourType>>()
val yLims: LiveData<Pair<YourType, YourType>> = _blueToothSwitchState
fun setBlueToothSwitchState(data: YourType) {
_blueToothSwitchState.value = data
}
fun setYLims(data: Pair<YourType, YourType>) {
_yLims.value = data
}
}
DashboardFragment.kt
class DashboardFragment : Fragment() {
...
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
shareViewModel.run {
bleSwitchState.observe(viewLifeCycleOwner) {
viewModel.setBlueToothSwitchState(it)
}
yLims.observe(viewLifeCycleOwner) {
viewModel.setYLims(it)
}
}
viewModel.run {
// Here, LiveData fires observe according to the life cycle of `DashboardFragment`.
// So when you go back to `DashboardFragment`, the LiveData is re-triggered and you still get the observation of that LiveData.
blueToothSwitchState.observe(viewLifeCycleOwner, ::handleBleSwitch)
yLims.observe(viewLifeCycleOwner) {
updatePlotWithNewData(it.first, it.second)
}
}
}
...
}
Edit 2:
In case you rotate the device, the Activity and Fragment will be re-initialized. At that time, LiveData will fire observe. To prevent that, use Event. It will keep your LiveData from observing the value until you set the value again for LiveData.
First, let's create a class Event.
open class Event<out T>(private val content: T) {
var hasBeenHandled = false
private set
fun getContentIfNotHandled(): T? = if (hasBeenHandled) {
null
} else {
hasBeenHandled = true
content
}
fun peekContent(): T = content
}
Next, modify the return type of the LiveData that you want to trigger once.
ShareViewModel.kt
class ShareViewModel: ViewModel() {
private val _test = MutableLiveData<Event<YourType>>()
val test: LiveData<Event<YourType>> = _test
fun setTest(value: YourType) {
_test.value = Event(value)
}
}
Add this extension to easily get LiveData's observations.
LiveDataExt.kt
fun <T> LiveData<Event<T>>.eventObserve(owner: LifecycleOwner, observer: (t: T) -> Unit) {
this.observe(owner) { it?.getContentIfNotHandled()?.let(observer) }
}
Finally in the view, you get the data observed by LiveDatat.
class DashboardFragment : Fragment() {
...
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
shareViewModel.test.eventObserve(viewLifeCycleOwner) {
Timber.d("This is test")
}
}
...
}
Note: When using LiveData with Event, make sure that LiveData is not reset when rotating the device. If LiveData is set to value again, LiveData will still trigger even if you use Event.
below is my ViewModel class which accepts application:Application as parameter.I want to launch another fragment from this class.But in remove() method,how do I pass fragment.
class EmailConfirmationFragmentViewModel(application: Application) : AndroidViewModel(application) {
private lateinit var viewModelApplication: Application
init {
this.viewModelApplication = application
}
var email = MutableLiveData<String>()
private var emailMutableLiveData: MutableLiveData<UserEmail>? = null
val userEmail: MutableLiveData<UserEmail>
get() {
if (emailMutableLiveData == null) {
emailMutableLiveData = MutableLiveData<UserEmail>()
}
return emailMutableLiveData!!
}
fun onEmailChanged(s: CharSequence, start: Int, before: Int, count: Int) {
if (s.toString() != null && !s.toString().equals(""))
email.value = s.toString()
}
fun onConfirmClicked(view: View) {
userEmail.value = UserEmail(email.value.toString())
launchResetPasswordFragment()
}
private fun launchResetPasswordFragment() {
try {
(viewModelApplication as FragmentActivity).supportFragmentManager.beginTransaction()
.replace(R.id.fl_Wrapper, OtpVerificationFragement()).remove(viewModelApplication.applicationContext).commit()
}
catch(e:Exception)
{
Log.e("Error","$e")
}
}
}
Lifecycle events and Fragment transactions should never take place inside of a view model. As discussed in the ViewModel Overview, a "ViewModel must never reference a view, Lifecycle, or any class that may hold a reference to the activity context." While the AndroidViewModel does introduce an anti-pattern by exposing a reference to the application, this specific use case is not an appropriate one. In situations where the view model should invoke a fragment transaction, it's most commonly handled by the general concept of an event dispatched from the view model to the Lifecycle Owner. I believe employing such a pattern can resolve your issue. While I don't know the state of your Fragment, I've devised a likely solution.
class EmailConfirmationViewModel() : ViewModel() {
val email: MutableLiveData<String> = MutableLiveData()
private val _resetFragment: MutableLiveData<Event> = MutableLiveData()
val resetFragment: LiveData<Event> = _resetFragment
val userEmail: UserEmail?
get() = email.value?.let { UserEmail(it) }
fun onEmailChanged(s: CharSequence) {
email.value = s.toString()
}
fun onConfirmClicked() {
resetFragment()
}
private fun resetFragment() {
_resetFragment.value = Event()
}
}
Where the supporting event classes could appear as such:
class Event : EventWithValue<Unit>(Unit)
open class EventWithValue<T>(
private val value: T,
) {
private var isHandled = false
fun getValueIfUnhandled(): T? = if (isHandled) {
null
} else {
handleValue()
}
private fun handleValue(): T {
isHandled = true
return value
}
}
class EventObserver<T>(
private val eventIfUnhandled: (value: T) -> Unit,
) : Observer<EventWithValue<T>?> {
override fun onChanged(event: EventWithValue<T>?) {
event?.getValueIfUnhandled()?.let { eventIfUnhandled(it) }
}
}
Through observing the event in the Fragment itself, you eliminate the need to reference any sort of view in the view model while maintaining the view model's role as the dispatcher. Here's a brief description of how you would listen to the event from your Lifecycle Owner, in this case, a Fragment.
class EmailConfirmationFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val view: View? = super.onCreateView(inflater, container, savedInstanceState)
val viewModel: EmailConfirmationViewModel by viewModels()
viewModel.resetFragment.observe(viewLifecycleOwner, EventObsever {
// Call a function of the activity's viewModel (ideal), or complete the transaction here through referencing the activity directly (ill-advised)
})
return view
}
}
I think exposing userEmail is a bit of a code smell in itself. Alternatively, you could define the resetFragment event as
private val _resetFragment: MutableLiveData<EventWithValue<UserEmail>> = MutableLiveData()
val resetFragment: LiveData<EventWithValue<UserEmail>> = _resetFragment
and receive the value of the userEmail directly within the event listener featured above. This would remove the need to expose the userEmail of the view model.
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.
My LiveData is not working.
ViewModel:
private var _email = MutableLiveData<String>()
fun setEmail(){
_email.postValue("azizjon#bla.mn")
}
fun getEmail(): LiveData<String>{
return _email
}
Fragment's onViewCreated method
:
mViewModel.getEmail().observe(viewLifecycleOwner, Observer {
tvEmail.text = it
})
mViewModel.setEmail() //Trying to post data to my LiveData.
The above code is not working as tvEmail is not chaning.
However, if I trust a button for posting data to LiveData like this, it is working:
//Inside fragment again
button.setOnClickListener {
mViewModel.setEmail()
}
When user clicks button, text in tvEmail is changing. If user does not click, nothing is happening. What am I missing here?
Edit:
I have just tested the code with Activity. Surprisingly, for Activitys it is working but not for Fragments.
#Azizjon Kholmatov - Best practice you can write your code inside the "onActivityCreated" function
please refer this. If you still having the problem let me know in comments section. I am happy to help. :)
class MainFragment : Fragment() {
companion object {
fun newInstance() = MainFragment()
}
private lateinit var viewModel: MainViewModel
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
return inflater.inflate(R.layout.main_fragment, container, false)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
viewModel = ViewModelProviders.of(this).get(MainViewModel::class.java)
viewModel.getEmail().observe(viewLifecycleOwner, Observer {
tvEmail.text = it
})
viewModel.setEmail("first#email.com")
button.setOnClickListener {
viewModel.setEmail("clicked#email.com")
}
}
}
And ViewModel class as follows
class MainViewModel : ViewModel() {
private var _email = MutableLiveData<String>()
fun setEmail(email: String = "example#email.com") {
_email.postValue(email)
}
fun getEmail(): LiveData<String> {
return _email
}
}