Observe LiveData instance in fragment and activity - android

I have a simple scenario where I do something in fragment and when I receive the LiveData I want to do something in Activity.
ViewModel:
class MyViewModel(application: Application) : AndroidViewModel(application) {
...
fun getUser(id: String): LiveData<User> {
return repository.getUser(id)
}
}
Fragment:
class MyFragment : Fragment() {
private lateinit var myViewModel: MyViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
activity?.run {
myViewModel = ViewModelProviders.of(this).get(MyViewModel::class.java)
} ?: throw Exception("Invalid Activity")
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment, container, false)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
button.setOnClickListener {
showProgressBar()
myViewModel.getUser(editText.text.toString()).observe(this, Observer { it ->
//TODO
})
}
}
}
Activity:
class MainActivity : AppCompatActivity() {
private lateinit var myViewModel: MyViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
myViewModel = ViewModelProviders.of(this).get(MyViewModel::class.java)
//Here I would like to observe the user instance returned from the getUser() method
}
}
So the issue I'm having is that I would like to have an instance of LiveData<User> in MyViewModel so that I could observe it in Activity and Fragment. How can I achieve this?

In Fragemnts (as best practice) Should Use
viewModel.userLiveData.observe(viewLifecycleOwner, Observer {
//your code here
})
In Activity Use
viewModel.userLiveData.observe(this, Observer {
//your code here
})

in class MyViewModel create
val userLiveData =MutableLiveData<User>()
and getter
fun getUserLiveData(id: String): MutableLiveData<User> {
return userLiveData
}
fun getUser(id: String){
val disposableUser = repository.getUser(id)
.subscribe({
userLivedata.postValue(it)
})
}
And in activity or Fragment called
myViewModel.getUserLiveData.observe(this, Observer { it ->
//TODO
})
myViewModel.getUser(...)
And Now in ViewModel you have object User (userLiveData.getValue())

Write method in Activity doSomethingWithUser(user: User)
And in your livedata
myViewModel.getUser(editText.text.toString()).observe(this, Observer { it ->
(requireActivity() as MainActivity).doSomethingWithUser(it)
})

Related

Is it normal for the ViewModel class to be called without setValue on LiveData?

I am studying the MVVM pattern.
I have a question regarding LiveData while using ViewModel class.
Even if I do not change the value of LiveData with setValue or postValue, it continues to observe and execute the fragment.
When addRoutine() is called, vm.observe also continues to run.
As you can see there is no setValue or postValue in addRoutine(), so LiveData has no value change at all.
But why does vm.observe keep running?
This is my code.
ViewModel.kt
class WriteRoutineViewModel : ViewModel() {
private val _items: MutableLiveData<List<RoutineModel>> = MutableLiveData(listOf())
private val rmList = arrayListOf<RoutineModel>()
val items: LiveData<List<RoutineModel>> = _items
fun addRoutine(workout: String) {
val rmItem = RoutineModel(UUID.randomUUID().toString(), workout, "TEST")
rmItem.getSubItemList().add(RoutineDetailModel("2","3","3123"))
rmList.add(rmItem)
// _items.postValue(rmList)
}
fun getListItems() : List<RoutineItem> {
val listItems = arrayListOf<RoutineItem>()
for(testRM in rmList) {
listItems.add(RoutineItem.RoutineModel(testRM.id,testRM.workout,testRM.unit))
val childListItems = testRM.getSubItemList().map { detail ->
RoutineItem.DetailModel("2","23","55")
}
listItems.addAll(childListItems)
}
return listItems
}
}
Fragment
class WriteRoutineFragment : Fragment() {
private var _binding : FragmentWriteRoutineBinding? = null
private val binding get() = _binding!!
private val vm : WriteRoutineViewModel by viewModels { WriteRoutineViewModelFactory() }
private lateinit var epoxyController : RoutineItemController
override fun onCreateView(inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?): View? {
_binding = FragmentWriteRoutineBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
getTabPageResult()
// RecyclerView(Epoxy) Update
vm.items.observe(viewLifecycleOwner) { updatedItems ->
epoxyController.setData(vm.getListItems())
}
}
private fun getTabPageResult() {
val navController = findNavController()
navController.currentBackStackEntry?.also { stack ->
stack.savedStateHandle.getLiveData<String>("workout")?.observe(
viewLifecycleOwner, Observer { result ->
vm.addRoutine(result)
stack.savedStateHandle?.remove<String>("workout")
}
)
}
}
}

LiveData observing not working after Room database updates

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.

Fragment.viewLifecycleOwnerLiveData.observe doesn't call with kodein

i'm making an app and i want to separate my UI logic into multiple UI classes with BaseUi class being lifecycle aware. I'm using Kodein as my DI and i have an issue with fragment.viewLifecycleOwnerLiveData.observe not being called when instance of my ui class is being retrieved by Kodein.
Here is my Fragment class:
class ListFragment : Fragment(), DIAware {
override val di: DI by closestDI()
override val diTrigger: DITrigger = DITrigger()
private var binding: FragmentMoviesBinding? = null
private val fragmentBinding get() = binding
private val kodeinMoviesUi: MoviesUi by instance() //fragment does not observe viewLifecycleOwnerLiveData
private val moviesUi: MoviesUi = MoviesUi(this) //fragment now observe viewLifecycleOwnerLiveData
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = FragmentMoviesBinding.inflate(inflater, container, false)
return binding?.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
diTrigger.trigger()
}
override fun onDestroyView() {
super.onDestroyView()
binding = null
}
}
BaseUi class:
abstract class BaseUi<F : Fragment>(private val fragment: F) : LifecycleObserver {
init {
fragment.viewLifecycleOwnerLiveData.observe(fragment, { subscribeToLifecycle() })
}
private fun subscribeToLifecycle() {
fragment.viewLifecycleOwner.lifecycle.addObserver(object : LifecycleObserver {
#OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
fun onCreate() {
onViewCreated()
}
})
}
abstract fun onViewCreated()
}
And UiModule:
val uiModule = DI.Module("uiModule") {
bind<ListFragment>() with provider { ListFragment() }
bind<MoviesUi>() with provider { MoviesUi(instance()) }
}
Cross post from https://github.com/Kodein-Framework/Kodein-DI/issues/353
Here is your problem bind<ListFragment>() with provider { ListFragment() }.
You bound the ListFragment with a provider, meaning every time you ask to the container it will create an instance of ListFragment. So, when you inject MoviesUi with private val kodeinMoviesUi: MoviesUi by instance(), it gets another instance of ListFragment.
I suggest that you define the binding for MoviesUi as a factory, waiting to receive a ListFragment instance:
bind<MoviesUi>() with factory {fragment: ListFragment -> MoviesUi(fragment) }
then you can inject it in the ListFragment like:
private val kodeinMoviesUi: MoviesUi by instance(args = this)

Live data giving old value again while changing fragment

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)
}
}

LiveData not working until user performs some action

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
}
}

Categories

Resources