Problem:
i have a layout like this:
in the parent fragment i have a searchview responsible for filtering the recyclerviews inside the child fragments.
the child fragments are inside a tablayout viewpager combination.
so the OnQueryTextListener is inside the parent fragment and i have to get an instance of the recyclerviews adapters from child fragments to do the filtering.
what is the proper way to do this?
What I've tried:
i searched and found getChildFramgentManager which returns an instance of the child fragment. but apparently, the fragments should be created dynamically? im not sure how that works with viewpager. so if there is a way to do this with getChildFramgentManager and viewpager please explain.
My code:
everything I've written is the standard procedure and im not that far into the project to add something that changes the outcome so im not gonna take your time by adding the code, but if the code is necessary please say so and i will add it.
In your viewpager's adapter you can store references to child fragments into a list and iterate through child fragments to call a method.
Something like this :
Parent fragment
class ParentFragment : Fragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view = inflater.inflate(R.layout.parent_fragment, container, false)
val search : SearchView = findViewById(R.id.search)
val pager : ViewPager = findViewById(R.id.pager)
val adapter = ViewPagerAdapter(supportFragmentManager)
pager.adapter = adapter
search.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String?): Boolean {
adapter.filter(query ?: "")
return true
}
override fun onQueryTextChange(newText: String?): Boolean {
return true
}
})
return view
}
}
Pager adapter
class ViewPagerAdapter(fm: FragmentManager) : FragmentStatePagerAdapter(fm) {
private var mList : MutableList<ChildFragment> = ArrayList()
init {
mList.add(ChildFragment())
mList.add(ChildFragment())
}
fun filter(text: String) {
mList.forEach {
it.filter(text)
}
}
override fun getItem(position: Int): Fragment {
return mList.get(position)
}
override fun getCount(): Int {
return mList.size
}
}
Child fragment
class ChildFragment : Fragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view = inflater.inflate(R.layout.child_fragment, container, false)
// get reference to recyclerview, set adapter...
return view
}
fun filter(text: String) {
// do job here
}
}
Related
I have a fragment which contains a ViewPager2, It has 3 child fragments, I want to receive an reference viewpager in child fragment to be able to change viewPager programmatically. Since parent fragment which contains viewPager is not activity I can not access the viewPager with findViewById. On another question a usersuggested val pager = container as ViewPager2 but it's returning null. Can someone help me with this. As seen in code snippets, I am trying to get an reference to viewPager in BillingFragment from OneFragment.
Please let me know if you need to see ViewPagerAdapter code snippet.
Child fragment
class OneFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
val view = inflater.inflate(R.layout.fragment_billing, container, false)
val pager = container as ViewPager2
// ^^^^^ page is null
return view;
}
ViewPager main fragment
class BillingFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
val view = inflater.inflate(R.layout.fragment_billing_main, container, false)
setupStepView(view)
setupViewPager(view)
setupButtons(view)
return view
}
private fun setupStepView(view: View) {
view.stepView.state
// You should specify only stepsNumber or steps array of strings.
// In case you specify both steps array is chosen.
.steps(
listOf(
"First Step",
"Second Step",
"Third Step"
)
) // You should specify only steps number or steps array of strings.
// In case you specify both steps array is chosen.
.stepsNumber(3)
.animationDuration(resources.getInteger(android.R.integer.config_shortAnimTime))
.commit()
view.stepView.setOnStepClickListener { position ->
// viewPager.setCurrentItem(position, false)
}
}
private fun setupViewPager(view: View) {
// viewPager = view.viewPager;
view.viewPager.adapter = ViewPagerAdapter(childFragmentManager, lifecycle)
view.viewPager.registerOnPageChangeCallback(
object : ViewPager2.OnPageChangeCallback() {
override fun onPageSelected(position: Int) {
super.onPageSelected(position)
view.stepView.go(position, true)
setButtons(position, view)
}
}
)
}
private fun setupButtons(view: View) {
view.backButton.setOnClickListener {
view.viewPager.setCurrentItem(view.viewPager.currentItem - 1, false)
}
view.nextButton.setOnClickListener {
view.viewPager.setCurrentItem(view.viewPager.currentItem + 1, false)
}
}
private fun setButtons(position: Int, view: View) {
when (position) {
0 -> {
view.backButton.visibility = View.INVISIBLE
view.nextButton.visibility = View.VISIBLE
}
1 -> {
view.backButton.visibility = View.VISIBLE
view.nextButton.visibility = View.VISIBLE
}
2 -> {
view.backButton.visibility = View.VISIBLE
view.nextButton.visibility = View.INVISIBLE
}
}
}
}
Accessing the child fragment's view in parent fragment is not a good idea. If you want to trigger any action from the fragment. You can follow these steps:
class MyViewPagerAdapter(...) : FragmentStateAdapter(...){
lsit : List<Fragment>
fun getItem(position :Int) = list[position]
}
class OneFragment : Fragment(){
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
val view = inflater.inflate(R.layout.fragment_billing, container, false)
val position = viewPager2.currentItem
(adapter.getItem(position) as? BillingFragment)?.
return view;
}
}
class BillingFragment : Fragment(){
override fun onViewCreated(...){
...
}
// pass the data in the parameter that you want to use or update
fun updateViewPager(position : Int){
// perform your actions
view.viewPager.currentItem = position
}
}
Alternatives
If you know about EventBus, then you can use EventBus.
The android's default way is to use BroadcastReceiver.
I have a fragment called MainFragment that contains a ViewPager that contains another fragment called LibraryFragment.
LibraryFragment contains a RecyclerView with a list of items that contain an ImageView. The ImageView's contents are loaded with Coil.
When an item is clicked, LibraryFragment navigates to another fragment called ArtistDetailFragment and uses the ImageView from the item.
The problem is that while the enter transition works fine, the ImageView does not return to the list item when navigating back and only fades away. Ive attached an example below:
Ive tried using postponeEnterTransition() and startPostponedEnterTransition() along with adding a SharedElementCallback but neither have worked that well. I've also ruled out Coil being the issue.
Heres LibraryFragment:
class LibraryFragment : Fragment() {
private val musicModel: MusicViewModel by activityViewModels()
private val libraryModel: LibraryViewModel by activityViewModels()
private lateinit var binding: FragmentLibraryBinding
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = FragmentLibraryBinding.inflate(inflater)
binding.libraryRecycler.adapter = ArtistAdapter(
musicModel.artists.value!!,
BindingClickListener { artist, itemBinding ->
navToArtist(artist, itemBinding)
}
)
return binding.root
}
private fun navToArtist(artist: Artist, itemBinding: ItemArtistBinding) {
// When navigation, pass the artistImage of the item as a shared element to create
// the image popup.
findNavController().navigate(
MainFragmentDirections.actionShowArtist(artist.id),
FragmentNavigatorExtras(
itemBinding.artistImage to itemBinding.artistImage.transitionName
)
)
}
}
Heres ArtistDetailFragment:
class ArtistDetailFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val binding = FragmentArtistDetailBinding.inflate(inflater)
sharedElementEnterTransition = TransitionInflater.from(requireContext())
.inflateTransition(android.R.transition.move)
val musicModel: MusicViewModel by activityViewModels()
val artistId = ArtistDetailFragmentArgs.fromBundle(requireArguments()).artistId
// Get the transition name used by the recyclerview ite
binding.artistImage.transitionName = artistId.toString()
binding.artist = musicModel.artists.value?.find { it.id == artistId }
return binding.root
}
}
And heres the RecyclerView Adapter/ViewHolder:
class ArtistAdapter(
private val data: List<Artist>,
private val listener: BindingClickListener<Artist, ItemArtistBinding>
) : RecyclerView.Adapter<ArtistAdapter.ViewHolder>() {
override fun getItemCount(): Int = data.size
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder(
ItemArtistBinding.inflate(LayoutInflater.from(parent.context))
)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(data[position])
}
// Generic ViewHolder for an artist
inner class ViewHolder(
private val binding: ItemArtistBinding
) : RecyclerView.ViewHolder(binding.root) {
// Bind the view w/new data
fun bind(artist: Artist) {
binding.artist = artist
binding.root.setOnClickListener { listener.onClick(artist, binding) }
// Update the transition name with the new artist's ID.
binding.artistImage.transitionName = artist.id.toString()
binding.executePendingBindings()
}
}
}
EDIT: I used postponeEnterTransition and startPostponedEnterTransition like this:
// LibraryFragment
override fun onResume() {
super.onResume()
postponeEnterTransition()
// Refresh the parent adapter to make the image reappear
binding.libraryRecycler.adapter = artistAdapter
// Do the Pre-Draw listener
binding.libraryRecycler.viewTreeObserver.addOnPreDrawListener {
startPostponedEnterTransition()
true
}
}
This only makes the RecyclerView itself refresh however, the shared element still fades away instead of returning to the RecyclerView item.
Chris Banes says;
You may wonder why we set the OnPreDrawListener on the parent rather than the view itself. Well that is because your view may not actually be drawn, therefore the listener would never fire and the transaction would sit there postponed forever. To work around that we set the listener on the parent instead, which will (probably) be drawn.
https://chris.banes.dev/fragmented-transitions/
try this;
change
// LibraryFragment
override fun onResume() {
super.onResume()
postponeEnterTransition()
// Refresh the parent adapter to make the image reappear
binding.libraryRecycler.adapter = artistAdapter
// Do the Pre-Draw listener
binding.libraryRecycler.viewTreeObserver.addOnPreDrawListener {
startPostponedEnterTransition()
true
}
}
enter code here
to
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
postponeEnterTransition()
binding = FragmentLibraryBinding.inflate(inflater)
binding.libraryRecycler.adapter = ArtistAdapter(
musicModel.artists.value!!,
BindingClickListener { artist, itemBinding ->
navToArtist(artist, itemBinding)
}
)
(view?.parent as? ViewGroup)?.doOnPreDraw {
// Parent has been drawn. Start transitioning!
startPostponedEnterTransition()
}
return binding.root
}
I use view-pager2 and When I remove the first item of view-pager, It does not delete and still remains but another item removed.
For example, here I want to remove the first item of view pager but it's not be removed:
My Main Activity:
class MainActivity : AppCompatActivity() {
private lateinit var myPagerAdapter: MyPagerAdapter
private val mFragmentList: ArrayList<FragmentOne> = ArrayList()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
if (mFragmentList.isEmpty()) {
mFragmentList.add(FragmentOne.newInstance(1))
mFragmentList.add(FragmentOne.newInstance(2))
mFragmentList.add(FragmentOne.newInstance(3))
}
myPagerAdapter = MyPagerAdapter(supportFragmentManager, lifecycle)
view_pager.adapter = myPagerAdapter
btn_show.setOnClickListener {
val dialog = Dialog(this)
dialog.setContentView(R.layout.fragment_dialog)
dialog.btn_remove.setOnClickListener {
//go to next item
view_pager.currentItem = 1
//remove first item
mFragmentList.removeAt(0)
//reload adapter
myPagerAdapter.notifyDataSetChanged()
}
dialog.show()
}
}
private inner class MyPagerAdapter(fragmentManager: FragmentManager, lifecycle: Lifecycle) :
FragmentStateAdapter(fragmentManager, lifecycle) {
override fun getItemCount(): Int {
return mFragmentList.size
}
override fun createFragment(position: Int): Fragment {
return mFragmentList[position]
}
}
}
FragmentOne:
class FragmentOne : Fragment() {
companion object {
fun newInstance(position: Int): FragmentOne {
val fragment = FragmentOne()
val bundle = Bundle()
bundle.putInt("id", position)
fragment.arguments = bundle
return fragment
}
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_one, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val id = arguments?.getInt("id")
tv?.text = id.toString()
}
}
If I don't use view-pager2, Just enough to add PagerAdapter.POSITION_NONE like this answer.
Can you help me?!
If you read the documentation it says
Note: The DiffUtil utility class relies on identifying items by ID. If you are using ViewPager2 to page through a mutable collection, you must also override getItemId() and containsItem().
Taken from this post - Migrate to ViewPager2
Since you are using a mutable ArrayList() you should also override the getItemId() method in your adapter, for the itemId you need a unique ID for every fragment, I solved it using the hashCode() function like this
override fun getItemId(position: Int): Long {
return mFragmentList[position].hashCode().toLong()
}
This should remove the first fragment and get the correct fragment from the list using the itemId
Hope this solves your problem.
Actually delete functionality is working properly 0th index fragment is deleted but after calling notfiydataset changed the first fragment gets th value as 1 and second as 2.
I did the same for java, with viewpager2 and its works!
#Override
public long getItemId(int position) {
Long longtype = Long.valueOf(mFragmentList.get(position).hashCode());
return longtype;
}
#Override
public boolean containsItem(long itemId) {
return super.containsItem(itemId);
}
I'm currently using a ViewPager and TabLayout for a couple of similar fragments. The fragments hosted within the ViewPager all have a RecyclerView. When I swipe more than one page over, the Fragment (I think?) is destroyed. When I swipe back it's recreated.
Upon debugging, I found that the adapter within the Fragment is non null and populated with the data from before. However, once the fragment is visible it no longer displays any entries.
Here's a video of what's going on.
This is a fragment within the ViewPager
class ArtistListFragment : Fragment(), ListView {
override val title: String
get() = "Artists"
private val artistList: RecyclerView by lazy { artist_list }
#Inject lateinit var api: SaddleAPIManager
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View?
= container?.inflate(R.layout.fragment_list_artist)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
artistList.apply {
setHasFixedSize(true)
if (adapter == null) {
adapter = ViewTypeAdapter(mapOf(AdapterConstants.ARTIST to ArtistDelegateAdapter()))
}
layoutManager = LinearLayoutManager(context)
if (savedInstanceState != null && savedInstanceState.containsKey("artist")) {
(adapter as ViewTypeAdapter).setItems(savedInstanceState.getParcelableArrayList<Artist>("artists"))
}
}
(activity?.application as SaddleApplication).apiComponent.inject(this)
refreshData()
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putParcelableArrayList(
"artists",
(artistList.adapter as ViewTypeAdapter).items as ArrayList<out Parcelable>
)
}
private fun refreshData() = launch(UI) {
val result = withContext(CommonPool) { api.getArtists() }
when(result) {
is Success -> (artistList.adapter as ViewTypeAdapter).setItems(result.data.results)
is Failure -> Snackbar.make(artistList, result.error.message ?: "Error", Snackbar.LENGTH_LONG).show()
}
}
}
This is the Fragment hosting the ViewPager
class NavigationFragment : Fragment() {
private val viewPager: ViewPager by lazy { pager }
private val tabLayout: TabLayout by lazy { tab_layout }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View?
= container?.inflate(R.layout.fragment_navigation)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewPager.apply {
adapter = NavigationPageAdapter(childFragmentManager)
}
tabLayout.apply {
setupWithViewPager(viewPager)
}
}
}
The adapter I'm using for paging
class NavigationPageAdapter(fragmentManager: FragmentManager) : FragmentStatePagerAdapter(fragmentManager) {
companion object {
const val NUM_PAGES = 4
}
private val pages: List<Fragment> = (0 until NUM_PAGES).map {
when (it) {
0 -> ArtistListFragment()
1 -> AlbumListFragment()
2 -> TrackListFragment()
else -> PlaylistListFragment()
} as Fragment
}
override fun getItem(position: Int): Fragment = pages[position]
override fun getCount(): Int = NUM_PAGES
override fun getPageTitle(position: Int): CharSequence? = (pages[position] as ListView).title
}
I've tried overriding onSaveInstanceState and reading the information from the bundle. It doesn't seem to do anything. The problem seems to actually be the RecyclerView displaying? It's populated with data which is why I'm stumped.
Try to use setOffscreenPageLimit for ViewPager to keep containing fragments as below:
viewPager.setOffscreenPageLimit(NavigationPageAdapter.NUM_PAGES)
I've figured out the problem. While setOffScreenPageLimit(NavigationAdapter.NUM_PAGES) did work, I am cautious to use it because of memory consumption concerns.
As it turns out, storing references to views using lazy is bad practice. Since onCreateView gets called many times in the lifecycle of the ArtistListFragment the same view wasn't being referenced that was currently inflated on the screen.
Removing all lazy instantiated views and accessing them directly with the Android Extensions solved my problem.
I want to create a dialog which contain's ViewPager inside it which have 3 pages and all pages have different layout structure. I want a solution by that i can set the layout content programmatically . I think this can be done by making fragments for each page but i don't know how to do this.
I go through these answers but i am not getting idea how to use them in my case.
Viewpager in Dialog?
ViewPager in Custom Dialog
ViewPager in simple Dialog
You can try and build your custom dialog through DialogFragment. Consider the XML layout would contain a ViewPager and the code to go about would be:
class PagerDialog : DialogFragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.element_fragment_pager_dialog, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupPager()
}
private fun setupPager() {
val pagerFragment1 = PagerFragment1.newInstance()
val pagerFragment2 = PagerFragment2.newInstance()
val pagerFragment3 = PagerFragment3.newInstance()
viewPager?.adapter = MyFragmentPagerAdapter(childFragmentManager).apply {
adapterReference = object : PageAdapterInterface {
override var fragmentList: List<Fragment> =
mutableListOf(pagerFragment1, pagerFragment2, pagerFragment3)
}
}
}
companion object {
const val tag = "PagerDialog"
}
}
I have used reference to the list because it might cause leaks when not handled correctly. So the PagerAdapterInterface would look like:
interface PageAdapterInterface {
var fragmentList: List<Fragment>
fun getItemCount() = fragmentList.size
#Throws(StackOverflowError::class)
fun getItemAt(index: Int) : Fragment {
if (index >= fragmentList.size) throw StackOverflowError()
return fragmentList[index]
}
}
Your view pager adapter can make use of this reference in manner that is accessing referentially like:
class MyFragmentPagerAdapter(childFragmentManager: FragmentManager) : FragmentStatePagerAdapter(childFragmentManager){
lateinit var adapterReference: PageAdapterInterface
override fun getItem(p0: Int): Fragment = adapterReference.getItemAt(p0)
override fun getCount(): Int = adapterReference.getItemCount()
}
Finally in your Activity or Fragment on create() or onViewCreated() functions respectively, you can initialize the dialog as shown:
class MyActivity: AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
// use childFragmentManager if the code is
// used within the Fragment
val prev = supportFragmentManager.findFragmentByTag(PagerDialog.tag)
if (prev != null) {
supportFragmentManager.beginTransaction()
.remove(prev)
.addToBackStack(null)
.commit()
}
PagerDialog().show(supportFragmentManager, PagerDialog.tag)
}
}
Note: DialogFragment is deprecated on > API 28 check out https://developer.android.com/reference/android/app/DialogFragment