I have basic ViewPager2 with Tablayout - and in each page I have different fragments.
When I need to open this view NOT from first (default) tab I'm doing smth like this:
viewPager.currentItem = selectedTabPosition
This code select tab, but inside it is opening fragment from first tab! Only when I'm selecting the tabs by tapping on it - I can see the right fragment in each tabs.
I've also try to select with Tablayout like this:
tabLayout.getTabAt(position)?.select()
But this code doesn't help and also work with this bug.
Also I've try to set viewPager.currentItem with post / postDelay - but this doesn't work too.
Maybe I've lost something? Or it is a bug in ViewPager2 ?
(EDIT - ViewPager code)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupPagerAdapter()
}
private fun setupPagerAdapter() {
val adapter = MainDocumentScreenPagerAdapter(this)
binding?.viewPager?.letUnit {
it.adapter = adapter
binding?.tabsPagerView?.attachViewPager(requireContext(), it, adapter)
// set tab
it.currentItem = params.pageType.ordinal
}
Adapter code
class MainDocumentScreenPagerAdapter (fragment: Fragment) : ViewPager2TitleAdapter(fragment) {
override fun getItemCount(): Int = DocumentPageType.values().size
override fun createFragment(position: Int): Fragment {
val pageType = DocumentPageType.values().firstOrNull { it.ordinal == position } ?: throw IllegalStateException()
val params = DocumentListFragment.createParams(pageType)
return DocumentListFragment.newInstance(params)
}
override fun getPageTitle(position: Int): Int? {
return when (position) {
DocumentPageType.ALL.ordinal -> DocumentPageType.ALL.title
DocumentPageType.SIGN.ordinal -> DocumentPageType.SIGN.title
DocumentPageType.ACCEPT.ordinal -> DocumentPageType.ACCEPT.title
DocumentPageType.CONFIRM.ordinal -> DocumentPageType.CONFIRM.title
DocumentPageType.REJECT.ordinal -> DocumentPageType.REJECT.title
else -> null
}
}
Where ViewPager2TitleAdapter is :
abstract class ViewPager2TitleAdapter(fragment: Fragment) : FragmentStateAdapter(fragment) {
abstract fun getPageTitle(position: Int): Int?
DocumentListFragment inside it create view, based on param object.
I've also try to create adapter inside OnCreate - but it doesn't affect this case.
The last, but not least - when I've try to open tab, which is out from screen (I've scrollable tabs) - viewPager open selected tab with correct fragment on it.. So, the problem occurs only when I've try to open first 4 tabs (look at the image), which is showing on screen. Bit starting from 5 and next tabs - has selected correct.
So, the decision is in this line of code:
it.setCurrentItem(params.pageType.ordinal, false)
but I was doing it like this:
it.currentItem = params.pageType.ordinal
boolean false make magic in this case - It disable smooth scroll. I've got it from this answer about ViewPager2:
https://stackoverflow.com/a/67319847/4809482
I think an easier more reliable fix is to defer to next run cycle instead of unsecure delay e.g
viewPager.post {
viewPager.setCurrentItem(1, true)
}
Related
I'm using custom Pages in my ViewPager and entire App is based on those screens.
There are 2 main abstract functions in those Pages.
First one is getScreen() which suppose to copy function of onCreateView from Fragment. Its called inside ViewPagerAdapters function to initialize layout for that screen.
Example from adapter:
override fun instantiateItem(container: ViewGroup, position: Int): View {
val page = pageList[position]
val layout = page.getScreen(container)
App.log("flowSwitch: instantiateItem: getScreen: ${page::class.java}")
container.addView(layout)
page.screenLayoutWasInitialized = true
return layout
}
Another function is onScreenSwitched(). This one suppose to be called only if I switch to the screen manually by swiping/clicking on tab/calling it in code to get into next screen.
There is initializing some values for Views, sometimes based on payload provided from previous screens.
I call this onScreenSwitched() function inside switchScreen() function which is part of my Navigation class. I just pass there screen name and payload. Its always called after mainViewPager.setCurrentItem(index, useAnim).
Example:
fun switchScreen(
screen: Class<out FlowScreen>,
payload: List<ScreenPayload>? = null,
action: (() -> Unit)? = null,
useAnim: Boolean = true,
){
App.log("AppNavigationFlow: MainTabActivity: switchScreen: $screen")
try {
val index = mainFlowList.indexOfFirst { item -> item::class.java == screen }
App.log("AppNavigationFlow: MainTabActivity: switchScreen: index: $index")
if (index >= 0){
delayedScreenSelection {
mainTabLayout.getTabAt(index)?.select()
App.log("AppNavigationFlow: MainTabActivity: switchScreen: pageNameAtPos: ${mainViewPager.adapter?.getPageTitle(index)}")
mainViewPager.setCurrentItem(index, useAnim)
mainPagerAdapter.getPageAtPos(index)?.apply {
App.log("AppNavigationFlow: MainTabActivity: switchScreen: page: ${this::class.java}")
mainCurrentPage?.apply {
setScreenVisibleState(false)
resetToDefault()
removeBottomSheet(false)
this#MainTabActivity.removeBottomSheet(false)
}
mainCurrentPage = this
mainCurrentPage?.apply {
setScreenVisibleState(true)
clearPayload()
clearAction()
}
payload?.let { mainCurrentPage?.sendPayload(payload) }
action?.let { mainCurrentPage?.setAction(action) }
onScreenSwitched()
onPageChanged()
}?:kotlin.run {
setTabsEnabled(true)
}
}
} else {
setTabsEnabled(true)
}
}catch (e: IndexOutOfBoundsException){
App.log("AppNavigationFlow: MainTabActivity: switchScreen: $e")
setTabsEnabled(true)
}
}
98% of users are getting always called getScreen function before onScreenSwitched function, therefore my layout is completely initialized by that time onScreenSwitched is called.
But for 2% of users, they are getting kotlin.UninitializedPropertyAccessException because for example I'm trying to setup text for Button which was not initialized yet in getScreen function.
How to prevent this? I'm not sure if ViewPager should allow that to happen. How can setContentView ignore instantiateItem call if layout was not initialized yet for that screen I'm switching to? I ditched Fragments because of this bug happening in Fragments too and its happening again with fully customized logic. How can I build something functional when I cant even rely on basic native components to work as it suppose to at first place? There is possibly something I'm missing but 98% of time its working and I personally cant simulate those crashes but I want to fix it for those 2% of users.
Example usage in Page:
private lateinit var toolbarTitle: TextView
private lateinit var acceptButton: LoadingButton
override fun getScreen(collection: ViewGroup): View {
val layout = CustomResources.inflateLayout(inflater, l, collection, false) as ViewGroup
toolbarTitle = layout.findText(R.id.actionbarTitle)
acceptButton = layout.findViewById(R.id.acceptButton)
return layout
}
override fun onScreenSwitched() {
super.onScreenSwitched()
acceptButton.setText(if(payload.ok) "Yes" else "No")
}
I am presenting a PagingSource returned by Room ORM on a PagingDataAdapter.
The RecyclerView is present on a Fragment -- I have two such fragments. When they are switched, they stop loading the items on next page and only placehodlers are shown on scrolling.
Please view these screen captures if it isn't clear what I mean--
When I scroll without switching fragments, all the items are loaded
When I switch Fragments before scrolling all the way down, the adapter stops loading new items
Relevant pieces of code (please ask if you would like to see some other part/file) -
The Fragment:
private lateinit var recyclerView: RecyclerView
private val recyclerAdapter = CustomersAdapter(this)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
recyclerView = view.findViewById(R.id.recycler_view)
recyclerView.adapter = recyclerAdapter
recyclerView.layoutManager = LinearLayoutManager(context)
viewLifecycleOwner.lifecycleScope.launch {
viewModel.customersFlow.collectLatest { pagingData ->
recyclerAdapter.submitData(pagingData)
}
}
}
View model-
class CustomersListViewModel(application: Application, private val debtOnly: Boolean): ViewModel() {
private val db = AppDatabase.instance(application)
private val customersDao = db.customersDao()
val customersFlow = Pager(PagingConfig(20)) {
if (debtOnly)
customersDao.getAllDebt()
else
customersDao.getAll()
}.flow.cachedIn(viewModelScope)
}
After I went through your code, I found the problem FragmentTransaction.replace function and flow.cachedIn(viewModelScope)
When the activity calls the replace fragment function, the CustomerFragment will be destroyed and its ViewModel will also be destroyed (the viewModel.onCleared() is triggered) so this time cachedIn(viewModelScope) is also invalid.
I have 3 solutions for you
Solution 1: Remove .cachedIn(viewModelScope)
Note that this is only a temporary solution and is not recommended.
Because of this, instances of fragments still exist on the activity but the fragments had destroyed (memory is still leaking).
Solution 2: Instead of using the FragmentTransaction.replace function in the Main activity, use the FragmentTransaction.add function:
It does not leak memory and can still use the cachedIn function. Should be used when the activity has few fragments and the fragment's view is not too complicated.
private fun switchNavigationFragment(navId: Int) {
when (navId) {
R.id.nav_customers -> {
switchFragment(allCustomersFragment, "Customer")
}
R.id.nav_debt -> {
switchFragment(debtCustomersFragment, "DebtCustomer")
}
}
}
private fun switchFragment(fragment: Fragment, tag: String) {
val existingFragment = supportFragmentManager.findFragmentByTag(tag)
supportFragmentManager.commit {
supportFragmentManager.fragments.forEach {
if (it.isVisible && it != fragment) {
hide(it)
}
}
if (existingFragment != fragment) {
add(R.id.fragment_container, fragment, tag)
.disallowAddToBackStack()
} else {
show(fragment)
}
}
}
Solution 3: Using with Navigation Component Jetpack
This is the safest solution.
It can be created using Android Studio's template or some of the following articles.
Navigation UI
A safer way to collect flows
I tried solution 1 and 2 and here is the result:
What's the objective
Im currently working on an app, which has a settings screen, containing a recyclerview. This recyclerview, on item click, opens the relative activity, which are the Settings' submenus (Code below). In the newly opened activity, i want to implement using the implementation androidx.prefrence a preference screen.
What's the problem
For this, im following this video. In this video, they set up a preference screen using PreferenceFragmentCompact. The problem with this, is the fact that we are setting up a new activity and then a new fragment, which in my app its not efficient, since im building my settings with just activities.
Considering this, is it possible to setup a Preference screen in an activity without using fragments? If so, how?
Code:
class ActivitySettings : AppCompatActivity(), AdapterSettings.OnItemClickListener {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_settings)
topToolbarBack.setNavigationOnClickListener {
finish()
}
val settingsList = listOf(
DataItemsSettings(getString(R.string.look), getString(R.string.lookdescription), R.drawable.ic_colored_color_lens),
DataItemsSettings(getString(R.string.playing), getString(R.string.playingdescription), R.drawable.ic_colored_view_carousel),
DataItemsSettings(getString(R.string.images), getString(R.string.imagesdscription), R.drawable.ic_colored_image),
DataItemsSettings(getString(R.string.audio), getString(R.string.audiodescription), R.drawable.ic_colored_volume_up),
DataItemsSettings(getString(R.string.other), getString(R.string.otherdescription), R.drawable.ic_colored_shape),
DataItemsSettings(getString(R.string.about), getString(R.string.aboutdescription), R.drawable.ic_colored_info)
)
val adapter = AdapterSettings(settingsList, this)
rvSettings.adapter = adapter
rvSettings.layoutManager = LinearLayoutManager(this)
}
override fun OnItemClick(position: Int) {
when(position) {
0 -> this.startActivity(Intent(this, ActivitySettingsLook::class.java))
1 -> this.startActivity(Intent(this, ActivitySettingsPlaying::class.java))
2 -> this.startActivity(Intent(this, ActivitySettingsImages::class.java))
3 -> this.startActivity(Intent(this, ActivitySettingsAudio::class.java))
4 -> this.startActivity(Intent(this, ActivitySettingsOther::class.java))
5 -> this.startActivity(Intent(this, ActivityAbout::class.java))
}
}
}
I'm trying to migrate to FragmentStateAdapter but got a problem with showing fragments.
When I first load the adapter, the createFragment() has been called for each Fragment. So from the beginning, I'm seeing the fragments with the correct data. But when I swipe back and forth - the first fragment start to be a blank white screen and if I do one more swipe again, the second page has an empty blank page too.
I tried to set a background color as a def param in the parent view to check is the fragment is still has been restored from the fragment manager. The result shows that yes - they are there but because I'm using arguments to declare the type that will be displayed in this fragment - after recycling the arguments are not exists anymore and that's why I see the blank white page.
Correct me if I'm wrong and what solution will fit better here?
Thanks
class ReviewContractAdapter(activity: FragmentActivity, private val cacheDirAbsolutePath: String):
FragmentStateAdapter(activity) {
companion object {
const val REVIEW_CONTRACT_NUMBER_OF_PAGES = 2
const val CONTRACT = "Contract"
const val PAD = "PAD"
}
override fun getItemCount(): Int = REVIEW_CONTRACT_NUMBER_OF_PAGES
override fun createFragment(position: Int): Fragment {
return PdfViewerFragment().apply {
arguments = Bundle().apply {
when (position) {
0 -> putString(CONTRACT_FILE_PATH, cacheDirAbsolutePath + CONTRACT_PATH)
else -> putString(PAD_FILE_PATH, cacheDirAbsolutePath + PAD_PATH)
}
}
}
}
}
Using Activity to init the Adapter
Nothing special there
review_contract_viewpager.adapter = adapter
TabLayoutMediator(review_contract_tabs, review_contract_viewpager) { tab, position ->
tab.text = when (position) {
0 -> ReviewContractAdapter.CONTRACT
else -> ReviewContractAdapter.PAD
}
}.attach()
I am fairly new to programming, and this is my first large/real project.
--Background--
I have a ViewPager set up within my MainActivity, that slides between 3 different fragments. In Addition, one fragment requires menu items within the toolbar, and a BottomNavigationView is held within the MainActivity.
Each time i move to a new item/page of the ViewPager, i want to select(check) the corresponding page item within the BottomNavView.
I have attempted multiple configurations of the one below, but can't get it perfect
--The Problem--
The time it takes to perform bottom_navigation.menu.getItem(pos).isChecked = true is too long... Approx 50ms on my HTC One, or 100ms in the emulator.
This delay causes the ViewPager scrolling action to be jarred/laggy.
I would like to know if there is a different way to check or highlight the corresponding item within the BottomNavBar, or more desirably, perform the checking action at the same time without impeding the scrolling animation of the ViewPager.
--Attempted Fixes--
Perform check within onPageSelected - (Result is slightly more laggy)
Perform check when scroll state is IDLE - (Results in smooth pager scroll, but the checked item is 'left behind' and updated too late, particularly when scrolling fast)
Find the navigation item in Anko's doAsync{ } - (Results don't change)
// Adds a page-change listener
viewPager.addOnPageChangeListener(object : ViewPager.OnPageChangeListener {
// Set Toolbar title and menu items
override fun onPageSelected(position: Int) {
when (position) {
0 -> {
supportActionBar!!.title = "Page1"
toolbar.menu.setGroupVisible(0, false)
}
1 -> {
supportActionBar!!.title = "Page2"
if (toolbar.menu.size() == 0) {
toolbar.inflateMenu(R.menu.toolbar_list_menu)
} else {
toolbar.menu.setGroupVisible(0, true)
}
}
2 -> {
supportActionBar!!.title = "Page3"
toolbar.menu.setGroupVisible(0, false)
}
}
}
// While view pager settling on new item/page, check the correct bottom navigation view item
override fun onPageScrollStateChanged(state: Int) {
if (state == ViewPager.SCROLL_STATE_SETTLING) {
val pos = viewPager.currentItem
bottom_navigation.menu.getItem(pos).isChecked = true
}
}
// Not needed, must be implemented
override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {}
})
I really hope someone has an answer to help with my apps performance :)
Thanks for your time.