How to avoid refreshing recyclerview refreshing on viewpager swipe? - android

In my android application the app structure according to feature requirement is as follows:
MainActivity has a Frame layout and a MainFragment.
When Activity onCreate method is called main fragment is setup on frame layout.
In MainFragment there is a Viewpager and a Tablayout.
Viewpager loads 5 Fragments lets say Fragment1, Fragment2, Fragment3, Fragment4, Fragment5
Each Fragment has a recyclerview.
All Fragments share same Recyclerview.Adapter the data is set accordingly.
Issue:
When app is loaded and MainFragment is setup all fragments are loaded at once as i have used viewPager.offscreenPageLimit = 4 but on swipe the recyclerview's onBindViewHolder method is called again and again which causes recyclerview data to blink and it all ends up in lagging on viewpager swipe.
Below is my code part
MainFragment.kt
#Keep
class MainFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
val rootView = inflater.inflate(R.layout.mainfragment, container, false)
mainActivity = activity as MainActivity
return rootView
}
companion object {
#JvmStatic
fun newInstance() =
BottomNavFragment().apply {
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setUpTabLayout()
}
private fun setUpTabLayout() {
try {
val adapter = FileViewPagerAdapterTEST(childFragmentManager)
adapter.addFragments()
viewPager.adapter = adapter
viewPager.offscreenPageLimit = 4
viewPager.currentItem = 1
// viewPager.post { viewPager.currentItem = 1 }
viewPager.addOnPageChangeListener(object : ViewPager.OnPageChangeListener {
override fun onPageScrolled(
position: Int,
positionOffset: Float,
positionOffsetPixels: Int
) {
}
override fun onPageSelected(position: Int) {
mainActivity.clearSearch()
}
override fun onPageScrollStateChanged(state: Int) {
}
})
tabLayout.setupWithViewPager(viewPager)
tabLayout.getTabAt(0)?.icon = mainActivity.getDrawable(R.drawable.icimg)
tabLayout.getTabAt(1)?.icon = mainActivity.getDrawable(R.drawable.icimgg)
tabLayout.getTabAt(2)?.icon = mainActivity.getDrawable(R.drawable.icimggg)
tabLayout.getTabAt(3)?.icon = mainActivity.getDrawable(R.drawable.icimggggg)
tabLayout.getTabAt(4)?.icon = mainActivity.getDrawable(R.drawable.icimgggggg)
tabLayout.getTabAt(0)?.text = getString(R.string.text1)
tabLayout.getTabAt(1)?.text = getString(R.string.text2)
tabLayout.getTabAt(2)?.text = getString(R.string.text3)
tabLayout.getTabAt(3)?.text = getString(R.string.text4)
tabLayout.getTabAt(4)?.text = getString(R.string.text5)
} catch (ex: Exception) {
ex.printStackTrace()
}
}
}
mainfragment.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
>
<View
android:id="#+id/view"
android:layout_width="match_parent"
android:layout_height="#dimen/_50sdp"
android:background="#color/colorPrimary" />
<com.google.android.material.tabs.TabLayout
android:id="#+id/tabLayout"
android:layout_width="match_parent"
android:layout_height="#dimen/_60sdp"
android:layout_marginStart="#dimen/_10sdp"
android:layout_marginTop="#dimen/_20sdp"
android:layout_marginEnd="#dimen/_10sdp"
android:background="#drawable/bg_round_view_white"
app:tabSelectedTextColor="#color/colorPrimary"
app:tabTextAppearance="#style/ThemeTextAppearanceTab">
</com.google.android.material.tabs.TabLayout>
<androidx.viewpager.widget.ViewPager
android:id="#+id/viewPager"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="#+id/tabLayout" />
</RelativeLayout>
ViewPager Adapter
class FileViewPagerAdapterTEST(fm: FragmentManager) : FragmentPagerAdapter(
fm,
BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT
) {
val fragments = arrayListOf<Fragment>()
fun addFragments() {
fragments.add(Fragment1())
fragments.add(Fragment2())
fragments.add(Fragment3())
fragments.add(Fragment4())
fragments.add(Fragment5())
}
override fun getItem(pos: Int): Fragment {
return fragments[pos]
}
override fun getCount(): Int {
return 5
}
}
Fragment1.kt (Funtionality in all child fragments is implemented same way)
#Keep
class Fragment1 : Fragment() {
private val fragmentViewModel: FileFragmentsViewModel by viewModel()
private val activityViewModel: MainActivityViewModel by activityViewModels()
private val sharedPreferencesManager = get<SharedPreferencesManager>()
private val repository = get<FilesRepository>()
private var listAdapter: ListRowItemAdapter? = null
private var tabType = TabType.ALL
private var listOfTab2 = mutableListOf<MyModelClass>()
private var listOfTab3 = mutableListOf<MyModelClass>()
private var lisOfTab1 = mutableListOf<MyModelClass>()
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val rootView = inflater.inflate(R.layout.fragment_first, container, false)
try {
fragmentViewModel.loadAllFiles()
listAdapter = ListRowItemAdapter(
sharedPreferencesManager,
fragmentViewModel,
activityViewModel,
repository,
inflater.context,
)
refreshFilesOnAdapter()
} catch (ex: Exception) {
ex.printStackTrace()
}
return rootView
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
Handler().postDelayed({
layoutProgress.visibility = View.GONE
val ll = LinearLayoutManager(context)
ll.isSmoothScrollbarEnabled = false;
setUpRecyclerView(ll, null)
handleViewModelObservers()
}, 2000)
}
private fun setUpRecyclerView(
layoutManager: RecyclerView.LayoutManager,
filesToSet: MutableList<MyModelClass>?
) {
// rvAllFiles.itemAnimator=null
// (rvAllFiles.itemAnimator as DefaultItemAnimator).supportsChangeAnimations = false
// rvAllFiles.itemAnimator?.changeDuration=0
(rvAllFiles.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
rvAllFiles.isNestedScrollingEnabled = false
rvAllFiles.layoutManager = layoutManager
rvAllFiles.adapter = listAdapter
if (filesToSet != null) {
when (tabType) {
TabType.TYPE1 -> {
listAdapter!!.setDataList(fragmentName, filesToSet, TabType.TYPE1)
}
TabType.TYPE2 -> {
listAdapter!!.setDataList(fragmentName, filesToSet, TabType.TYPE2)
}
TabType.TYPE3 -> {
listAdapter!!.setDataList(fragmentName, filesToSet, TabType.TYPE3)
}
}
}
}
private fun handleViewModelObservers() {
observeBottomTabChange()
}
private fun observeBottomTabChange() {
//Bottom navigation menu change (i.e Documents,Recent,Bookmarks)
activityViewModel.selectedFragmentMain.observe(viewLifecycleOwner,
{ changedTabType ->
listAdapter!!.setCurrentBottomTabType(fragmentName, changedTabType)
tabType = changedTabType
})
}
private fun refreshFilesOnAdapter() {
fragmentViewModel.getAllItems().observe(viewLifecycleOwner,
{ filesList ->
lisOfAllItems = filesList
listAdapter!!.setAllItemsList(filesList)
})
fragmentViewModel.getLiveDataFromDb()?.observe(viewLifecycleOwner, { filesList ->
fragmentViewModel.getAllItems().postValue(
fragmentViewModel.checkFilesWithDB(
filesList,
fragmentViewModel.getAllItems().value
)
)
fragmentViewModel.getTab2FileFromDB(FileType.ALL)
?.observe(viewLifecycleOwner,
{ tab2list ->
listOfTab2 = tab2list
listAdapter!!.setttab2list(
listOfTab2
.toMutableList().asReversed()
)
})
fragmentViewModel.getTab3FileFromDB
(FileType.ALL)
?.observe(viewLifecycleOwner,
{ tab3list ->
listOfTab3 = tablist3
listAdapter!!.setttab3list
(listOfTab3
})
})
}
}
fragment_first.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
>
<androidx.recyclerview.widget.RecyclerView
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="#string/appbar_scrolling_view_behavior"
android:paddingBottom="#dimen/_10sdp"
android:clipToPadding="false"
android:id="#+id/rvAllFiles" />
<include
layout="#layout/layout_fragment_list_status"
android:id="#+id/layoutNoResults"
android:visibility="gone" />
<include
layout="#layout/layout_fragment_list_progress"
android:id="#+id/layoutProgress"
/>
</RelativeLayout>
How this lag issue can be solved so that when MainActivity is loaded and MainFragment is set on it all fragments are loaded at once and Viewpager retains them so that on swipe only loaded fragment data should be displayed instead of refreshing Recyclerviews of fragments.
Can somebody please help me out. Any help will be appreciated.

Related

Kotlin-Attempt to invoke virtual method 'void androidx.recyclerview.widget.RecyclerView.setAdapter(androidx.recyclerview.widget.RecyclerView$Adapter)'

I Am trying to run the application but it crashes when i try to access the content of a bottom navigation bar which has a fragment in it and the fragement cointains a recyclerView.The adpater is null here is the error
java.lang.NullPointerException: Attempt to invoke virtual method 'void androidx.recyclerview.widget.RecyclerView.setAdapter(androidx.recyclerview.widget.RecyclerView$Adapter)' on a null object reference
at com.example.accers.ChatFragment.recyclerView(ChatFragment.kt:67)
at com.example.accers.ChatFragment.onCreateView(ChatFragment.kt:41)
Fragment xml
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ChatFragment">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:id="#+id/ll_layout_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:background="#E4E4E4"
android:orientation="horizontal">
<EditText
android:id="#+id/et_message"
android:inputType="textShortMessage"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:layout_weight=".5"
android:background="#drawable/round_button"
android:backgroundTint="#android:color/white"
android:hint="Type a message..."
android:padding="10dp"
android:singleLine="true" />
<Button
android:id="#+id/btn_send"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="10dp"
android:layout_weight="1"
android:background="#drawable/round_button"
android:backgroundTint="#26A69A"
android:text="Send"
android:textColor="#android:color/white" />
</LinearLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/rv_messages"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_above="#id/ll_layout_bar"
tools:itemCount="20"
tools:listitem="#layout/message_item" />
<!-- <View-->
android:layout_below="#+id/dark_divider"
<!-- android:layout_width="match_parent"-->
<!-- android:layout_height="10dp"-->
<!-- android:background="#42A5F5"-->
<!-- android:id="#+id/dark_divider"/>-->
</RelativeLayout>
</FrameLayout>
Fragment Class
class ChatFragment : Fragment() {
private val TAG = "ChatFragment"
var messagesList = mutableListOf<Message>()
private lateinit var adapter: MessagingAdapter
private val botList = listOf("Cassandra", "Francesca", "Luigi", "Nico","Lesley","Hiyle","Roselind")
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
// Inflate the layout for this fragment
var view = inflater.inflate(R.layout.fragment_chat, container, false)
// var button: Button = view.findViewById(R.id.btn_send)
recyclerView()
clickEvents()
val random = (0..3).random()
customBotMessage("Hello! Today you're speaking with ${botList[random]}, how may I help?")
return view
}
private fun clickEvents() {
//Send a message
btn_send.setOnClickListener {
sendMessage()
}
// Scroll back to correct position when user clicks on text view
et_message.setOnClickListener {
GlobalScope.launch {
delay(100)
withContext(Dispatchers.Main) {
rv_messages.scrollToPosition(adapter.itemCount - 1)
}
}
}
}
private fun recyclerView() {
adapter = MessagingAdapter()
rv_messages.adapter = adapter
rv_messages.layoutManager = LinearLayoutManager(activity)
}
override fun onStart() {
super.onStart()
//In case there are messages, scroll to bottom when re-opening app
GlobalScope.launch {
delay(100)
withContext(Dispatchers.Main) {
rv_messages.scrollToPosition(adapter.itemCount - 1)
}
}
}
private fun sendMessage() {
val message = et_message.text.toString()
val timeStamp = Time.timeStamp()
if (message.isNotEmpty()) {
//Adds it to our local list
messagesList.add(Message(message, SEND_ID, timeStamp))
et_message.setText("")
adapter.insertMessage(Message(message, SEND_ID, timeStamp))
rv_messages.scrollToPosition(adapter.itemCount - 1)
botResponse(message)
}
}
private fun botResponse(message: String) {
val timeStamp = Time.timeStamp()
GlobalScope.launch {
//Fake response delay
delay(1000)
withContext(Dispatchers.Main) {
//Gets the response
val response = BotResponse.basicResponses(message)
//Adds it to our local list
messagesList.add(Message(response, RECEIVE_ID, timeStamp))
//Inserts our message into the adapter
adapter.insertMessage(Message(response, RECEIVE_ID, timeStamp))
//Scrolls us to the position of the latest message
rv_messages.scrollToPosition(adapter.itemCount - 1)
//Starts Google
when (response) {
OPEN_GOOGLE -> {
val site = Intent(Intent.ACTION_VIEW)
site.data = Uri.parse("https://www.google.com/")
startActivity(site)
}
OPEN_SEARCH -> {
val site = Intent(Intent.ACTION_VIEW)
val searchTerm: String? = message.substringAfterLast("search")
site.data = Uri.parse("https://www.google.com/search?&q=$searchTerm")
startActivity(site)
}
}
}
}
}
private fun customBotMessage(message: String) {
GlobalScope.launch {
delay(1000)
withContext(Dispatchers.Main) {
val timeStamp = Time.timeStamp()
messagesList.add(Message(message, RECEIVE_ID, timeStamp))
adapter.insertMessage(Message(message, RECEIVE_ID, timeStamp))
rv_messages.scrollToPosition(adapter.itemCount - 1)
}
}
}
}
My Adapter class
class MessagingAdapter: RecyclerView.Adapter<MessagingAdapter.MessageViewHolder>() {
var messagesList = mutableListOf<Message>()
inner class MessageViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
init {
itemView.setOnClickListener {
//Remove message on the item clicked
messagesList.removeAt(adapterPosition)
notifyItemRemoved(adapterPosition)
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MessageViewHolder {
return MessageViewHolder(
LayoutInflater.from(parent.context).inflate(R.layout.message_item, parent, false)
)
}
override fun getItemCount(): Int {
return messagesList.size
}
#SuppressLint("SetTextI18n")
override fun onBindViewHolder(holder: MessageViewHolder, position: Int) {
val currentMessage = messagesList[position]
when (currentMessage.id) {
SEND_ID -> {
holder.itemView.tv_message.apply {
text = currentMessage.message
visibility = View.VISIBLE
}
holder.itemView.tv_bot_message.visibility = View.GONE
}
RECEIVE_ID -> {
holder.itemView.tv_bot_message.apply {
text = currentMessage.message
visibility = View.VISIBLE
}
holder.itemView.tv_message.visibility = View.GONE
}
}
}
fun insertMessage(message: Message) {
this.messagesList.add(message)
notifyItemInserted(messagesList.size)
}
}
MainActivity Class
Main Activity class has the bottom nav bar to replace the fragments
val navHostFragment = supportFragmentManager.findFragmentById(R.id.mainContainer) as NavHostFragment
navController = navHostFragment.navController
val bottomNavigationView = findViewById<BottomNavigationView>(R.id.bottomNavigationView)
setupWithNavController(bottomNavigationView, navController)
I Have tried simillar solutions but i can't still figure how to apply simillar asked question and errors. Thank you
You are using kotlin synthetics. Internally it will work as getView().findViewById(R.id.rv_messages)
Since in oncreateView, you are trying to access view before even view is attached in the fragment layout tree.
getView() will always return null.
Several things you can do. You can pass view to the recycler view function and access like view.rv_messages.
It's better to handle like the below.
Else you can move all view related to onViewCreated(). In onCreateView() you will just inflate and return the view. So in onViewCreated() when it calls getView() , since view is already added in onCreateView it will return the correct view object.
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
// Inflate the layout for this fragment
var view = inflater.inflate(R.layout.fragment_chat, container, false)
return view
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
recyclerView()
clickEvents()
val random = (0..3).random()
customBotMessage("Hello! Today you're speaking with ${botList[random]}, how may I help?")
}
Also synthetics have been deprecated, and currently, it is not recommended. will strongly recommend you to use view binding for binding the views. As ,earlier I had a weird issue with synthetics which I have covered here.
Refer here for more details about deprecation of kotlin synthetics.

Android espresso does not navigate to other Fragment?

In my project that makes open new fragment with NavigationComponenet when click button. I want to test if fragment open when click button, But it don't work properly. Only it click button and does not open another fragment. So, I can't test if it works. Why it does not navigate?
#RunWith(AndroidJUnit4::class)
class WelcomeFragmentTestDoctor {
val phoneHelper = PhoneHelper
private lateinit var scenario: FragmentScenario<WelcomeFragment>
#Before
fun setup() {
scenario = launchFragmentInContainer(themeResId = R.style.AppTheme)
scenario.moveToState(Lifecycle.State.STARTED)
Intents.init()
}
#After
fun tearDown(){
Intents.release()
}
#Test
fun clickApplyAsADoctor(){
val navController = TestNavHostController(
ApplicationProvider.getApplicationContext())
scenario.onFragment { fragment ->
navController.setGraph(R.navigation.auth_navigation)
Navigation.setViewNavController(fragment.requireView(), navController)
}
onView(withId(R.id.buttonDoctor)).perform(click())
Assert.assertEquals(navController.currentDestination?.id, R.id.action_welcomeFragment_to_doctorRegistrationFragment)
}
}
fragment_doctor_registration.xml
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="#+id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="#dimen/_14sdp"
android:paddingBottom="#dimen/_14sdp"
app:layout_constraintTop_toTopOf="parent">
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.core.widget.NestedScrollView>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
DoctorRegistrationFragment.kt
class DoctorRegistrationFragment : Fragment() {
private lateinit var mBinding: FragmentDoctorRegistrationBinding
private val mViewModel: DoctorRegistrationViewModel by inject()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
mBinding = FragmentDoctorRegistrationBinding.inflate(inflater, container, false)
return mBinding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
DoctorRegistrationComponent.inject()
with(mBinding) {
backButton.setOnCrashOnClickListener {
findNavController().popBackStack()
}
btnSend.setOnCrashOnClickListener {
mViewModel.onEvent(
DoctorRegistrationInteractions.RegisterStart(fdr_name.text, fdr_surname.text,
fdr_title.text, fdr_diploma.text, fdr_branch.text,
inputLogin.lifEdittext.text.toString(), fdr_email.text, fdr_address.text,
fdr_company.text, fdr_tax.text
))
}
}
with(mViewModel) {
actions.map { it.getContentIfNotHandled() }.onEach(::handleActions).launchIn(viewLifecycleOwner.lifecycleScope)
}
}
private fun handleActions(action: DoctorRegistrationActions) {
when (action) {
is DoctorRegistrationActions.ErrorMessage -> PopupMessage.error(requireActivity(),message = action.message)
DoctorRegistrationActions.Init -> { }
is DoctorRegistrationActions.SuccessMessage -> {
PopupMessage.success(requireActivity(), message = action.message)
findNavController().popBackStack()
}
}
}
}
You need to check the fragment id not the Action id
Assert.assertEquals(navController.currentDestination?.id, R.id.doctorRegistrationFragment)
Assuming that doctorRegistrationFragment is the id of your fragment tag in your nav graph
<fragment
android:id="#+id/doctorRegistrationFragment"
/* rest of attrs */ >

