17

I am working on a ViewPager with 6 tabs where it has only one fragment TimesListFragment

Depending on the arguments passed to TimesListFragment it calls api eg; science , technology, travel etc

I have followed Google's GithubBrowserSample for my app

I have TimesListFragment -> TimesViewModel -> TimesRepository

There are 6 tabs , when I hit the api all the tabs show the same result which if of the last argument

StoriesPagerAdapter.kt

class StoriesPagerAdapter(fragmentManager: FragmentManager?)
    :FragmentStatePagerAdapter(fragmentManager){
    private val sections= arrayListOf("science","technology","business","world","movies","travel")
    override fun getItem(position: Int): Fragment {
        return TimesListFragment.newInstance(sections[position])
    }
    override fun getCount(): Int {
        return sections.size
    }
    override fun getPageTitle(position: Int): CharSequence? {
        return sections[position]
    }
}

issue : all tabs shows data of travel arguments

TimesViewModel

class TimesViewModel @Inject constructor(private val timesRepository: TimesRepository) :
        ViewModel() {
    lateinit var data: LiveData<Resource<TimesStoriesResponse>>
    fun fetchStories(section:String): LiveData<Resource<TimesStoriesResponse>> {
        data = timesRepository.loadStories(section)
        return data
    }
}

TimesRepository.kt

class TimesRepository @Inject constructor(private val apiService: ApiService,
                                          private val timesDao: TimesDao,
                                          private val appExecutors: AppExecutors) {

    private val repoListRateLimit = RateLimiter<String>(10, TimeUnit.MINUTES)

    fun loadStories(section:String): LiveData<Resource<TimesStoriesResponse>> {
        return object : NetworkBoundResource<TimesStoriesResponse, TimesStoriesResponse>(appExecutors) {
            override fun saveCallResult(item: TimesStoriesResponse) {
                timesDao.insert(item)
            }

            override fun shouldFetch(data: TimesStoriesResponse?): Boolean {
                return data == null  || repoListRateLimit.shouldFetch(section)
            }

            override fun loadFromDb() = timesDao.load()

            override fun createCall() = apiService.getTopStories(section,ConfigConstant.TIMES_KEY)

            override fun onFetchFailed() {
                repoListRateLimit.reset(section)
            }
        }.asLiveData()
    }

ApiService.kt

interface ApiService {
    @GET("svc/topstories/v2/{section}.json?")
    fun getTopStories(@Path ("section") section:String,@Query("api-key") apiKey:String)
            :LiveData<ApiResponse<TimesStoriesResponse>>
}

TimesListFragment.kt

private fun initViewModel() {

        viewModel = ViewModelProviders.of(this, viewModelFactory).get(section,TimesViewModel::class.java)

    }

    private fun initData() {
        viewModel?.fetchStories(section)?.observe(this, Observer {

            when (it?.status) {

                Status.LOADING -> {
                    showLoading(true)
                    showError(false,null)
                }


                Status.SUCCESS -> {
                    showLoading(false)
                    showError(false,null)
                    showSuccessData(it.data)
                }

                Status.ERROR -> {
                    showLoading(false)
                    showError(true,it.message)
                }

            }
        })
    }

note : both methods are called in onViewCreated() of TimesListFragmnet

karthik kolanji
  • 2,044
  • 5
  • 20
  • 56

2 Answers2

36

This should be happening due to your ViewModel.

Typically, there's one ViewModel per Activity or Fragment, due to the way a ViewModel is designed to work. One of the major benefits in using a ViewModel is that it's lifecycle is completely separate from the lifecycle of your Fragment, therefore, your Fragment can be destroyed and recreated multiple times and you'll still be able to restore current data that's stored in your ViewModel.

Therefore, this means that with the typical code to fetch the ViewModel from the ViewModelProviders, you'll be fetching the exact same ViewModel.

Typically, this won't cause a problem, but in your ViewPager, you're reusing the same TimesListFragment which is most likely calling up the same ViewModel, therefore causing each fragment to show the same data.

The solution for this is to use:

ViewModelProviders.of(this).get(KEY, TimesViewModel::class.java)

Note the KEY which is used to differentiate between the ViewModels that needs to be fetched. So by using the positions in the ViewPager as a unique key, you should be able to have a unique ViewModel for each TimesListFragment.

Jackey
  • 3,184
  • 1
  • 11
  • 12
  • When every time I call TimesListFragment its onCreate() is called , this means every time new instance of fragment is created , so new instance of TimesViewModel . Is I am right ? – karthik kolanji Feb 25 '19 at 09:02
  • Nope, the way `ViewModelProviders.of(this).get(TimesViewModel::class.java)` works is that it'll create a new instance of `TimesViewModel` if it doesn't exist. But if it already exists, then it'll return the existing one. Therefore, all 6 `TimesListFragment` would be using the same `ViewModel`. But if you use `ViewModelProviders.of(this).get(KEY, TimesViewModel::class.java)` where you can specify a `KEY`, then you'll be able to create a unique `ViewModel` that belongs to each `TimesListFragment`. – Jackey Feb 25 '19 at 09:08
  • 3
    tried passing tab title(science , technology etc) in KEY to differentiate ViewModel , didn't worked :( . viewModel = ViewModelProviders.of(this, viewModelFactory).get(section,TimesViewModel::class.java) – karthik kolanji Feb 25 '19 at 09:09
  • Any suggestions ? – karthik kolanji Feb 25 '19 at 09:58
  • Please show the parts of the code where you fetch the ViewModel in TimesListFragment. – Jackey Feb 25 '19 at 10:18
  • please see my post – karthik kolanji Feb 25 '19 at 10:27
  • I found the problem . The values are inserted to the database are replaced by the last api data and that is shown in the UI . Now figuring out how to solved this – karthik kolanji Feb 25 '19 at 15:02
  • 2
    not workign for me, I'm having 2 different fragments (which inherits from the same parent) who live inside FragmentPagerAdapter, in each of these fragments I instantiate view model (same viewmodel class but 2 instances). chatRoomViewModel = ViewModelProviders.of(this).get("KEY1", ChatRoomViewModel.class); chatRoomViewModel = ViewModelProviders.of(this).get("KEY2", ChatRoomViewModel.class); even though im changing data in fragment 1, the observer method in fragment2 is called, is there a way toa void it? – Zlil Korman Nov 25 '19 at 15:44
  • @karthikkolanji how did you fix this?? I tried passing the title as the Key and it's not working – hushed_voice Jan 16 '20 at 10:06
  • @ hushed_voice -> it was room issue .. refer my project https://github.com/karthikkolanji/new_york_times – karthik kolanji Jan 17 '20 at 08:44
  • For me this issue is coming if I am using by viewModels({requireParentFragment()}) from ktx, there is no way to pass key. All the fragments are replaced with last fragment – Cyph3rCod3r Oct 13 '20 at 17:01
1

You need to tell the ViewPager not to load pages that are currently not showing. By default it loads at least one page to the left and one to the right, I think.

VladimirVip
  • 384
  • 3
  • 13