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
Related
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.
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
}
}
I have 2 fragments that i navigate throught navigation component from Jetpack. When i go back from fragment 2 to fragment 1, i fire an event throught livedata from fragment 2 and fragment1 should be observing. Althought, fragment 1 didnt observed and when i sste a breakpoint to check how many observers liveData has, it say 0.
Code below
class Fragment1 :
Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
myViewModel.selectedItem.observe(viewLifecycleOwner, {
println("Observed")
})
findNavController().navigate(
Fragment1Directions.actionToFragment2()
)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
myViewModel = ViewModelProvider(requireActivity()).get(MyViewModel::class.java)
}
private lateinit var myViewModel: MyViewModel
}
class Fragment2 :
Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
myButton.setOnClickListener{
MyViewModel().selectItem(true)
findNavController().navigateUp()
}
}
}
class MyViewModel : ViewModel() {
private val mutableSelectedItem = MutableLiveData<Boolean>()
val selectedItem: LiveData<Boolean> get() = mutableSelectedItem
fun selectItem(value: Boolean) {
mutableSelectedItem.value = value
}
}
It's strange to not have observer for
myViewModel.selectedItem.observe(viewLifecycleOwner, {
println("Observed")
})
when it's called onViewCreated and with viewLifecycleOwner, but it looks like you have a mistake for creating common ViewModel for Fragment1 and Fragment2
Both should use
myViewModel = ViewModelProvider(requireActivity()).get(MyViewModel::class.java) to get same ViewModel for both fragments
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 ->
...
}
}
}
Basically I have a state management system using ViewModel that looks like this:
class ViewModelA: ViewModel() {
private val repository: RepositoryA by inject()
private val _stateLiveData = MutableLiveData<ViewState>()
val stateLiveData: LiveData<ViewState> get() = _stateLiveData
private val _eventLiveData = SingleLiveEvent<ViewEvent>()
val eventLiveData: LiveData<ViewEvent> get() = _eventLiveData
private val exceptionHandler = CoroutineExceptionHandler { _, _ ->
_stateLiveData.postValue(ViewState.Error)
}
fun loadList() {
if (_stateLiveData.value is ViewState.Loading) return
launch(exceptionHandler) {
_stateLiveData.run {
value = ViewState.Loading
value = repository.getDocumentList().let {
if (it.isEmpty()) ViewState.Error
else ViewState.Data(it)
}
}
}
}
}
But whenever I am sharing a ViewModel with several Fragments, it becomes bigger and bigger. I am looking for a solution for this, because I don't want to centralize all the logic for an entire application flow inside a ViewModel and I also don't want to pass arguments here and there all the time.
PS: Sorry about my bad english.
Edit: Clarify a bit the question.
I didn't quite understand your question. However, if your question was as follows:
How can I share the same ViewModel Object and use it inside multiple Fragments.
You can check the documentation of ViewModelProvider which is a utility class that provides ViewModels for a specific scope like Activity.
Following is an example code of the usage of ViewModelProvider within two Fragments that will be created and used in the same Activity object:
// An example ViewModel
class SharedViewModel : ViewModel() {
val intLiveData = MutableLiveData<Int>() // an example LiveData field
}
// the first fragment
class Fragment1 : Fragment() {
private lateinit var viewModel: SharedViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel = requireActivity().let { activity ->
ViewModelProvider(activity).get(SharedViewModel::class.java)
}
}
}
// the other fragment
class Fragment2 : Fragment() {
private lateinit var viewModel: SharedViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel = requireActivity().let { activity ->
ViewModelProvider(activity).get(SharedViewModel::class.java)
}
}
}