In Kotlin Android, viewModel has no observer - android

I made a toolbar in a BaseActivity to implement a common and the code is as follows.
// BaseActivity
abstract class BaseActivity<T : ViewBinding> : AppCompatActivity() {
lateinit var cartCnt: TextView
private val viewModel by lazy {
ViewModelProvider(this, CartViewModelFactory())[CartViewModel::class.java]
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, layoutId)
mContext = this
viewModel.cartItemList.observe(this){
cartCnt.text = it.size.toString()
}
supportActionBar?.let {
setCustomActionBar()
}
}
open fun setCustomActionBar() {
val defActionBar = supportActionBar!!
defActionBar.elevation = 0F
defActionBar.displayOptions = ActionBar.DISPLAY_SHOW_CUSTOM
defActionBar.setCustomView(R.layout.custom_action_bar)
val toolbar = defActionBar.customView.parent as Toolbar
toolbar.setContentInsetsAbsolute(0, 0)
cartCnt = defActionBar.customView.findViewById(R.id.cartCnt)
}
}
In BaseActivity, the text of TextView called cartCnt (the number of products currently in the shopping cart) is observed from MutableLiveData in the CartView Model.
Is as follows : cartviewmodel
// CartViewModel
class CartViewModel() : ViewModel() {
private val list = mutableListOf<Cart>()
private val _cartItemList: MutableLiveData<List<Cart>> = MutableLiveData()
val cartItemList: LiveData<List<Cart>> get() = _cartItemList
private val repository by lazy {
CartRepository.getInstance()
}
init {
getAllCartItems()
}
fun getAllCartItems() {
viewModelScope.launch {
repository!!.getRequestMyCartList {
if (it is Result.Success) {
list.addAll(it.data.data!!.carts)
_cartItemList.value = list
}
}
}
}
fun addToCartItem(id: Int) {
viewModelScope.launch {
repository!!.postRequestAddCart(id) {
if (it is Result.Success) {
list.add(it.data.data!!.cart)
_cartItemList.value = list
}
}
}
}
}
The observer of the View Model existed only in SplashActivity, which first inherited BaseActivity. (verified as a function hasObservers.).
When I clicked on the shopping basket button on the product list page, I communicated with the server and confirmed that the shopping basket data was normally put in the server table, and I also confirmed that the 200 status code was returned normally.
However, when Fragment, which has a product list page, declared cartViewModel and called the addToCartItem function, there was no observer attached to the cartViewModel. This is the part confirmed through the hasObservers function.
The view structure roughly has MainActivity inherited from BaseActivity, and TodayFragment exists in MainActivity.
And, TodayFragment's code is as follows.
// TodayFragment
class TodayFragment : BaseFragment<FragmentTodayBinding>() {
override val layoutId: Int = R.layout.fragment_today
private lateinit var bannerViewPager: BannerRecyclerviewAdapter
private lateinit var productAdapter: ProductHorizonRecyclerviewAdapter
private val cartViewModel by lazy {
ViewModelProvider(this, CartViewModelFactory())[CartViewModel::class.java]
}
override fun init() {
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
initProductRecyclerview()
setValues()
}
override fun setValues() {
HomeViewModel.currentPosition.observe(viewLifecycleOwner) {
binding.bannerViewpager.currentItem = it
}
}
private fun initProductRecyclerview(){
binding.productRecyclerView.apply {
productAdapter = ProductHorizonRecyclerviewAdapter(){
cartViewModel.addToCartItem(it.id)
}
adapter = productAdapter
layoutManager = LinearLayoutManager(requireContext(), LinearLayoutManager.HORIZONTAL, false)
}
}
}
In other words, when the cartViewModel's addToCartItem function is called through the product list page in TodayFragment, the mutableLiveData of the cartViewModel changes, and the cartCnt TextView of BaseActivity is observing this change.
In this situation, I wonder why the first SplashActivity, which appears in the activity stack structure, has observer, and then disappears in the Today Fragment.
Somebody help me.

You are recreating cartViewModel in TodayFragment by passing it a factory which is why it doesn't have the BaseActivity observer. Try this from within TodayFragment
private val cartViewModel: CartViewModel by activityViewModels()
or
private val cartViewModel by lazy {
ViewModelProvider(requireActivity())[CartViewModel::class.java]
}
Then if you call cartViewModel.addToCartItem() in TodayFragment it should call the observer in BaseActivity.

Related

How to get data from fragment to activity? [duplicate]

