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))
}
}
}
Related
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 have basic ViewPager2 with Tablayout - and in each page I have different fragments.
When I need to open this view NOT from first (default) tab I'm doing smth like this:
viewPager.currentItem = selectedTabPosition
This code select tab, but inside it is opening fragment from first tab! Only when I'm selecting the tabs by tapping on it - I can see the right fragment in each tabs.
I've also try to select with Tablayout like this:
tabLayout.getTabAt(position)?.select()
But this code doesn't help and also work with this bug.
Also I've try to set viewPager.currentItem with post / postDelay - but this doesn't work too.
Maybe I've lost something? Or it is a bug in ViewPager2 ?
(EDIT - ViewPager code)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupPagerAdapter()
}
private fun setupPagerAdapter() {
val adapter = MainDocumentScreenPagerAdapter(this)
binding?.viewPager?.letUnit {
it.adapter = adapter
binding?.tabsPagerView?.attachViewPager(requireContext(), it, adapter)
// set tab
it.currentItem = params.pageType.ordinal
}
Adapter code
class MainDocumentScreenPagerAdapter (fragment: Fragment) : ViewPager2TitleAdapter(fragment) {
override fun getItemCount(): Int = DocumentPageType.values().size
override fun createFragment(position: Int): Fragment {
val pageType = DocumentPageType.values().firstOrNull { it.ordinal == position } ?: throw IllegalStateException()
val params = DocumentListFragment.createParams(pageType)
return DocumentListFragment.newInstance(params)
}
override fun getPageTitle(position: Int): Int? {
return when (position) {
DocumentPageType.ALL.ordinal -> DocumentPageType.ALL.title
DocumentPageType.SIGN.ordinal -> DocumentPageType.SIGN.title
DocumentPageType.ACCEPT.ordinal -> DocumentPageType.ACCEPT.title
DocumentPageType.CONFIRM.ordinal -> DocumentPageType.CONFIRM.title
DocumentPageType.REJECT.ordinal -> DocumentPageType.REJECT.title
else -> null
}
}
Where ViewPager2TitleAdapter is :
abstract class ViewPager2TitleAdapter(fragment: Fragment) : FragmentStateAdapter(fragment) {
abstract fun getPageTitle(position: Int): Int?
DocumentListFragment inside it create view, based on param object.
I've also try to create adapter inside OnCreate - but it doesn't affect this case.
The last, but not least - when I've try to open tab, which is out from screen (I've scrollable tabs) - viewPager open selected tab with correct fragment on it.. So, the problem occurs only when I've try to open first 4 tabs (look at the image), which is showing on screen. Bit starting from 5 and next tabs - has selected correct.
So, the decision is in this line of code:
it.setCurrentItem(params.pageType.ordinal, false)
but I was doing it like this:
it.currentItem = params.pageType.ordinal
boolean false make magic in this case - It disable smooth scroll. I've got it from this answer about ViewPager2:
https://stackoverflow.com/a/67319847/4809482
I think an easier more reliable fix is to defer to next run cycle instead of unsecure delay e.g
viewPager.post {
viewPager.setCurrentItem(1, true)
}
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.
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
I'm currently using Android Navigation Architecture in my project. It has a feature that can launch any fragment with a shortcut. Currently I'm using NavController to navigate to desired destination when clicking at a shortcut.
But when I clicked a shortcuts with multiple times, every time a new instance of the fragment will be created.
So, my question is, Is there any way to only accept one instance of a fragment when navigate to it with NavController?
I'm googling many times but found nothing. Thanks in advance.
Add a check before navigating to the destination as it would not add a new instance.
class A: AppCompatActivity {
fun onCreate(...) {
// ...While navigating
if (navController.currentDestination?.id != desiredDestination?.id) {
navController.navigate(desiredDestination)
}
// ...else do nothing
}
}
Callback from NavController: https://developer.android.com/reference/androidx/navigation/NavController#addOnDestinationChangedListener(androidx.navigation.NavController.OnDestinationChangedListener)
You can use by navGraphViewModels delegate
The most important thing is to set id to your views in order to save state during config changes.This has not mentioned in official docs.
by default fragment navigation won't be saved during config changes(rotation and ...).
ViewModel will remain across config changes and you can save your state there then restore it.
Also check these links:
https://code.luasoftware.com/tutorials/android/android-jetpack-navigation-lost-state-after-navigation/
and
Android navigation component: how save fragment state
You can use safeOnClickListener instead of default onClickListener for capturing click on shortcut, so basically with safeOnClickListener you ignore all the click event for a given duration.
class SafeClickListener(
private var defaultInterval: Int = 2000,
private val onSafeCLick: (View) -> Unit
) : View.OnClickListener {
private var lastTimeClicked: Long = 0
override fun onClick(v: View) {
if (SystemClock.elapsedRealtime() - lastTimeClicked < defaultInterval) {
return
}
lastTimeClicked = SystemClock.elapsedRealtime()
onSafeCLick(v)
}
}
fun View.setSafeOnClickListener(delay: Int = 2000, onSafeClick: (View) -> Unit) {
val safeClickListener = SafeClickListener(delay) {
onSafeClick(it)
}
setOnClickListener(safeClickListener)
}