I have a single activity with many fragments (Using jetpack navigation). On my first fragment, i have a recyclerview. If i scroll on the first fragment and then navigate to the other fragment, the fragment retains the scroll position and i don't want that. An example is as follows:
i.e. Suppose i have two fragments A and B, When my app starts it starts on A. Suppose i start scrolling on A and then navigate to B. My app retains the scroll position on B which is not what i want. I want fragment B to start on top. And then when it returns to fragment A, i want it to retain the scroll position it previously scrolled.
Fragment A.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">
<data>
<import type="android.view.View" />
<variable
name="ViewModel"
type="....AccountViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="#+id/Layout_Fragment_Account"
android:layout_width="match_parent"
android:layout_height="match_parent">
<!--
Recyclerview
-->
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/RecyclerView_Account"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="#{ViewModel.accountListVisibility? View.VISIBLE : View.GONE}"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:visibility="gone" />
<!--
Empty Views and group
-->
<androidx.constraintlayout.widget.Group
android:id="#+id/Empty_View"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="#{ViewModel.accountEmptyViewVisibility?
View.VISIBLE : View.GONE}"
app:constraint_referenced_ids="Empty_View_Illustration,Empty_View_Title,Empty_View_Subtitle" />
<ImageView
android:id="#+id/Empty_View_Illustration"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:scaleType="centerCrop"
app:layout_constraintBottom_toTopOf="#+id/Empty_View_Title"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed"
app:srcCompat="#drawable/il_account" />
<TextView
android:id="#+id/Empty_View_Title"
style="#style/Locky.Text.Title6"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginEnd="32dp"
android:text="#string/text_title_emptyView_accounts"
android:textAlignment="center"
app:layout_constraintBottom_toTopOf="#id/Empty_View_Subtitle"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#+id/Empty_View_Illustration"
app:layout_constraintVertical_chainStyle="packed"
app:layout_constraintWidth_default="percent"
app:layout_constraintWidth_percent=".8" />
<TextView
android:id="#+id/Empty_View_Subtitle"
style="#style/Locky.Text.Subtitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginStart="32dp"
android:layout_marginEnd="32dp"
android:layout_marginBottom="?attr/actionBarSize"
android:text="#string/text_subtitle_emptyView_accounts"
android:textAlignment="center"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.6"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#+id/Empty_View_Title"
app:layout_constraintWidth_default="percent"
app:layout_constraintWidth_percent=".8" />
<!--
Progress Bar
-->
<include
android:id="#+id/Progress_Bar"
layout="#layout/custom_view_list_loading"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="#{ViewModel.loadingStatus? View.VISIBLE : View.GONE}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
Fragment A.kt:
class AccountFragment : Fragment() {
private var _binding: FragmentAccountBinding? = null
private var _viewModel: AccountViewModel? = null
private var _lastClickTime: Long = 0
private val binding get() = _binding!!
private val viewModel get() = _viewModel!!
companion object {
const val TAG = "ACCOUNT_FRAGMENT_DEBUG"
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
_binding = FragmentAccountBinding.inflate(inflater, container, false)
// Fetch view model
_viewModel = ViewModelProvider(this).get(AccountViewModel::class.java)
//Bind view model to layout
binding.viewModel = _viewModel
// Bind lifecycle owner
binding.lifecycleOwner = this
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
/* Hides the soft keyboard */
hideSoftKeyboard(binding.root)
/* Observe snack bar events */
observeSnackBarEvent()
/* Observe the account list changes */
observeAccounts()
/* Observe back stack entry result after navigating from sort sheet */
observeBackStackEntryForSortSheet()
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setHasOptionsMenu(true)
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.menu_toolbar_filter, menu)
super.onCreateOptionsMenu(menu, inflater)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.Toolbar_Filter -> {
navigateToSort()
true
}
else -> false
}
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
/*
* My Functions
*/
private fun observeBackStackEntryForSortSheet() {
val navController = findNavController()
// After a configuration change or process death, the currentBackStackEntry
// points to the dialog destination, so you must use getBackStackEntry()
// with the specific ID of your destination to ensure we always
// get the right NavBackStackEntry
val navBackStackEntry = navController.getBackStackEntry(R.id.Fragment_Account)
// Create our observer and add it to the NavBackStackEntry's lifecycle
val observer = LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_RESUME
&& navBackStackEntry.savedStateHandle.contains(KEY_ACCOUNTS_SORT)
) {
viewModel.sortChange(
navBackStackEntry.savedStateHandle.get<AccountSort>(
KEY_ACCOUNTS_SORT
)!!
)
navBackStackEntry.savedStateHandle.remove<AccountSort>(KEY_ACCOUNTS_SORT)
}
}
navBackStackEntry.lifecycle.addObserver(observer)
// As addObserver() does not automatically remove the observer, we
// call removeObserver() manually when the view lifecycle is destroyed
viewLifecycleOwner.lifecycle.addObserver(LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_DESTROY) {
navBackStackEntry.lifecycle.removeObserver(observer)
}
})
}
private fun observeSnackBarEvent() {
viewModel.showSnackBarEvent.observe(viewLifecycleOwner, Observer {
if (it != null) {
snackBarAction(it)
}
})
}
private fun observeAccounts() {
with(viewModel) {
accounts.observe(viewLifecycleOwner, Observer {
if (it != null) {
//set loading flag to hide loading animation
doneLoading()
//Alternate visibility for account list and empty view
alternateAccountListVisibility(it.size)
//Submit the cards
initiateAccounts().submitList(it)
}
})
}
}
private fun initiateAccounts(): AccountAdapter {
val adapter = AccountAdapter(
AccountClickListener {
navigateToSelectedAccount(it)
},
AccountOptionsClickListener { view, card ->
view.apply {
isEnabled = false
}
createPopupMenu(view, card)
})
binding.RecyclerViewAccount.apply {
this.adapter = adapter
setHasFixedSize(true)
}
return adapter
}
private fun createPopupMenu(view: View, account: Account) {
requireContext().createPopUpMenu(
view,
R.menu.menu_moreoptions_account,
PopupMenu.OnMenuItemClickListener {
when (it.itemId) {
R.id.Menu_CopyUsername -> copyToClipboardAndToast(account.username)
R.id.Menu_CopyPass -> copyToClipboardAndToast(account.password)
R.id.Menu_ShowPass -> triggerSnackBarEvent(account.password)
else -> false
}
}, PopupMenu.OnDismissListener {
view.apply {
isEnabled = true
}
})
}
private fun navigateToSort() {
if (SystemClock.elapsedRealtime() - _lastClickTime >= 800) {
_lastClickTime = SystemClock.elapsedRealtime()
navigateTo(AccountFragmentDirections.actionFragmentAccountToBottomSheetFragmentAccountFilter())
}
}
private fun navigateToSelectedAccount(account: Account) {
navigateTo(
AccountFragmentDirections.actionFragmentAccountToFragmentViewAccount(
account
)
)
}
private fun snackBarAction(message: String) {
binding.LayoutFragmentAccount.snackbar(message) {
action(getString(R.string.button_snack_action_close)) { dismiss() }
}
viewModel.doneShowingSnackBar()
}
private fun triggerSnackBarEvent(message: String): Boolean {
viewModel.setSnackBarMessage(message)
return true
}
private fun copyToClipboardAndToast(message: String): Boolean {
copyToClipboard(message)
toast(getString(R.string.message_copy_successful))
return true
}
Fragment B.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">
<data>
<variable
name="Account"
type="....Account" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="#+id/Layout_Credential_View"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="#+id/Account_Logo"
imageUrl="#{Account.logoUrl}"
loadingResource="#{#drawable/ic_image_loading}"
errorResource="#{#drawable/ic_account_placeholder}"
android:layout_width="100dp"
android:layout_height="100dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:src="#drawable/ic_account_placeholder" />
<TextView
android:id="#+id/Account_Name"
style="#style/Locky.Text.Title5.Name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="32dp"
android:textAlignment="center"
android:text="#{Account.accountName}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#+id/Account_Logo"
tools:text="This can be a very very very long title toooooo" />
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/RecyclerView_Credentials_Field"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="24dp"
android:layout_marginEnd="8dp"
android:nestedScrollingEnabled="true"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#+id/Account_Name" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
Fragment B.kt
class ViewAccountFragment : Fragment() {
private var _binding: FragmentViewAccountBinding? = null
private var _viewModel: ViewAccountViewModel? = null
private lateinit var _account: Account
private val binding get() = _binding!!
private val viewModel get() = _viewModel!!
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
//Fetch the layout and do the binding
_binding = FragmentViewAccountBinding.inflate(inflater, container, false)
//Instantiate view model
_viewModel = ViewModelProvider(this).get(ViewAccountViewModel::class.java)
binding.lifecycleOwner = this
//Fetch the account clicked on the previous screen
_account = ViewAccountFragmentArgs.fromBundle(requireArguments()).parcelcredaccount
with(_account) {
//Bind the account to the layout for displaying
binding.account = this
//Submit the account details to the recyclerview
initiateCredentialsFieldList().submitList(viewModel.fieldList(this))
}
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
/* Hides the soft keyboard */
hideSoftKeyboard(binding.root)
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setHasOptionsMenu(true)
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.menu_credentials_actions, menu)
super.onCreateOptionsMenu(menu, inflater)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.Action_Duplicate -> {
/*
* We set the account id to empty here
* When the add screen receives it, it wil perceive it as a new account that needs to be
* added to the database
*/
navigateToEditScreen(_account.copy(accountID = generateUniqueID()))
true
}
R.id.Action_Edit -> {
navigateToEditScreen(_account)
true
}
R.id.Action_Delete -> {
deleteConfirmationDialog(_account.accountName)
true
}
else -> false
}
}
private fun initiateCredentialsFieldList(): CredentialsViewAdapter {
val credentialsAdapter =
CredentialsViewAdapter(
CopyClickListener { data ->
copyToClipboardAndToast(data)
},
ViewClickListener { data ->
snackBarAction(data)
})
binding.RecyclerViewCredentialsField.apply {
adapter = credentialsAdapter
setHasFixedSize(true)
}
return credentialsAdapter
}
private fun deleteAndNavigateBackToAccountList() {
with(_account) {
viewModel.delete(accountID)
toast(getString(R.string.message_credentials_deleted, accountName))
findNavController().popBackStack()
}
}
navigation.xml
<fragment
android:id="#+id/Fragment_Account"
android:name="....AccountFragment"
android:label="Accounts"
tools:layout="#layout/fragment_account">
<action
android:id="#+id/action_Fragment_Account_to_Fragment_View_Account"
app:destination="#id/Fragment_View_Account" />
<action
android:id="#+id/action_Fragment_Account_to_BottomSheet_Fragment_Account_Filter"
app:destination="#id/BottomSheet_Fragment_Account_Filter" />
</fragment>
MainActivity.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.drawerlayout.widget.DrawerLayout
android:id="#+id/Drawer_Main"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.main.main.MainActivity">
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:id="#+id/Layout_Coordinator_Main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true">
<com.google.android.material.appbar.MaterialToolbar
android:id="#+id/Toolbar_Main"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="#color/colorOnSurface"
android:outlineAmbientShadowColor="#color/colorShadowColor"
android:outlineSpotShadowColor="#color/colorShadowColor">
<TextView
android:id="#+id/Toolbar_Main_Title"
style="#style/Locky.Text.Title6.Toolbar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="#string/app_name" />
</com.google.android.material.appbar.MaterialToolbar>
<androidx.core.widget.NestedScrollView
android:id="#+id/Nested_Scroll"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="?attr/actionBarSize"
android:fillViewport="true">
<fragment
android:id="#+id/Navigation_Host"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNavHost="true"
app:navGraph="#navigation/navigation_drawer_main" />
</androidx.core.widget.NestedScrollView>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="#+id/FAB_Search"
style="#style/Locky.FloatingActionButton.Mini"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="0dp"
android:layout_marginBottom="85dp"
app:layout_anchor="#id/FAB_Add"
app:layout_anchorGravity="top|center_horizontal"
app:layout_behavior="com.google.android.material.behavior.HideBottomViewOnScrollBehavior"
app:srcCompat="#drawable/ic_search" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="#+id/FAB_Add"
style="#style/Locky.FloatingActionButton.Normal"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="#dimen/fab_margin"
app:layout_behavior="com.google.android.material.behavior.HideBottomViewOnScrollBehavior"
app:srcCompat="#drawable/ic_add" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
<com.google.android.material.navigation.NavigationView
android:id="#+id/Navigation_View"
style="#style/Locky.Widget.Custom.NavigationView"
android:layout_width="280dp"
android:layout_height="match_parent"
android:layout_gravity="start"
android:clipToPadding="false"
android:paddingStart="0dp"
android:paddingEnd="16dp"
app:headerLayout="#layout/drawer_header"
app:itemTextAppearance="#style/Locky.Text.Body.Drawer"
app:menu="#menu/menu_drawer_main" />
</androidx.drawerlayout.widget.DrawerLayout>
MainActivity.kt
class MainActivity : AppCompatActivity() {
private lateinit var _binding: ActivityMainBinding
private lateinit var _viewModel: MainActivityViewModel
private lateinit var _appBarConfiguration: AppBarConfiguration
//Fragments that can navigate with the drawer
private val _navigationFragments = setOf(
R.id.Fragment_Card,
R.id.Fragment_Account,
R.id.Fragment_Device
)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
_binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
_viewModel = ViewModelProvider(this).get(MainActivityViewModel::class.java)
_binding.lifecycleOwner = this
//Set the support action bar to the toolbar
setSupportActionBar(_binding.ToolbarMain)
//Remove the default actionbar title
supportActionBar?.setDisplayShowTitleEnabled(false)
/* Updates the app settings*/
updateAppSettings()
//Setup the navigation components
navigationUISetup()
//Load FABs
listenerForAddFab()
listenerForSearchFab()
//Scroll changes to adjust toolbar elevation accordingly
setUpNestedScrollChangeListener()
}
override fun onOptionsItemSelected(item: MenuItem) =
item.onNavDestinationSelected(findNavController(R.id.Navigation_Host)) || super.onOptionsItemSelected(
item
)
override fun onSupportNavigateUp() =
findNavController(R.id.Navigation_Host).navigateUp(_appBarConfiguration)
override fun finish() {
super.finish()
ActivityNavigator.applyPopAnimationsToPendingTransition(this)
}
private fun navigationUISetup() {
//Fetch the Nav Controller
val navController = findNavController(R.id.Navigation_Host)
//Setup the App Bar Configuration
_appBarConfiguration = AppBarConfiguration(_navigationFragments, _binding.DrawerMain)
//Use Navigation UI to setup the app bar config and navigation view
NavigationUI.setupActionBarWithNavController(this, navController, _appBarConfiguration)
NavigationUI.setupWithNavController(_binding.NavigationView, navController)
//Set the mini FABs with navigation to navigate to fragments accordingly.
Navigation.setViewNavController(_binding.FABAdd, navController)
Navigation.setViewNavController(_binding.FABSearch, navController)
//Add on change destination listener to navigation controller to handle fab visibility
navigationDestinationChangeListener_FAB(navController)
//Add on change destination listener to navigation controller to handle screen title visibility
navigationDestinationChangeListener_ToolbarTitle(navController)
}
private fun setUpNestedScrollChangeListener() =
_binding.NestedScroll.setOnScrollChangeListener { _, _, scrollY, _, _ ->
if (scrollY > 0) {
_binding.ToolbarMain.elevation = 12F
} else {
_binding.ToolbarMain.elevation = 0F
}
}
private fun navigationDestinationChangeListener_ToolbarTitle(navController: NavController) {
navController.addOnDestinationChangedListener { _, nd, _ ->
when (nd.id) {
R.id.Fragment_Account -> updateToolbar(getString(R.string.text_title_screen_accounts))
R.id.Fragment_Card -> updateToolbar(getString(R.string.text_title_screen_cards))
R.id.Fragment_Device -> updateToolbar(getString(R.string.text_title_screen_devices))
R.id.Fragment_Settings -> updateToolbar(getString(R.string.text_title_screen_settings))
R.id.Fragment_Profile -> updateToolbar(getString(R.string.text_title_screen_profile))
R.id.Fragment_About -> updateToolbar(getString(R.string.text_title_screen_about))
R.id.Fragment_Donate -> updateToolbar(getString(R.string.text_title_screen_donate))
else -> {
//Show the toolbar
updateToolbar(null)
}
}
}
}
private fun navigationDestinationChangeListener_FAB(navController: NavController) {
navController.addOnDestinationChangedListener { nc, nd, _ ->
when (nd.id) {
nc.graph.startDestination,
R.id.Fragment_Card,
R.id.Fragment_Device -> {
_binding.DrawerMain.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED)
//Show all the FABs
showFABs()
}
else -> {
_binding.DrawerMain.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED)
//Hide all the FABs
hideFABs()
}
}
}
}
private fun getFadeNavOptions(): NavOptions? {
return NavOptions.Builder()
.setEnterAnim(R.anim.anim_fade_in)
.setExitAnim(R.anim.anim_fade_out)
.build()
}
private fun hideFABs() {
_binding.FABSearch.hide()
_binding.FABAdd.hide()
}
private fun showFABs() {
_binding.FABSearch.show()
_binding.FABAdd.show()
showFABFromSlidingBehavior(_binding.FABSearch, _binding.FABSearch.isVisible)
showFABFromSlidingBehavior(_binding.FABAdd, _binding.FABAdd.isVisible)
}
private fun showFABFromSlidingBehavior(fab: FloatingActionButton, isVisible: Boolean) {
val layoutParams: ViewGroup.LayoutParams = fab.layoutParams
if (layoutParams is CoordinatorLayout.LayoutParams) {
val behavior = layoutParams.behavior
if (behavior is HideBottomViewOnScrollBehavior) {
if (isVisible) {
behavior.slideUp(fab)
} else {
behavior.slideDown(fab)
}
}
}
}
I have attached a gif to demontstrate the issue here:
In the GIF i navigate from 3 fragments (Fragment A > Fragment B > Fragment C)
Is there anything i am doing wrong here ?
you have the same layoutmanager for both fragments, when you populate your different fragments; the same layoutmanager is called. Which then tries to restore the same position thinking its the same recyclerview, which is kind of a feature when you think about it.
from the docs:
Called when the RecyclerView is ready to restore the state based on a
previous RecyclerView. Notice that this might happen after an actual
layout, based on how Adapter prefers to restore State. See
RecyclerView.Adapter.getStateRestorationPolicy()
which means what we need is not to restore the state which can be done by passing
PREVENT to RecyclerView.Adapter.StateRestorationPolicy
solution1: in your fragment B adapter just call adapter.stateRestorationPolicy = PREVENT
solution2: create a different layoutmanager for fragment B in case you want to restore position for something else.
EDIT :: QA :: how can i set the view to be on top (Near Status Bar) :
Well, since you are populating your fragments inside a NestedScrollView you should call NestedScrollView.scrollTo(0, 0); when you navigate to the required fragment probably by waiting on a callback from addOnDestinationChangedListener inside your MainActivity.kt
Related
When trying to swipe between any of my viewpager2 fragments, the view gets stuck at 75% of the transition.
Picture
Swiping from first fragment to second fragment:
Swiping from second fragment to first fragment:
Viewpager_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">
<androidx.viewpager2.widget.ViewPager2
android:id="#+id/viewPager"
android:layout_width="match_parent"
android:layout_height="0dp"
android:orientation="horizontal"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.tabs.TabLayout
android:id="#+id/tl_on_boarding_item"
style="#style/ShapeAppearanceOverlay.EXAMPLE.ShopTablayout"
android:layout_width="0dp"
android:layout_height="16dp"
android:layout_marginBottom="24dp"
android:background="#android:color/transparent"
app:layout_constraintBottom_toBottomOf="#+id/viewPager"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:tabBackground="#drawable/unselected_shop_item_tab"
app:tabIndicator="#drawable/selected_on_boarding_item_tab"
app:tabIndicatorColor="#color/primary"
app:tabIndicatorFullWidth="false"
app:tabIndicatorGravity="center"
app:tabIndicatorHeight="8dp"
app:tabMaxWidth="16dp" />
</androidx.constraintlayout.widget.ConstraintLayout>
Viewpager_Fragment
#AndroidEntryPoint
class OnBoardingFragmentHolder #Inject constructor(
private val onBoardingViewPagerAdapter: OnBoardingViewPagerAdapter,
private val mediator: TabLayoutHelper,
) : InvisibleBottomNavFragment<FragmentOnBoardingHolderBinding>() {
override val bindingInflater: (LayoutInflater) -> FragmentOnBoardingHolderBinding
get() = FragmentOnBoardingHolderBinding::inflate
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
initTablayout()
}
private fun initTablayout() {
binding.viewPager.adapter = onBoardingViewPagerAdapter
binding.viewPager.offscreenPageLimit = 2
mediator.init(binding.tlOnBoardingItem, binding.viewPager)
}
override val onDestroyView: () -> Unit
get() = {
binding.viewPager.adapter = null
mediator.onDestroyView()
}
}
InvisibleBottom Fragment
abstract class InvisibleBottomNavFragment<out T : ViewBinding> : BindingFragment<T>() {
override fun onAttach(context: Context) {
super.onAttach(context)
postStickyEvent(MainActivityBusEventBottomNav(toBeClosed = true,
fromClass = "InvisibleBottomNavFragment.kt"))
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
postStickyEvent(MainActivityBusEventBottomNav(toBeClosed = true,
fromClass = "InvisibleBottomNavFragment.kt"))
}
override fun onResume() {
super.onResume()
postStickyEvent(MainActivityBusEventBottomNav(toBeClosed = true,
fromClass = "InvisibleBottomNavFragment.kt"))
}
}
Viewpager
class OnBoardingViewPagerAdapter #Inject constructor(
fragment: Fragment
) : FragmentStateAdapter(fragment) {
private companion object {
private const val FRAGMENT_ITEM_COUNT = 3
}
override fun getItemCount(): Int = FRAGMENT_ITEM_COUNT
override fun createFragment(position: Int): Fragment = when (position) {
0 -> OnBoardingFirstFragment()
1 -> OnBoardingSecondFragment()
2 -> OnBoardingThirdFragment()
else -> throw IllegalStateException("$position can not be satisfied")
}
}
What I find most weird is that when I fast swipe between the fragments (e.g not holding the mouse button up to the end and just swiping) everything works fine.
I want to achieve a layout as shown in the Android Studio preview (left). However if executed in the emulator, only the button is visible and the RecyclerView is not visible/populated (right).
The XML code:
<?xml version="1.0" encoding="utf-8"?>
<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"
style="#style/AppTheme">
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/list"
android:name="com.example.app.ItemFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
app:layoutManager="LinearLayoutManager"
tools:context=".ItemFragment"
tools:listitem="#layout/fragment_item"
android:scrollbars="vertical"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="#+id/floatingActionButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|center_horizontal"
android:layout_margin="#dimen/fab_margin"
android:clickable="true"
android:focusable="true"
android:src="#android:drawable/ic_input_add"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
However if the RecyclerView is alone in the fragment the list is populated (but of course the action button is not showing). Code see below. And yes, my list which should be shown is not empty.
<?xml version="1.0" encoding="utf-8"?>
<androidx.recyclerview.widget.RecyclerView 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:id="#+id/list"
android:name="com.example.app.ItemFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
app:layoutManager="LinearLayoutManager"
tools:context=".ItemFragment"
tools:listitem="#layout/fragment_item" >
</androidx.recyclerview.widget.RecyclerView>
I have already tried using RelativeLayout and FrameLayout, but I still get the same result. The same behavior occurs if I use e.g. a TextView instead of the action button.
--- Requested additional info ---
Adapter class (automatically generated by Android Studio, template):
class MyItemRecyclerViewAdapter(
private val mValues: List<DummyItem>,
private val mListener: OnListFragmentInteractionListener?
) : RecyclerView.Adapter<MyItemRecyclerViewAdapter.ViewHolder>() {
private val mOnClickListener: View.OnClickListener
init {
mOnClickListener = View.OnClickListener { v ->
val item = v.tag as DummyItem
// Notify the active callbacks interface (the activity, if the fragment is attached to
// one) that an item has been selected.
mListener?.onListFragmentInteraction(item)
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.fragment_item, parent, false)
return ViewHolder(view)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = mValues[position]
holder.mIdView.text = item.id
holder.mContentView.text = item.content
with(holder.mView) {
tag = item
setOnClickListener(mOnClickListener)
}
}
override fun getItemCount(): Int = mValues.size
inner class ViewHolder(val mView: View) : RecyclerView.ViewHolder(mView) {
val mIdView: TextView = mView.item_number
val mContentView: TextView = mView.content
override fun toString(): String {
return super.toString() + " '" + mContentView.text + "'"
}
}
}
List fragment (automatically generated by Android Studio, template):
class ItemFragment : Fragment() {
// TODO: Customize parameters
private var columnCount = 1
private var listener: OnListFragmentInteractionListener? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
arguments?.let {
columnCount = it.getInt(ARG_COLUMN_COUNT)
}
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val view = inflater.inflate(R.layout.fragment_item_list, container, false)
// Set the adapter
if (view is RecyclerView) {
with(view) {
layoutManager = when {
columnCount <= 1 -> LinearLayoutManager(context)
else -> GridLayoutManager(context, columnCount)
}
adapter = MyItemRecyclerViewAdapter(DummyContent.ITEMS, listener)
}
}
return view
}
override fun onAttach(context: Context) {
super.onAttach(context)
if (context is OnListFragmentInteractionListener) {
listener = context
} else {
throw RuntimeException(context.toString() + " must implement OnListFragmentInteractionListener")
}
}
override fun onDetach() {
super.onDetach()
listener = null
}
/**
* This interface must be implemented by activities that contain this
* fragment to allow an interaction in this fragment to be communicated
* to the activity and potentially other fragments contained in that
* activity.
*
*
* See the Android Training lesson
* [Communicating with Other Fragments](http://developer.android.com/training/basics/fragments/communicating.html)
* for more information.
*/
interface OnListFragmentInteractionListener {
// TODO: Update argument type and name
fun onListFragmentInteraction(item: DummyItem?)
}
companion object {
// TODO: Customize parameter argument names
const val ARG_COLUMN_COUNT = "column-count"
// TODO: Customize parameter initialization
#JvmStatic
fun newInstance(columnCount: Int) =
ItemFragment().apply {
arguments = Bundle().apply {
putInt(ARG_COLUMN_COUNT, columnCount)
}
}
}
}
Try this for your layout
<?xml version="1.0" encoding="utf-8"?>
<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"
style="#style/AppTheme"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/list"
android:name="com.example.app.ItemFragment"
android:layout_width="0dp"
android:layout_height="0dp"
android:scrollbars="vertical"
app:layoutManager="LinearLayoutManager"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:context=".ItemFragment"
tools:listitem="#layout/fragment_item" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="#+id/floatingActionButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|center_horizontal"
android:layout_margin="#dimen/fab_margin"
android:clickable="true"
android:focusable="true"
android:src="#android:drawable/ic_input_add"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
Inside ItemFragment.kt replace onCreateView with
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_item_list, container, false)
}
After that go ahead and implement onViewCreated as such
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
with(list) {
layoutManager = when {
columnCount <= 1 -> LinearLayoutManager(requireContext())
else -> GridLayoutManager(requireContext(), columnCount)
}
adapter = MyItemRecyclerViewAdapter(DummyContent.ITEMS)
}
}
This should fix your problem because the AS template is assuming that the rootview will be a RecyclerView and is treating the whole layout as such.
I see the problem with a RecyclerView in viewpager fragment and I use bottom naviagation bar. When I tab on the navigation bar to change a screen and then pressed on back button it will go to the first screen which have the viewpager but the recyclerview doesn't appear and when I tab the naviagtion bar to reselect this screen it appear.
The recyclerview is show the list from firestore database and using ViewModel.
I try to set retainInstance to true but it doesn't work. And try to setup the recyclerview in onCreate and onCreateView it doesn't work too.
Edit: this is my code.
MainActivity.kt
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val navView: BottomNavigationView = findViewById(R.id.nav_view)
val navController = findNavController(R.id.nav_host_fragment)
navView.setupWithNavController(navController)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId){
android.R.id.home -> {
onBackPressed()
return true
}
}
return super.onOptionsItemSelected(item)
}
}
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="#+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="#+id/nav_view"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="0dp"
android:layout_marginEnd="0dp"
android:background="#drawable/bg_nav"
app:itemIconTint="#color/navbar_color"
app:itemTextColor="#color/navbar_color"
app:itemTextAppearanceActive="#style/navbar_font"
app:itemTextAppearanceInactive="#style/navbar_font"
app:itemBackground="#drawable/nav_ripple"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:menu="#menu/bottom_nav_menu"/>
<fragment
android:id="#+id/nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="0dp"
android:layout_height="0dp"
app:defaultNavHost="true"
app:layout_constraintBottom_toTopOf="#id/nav_view"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:navGraph="#navigation/mobile_navigation" />
</androidx.constraintlayout.widget.ConstraintLayout>
HomeFragement.kt (it contain tablayout and viewpager)
class HomeFragment : Fragment() {
private lateinit var homeViewModel: HomeViewModel
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
homeViewModel =
ViewModelProviders.of(this).get(HomeViewModel::class.java)
val root = inflater.inflate(R.layout.fragment_home, container, false)
return root
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
setupTab()
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
}
private fun setupTab(){
Log.d("SET UP TAB", "SETTING UP")
val pageTabAdapter = PageTabAdapter(activity!!.supportFragmentManager, activity_tab.tabCount)
view_activity.adapter = pageTabAdapter
view_activity.addOnPageChangeListener(TabLayout.TabLayoutOnPageChangeListener(activity_tab))
activity_tab.setOnTabSelectedListener(object : TabLayout.OnTabSelectedListener{
override fun onTabReselected(p0: TabLayout.Tab?) {
}
override fun onTabUnselected(p0: TabLayout.Tab?) {
}
override fun onTabSelected(p0: TabLayout.Tab?) {
view_activity.currentItem = p0!!.position
}
})
}
}
fragment_home.xml
<?xml version="1.0" encoding="utf-8"?>
<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:background="#android:color/white"
android:orientation="vertical">
<TextView
android:id="#+id/home_header"
style="#style/page_header"
android:text="#string/title_home"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
<Button
android:id="#+id/calendar_btn"
android:layout_width="60dp"
android:layout_height="40dp"
android:background="#drawable/btn_calendar"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="#id/home_header"
app:layout_constraintBottom_toBottomOf="#id/home_header"
android:layout_marginEnd="10dp"/>
<com.google.android.material.tabs.TabLayout
android:id="#+id/activity_tab"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:tabRippleColor="#color/colorPrimary"
app:tabTextAppearance="#style/tab_font"
app:tabIndicatorHeight="2.5dp"
app:tabGravity="fill"
app:layout_constraintTop_toBottomOf="#id/home_header"
app:layout_constraintStart_toStartOf="parent"
android:background="#android:color/white">
<com.google.android.material.tabs.TabItem
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="กิจกรรมในเดือนนี้" />
<com.google.android.material.tabs.TabItem
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="กิจกรรมที่ผ่านไปแล้ว" />
</com.google.android.material.tabs.TabLayout>
<androidx.viewpager.widget.ViewPager
android:id="#+id/view_activity"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintTop_toBottomOf="#id/activity_tab"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
PageAdapter.kt
class PageTabAdapter(private val fragmentManager: FragmentManager, private val anInt: Int) :
FragmentStatePagerAdapter(fragmentManager) {
override fun getItem(position: Int): Fragment {
return when (position) {
0 -> {
NewActivity.newInstance()
}
1 -> {
PastActivity.newInstance()
}
else -> {
null!!
}
}
}
override fun getCount(): Int {
return anInt
}
}
ActivityAdapter.kt (this is a recyclerview adapter)
class ActivityAdapter(
private val mContext: Context, private val mItems: List<ActivityModel>
): RecyclerView.Adapter<ActivityAdapter.Holder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Holder {
val inflater = LayoutInflater.from(this.mContext)
val view = inflater.inflate(R.layout.activity_list, parent, false)
return Holder(view)
}
override fun getItemCount(): Int {
return this.mItems.size
}
override fun onBindViewHolder(holder: Holder, position: Int) {
val item = this.mItems[position]
holder.setName(item.name)
val date = item.date?.toDate()
val dateFormat: DateFormat = SimpleDateFormat("dd-MMM-yyyy")
val newDate = dateFormat.format(date).toString()
holder.setDate(newDate)
val period = item.start + " - " +item.end
holder.setPeriod(period)
holder.setButton(item)
}
inner class Holder(view: View): RecyclerView.ViewHolder(view){
private var activityName: TextView = view.activity_list_name
private var activityDate: TextView = view.activity_list_date
private var activityPeriod: TextView = view.activity_list_period
private var activityPicture: ImageView = view.activity_list_pic
private var activityButton: Button = view.activity_list_btn
fun setName(name: String?){
this.activityName.text = name
}
fun setDate(date: String?){
this.activityDate.text = date
}
fun setPeriod(period: String?){
this.activityPeriod.text = period
}
fun setPicture(picpath: String?){
}
fun setButton(data: ActivityModel){
}
}
}
}
PS.: I'm using Kotlin in my project
Thank you
I have these activity, fragments, its viewmodels, and their adapter. I can already call the next fragment on click of a recyclerview item, but the new fragment overlays on the first fragment.
Refer to screenshot below:
Next screenshot is the old fragment view:
As for the mainactivity:
class MainActivity : AppCompatActivity(), RecyclerViewClickListener {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val navView: BottomNavigationView = findViewById(R.id.nav_view)
val navController = findNavController(R.id.nav_host_fragment)
// Passing each menu ID as a set of Ids because each
// menu should be considered as top level destinations.
val appBarConfiguration = AppBarConfiguration(
setOf(
R.id.navigation_home,
R.id.navigation_messages,
R.id.navigation_notifications,
R.id.navigation_account
)
)
setupActionBarWithNavController(navController, appBarConfiguration)
navView.setupWithNavController(navController)
if (savedInstanceState == null) {
supportFragmentManager
.beginTransaction()
.add(R.id.fragment_home, HomeFragment.newInstance(), "dormList")
.commit()
}
}
override fun onRecyclerViewItemClick(view: View, dorms: Dorms) {
val detailsFragment = dormDetailsFragment.newInstance(dorms)
supportFragmentManager
.beginTransaction()
.replace(R.id.fragment_home, detailsFragment, "Dorm Details")
.addToBackStack(null)
.commit()
}
}
HomeFragment:
class HomeFragment : Fragment(), RecyclerViewClickListener {
private lateinit var factory: HomeViewModelFactory
private lateinit var viewModel: HomeViewModel
private var callback : RecyclerViewClickListener? = null
companion object {
fun newInstance(): HomeFragment {
return HomeFragment()
}
}
override fun onAttach(context: Context) {
super.onAttach(context)
if(context is RecyclerViewClickListener) callback = context
else throw ClassCastException("$context must implement Callback")
}
override fun onDetach() {
super.onDetach()
callback = null
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_home, container, false)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
val api = DormsAPI()
val repository = DormRepository(api)
factory = HomeViewModelFactory(repository)
viewModel = ViewModelProviders.of(this, factory).get(HomeViewModel::class.java)
viewModel.getDorms()
viewModel.dorms.observe(viewLifecycleOwner, Observer { dorms ->
recyclerViewDorms.also{
it.layoutManager = LinearLayoutManager(requireContext())
it.setHasFixedSize(true)
it.adapter = dormAdapter(dorms, this)
}
})
}
override fun onRecyclerViewItemClick(view: View, dorms: Dorms) {
when(view.id){
R.id.button_reserve -> {
// TODO: Go to new account if not signed up, etc...
Toast.makeText(requireContext(), "Reserve button clicked", Toast.LENGTH_LONG).show()
}
R.id.layoutBox -> {
// TODO: Go to Dorm Details
callback?.onRecyclerViewItemClick(view, dorms)
}
}
}
}
Home View Model
class HomeViewModel(private val repository: DormRepository) : ViewModel() {
private lateinit var job: Job
private val _dorms = MutableLiveData<List<Dorms>>()
val dorms: LiveData<List<Dorms>>
get() = _dorms
fun getDorms() {
job = Coroutines.ioThenMain(
{ repository.getDorms() },
{ _dorms.value = it }
)
}
override fun onCleared() {
super.onCleared()
if(::job.isInitialized) job.cancel()
}
}
Interface:
interface RecyclerViewClickListener {
fun onRecyclerViewItemClick(view: View, dorms: Dorms)
}
Details Fragment:
class dormDetailsFragment : Fragment() {
companion object {
private const val DORMS = "model"
fun newInstance(dorms: Dorms): dormDetailsFragment{
val args = Bundle()
args.putSerializable(DORMS, dorms)
val fragment = dormDetailsFragment()
fragment.arguments = args
return fragment
}
}
private lateinit var viewModel: DormDetailsViewModel
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val fragmentDormDetailsBinding =
FragmentDormDetailsBinding.inflate(inflater,container,false)
val model = arguments!!.getSerializable(DORMS) as Dorms
fragmentDormDetailsBinding.dormDetails = model
return fragmentDormDetailsBinding.root
}
}
Home Fragment Layout
<?xml version="1.0" encoding="utf-8"?>
<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:id="#+id/fragment_home">
<TextView
android:id="#+id/text_home"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:textAlignment="center"
android:textSize="20sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="#+id/refreshLayout">
<androidx.recyclerview.widget.RecyclerView
tools:listitem="#layout/layout_home"
android:id="#+id/recyclerViewDorms"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintBottom_toBottomOf="parent" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
Details Layout
<?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">
<data>
<import type="android.view.View" />
<variable
name="dormDetails"
type="com.pptt.roomy.data.models.Dorms" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.pptt.roomy.ui.home.dormDetails.DormDetailsFragment"
android:id="#+id/DormDetailsFrag">
<ImageView
app:image="#{dormDetails.image}"
android:id="#+id/image"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintDimensionRatio="1:1"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
android:background="#drawable/propertysample"
/>
<TextView
android:text="#{String.valueOf(dormDetails.dormPrice)}"
tools:text="Php 2500"
android:id="#+id/textViewPrice"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingTop="6dp"
android:layout_marginStart="10dp"
android:textSize="20sp"
android:textStyle="normal"
android:textColor="#000000"
app:layout_constraintTop_toBottomOf="#id/image"
app:layout_constraintLeft_toLeftOf="parent" />
<TextView
android:text="#{dormDetails.dormName}"
tools:text="Dorm ni Jupa"
android:id="#+id/textViewPropertyName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingTop="1dp"
android:layout_marginStart="10dp"
android:textSize="24sp"
android:textStyle="bold"
android:textColor="#000000"
app:layout_constraintTop_toBottomOf="#id/textViewPrice"
app:layout_constraintLeft_toLeftOf="parent" />
<TextView
android:text="#{dormDetails.dormType}"
tools:text="1 BR with Dining and Kitchen"
android:id="#+id/textViewRoomType"
android:layout_below="#id/textViewPropertyName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingTop="5dp"
android:layout_marginLeft="40dp"
android:textSize="16sp"
app:layout_constraintTop_toBottomOf="#+id/textViewPropertyName"
app:layout_constraintLeft_toLeftOf="parent"/>
<TextView
android:text="#{dormDetails.dormAddress}"
android:id="#+id/textViewAddress"
android:layout_marginBottom="5dp"
tools:text="455 San Jose II St., Brgy. 425, Sampaloc, Manila"
android:textAppearance="#style/Base.TextAppearance.AppCompat.Small"
android:padding="5dp"
android:layout_marginLeft="40dp"
android:layout_width="wrap_content"
android:textAlignment="center"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="#+id/textViewRoomType"
app:layout_constraintLeft_toLeftOf="parent"/>
<TextView
android:text="#{dormDetails.dormDetails}"
android:id="#+id/textViewDescription"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:text="A very long textarea to contain dorm description. Should be multiline"
android:padding="5dp"
android:layout_marginLeft="20dp"
app:layout_constraintTop_toBottomOf="#id/textViewAddress"
app:layout_constraintLeft_toLeftOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
Anything else that's needed will be edited for later.
Remove these lines:
if (savedInstanceState == null) {
supportFragmentManager
.beginTransaction()
.add(R.id.fragment_home, HomeFragment.newInstance(), "dormList")
.commit()
}
You're adding one HomeFragment via the NavHostFragment and another manually. You don't need to manually add Fragment when using Navigation.
You should also be updating your onRecyclerViewItemClick to use navigate() as per the Navigate to a destination documentation:
override fun onRecyclerViewItemClick(view: View, dorms: Dorms) {
val navController = findNavController(R.id.nav_host_fragment)
// If you're using Safe Args, use the ID generated from
// the navigation graph and make sure you have
// an argument of the correct type
navController.navigate(
HomeFragmentDirections.actionHomeToDetails(dorms))
}
You might find it helpful to look at the Pass data between destinations documentation to see how to create an <argument> in your graph for your Dorms object and how to set up Safe Args to generate the Directions class for you.
I've been trying to combine a bottom navigation view, a tablayout and a viewpager. I already have the tableyout working with the viewpager, but when selecting another item (different from the one containing the tableyout) from the bottom navigation view nothing happens and it seems that they were disabled. I do not know what I'm doing wrong. I am a novice on Android.
I want something like this:
MainActivity.kt
class MainActivity : AppCompatActivity() {
private val onNavigationItemSelectedListener = BottomNavigationView.OnNavigationItemSelectedListener { item ->
when (item.itemId) {
R.id.navigation_data_sheet -> {
replaceFragment(DataSheetFragment())
return#OnNavigationItemSelectedListener true
}
R.id.navigation_search -> {
replaceFragment(SearchFragment())
return#OnNavigationItemSelectedListener true
}
R.id.navigation_notifications -> {
replaceFragment(NotificationsFragment())
return#OnNavigationItemSelectedListener true
}
R.id.navigation_profile -> {
val intent = Intent(this#MainActivity, NoLoginActivity::class.java)
startActivity(intent)
finish()
return#OnNavigationItemSelectedListener true
}
}
false
}
private fun replaceFragment(fragment: Fragment) {
val fragmentTransition = supportFragmentManager.beginTransaction()
fragmentTransition.replace(R.id.fragmentContainer, fragment)
fragmentTransition.commit()
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
replaceFragment(DataSheetFragment())
val navView: BottomNavigationView = findViewById(R.id.bottom_navigation)
navView.setOnNavigationItemSelectedListener(onNavigationItemSelectedListener)
}}
PagerAdapter.kt
class PagerAdapter(fm: FragmentManager) : FragmentPagerAdapter(fm) {
override fun getItem(position: Int): Fragment {
return when (position) {
0 -> {
SpecimensFragment()
}
else -> {
return FungiFragment()
}
}
}
override fun getCount(): Int {
return 2
}
override fun getPageTitle(position: Int): CharSequence {
return when (position) {
0 -> "Especímenes"
else -> {
return "Hongos"
}
}
}}
DataSheetFragment.kt: Fragment que contiene el tablayout
class DataSheetFragment : Fragment() {
private lateinit var viewPager: ViewPager
private lateinit var tabs: TabLayout
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val view: View = inflater.inflate(R.layout.fragment_data_sheet, container, false)
viewPager = view.findViewById(R.id.viewPager)
tabs = view.findViewById(R.id.data_sheet_tabs)
val fragmentAdapter = PagerAdapter(childFragmentManager)
viewPager.adapter = fragmentAdapter
tabs.setupWithViewPager(viewPager)
return view
}}
fragment_data_sheet.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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"
tools:context=".Fragments.DataSheetFragment"
>
<androidx.appcompat.widget.Toolbar
android:id="#+id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:title="#string/data_sheet"
app:titleTextColor="#color/white"
android:background="#color/colorPrimary"
>
</androidx.appcompat.widget.Toolbar>
<com.google.android.material.tabs.TabLayout
android:id="#+id/data_sheet_tabs"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="#id/toolbar"
android:background="#color/background_color"
app:tabTextAppearance="#style/TabLayoutTextAppearance"
>
<com.google.android.material.tabs.TabItem
android:id="#+id/specimens_tab"
android:text="#string/specimens"
android:layout_width="match_parent"
android:layout_height="wrap_content"
/>
<com.google.android.material.tabs.TabItem
android:id="#+id/fungi_tab"
android:text="#string/fungi"
android:layout_width="match_parent"
android:layout_height="wrap_content"
/>
</com.google.android.material.tabs.TabLayout>
<androidx.viewpager.widget.ViewPager
android:id="#+id/viewPager"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="#id/data_sheet_tabs"
/>
</androidx.constraintlayout.widget.ConstraintLayout>