This question already has answers here:
Passing data between a fragment and its container activity
(16 answers)
Closed 1 year ago.
How to correctly transfer data from a fragment to an activity? I do as follows:
1- Create an interface
interface IProfileToActivity {
fun profileInfo(data: AllHeroes.Global)
}
2- Then I inheritance in the activity
class ProfileActivity : AppCompatActivity(), IProfileToActivity {
private lateinit var myBinding: ActivityProfileBinding
override fun profileInfo(data: AllHeroes.Global) {
myBinding.tvUsername.text = data.name
myBinding.tvDivision.text = data.rank.rankName
Log.i("Apex Info 3", data.toString())
}
}
3- sending from a fragment
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
(activity as? IProfileToActivity)?.profileInfo(allInfoApexResponse.global)
mHeroesAdapter.heroesList(allAdapterListHero)
}
but nothing happens, why? what did I do wrong?
You need not create an interface here. You can use requireActivity() to get a reference to the parent activity. Using it you can access public fields and functions of you activity.
class ProfileActivity : AppCompatActivity() {
private lateinit var myBinding: ActivityProfileBinding
fun profileInfo(data: AllHeroes.Global) {
myBinding.tvUsername.text = data.name
myBinding.tvDivision.text = data.rank.rankName
Log.i("Apex Info 3", data.toString())
}
}
And in your fragment:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
(requireActivity as ProfileToActivity).profileInfo(allInfoApexResponse.global)
mHeroesAdapter.heroesList(allAdapterListHero)
}
There are many ways to pass data from fragment to activity:
Using shared ViewModel.
A ViewModel is used to manage and store UI related data in a
lifecycle conscious way.
~Read more
class SharedViewModel: ViewModel() {
private val currItems: MutableLiveData<List<Item>> =
MutableLiveData<List<Item>>(listOf())
fun getCurrItem(): LiveData<List<Item>> {
return currItems
}
fun sendCurrItems(items: MutableList<Item>) {
currItems.postValue(items)
}
}
class ItemFragment: Fragment() {
private val sharedModel: SharedViewModel by activityViewModels()
}
MainActivity: AppCompactActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val model = ViewModelProvider(this).get(SharedViewModel::class.java)
}
}
In the above class, data is being stored and updated using an MutableList. Thing to be noted here is, the above class is a singleton class, i.e. once it is created, it gets destroyed only when the activity is ended.
Let us assume that an item has to be shared from a ItemFragment to the MainActivity
One callback has to be implemented the MainActivity. For that, one can use an Interface
interface ItemListener{
fun sendItem(item : MutableList<Item>)
}
ItemFragment:
class ItemFragment: Fragment() {
override fun sendItems(items: MutableList<Item>?) {
// Send an Item from here as well as update it
}
// Or just simply call sendItem method.
}
MainActivity:
class MainActivity: AppCompactActivity(){
fun receiveItem(context : Context){
private var mCallback: ItemListener? = null
mCallback = context
}
}

Remove Current Fragment and Launch Another Fragment from View Model

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.

When to call livedata observer?

