I have a Button in my viewpager2 (Page: UserDataFragment) which navigates the user to another screen (UserDataChangeEmailFragment), where they can change their password etc. The problem I encounter is that the back button on this screen is not working (e.g when the user presses back and or the toolbar button, the user is not navigated back to UserDataFragment).
The only error I then get is: SPAN_EXCLUSIVE_EXCLUSIVE spans cannot have a zero length.
Graph Picture (trying to navigate from x to y):
Graph Structure
UserDataHolderFragment (Holder for the ViewPager2)
|
|--UserDataFragment (First Page of the ViewPager2, here is the **Button**)
| |
| |-- UserDataChangeEmailFragment (Back button not working)
|
|
|--UserDataAddressFragment (Second Page of the ViewPager2)
Graph Code (Deleted unnecessary stuff)
<navigation android:id="#+id/nav_user_data"
app:startDestination="#id/userDataHolderFragment">
<fragment
android:id="#+id/userDataHolderFragment"
android:name="com.rsb3000.app.framework.ui.view.fragment.user.loggedIn.userdata.UserDataHolderFragment"
tools:layout="#layout/fragment_user_data_holder" >
<action
android:id="#+id/action_userDataHolderFragment_to_userDataChangeEmailFragment"
app:destination="#id/userDataChangeEmailFragment" />
<action
android:id="#+id/action_userDataHolderFragment_to_userDataChangePasswordFragment"
app:destination="#id/userDataChangePasswordFragment" />
</fragment>
<fragment
android:id="#+id/userDataFragment"
android:name="com.rsb3000.app.framework.ui.view.fragment.user.loggedIn.userdata.UserDataFragment"
tools:layout="#layout/fragment_user_data">
</fragment>
<fragment
android:id="#+id/userDataAddressFragment"
android:name="com.rsb3000.app.framework.ui.view.fragment.user.loggedIn.userdata.UserDataAddressFragment"
tools:layout="#layout/fragment_user_data_address" >
</fragment>
<fragment
android:id="#+id/userDataChangeEmailFragment"
android:name="com.rsb3000.app.framework.ui.view.fragment.user.loggedIn.userdata.UserDataChangeEmailFragment"
tools:layout="#layout/fragment_user_data_change_email" />
</navigation>
UserDataHolderFragment Code (Navigation button here)
#AndroidEntryPoint
class UserDataHolderFragment : Fragment(R.layout.fragment_user_data_holder) {
private val binding: FragmentUserDataHolderBinding by viewBinding(FragmentUserDataHolderBinding::bind)
val userDataViewPagerAdapter: UserDataViewPagerAdapter get() = UserDataViewPagerAdapter(SnackbarUtils(), this)
#Inject lateinit var tabLayoutHelper: TabLayoutHelper
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.headline.userStandardToolbar.setupWithNavController(findNavController(), AppBarConfiguration(findNavController().graph))
bindObjects()
bindTablayout()
}
private fun bindObjects() {
with(binding) {
lifecycleOwner = viewLifecycleOwner
vpUserData.adapter = userDataViewPagerAdapter
}
}
private fun bindTablayout() {
tabLayoutHelper.init(binding.tlUserData, binding.vpUserData) { tab, position ->
when(position) {
0 -> tab.text = requireContext().getString(R.string.fragment_user_data_holder_user_data_tab)
1 -> tab.text = requireContext().getString(R.string.fragment_user_data_holder_user_address_tab)
}
}
}
override fun onDestroyView() {
super.onDestroyView()
tabLayoutHelper.onDestroyView()
binding.vpUserData.adapter = null
}
}
UserDataFragment
private fun bindBtnClicks() {
btnGoToChangeAddress.setOnClickListener {
findNavController().navigate(
UserDataHolderFragmentDirections.actionUserDataFragmentHolderToUserDataChangeEmailFragment()
)
}
}
UserDataChangeEmailFragment (BACK BUTTON NOT WORKING HERE)
class UserDataChangeEmailFragment : Fragment(R.layout.fragment_user_data_change_email) {
private val binding: FragmentUserDataChangeEmailBinding by viewBinding(FragmentUserDataChangeEmailBinding::bind)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.headline.userStandardToolbar.setupWithNavController(findNavController(), AppBarConfiguration(findNavController().graph))
}
}
Okay, I've solved my error. This had nothing to do with my nav_graph nor the code in the fragment. The problem was that I navigated to the other fragment within the livedata. Clicking the back button immediately triggered the observation of the livedata and navigated me again and again.
Solution: Before navigating, reset the livedata. You can see more solutions in this stackoverflow post
Related
The problem I have seems to be quite weird.
In words
I have an app with a bottom navigation menu with 3 buttons, 3 fragments for each button and one MainActivity. When navigating to any of those fragments everything works as expected.
The problem arises when I navigate to another fragment (let's call it fragment 4 or fr4) from any of those 3 fragments.
Say I'm in fr1, I have a button that takes me to fr4. When I go back to fr1 (either using the android back button or by pressing the bottom bar button for fr1) then every thing that I do in the main activity or any of the 3 fragments is repeated 2 times. If I then go to fr4 again, and then back to fr1, then everything is repeated 3 times and so on.
In the code below fr1 is fragment_home and fr4 is fragment_profile.
Code
MainActivity.kt
class MainActivity : AppCompatActivity() {
private lateinit var navView: BottomNavigationView
private lateinit var binding: ActivityMainBinding
private val sharedViewModel: SharedViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
navView = binding.navView
val navController = findNavController(R.id.nav_host_fragment_activity_main)
navView.setupWithNavController(navController)
}
override fun onStart() {
super.onStart()
Timber.i("onStart main activity")
}
override fun onStop() {
super.onStop()
Timber.i("onStop main activity")
}
}
framgnet1.kt
class HomeFragment : Fragment() {
private var _binding: FragmentHomeBinding? = null
private val binding get() = _binding!!
private val sharedViewModel: SharedViewModel by activityViewModels()
private val homeViewModel: HomeViewModel by viewModels()
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
_binding = FragmentHomeBinding.inflate(inflater, container, false)
val root: View = binding.root
binding.lifecycleOwner = viewLifecycleOwner
binding.sharedViewModel = sharedViewModel
binding.homeViewModel = homeViewModel
binding.historyButton.setOnClickListener{
Timber.i("profile button clicked")
}
binding.profileButton.setOnClickListener { view ->
profileButtonClicked(view)
}
return root
}
fun profileButtonClicked() {
Timber.i("profile button clicked")
val action = HomeFragmentDirections.homeToProfileAction()
NavHostFragment.findNavController(this).navigate(action)
}
}
mobile_navigation.xml
<?xml version="1.0" encoding="utf-8"?>
<navigation 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/mobile_navigation"
app:startDestination="#+id/navigation_home">
<fragment
android:id="#+id/navigation_home"
android:name="com.comp.comp.ui.home.HomeFragment"
android:label="#string/title_home"
tools:layout="#layout/fragment_home">
<action
android:id="#+id/home_to_profile_action"
app:destination="#id/fragment_profile"
app:launchSingleTop="true" />
</fragment>
<fragment
android:id="#+id/navigation_dashboard"
android:name="com.comp.comp.ui.dashboard.DashboardFragment"
android:label="#string/title_dashboard"
tools:layout="#layout/fragment_dashboard" >
</fragment>
<fragment
android:id="#+id/navigation_notifications"
android:name="com.comp.comp.ui.notifications.NotificationsFragment"
android:label="#string/title_notifications"
tools:layout="#layout/fragment_notifications" />
<fragment
android:id="#+id/fragment_profile"
android:name="com.comp.comp.fragment_profile"
android:label="fragment_profile"
tools:layout="#layout/fragment_profile" />
</navigation>
I've tried toggling launchSingleTop="true" on the navigation action to no avail.
What happens is the following:
In the home fragment if I press the history button it prints "profile button clicked" once
If I then tap the profile button, the app navigates to the profile fragment
I go back to the home fragment either using the back button or pressing the home button on the bottom bar
If I now press the history button "profile button clicked" is printed twice.
If I repeat the steps above then next time I press history button it will print "profile button clicked" 3 times and so on.
I've also tested to go to another activity, the onStop() method in my main activity runs twice as well if I have been to the profile page once before. Same when I go back to the main activity the onStart() method runs two times. Everything I do would run 2 times (or more) depending on how many times I go to the profile page. It looks like it created a main activity every time I visit the profile page that are alive at the same time. Any ideas why?
I tried everything to no avail. Then I realised that at every configuration change it would plant a new Jake Wharton's Timber debug tree and it was logging everything for as many trees there were.
Hope this can help someone. My mistake was the the Timber planing was in onCreate of the MainActivity, while it should be in the OnCreate of the Application.
I am using bottom navigation.
In the switched screen, there is also a function to open a dialog fragment.
I also used navigation for this.
This is because, as soon as this dialog is finished, data must be delivered to the screen that opened the dialog.
I used safe args for this.
But I got the same error as the title.
i know where the error is, but i don't know exactly why it occurred.
According to a search on Stack Overflow, there are people who have had the same problem as me, but it doesn't seem to be an exact solution because the cases are different.
nav_graph
<navigation 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/nav_graph"
app:startDestination="#id/calendar">
<fragment
android:id="#+id/calendar"
android:name="com.example.writeweight.fragment.CalendarFragment"
android:label="fragment_calendar"
tools:layout="#layout/fragment_calendar" >
</fragment>
<fragment
android:id="#+id/list"
android:name="com.example.writeweight.fragment.WorkoutListFragment"
android:label="fragment_workout_list"
tools:layout="#layout/fragment_workout_list" />
<fragment
android:id="#+id/write"
android:name="com.example.writeweight.fragment.WritingRoutineFragment"
android:label="WritingRoutineFragment"
tools:layout="#layout/fragment_writing_routine">
<action
android:id="#+id/action_write_to_bodyPartDialog"
app:destination="#id/bodyPartDialog" />
<argument
android:name="title"
app:argType="string"
android:defaultValue="Write" />
</fragment>
<dialog
android:id="#+id/bodyPartDialog"
android:name="com.example.writeweight.fragment.BodyPartDialogFragment"
android:label="BodyPartDialogFragment"
tools:layout="#layout/fragment_body_part_dialog">
<action
android:id="#+id/action_bodyPartDialog_to_write"
app:destination="#id/write">
</action>
</dialog>
</navigation>
MainActivity
class MainActivity : AppCompatActivity() {
private lateinit var bottomNav: BottomNavigationView
private lateinit var navController: NavController
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
bottomNav = findViewById(R.id.bottom_nav)
val navHostFragment = supportFragmentManager.findFragmentById(R.id.nav_host_container) as NavHostFragment
navController = navHostFragment.navController
bottomNav.setupWithNavController(navController)
}
}
DialogFragment
class BodyPartDialogFragment : DialogFragment() {
private lateinit var ll: LinearLayout
private lateinit var startBtn: Button
private lateinit var title: String
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view: View = inflater.inflate(R.layout.fragment_body_part_dialog, container, false)
initView(view)
setLayout()
startBtn?.setOnClickListener { v ->
title = BodyPartCustomView.getTitle()
val action = BodyPartDialogFragmentDirections.actionBodyPartDialogToWrite(title)
findNavController()?.navigate(action) // Possibly the location of the error.
dismiss()
}
return view
}
private fun initView(view: View) {
ll = view.findViewById(R.id.ll_body_part)
startBtn = view.findViewById(R.id.start)
}
private fun setLayout() {
ll?.apply { clipToOutline = true }
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val dialog:Dialog = super.onCreateDialog(savedInstanceState)
dialog.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
dialog.setCanceledOnTouchOutside(false)
return dialog
}
override fun onDismiss(dialog: DialogInterface) {
Toast.makeText(context, "CHECK", Toast.LENGTH_SHORT).show()
}
}
It can be resolved by checking the current destination first before navigating
For example
Fragments A, B, and C
navigating from A to B while clicking on a button in fragment A that navigates to C might lead to crashes in some cases
for that you should check the current destination first as follows:
if(findNavController().currentDestination?.id==R.id.AFragment)
findNavController().navigate(
AFragmentDirections.actionAFragmentToCFragment()
)
maybe you can just make sure id in fragment navigation is same with id in menu button because i already experienced it
I've created a simple, two fragment example app using jetpack Navigation component (androidx.navigation). First fragment navigates to second one, which overrides backbutton behavior with OnBackPressedDispatcher.
activity layout
<LinearLayout 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:padding="#dimen/box_inset_layout_padding"
tools:context=".navigationcontroller.NavigationControllerActivity">
<fragment
android:name="androidx.navigation.fragment.NavHostFragment"
android:id="#+id/nav_host"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNavHost="true"
app:navGraph="#navigation/nav_graph" />
</LinearLayout>
FragmentA:
class FragmentA : Fragment() {
lateinit var buttonNavigation: Button
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view = inflater.inflate(R.layout.fragment_a, container, false)
buttonNavigation = view.findViewById<Button>(R.id.button_navigation)
buttonNavigation.setOnClickListener { Navigation.findNavController(requireActivity(), R.id.nav_host).navigate(R.id.fragmentB) }
return view
}
}
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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=".navigationcontroller.FragmentA">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="fragment A" />
<Button
android:id="#+id/button_navigation"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="go to B" />
</LinearLayout>
FragmentB:
class FragmentB : Fragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view = inflater.inflate(R.layout.fragment_b, container, false)
requireActivity().onBackPressedDispatcher.addCallback(object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
val textView = view.findViewById<TextView>(R.id.textView)
textView.setText("backbutton pressed, press again to go back")
this.isEnabled = false
}
})
return view
}
}
<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=".navigationcontroller.FragmentA">
<TextView
android:id="#+id/textView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:text="fragment B" />
</FrameLayout>
Intended behavior of backbutton in FragmentB (first touch changes text without navigation, second navigates back) works fine when I test the app manually.
I've added instrumented tests to check backbutton behavior in FragmentB and that's where problems started to arise:
class NavigationControllerActivityTest {
lateinit var fragmentScenario: FragmentScenario<FragmentB>
lateinit var navController: TestNavHostController
#Before
fun setUp() {
navController = TestNavHostController(ApplicationProvider.getApplicationContext())
fragmentScenario = FragmentScenario.launchInContainer(FragmentB::class.java)
fragmentScenario.onFragment(object : FragmentScenario.FragmentAction<FragmentB> {
override fun perform(fragment: FragmentB) {
Navigation.setViewNavController(fragment.requireView(), navController)
navController.setLifecycleOwner(fragment.viewLifecycleOwner)
navController.setOnBackPressedDispatcher(fragment.requireActivity().getOnBackPressedDispatcher())
navController.setGraph(R.navigation.nav_graph)
// simulate backstack from previous navigation
navController.navigate(R.id.fragmentA)
navController.navigate(R.id.fragmentB)
}
})
}
#Test
fun whenButtonClickedOnce_TextChangedNoNavigation() {
Espresso.pressBack()
onView(withId(R.id.textView)).check(matches(withText("backbutton pressed, press again to go back")))
assertEquals(R.id.fragmentB, navController.currentDestination?.id)
}
#Test
fun whenButtonClickedTwice_NavigationHappens() {
Espresso.pressBack()
Espresso.pressBack()
assertEquals(R.id.fragmentA, navController.currentDestination?.id)
}
}
Unfortunately, while whenButtonClickedTwice_NavigationHappens passes, whenButtonClickedOnce_TextChangedNoNavigation fails due to text not being changed, just like OnBackPressedCallback was never called. Since app works fine during manual tests, there must be something wrong with test code. Can anyone help me ?
If you're trying to test your OnBackPressedCallback logic, it is better to do that directly, rather than try to test the interaction between Navigation and the default activity's OnBackPressedDispatcher.
That would mean that you'd want to break the hard dependency between the activity's OnBackPressedDispatcher (requireActivity().onBackPressedDispatcher) and your Fragment by instead injecting in the OnBackPressedDispatcher, thus allowing you to provide a test specific instance:
class FragmentB(val onBackPressedDispatcher: OnBackPressedDispatcher) : Fragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view = inflater.inflate(R.layout.fragment_b, container, false)
onBackPressedDispatcher.addCallback(object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
val textView = view.findViewById<TextView>(R.id.textView)
textView.setText("backbutton pressed, press again to go back")
this.isEnabled = false
}
})
return view
}
}
This allows you to have your production code provide a FragmentFactory:
class MyFragmentFactory(val activity: FragmentActivity) : FragmentFactory() {
override fun instantiate(classLoader: ClassLoader, className: String): Fragment =
when (loadFragmentClass(classLoader, className)) {
FragmentB::class.java -> FragmentB(activity.onBackPressedDispatcher)
else -> super.instantiate(classLoader, className)
}
}
// Your activity would use this via:
override fun onCreate(savedInstanceState: Bundle?) {
supportFragmentManager.fragmentFactory = MyFragmentFactory(this)
super.onCreate(savedInstanceState)
// ...
}
This would mean you could write your tests such as:
class NavigationControllerActivityTest {
lateinit var fragmentScenario: FragmentScenario<FragmentB>
lateinit var onBackPressedDispatcher: OnBackPressedDispatcher
lateinit var navController: TestNavHostController
#Before
fun setUp() {
navController = TestNavHostController(ApplicationProvider.getApplicationContext())
// Create a test specific OnBackPressedDispatcher,
// giving you complete control over its behavior
onBackPressedDispatcher = OnBackPressedDispatcher()
// Here we use the launchInContainer method that
// generates a FragmentFactory from a constructor,
// automatically figuring out what class you want
fragmentScenario = launchFragmentInContainer {
FragmentB(onBackPressedDispatcher)
}
fragmentScenario.onFragment(object : FragmentScenario.FragmentAction<FragmentB> {
override fun perform(fragment: FragmentB) {
Navigation.setViewNavController(fragment.requireView(), navController)
navController.setGraph(R.navigation.nav_graph)
// Set the current destination to fragmentB
navController.setCurrentDestination(R.id.fragmentB)
}
})
}
#Test
fun whenButtonClickedOnce_FragmentInterceptsBack() {
// Assert that your FragmentB has already an enabled OnBackPressedCallback
assertTrue(onBackPressedDispatcher.hasEnabledCallbacks())
// Now trigger the OnBackPressedDispatcher
onBackPressedDispatcher.onBackPressed()
onView(withId(R.id.textView)).check(matches(withText("backbutton pressed, press again to go back")))
// Check that FragmentB has disabled its Callback
// ensuring that the onBackPressed() will do the default behavior
assertFalse(onBackPressedDispatcher.hasEnabledCallbacks())
}
}
This avoids testing Navigation's code and focuses on testing your code and specifically your interaction with OnBackPressedDispatcher.
The reason for FragmentB's OnBackPressedCallback to be ignored is the way how OnBackPressedDispatcher treats its OnBackPressedCallbacks. They are run as chain-of-command, meaning that most recently registered one that is enabled will 'eat' the event so others will not receive it. Therefore, most recently registered callback inside FragmentScenario.onFragment() (which is enabled by lifecycleOwner, so whenever Fragment is at least in lifecycle STARTED state. Since fragment is visible during the test when backbutton is pressed, callback is always enabled at the time), will have priority over previously registered one in FragmentB.onCreateView().
Therefore, TestNavHostController's callback must be added before FragmentB.onCreateView() is executed.
This leads to changes in test code #Before method:
#Before
fun setUp() {
navController = TestNavHostController(ApplicationProvider.getApplicationContext())
fragmentScenario = FragmentScenario.launchInContainer(FragmentB::class.java, initialState = Lifecycle.State.CREATED)
fragmentScenario.onFragment(object : FragmentScenario.FragmentAction<FragmentB> {
override fun perform(fragment: FragmentB) {
navController.setLifecycleOwner(fragment.requireActivity())
navController.setOnBackPressedDispatcher(fragment.requireActivity().getOnBackPressedDispatcher())
navController.setGraph(R.navigation.nav_graph)
// simulate backstack from previous navigation
navController.navigate(R.id.fragmentA)
navController.navigate(R.id.fragmentB)
}
})
fragmentScenario.moveToState(Lifecycle.State.RESUMED)
fragmentScenario.onFragment(object : FragmentScenario.FragmentAction<FragmentB> {
override fun perform(fragment: FragmentB) {
Navigation.setViewNavController(fragment.requireView(), navController)
}
})
}
Most important change is to launch Fragment in CREATED state (instead of default RESUMED) to be able to tinker with it before onCreateView().
Also, notice that Navigation.setViewNavController() is run in separate onFragment() after moving fragment to RESUMED state - it accepts View parameter, so it cannot be used before onCreateView()
I have 1 activity with 3 fragments: Sign In/Sign Up/Forgot Password
Sign In, which is the main fragment among these 3 works just fine, however, I have the issue when working with Sign Up/Forgot Password fragments.
Currently I have this navigation component:
my nav component
Sign In Fragment:
class SignInFragment : Fragment() {
private val loginViewModel: LoginViewModel by activityViewModels()
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view = inflater.inflate(R.layout.fragment_sign_in, container, false)
// see "ProgressButton" in dependencies of build.gradle file.
// used for more interactive button.
bindProgressButton(view.button_sign_in_submit)
loginViewModel.signInState.observe(viewLifecycleOwner, { state ->
when (state) {
is UserSignInState.Loading -> view.button_sign_in_submit.showProgress {
buttonTextRes = R.string.status_loading
progressColor = Color.WHITE
}
is UserSignInState.SignedIn -> {
view.button_sign_in_submit.hideProgress(R.string.button_sign_in_submit)
startActivity(Intent(this.activity, ProfileActivity::class.java))
this.activity?.finish()
}
is UserSignInState.Failure -> {
view.button_sign_in_submit.hideProgress(R.string.button_sign_in_submit)
view.sign_in_input_password_layout.error = state.exception.localizedMessage
}
}
})
// Used for resetting focus and hiding keyboard for better user experience.
view.sign_in_input_email.setOnFocusChangeListener { _, hasFocus -> if (!hasFocus) hideKeyboard() }
view.sign_in_input_password.setOnFocusChangeListener { _, hasFocus -> if (!hasFocus) hideKeyboard() }
view.button_go_to_sign_up.setOnClickListener {
hideKeyboard()
this.findNavController().navigate(R.id.action_signInFragment_to_signUpFragment)
}
view.button_go_to_forgot_password.setOnClickListener {
hideKeyboard()
this.findNavController().navigate(R.id.action_signInFragment_to_forgotPasswordFragment)
}
view.button_sign_in_submit.setOnClickListener {
hideKeyboard()
view.sign_in_input_email_layout.error = null
view.sign_in_input_password_layout.error = null
val email = view.sign_in_input_email.text.toString()
val password = view.sign_in_input_password.text.toString()
if (email.isNotEmpty() && password.isNotEmpty()) {
if (isEmailValid(email)) {
loginViewModel.userSignIn(email, password)
} else {
view.sign_in_input_email_layout.error = getString(R.string.error_message_email_is_not_valid)
}
} else {
view.sign_in_input_password_layout.error = getString(R.string.error_message_please_fill_in_all_fields)
}
}
return view
}
}
ForgotPassword Fragment:
class ForgotPasswordFragment : Fragment() {
private val loginViewModel: LoginViewModel by activityViewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view = inflater.inflate(R.layout.fragment_forgot_password, container, false)
// see "ProgressButton" in dependencies of build.gradle file.
// used for more interactive button.
bindProgressButton(view.button_forgot_password_submit)
this.retainInstance = false
loginViewModel.passwordResetState.observe(viewLifecycleOwner, { state ->
when (state) {
is UserPasswordResetState.Loading -> view.button_forgot_password_submit.showProgress {
buttonTextRes = R.string.status_loading
progressColor = Color.WHITE
}
is UserPasswordResetState.Sent -> {
view.button_forgot_password_submit.hideProgress(R.string.button_forgot_password_submit)
alertDialogSuccess(requireContext(), getString(R.string.alert_dialog_message_forgot_password_email_has_been_sent))
activity?.findNavController(R.id.nav_host_fragment_login)?.navigate(R.id.action_forgotPasswordFragment_to_signInFragment)
}
is UserPasswordResetState.Error -> {
view.button_forgot_password_submit.hideProgress(R.string.button_forgot_password_submit)
// Could be possibly replaced with implementation of "alertDialogFailure"
view.forgot_password_input_field_email_layout.error = state.exception.localizedMessage
}
}
})
view.forgot_password_input_field_email.setOnFocusChangeListener { _, hasFocus -> if (!hasFocus) hideKeyboard() }
view.button_forgot_password_submit.setOnClickListener {
hideKeyboard()
if (isEmailValid(view.forgot_password_input_field_email.text.toString())) {
loginViewModel.userResetPassword(view.forgot_password_input_field_email.text.toString())
} else if (!isEmailValid(view.forgot_password_input_field_email.text.toString())) {
view.forgot_password_input_field_email_layout.error = getString(R.string.error_message_email_is_not_valid)
} else {
view.forgot_password_input_field_email_layout.error = getString(R.string.error_message_unknown)
}
}
return view
}
}
Navigation XML
<?xml version="1.0" encoding="utf-8"?>
<navigation 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/nav_graph_login"
app:startDestination="#id/signInFragment">
<fragment
android:id="#+id/signInFragment"
android:name="my.test.movieexpert._login.view.fragments.SignInFragment"
android:label="fragment_sign_in"
tools:layout="#layout/fragment_sign_in">
<action
android:id="#+id/action_signInFragment_to_signUpFragment"
app:destination="#id/signUpFragment"
app:enterAnim="#anim/nav_default_enter_anim"
app:exitAnim="#anim/nav_default_exit_anim" />
<action
android:id="#+id/action_signInFragment_to_forgotPasswordFragment"
app:destination="#id/forgotPasswordFragment"
app:enterAnim="#anim/nav_default_enter_anim"
app:exitAnim="#anim/nav_default_exit_anim" />
</fragment>
<fragment
android:id="#+id/signUpFragment"
android:name="my.test.movieexpert._login.view.fragments.SignUpFragment"
android:label="fragment_sign_up"
tools:layout="#layout/fragment_sign_up">
<action
android:id="#+id/action_signUpFragment_to_signInFragment"
app:destination="#id/signInFragment"
app:enterAnim="#anim/nav_default_enter_anim"
app:exitAnim="#anim/nav_default_exit_anim"
app:popUpTo="#+id/signInFragment"
app:popUpToInclusive="true" />
</fragment>
<fragment
android:id="#+id/forgotPasswordFragment"
android:name="my.test.movieexpert._login.view.fragments.ForgotPasswordFragment"
android:label="fragment_forgot_password"
tools:layout="#layout/fragment_forgot_password">
<action
android:id="#+id/action_forgotPasswordFragment_to_signInFragment"
app:destination="#id/signInFragment"
app:enterAnim="#anim/nav_default_enter_anim"
app:exitAnim="#anim/nav_default_exit_anim"
app:popUpTo="#+id/signInFragment"
app:popUpToInclusive="true" />
</fragment>
</navigation>
What happens is: I launch my app. Press - Forgot Password, then enter my e-mail, I get alert dialog that the e-mail has been sent and then I am transferred to my sign-in screen. However, when I next time press "Forgot Password" I do not see that same field and etc, instead all I get is that AlertDialog that was run last time with Success message. (Yet I don't receive the message) so the Fragment gets stuck and I can never again get inside unless I restart the app.
I have no idea what is the cause of this and how to fix this, please help me out here.
Dismiss the alert dialog before navigating for success or failure cases.
The problem lies here:
private val loginViewModel: LoginViewModel by activityViewModels()
by activityViewModels() makes it so that the state persists between all Fragments that are referencing the ViewModel in such manner.
Simply replace activityViewModels() with viewModels()
I am making app, which supports different device orientations. Navigation is carried out by Android Jetpack's Navigation. App main screen for landscape orientation is present below. It is list wrapper fragment (it is NavHostFragment, it is added to activity's layout in fragment tag), wrapper includes list fragment (fragment) and details fragment (FrameLayout). Portrait orientation is similar (wrapper and list in it, details is accessible throw navigation).
My problem is after I change device orientation I get exception
java.lang.IllegalStateException: no current navigation node
First version of my layout with mocked data worked fine, the error appears after I add ROOM to my app, new order and update order fragments. It is a pity, I cannot localize source of error more exact.
List wrapper code
class OrderListWrapperFragment : RxFragment() {
private val disposable = CompositeDisposable()
var selectedOrderId: Long = 0
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val bundle = savedInstanceState ?: arguments
bundle?.let {
selectedOrderId = it.getLong(EXTRA_ORDER_ID)
}
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? = inflater.inflate(R.layout.orders__list_wrapper, container, false)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
initializeToolbar(toolbar, getString(R.string.orders_title), false)
newOrderButton
.clicks()
.subscribe {
findNavController()
.navigate(R.id.action_orderListWrapperFragment_to_orderNewFragment)
}
.addTo(disposable)
childFragmentManager.registerFragmentLifecycleCallbacks(callback, false)
}
override fun onDestroyView() {
super.onDestroyView()
childFragmentManager.unregisterFragmentLifecycleCallbacks(callback)
disposable.clear()
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putLong(EXTRA_ORDER_ID, selectedOrderId)
}
private val callback = object : FragmentManager.FragmentLifecycleCallbacks() {
private val disposable = CompositeDisposable()
override fun onFragmentResumed(fm: FragmentManager, fragment: Fragment) {
super.onFragmentResumed(fm, fragment)
if (fragment is OrderListFragment) {
fragment
.selectedItemIdChanges
.subscribeBy(onNext = {
selectedOrderId = it
if (orderDetailsContainer != null) {
childFragmentManager.commit {
replace(
R.id.orderDetailsContainer,
OrderDetailsFragment.newInstance(it)
)
}
} else {
findNavController()
.navigate(
R.id.action_orderListWrapperFragment_to_orderDetailsFragment,
bundleOf(EXTRA_ORDER_ID to it)
)
selectedOrderId = 0
}
},
onError = {
Log.d("detailView", it.toString())
})
.addTo(disposable)
val orderId = selectedOrderId
if (orderId != 0L) {
if (orderDetailsContainer != null) {
childFragmentManager.commit {
replace(
R.id.orderDetailsContainer,
OrderDetailsFragment.newInstance(orderId)
)
}
} else {
findNavController()
.navigate(//exception throws here
R.id.action_orderListWrapperFragment_to_orderDetailsFragment,
bundleOf(EXTRA_ORDER_ID to orderId)
)
selectedOrderId = 0
}
}
}
}
override fun onFragmentPaused(fm: FragmentManager, fragment: Fragment) {
super.onFragmentPaused(fm, fragment)
if (fragment is OrderListFragment) {
disposable.clear()
}
}
}
companion object {
private const val EXTRA_ORDER_ID = "EXTRA_ORDER_ID"
}
}
My navigation graph
<?xml version="1.0" encoding="utf-8"?>
<navigation 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/navigation_graph"
app:startDestination="#id/orderListWrapperFragment">
<fragment
android:id="#+id/orderListWrapperFragment"
android:name="com.mtgshipping.app.orders.orderList.OrderListWrapperFragment"
android:label="OrderListWrapperFragment"
tools:layout="#layout/orders__list_wrapper">
<action
android:id="#+id/action_orderListWrapperFragment_to_orderDetailsFragment"
app:destination="#id/orderDetailsFragment"/>
<action
android:id="#+id/action_orderListWrapperFragment_to_orderNewFragment"
app:destination="#id/orderNewFragment"/>
<action
android:id="#+id/action_orderListWrapperFragment_to_orderUpdateFragment"
app:destination="#id/orderUpdateFragment"/>
</fragment>
<fragment
android:id="#+id/orderDetailsFragment"
android:name="com.mtgshipping.app.orders.orderDetails.OrderDetailsFragment"
android:label="OrderDetailsFragment"
tools:layout="#layout/orders__order_details">
<action
android:id="#+id/action_orderDetailsFragment_to_orderUpdateFragment"
app:destination="#id/orderUpdateFragment"/>
</fragment>
<fragment
android:id="#+id/orderNewFragment"
android:name="com.mtgshipping.app.orders.orderNew.OrderNewFragment"
android:label="OrderNewFragment"
tools:layout="#layout/orders__order_new">
<action
android:id="#+id/action_orderNewFragment_to_orderListWrapperFragment"
app:destination="#id/orderListWrapperFragment"/>
</fragment>
<fragment
android:id="#+id/orderUpdateFragment"
android:name="com.mtgshipping.app.orders.orderUpdate.OrderUpdateFragment"
android:label="OrderUpdateFragment"
tools:layout="#layout/orders__order_update">
<action
android:id="#+id/action_orderUpdateFragment_to_orderListWrapperFragment"
app:destination="#id/orderListWrapperFragment"/>
</fragment>
</navigation>
I made some debug in NavController, it showed in line 746 NavDestination currentNode = mBackStack.isEmpty() ? mGraph : mBackStack.getLast().getDestination(); after device rotation mGraph is null, other private fields is also null. Maybe something prevents NavController to initialize properly. I can provide layouts and other code, if it will be need.
Thanks to Slav's comment, he was right. I updated navigation module to 2.2.0 navigation_version = '2.2.0' in app's module build.gradle
implementation "androidx.navigation:navigation-fragment-ktx:$navigation_version"
implementation "androidx.navigation:navigation-ui-ktx:$navigation_version"
After doing this problem is no longer appears, looks like that was a bug in navigation.
You can also fix it like this.
In your host activity in manifest adding this atribute:
<activity android:name=".MainActivity"
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout"
But the best way is change your dependencies for navigation from:
implementation "android.arch.navigation:navigation-fragment-ktx:$navigation_version"
implementation "android.arch.navigation:navigation-ui-ktx:$navigation_version"
to
implementation "androidx.navigation:navigation-fragment-ktx:$nav_version"
implementation "androidx.navigation:navigation-ui-ktx:$nav_version"
The problem for me was the version of lifecycle dependency
"androidx.lifecycle:lifecycle-extensions:$lifecycle_version
version '2.2.0' cause the problem, I returned to use '2.1.0'
lifecycle_version = '2.1.0'
None of these solutions fixed my issue.
I fixed it by including my nested graph into the main graph.
I have a nav_main which includes a nav_sub_1.
nav_sub_1 also includes another sub graph, nav_sub_2
When I tried to start my activity by setting nav_sub_2, the IllegalStateException occured
java.lang.IllegalStateException: no current navigation node
But this would not happen by setting nav_main or nav_sub_1.
My main graph nav_main looks like this:
<navigation
<fragment...>
<include app:graph="#navigation/nav_sub_1
<navigation/>
and nav_sub_1:
<navigation
<fragment...>
<include app:graph="#navigation/nav_sub_2
<navigation/>
I included nav_sub_2 in nav_main and the issue was fixed!
<navigation
<fragment...>
<include app:graph="#navigation/nav_sub_1
<include app:graph="#navigation/nav_sub_2
<navigation/>
For me the fragment had the wrong parent. Changing this
class MyFragment: DynamicNavHostFragment() ...
to this
class MyFragment: Fragment() ...
Fixed that issue