FragmentStateAdapter and ViewPager2 is loading fragment slower than FragmentStatePagerAdapter and ViewPager

In my app, am showing list of month calendar which can be scrollable to previous and next month. While swiping to previous and next, my fragments are loading smoothly and without any delay if I use FragmentStatePagerAdapter and ViewPager but it's now deprecated. So, I updated
ViewPager to ViewPager2 
FragmentStatePagerAdapter to FragmentStateAdapter 
After updating, I can see there is a delay in loading the fragments while swiping previous and next. But the same code works smoothly with FragmentStatePagerAdapter and ViewPager.
I also tried mViewPager2.setOffscreenPageLimit(//Interger Value//) but no help.
Please assist me.
fragment_employee_months_holder
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.viewpager2.widget.ViewPager2
android:id="#+id/fragment_months_viewpager"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clickable="true"
android:focusable="true" />
<ProgressBar
android:id="#+id/progressBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:backgroundTint="#color/colorPrimary" />
</RelativeLayout>
EmployeeSummaryAdapter
class EmployeeSummaryAdapter(
fragment: Fragment,
private val mCodes: List<String>,
private val mListener: NavigationListener,
private val progressBarInterface: ProgressBarInterface
) : FragmentStateAdapter(fragment) {
private val mFragments = SparseArray<EmployeeSummaryFragment>()
override fun createFragment(position: Int): EmployeeSummaryFragment {
val bundle = Bundle()
val code = mCodes[position]
bundle.putString(DAY_CODE, code)
val fragment = EmployeeSummaryFragment(progressBarInterface)
fragment.arguments = bundle
fragment.listener = mListener
mFragments.put(position, fragment)
return fragment
}
override fun getItemCount(): Int = mCodes.size
override fun getItemId(position: Int): Long {
return position.toLong()
}
}
EmployeeSummaryFragmentHolder
class EmployeeSummaryFragmentHolder : MyFragmentHolder(), NavigationListener, ProgressBarInterface {
private var viewPager2: ViewPager2? = null
private lateinit var mEmployeeSummaryAdapter: EmployeeSummaryAdapter
private lateinit var mView: View
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
currentDayCode = Formatter.getTodayCode()
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
mView = inflater.inflate(R.layout.fragment_employee_months_holder, container, false)
viewPager2 = mView.fragment_months_viewpager
viewPager2!!.id = (System.currentTimeMillis() % 100000).toInt()
Handler(Looper.getMainLooper()).postDelayed({
setupFragment()
}, 0)
return mView
}
private fun setupFragment() {
val codes = getMonths(currentDayCode)
mEmployeeSummaryAdapter = EmployeeSummaryAdapter(this, codes, this, this)
viewPager2.apply {
viewPager2!!.adapter = mEmployeeSummaryAdapter
val myPageChangeCallback = object : ViewPager2.OnPageChangeCallback() {
override fun onPageScrollStateChanged(state: Int) { }
override fun onPageScrolled(position: Int, positionOffset: Float,
positionOffsetPixels: Int) { }
override fun onPageSelected(position: Int) { }
}
viewPager2!!.registerOnPageChangeCallback(myPageChangeCallback)
}
}
}
EmployeeSummaryFragment
class EmployeeSummaryFragment(val progressBarInterface: ProgressBarInterface) :
Fragment(), MonthlyCalendar {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val view = inflater.inflate(R.layout.fragment_employee_month, container, false)
...
...
return view
}
override fun onResume() {
super.onResume()
...
...
}
}
I tried to look into your code and I didn't find anything suspicious, but you definitely need to set your mViewPager2.setOffscreenPageLimit(//Interger Value//) into your EmployeeSummaryFragmentHolder.
viewpager2.offscreenPageLimit = 2

