Move between fragments without recreation- Kotlin - android

I'm trying to move between two fragments without recreation of them so the data in the previous fragment won't disappear.
I tried to look over the internet for answers and tried for hours but without success. I looked at those links:
link 1
link 2
link 3- android developer site
After show() and hide() I also tried the AddToBackStack() but yet no success
link 4
class MainActivity : AppCompatActivity(){
private val onNavigationItemSelectedListener = BottomNavigationView.OnNavigationItemSelectedListener { item ->
#Override
when (item.itemId) {
R.id.navigation_home -> {
//replaceFragment(SignInFragment())
supportFragmentManager.beginTransaction().hide(AllEventsFragment()).commit()
supportFragmentManager.beginTransaction().show(SignInFragment()).commit()
return#OnNavigationItemSelectedListener true
}
R.id.navigation_events -> {
//replaceFragment(AllEventsFragment())
supportFragmentManager.beginTransaction().hide(SignInFragment()).commit()
supportFragmentManager.beginTransaction().show(AllEventsFragment()).commit()
if (currentUser.isNotEmpty()) {
updateRecyclerView()
sign_in_error?.visibility = View.INVISIBLE
}
return#OnNavigationItemSelectedListener true
}
}
return#OnNavigationItemSelectedListener false
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
supportFragmentManager.beginTransaction().add(R.id.activity_main, AllEventsFragment(), "2").commit()
supportFragmentManager.beginTransaction().add(R.id.activity_main, SignInFragment(), "1").commit()
val navView: BottomNavigationView = findViewById(R.id.nav_view)
navView.setOnNavigationItemSelectedListener(onNavigationItemSelectedListener)
personInEvent = false
}
The result is overlapping fragments without an option to really navigate between them. I really tried everything I know there are some answers over the internet but none of them helped me fix my issue. I would really appreciate some help with this frustrating issue.
Before navigation:
After navigation:

supportFragmentManager.beginTransaction().hide(AllEventsFragment()).commit()
your recreating your fragments every time!
calling AllEventsFragment() is equivelant to new AllEventsFragment()
you need to instantiate them first
for example,
your code needs to be like this,
val fragment1: Fragment = SignInFragment()
val fragment2: Fragment = AllEventsFragment()
var active = fragment1
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
supportFragmentManager.beginTransaction().add(R.id.activity_main,fragment2 , "2").commit()
supportFragmentManager.beginTransaction().add(R.id.activity_main, fragment1, "1").commit()
val navView: BottomNavigationView = findViewById(R.id.nav_view)
navView.setOnNavigationItemSelectedListener(onNavigationItemSelectedListener)
personInEvent = false
}
inside your listener
R.id.navigation_home -> {
supportFragmentManager.beginTransaction().beginTransaction().hide(active).show(fragment1).commit();
active = fragment1;
return#OnNavigationItemSelectedListener true
}
R.id.navigation_events -> {
//replaceFragment(AllEventsFragment())
supportFragmentManager.beginTransaction().beginTransaction().hide(active).show(fragment2).commit();
active = fragment2
)
//handle rest of the cases

Take a look at architicture components, yuo can also achieve that in the old way Android - save/restore fragment state
When a fragment isnt viewable its paused or might even be destroyed use bundle to perserve data.
What you are trying to acheive could be done using two containers but you really shouldn't

Related

Error using <FragmentContainerView>, but no error with <fragment>

