this my Auth Activity
class AuthActivity : AppCompatActivity() {
private lateinit var navController: NavController
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding = ActivityAuthBinding.inflate(layoutInflater)
setContentView(binding.root)
navController = Navigation.findNavController(this, fragment.id)
NavigationUI.setupActionBarWithNavController(this, navController)
}
override fun onSupportNavigateUp(): Boolean {
return NavigationUI.navigateUp(navController, null)
}
}
LoginFragment -> if login is success goto "AcceptCodeFragment"
viewModel.loginResponse.observe(viewLifecycleOwner, { response ->
viewBinding.pbLogin.visible(response is Resource.Loading)
when (response) {
is Resource.Success -> {
viewBinding.tvResponse.text = response.value.message
val action = LoginFragmentDirections.actionLoginFragmentToAcceptCodeFragment()
findNavController().navigate(action)
}
is Resource.Error -> if (response.isNetworkError) {
requireView().snackBar("Check your connection")
} else {
requireView().snackBar(response.errorBody.toString())
}
}
in AcceptCodeFragment Back button not work.
Two fragments using same viewmodel.
Your issue is not with the back button not working, it is that LiveData is for state, not events like your loginResponse. As LiveData is for events, it redelivers the previous response when you go back to your LoginFragment. This then triggers your navigate() call again, pushing you right back to your AcceptCodeFragment.
As per the LiveData with SnackBar, Navigation, and other events blog post, LiveData cannot be directly used for events. Instead, you should consider using an event wrapper or another solution (such as a Kotlin Flow) that allow your events to only be handled once.
Related
I'm using branch.io for handling deep links. Deep links can contain custom metadata in a form of JsonObject. The data can be obtained by setting up a listener, inside MainActivity#onStart() which is triggered when a link is clicked.
override fun onStart() {
super.onStart()
Branch
.sessionBuilder(this)
.withCallback { referringParams, error ->
if (error == null) {
val eventId = referringParams?.getString("id")
//Here I would like to navigate user to event screen
} else {
Timber.e(error.message)
}
}
.withData(this.intent?.data).init()
}
When I retrieve eventId from referringParams I have to navigate the user to the specific event. When I was using Navigation components with fragments I could just do:
findNavController(R.id.navHost).navigate("path to event screen")
But with compose is different because I can't use navController outside of Composable since its located in MainActivity#onCreate()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
//I cant access navController outside of composable function
val navController = rememberNavController()
NavHost(
navController = navController,
startDestination = "HomeScreen",
) {
}
}
}
My question is, how can I navigate the user to a specific screen from MainActivity#onStart() when using jetpack compose navigation
rememberNavController has pretty simple implementation: it creates NavHostController with two navigators, needed by Compose, and makes sure it's restored on configuration change.
Here's how you can do the same in your activity, outside of composable scope:
private lateinit var navController: NavHostController
private val navControllerBundleKey = "navControllerBundleKey"
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
navController = NavHostController(this).apply {
navigatorProvider.addNavigator(ComposeNavigator())
navigatorProvider.addNavigator(DialogNavigator())
}
savedInstanceState
?.getBundle(navControllerBundleKey)
?.apply(navController::restoreState)
setContent {
// pass navController to NavHost
}
}
override fun onSaveInstanceState(outState: Bundle) {
outState.putBundle(navControllerBundleKey, navController.saveState())
super.onSaveInstanceState(outState)
}
this my Auth Activity
class AuthActivity : AppCompatActivity() {
private lateinit var navController: NavController
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding = ActivityAuthBinding.inflate(layoutInflater)
setContentView(binding.root)
navController = Navigation.findNavController(this, fragment.id)
NavigationUI.setupActionBarWithNavController(this, navController)
}
override fun onSupportNavigateUp(): Boolean {
return NavigationUI.navigateUp(navController, null)
}
}
LoginFragment -> if login is success goto "AcceptCodeFragment"
viewModel.loginResponse.observe(viewLifecycleOwner, { response ->
viewBinding.pbLogin.visible(response is Resource.Loading)
when (response) {
is Resource.Success -> {
viewBinding.tvResponse.text = response.value.message
val action = LoginFragmentDirections.actionLoginFragmentToAcceptCodeFragment()
findNavController().navigate(action)
}
is Resource.Error -> if (response.isNetworkError) {
requireView().snackBar("Check your connection")
} else {
requireView().snackBar(response.errorBody.toString())
}
}
in AcceptCodeFragment Back button not work.
Two fragments using same viewmodel.
Your issue is not with the back button not working, it is that LiveData is for state, not events like your loginResponse. As LiveData is for events, it redelivers the previous response when you go back to your LoginFragment. This then triggers your navigate() call again, pushing you right back to your AcceptCodeFragment.
As per the LiveData with SnackBar, Navigation, and other events blog post, LiveData cannot be directly used for events. Instead, you should consider using an event wrapper or another solution (such as a Kotlin Flow) that allow your events to only be handled once.
this my Auth Activity
class AuthActivity : AppCompatActivity() {
private lateinit var navController: NavController
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding = ActivityAuthBinding.inflate(layoutInflater)
setContentView(binding.root)
navController = Navigation.findNavController(this, fragment.id)
NavigationUI.setupActionBarWithNavController(this, navController)
}
override fun onSupportNavigateUp(): Boolean {
return NavigationUI.navigateUp(navController, null)
}
}
LoginFragment -> if login is success goto "AcceptCodeFragment"
viewModel.loginResponse.observe(viewLifecycleOwner, { response ->
viewBinding.pbLogin.visible(response is Resource.Loading)
when (response) {
is Resource.Success -> {
viewBinding.tvResponse.text = response.value.message
val action = LoginFragmentDirections.actionLoginFragmentToAcceptCodeFragment()
findNavController().navigate(action)
}
is Resource.Error -> if (response.isNetworkError) {
requireView().snackBar("Check your connection")
} else {
requireView().snackBar(response.errorBody.toString())
}
}
in AcceptCodeFragment Back button not work.
Two fragments using same viewmodel.
Your issue is not with the back button not working, it is that LiveData is for state, not events like your loginResponse. As LiveData is for events, it redelivers the previous response when you go back to your LoginFragment. This then triggers your navigate() call again, pushing you right back to your AcceptCodeFragment.
As per the LiveData with SnackBar, Navigation, and other events blog post, LiveData cannot be directly used for events. Instead, you should consider using an event wrapper or another solution (such as a Kotlin Flow) that allow your events to only be handled once.
I am developing an App with the google recommended SingleActivity pattern. My scenario is: My App starts with the startDestination at HomeFragment. And If the user is not loggedIn I want to start from LoginFragment. My logic is I save loggedIn status in DataStore which is initially false. When My app starts launching. In my MainActivity, I observe that loggedIn status through ViewModel. Then I passed that status to a function. This is my function.
private fun setDestinationForApp(isLoggedIn: Boolean) {
navController.addOnDestinationChangedListener { _, destination, _ ->
if (destination.id == R.id.dest_home) {
if (!isLoggedIn) {
val authNavOptions = NavOptions.Builder()
.setPopUpTo(R.id.dest_home, true)
.build()
navController.navigate(R.id.action_home_to_login, Bundle(), authNavOptions)
}
}
}
}
This is fine enough. But what I faced is : My app starts show HomeFragment default and then if the user is not loggedIn, destinate to LoginFragment.
I want to display corresponding destination fragment according to my loggedIn status from DataStore. Not want to display the default HomeFragment first.
This may be of Activity lifecycle and Navigation Components callback listeners. But I am not smart enough like that. Please help me I am stuck in this. If my question is not clear or cant be solved. Please help me the appropriate way.
This is my whole MainActivity.
#AndroidEntryPoint
class MainActivity : BaseActivity<ActivityMainBinding>(ActivityMainBinding::inflate) {
private val navHostFragment: NavHostFragment by lazy {
supportFragmentManager.findFragmentById(R.id.container) as NavHostFragment
}
private val navController: NavController by lazy {
navHostFragment.navController
}
private val appBarConfiguration: AppBarConfiguration by lazy {
AppBarConfiguration(navController.graph)
}
private val loadingDialog: LoadingDialog by lazy { LoadingDialog(this) }
private val viewModel: MainViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setupNavigation()
}
override fun observe() {
super.observe()
lifecycleScope.launchWhenCreated {
viewModel.authState.collect {
setDestinationForApp(it)
}
}
}
private fun setupNavigation() {
setSupportActionBar(binding.authToolbar)
setupActionBarWithNavController(navController, appBarConfiguration)
}
override fun onSupportNavigateUp(): Boolean {
return navController.navigateUp(appBarConfiguration) || super.onSupportNavigateUp()
}
fun showLoadingDialog(text: String) {
loadingDialog.apply {
setMessage(text)
setCanceledOnTouchOutside(false)
setCancelable(false)
show()
}
}
fun hideLoadingDialog() {
loadingDialog.hide()
}
private fun setDestinationForApp(isLoggedIn: Boolean) {
navController.addOnDestinationChangedListener { _, destination, _ ->
if (destination.id == R.id.dest_home) {
if (!isLoggedIn) {
val authNavOptions = NavOptions.Builder()
.setPopUpTo(R.id.dest_home, true)
.build()
navController.navigate(R.id.action_home_to_login, Bundle(), authNavOptions)
}
}
}
}
}
This is because the navigation component starts with the start graph fragment. And anyway if you want to go back from LoginFragment. it gets back to HomeFragment. So in this case you need to check in startup process for loggedIn status and if the user is not loggedIn you can change the starting point of graph programmatically. This way the starting point will be LoginFragment and it will never navigate to HomeFragment. And after the login process you can revert this logic to set HomeFragment as starting point of your navigation graph.
I am using the navigation component in my Android app, which automatically provides me with back and up navigation. Now I don't want to change any of those behaviours, but I want to add some logging specific to the fragment where the user presses either the up button in the toolbar or the back button.
I tried this, and it worked only for the back button, and I didn't figure out how to leave the default navigation behaviour intact. Plus it seems like this adds a callback at the activity level, so it's not specific to the fragment where I add the callback.
And it seems like onOptionsItemSelected is called for normal menu items, but not for the Up button.
How can I handle this consistently without changing the behaviour of my entire app?
You are very close to the answer.
Try this.
Note: This code is copied from my app. Change as per your requirement.
In Activity:
In onCreate():
// Observe action state live data
activityViewModel.actionStateMutableLiveData.observe(this, Observer { actionState ->
actionState?.let {
if (actionState != "NO_ACTION") {
when (actionState) {
"NAVIGATE_UP" -> {
if (!navController.navigateUp()) {
finish()
}
}
}
// Reset action state
activityViewModel.setActionState("NO_ACTION")
}
}
})
And,
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
android.R.id.home -> {
navController.currentDestination?.id?.let { currentDestinationId ->
return when (currentDestinationId) {
R.id.fragmentToLog -> {
false
}
else -> {
activityViewModel.setActionState("NAVIGATE_UP")
true
}
}
}
activityViewModel.setActionState("NAVIGATE_UP")
return true
}
else -> {
item.onNavDestinationSelected(findNavController(R.id.fragment_activitymain))
|| super.onOptionsItemSelected(item)
}
}
}
Fragment:
In onViewCreated():
requireActivity().onBackPressedDispatcher.addCallback(this) {
handleNavigateBack()
}
And
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
android.R.id.home -> {
handleNavigateBack()
return true
}
}
return super.onOptionsItemSelected(item)
}
private fun handleNavigateBack() {
// TODO: Add your fragment logs here
activityViewModel.setActionState("NAVIGATE_UP")
}
Activity ViewModel:
// Action state
var actionStateMutableLiveData = MutableLiveData(NO_ACTION)
private set
fun setActionState(actionStateValue: String?) =
actionStateMutableLiveData.postValue(actionStateValue)
Concepts used:
MVVM architecture
Live Data & View model
Android architecture navigation components
Please comment if anything is not clear.
in your activity you should set up NavigationUI first by following code
val navController = this.findNavController(R.id.nav_app_id)
NavigationUI.setupActionBarWithNavController(this,navController)
in Fragment you could detect when user click back at toolbar in onOptionsItemSelected and then call findNavController().navigateUp()
One possible solution involves three steps: Define an interface, let the desired fragments implement that interface and override the onSupportNavigateUp / onNavigateUp / onBackPressed method in your hosting activity.
First, define an interface, e.g.:
interface CustomNavAction {
fun logSomeStuff()
}
Secondly, add the interface to the desired Fragment, e.g.:
class AFragment : Fragment(), CustomNavAction {
...
override fun logSomeStuff() {
Log.d("TAG", "Up or back")
}
...
}
Finally, override the onSupportNavigateUp / onNavigateUp / onBackPressed method in the Activity that hosts the navigation component, find the current fragment and check if it implements the interface. If so, you can call the action. For example:
override fun onSupportNavigateUp(): Boolean {
val navController = findNavController(R.id.nav_host_fragment)
// find current fragment, and invoke custom action if up button is pressed
val navHostFragment = supportFragmentManager.fragments[0]
val currentFragment = navHostFragment.childFragmentManager.fragments[0]
if (currentFragment is CustomNavAction) {
(currentFragment as CustomNavAction).logSomeStuff()
}
return navController.navigateUp(appBarConfiguration) || super.onSupportNavigateUp()
}
override fun onBackPressed() {
super.onBackPressed()
...
// similar logic as onSupportNavigateUp
}
In this way, you don't mess with the overall navigation, and only the fragments implementing the interface will log something.
This solution is not specific to the fragment but in this way, you can detect the back press and navigation up with less code
In Fragment, onCreate add this
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// enable the callback, when we receive a back press event disable the callback and dispatch the event to the activity
requireActivity().onBackPressedDispatcher.addCallback(
this,
object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
Log.d(
"SomeTag",
"User pressed up back button to navigate back from fragment."
)
isEnabled = false
requireActivity().onBackPressed()
}
})
}
Now, in your activity, you need to override the default ToolBar#onNavigationOnClickListener()
toolbar.setNavigationOnClickListener {
val currentFragment = navHostFragment.childFragmentManager.primaryNavigationFragment
Log.d(
"SomeTag",
"User pressed up arrow to navigate back from fragment: $currentFragment"
)
navController.navigateUp(appBarConfiguration)
}