I am using live data from a shared ViewModel across multiple fragments. I have a sign-in fragment which takes user's phone number and password and then the user presses sign in button I am calling the API for that, now if the sign-in fails I am showing a toast "Sign In failed", now if the user goes to "ForgotPassword" screen which also uses the same view model as "SignInFragment" and presses back from the forgot password screen, it comes to sign-in fragment, but it again shows the toast "Sign In failed" but the API is not called, it gets data from the previously registered observer, so is there any way to fix this?
SignInFragment.kt
class SignInFragment : Fragment() {
private lateinit var binding: FragmentSignInBinding
//Shared view model across two fragments
private val onBoardViewModel by activityViewModels<OnBoardViewModel>()
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = DataBindingUtil.inflate(
inflater,
R.layout.fragment_sign_in,
container,
false
)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
onBoardViewModel.signInResponse.observe(viewLifecycleOwner) { response ->
//This is calling again after coming back from new fragment it.
showToast("Sign In Failed")
}
}
override fun onClick(v: View?) {
when (v?.id!!) {
R.id.forgotPasswordTV -> {
findNavController().navigate(SignInFragmentDirections.actionSignInFragmentToForgotPasswordFragment())
}
R.id.signInTV -> {
val phoneNumber = binding.phoneNumberET.text
val password = binding.passwordET.text
val signInRequestModel = SignInRequestModel(
phoneNumber.toString(),
password.toString(),
""
)
//Calling API for the sign-in
onBoardViewModel.callSignInAPI(signInRequestModel)
}
}
}
}
ForgotPasswordFragment
class ForgotPasswordFragment : Fragment() {
private lateinit var binding: FragmentForgotPasswordBinding
//Shared view model across two fragments
private val onBoardViewModel by activityViewModels<OnBoardViewModel>()
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = DataBindingUtil.inflate(
inflater,
R.layout.fragment_forgot_password,
container,
false
)
return binding.root
}
}
OnBoardViewModel
class OnBoardViewModel : ViewModel() {
private var repository: OnBoardRepository = OnBoardRepository.getInstance()
private val signInRequestLiveData = MutableLiveData<SignInRequestModel>()
//Observing this data in sign in fragment
val signInResponse: LiveData<APIResource<SignInResponse>> =
signInRequestLiveData.switchMap {
repository.callSignInAPI(it)
}
//Calling this function from sign in fragment
fun callSignInAPI(signInRequestModel: SignInRequestModel) {
signInRequestLiveData.value = signInRequestModel
}
override fun onCleared() {
super.onCleared()
repository.clearRepo()
}
}
I have tried to move this code inside onActivityCreated but it's still getting called after coming back from new fragment.
onBoardViewModel.signInResponse.observe(viewLifecycleOwner) { response ->
showToast("Sign In Failed")
}
Using SingleLiveEvent class instead of LiveData in OnBoardViewModel class will solve your problem:
val signInResponse: SingleLiveEvent <APIResource<SignInResponse>>.
class SingleLiveEvent<T> : MutableLiveData<T>() {
private val pending = AtomicBoolean(false)
override fun observe(owner: LifecycleOwner, observer: Observer<in T>) {
super.observe(owner, Observer<T> { t ->
if (pending.compareAndSet(true, false)) {
observer.onChanged(t)
}
})
}
override fun setValue(t: T?) {
pending.set(true)
super.setValue(t)
}
fun call() {
postValue(null)
}
}
This is a lifecycle-aware observable that sends only new updates after subscription. This LiveData only calls the observable if there's an explicit call to setValue() or call().
I would provide a way to reset your live data. Give it a nullable type. Your observers can ignore it when they get a null value. Call this function when you receive login data, so you also won't be repeating messages on a screen rotation.
class OnBoardViewModel : ViewModel() {
// ...
fun consumeSignInResponse() {
signInRequestLiveData.value = null
}
}
onBoardViewModel.signInResponse.observe(viewLifecycleOwner) { response ->
if (response != null) {
showToast("Sign In Failed")
onBoardViewModel.consumeSignInResponse()
}
}
For Kotlin users #Sergey answer can also be implemented using delegates like below
class SingleLiveEvent<T> : MutableLiveData<T>() {
var curUser: Boolean by Delegates.vetoable(false) { property, oldValue, newValue ->
newValue != oldValue
}
override fun observe(owner: LifecycleOwner, observer: Observer<in T>) {
super.observe(owner, Observer<T> { t ->
if (curUser) {
observer.onChanged(t)
curUser = false
}
})
}
override fun setValue(t: T?) {
curUser = true
super.setValue(t)
}
fun call() {
postValue(null)
}
}
Related
I'm trying to use Firebase auth with MVVM and Dependency Injection, the issue is that my auth state is stuck in Loading for several minutes before authentication, It takes so much time to authenticate. It was working fine until 2 days ago and I can't find a solution to it.
My ViewModel
#HiltViewModel
class AppViewModel #Inject constructor(private val firebaseAuth: FirebaseAuth) : ViewModel() {
private val _register = MutableStateFlow<Resource<FirebaseUser>>(Resource.Unspecified())
val register : Flow<Resource<FirebaseUser>> = _register
private val _validation = Channel<RegisterFieldState>()
val validation = _validation.receiveAsFlow()
fun signUpWithEmail(email: String, password: String) {
viewModelScope.launch {
_register.emit(Resource.Loading())
}
firebaseAuth.createUserWithEmailAndPassword(email, password)
.addOnSuccessListener {
it.user?.let {
_register.value = Resource.Success(it)
}
}
.addOnFailureListener {
_register.value = Resource.Error(it.message.toString())
}
}
My register fragment
#AndroidEntryPoint
class RegisterFragment : Fragment() {
private lateinit var _binding : FragmentRegisterBinding
private val binding get() = _binding
private val viewModel : AppViewModel by viewModels()
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
_binding = FragmentRegisterBinding.inflate(layoutInflater,container,false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.loginButton.setOnClickListener {
signUp()
}
}
private fun signUp(){
binding.apply {
viewModel.signUpWithEmail(email.text.toString(), password.text.toString())
// val intent = Intent(this#RegisterActivity,HomeActivity::class.java)
// startActivity(intent)
lifecycleScope.launchWhenStarted {
viewModel.register.collect{
when(it){
is Resource.Loading -> {
Toast.makeText(context,"Loading", Toast.LENGTH_SHORT).show()
}
is Resource.Success -> {
// findNavController().navigate(R.id.loginFragment)
Toast.makeText(context,"Successful", Toast.LENGTH_SHORT).show()
Log.d("Testing",it.toString())
}
is Resource.Error -> {
Toast.makeText(context,"Error", Toast.LENGTH_SHORT).show()
Log.d("Testing Failure",it.toString())
}
else -> {
Toast.makeText(context,"N/A", Toast.LENGTH_SHORT).show()}
}
}
}
}
}
}
Solution : Downgraded my emulator version to 31.2.9
StateFlow is emitting new data after change, but ListAdapter is not being updated/notified, but when configuration is changed(i.e device is rotated from Portrait to Landscape mode) update is occurred:
class TutorialListFragment : Fragment() {
private lateinit var binding: FragmentTutorialListBinding
private val viewModel: ITutorialViewModel by viewModels<TutorialViewModelImpl>()
private lateinit var adapter: TutorialAdapter
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = FragmentTutorialListBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val recyclerView = binding.recyclerView
adapter = TutorialAdapter()
recyclerView.adapter = adapter
loadData()
}
private fun loadData() {
viewModel
.getTutorialList()
val tutorialList: MutableList<TutorialResponse> = mutableListOf()
viewModel
.tutorialListStateFlow
.onEach { list ->
list.forEach {tutorialResponse->
tutorialList.add(tutorialResponse)
Log.e("TUTORIAL_LIST_FRAG", "$tutorialResponse")
}
adapter.submitList(tutorialList)
}.launchIn(viewLifecycleOwner.lifecycleScope)
}
}
View model is:
class TutorialViewModelImpl: ViewModel(), ITutorialViewModel {
private val mTutorialRepository: ITutorialRepository = TutorialRepositoryImpl()
private val _tutorialListStateFlow = MutableStateFlow<List<TutorialResponse>>(mutableListOf())
override val tutorialListStateFlow: StateFlow<List<TutorialResponse>>
get() = _tutorialListStateFlow.asStateFlow()
init {
mTutorialRepository
.getTutorialListSuccessListener {
viewModelScope
.launch {
_tutorialListStateFlow.emit(it)
Log.e("TUTORIAL_GL_VM", "$it")
}
}
}
override fun getTutorialList() {
// Get list
mTutorialRepository.getTutorialList()
}
}
When I look into Logcat I see this line:
Log.e("TUTORIAL_GL_VM", "$it")
prints all the changes, but no update in ListAdapter.
I assume your data from mTutorialRepository is not a flow ,so you must add .toList() if you want to emit list in stateFlow to get notified
mTutorialRepository.getTutorialListSuccessListener {
viewModelScope.launch {
// here add .toList()
_tutorialListStateFlow.emit(it.toList())
}
}
or if it still does not works, try to change your loadData() like this
private fun loadData() {
// idk what are doing with this ??
viewModel.getTutorialList()
lifecycleScope.launch {
viewModel.tutorialListStateFlow.collect { list ->
adapter.submitList(list)
}
}
}
My app contains a Room db of cocktail recipes that it downloads via a Retrofit api call, all of that is working well. To focus in on where my problem lies, my use case is a user adding a cocktail to a list. This is done via a DialogFragment and here the DialogFragment displays, the transaction executes, the DialogFragment goes away and the Room db is updated. The cocktail fragment does not get the update though - if you navigate away and back, the update is visible so I know the transaction worked as expected. One thing I just noticed is if I rotate the device, the update gets picked up as well.
Here is the relevant section of my fragment:
class CocktailDetailFragment : BaseFragment<CocktailDetailViewModel, FragmentCocktailDetailBinding, CocktailDetailRepository>() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel.cocktail.observe(viewLifecycleOwner, Observer {
when(it){
is Resource.Success -> {
updateCocktail(it.value)
}
}
})
}
private fun updateCocktail(cocktail: Cocktail) {
with(binding){
detailCocktailName.text = cocktail.cocktailName
//...
//this is the piece of functionality i'm expecting the LiveData observer to execute and change the drawable
if(cocktail.numLists > 0) {
detailCocktailListImageView.setImageResource(R.drawable.list_filled)
} else {
detailCocktailListImageView.setImageResource(R.drawable.list_empty)
}
}
}
override fun getViewModel() = CocktailDetailViewModel::class.java
override fun getFragmentBinding(
inflater: LayoutInflater,
container: ViewGroup?
) = FragmentCocktailDetailBinding.inflate(inflater,container,false)
override fun getFragmentRepository(): CocktailDetailRepository {
val dao = GoodCallDatabase(requireContext()).goodCallDao()
return CocktailDetailRepository(dao)
}
}
BaseFragment:
abstract class BaseFragment<VM: BaseViewModel, B: ViewBinding, R: BaseRepository>: Fragment() {
protected lateinit var userPreferences: UserPreferences
protected lateinit var binding: B
protected lateinit var viewModel: VM
protected val remoteDataSource = RemoteDataSource()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
userPreferences = UserPreferences(requireContext())
binding = getFragmentBinding(inflater, container)
val factory = ViewModelFactory(getFragmentRepository())
viewModel = ViewModelProvider(this, factory).get(getViewModel())
return binding.root
}
abstract fun getViewModel() : Class<VM>
abstract fun getFragmentBinding(inflater: LayoutInflater, container: ViewGroup?): B
abstract fun getFragmentRepository(): R
}
ViewModelFactory:
class ViewModelFactory(
private val repository: BaseRepository
): ViewModelProvider.NewInstanceFactory() {
#Suppress("UNCHECKED_CAST")
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
return when{
modelClass.isAssignableFrom(CocktailDetailViewModel::class.java) -> CocktailDetailViewModel(repository as CocktailDetailRepository) as T
else -> throw IllegalArgumentException("ViewModel class not found")
}
}
}
ViewModel:
class CocktailDetailViewModel(
private val repository: CocktailDetailRepository
): BaseViewModel(repository) {
private val _cocktail: MutableLiveData<Resource<Cocktail>> = MutableLiveData()
val cocktail: LiveData<Resource<Cocktail>>
get() = _cocktail
fun getCocktailByCocktailId(cocktailId: Int) = viewModelScope.launch {
_cocktail.value = Resource.Loading
_cocktail.value = repository.getCocktail(cocktailId)
}
}
Repository:
class CocktailDetailRepository(
private val dao: GoodCallDao
):BaseRepository(dao) {
suspend fun getCocktail(cocktailId: Int) = safeApiCall {
dao.getCocktail(cocktailId)
}
}
Safe Api Call (I use this so db/api calls run on IO):
interface SafeApiCall {
suspend fun <T> safeApiCall(
apiCall: suspend () -> T
): Resource<T> {
return withContext(Dispatchers.IO) {
try {
Resource.Success(apiCall.invoke())
} catch (throwable: Throwable) {
when (throwable) {
is HttpException -> {
Resource.Failure(false, throwable.code(), throwable.response()?.errorBody())
}
else -> {
Resource.Failure(true, null, null)
}
}
}
}
}
}
Resource:
sealed class Resource<out T> {
data class Success<out T>(val value: T) : Resource<T>()
data class Failure(
val isNetworkError: Boolean,
val errorCode: Int?,
val errorBody: ResponseBody?
): Resource<Nothing>()
object Loading: Resource<Nothing>()
}
Dao:
#Query("SELECT * FROM cocktail c WHERE c.cocktail_id = :cocktailId")
suspend fun getCocktail(cocktailId: Int): Cocktail
Thank you in advance for any help! Given the issue and how the app is working, I believe I have provided all the relevant parts but please advise if more of my code is required to figure this out.
I looked for many articles and tried to understand how Live Data is observe changes when MVVM architecture is used.
I have a Fragment A, ViewModel and Repository class.
ViewModel is initiated in onCreateView() method of the fragment.
Api call is initiated just after that in onCreateView() method of fragment.
Data from the Server is observed in onViewCreated method of the fragment.
For the first, it is running perfectly fine. But When I update the user name from another Fragment B and come back to Fragment A.
Api is called again in onResume() method of Fragment A to update UI. But here my Live Data is not observed again and UI is not updated
I didn't understand what I am doing wrong? Why observer is not triggering second time?
Below is the code
class FragmentA : Fragment(){
private lateinit var dealerHomeViewModel: DealerHomeViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view = inflater.inflate(R.layout.fragment_home_dealers, container, false)
val dealerHomeFactory = DealerHomeFactory(token!!)
dealerHomeViewModel = ViewModelProvider(this,dealerHomeFactory).get(DealerHomeViewModel::class.java)
dealerHomeViewModel.getDealerHomeData()
return view
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
dealerHomeViewModel.dealerInfoLiveData.observe(viewLifecycleOwner, androidx.lifecycle.Observer {dealerInfo ->
// Update UI
tvDealerName.text = dealerInfo.name
})
}
override fun onResume() {
super.onResume()
dealerHomeViewModel.getDealerHomeData()
}
}
//=========================== VIEW MODEL ===================================//
class DealerHomeViewModel(val token:String) : ViewModel() {
var dealerInfoLiveData:LiveData<DealerInfo>
init {
dealerInfoLiveData = MutableLiveData()
}
fun getDealerHomeData(){
dealerInfoLiveData = DealerHomeRepo().getDealerHomePageInfo(token)
}
}
//======================== REPOSITORY ================================//
class DealerHomeRepo {
fun getDealerHomePageInfo(token:String):LiveData<DealerInfo>{
val responseLiveData:MutableLiveData<DealerInfo> = MutableLiveData()
val apiCall: ApiCall? = RetrofitInstance.getRetrofit()?.create(ApiCall::class.java)
val dealerInfo: Call<DealerInfo>? = apiCall?.getDealerInfo(Constants.BEARER+" "+token,Constants.XML_HTTP)
dealerInfo?.enqueue(object : Callback<DealerInfo>{
override fun onFailure(call: Call<DealerInfo>, t: Throwable) {
Log.d(Constants.TAG,t.toString())
}
override fun onResponse(call: Call<DealerInfo>, response: Response<DealerInfo>) {
if(response.isSuccessful){
when(response.body()?.status){
Constants.SUCCESS -> {
responseLiveData.value = response.body()
}
Constants.FAIL -> {
}
}
}
}
})
return responseLiveData
}
}
I think your problem is that you are generating a NEW mutableLiveData each time you use your getDealerHomePageInfo(token:String method.
First time you call getDealerHomePageInfo(token:String) you generate a MutableLiveData and after on onViewCreated you observe it, it has a value.
In onResume, you call again getDealerHomePageInfo(token:String) that generates a NEW MutableLiveData so your observer is pointing to the OLD one.
What would solve your problem is to pass the reference of your viewModel to your repository so it updates the MutableLiveData with each new value, not generate a new one each time.
Edited Answer:
I would do something like this for ViewModel:
class DealerHomeViewModel(val token:String) : ViewModel() {
private val _dealerInfoLiveData:MutableLiveData<DealerInfo> = MutableLiveData()
val dealerInfoLiveData:LiveData = _dealerInfoLiveData
fun getDealerHomeData(){
DealerHomeRepo().getDealerHomePageInfo(token, _dealerInfoLiveData)
}
}
And this for the DealerHomeRemo
class DealerHomeRepo{
fun getDealerHomePageInfo(token:String, liveData: MutableLiveData<DealerInfo>){
val apiCall: ApiCall? = RetrofitInstance.getRetrofit()?.create(ApiCall::class.java)
val dealerInfo: Call<DealerInfo>? = apiCall?.getDealerInfo(Constants.BEARER+" "+token,Constants.XML_HTTP)
dealerInfo?.enqueue(object : Callback<DealerInfo>{
override fun onFailure(call: Call<DealerInfo>, t: Throwable) {
Log.d(Constants.TAG,t.toString())
}
override fun onResponse(call: Call<DealerInfo>, response: Response<DealerInfo>) {
if(response.isSuccessful){
when(response.body()?.status){
Constants.SUCCESS -> {
liveData.value = response.body()
}
Constants.FAIL -> {
}
}
}
}
})
}
For Observers, use the LiveData as before:
dealerHomeViewModel.dealerInfoLiveData.observe(viewLifecycleOwner, androidx.lifecycle.Observer {dealerInfo ->
// Update UI
tvDealerName.text = dealerInfo.name
})
I'm trying to understand the concepts of MVVM but i'm having a hard time trying to understand how to communicate between The model class and UI (The fragment) in this case.
Here's the (shitty, be aware) code:
LoginFragment.kt
class LoginFragment: Fragment(), AuthListener {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val binding = DataBindingUtil.inflate<CredentialsLoginFragmentBinding>(
inflater,
R.layout.credentials_login_fragment,
container,
false
)
val viewModel = ViewModelProviders.of(this).get(LoginViewModel::class.java)
val view: View = binding.root
val registerButton: Button = view.findViewById(R.id.register_button)
binding.viewModel = viewModel
viewModel.authListener = this
registerButton.setOnClickListener {
val transaction: FragmentTransaction? = fragmentManager?.beginTransaction()
transaction?.replace(R.id.fragment_container, SignupFragment())?.commit()
}
return view
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val constraintRoot: MotionLayout = view.findViewById(R.id.sign_in_root)
ActivityUtils().switchLayoutAnimationKeyboard(constraintRoot = constraintRoot)
}
override fun onStarted() {
Toast.makeText(context, "Started", Toast.LENGTH_SHORT).show()
}
override fun onSuccess() {
Toast.makeText(context, "Success", Toast.LENGTH_SHORT).show()
}
override fun onError(message: String) {
Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
}}
LoginViewModel.kt
class LoginViewModel: ViewModel(){
var username: String? = null
var password: String? = null
var isCredentialsValid: Boolean = false
var authListener: AuthListener? = null
private val context: Context? = null
fun onLoginButtonClicked(view: View){
if(username.isNullOrEmpty() || password.isNullOrEmpty()){
authListener?.onError("Invalid username or password")
isCredentialsValid = false
return
}
if(!username.isNullOrEmpty() && password!!.length >= 8){
isCredentialsValid = true
authListener?.onSuccess()
}else{
authListener?.onError("Invalid")
}
}}
Lets assume now that I enter an username and password and both meet the criteria. Now i'd like to, when i click on the "Log in" button, the current fragment is replaced by a menu fragment, for example.
How could i achieve something like that ? I've tried to replace from the ViewModel class, but that doesn't work.
Should I take the result of "isCredentialsValid" from the VM class and respond accordingly in the LoginFragment class ?
Thank you.
You have to use live data for updating the data from viewModel to view. I will post the code how it should be, but make sure that you need to understand the concept of LiveData.
LoginViewModel.kt
class LoginViewModel: ViewModel(){
var username: String? = null
var password: String? = null
var isCredentialsValid: Boolean = false
var authListener: AuthListener? = null
private val context: Context? = null
// LiveData to udpate the UI
private val _isValidCredential = MutableLiveData<Boolean>()
val isValidCredential: LiveData<Boolean> = _isValidCredential
fun onLoginButtonClicked(view: View){
if(username.isNullOrEmpty() || password.isNullOrEmpty()){
authListener?.onError("Invalid username or password")
isCredentialsValid = false
return
}
if(!username.isNullOrEmpty() && password!!.length >= 8){
isCredentialsValid = true
// to update the value of live data wherever you need
_isValidCredential.value = true
authListener?.onSuccess()
}else{
authListener?.onError("Invalid")
// to update the value of live data wherever you need
_isValidCredential.value = false
}
}
}
Your Fragment should be
LoginFragment.kt
class LoginFragment: Fragment(), AuthListener {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val binding = DataBindingUtil.inflate<CredentialsLoginFragmentBinding>(
inflater,
R.layout.credentials_login_fragment,
container,
false
)
val viewModel = ViewModelProviders.of(this).get(LoginViewModel::class.java)
val view: View = binding.root
val registerButton: Button = view.findViewById(R.id.register_button)
binding.viewModel = viewModel
viewModel.authListener = this
// This is the way you need to observe the value
viewModel.isValidCredential.observe(viewLifecycleOwner, Observer {
if(it){
// do your navigation stuff here
}else{
// do your stuff if not valid credential
}
})
registerButton.setOnClickListener {
val transaction: FragmentTransaction? =
fragmentManager?.beginTransaction()
transaction?.replace(R.id.fragment_container, SignupFragment())?.commit()
}
return view
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val constraintRoot: MotionLayout = view.findViewById(R.id.sign_in_root)
ActivityUtils().switchLayoutAnimationKeyboard(constraintRoot = constraintRoot)
}
override fun onStarted() {
Toast.makeText(context, "Started", Toast.LENGTH_SHORT).show()
}
override fun onSuccess() {
Toast.makeText(context, "Success", Toast.LENGTH_SHORT).show()
}
override fun onError(message: String) {
Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
}}
A typical way of communicating back to the UI from the view model is using livedata. In your LoginViewModel, you would set your livedata to either true or false. Inside your view LoginFragment.kt you would have an observer. This observers job is to fire anytime a livedata's value has changed. That way you can have logic in your view that can either shows an error message liveData = false or launch the menu fragment = true.
Here is a good example using livedata to pass data to the view (fragment) this in the docs: https://developer.android.com/topic/libraries/architecture/viewmodel#implement