Today, I discovered a very weird bug in my app. It goes the following: When the user navigates to the "profile screen", then opens the "information screen" and after that clicks on "agb", my app opens a browser and navigates the user to my website. Now the weird thing: When the user navigates back (to the "information screen", the bottomnavigation indicates, that he is currently on the home tab. Navigating back again (to the "profile screen"), then solves this issue. So it goes:
"Profile Screen" -> "Information Screen" -> Clicking on link -> "Opening Browser"
-> Clicking Back -> State gets lost -> "Information Screen" -> Clicking Back -> State gets restored -> "Profile Screen".
I added some pictures that show the error (red indicates the state, blue a action)
Navigation
Profile Screen (correct state)
Information Screen (correct state)
Browser (Probably state destroyed now)
Information Screen (state destroyed)
Profile Screen (state restored)
Fragment (Information Screen)
class UserInformationFragment : Fragment(R.layout.fragment_user_information) {
private var _binding: FragmentUserInformationBinding? = null
private val binding: FragmentUserInformationBinding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentUserInformationBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
initUserToolbar()
with(binding) {
agbCv.setOnClickListener { startBrowser(INTERNET_AGB) }
dataProtectionCv.setOnClickListener { startBrowser(INTERNET_DATA_PROTECTION) }
impressumCv.setOnClickListener { startBrowser(INTERNET_IMPRESSUM) }
}
}
private fun startBrowser(url: String) {
val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
startActivity(browserIntent)
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
One Idea I had was to set the state manually to the profile screen again after the user navigates back. But that would be a real mess and weird to do..
Navigation Version
navigation_version = 2.3.5
implementation "androidx.navigation:navigation-fragment-ktx:$navigation_version"
implementation "androidx.navigation:navigation-ui-ktx:$navigation_version"
You have to use Deep link.
Suppose that you have fragment one that you want to come back from browser to it.
Add deep link tag to it like below:
mobile_navigation.xml
<fragment
android:id="#+id/fragment_one"
android:name="one"
android:label="one"
>
<deepLink
android:id="#+id/deepLink"
app:uri="www.Deeplinkuri.com" />
<action
</fragment>
You get www.Deeplinkuri.com example URI from back end team and add to your fragment.
You should ask a link from server side.
NOW WHEN YOU BACK FROM THE BROWSER YOU CAN OPEN YOUR OWN PAGE.
Simplest way is keep your last selected state in local static variable and onResume() apply that selected state from the static variable.
companion object {
const val LAST_SELECTED_STATE = SHOULD_BE_LAST_SELECTED_MENU_ID
}
override fun onResume() {
yourBottomNavigation. setSelectedItemId = LAST_SELECTED_STATE
}
Okay, I've managed to solve this problem. The problem had nothing to do with opening the browser, but with the recreation of my activity and reinflating of my bottomnavigation#menu. Before I had this:
private fun setUpBottomNav(newMenu: Int) {
with(binding.bottomNavigationView) {
menu.clear()
inflateMenu(newMenu)
setupWithNavController(findNavController(R.id.fragment_container))
// fix blinking when re selecting bottom nav item
setOnItemReselectedListener {}
}
}
The problem was that setUpBottomNav() was called, after getting back from the browser to my app, because the activity was recreated. The new solution is this:
private fun setUpBottomProfile(newMenu: Int) {
val controller = (supportFragmentManager.findFragmentById(R.id.fragment_container) as NavHostFragment).navController
with(binding.bottomNavigationView) {
val currentSelectedItem = selectedItemId
menu.clear()
inflateMenu(newMenu)
selectedItemId = currentSelectedItem
setupWithNavController(controller)
// fix blinking when re selecting bottom nav item
setOnItemReselectedListener {}
}
}
Related
I have an app bar defined from my fragment rather than activity by using
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.toolbar.apply {
//add menu
inflateMenu(R.menu.menu_fragment)
//setup with navcontroller/navgraph
setupWithNavController(findNavController())
}
}
The problem I'm facing is trying to implement a warning message when a user clicks the Navigate Up button using the app bar. I want this behaviour only in one fragment.
I've found solutions online pertaining to app bars defined in an activity but they don't seem to work for me (such as using override fun onSupportNavigateUp().
Any ideas if I may be able to accomplish this?
Update
Initially, I implemented the chosen answer which worked but was causing some memory leaks. The kind individual who answered this question also found a workaround for the memory leaks here . Unfortunately, it didn't work so great for me (I believe because I am using navigation components) but it may work for you.
I later realized that I could easily override the navigate up default behaviour by adding this piece of line to my toolbar code:
binding.toolbar.apply {
//add menu
inflateMenu(R.menu.menu_fragment)
//setup with navcontroller/navgraph
setupWithNavController(findNavController())
//****************ADD THIS******************
setNavigationOnClickListener { view ->
//do what you want after user clicks navigate up button
}
}
The problem I'm facing is trying to implement a warning message when a user clicks the Navigate Up button using the app bar. I want this behaviour only in one fragment.
So, you just need to catch the event of hitting the UP button of the app bar for that particular fragment.
You can enable the options menu for that fragment:
setHasOptionsMenu(true)
And override onOptionsItemSelected to catch the UP button id:
override fun onOptionsItemSelected(item: MenuItem): Boolean {
if (item.itemId == android.R.id.home) {
// Handle the UP button here
Toast.makeText(requireContext(), "UP button clicked", Toast.LENGTH_SHORT).show()
return true
}
return super.onOptionsItemSelected(item)
}
Note: if you want to use a unique toolbar for that fragment other than the default one, check this answer.
now I am unable to inflate my menu using inflateMenu(R.menu.menu_fragment). Any ideas?
You can remove this inflation, and instead override onCreateOptionsMenu for that:
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.menu_fragment, menu)
}
onCreateOptionsMenu() didn't work for me,
I have write this code part in onCreate() for Activity; (navigationView is id my NavigationView)
for (i in 0 until navigationView.menu!!.size()) {
val item = navigationView.menu.getItem(i)
val s = SpannableString(item.title)
s.setSpan(AlignmentSpan.Standard(Layout.Alignment.ALIGN_CENTER), 0, s.length, 0)
item.title = s
}
I am setting a navigation graph programmatically to set the start destination depending on some condition (for example, active session), but when I tested this with the "Don't keep activities" option enabled I faced the following bug.
When activity is just recreated and the app calls method NavController.setGraph, NavController forces restoring the Navigation back stack (from internal field mBackStackToRestore in onGraphCreated method) even if start destination is different than before so the user sees the wrong fragment.
Here is my MainActivity code:
class MainActivity : AppCompatActivity() {
lateinit var navController: NavController
lateinit var navHost: NavHostFragment
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.main_activity)
log("fresh start = ${savedInstanceState == null}")
navHost = supportFragmentManager.findFragmentById(R.id.main_nav_host) as NavHostFragment
navController = navHost.navController
createGraph(App.instance.getValue())
}
private fun createGraph(bool: Boolean) {
Toast.makeText(this, "Is session active: $bool", Toast.LENGTH_SHORT).show()
log("one: ${R.id.fragment_one}, two: ${R.id.fragment_two}")
val graph =
if (bool) {
log("fragment one")
navController.navInflater.inflate(R.navigation.nav_graph).also {
it.startDestination = R.id.fragment_one
}
} else {
log("fragment two")
navController.navInflater.inflate(R.navigation.nav_graph).also {
it.startDestination = R.id.fragment_two
}
}
navController.setGraph(graph, null)
}
}
App code:
class App : Application() {
companion object {
lateinit var instance: App
}
private var someValue = true
override fun onCreate() {
super.onCreate()
instance = this
}
fun getValue(): Boolean {
val result = someValue
someValue = !someValue
return result
}
}
Fragment One and Two are just empty fragments.
How it looks like:
Repository with full code and more explanation available by link
My question: is it a Navigation library bug or I am doing something wrong? Maybe I am using a bad approach and there is a better one to achieve what I want?
As you tried in your repository, It comes from save/restoreInstanceState.
It means you set suit graph in onCreate via createGraph(App.instance.getValue()) and then fragmentManager in onRestoreInstanceState will override your configuration for NavHostFragment.
So you can set another another time the graph in onRestoreInstanceState. But it will not work because of this line and backstack is not empty. (I think this behavior may be a bug...)
Because of you're using a graph (R.navigation.nav_graph) for different situation and just change their startDestination, you can be sure after process death, used graph is your demand graph. So just override startDestination in onRestoreInstanceState.
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
super.onRestoreInstanceState(savedInstanceState)
if (codition) {
navController.graph.startDestination = R.id.fragment_one
} else {
navController.graph.startDestination = R.id.fragment_two
}
}
Looks like there is some wrong behaviour in the library and my approach wasn't 100% correct too. At least, there is the better one and it works well.
Because I am using the same graph and only changing the start destination, I can simply set that graph in onCreate of my activity and set some default start destination there. Then, in createGraph method, I can do the following:
// pop backStack while it is not empty
while (navController.currentBackStackEntry != null) {
navController.popBackStack()
}
// then just navigate to desired destination with additional arguments if needed
navController.navigate(destinationId, destinationBundle)
I'm using the Navigation Component in android where I have set 6 fragments initially. The problem is when I added a new fragment (ProfileFragment).
When I navigate to this new fragment from the start destination, pressing the native back button does not pop the current fragment off. Instead, it just stays to the fragment I'm in - the back button does nothing.
Here's my 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/dashboard_navigation"
app:startDestination="#id/dashboardFragment"
>
<fragment
android:id="#+id/dashboardFragment"
android:name="com.devssocial.localodge.ui.dashboard.ui.DashboardFragment"
android:label="DashboardFragment"
>
<action
android:id="#+id/action_dashboardFragment_to_newPostFragment"
app:destination="#id/newPostFragment"
app:enterAnim="#anim/slide_in_up"
app:exitAnim="#anim/slide_out_down"
app:popEnterAnim="#anim/slide_in_up"
app:popExitAnim="#anim/slide_out_down"
/>
<action
android:id="#+id/action_dashboardFragment_to_notificationsFragment"
app:destination="#id/notificationsFragment"
app:enterAnim="#anim/slide_in_up"
app:exitAnim="#anim/slide_out_down"
app:popEnterAnim="#anim/slide_in_up"
app:popExitAnim="#anim/slide_out_down"
/>
<action
android:id="#+id/action_dashboardFragment_to_mediaViewer"
app:destination="#id/mediaViewer"
app:enterAnim="#anim/slide_in_up"
app:exitAnim="#anim/slide_out_down"
app:popEnterAnim="#anim/slide_in_up"
app:popExitAnim="#anim/slide_out_down"
/>
<action
android:id="#+id/action_dashboardFragment_to_postDetailFragment"
app:destination="#id/postDetailFragment"
app:enterAnim="#anim/slide_in_up"
app:exitAnim="#anim/slide_out_down"
app:popEnterAnim="#anim/slide_in_up"
app:popExitAnim="#anim/slide_out_down"
/>
====================== HERE'S THE PROFILE ACTION ====================
<action
android:id="#+id/action_dashboardFragment_to_profileFragment"
app:destination="#id/profileFragment"
app:enterAnim="#anim/slide_in_up"
app:exitAnim="#anim/slide_out_down"
app:popEnterAnim="#anim/slide_in_up"
app:popExitAnim="#anim/slide_out_down"
/>
=====================================================================
</fragment>
<fragment
android:id="#+id/profileFragment"
android:name="com.devssocial.localodge.ui.profile.ui.ProfileFragment"
android:label="fragment_profile"
tools:layout="#layout/fragment_profile"
/>
</navigation>
In the image above, the highlighted arrow (in the left) is the navigation action I'm having troubles with.
In my Fragment code, I'm navigating as follows:
findNavController().navigate(R.id.action_dashboardFragment_to_profileFragment)
The other navigation actions are working as intended. But for some reason, this newly added fragment does not behave as intended.
There are no logs showing when I navigate to ProfileFragment and when I press the back button.
Am I missing something? or is there anything wrong with my action/fragment configurations?
EDIT:
I do not do anything in ProfileFragment. Here's the code for it:
class ProfileFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
return inflater.inflate(R.layout.fragment_profile, container, false)
}
}
And my activity xml containing the nav host:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
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">
<fragment
android:id="#+id/dashboard_navigation"
app:layout_behavior="#string/appbar_scrolling_view_behavior"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:navGraph="#navigation/dashboard_navigation"
app:defaultNavHost="true"/>
</FrameLayout>
if you are using setupActionBarWithNavController in Navigation Component such as:
setupActionBarWithNavController(findNavController(R.id.fragment))
then also override and config this methods in your main activity:
override fun onSupportNavigateUp(): Boolean {
val navController = findNavController(R.id.fragment)
return navController.navigateUp() || super.onSupportNavigateUp()
}
My MainActivity.kt
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
setupActionBarWithNavController(findNavController(R.id.fragment))
}
override fun onSupportNavigateUp(): Boolean {
val navController = findNavController(R.id.fragment)
return navController.navigateUp() || super.onSupportNavigateUp()
}
}
For anyone using LiveData in a previous Fragment which is a Home Fragment, whenever you go back to the previous Fragment by pressing back button the Fragment is starting to observe the data and because ViewModel survives this operation it immediately emits the last emitted value which in my case opens the Fragment from which I pressed the back button, that way it looks like the back button is not working the solution for this is using something that emits data only once. I used this :
class SingleLiveData<T> : MutableLiveData<T>() {
private val pending = AtomicBoolean()
/**
* Adds the given observer to the observers list within the lifespan of the given
* owner. The events are dispatched on the main thread. If LiveData already has data
* set, it will be delivered to the observer.
*
* #param owner The LifecycleOwner which controls the observer
* #param observer The observer that will receive the events
* #see MutableLiveData.observe
*/
#MainThread
override fun observe(owner: LifecycleOwner, observer: Observer<in T>) {
super.observe(owner, Observer { t ->
if (pending.compareAndSet(true, false)) {
observer.onChanged(t)
}
})
}
/**
* Sets the value. If there are active observers, the value will be dispatched to them.
*
* #param value The new value
* #see MutableLiveData.setValue
*/
#MainThread
override fun setValue(value: T?) {
pending.set(true)
super.setValue(value)
}
This problem happened to me while using MutableLiveData to navigate between fragments and was observing the live data object at more than one fragment.
I solved it by observing the live data object one time only or by using SingleLiveEvent instead of MutableLiveData. So If you're having the same scenario here, try to observe the live data object one time only or use SingleLiveEvent.
You can use this following for the Activity
onBackPressedDispatcher.addCallback(
this,
object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
onBackPressed()
// if you want onBackPressed() to be called as normal afterwards
}
}
)
For the fragment, It will be needed requireActivity() along with Callback
requireActivity().onBackPressedDispatcher.addCallback(
this,
object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
requireActivity().onBackPressed()
// if you want onBackPressed() to be called as normal afterwards
}
}
)
If you have a Button or something else to perform an action then you can use
this.findNavController().popBackStack()
You need to set the MutableLiveData to null once the navigation is done.
For example
private val _name = MutableLiveData<String>()
val name: LiveData<String>
get() = _name
fun printName(){
_name.value = "John"
}
fun navigationComplete(){
_name.value = null
}
Now say you are observing the "name" in your fragment and you are doing some navigation once the name is John then should be like that:
viewModel.name.observe(viewLifecycleOwner, Observer { name ->
when (name) {
"John" -> {
this.findNavController() .navigate(BlaBlaFragmentDirections.actionBlaBlaFragmentToBlaBlaFragment())
viewModel.navigationComplete()
}
}
})
Now your back button will be working without a single problem.
Some data are almost used only once, like a Snackbar message or navigation event therefore you must tell set the value to null once done used.
The problem is that the value in _name remains true and it’s not possible to go back to previous fragment.
If you use Moxy or similar libs, checkout the strategy when you navigate from one fragment to second.
I had the same issue when strategy was AddToEndSingleStrategy.
You need change it to SkipStrategy.
interface ZonesListView : MvpView {
#StateStrategyType(SkipStrategy::class)
fun navigateToChannelsList(zoneId: String, zoneName: String)
}
Call onBackPressed in OnCreateView
private fun onBackPressed() {
requireActivity().onBackPressedDispatcher.addCallback(this) {
//Do something
}
}
For everyone who is using LiveData for setting navigation ids, there's no need to use SingleLiveEvent. You can just set the destinationId as null after you set its initial value.
For instance if you want to navigate from Fragment A to B.
ViewModel A:
val destinationId = MutableLiveData<Int>()
fun onNavigateToFragmentB(){
destinationId.value = R.id.fragmentB
destinationId.value = null
}
This will still trigger the Observer in the Fragment and will do the navigation.
Fragment A
viewModel.destinationId.observe(viewLifecycleOwner, { destinationId ->
when (destinationId) {
R.id.fragmentB -> navigateTo(destinationId)
}
})
The Simplest Answer for your problem (If it has something to do with fragments - Bottom navigation) could be
To set defaultNavHost = "false"
From Official Documentation it says->
Let's say you have 3 fragments set for Bottom Navigation, then setting
"defaultNavHost = true" will make fragment A acts like a parent, so when user clicks on back button in fragment 3 , it comes to fragment 1 instead of closing the activity (Bottom Navigation as Example).
Your XML should look like this, if you wanna just press back and close the activity from any fragment you are in.
<fragment
android:id="#+id/fragmentContainerView"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_above="#+id/bottom_nav"
app:defaultNavHost="false"
app:navGraph="#navigation/visit_summary_navigation" />
Set the MutableLiveData to false after navigation
Put this code in your ViewModel.kt
private val _eventNextFragment = MutableLiveData<Boolean>()
val eventNextFragment: LiveData<Boolean>
get() = _eventNextFragment
fun onNextFragment() {
_eventNextFragment.value = true
}
fun onNextFragmentComplete(){
_eventNextFragment.value = false
}
Let's say you want to navigate to another fragment, you'll call the onNextFragmentComplete method from the viewModel immediately after navigating action.
Put this code in your Fragment.kt
private fun nextFragment() {
val action = actionFirstFragmentToSecondFragment()
NavHostFragment.findNavController(this).navigate(action)
viewModel.onNextFragmentComplete()
}
I had faced the same issue due to the below "run blocking" code block. So don't use it if not necessary.
I am having difficulty with implementing a proper navigation structure in my app. I want it to behave similarly to the navigation in YouTube and Instagram. The biggest problem I am having is with the backstack and fragment recreation.
I'm currently using the single activity with multiple fragments approach. I have an app bar and bottom navigation view with 3 menu items setup in the main activity. The app bar has one menu item that navigates to the user profile fragment when selected, and each of the bottom nav's menu items navigates to different root fragments(home, search, and profile) when selected. I'm also using google's firebase database and firestore to store user data(email, uid, password, etc...) and photos.
I've tried using the supportFragmentManager.beginTransaction().replace way, and android jetpack's navigation architecture, but haven't been able to produce the results I need with either.
I'm able to navigate to the proper destinations using the supportFragmentManager way, but can't seem to implement a proper backwards navigation structure. I've tried to find other code samples of implementing this, but was unable to find anything that works, and a lot of these samples are older versions in java code with deprecated methods, which makes it difficult when trying to convert to kotlin code.
The jetpack navigation component is a bit easier to use, but I cannot get it to behave properly either. To my knowledge, the current navigation does not support multiple backstacks and does not have a proper backwards navigation structure unless you add the NavigationExtensions file provided here: https://github.com/googlesamples/android-architecture-components/tree/master/NavigationAdvancedSample. Using this sample, I am having problems with:
1.Navigating backwards does not return to the originally saved fragment state, it instead recreates a brand new fragment.
2.Navigating to the profile fragment from the app bar works, but crashes when the user is inside the fragment and presses it again.
3.Passing a default set of arguments to the user fragment item menu in the bottom navigation view. I originally had the account profile fragment tied to a bottom nav menu item (still do for testing purposes) with the logged in user's uid set as the default arguments. The fragment(UserFragment) used takes the uid argument and uses it to fetch the proper information from google's firebase. I was previously able to achieve this by using the regular jetpack navigation component(without the advanced sample) and adding the following code in the MainActivity:
val navArgument1 = NavArgument.Builder().setDefaultValue(uid).build()
val orderDestination = navController.graph.findNode(R.id.user_Fragment)
orderDestination?.addArgument("destinationUid",navArgument1)
Then within the user fragment, I use this code to get the proper uid:
uid = arguments?.getString("destinationUid")
With the advanced sample navigation component, I'm not able to pass this default argument into the user fragment. I keep getting an error that says something like "There is no navigation controller associated with this fragment," and the app crashes.
The Main Activity
class ExploreActivity : AppCompatActivity(),BottomNavigationView.OnNavigationItemSelectedListener{
override fun onNavigationItemSelected(p0:MenuItem):Boolean{
when(p0.itemId){
R.id.home->{
val homeViewFragment = HomeViewFragment()
supportFragmentManager.beginTransaction().replace(R.id.nav_host_fragment,homeViewFragment).commit()
return true
}
R.id.world->{
val publicViewFragment = PublicViewFragment()
supportFragmentManager.beginTransaction().replace(R.id.nav_host_fragment,publicViewFragment).commit()
return true
}
R.id.account->{
val userFragment = UserFragment()
val bundle = Bundle()
val uid=FirebaseAuth.getInstance().currentUser?.uid
bundle.putString("destinationUid",uid)
userFragment.arguments=bundle
supportFragmentManager.beginTransaction().replace(R.id.nav_host_fragment,userFragment).commit()
return true
}
}
return false
}
override fun onCreate(savedInstanceState:Bundle?){
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_explore)
bottom_navigation_explore.setOnNavigationItemSelectedListener(this)
bottom_navigation_explore.selectedItemId=R.id.home
}
override fun onActivityResult(requestCode:Int,resultCode:Int,data:Intent?){
super.onActivityResult(requestCode,resultCode,data)
if(requestCode==UserFragment.PICK__PROFILE_FROM_ALBUM&&resultCode==Activity.RESULT_OK){
val imageUri=data?.data
val uid=FirebaseAuth.getInstance().currentUser?.uid
val storageRef=FirebaseStorage.getInstance().reference.child("userProfileImages")
.child(uid!!)
storageRef.putFile(imageUri!!).continueWithTask{task:Task<UploadTask.TaskSnapshot>->
return#continueWithTask storageRef.downloadUrl
}.addOnSuccessListener{uri->
val map=HashMap<String,Any>()
map["image"]=uri.toString()
FirebaseFirestore.getInstance().collection("profileImages").document(uid).set(map)
}
}
}
}
User Fragment
class UserFragment : Fragment(){
var fragmentView : View? = null
var firestore : FirebaseFirestore? = null
var uid : String? = null
var auth : FirebaseAuth? = null
var currentUserUid : String? = null
companion object{
var PICK__PROFILE_FROM_ALBUM = 10
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
fragmentView = LayoutInflater.from(activity).inflate(R.layout.activity_main,container,false)
uid = arguments?.getString("destinationUid")
firestore = FirebaseFirestore.getInstance()
auth = FirebaseAuth.getInstance()
currentUserUid = auth?.currentUser?.uid
return fragmentView
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
if(uid == currentUserUid) {
fragmentView?.btn_follow_signout_main?.text = "Signout"
fragmentView?.btn_follow_signout_main?.setOnClickListener {
activity?.finish()
startActivity(Intent(activity, LoginActivity::class.java))
auth?.signOut()
}
requestPermissions(arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE),1)
iv_createpost_main.setOnClickListener {
if (ContextCompat.checkSelfPermission(context!!, Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED){
startActivity(Intent(activity,CreatePost::class.java))
}
return#setOnClickListener
}
//add explanation of why permission is needed
if (ContextCompat.checkSelfPermission(context!!, Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) {
iv_profilepicture_main.setOnClickListener {
val intent = Intent(Intent.ACTION_PICK)
intent.type = "image/*"
activity?.startActivityForResult(intent, PICK__PROFILE_FROM_ALBUM)
}
}
}
else{
fragmentView?.btn_follow_signout_main?.text = "Follow +"
fragmentView?.btn_follow_signout_main?.setOnClickListener {
requestFollow()
}
}
getProfileImage()
getUserName()
}
private fun getProfileImage() {
firestore?.collection("profileImages")!!.document(uid!!).get().addOnCompleteListener { task ->
if(task.isSuccessful){
val url = task.result!!["image"]
if(url != null){
Glide.with(activity!!).load(url).into(iv_profilepicture_main)
}
else{
iv_profilepicture_main.setImageResource(R.drawable.ic_account)
}
}
}
}
private fun getUserName(){
firestore?.collection("users")!!.document(uid!!).get().addOnCompleteListener { task ->
if(task.isSuccessful){
val username = task.result!!["username"]
if(username != null){
activity?.setTitle("" + username)
}
}
}
}
}
My project is currently setup using the support fragment manager, but I've been going back and forth between using it and the navigation component to try and make things work.
I have two other fragments tied to the bottom nav, but I've only included the relevant code where I believe my problem lies. The other two fragments have a user profile picture that navigates the user to the selected profile when it is clicked. I do not have any problems with those transactions, because I can easily apply the bundle and arguments with the setOnClickListener method.
TL;DR
To summarize everything: I am looking for a way to implement a proper navigation flow throughout my app. I'm having problems with backwards navigation and fragments being recreated when they shouldn't. I've tried using the fragment manager and the android jetpack navigation component, but haven't had luck with either. If anyone has any information on how to achieve this using android kotlin and the latest methods, and would like to share, I'd appreciate it.
Thanks.
I can not update NavDestination's label at runtime.
it reflects but not from the first time i enter the screen, it doesn't reflected instantaneously
My ViewModel
class PrepareOrderDetailsViewModel(
brief: MarketHistoryResponse,
private val ordersRepository: OrdersRepository
) : BaseViewModel() {
private val _briefLiveData = MutableLiveData(brief)
val orderIdLiveData: LiveData<Int?> =
Transformations.distinctUntilChanged(Transformations.map(_briefLiveData) { it.id })
}
LiveData observation in the fragment
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
registerObservers()
}
private fun registerObservers() {
viewModel.orderIdLiveData.observe(viewLifecycleOwner, Observer {
findNavController().currentDestination?.label = getString(R.string.prepare_order_details_title, it)
})
}
As per the Navigation UI documentation, the NavigationUI methods, such as the setupActionBarWithNavController() method rely on an OnDestinationChangedListener, which gets called every time you navigate() to a new destination. That's why the label is not instantly changed - it is only updated when you navigate to a new destination.
The documentation does explain that for the top app bar:
the label you attach to destinations can be automatically populated from the arguments provided to the destination by using the format of {argName} in your label.
This allows you to update your R.string.prepare_order_details_title to be in the form of
<string name="prepare_order_details_title">Prepare order {orderId}</string>
By using that same argument on your destination, your title will automatically be populated with the correct information.
Of course, if you don't have an argument that you can determine ahead of time, then you'd want to avoid setting an android:label on your destination at all and instead manually update your action bar's title, etc. from that destination.
I reach to a workaround for that issue by accessing the SupportActionBar itself and set the title on label behalf
private fun registerObservers() {
viewModel.orderIdLiveData.observe(viewLifecycleOwner, Observer {
(activity as AppCompatActivity).supportActionBar?.title =
getString(R.string.prepare_order_details_title, it)
})
}