i have an issue with an app that use ViewPager for display fragment. All works fine until the app goes in background and be killed from OS. It seems that after restore i have 2 IncidentScreenFragment that handle events, one with a null presenter (MVP) that crash my app.
My HomeActivity looks like:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
presenter.onViewCreated()
initViews(savedInstanceState)
}
private fun initViews(savedInstanceState: Bundle?){
mapView.onCreate(savedInstanceState)
mapView.getMapAsync(this)
initFragment()
initMenu()
}
private fun initFragment(){
homeFragment = HomeScreenFragment.newInstance()
incidentFragment = IncidentScreenFragment.newInstance()
chatFragment = ChatFragment.newInstance()
weatherFragment = WeatherFragment.newInstance()
viewPager.adapter = ViewPagerAdapter(supportFragmentManager, this)
viewPager.offscreenPageLimit = 4
viewPager?.addOnPageChangeListener(object : ViewPager.OnPageChangeListener {
override fun onPageScrollStateChanged(state: Int) {}
override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {}
override fun onPageSelected(position: Int) {bottom_navigation.currentItem = position}
})
}
override fun getFragmentByPos(pos: Int): Fragment {
return when(pos){
0 -> homeFragment
1 -> incidentFragment
2 -> chatFragment
3 -> weatherFragment
else -> {
homeFragment
}
}
}
And my Adapter:
class ViewPagerAdapter internal constructor(fm: FragmentManager, activity:infinite_software.intelligence_center.intelligencecenter.ui.home.FragmentManager) : FragmentPagerAdapter(fm) {
private val COUNT = 4
private val activity = activity
override fun getItem(position: Int): Fragment{
var fragment: Fragment? = null
when (position) {
0 -> fragment = activity.getFragmentByPos(0)
1 -> fragment = activity.getFragmentByPos(1)
2 -> fragment = activity.getFragmentByPos(2)
3 -> fragment = activity.getFragmentByPos(3)
}
return fragment!!
}
override fun destroyItem(container: ViewGroup, position: Int, `object`: Any) {
super.destroyItem(container, position, `object`)
}
override fun getCount(): Int {
return COUNT
}
override fun getPageTitle(position: Int): CharSequence? {
return "Section " + (position + 1)
}
}
Each Fragment have a static method that return new Fragment:
companion object {
fun newInstance(): HomeScreenFragment {
return HomeScreenFragment()
}
}
When the app has been killed in background i figure out that there is 2 objects (Fragment) that listen to event, one with Presenter correctly instantiate and one without.
Below my abstract BaseFragment class:
abstract class BaseFragment<P : BasePresenter<BaseView>> : BaseView,Fragment() {
protected lateinit var presenter: P
override fun getContext(): Context {
return activity as Context
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return super.onCreateView(inflater, container, savedInstanceState)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
presenter = instantiatePresenter()
}
override fun showError(error: String) {
(activity as BaseActivity<BasePresenter<BaseView>>).showError(error)
}
override fun showError(errorResId: Int) {
(activity as BaseActivity<BasePresenter<BaseView>>).showError(errorResId)
}
abstract fun onBackPressed(): Boolean
/**
* Instantiates the presenter the Fragment is based on.
*/
protected abstract fun instantiatePresenter(): P
abstract val TAG: String
Incident Fragment code:
class IncidentScreenFragment: BaseFragment<IncidentScreenPresenter>(), BaseView, IncidentView, AlertFilterListener, AlertItemClickListener, IncidentDetailListener {
var rvAdapter : IncidentAdapter? = null
var state : Int = LIST_STATE
override fun instantiatePresenter(): IncidentScreenPresenter {
return IncidentScreenPresenter(this)
}
override val TAG: String
get() = "INCIDENT"
override fun getContext(): Context {
return activity as Context
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
super.onCreateView(inflater, container, savedInstanceState)
return inflater.inflate(R.layout.fragment_incident, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
initViews()
presenter.onViewCreated()
initObserve()
}
private fun initViews(){
//Reclycler view
alertRV.layoutManager = LinearLayoutManager(context)
rvAdapter = IncidentAdapter(ArrayList(), context, this)
alertRV.adapter = rvAdapter
//Apply Listeners
headerBox.setFilterListener(this)
incidentDetailView.setListener(this)
}
override fun initObserve() {
//Init observe presenter model
val alertObserver = Observer<ArrayList<AlertModel>> { alerts ->
Timber.d("Data received from Presenter [$alerts]")
showAlertList(alerts)
}
presenter.filteredAlertList.observe(context as BaseActivity<BasePresenter<BaseView>>,alertObserver)
}
override fun updateThisFilters(boxState: Boolean, level: Int) {
presenter.updateFilterList(boxState,level)
}
fun showOnlyThisLevel(level:Int){
presenter.showOnlyThisLevel(level)
headerBox.disableBoxExcept(level)
}
fun showAlertList(list: ArrayList<AlertModel>){
rvAdapter?.updateData(list)
}
override fun onItemClick(model: AlertModel) {
presenter.loadAlertDetail(model)
}
override fun showAlertDetail(model: AlertModel) {
incidentDetailView.setUpFromModel(model)
WhiteWizard.slideLeftEffect(incidentDetailView,incidentListRootElement)
state = DETAIL_STATE
}
override fun onbackFromDetailPressed() {
WhiteWizard.slideRightEffect(incidentListRootElement,incidentDetailView)
state = LIST_STATE
}
override fun showLoader() {
loaderIncident.visibility = View.VISIBLE
}
override fun hideLoader() {
loaderIncident.visibility = View.INVISIBLE
}
override fun onBackPressed(): Boolean {
when(state){
LIST_STATE -> return false
DETAIL_STATE -> {
onbackFromDetailPressed()
return true
}
else -> return false
}
}
fun newInstance(): IncidentScreenFragment {
return IncidentScreenFragment()
}
}
When i click on the button in homePage to display fragment content i got:
Process: XXXXXX, PID: 3192
kotlin.UninitializedPropertyAccessException: lateinit property presenter has not been initialized
at infinite_software.intelligence_center.intelligencecenter.base.BaseFragment.getPresenter(BaseFragment.kt:11)
at XXXXXX.ui.home.incidentScreen.IncidentScreenFragment.showOnlyThisLevel(IncidentScreenFragment.kt:78)
at XXXXXX.ui.home.HomeActivity.filterDataWithSeverity(HomeActivity.kt:110)
at XXXXXX.ui.home.homeScreen.HomeScreenFragment.filterBy(HomeScreenFragment.kt:76)
at XXXXXX.ui.home.homeScreen.HomeScreenFragment$initViews$5.onClick(HomeScreenFragment.kt:56)
If i try to print the id of Fragment, i obtain 2 different ids from method call showOnlyThisLevel() and onBackPressed(). What i miss ?
After doing some research, it seems that the problem stems from the misnaming of FragmentPagerAdapter's method - being named getItem(), but not clearly specifying that the abstract method getItem(int position) is supposed to return a new instance of a fragment rather than just "get an instance of one".
Of course, there is not much we can do about an incorrect name after it's been out in the wild for 7 years, but at least we can fix the bug that stems from this issue in your code ;)
Without further ado, the cause of your NPE is that onCreateView (where your Presenter is instantiated) is never called.
This happens because you are creating the fragment here:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
...
homeFragment = HomeScreenFragment.newInstance()
incidentFragment = IncidentScreenFragment.newInstance()
}
You return this fragment from inside getItem(int position) in your FragmentPagerAdapter:
override fun getItem(position: Int): Fragment = when(position) {
...
1 -> activity.incidentFragment
...
}
So what we know about activity.incidentFragment is that in it, onCreateView() is never called.
This is caused by the fact that it's never actually added to a FragmentManager and never displayed on the screen.
That's because super.onCreate(savedInstanceState) in Activity recreates all Fragments, using their no-args constructor, via reflection, while keeping their tag (see findFragmentByTag).
So as you can see in this answer, or as I can quote here:
// Do we already have this fragment?
String name = makeFragmentName(container.getId(), itemId);
Fragment fragment = mFragmentManager.findFragmentByTag(name);
if (fragment != null) {
if (DEBUG) Log.v(TAG, "Attaching item #" + itemId + ": f=" + fragment);
mCurTransaction.attach(fragment);
} else {
fragment = getItem(position);
if (DEBUG) Log.v(TAG, "Adding item #" + itemId + ": f=" + fragment);
mCurTransaction.add(container.getId(), fragment,
makeFragmentName(container.getId(), itemId));
The getItem(position) method is only called if the Fragment is not found by the fragment tag that the FragmentPagerAdapter sets for the fragment, which IS automatically recreated after low memory condition kills your app.
Therefore, YOUR new fragment (that you create by hand in the Activity) is NEVER used, and therefore it has no view, never initialized, never added to FragmentManager, it's not the same instance as what's actually inside your ViewPager, and it crashes when you call it. Boom!
Solution is to instantiate the Fragment inside FragmentPagerAdapter's getItem(position) method. To get an instance of the fragment, use this answer.
Related
I'm trying to add more fragments dynamically inside my ViewPager2 however when I call the adapter.add(MyNewFragment, position) 2 things happens, if I use notifyDataSetChange() is not showing up the fragment but if I put notifyItemInserted(position) is showing up but the app crashes with an java.lang.IllegalStateException: Fragment already added.
This is my adapter class:
class ViewPagerAdapter(
list: MutableList<Fragment>,
fm: FragmentManager,
lifecycle: Lifecycle,
//fragmentActivity: FragmentActivity
) : FragmentStateAdapter(fm, lifecycle) {
var fragmentList = list
override fun getItemCount(): Int {
return fragmentList.size
}
fun addScreen(fragment: Fragment, position: Int) {
if(!fragmentList.contains(fragment)){
fragmentList.add(position, fragment)
//notifyItemInserted(position)
notifyDataSetChanged()
}
}
override fun createFragment(position: Int): Fragment {
return fragmentList[position]
}
// I tried to use these methods that I saw here but then the viewpager is totally blank and does not load anything or do the same Fragment already added error.
/* override fun getItemId(position: Int): Long {
return fragmentList[position].id.toLong()
}
override fun containsItem(itemId: Long): Boolean = fragmentList.any { it.id.toLong() == itemId }*/
}
And this is my the screen where the viewPager is:
#AndroidEntryPoint
class ViewPagerScreen : Fragment() {
//...
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
if (!::savedViewInstance.isInitialized) {
_binding = FragmentViewPagerScreenBinding.inflate(inflater, container, false)
//....
savedViewInstance = binding.root
}
binding.viewPager.isSaveEnabled = false
return savedViewInstance
}
fun addNewScreen(){
adapter.addScreen(NewFragment(), binding.viewPager.currentItem + 1)
}
private fun initViewPager() {
binding.viewPager.offscreenPageLimit = 2
binding.viewPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
override fun onPageScrolled(
position: Int,
positionOffset: Float,
positionOffsetPixels: Int
) {
super.onPageScrolled(position, positionOffset, positionOffsetPixels)
}
})
adapter = ViewPagerAdapterByFragments(
mutableListOf(FirstScreen(), SecondScreen(), ThirdScreen()),
this#ViewPagerScreen.childFragmentManager,
lifecycle
)
binding.viewPager.adapter = adapter
binding.viewPager.isUserInputEnabled = false
}
}
I think I solved the problem, thanks #snachmsm for pointing me out where to look for.
What I did was quite simple, I only had to change the param list: MutableList<Fragment> to list:MutableList<Pair<String,Fragment>> where my String is my fragmentId. Then on the method createFragment return a new fragment base on my fragmentId.
Here's the code itself:
override fun createFragment(position: Int): Fragment {
return when (fragmentList!![position]!!.first) {
"firstScreen" -> return FirstScreen()
"secondScreen" ->return SecondScreen()
"thirdScreen" ->return ThirdScreen()
"newScreen" -> return NewScreen()
else -> AnotherScreen()
}
}
Also my addScreenMethod() now looks like this:
fun addScreen(newfragment: Pair<String, Fragment>, position: Int) {
val result = fragmentList.find {
it.first == newfragment.first
}
if (result == null) {
fragmentList.add(position, newfragment)
notifyItemInserted(position)
}
}
I am getting exception:
E/MessageQueue-JNI: java.lang.IllegalArgumentException: No view found for id 0x7f0a0554 (ua.com.company.app:id/vpBanners) for fragment BannerItemFragment{30698be} (4c80b228-4303-4c80-b99d-a55b8359b8c2) id=0x7f0a0554}
My hierarchy looks like so:
My adapter for vpHome:
class HomeViewPagerAdapter(fm: FragmentManager) : FragmentStatePagerAdapter(fm, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {
private val mFragmentList: MutableList<Fragment> = ArrayList()
private val mFragmentTitleList: MutableList<String> = ArrayList()
override fun getItem(position: Int): Fragment {
return mFragmentList[position]
}
override fun getCount(): Int {
return mFragmentList.size
}
override fun getPageTitle(position: Int): CharSequence? {
return mFragmentTitleList[position]
}
fun addFragment(fragment: Fragment, title: String) {
mFragmentList.add(fragment)
mFragmentTitleList.add(title)
}
}
And I apply it in this way:
private fun setupViewPager(viewPager: DisableSwipeViewPager) {
vpAdapter = HomeViewPagerAdapter(childFragmentManager).apply {
addFragment(ForYouFragment(), "for you")
addFragment(AnotherFragment1(), "a1")
addFragment(AnotherFragment2(), "a2")
}
viewPager.adapter = vpAdapter
}
Next, my SnapBannersCarouselViewHolder to handle items with ViewPager inside:
class SnapBannersCarouselViewHolder(
private val mBinding: HomeFragmentItemSnapBannersCarouselBinding
) : RecyclerView.ViewHolder(mBinding.root) {
companion object {
// ...
fun newInstance(
inflater: LayoutInflater,
parent: ViewGroup,
onMoreInteractionListener: ((infoBlockId: String) -> Unit)?
): SnapBannersCarouselViewHolder {
val binding =
HomeFragmentItemSnapBannersCarouselBinding.inflate(inflater, parent, false)
return SnapBannersCarouselViewHolder(
binding,
onMoreInteractionListener
)
}
}
fun bind(item: SnapBannersItem, fragmentManager: FragmentManager) {
// ...
val adapter = BannerPagesAdapter(fragmentManager, item.banners)
with(mBinding.vpBanners) {
// ...
this.adapter = adapter
offscreenPageLimit = 3
}
}
}
My RecyclerView adapter ForYouContentAdapter:
class ForYouContentAdapter(
var data: List<HomeBaseItem> = emptyList(),
var fragmentManagerRetriever: () -> FragmentManager
) : BaseRecyclerViewAdapter<HomeBaseItem>(data) {
enum class ViewType(val value: Int) {
// ...
SNAP_BANNERS(6)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return when (viewType) {
// ...
ViewType.SNAP_BANNERS.value -> SnapBannersCarouselViewHolder.newInstance(
mInflater!!,
parent,
onBannersMoreInteractionListener // todo possibly change it
)
else -> throw RuntimeException("Can not create view holder for undefined view type")
}
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (getItemViewType(position)) {
// ...
ViewType.SNAP_BANNERS.value -> {
val vHolder = holder as SnapBannersCarouselViewHolder
vHolder.bind(
getItem(position) as SnapBannersItem,
fragmentManagerRetriever.invoke()
)
}
}
}
}
And my fragmentManagerRetriever implementation in ForYouFragment looks like so:
private val fragmentManagerRetriever: () -> FragmentManager = {
childFragmentManager
}
My BannerItemFragment code:
class BannerItemFragment : Fragment() {
private lateinit var mBinding: HomeFragmentSnapBannerItemBinding
companion object {
// ...
fun newInstance(
item: RectWebViewItem
): BannerItemFragment {
return BannerItemFragment()
}
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
mBinding = HomeFragmentSnapBannerItemBinding.inflate(inflater, container, false)
return mBinding.root
}
// ...
}
In every place where I need to create fragments inside other fragments I am using childFragmentManager.
And when I open ForYouFragment first time, it works normally. My item with ViewPager works normally. But when I replace fragment in Activity's container (adding to back stack) being on ForYouFragment and then return back (back on HomeFragment because ForYouFragment inside HomeFragment), I am getting error.
To replace fragments I am using this method inside ForYouFragment:
private fun showAnotherFragment() {
ActivityUtils.replaceFragmentToActivity(
requireActivity(),
SomeFragment.newInstance(),
true
)
}
And ActivityUtils code:
object ActivityUtils {
fun replaceFragmentToActivity(
activity: FragmentActivity,
fragment: Fragment,
addToBackStack: Boolean
) {
replaceFragmentToActivity(activity, fragment, addToBackStack, containerId = R.id.fragmentContainer)
}
fun replaceFragmentToActivity(
activity: FragmentActivity,
fragment: Fragment,
addToBackStack: Boolean,
containerId: Int
) {
val fragmentManager = activity.supportFragmentManager
val transaction = fragmentManager.beginTransaction()
transaction.replace(containerId, fragment)
if (addToBackStack) {
transaction.addToBackStack(null)
}
transaction.commitAllowingStateLoss()
}
}
Please, help me understand why I am getting this exception?
UPD
Adapter for ViewPager inside RecyclerView:
class BannerPagesAdapter(
fragmentManager: FragmentManager,
var data: List<RectWebViewItem>
) : FragmentStatePagerAdapter(
fragmentManager,
BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT
) {
private var fragments: MutableList<BannerItemFragment> = mutableListOf()
init {
// initial empty fragments
for (i in data.indices) {
fragments.add(BannerItemFragment())
}
}
override fun getItem(position: Int): Fragment {
return BannerItemFragment.newInstance(data[position])
}
override fun getCount(): Int {
return fragments.size
}
override fun instantiateItem(container: ViewGroup, position: Int): Any {
val f = super.instantiateItem(container, position)
fragments[position] = f as BannerItemFragment
return f
}
}
Solved
The problem was that fragments want to be attached to the ViewPager before the ViewPager is attached to its parent. This question outlined here.
So, to solve this problem, I created custom ViewPager:
/**
* Use this ViewPager when you need to place ViewPager inside RecyclerView.
* [LazyViewPager] allows you to set [PagerAdapter] in lazy way. This prevents IllegalStateException
* in case when the fragments want to be attached to the viewpager before the viewpager is
* attached to its parent
*/
class LazyViewPager
#JvmOverloads
constructor(
context: Context,
attrs: AttributeSet? = null
) : ViewPager(context, attrs) {
private var mPagerAdapter: PagerAdapter? = null
override fun onAttachedToWindow() {
super.onAttachedToWindow()
if (mPagerAdapter != null) {
super.setAdapter(mPagerAdapter)
}
}
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
super.setAdapter(null)
}
#Deprecated("Do not use this method to set adapter. Use setAdapterLazy() instead.")
override fun setAdapter(adapter: PagerAdapter?) {}
fun setAdapterLazy(adapter: PagerAdapter?) {
mPagerAdapter = adapter
}
}
And then, instead of using setAdapter() I use setAdapterLazy().
Also, it is important to reset adapter to null in onDetachedFromWindow().
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);
}
Ok, I got the following fragment with callback there witch I'm setting to ViewPager2 adapter:
class PremiumFragment : BaseFragment(), OnPageChanged {
override fun getLogTag(): String {
return TAG
}
private lateinit var pagerCounter: ImageView
private lateinit var viewPager: ViewPager2
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewPager = view.findViewById(R.id.view_pager) as ViewPager2
viewPager.adapter = ViewPagerAdapter(this)
pagerCounter = view.findViewById(R.id.counter)
}
companion object {
const val TAG = "PremiumFragment"
fun getInstance(): BaseFragment {
Log.d(TAG, "getInstance()")
val args = Bundle()
args.putInt(ARG_LAYOUT_ID, R.layout.premium_f)
val fragment =
PremiumFragment()
fragment.arguments = args
return fragment
}
}
override fun onPageChanged() {
when (viewPager.currentItem) {
0 -> pagerCounter.setImageResource(R.drawable.circle_1)
1 -> pagerCounter.setImageResource(R.drawable.circle_2)
2 -> pagerCounter.setImageResource(R.drawable.circle_3)
}
}
}
My adapter:
class ViewPagerAdapter constructor(val callback: OnPageChanged) : RecyclerView.Adapter<PagerVH>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PagerVH =
PagerVH(LayoutInflater.from(parent.context).inflate(R.layout.viewpager_f, parent, false))
override fun getItemCount(): Int = Images.values().size
override fun onBindViewHolder(holder: PagerVH, position: Int) = holder.itemView.run {
pager_container.findViewById<ImageView>(R.id.imageView)
.setImageResource(Images.values()[position].image)
}
override fun getItemViewType(position: Int): Int {
callback.onPageChanged()
return super.getItemViewType(position)
}
companion object {
enum class Images(private val value: Int, val image: Int) {
First(0, R.drawable.wow_girl),
Second(1, R.drawable.insights_girl_2),
Third(2, R.drawable.remind_girl);
}
}
}
It should set new drawable every time we swipe ViewPager, but it changes image in fragment only once an then it is not working, have completely no idea where in adapter I should call my callback for a proper behavior, please tell me where should I call this callback? Appreciate any advices!
So I found out that you should call callback from onViewDetachedFromWindow for it calls every time you swipe fragment, and it is quite logical
override fun onViewDetachedFromWindow(holder: PagerVH) {
callback.onPageChanged()
super.onViewDetachedFromWindow(holder)
}
I am trying to implement a simple swiping action between the three main screens of my app (feed, forum & profile).
I've been looking for guides online but they all seem to take the same approach and show how to implement the adapter using fragments that are simple instances of a basic class that for example just inflates a title and a number on the screen.
In my case, the fragments are not all instances of the same class and are completely different.
M problem lays in the FragmentPagerAdapter. I am not sure how to return each fragment in the getItem method.
I have tried the following but it doesn't except it as a valid return statement and is still expecting one:
class PagesPagerAdapter(fragmentManager: FragmentManager) : FragmentPagerAdapter(fragmentManager) {
override fun getItem(p0: Int): Fragment? {
return when (p0){
0 -> FeedFragment.newInstance()
1 -> BoardFragment.newInstance()
2 -> ProfileFragment.newInstance()
else -> FeedFragment.newInstance()
}
}
override fun getCount(): Int {
return 3
}
}
This is an example of one of my fragments:
class FeedFragment : Fragment() {
val galleryAdapter = GroupAdapter<ViewHolder>()
private fun setUpGalleryAdapter() {
feed_gallary.adapter = galleryAdapter
val galleryLayoutManager = GridLayoutManager(this.context, 3)
feed_gallary.layoutManager = galleryLayoutManager
val dummyUri =
"https://firebasestorage.googleapis.com/v0/b/dere-3d530.appspot.com/o/20150923_100950.jpg?alt=media&token=97f4b02c-75d9-4d5d-bc86-a3ffaa3a0011"
val imageUri = Uri.parse(dummyUri)
if (imageUri != null) {
galleryAdapter.add(FeedImage(imageUri))
galleryAdapter.add(FeedImage(imageUri))
galleryAdapter.add(FeedImage(imageUri))
galleryAdapter.add(FeedImage(imageUri))
galleryAdapter.add(FeedImage(imageUri))
}
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
setUpGalleryAdapter()
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? =
inflater.inflate(R.layout.fragment_feed, container, false)
companion object {
fun newInstance(): FeedFragment = FeedFragment()
}
}
And this is my main activity:
class MainActivity : AppCompatActivity() {
private lateinit var viewPager: ViewPager
private lateinit var pagerAdapter: PagesPagerAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
viewPager = findViewById<ViewPager>(R.id.view_pager)
pagerAdapter = PagesPagerAdapter(supportFragmentManager)
viewPager.adapter = pagerAdapter
}
}
I have also tried to define a variable currentFragment, have it reassigned for each of the when statements and have it be the return value, but as all my fragments are unique I am getting a type mismatch when trying to assign them all to one variable.
Is there some Array I can store the fragments in and then return the array position?
I feel like there is probably a simple elegant and common solution here.
Without seeing all of your code I cannot be sure but one thing to note is you must return an instance of your fragment not a reference to the class. Ex: FeedFragment() instead of FeedFragment.
Also, your when statement does not have a default case resulting in a compile error for the getItem method as you have a code path that doesn't return anything. Try the following code for getItem
override fun getItem(p0: Int): Fragment {
when(p0){
0 -> return FeedFragment.newInstance()
1 -> return BoardFragment.newInstance()
else -> return ProfileFragment.newInstance()
}
}
Unfortunately, there is no elegant solution for this case. You can store created fragment in array and then use this array when you need to access related fragment instance.
class PagesPagerAdapter(fragmentManager: FragmentManager) : FragmentPagerAdapter(fragmentManager) {
private val fragments = SparseArray<WeakReference<Fragment>>()
override fun getCount() = 3
override fun getItem(position: Int): Fragment? {
val fragmentReference = fragments[position]
return if (fragmentReference?.get() != null) {
fragmentReference.get()
} else {
return when (position) {
0 -> FeedFragment.newInstance()
1 -> BoardFragment.newInstance()
2 -> ProfileFragment.newInstance()
else -> error("Incorrect position: $position")
}
}
}
override fun instantiateItem(container: ViewGroup, position: Int): Any {
val fragment = super.instantiateItem(container, position) as Fragment
fragments.put(position, WeakReference(fragment))
return fragment
}