Using a PagerSlidingTabStrip with a Viewpager inside of a fragment:
The main Activity contains a "main fragment" that changes depending on what item you click in navigation drawer.
When loading the initial fragment containing the Viewpager everything shows up fine (all pages are populated).
Replacing that main fragment with another one and then going back to the viewpager fragment turns every page in the viewpager blank, but the PagerSlidingTabStrip tabs are still there.
Any ideas?
I had a problem like that
try this
mPager.setAdapter(new BasePagerAdapter(getChildFragmentManager(), getResources()));
you probably have this
mPager.setAdapter(new BasePagerAdapter(getFragmentManager(), getResources()));
EDIT:
and in your BasePagerAdapter extend FragmentStatePagerAdapter
public class BasePagerAdapter extends FragmentStatePagerAdapter {
Write ur code ie you are using for setting up your pager adapter inside
#Override
public void onResume() {
// TODO Auto-generated method stub
super.onResume();
pager.setAdapter(adapter);
}
Replacing getFragmentManager() with getChildFragmentManager() helped me.
Sorry to reply on an old post, but writing this because non of Stackoverflow solutions for my particular problem helped me.
If you are using new architecture components view model with a master detail shared view model and after returning from detail fragment get blank view pager, do the view model initialization in onViewCreated method of master fragment and not in onCreate (only needed in master fragment).
Also as other answers say remember to use childFragmentManager in view pager adapter.
like this:
class SharedViewModel : ViewModel() {
val selected = MutableLiveData<Item>()
fun select(item: Item) {
selected.value = item
}
}
class MasterFragment : Fragment() {
private lateinit var itemSelector: Selector
private lateinit var model: SharedViewModel
// In the master fragment do the view model initialization in onViewCreated
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
model = activity?.run {
ViewModelProviders.of(this).get(SharedViewModel::class.java)
} ?: throw Exception("Invalid Activity")
model.selected.observe(this, Observer<Item> { 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
})
}
}
Related
I have recyclerView and after click of card I would like to replace fragments in activity. The problem is I have no access to activity. Here is my code in adapter:
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val itemsViewModel = mList[position]
holder.tagImage.setImageResource(itemsViewModel.tagImage)
holder.tagName.text = itemsViewModel.tagName
holder.tagDescription.text = itemsViewModel.tagDescription
holder.itemView.setOnClickListener {
Log.d(InTorry.TAG, itemsViewModel.tagName)
val fragment = ProductsFragment()
val transaction = activity?.supportFragmentManager?.beginTransaction()
transaction?.replace(R.id.homeFragmentsContainer, fragment)
//transaction?.disallowAddToBackStack()
transaction?.commit()
}
}
The above replace code works in fragment but in adapter there is "activity?" error.
Kind Regards
Jack
There are multiple ways to solve this problem.
Using Context
You can use the context from holder.itemView and cast it into an Activity.
This is probably the simplest way, however this can be problematic since a Context may represent an Activity, a Service, an Application, etc. in which case it may lead to a ClassCastException when used simply.
Using Callback
You can set up a callback from your Adapter to your Activity or Fragment and then replace your Fragment.
Use JetPack Navigation
This is my personal favorite as the latest versions allow you to access NavController from Activity, Fragment or any View in the hierarchy to navigate. This is just one of many benefits of using this library.
Here is a link to Jetpack Navigation.
The Simplest and Safer way to solve this is my using Callback from your holder to activity. Below is the step by step process :
Decalare an Interface
interface OnItemClick {
fun onClick()
}
Implement that interface in you Activity and put the desired code
class MainActivity : OnItemClick {
...
override onClick() {
// Do whatever you want
val fragment = ProductsFragment()
val transaction = fragmentManager.beginTransaction()
transaction.replace(R.id.homeFragmentsContainer, fragment)
transaction.commit()
}
}
Create a variable of that Interface type in you Adapter and in your onBindViewHolder method invoke that interface
class MyAdapter(val listener : OnItemClick) {
...
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
...
listener.onClick()
}
Finally pass that interface to your Adapter from you Activity
class MainActivity : OnItemClick {
val adapter = MyAdapter(this)
...
}
NOTE : Please don't pass activity to context here and there you will get unexpected result and most probably a crash.
After watching this video https://www.youtube.com/watch?v=WqrpcWXBz14
I managed to do it this way
In Adapter
class TagsAdapter(var mList: List<TagsViewModel>) :
RecyclerView.Adapter<TagsAdapter.ViewHolder>() {
var onItemClick: ((TagsViewModel) -> Unit)? = null//click listener STEP 1!!!
override fun onBindViewHolder(holder: TagsAdapter.ViewHolder, position: Int) {
holder.itemView.setOnClickListener {
onItemClick?.invoke(itemsViewModel)//click listener STEP 2!!!
}
}
}
And in Fragment
class TagsFragment : Fragment() {
private lateinit var tagsRecyclerView: RecyclerView
private var tagsArray = ArrayList<TagsViewModel>()
private lateinit var adapter: TagsAdapter
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
adapter = TagsAdapter(tagsArray)
tagsRecyclerView.adapter = adapter
adapter.onItemClick = {//click listener STEP 3!!!
val fragment = ProductsFragment()
val transaction = activity?.supportFragmentManager?.beginTransaction()
transaction?.replace(R.id.homeFragmentsContainer, fragment)
//transaction?.disallowAddToBackStack()
transaction?.commit()
}
}
}
It looks very clean and easy. I don't know is a correct way but it works
What i have: Application with BottomNavigation using Android Navigation Component. One of fragments - fragment with other two fragments with recyclerviews in viewPager2. When i navigating to another tab and go back, recyclerview is fully redrawing content
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
binding.schedulePager.apply {
isSaveFromParentEnabled = true
offscreenPageLimit = 2
adapter = ScheduleViewPager(childFragmentManager, lifecycle)
setPageTransformer(ZoomOutPageTransformer())
}
... other stuff
}
class ScheduleViewPager(
fragmentManager: FragmentManager,
lifecycle: Lifecycle
) : FragmentStateAdapter(fragmentManager,lifecycle) {
override fun getItemCount(): Int = 2
override fun createFragment(position: Int): Fragment = PageFragment().apply { arguments = SchedulePageFragmentArgs.bundle(position) }
}
if i'm using FragmentActivity instead of FragmentManager, content in fragments is not redrawing, but i need to use FragmentManager fof access ViewModel from fragment with viewPager
class PageFragment : BaseFragment<FragmentPageBinding>(), SharedPreferences.OnSharedPreferenceChangeListener {
private val viewModel: ScheduleViewModel by viewModels({requireParentFragment()})
I have one MainActivity with -> FragmentA which contains -> ViewPager with (Fragment1,Fragment2,Fragment3)
Now in FragmentA I have one spinner and any selection must reflect the changes inside viewpager's currently visible fragment.
How can I achieve that? I don't want to follow ViewModel or EventBus approach for now as I am working on very old project. I want to use interface to communicate between them.
Create an interface inside your FragmentA
interface OnSpinnerValue{
fun onSpinnerValueChanged()
}
Create a WeakReference for the current selected fragment
private var _currentPage: WeakReference<OnSpinnerValue>? = null
private val currentPage
get() = _currentPage?.get()
fun setCurrentPage(page: OnSpinnerValue) {
_currentPage = WeakReference(page)
}
Now implement this interface in every child fragment of ViewPager
class Fragment1() : Fragment(), OnAddEvent {
override fun onSpinnerValueChanged() {
// implement your method
}
}
And, update currentPage value of the FragmentA, according to the selected fragment, and update it in onResume() of each child fragment
override fun onResume() {
super.onResume()
(parentFragment as FragmentA).setCurrentPage(this)
}
Now, trigger onSpinnerValueChanged from your spinner's onItemSelected methods
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
currentPage?.onSpinnerValueChanged()
}
It is strange if you don't want to use ViewModel ,
I know may be it is not best solutions but you can :
you can create function itIsYourFunctionToUpdate() inside your fragment and update him on FragmentA() , calling function with her object like fragmentB.itIsYourFunctionToUpdate()
or
if (ViewPager.getCurrentItem() == 0 && page != null) {
((FragmentClass1)page).itIsYourFunctionToUpdate("new item");
}
Also you can update viewPager like this mAdapter.notifyDataSetChanged() and all fragment inside viewPager must be updated
The problem: I'm facing a problem with fragments and shared viewmodel LiveData ... The problem that is there is a FragmentA that update data in shared viewmodel and observe for it's changes then display the result for the user inside FragmentA, FragmentA can launch new instance of FragmentA to get new data and display it so the fragment launch it self and old instance gets added to the back stack, until here nothing is wrong the new instance updates LiveData in viewModel and displays the new data perfectly the problem that is when i popUpBackstack() return to FragmentA old instance the data displays in it is the data that new FragmentA instance gets it which means that old FragmentA instance still observing the data even if i remove the observers ... this is general overview about the problem now i will show you fragments structure, the code and what the solution's that i'v tried.
Expected behavior: what i want to achieve is that when FragmentA launch it self and gets added to back stack stop observing the data in viewModel and when i back to it displays the old data that's all.
Fragment structure: i use one activity to hold all the fragments ... MainActivity have FindMoviesFragment inside it there is a viewPager which holds FragmentMovies which launches MovieDetailsFragment and inside it there is ViewPager also which holds the fragments that displays the data from MoviesViewModel it will get clear when you see the code below.
This code shows how MovieDetailsFragment initialize MoviesViewModel and updates data in viewmodel:
class MovieDetailsFragment:Fragment(R.layout.movie_details_fragment) {
private val args: MovieDetailsFragmentArgs by navArgs()
private val fragmentList:ArrayList<Fragment> by lazy {
arrayListOf(MovieDetailsOneFragment(args.movieId), MovieDetailsTwoFragment(),MovieDetailsThreeFragment())
}
private lateinit var pagerAdapter: ViewPagerAdapter
private lateinit var moviesViewModel: MoviesViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
moviesViewModel = ViewModelProvider(requireActivity()).get(MoviesViewModel::class.java)
//these two lines updates the data in viewmodel
moviesViewModel.getMovieDetails(args.movieId)
moviesViewModel.changeMovieID(args.movieId)
}
}
//---------------------MoviesViewModel------------------//
class MoviesViewModel constructor(private val repo:Repository):ViewModel() {
private val _currentMovieDetails = MutableLiveData<Resource<MovieDetails>>()
val currentMovieDetails :LiveData<Resource<MovieDetails>>
get() = _currentMovieDetails
fun getMovieDetails(movieID:Int) = viewModelScope.launch {
_currentMovieDetails.postValue(Resource.loading(null))
val result = repo.getMovieDetails(movieID)
if(result.isSuccessful){
_currentMovieDetails.postValue(Resource.success(result.body()))
}
else{
_currentMovieDetails.postValue(Resource.error(result.errorBody().toString(),null))
}
}
}
And inside MovieDetailsOne which is inside viewpager in MovieDetailsFragment i observe the data like this:
class MovieDetailsOneFragment(private val movieId: Int):Fragment(R.layout.movie_details_one) {
private lateinit var moviesViewModel:MoviesViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
//this is how i define viewModel(global scope)
moviesViewModel = ViewModelProvider(requireActivity()).get(MoviesViewModel::class.java)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
//in this method i observe on changes in currentMovieDetils liveData
subscribeToObservers()
//i try to call this from on create and nothing changes too
}
}
Now What i tried is the following:
-Loacal scope for fragments
//Define viewModel like this in MovieDetilsFragment
moviesViewModel = ViewModelProvider(this).get(MoviesViewModel::class.java)
//And in MovieDetailsOne like this
moviesViewModel = ViewModelProvider(this).get(MoviesViewModel::class.java)
//and this doesn't work MovieDetailsOne don't observe any changes
-Observe once extinction function:
fun <T> LiveData<T>.observeOnce(lifecycleOwner: LifecycleOwner, observer: Observer<T>) {
observe(lifecycleOwner, object : Observer<T> {
override fun onChanged(t: T?) {
observer.onChanged(t)
removeObserver(this)
}
})
}
//and this doesn't work MovieDetailsOne don't observe first time it's lanches
I know i took so long to explain :D but I'm trying to give you clear idea and sorry for you time <3 ... if you want any additional information about the code or the problem comment down below.
Finally i achieve the behavior that i want by doing some steps which is:
Attach ViewPager childs using childFragmentManager which means that android will treat viewpager fragments as childs for viewPager holder:
//instead of doing this
pagerAdapter = ViewPagerAdapter(activity?.supportragmentManager, lifecycle, fragmentList)
//do this
pagerAdapter = ViewPagerAdapter(childFragmentManager, lifecycle, fragmentList)
Define local viewModel for parent fragment like this:
//instead of doing this
moviesViewModel = ViewModelProvider(requireActivity()).get(MoviesViewModel::class.java)
//do this
moviesViewModel = ViewModelProvider(this).get(MoviesViewModel::class.java)
Define parent fragment scope viewModel in childs fragment:
//instead of doing this
moviesViewModel = ViewModelProvider(requireActivity()).get(MoviesViewModel::class.java)
//do this
private val moviesViewModel:MoviesViewModel by viewModels(
{requireParentFragment()}
)
by doing these steps parent fragment will stop observing data when it's destroyed and child fragments also.
This question already has answers here:
Get current fragment with ViewPager2
(18 answers)
Closed 1 year ago.
i used this ViewPager2 dependency:
implementation 'androidx.viewpager2:viewpager2:1.0.0'
then i wrote code and make Adapter for FragmentStateAdapter like below:
private inner class ViewPagerAdapter(fragmentManager: FragmentManager,
lifecycle: Lifecycle, config: TribunConfig?)
: FragmentStateAdapter(fragmentManager, lifecycle) {
private val config: TribunConfig?
override fun createFragment(position: Int): Fragment {
return TribunNewsFragment.newInstance(if (menuTribun!!.get(position) == "News") "home"
else menuTribun!!.get(position)!!.replace(" ", "").toLowerCase(), config)!!
}
override fun getItemCount(): Int {
return menuTribun!!.size
}
init {
this.config = config
}
}
an in method onCreate i implement some code related viewpager2 and its adapter like below:
viewPager2!!.offscreenPageLimit = 1
viewPager2!!.adapter = ViewPagerAdapter(supportFragmentManager, lifecycle, config)
TabLayoutMediator(tabLayout!!, viewPager2!!, TabLayoutMediator.TabConfigurationStrategy { tab: TabLayout.Tab?, position: Int -> tab!!.text = menuTribun!!.get(position) }).attach()
But i have problem when i want to make swipeRefresh. My swipeRefresh needs current fragment to access the method inside fragment. But there is NO METHOD getItem in FragmentStateAdapter. Anyone can help me?
Create property in your adapter to save all your fragments
Save current position
Get fragment from fragment list (property) by position when you need it
Something like this:
val fragments: MutableList<Fragment> = mutableListOf()
private val config: TribunConfig?
override fun createFragment(position: Int): Fragment {
val fragment = TribunNewsFragment.newInstance(if (menuTribun!!.get(position) == "News") "home"
else menuTribun!!.get(position)!!.replace(" ", "").toLowerCase(), config)!!
fragments.add(fragment)
return fragment
}
Parent:
yourTabLayout.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
override fun onTabSelected(tab: TabLayout.Tab?) {
val fragment = adapter.fragments[tab!!.position]
// cast fragment to your fragment class and do what you want
}
override fun onTabUnselected(tab: TabLayout.Tab?) {
}
override fun onTabReselected(tab: TabLayout.Tab?) {
}
})
Also you can use when or if-else for your fragments position or create base class with methods you need and cast getting fragment to it.
Following #Oleg's answer, you can access the Fragments like this in your Adapter instead of creating a local list and adding each fragment on createFragment method:
private val fragmentList: List<TribunNewsFragment>
get() {
return fragment.childFragmentManager.fragments.filterIsInstance<TribunNewsFragment>()
}
It's important to use childFragmentManager instead of fragmentManager if you are creating the ViewPager inside another fragment.
And it survives configuration changes, regarding #lawonga's comment on the answer's topic.