Migrating from ViewPager to ViewPager2 with FragmentStateAdapter causes visible screen yank and StrictMode to be triggered

I've followed the Android documentation and this tutorial to migrate to ViewPager2. When swiping the 4 screens from left to right, there is a visible screen yank which is not a great user experience. I've used strictmode to measure the performance.
The documentation explicitly mentions that createFragment must provide a new Fragment each time so the implementation seems to be implemented correct.
MainActivity:
private fun initializeViewPager(items: List<Item>) {
val tabAdapter = TabAdapter(this, items)
viewPager2.adapter = tabAdapter
TabLayoutMediator(tabLayout, viewPager2) { tab, position ->
tab.text = items[position].abbreviation
}.attach()
class TabAdapter(activity: AppCompatActivity, private val items: List<Item>) :
FragmentStateAdapter(activity) {
override fun getItemCount(): Int {
return items.size
}
override fun getItemId(position: Int): Long {
return items[position].getId()
}
override fun createFragment(position: Int): Fragment {
return VersesFragment.getInstance(items[position])
}
}
SomeFragment:
class SomeFragment : DaggerFragment() {
var adapter: ItemAdapter? = null
private lateinit var binding: FragmentSomeBinding
private lateinit var item: Item
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
handleArguments()
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
binding = FragmentSomeBinding.inflate(inflater, container, false).apply {
lifecycleOwner = viewLifecycleOwner
}
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
//
.. Setting of view elements using the serialized item
//
}
private fun handleArguments() {
val args = arguments
?: throw NullPointerException("getArguments() seems to be null. ARG_BIBLETRANSLATION needed!")
if (!args.containsKey(ARG_BIBLETRANSLATION)) throw NullPointerException("getArguments() does not contain ARG_BIBLETRANSLATION!")
item = args[ARG_BIBLETRANSLATION] as Item
}
companion object {
const val ARG_MY_ITEM = "arg_my_item"
fun getInstance(item: Item): Fragment {
val fragment = VersesFragment()
fragment.arguments = Bundle(2).apply {
putSerializable(ARG_MY_ITEM, item)
}
return fragment
}
}
}
activity_main.xml:
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/recycler"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</FrameLayout>
</layout>
Just for completeness, here is the original code that does perform fine:
MainActivity:
val adapter = SomePagerAdapter(items as ArrayList<Item>)
viewPager.adapter = sdapter
SomePagerAdapter:
class SomePagerAdapter(private val items: ArrayList<Item>) : PagerAdapter() {
override fun instantiateItem(container: ViewGroup, position: Int): Any {
// Same code as in SomeFragment
}
}
The tutorial from raywenderlich is super simple, but when enabling
strictmode, it does get triggered, while strictmode does not get triggered with the old viewpager and the same code:
private fun enableStrictMode() {
StrictMode.setThreadPolicy(
StrictMode.ThreadPolicy.Builder()
.detectDiskReads()
.detectDiskWrites()
.detectNetwork()
.penaltyLog()
.build()
)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
StrictMode.VmPolicy.Builder()
.detectNonSdkApiUsage()
.detectAll()
}
}
https://cs.android.com/androidx/platform/frameworks/support/+/androidx-master-dev:viewpager2/viewpager2/src/main/java/androidx/viewpager2/adapter/FragmentStateAdapter.java
it seems the adapter caches fragments but does not pre-init views. forward scroll is janky, backward works fine. 1-> 2 -> 3 slow, 3 ->2 ->1 fine

