I am using Android navigation with safe args and directions for my Android app. I am passing an Int argument in a navigation action. However, after the first call to that action the passed argument, is not getting updated.
This is how I set the argument loadedTab and navigate:
val direction = HomeFragmentDirections.actionNavigationHomeToNavigationHistoric()
direction.setLoadedTab(index)
findNavController().navigate(direction)
and this is how I get the argument in the destination fragment:
private val args : HistoricFragmentArgs by navArgs()
...
...
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val loadedTab = args.loadedTab
}
Both fragments (origin and destination) are part a bottom navigation view, so I don't have to go back to the origin (destroy the destination fragment) to be able to navigate to the destination again
Thanks in advance
Related
I have a custom dialog with X button, that dialog is called in a Fragment and here i have to manage all the clicks from the Alert.
Which is the best way to do so?
I actually was going to set the click listeners in the DialogFragment but i have to change some layout stuff and set variables from my Fragment so it will be better if i manage it from the fragment directly.
Here is my code now:
class ElencoDialog(private val testata: Testata, private val elimina: Boolean): DialogFragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
btnInvia = view.findViewById(R.id.btnInvia)
btnInvia.setOnClickListener {
}
}
}
And here is my fragment where i show the dialog:
class ElencoFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
adapter = LettureListAdapter({
ElencoDialog(it, false).show(parentFragmentManager, "ElencoDialog")
}, {
ElencoDialog(it, true).show(parentFragmentManager, "ElencoDialog")
})
}
}
So instead of managing the click from the DialogFragment how can i manage the clicks directly from my Fragment?
First of all it is bad practice to have anything apart from the default constructor in a DialogFragment (or any Fragment). Although this might work initially, the system might need to recreate the fragment for various reasons, rotation, low memory etc. and it will attempt to use an empty constructor to do so. You should instead be using fragment arguments to pass simple data (covered in another question), a ViewModel for more complex data (I prefer this method anyway) or the new fragment results API, which I've outlined below.
But in answer to your specific question, to interact between your dialog fragment and main fragment, you have a few options:
Target fragment
You can set your original fragment as a target of your dialog but using setTargetFragment(Fragment). The fragment can then be retrieved safely from your dialog using getTargetFragment. It would probably be best practice to have your fragment implement an interface which you can cast to has the relevant callback methods.
Fragment results API
This is a relatively new API that attempts to replace the above, you can read more about it here: Communicating between fragments.
ViewModel
You can use a shared ViewModel scoped to the activity or parent fragment and keep your state in there. This would also solve the problem of having to pass your initial state through your fragment constructor. I won't explain how they work here as that's another question, but I would take a look here: ViewModel overview.
Pass callBack from fragment to your Dialog Fragment
class ElencoFragment() : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
adapter = LettureListAdapter({
ElencoDialog(it, false){
//handle Callback
}.show(parentFragmentManager, "ElencoDialog")
}, {
ElencoDialog(it, true){
//handle Callback
}.show(parentFragmentManager, "ElencoDialog")
})
}
}
Your Dialog Fragment
class ElencoDialog(private val testata: Testata, private val elimina: Boolean, block : () -> Unit): DialogFragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
btnInvia = view.findViewById(R.id.btnInvia)
btnInvia.setOnClickListener {
block()
}
}
}
I'm trying to develop an app where I have spinner in fragment(first fragment), to update spinner options user has to open a dialog window(second fragment) and put input in there. Data is passed through interface as a bundle and then into first fragment arguments. My spinner is in first fragment view so I couldn't figure out how to call a first argument view function from second argument, instead inside first argument view I constantly check if the arguments are updated, something like this
if(!displayedText.isNullOrEmpty()) {
updateListSpinner()
arguments?.clear()
}
so in fragment view I constantly check if arguments were updated and then I clear them, everything works as it should but I just wonder what's better way of doing it.
You can use Fragment Result Listeners. More documentation: Communicating between fragments. So in your instance, whenever the user interacts with the dialog fragment, send a result to the first fragment, which will be registered to use the data and you can update your spinner.
Here is some working code that controls navigation in my app:
In second fragment:
open fun navigate(idOfDestination: Int, bundle: Bundle?) {
val tempBundle = bundle ?: Bundle()
tempBundle.putInt(NAVIGATION_DESTINATION, idOfDestination)
activity?.supportFragmentManager?.setFragmentResult(NAVIGATION_RESULT, tempBundle)
}
In target fragment (which in your case would be the first fragment), register the listener:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
activity?.supportFragmentManager?.setFragmentResultListener(NAVIGATION_RESULT, this, FragmentResultListener { _, bundle ->
val destination = bundle.getInt(NAVIGATION_DESTINATION)
bundle.remove(NAVIGATION_DESTINATION)
navigationHandler(bundle,destination)
})
}
private fun navigationHandler(bundle: Bundle?, idOfDestination: Int) {
navController = Navigation.findNavController(binding.root)
if (!FeatureControlManager.isDestinationAllowedToGo(idOfDestination)) {
navController = childFragmentManager.fragments.first().findNavController()
}
navController.validateAndNavigate(navController, idOfDestination, childFragmentManager, bundle)
}
Let me add more context:
I run bottom view navigation with a ViewPager2. For all 4 of my tab/fragments of my bottom navigation view I have an options menu which I created separately dynamically in each fragment.
Now, when we navigate through the app, it behaves correctly as every options menu is displayed ONLY for their respective fragment.
Problem is: Only when the app is launched, all the options menus from all the the 4 fragments show up on the start destination fragment's tab. BUT, once we swipe and swipe back, only the start destinations options menu is shown on the app bar. As is for every of the other 3 fragment/tabs.
Theory: I think it has something to do with onCreateOptionsMenu which is called when all four fragments are also created to which they share an app bar.
Is anybody familiar with this type of issue? Here is my PagerAdapters code for my ViewPager:
const val F1_PAGE_INDEX = 0
const val F2_PAGE_INDEX = 1
const val F3_PAGE_INDEX = 2
const val F4_PAGE_INDEX = 3
class PagerAdapter(fragment: Fragment) : FragmentStateAdapter(fragment) {
/**
* Mapping of the ViewPager page indexes to their respective Fragments
*/
private val tabFragmentsCreators: Map<Int, () -> Fragment> = mapOf(
F1_PAGE_INDEX to { FirstFragment() },
F2_PAGE_INDEX to { SecondFragment() },
F3_PAGE_INDEX to { ThirdFragment() },
F4_PAGE_INDEX to { FourthFragment() }
)
override fun getItemCount() = tabFragmentsCreators.size
override fun createFragment(position: Int): Fragment {
return tabFragmentsCreators[position]?.invoke() ?: throw IndexOutOfBoundsException()
}
}
Here is also my Home View Pager Fragment where I create my bottom Navigation and affect to to the main fragments:
class HomeViewPagerFragment(): Fragment() {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val binding = FragmentViewPagerBinding.inflate(inflater, container, false)
val viewPager = binding.viewPager
viewPager.isUserInputEnabled = false
viewPager.adapter = PagerAdapter(this)
//Save states of four fragments
viewPager.offscreenPageLimit = 4
(activity as AppCompatActivity).setSupportActionBar(binding.toolbar)
val bottomNavigation = binding.bottomNavView
bottomNavigation.setOnNavigationItemSelectedListener(
BottomNavigationView.OnNavigationItemSelectedListener { item ->
when (item.itemId) {
R.id.fragment1_destination -> viewPager.currentItem = F1_PAGE_INDEX
R.id.fragment2_destination -> viewPager.currentItem = F2_PAGE_INDEX
R.id.fragment3_destination -> viewPager.currentItem = F3_PAGE_INDEX
R.id.fragment4_destination -> viewPager.currentItem = F4_PAGE_INDEX
}
true
})
return binding.root
}
}
The starting destination (first fragment) is where all the fragment's menu are shown at app start.
Any kind of help is appreciated! Thank you!
Issue: After endless hours, we have found a solution. The issue was directly created when we set the off screen page limit to 4. That causes all the fragments to be created at the same time, thus, obligates the option menus to be shown at launch on the starting destinations fragment since we instructed setHasOptionsMenu(true) in the onCreate of each fragment.
Solution: Simply, set the options menu to true in the onResumeof the fragment to only be called when we swipe to the respective fragment in this manner:
override fun onResume() {
super.onResume()
setHasOptionsMenu(true)
}
I think what you need to do is, make OptionsMenu visible only when that fragment is visible.
Try this: Put this in each fragment
override fun onResume(){
super.onResume()
setHasOptionsMenu(isVisible())
}
This will make the options menu visible only when that fragment is visible. You can make it hidden in onCreateView or in onPause if just onResume doesn't work.
I'm creating a portfolio app that uses Android's new Jetpack Navigation NavController. I've created a fragment with a RecyclerView that will be populated with views that you can click on to navigate to various app demos (and maybe actual apps I've made if I can figure that out). I'm currently populating the list manually, but I'd like to automate that process.
class PortfolioFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
return inflater.inflate(R.layout.fragment_portfolio, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val navController = view.findNavController()
val currentDestination = navController.currentDestination
// How get list of actions?
val appActions = listOf(
R.id.action_portfolioFragment_to_testFragment
)
portfolio_app_list.layoutManager = LinearLayoutManager(context)
portfolio_app_list.adapter = PortfolioAdapter(appActions)
}
}
I couldn't find any methods on NavGraph, nor NavDestination to get the list of actions available for currentDestination. I know that my navigation_main.xml has them, and I could use XMLPullParser or something like that to get them, but I may start using the Safe Args plugin which we'll mean that getting the actual class from the XML would be a pain.
This might be considered a bit hacky.
NavDestination has a private property called mActions which contains the NavActions associated with the NavDestination.
I solved this by using reflection to get access to it.
val currentDestination = findNavController().currentDestination
val field = NavDestination::class.java.getDeclaredField("mActions")
field.isAccessible = true
val fieldValue = field.get(currentDestination) as SparseArrayCompat<*>
val appActions = arrayListOf<Any>()
fieldValue.forEach { key, any ->
appActions.add(any)
}
The NavActions are returned as a SparseArrayCompat which is then iterated to obtain the List
I'm starting bottomSheetDialogFragment from a fragment A.
I want to select the date from that bottomSheetDialogFragment then set it in the fragment A.
The select date is already done, I just want to get it in the fragment A to set it in some fields.
How can I get the value?
Any suggestions how to do it?
Create an interface class like this
public interface CustomInterface {
public void callbackMethod(String date);
}
Implement this interface in your Activity or Fragment. and make an object of this Interface.
private CustomInterface callback;
Initialize it in onCreate or onCreateView
callback=this;
Now pass this callback in your BottomSheetDialogFragment constructor when you call it.
yourBottomSheetObject = new YourBottomSheet(callback);
yourBottomSheetObject.show(getSupportFragmentManager()," string");
Now in your BottomSheetFragment's constructor
private CustomInterface callback;
public SelectStartTimeSheet(CustomInterface callback){
this.callback=callback;
}
And at last use this callback object to set your date
callback.callbackMethod("your date");
and yout will recieve this date in your Fragment or Your Activity in callbackMethod function.
override the constructor of a fragment is a bad practice as the document said:
Every fragment must have an
* empty constructor, so it can be instantiated when restoring its
* activity's state.
if you using another constructor that passing a callback as the param, when the fragment is resotored by the framework, your app crash
the recommend way is using viewModel and livedata.
Android navigation architecture component
eg:
Suppose you open Fragment B from Fragment A using navController.
and you want some data from fragment B to Fragment A.
class B :BottomSheetDialogFragment() {
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val root = inflater.inflate(R.layout.your_layout, container, false)
root.sampleButton.setOnClickListener {
val navController = findNavController()
navController.previousBackStackEntry?.savedStateHandle?.set("your_key", "your_value")
dismiss()
}
}
and in your Fragment A:
findNavController().currentBackStackEntry?.savedStateHandle?.getLiveData<String>("your_key")
?.observe(viewLifecycleOwner) {
if (it == "your_value") {
//your code
}
}
you can use do as below:
Select Account Fragment code
class SelectAccountFragment(val clickListener: OnOptionCLickListener) : BottomSheetDialogFragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.bottom_fragment_accounts, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val list = DataProcessorApp(context).allUsers
val rvAccounts = view.findViewById<RecyclerView>(R.id.rvAccounts)
rvAccounts.layoutManager = LinearLayoutManager(context)
rvAccounts.adapter = AccountsAdapter(context, list)
Log.e(tag,"Accounts "+list.size);
tvAccountAdd.setOnClickListener {
val intent = Intent(context,LoginActivity::class.java)
startActivity(intent)
}
tvManageAccounts.setOnClickListener {
Log.e(tag,"Manage Click")
clickListener.onManageClick()
}
}
interface OnOptionCLickListener{
fun onManageClick()
}
}
Now show and get call back into another fragment /activity as below
SelectAccountFragment accountFragment = new SelectAccountFragment(() -> {
//get fragment by tag and dismiss it
BottomSheetDialogFragment fragment = (BottomSheetDialogFragment) getChildFragmentManager().findFragmentByTag(SelectAccountFragment.class.getSimpleName();
if (fragment!=null){
fragment.dismiss();
}
});
accountFragment.show(getChildFragmentManager(),SelectAccountFragment.class.getSimpleName());
If you are using BottomSheetDialogFragment , since it's a fragment, you should create your interface and bind to it at onAttach lifecycle method of the fragment , doing the appropriate cast of activity reference to your listener/callback type.
Implement this interface in your activity and dispatch change when someone click in a item of fragment's inner recyclerview, for instance
It's a well known pattern and are explained better at here
One big advice is rethink your app architecture, since the best approach is to always pass primitive/simple/tiny data between Android components through Bundle, and your components are able to retrieve the required state with their dependencies later on.
For example, you should never pass along large Objects like Bitmaps, Data Classes , DTO's or View References.
first there is some serialization process going on regarding Parcel which impacts in app responsiveness
second it can lead you to TransactionTooLarge type of error.
Hope that helps!
You can also use LocalBroadcastManager. And as hglf said, it is better to keep the empty constructor for your fragment and use newInstance(Type value) instead to instantiate your fragment if you still want to use the interface callBack way.
You can use the benefit of Navigation library:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val navController = findNavController();
// After a configuration change or process death, the currentBackStackEntry
// points to the dialog destination, so you must use getBackStackEntry()
// with the specific ID of your destination to ensure we always
// get the right NavBackStackEntry
val navBackStackEntry = navController.getBackStackEntry(R.id.your_fragment)
// Create our observer and add it to the NavBackStackEntry's lifecycle
val observer = LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_RESUME
&& navBackStackEntry.savedStateHandle.contains("key")) {
val result = navBackStackEntry.savedStateHandle.get<String>("key");
// Do something with the result
}
}
navBackStackEntry.lifecycle.addObserver(observer)
// As addObserver() does not automatically remove the observer, we
// call removeObserver() manually when the view lifecycle is destroyed
viewLifecycleOwner.lifecycle.addObserver(LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_DESTROY) {
navBackStackEntry.lifecycle.removeObserver(observer)
}
})
}
For more info, read the document.
The accepted answer is wrong.
What you can do is just user Fragment A's childFragmentManager when calling show().
like this:
val childFragmentManager = fragmentA.childFragmentManager
bottomSheetDialogFragment.show(childFragmentManager, "dialog")