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)
}
}
}
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.
Hey I am working in kotlin flow in android. I noticed that my kotlin flow collectLatest is calling twice and sometimes even more. I tried this answer but it didn't work for me. I printed the log inside my collectLatest function it print the log. I am adding the code
MainActivity.kt
class MainActivity : AppCompatActivity(), CustomManager {
private val viewModel by viewModels<ActivityViewModel>()
private lateinit var binding: ActivityMainBinding
private var time = 0
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
setupView()
}
private fun setupView() {
viewModel.fetchData()
lifecycleScope.launchWhenStarted {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.conversationMutableStateFlow.collectLatest { data ->
Log.e("time", "${time++}")
....
}
}
}
}
}
ActivityViewModel.kt
class ActivityViewModel(app: Application) : AndroidViewModel(app) {
var conversationMutableStateFlow = MutableStateFlow<List<ConversationDate>>(emptyList())
fun fetchData() {
viewModelScope.launch {
val response = ApiInterface.create().getResponse()
conversationMutableStateFlow.value = response.items
}
}
.....
}
I don't understand why this is calling two times. I am attaching logs
2022-01-17 22:02:15.369 8248-8248/com.example.fragmentexample E/time: 0
2022-01-17 22:02:15.629 8248-8248/com.example.fragmentexample E/time: 1
As you can see it call two times. But I load more data than it call more than twice. I don't understand why it is calling more than once. Can someone please guide me what I am doing wrong. If you need whole code, I am adding my project link.
You are using a MutableStateFlow which derives from StateFlow, StateFlow has initial value, you are specifying it as an emptyList:
var conversationMutableStateFlow = MutableStateFlow<List<String>>(emptyList())
So the first time you get data in collectLatest block, it is an empty list. The second time it is a list from the response.
When you call collectLatest the conversationMutableStateFlow has only initial value, which is an empty list, that's why you are receiving it first.
You can change your StateFlow to SharedFlow, it doesn't have an initial value, so you will get only one call in collectLatest block. In ActivityViewModel class:
var conversationMutableStateFlow = MutableSharedFlow<List<String>>()
fun fetchData() {
viewModelScope.launch {
val response = ApiInterface.create().getResponse()
conversationMutableStateFlow.emit(response.items)
}
}
Or if you want to stick to StateFlow you can filter your data:
viewModel.conversationMutableStateFlow.filter { data ->
data.isNotEmpty()
}.collectLatest { data ->
// ...
}
The reason is collectLatest like backpressure. If you pass multiple items at once, flow will collect latest only, but if there are some time between emits, flow will collect each like latest
EDITED:
You really need read about MVVM architecture.
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
setupView()
}
private fun setupView() {
if (supportFragmentManager.findFragmentById(R.id.fragmentView) != null)
return
supportFragmentManager
.beginTransaction()
.add(R.id.fragmentView, ConversationFragment())
.commit()
}
}
Delele ActivityViewModel and add that logic to FragmentViewModel.
Also notice you don't need use AndroidViewModel, if you can use plain ViewModel. Use AndroidViewModel only when you need access to Application or its Context
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 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.
I am using Bottom Navigation with Navigation Architecture Component. When the user navigates from one item to another(via Bottom navigation) and back again view model call repository function to fetch data again. So if the user goes back and forth 10 times the same data will be fetched 10 times. How to avoid re-fetching when the fragment is recreated data is already there?.
Fragment
class HomeFragment : Fragment() {
#Inject
lateinit var viewModelFactory: ViewModelProvider.Factory
private lateinit var productsViewModel: ProductsViewModel
private lateinit var productsAdapter: ProductsAdapter
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
// Inflate the layout for this fragment
return inflater.inflate(R.layout.fragment_home, container, false)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
initViewModel()
initAdapters()
initLayouts()
getData()
}
private fun initViewModel() {
(activity!!.application as App).component.inject(this)
productsViewModel = activity?.run {
ViewModelProviders.of(this, viewModelFactory).get(ProductsViewModel::class.java)
}!!
}
private fun initAdapters() {
productsAdapter = ProductsAdapter(this.context!!, From.HOME_FRAGMENT)
}
private fun initLayouts() {
productsRecyclerView.layoutManager = LinearLayoutManager(this.activity)
productsRecyclerView.adapter = productsAdapter
}
private fun getData() {
val productsFilters = ProductsFilters.builder().sortBy(SortProductsBy.NEWEST).build()
//Products filters
productsViewModel.setInput(productsFilters, 2)
//Observing products data
productsViewModel.products.observe(viewLifecycleOwner, Observer {
it.products()?.let { products -> productsAdapter.setData(products) }
})
//Observing loading
productsViewModel.networkState.observe(viewLifecycleOwner, Observer {
//Todo showing progress bar
})
}
}
ViewModel
class ProductsViewModel
#Inject constructor(private val repository: ProductsRepository) : ViewModel() {
private val _input = MutableLiveData<PInput>()
fun setInput(filters: ProductsFilters, limit: Int) {
_input.value = PInput(filters, limit)
}
private val getProducts = map(_input) {
repository.getProducts(it.filters, it.limit)
}
val products = switchMap(getProducts) { it.data }
val networkState = switchMap(getProducts) { it.networkState }
}
data class PInput(val filters: ProductsFilters, val limit: Int)
Repository
#Singleton
class ProductsRepository #Inject constructor(private val api: ApolloClient) {
val networkState = MutableLiveData<NetworkState>()
fun getProducts(filters: ProductsFilters, limit: Int): ApiResponse<ProductsQuery.Data> {
val products = MutableLiveData<ProductsQuery.Data>()
networkState.postValue(NetworkState.LOADING)
val request = api.query(ProductsQuery
.builder()
.filters(filters)
.limit(limit)
.build())
request.enqueue(object : ApolloCall.Callback<ProductsQuery.Data>() {
override fun onFailure(e: ApolloException) {
networkState.postValue(NetworkState.error(e.localizedMessage))
}
override fun onResponse(response: Response<ProductsQuery.Data>) = when {
response.hasErrors() -> networkState.postValue(NetworkState.error(response.errors()[0].message()))
else -> {
networkState.postValue(NetworkState.LOADED)
products.postValue(response.data())
}
}
})
return ApiResponse(data = products, networkState = networkState)
}
}
Navigation main.xml
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="#+id/mobile_navigation.xml"
app:startDestination="#id/home">
<fragment
android:id="#+id/home"
android:name="com.nux.ui.home.HomeFragment"
android:label="#string/title_home"
tools:layout="#layout/fragment_home"/>
<fragment
android:id="#+id/search"
android:name="com.nux.ui.search.SearchFragment"
android:label="#string/title_search"
tools:layout="#layout/fragment_search" />
<fragment
android:id="#+id/my_profile"
android:name="com.nux.ui.user.MyProfileFragment"
android:label="#string/title_profile"
tools:layout="#layout/fragment_profile" />
</navigation>
ViewModelFactory
#Singleton
class ViewModelFactory #Inject
constructor(private val viewModels: MutableMap<Class<out ViewModel>, Provider<ViewModel>>) : ViewModelProvider.Factory {
#Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T {
val creator = viewModels[modelClass]
?: viewModels.asIterable().firstOrNull { modelClass.isAssignableFrom(it.key) }?.value
?: throw IllegalArgumentException("unknown model class $modelClass")
return try {
creator.get() as T
} catch (e: Exception) {
throw RuntimeException(e)
}
}
}
One simple solution would be to change the ViewModelProvider owner from this to requireActivity() in this line of code:
ViewModelProviders.of(this, viewModelFactory).get(ProductsViewModel::class.java)
Therefore, as the activity is the owner of the viewmodel and the lifcycle of viewmodel attached to the activity not to the fragment, navigating between fragments within the activity won't recreated the viewmodel.
In onActivityCreated(), you are calling getData(). In there, you have:
productsViewModel.setInput(productsFilters, 2)
This, in turn, changes the value of _input in your ProductsViewModel. And, every time that _input changes, the getProducts lambda expression will be evaluated, calling your repository.
So, every onActivityCreated() call triggers a call to your repository.
I do not know enough about your app to tell you what you need to change. Here are some possibilities:
Switch from onActivityCreated() to other lifecycle methods. initViewModel() could be called in onCreate(), while the rest should be in onViewCreated().
Reconsider your getData() implementation. Do you really need to call setInput() every time we navigate to this fragment? Or, should that be part of initViewModel() and done once in onCreate()? Or, since productsFilters does not seem to be tied to the fragment at all, should productsFilters and the setInput() call be part of the init block of ProductsViewModel, so it only happens once?
When you select other pages via bottom navigation and come back, fragment destroy and recreate. So the onCreate, onViewCreated and onActivityCreate
will run again. But viewModel is still alive.
So you can call your function (getProducts) inside the "init" in viewModel to run it once.
init {
getProducts()
}
define your ProductsViewModel by static in mainActivity and initialize in onCreate method.
Now just use it this way in fragment:
MainActivity.productsViewModel.products.observe(viewLifecycleOwner, Observer {
it.products()?.let { products -> productsAdapter.setData(products) }
})