I am developing a mobile app and I am currently trying to rework my workflow to more appropriately leverage Activities and Fragments for their intended purposes and I have run across a strange issue I can't figure out. I have a fragment I am trying to add to an Activity, but what I try and use FragmentContainerView, the app crashes on launch, but it doesn't happen when I just use a tag with all the same attributes. In looking at the Logcat, the error comes from null being assigned to the last line of the utils file where it tries to assign to topAppBar view the view with an id of top_app_bar. Here is the relevant code:
MainActivity.kt
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
fragmentManager.beginTransaction().replace(R.id.frame_layout, HomeFragment()).commit()
utils = Utils(this)
topAppBar = findViewById(R.id.top_app_bar)
drawerLayout = findViewById(R.id.drawer_layout)
val navigationView: NavigationView = findViewById(R.id.navigation_view)
topAppBar.setNavigationOnClickListener {
if (!drawerLayout.isDrawerOpen(GravityCompat.START)) {
drawerLayout.openDrawer(GravityCompat.START)
}
else {
drawerLayout.closeDrawer(GravityCompat.START)
}
}
navigationView.setNavigationItemSelectedListener { item ->
val id: Int = item.itemId
drawerLayout.closeDrawer(GravityCompat.START)
when (id) {
R.id.navigation_home -> { utils.replaceFragment(HomeFragment(), getString(R.string.app_name)) }
R.id.navigation_recipes -> { utils.replaceActivity(this, item.title.toString().lowercase()) }
R.id.navigation_budget -> { utils.replaceActivity(this, item.title.toString().lowercase()) }
R.id.navigation_inventory -> { utils.replaceActivity(this, item.title.toString().lowercase()) }
R.id.navigation_customers -> { utils.replaceActivity(this, item.title.toString().lowercase()) }
R.id.navigation_reports -> { utils.replaceActivity(this, item.title.toString().lowercase()) }
}
true
}
activity_main.xml
<androidx.fragment.app.FragmentContainerView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="#+id/fragment_container_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:name="com.example.bakingapp.ui.TopAppBarFragment"
tools:layout="#layout/fragment_top_app_bar" />
Utils.kt (There is more to this file, but it is not relevant to the problem)
class Utils(activity: Activity) {
private var currentActivity: Activity
private var fragmentManager: FragmentManager
private var topAppBar: MaterialToolbar
val activitiesList = listOf("recipes", "budget", "inventory", "customers",
"reports")
init {
currentActivity = activity
fragmentManager = (activity as AppCompatActivity).supportFragmentManager
topAppBar = currentActivity.findViewById(R.id.top_app_bar)
}
}
This works perfectly fine when I use instead of what is currently there, but I get warnings saying I shouldn't use fragment. I should be able to use the more proper tag, but I don't understand why it can't find the view when I use this method, but it can find the view when I use the tag. If someone could explain what is happening here and what I can do to fix the issue, I would really appreciate it.
Step 1 : Add FragmentContainerView to your activity xml
<androidx.fragment.app.FragmentContainerView
android:id="#+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent" />
Step 2 : In your MainActivity.class file, declare FragmentManager
private FragmentManager manager;
Step 3 : Initialize FragmentManager in onCreate()
manager = getSupportFragmentManager();
Step 4 : In your onOptionsItemSelected() begin this fragment
Bundle bundle = new Bundle();
manager.beginTransaction()
.replace(R.id.container/*Your View Id*/, YourFragment.class, bundle, "TAG")
.setReorderingAllowed(true)
//.setCustomAnimations(R.anim.anim_enter, R.anim.anim_exit)
.addToBackStack("TAG")
.commit();
Have you tried putting FragmentContainerView inside a layout? Instead of using it as parent layout.
That could solve it.
I managed to figure out the problem. It was a scope issue. A lot of logic for a Fragment I was handling in MainActivity. I moved pretty much everything in my onCreate function into the relevant Fragment file and from there was able to get the FragmentContainerView working after refactoring this code

When setting navigation graph programmatically after recreating activity, wrong fragment is shown

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)

androidx - Implement androidx Preferences in an Activity (Kotlin)

What's the objective
Im currently working on an app, which has a settings screen, containing a recyclerview. This recyclerview, on item click, opens the relative activity, which are the Settings' submenus (Code below). In the newly opened activity, i want to implement using the implementation androidx.prefrence a preference screen.
What's the problem
For this, im following this video. In this video, they set up a preference screen using PreferenceFragmentCompact. The problem with this, is the fact that we are setting up a new activity and then a new fragment, which in my app its not efficient, since im building my settings with just activities.
Considering this, is it possible to setup a Preference screen in an activity without using fragments? If so, how?
Code:
class ActivitySettings : AppCompatActivity(), AdapterSettings.OnItemClickListener {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_settings)
topToolbarBack.setNavigationOnClickListener {
finish()
}
val settingsList = listOf(
DataItemsSettings(getString(R.string.look), getString(R.string.lookdescription), R.drawable.ic_colored_color_lens),
DataItemsSettings(getString(R.string.playing), getString(R.string.playingdescription), R.drawable.ic_colored_view_carousel),
DataItemsSettings(getString(R.string.images), getString(R.string.imagesdscription), R.drawable.ic_colored_image),
DataItemsSettings(getString(R.string.audio), getString(R.string.audiodescription), R.drawable.ic_colored_volume_up),
DataItemsSettings(getString(R.string.other), getString(R.string.otherdescription), R.drawable.ic_colored_shape),
DataItemsSettings(getString(R.string.about), getString(R.string.aboutdescription), R.drawable.ic_colored_info)
)
val adapter = AdapterSettings(settingsList, this)
rvSettings.adapter = adapter
rvSettings.layoutManager = LinearLayoutManager(this)
}
override fun OnItemClick(position: Int) {
when(position) {
0 -> this.startActivity(Intent(this, ActivitySettingsLook::class.java))
1 -> this.startActivity(Intent(this, ActivitySettingsPlaying::class.java))
2 -> this.startActivity(Intent(this, ActivitySettingsImages::class.java))
3 -> this.startActivity(Intent(this, ActivitySettingsAudio::class.java))
4 -> this.startActivity(Intent(this, ActivitySettingsOther::class.java))
5 -> this.startActivity(Intent(this, ActivityAbout::class.java))
}
}
}

androidx.navigation multiple destinations for menu item