On a button click i have to get some value from API call and then launch one screen. I have two options:
Call the observer each time when user will click on button.
Call the observer on fragment onActivityCreated() and store the value in variable and act accordingly on button click.
So which approach I should follow?
Actually it's up to you. But i always prefer to call it in Activity's onCreate() function, so activity only has 1 observer. If you call it in button click, it will give you multiple observers as much as button clicking
Here is some example :
class HomeProfileActivity: BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
initObserver()
initView()
}
private fun initObserver() {
viewModel.profileWorkProccess.observe(this, {
swipeRefreshLayout.isRefreshing = it
})
viewModel.isLoadingJobs.observe(this, {
layoutProgressBarJobs.visibility = View.VISIBLE
recyclerViewJobs.visibility = View.GONE
dotsJobs.visibility = View.GONE
})
//other viewmodel observing ......
}
private fun initView() {
imageProfile.loadUrl(user.image, R.drawable.ic_user)
textName.text = identity.user?.fullName
textAddress.text = identity.user?.city
buttonGetData.setOnClickListener { viewModel.getData(this) }
}
}
If the button is placed on the Activity, and data is displayed in the Fragment, you need to store variable in Activity ViewModel and observe it in Fragment
You only need to call observe one time when fragment is created.
For example:
class MyActivity : AppCompatActivity() {
val viewModel: MyActViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
myButton.setOnClickListener { view ->
viewModel.getData()
}
}
}
class MyActViewModel: ViewModel {
val data: LiveData<String> = MutableLiveData()
fun getData() {}
}
class MyFragment: Fragment {
val actViewModel: MyActViewModel by activityViewModels()
override fun onActivityCreated(...) {
....
actViewModel.data.observe(viewLifecycleOwner, Observer { data ->
...
}
}
}

Android: Variable gets uninitialized in ViewModel after being initialized in the Fragment

I have a callback method in my fragment which gets called from it's ViewModel. It initializes the variable in the OnCreateView() method of the fragment, but when the ViewModel calls it to use it, its null.
I am thinking that it has something to do with maybe the VM getting recreated somehow? I just can't seem to figure it out.
I am following this answer's of how the VM drives the UI. They provide Google's sample of a callback interface being created (TasksNavigator.java), Overriding the method in the View (TasksActivity.java), and then calling that method from the VM (TasksViewModel.java) but it doesn't seem to work for me.
Fragment
class SearchMovieFragment : Fragment(), SearchNavigator {
companion object {
fun newInstance() = SearchMovieFragment()
}
private lateinit var searchMovieFragmentViewModel: SearchMovieFragmentViewModel
private lateinit var binding: SearchMovieFragmentBinding
private lateinit var movieRecyclerView: RecyclerView
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
searchMovieFragmentViewModel = ViewModelProvider(this).get(SearchMovieFragmentViewModel::class.java)
binding = DataBindingUtil.inflate(inflater, R.layout.search_movie_fragment, container, false)
binding.viewmodel = searchMovieFragmentViewModel
searchMovieFragmentViewModel.setNavigator(this)
setUpRecyclerView(container!!.context)
return binding.root
}
private fun setUpRecyclerView(context: Context) {
movieRecyclerView = binding.searchMovieFragmentRecyclerView.apply {
this.layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
}
val adapter = MovieListAdapter()
binding.searchMovieFragmentRecyclerView.adapter = adapter
searchMovieFragmentViewModel.movieList.observe(viewLifecycleOwner, Observer {
adapter.submitList(it)
})
}
override fun openDetails() {
Log.d("TEST", "opening details")
}
}
ViewModel
class SearchMovieFragmentViewModel : ViewModel(), MovieSearchItemViewModel {
private lateinit var searchNavigator: SearchNavigator
val editTextContent = MutableLiveData<String>()
var movieList = Repository.getMovieList("batman")
fun setNavigator(_searchNavigator: SearchNavigator) {
this.searchNavigator = _searchNavigator
if (searchNavigator != null) {
Log.d("TEST", "its not null $searchNavigator") // Here it is not null
}
}
private fun getMovieDetail(movieId: String) {
val movie = Repository.getMovieDetail(movieId)
Log.d("TEST", "checking ${this.searchNavigator}") // Here is where I call it but it is null
// searchNavigator.openDetails()
}
private fun getMovieList(movieSearch: String): MutableLiveData<List<Movie>> = Repository.getMovieList(movieSearch)
override fun displayMovieDetailsButton(movieId: String) {
Log.d("TEST", "button clicked $movieId")
getMovieDetail(movieId)
}
}
CallBack Interface
interface SearchNavigator {
fun openDetails()
}
Initiate ViewModel in below method of fragment
override onActivityCreated(#Nullable final Bundle savedInstanceState){
searchMovieFragmentViewModel = ViewModelProvider(this).get(SearchMovieFragmentViewModel::class.java)
}
I will recommend use live data to create connection between ViewModel and and Fragment it will be safer and correct approach.
Trigger openDetails based on the trigger's from your live data.It's forbidden to send your view(context) instance to ViewModel even if you wrap it as there is high probability of memory leaks.
But if you still want to follow this approach then you should Register and unregister fragment instance in your ViewModel (keep a list of SearchNavigator) it onStop() and onStart() .
and loop through them to call openDetails

Share data between viewmodels

I have an activity containing an associated view model. The view model loads the data from an online repository. In the same activity I have a table layout containing a viewpager. In the view page are several fragments.
How can I access the activity view model in my fragments?
you can share viewmodel between fragments like this
class MasterFragment : Fragment() {
private lateinit var itemSelector: Selector
private lateinit var model: SharedViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
model = activity?.run {
ViewModelProviders.of(this).get(SharedViewModel::class.java)
} ?: throw Exception("Invalid Activity")
itemSelector.setOnClickListener { item ->
// Update the UI
}
}
}
class DetailFragment : Fragment() {
private lateinit var model: SharedViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
model = activity?.run {
ViewModelProviders.of(this).get(SharedViewModel::class.java)
} ?: throw Exception("Invalid Activity")
model.selected.observe(this, Observer<Item> { item ->
// Update the UI
})
}
}
https://developer.android.com/topic/libraries/architecture/viewmodel
or just make your viewmodel in your activity public then call it in your fragment like:
(activity as MainActivity).viewmodel // MainActivity is your activity

Categories

Resources