How to implement shared transition element from RecyclerView item to Fragment with Android Navigation Component?

I have a pretty straightforward case. I want to implement shared element transition between an item in recyclerView and fragment. I'm using android navigation component in my app.
There is an article about shared transition on developer.android and topic on stackoverflow but this solution works only for view that located in fragment layout that starts transition and doesn't work for items from RecyclerView. Also there is a lib on github but i don't want to rely on 3rd party libs and do it by myself.
Is there some solution for this? Maybe it should work and this is just a bug? But I haven't found any information about it.
code sample:
transition start
class TransitionStartFragment: Fragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_transition_start, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val testData = listOf("one", "two", "three")
val adapter = TestAdapter(testData, View.OnClickListener { transitionWithTextViewInRecyclerViewItem(it) })
val recyclerView = view.findViewById<RecyclerView>(R.id.test_list)
recyclerView.adapter = adapter
val button = view.findViewById<Button>(R.id.open_transition_end_fragment)
button.setOnClickListener { transitionWithTextViewInFragment() }
}
private fun transitionWithTextViewInFragment(){
val destination = TransitionStartFragmentDirections.openTransitionEndFragment()
val extras = FragmentNavigatorExtras(transition_start_text to "transitionTextEnd")
findNavController().navigate(destination, extras)
}
private fun transitionWithTextViewInRecyclerViewItem(view: View){
val destination = TransitionStartFragmentDirections.openTransitionEndFragment()
val extras = FragmentNavigatorExtras(view to "transitionTextEnd")
findNavController().navigate(destination, extras)
}
}
layout
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:id="#+id/transition_start_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="transition"
android:transitionName="transitionTextStart"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="#+id/open_transition_end_fragment"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toBottomOf="#id/transition_start_text"
android:text="open transition end fragment" />
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/test_list"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="#id/open_transition_end_fragment"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />
adapter for recyclerView
class TestAdapter(
private val items: List<String>,
private val onItemClickListener: View.OnClickListener
) : RecyclerView.Adapter<TestAdapter.ViewHodler>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHodler {
return ViewHodler(LayoutInflater.from(parent.context).inflate(R.layout.item_test, parent, false))
}
override fun getItemCount(): Int {
return items.size
}
override fun onBindViewHolder(holder: ViewHodler, position: Int) {
val item = items[position]
holder.transitionText.text = item
holder.itemView.setOnClickListener { onItemClickListener.onClick(holder.transitionText) }
}
class ViewHodler(itemView: View) : RecyclerView.ViewHolder(itemView) {
val transitionText = itemView.findViewById<TextView>(R.id.item_test_text)
}
}
in onItemClick I pass the textView form item in recyclerView for transition
transition end
class TransitionEndFragment : Fragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
setUpTransition()
return inflater.inflate(R.layout.fragment_transition_end, container, false)
}
private fun setUpTransition(){
sharedElementEnterTransition = TransitionInflater.from(context).inflateTransition(android.R.transition.move)
}
}
layout
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:id="#+id/transition_end_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="transition"
android:transitionName="transitionTextEnd"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
fun transitionWithTextViewInFragment() - has transition.
fun transitionWithTextViewInRecyclerViewItem(view: View) - no transition.
To solve the return transition problem you need to add this lines on the Source Fragment (the fragment with the recycler view) where you initialize your recycler view
// your recyclerView
recyclerView.apply {
...
adapter = myAdapter
postponeEnterTransition()
viewTreeObserver
.addOnPreDrawListener {
startPostponedEnterTransition()
true
}
}
Here is my example with RecyclerView that have fragment shared transition.
In my adapter i am setting different transition name for each item based on position(In my example it is ImageView).
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = items[position]
holder.itemView.txtView.text=item
ViewCompat.setTransitionName(holder.itemView.imgViewIcon, "Test_$position")
holder.setClickListener(object : ViewHolder.ClickListener {
override fun onClick(v: View, position: Int) {
when (v.id) {
R.id.linearLayout -> listener.onClick(item, holder.itemView.imgViewIcon, position)
}
}
})
}
And when clicking on item, my interface that implemented in source fragment:
override fun onClick(text: String, img: ImageView, position: Int) {
val action = MainFragmentDirections.actionMainFragmentToSecondFragment(text, position)
val extras = FragmentNavigator.Extras.Builder()
.addSharedElement(img, ViewCompat.getTransitionName(img)!!)
.build()
NavHostFragment.findNavController(this#MainFragment).navigate(action, extras)
}
And in my destination fragment:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
info("onCreate")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
sharedElementEnterTransition = TransitionInflater.from(context).inflateTransition(android.R.transition.move)
}
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
info("onCreateView")
return inflater.inflate(R.layout.fragment_second, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
info("onViewCreated")
val name=SecondFragmentArgs.fromBundle(arguments).name
val position=SecondFragmentArgs.fromBundle(arguments).position
txtViewName.text=name
ViewCompat.setTransitionName(imgViewSecond, "Test_$position")
}
Faced the same issue as many on SO with the return transition but for me the root cause of the problem was that Navigation currently only uses replace for fragment transactions and it caused my recycler in the start fragment to reload every time you hit back which was a problem by itself.
So by solving the second (root) problem the return transition started to work without delayed animations. For those of you who are looking to keep the initial state when hitting back here is what I did :
just adding a simple check in onCreateView as so
private lateinit var binding: FragmentSearchResultsBinding
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return if (::binding.isInitialized) {
binding.root
} else {
binding = DataBindingUtil.inflate(inflater, R.layout.fragment_search_results, container, false)
with(binding) {
//doing some stuff here
root
}
}
So triple win here: recycler is not redrawn, no refetching from server and also return transitions are working as expected.
I have managed return transitions to work.
Actually this is not a bug in Android and not a problem with setReorderingAllowed = true. What happens here is the original fragment (to which we return) trying to start transition before its views/data are settled up.
To fix this we have to use postponeEnterTransition() and startPostponedEnterTransition().
For example:
Original fragment:
class FragmentOne : Fragment(R.layout.f1) {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
postponeEnterTransition()
val items = listOf("one", "two", "three", "four", "five")
.zip(listOf(Color.RED, Color.GRAY, Color.GREEN, Color.BLUE, Color.YELLOW))
.map { Item(it.first, it.second) }
val rv = view.findViewById<RecyclerView>(R.id.rvItems)
rv.adapter = ItemsAdapter(items) { item, view -> navigateOn(item, view) }
view.doOnPreDraw { startPostponedEnterTransition() }
}
private fun navigateOn(item: Item, view: View) {
val extras = FragmentNavigatorExtras(view to "yura")
findNavController().navigate(FragmentOneDirections.toTwo(item), extras)
}
}
Next fragment:
class FragmentTwo : Fragment(R.layout.f2) {
val item: Item by lazy { arguments?.getSerializable("item") as Item }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
sharedElementEnterTransition =
TransitionInflater.from(context).inflateTransition(android.R.transition.move)
val tv = view.findViewById<TextView>(R.id.tvItemId)
with(tv) {
text = item.id
transitionName = "yura"
setBackgroundColor(item.color)
}
}
}
For more details and deeper explanation see:
https://issuetracker.google.com/issues/118475573
and
https://chris.banes.dev/2018/02/18/fragmented-transitions/
Android material design library contains MaterialContainerTransform class which allows to easily implement container transitions including transitions on recycler-view items. See container transform section for more details.
Here's an example of such a transition:
// FooListFragment.kt
class FooListFragment : Fragment() {
...
private val itemListener = object : FooListener {
override fun onClick(item: Foo, itemView: View) {
...
val transitionName = getString(R.string.foo_details_transition_name)
val extras = FragmentNavigatorExtras(itemView to transitionName)
navController.navigate(directions, extras)
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// Postpone enter transitions to allow shared element transitions to run.
// https://github.com/googlesamples/android-architecture-components/issues/495
postponeEnterTransition()
view.doOnPreDraw { startPostponedEnterTransition() }
...
}
// FooDetailsFragment.kt
class FooDetailsFragment : Fragment() {
...
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
sharedElementEnterTransition = MaterialContainerTransform().apply {
duration = 1000
}
}
}
And don't forget to add unique transition names to the views:
<!-- foo_list_item.xml -->
<LinearLayout ...
android:transitionName="#{#string/foo_item_transition_name(foo.id)}">...</LinearLayout>
<!-- fragment_foo_details.xml -->
<LinearLayout ...
android:transitionName="#string/foo_details_transition_name">...</LinearLayout>
<!-- strings.xml -->
<resources>
...
<string name="foo_item_transition_name" translatable="false">foo_item_transition_%1$s</string>
<string name="foo_details_transition_name" translatable="false">foo_details_transition</string>
</resources>
The full sample is available on GitHub.
You can also take a look at Reply - an official android material sample app where a similar transition is implemented, see HomeFragment.kt & EmailFragment.kt. There's a codelab describing the process of implementing transitions in the app, and a video tutorial.

Categories

Resources