I have a single activity app using the androidx navigation library. For one of the menu destinations I effectively have a fragment as destination with no view whatsoever that depending on the state of the user provided configuration either redirects to the real destination that should be there or to one of currently two different views that tell the user that either he needs to setup a configuration first or that there currently is no active configuration (deleted?) and he needs to select one of the available configurations.
Now, functionally this approach works perfectly fine. However, since androidx navigation ties menu items to destinations by id the menu item that gets you to that view is never selected as it matches the fragment destination with no view in it.
I tried to add a NavController.OnDestinationChangedListener to my Activity and added it to the navController navController.addOnDestinationChangedListener(this). But it seems to get overwritten by the navigation afterwards.
override fun onDestinationChanged(controller: NavController, destination: NavDestination, arguments: Bundle?) {
val destinations = listOf(R.id.destinationA, R.id.destinationB, R.id.destinationC)
if(destinations.contains(destination.id)) {
nav_view.menu.getItem(0).isChecked = true
}
}
It is deffinitely the right menu item. As when I change isChecked = true to isEnabled = false I can no longer click on it.
Also when I do this odd hack it works
GlobalScope.launch(Dispatchers.Main) {
delay(1000)
nav_view.menu.getItem(0).isChecked = true
}
Needless to say this is not a very good solution.
Anyone here knows how to overwride the default behaviour of androidx navigation in this regard?
I´ll come back to this later and report back if I find a proper solution to this.
Adding a listener to the drawer opening and setting the selected menu item then might be a good workaround for this if it is not possible to do currently.
Instead of using setupWithNavController(), as mentioned in the documentation, setup it up yourself.
As mentioned here, onNavDestinationSelected() helper method in NavigationUI is called when the menu item is clicked when you set it up using setupWithNavController(). So you could try something like this:
yourNavigationView.setNavigationItemSelectedListener { item: MenuItem ->
if(item.itemId == R.id.noViewFragmentId) {
val isConfigurationProvided = ...
if(!isConfigurationProvided) {
//Perform your actions (navigate to either of the two alternate views)
return#setNavigationItemSelectedListener true
}
}
val success = NavigationUI.onNavDestinationSelected(item, navController)
if(success) {
drawerLayout.closeDrawer(GravityCompat.START)
item.isChecked = true
}
success
}
I´ll add this as a possible solution and stick with it for the time being. I still feel like there should be a better way to do this, so I will not accept it as an awnswer.
It´s essentially the idea I got at the end of writing the question
Adding a listener to the drawer opening and setting the selected menu item then might be a good workaround for this if it is not possible to do currently.
class SetActiveMenuDrawerListener(
private val navController: NavController,
navigationView: NavigationView) : DrawerLayout.DrawerListener {
private var checked = false
private val destinations = listOf(R.id.destinationA, R.id.destinationB, R.id.destinationC)
private val menu = navigationView.menu.getItem(0)
init {
navController.addOnDestinationChangedListener { _, _, _ -> checked = false }
}
override fun onDrawerSlide(drawerView: View, slideOffset: Float) {
}
override fun onDrawerOpened(drawerView: View) {
}
override fun onDrawerClosed(drawerView: View) {
}
override fun onDrawerStateChanged(newState: Int) {
if(checked) return
val currentDestination = navController.currentDestination ?: return
if(destinations.contains(currentDestination.id)) {
menu.isChecked = true
}
checked = true
}
}
Then add this to the DrawerLayout
drawer_layout.addDrawerListener(SetActiveMenuDrawerListener(navController, nav_view))
I did add the code into the onDrawerStateChanged instead onDrawerOpened, because onDrawerOpened gets called a bit late if clicking the drawer and not at all while dragging it.
It´s not the pretties thing to look at, but it gets the job done.

Android: BottomNavigationView first fragment switch is super delayed

Attempting to use a bottom nav view, the standard view provided by android. The first time I click on any fragment, it's a SUPER delayed UI reaction time (about 2 seconds until the ripple, item selection update, and new fragment show)
It's only the first time I switch to any fragment, after that, it behaves as expected.
I found a similar question already on here, but there were zero suggestions or answers. See that post here
Find below the logic I user for switching the fragments.
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
bottom_nav_bar.inflateMenu(R.menu.bottom_nav_bar_menu)
bottom_nav_bar.selectedItemId = R.id.nav_summary
bottom_nav_bar.setOnNavigationItemSelectedListener { menuItem ->
when (menuItem.itemId) {
R.id.nav_1-> startFragment1()
R.id.nav_2 -> startFragment2()
else -> startFragment3()
}
true
}
}
fun startFragment1() = replaceFragment(Fragment1(), "TAG1")
fun startFragment2() = replaceFragment(Fragment2(), "TAG2")
fun startFragment3() = replaceFragment(Fragment3(), "TAG3")
private fun replaceFragment(fragment: Fragment, fragmentTag: String) {
fragmentManager.beginTransaction()
.setCustomAnimations(R.animator.fade_in, R.animator.fade_out)
.replace(R.id.fragment_container, fragment, fragmentTag)
.commit()
}
Use
supportfragmentmanager
instead of just normal FragmentManager. It’s smoother. Of course you have to change to imports to v4 Fragment but after that it should work better. I was having all kinds of weird stuff happen till I made that switch.

Categories

Resources