Navigation Architecture Component - Dialog Fragments - android

Is it possible to use the new Navigation Architecture Component with DialogFragment? Do I have to create a custom Navigator?
I would love to use them with the new features in my navigation graph.

May 2019 Update:
DialogFragment are now fully supported starting from Navigation 2.1.0, you can read more here and here
Old Answer for Navigation <= 2.1.0-alpha02:
I proceeded in this way:
1) Update Navigation library at least to version 2.1.0-alpha01 and copy both files of this modified gist in your project.
2) Then in your navigation host fragment, change the name parameter to your custom NavHostFragment
<fragment
android:id="#+id/nav_host_fragment"
android:name="com.example.app.navigation.MyNavHostFragment"
app:defaultNavHost="true"
app:navGraph="#navigation/nav_graph"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#id/toolbar" />
3) Create your DialogFragment subclasses and add them to your nav_graph.xml with:
<dialog
android:id="#+id/my_dialog"
android:name="com.example.ui.MyDialogFragment"
tools:layout="#layout/my_dialog" />
4) Now launch them from fragments or activity with
findNavController().navigate(R.id.my_dialog)
or similar methods.

No, as of the 1.0.0-alpha01 build, there is no support for dialogs as part of your Navigation Graph. You should just continue to use show() to show a DialogFragment.

Yes. The framework is made in such a way that you can create a class extending the Navigator abstract class for the views that does not come out-of-the box and add it to your NavController with the method getNavigatorProvider().addNavigator(Navigator navigator)
If you are using the NavHostFragment, you will also need to extend it to add the custom Navigator or just create your own MyFragment implementing NavHost interface. It's so flexible that you can create your own xml parameters with custom attrs defined in values, like you do creating custom views. Something like this (not tested):
#Navigator.Name("dialog-fragment")
class DialogFragmentNavigator(
val context: Context,
private val fragmentManager: FragmentManager
) : Navigator<DialogFragmentNavigator.Destination>() {
override fun navigate(destination: Destination, args: Bundle?,
navOptions: NavOptions?, navigatorExtras: Extras?
): NavDestination {
val fragment = Class.forName(destination.name).newInstance() as DialogFragment
fragment.show(fragmentManager, destination.id.toString())
return destination
}
override fun createDestination(): Destination = Destination(this)
override fun popBackStack() = fragmentManager.popBackStackImmediate()
class Destination(navigator: DialogFragmentNavigator) : NavDestination(navigator) {
// The value of <dialog-fragment app:name="com.example.MyFragmentDialog"/>
lateinit var name: String
override fun onInflate(context: Context, attrs: AttributeSet) {
super.onInflate(context, attrs)
val a = context.resources.obtainAttributes(
attrs, R.styleable.FragmentNavigator
)
name = a.getString(R.styleable.FragmentNavigator_android_name)
?: throw RuntimeException("Error while inflating XML. " +
"`name` attribute is required")
a.recycle()
}
}
}
Usage
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/navigation"
app:startDestination="#id/navigation_home">
<fragment
android:id="#+id/navigation_assistant"
android:name="com.example.ui.HomeFragment"
tools:layout="#layout/home">
<action
android:id="#+id/action_nav_to_dialog"
app:destination="#id/navigation_dialog" />
</fragment>
<dialog-fragment
android:id="#+id/navigation_dialog"
android:name="com.example.ui.MyDialogFragment"
tools:layout="#layout/my_dialog" />
</navigation>
The fragment that will navigate.
class HomeFragment : Fragment(), NavHost {
private val navControllerInternal: NavController by lazy(LazyThreadSafetyMode.NONE){
NavController(context!!)
}
override fun getNavController(): NavController = navControllerInternal
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Built-in navigator for `fragment` XML tag
navControllerInternal.navigatorProvider.addNavigator(
FragmentNavigator(context!!, childFragmentManager, this.id)
)
// Your custom navigator for `dialog-fragment` XML tag
navControllerInternal.navigatorProvider.addNavigator(
DialogFragmentNavigator(context!!, childFragmentManager)
)
navControllerInternal.setGraph(R.navigation.my_navigation)
}
override fun onCreateView(inflater: LayoutInflater,
container: ViewGroup?, savedInstanceState: Bundle?): View? {
super.onCreateView(inflater, container, savedInstanceState)
val view = inflater.inflate(R.layout.home)
view.id = this.id
view.button.setOnClickListener{
getNavController().navigate(R.id.action_nav_to_dialog)
}
return view
}
}

Yes it is possible, You can access view of parent fragment from dialog fragment by calling getParentFragment().getView(). And use the view for navigation.
Here is the example
Navigation.findNavController(getParentFragment().getView()).navigate(R.id.nextfragment);

I created custom navigator for DialogFragment.
Sample is here.
(It's just sample, so it might be any problem.)
#Navigator.Name("dialog_fragment")
class DialogNavigator(
private val fragmentManager: FragmentManager
) : Navigator<DialogNavigator.Destination>() {
companion object {
private const val TAG = "dialog"
}
override fun navigate(destination: Destination, args: Bundle?,
navOptions: NavOptions?, navigatorExtras: Extras?) {
val fragment = destination.createFragment(args)
fragment.setTargetFragment(fragmentManager.primaryNavigationFragment,
SimpleDialogArgs.fromBundle(args).requestCode)
fragment.show(fragmentManager, TAG)
dispatchOnNavigatorNavigated(destination.id, BACK_STACK_UNCHANGED)
}
override fun createDestination(): Destination {
return Destination(this)
}
override fun popBackStack(): Boolean {
return true
}
class Destination(
navigator: Navigator<out NavDestination>
) : NavDestination(navigator) {
private var fragmentClass: Class<out DialogFragment>? = null
override fun onInflate(context: Context, attrs: AttributeSet) {
super.onInflate(context, attrs)
val a = context.resources.obtainAttributes(attrs,
R.styleable.FragmentNavigator)
a.getString(R.styleable.FragmentNavigator_android_name)
?.let { className ->
fragmentClass = parseClassFromName(context, className,
DialogFragment::class.java)
}
a.recycle()
}
fun createFragment(args: Bundle?): DialogFragment {
val fragment = fragmentClass?.newInstance()
?: throw IllegalStateException("fragment class not set")
args?.let {
fragment.arguments = it
}
return fragment
}
}
}

Version 2.1.0-alpha03 was Released so we can finally use DialogFragments. Unfortunately for me, I have some issues with the backstack when using cancelable dialogs. Probably I have a faulty implementation of my dialogs..
[LATER-EDIT] My implementation was good, the problem is related to Wrong dialog counting for DialogFragmentNavigator as is described in the issue tracker
As a workaround you can have a look on my recommendation

Updated for:
implementation "androidx.navigation:navigation-ui-ktx:2.2.0-rc04"
And use in my_nav_graph.xml
<dialog
android:id="#+id/my_dialog"
android:name="com.example.ui.MyDialogFragment"
tools:layout="#layout/my_dialog" />

Yes. It's possible in the latest update of Navigation Component. You can check this link to have a clear concept. raywenderlich.com

One option would be to just use a regular fragment and make it look similar to a dialog. I found it was not worth the hassle so I used the standard way using show(). If you insist See here for a way of doing it.

Yes, It is possible now. In it's initial release it wasn't possible but now
from "androidx.navigation:navigation-fragment:2.1.0-alpha03" this navigation version you can use dialog fragment in navigation component.
Check this out:- Naviagtion dialog fragment support

Related

How to add existing fragment from inside Compose

I want to open an existing fragment from compose or if i can add inside the same compose in full screen, that will also work. Also how to navigate from one existing fragment to another existing fragment from compose.
Our recommendation for using Fragments in Compose is documented here.
Specifically, you should use the AndroidViewBinding composable to inflate an XML containing a FragmentContainerView hosting the fragment you want to use in Compose. AndroidViewBinding has fragment-specific handling which is why you want to use this over AndroidView. Note that you need to have view binding enabled for this.
Example XML file:
<androidx.fragment.app.FragmentContainerView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/fragment_container_view"
android:layout_height="match_parent"
android:layout_width="match_parent"
android:name="com.example.MyFragment" />
And then in your Composable function:
#Composable
fun FragmentInComposeExample() {
AndroidViewBinding(MyFragmentLayoutBinding::inflate) {
val myFragment = fragmentContainerView.getFragment<MyFragment>()
// ...
}
}
I solved this as following:
#Composable
fun ReusableFragmentComponent(
someArgumentForFragment: FragmentArgument,
fragmentManager: FragmentManager,
modifier: Modifier = Modifier,
tag: String = "ReusableFragmentTag"
) {
AndroidView(
modifier = modifier,
factory = { context ->
FrameLayout(context).apply {
id = ViewCompat.generateViewId()
}
},
update = {
val fragmentAlreadyAdded = fragmentManager.findFragmentByTag(tag) != null
if (!fragmentAlreadyAdded) {
fragmentManager.commit {
add(it.id, ReusableFragment.newInstance(someArgumentForFragment), tag)
}
}
}
)
}
In my case I called this from a fragment (hosted by navigation component), in an effort to make our reusable fragments compose compatible. I did that like so:
class ReusableFragments : Fragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
return ComposeView(requireContext()).apply {
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setContent {
Column {
Text("ReusableFragment 1")
ReusableFragmentComponent(someArgument1, childFragmentManager, tag = "ReusableFragmentTag1")
Text("ReusableFragment 2")
ReusableFragmentComponent(someArgument2, childFragmentManager, tag = "ReusableFragmentTag2")
}
}
}
}
}
By making the tag customizable it's possible to add multiple different instances of the same fragment to the same fragment manager.
In order to work with your existing xml views and fragments, in a Jetpack Compose app, you can refer to Interoperability APIs.
Specifically, I understand that you want to manage Android Views in Compose.
I want to open an existing fragment from compose
#Composable
fun CustomView() {
AndroidView(
factory = { context ->
MyView(context).apply {
// myView listeners
}
},
update = { view ->
// Recomposition logics
}
)
}
..how to navigate from one existing fragment to another existing fragment from compose.
In the above example you now have a composable function, so just use Jetpack Compose navigation library.
This helped me:
#Composable
fun MyFragmentView(
fragmentManager: FragmentManager,
modifier: Modifier = Modifier
) {
AndroidView(
modifier = modifier.fillMaxSize(),
factory = { context ->
val containerId = R.id.container // some unique id
val fragmentContainerView = FragmentContainerView(context).apply {
id = containerId
}
val fragment = MyFragment()
fragmentManager.beginTransaction()
.replace(containerId, fragment, fragment.javaClass.simpleName)
.commitAllowingStateLoss()
fragmentContainerView
}
)
}
ids.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<item name="container" type="id"/>
</resources>

android jetpack navigation instrumented test fail on back navigation

I've created a simple, two fragment example app using jetpack Navigation component (androidx.navigation). First fragment navigates to second one, which overrides backbutton behavior with OnBackPressedDispatcher.
activity layout
<LinearLayout 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:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="#dimen/box_inset_layout_padding"
tools:context=".navigationcontroller.NavigationControllerActivity">
<fragment
android:name="androidx.navigation.fragment.NavHostFragment"
android:id="#+id/nav_host"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNavHost="true"
app:navGraph="#navigation/nav_graph" />
</LinearLayout>
FragmentA:
class FragmentA : Fragment() {
lateinit var buttonNavigation: Button
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view = inflater.inflate(R.layout.fragment_a, container, false)
buttonNavigation = view.findViewById<Button>(R.id.button_navigation)
buttonNavigation.setOnClickListener { Navigation.findNavController(requireActivity(), R.id.nav_host).navigate(R.id.fragmentB) }
return view
}
}
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".navigationcontroller.FragmentA">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="fragment A" />
<Button
android:id="#+id/button_navigation"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="go to B" />
</LinearLayout>
FragmentB:
class FragmentB : Fragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view = inflater.inflate(R.layout.fragment_b, container, false)
requireActivity().onBackPressedDispatcher.addCallback(object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
val textView = view.findViewById<TextView>(R.id.textView)
textView.setText("backbutton pressed, press again to go back")
this.isEnabled = false
}
})
return view
}
}
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".navigationcontroller.FragmentA">
<TextView
android:id="#+id/textView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:text="fragment B" />
</FrameLayout>
Intended behavior of backbutton in FragmentB (first touch changes text without navigation, second navigates back) works fine when I test the app manually.
I've added instrumented tests to check backbutton behavior in FragmentB and that's where problems started to arise:
class NavigationControllerActivityTest {
lateinit var fragmentScenario: FragmentScenario<FragmentB>
lateinit var navController: TestNavHostController
#Before
fun setUp() {
navController = TestNavHostController(ApplicationProvider.getApplicationContext())
fragmentScenario = FragmentScenario.launchInContainer(FragmentB::class.java)
fragmentScenario.onFragment(object : FragmentScenario.FragmentAction<FragmentB> {
override fun perform(fragment: FragmentB) {
Navigation.setViewNavController(fragment.requireView(), navController)
navController.setLifecycleOwner(fragment.viewLifecycleOwner)
navController.setOnBackPressedDispatcher(fragment.requireActivity().getOnBackPressedDispatcher())
navController.setGraph(R.navigation.nav_graph)
// simulate backstack from previous navigation
navController.navigate(R.id.fragmentA)
navController.navigate(R.id.fragmentB)
}
})
}
#Test
fun whenButtonClickedOnce_TextChangedNoNavigation() {
Espresso.pressBack()
onView(withId(R.id.textView)).check(matches(withText("backbutton pressed, press again to go back")))
assertEquals(R.id.fragmentB, navController.currentDestination?.id)
}
#Test
fun whenButtonClickedTwice_NavigationHappens() {
Espresso.pressBack()
Espresso.pressBack()
assertEquals(R.id.fragmentA, navController.currentDestination?.id)
}
}
Unfortunately, while whenButtonClickedTwice_NavigationHappens passes, whenButtonClickedOnce_TextChangedNoNavigation fails due to text not being changed, just like OnBackPressedCallback was never called. Since app works fine during manual tests, there must be something wrong with test code. Can anyone help me ?
If you're trying to test your OnBackPressedCallback logic, it is better to do that directly, rather than try to test the interaction between Navigation and the default activity's OnBackPressedDispatcher.
That would mean that you'd want to break the hard dependency between the activity's OnBackPressedDispatcher (requireActivity().onBackPressedDispatcher) and your Fragment by instead injecting in the OnBackPressedDispatcher, thus allowing you to provide a test specific instance:
class FragmentB(val onBackPressedDispatcher: OnBackPressedDispatcher) : Fragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view = inflater.inflate(R.layout.fragment_b, container, false)
onBackPressedDispatcher.addCallback(object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
val textView = view.findViewById<TextView>(R.id.textView)
textView.setText("backbutton pressed, press again to go back")
this.isEnabled = false
}
})
return view
}
}
This allows you to have your production code provide a FragmentFactory:
class MyFragmentFactory(val activity: FragmentActivity) : FragmentFactory() {
override fun instantiate(classLoader: ClassLoader, className: String): Fragment =
when (loadFragmentClass(classLoader, className)) {
FragmentB::class.java -> FragmentB(activity.onBackPressedDispatcher)
else -> super.instantiate(classLoader, className)
}
}
// Your activity would use this via:
override fun onCreate(savedInstanceState: Bundle?) {
supportFragmentManager.fragmentFactory = MyFragmentFactory(this)
super.onCreate(savedInstanceState)
// ...
}
This would mean you could write your tests such as:
class NavigationControllerActivityTest {
lateinit var fragmentScenario: FragmentScenario<FragmentB>
lateinit var onBackPressedDispatcher: OnBackPressedDispatcher
lateinit var navController: TestNavHostController
#Before
fun setUp() {
navController = TestNavHostController(ApplicationProvider.getApplicationContext())
// Create a test specific OnBackPressedDispatcher,
// giving you complete control over its behavior
onBackPressedDispatcher = OnBackPressedDispatcher()
// Here we use the launchInContainer method that
// generates a FragmentFactory from a constructor,
// automatically figuring out what class you want
fragmentScenario = launchFragmentInContainer {
FragmentB(onBackPressedDispatcher)
}
fragmentScenario.onFragment(object : FragmentScenario.FragmentAction<FragmentB> {
override fun perform(fragment: FragmentB) {
Navigation.setViewNavController(fragment.requireView(), navController)
navController.setGraph(R.navigation.nav_graph)
// Set the current destination to fragmentB
navController.setCurrentDestination(R.id.fragmentB)
}
})
}
#Test
fun whenButtonClickedOnce_FragmentInterceptsBack() {
// Assert that your FragmentB has already an enabled OnBackPressedCallback
assertTrue(onBackPressedDispatcher.hasEnabledCallbacks())
// Now trigger the OnBackPressedDispatcher
onBackPressedDispatcher.onBackPressed()
onView(withId(R.id.textView)).check(matches(withText("backbutton pressed, press again to go back")))
// Check that FragmentB has disabled its Callback
// ensuring that the onBackPressed() will do the default behavior
assertFalse(onBackPressedDispatcher.hasEnabledCallbacks())
}
}
This avoids testing Navigation's code and focuses on testing your code and specifically your interaction with OnBackPressedDispatcher.
The reason for FragmentB's OnBackPressedCallback to be ignored is the way how OnBackPressedDispatcher treats its OnBackPressedCallbacks. They are run as chain-of-command, meaning that most recently registered one that is enabled will 'eat' the event so others will not receive it. Therefore, most recently registered callback inside FragmentScenario.onFragment() (which is enabled by lifecycleOwner, so whenever Fragment is at least in lifecycle STARTED state. Since fragment is visible during the test when backbutton is pressed, callback is always enabled at the time), will have priority over previously registered one in FragmentB.onCreateView().
Therefore, TestNavHostController's callback must be added before FragmentB.onCreateView() is executed.
This leads to changes in test code #Before method:
#Before
fun setUp() {
navController = TestNavHostController(ApplicationProvider.getApplicationContext())
fragmentScenario = FragmentScenario.launchInContainer(FragmentB::class.java, initialState = Lifecycle.State.CREATED)
fragmentScenario.onFragment(object : FragmentScenario.FragmentAction<FragmentB> {
override fun perform(fragment: FragmentB) {
navController.setLifecycleOwner(fragment.requireActivity())
navController.setOnBackPressedDispatcher(fragment.requireActivity().getOnBackPressedDispatcher())
navController.setGraph(R.navigation.nav_graph)
// simulate backstack from previous navigation
navController.navigate(R.id.fragmentA)
navController.navigate(R.id.fragmentB)
}
})
fragmentScenario.moveToState(Lifecycle.State.RESUMED)
fragmentScenario.onFragment(object : FragmentScenario.FragmentAction<FragmentB> {
override fun perform(fragment: FragmentB) {
Navigation.setViewNavController(fragment.requireView(), navController)
}
})
}
Most important change is to launch Fragment in CREATED state (instead of default RESUMED) to be able to tinker with it before onCreateView().
Also, notice that Navigation.setViewNavController() is run in separate onFragment() after moving fragment to RESUMED state - it accepts View parameter, so it cannot be used before onCreateView()

How do I programmatically add a fragment to a fragment in kotlin

I have a fragment which is really a screen in my app... on this screen I want to load one of a number of fragments depending on the properties of a model which gets passed to the screen fragment. I'll do this using a conditional when... but first:
I can't actually even load a basic fragment. Here is the code for the screen fragment:
class EditCommandFragment : Fragment() {
private val args by navArgs<EditCommandFragmentArgs>()
private lateinit var fragContainer: ConstraintLayout
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
setHasOptionsMenu(true)
// Inflate the layout for this fragment
val view = inflater.inflate(R.layout.fragment_edit_command, container, false)
//get a reference to the container.
fragContainer = view.findViewById<ConstraintLayout>(R.id.command_edit_container)
// add one of the edit fragments //this doesn't work
childFragmentManager.beginTransaction().add(fragContainer, EditCommandUIFragment.newInstance()).commit()
return view
}
This error thrown is:
None of the following functions can be called with the arguments supplied:
public open fun add(p0: Fragment, p1: String?): FragmentTransaction defined in androidx.fragment.app.FragmentTransaction
public open fun add(p0: Int, p1: Fragment): FragmentTransaction defined in androidx.fragment.app.FragmentTransaction
but this makes no sense. In all the tutorials I can find you pass the container as p0 and then the class of the fragment you want to add as p1... but this is asking for a string or an int, what??
How do I correctly add my EditCommandUIFragment to the container?
To add a Fragment you need an instance of the Fragment, not its "view" (that's the Fragment's responsibility to manage).
Construct an instance of your "child" Fragment by using the "recommended methods":
val yourNewFragment = YourNewFragment.newInstance()
Obtain the container where you will place this...
val container = R.id.place_where_you_will_put_it
Perform the transaction:
childFragmentManager.beginTransaction().add(container, yourNewFragment, "A TAG or NULL")
Now, keep in mind, if you do use a Tag, it's sometimes good practice (this really depends on your app/lifecycle/etc.) to check if the fragment is not there already...
val frag = childFragmentManager.findFragmentByTag("The Tag You Used Above")
if (frag == null) {
// add it
}
You get the idea.
If you do not use a TAG" then you can use the other alternative...
val frag = childFragmentManager.findFragmentById(id of the container where the fragment is supposed to be, aka: R.id.place_where_you_will_put_it)
This can be used to, for example, replace(...) a fragment, instead of add.
I, in my project, to do something similar to yours I used viewPager2
In practice, in the parent fragment, you position it inside the xml file that acts as a view
<androidx.viewpager2.widget.ViewPager2 android:id="#+id/viewpager"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginTop="15dp"
app:layout_constraintTop_toBottomOf="#+id/tabs"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
/>
then create a class that extends from FragmentStateAdapter
class ViewPagerAdapter(fragment: Fragment) : FragmentStateAdapter(fragment) {
private val mFragmentList = ArrayList<Fragment>()
private val mFragmentTitleList = ArrayList<String>()
fun addFrag(fragment: Fragment, title: String) {
mFragmentList.add(fragment)
mFragmentTitleList.add(title)
}
override fun getItemCount(): Int {
return mFragmentList.size
}
override fun createFragment(position: Int): Fragment {
return mFragmentList[position]
}
}
after inside the on-create of the fragment parent do the setup (see the documentation)
for example in my example to add the children Fragments just call
viewPagerAdapter.addFrag(MyFragment(), "Title")
or to say which fragment to show
viewpager.currentItem = 5

Bottom Navigation View Null

I am trying to set a badge to a BottomNavigationView by following this straightforward approach.
However, when I initialize the BottomNavigationView I get:
java.lang.IllegalStateException: view.findViewById(R.id.bottom_navigation_view) must not be null
I am initializing the BottomNativigationView from a fragment. I am guessing that is the issue, but I cannot figure out the solution.
private lateinit var bottomNavigation: BottomNavigationView
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view = inflater.inflate(R.layout.fragment_home, container, false)
bottomNavigation = view.findViewById(R.id.bottom_navigation_view)
}
Here is the BottomNavigationView xml for the Activity that sets up navigation for the fragments.
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="#+id/bottom_navigation_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#color/colorWhite"
app:itemIconTint="#color/navigation_tint"
app:itemTextColor="#color/navigation_tint"
app:labelVisibilityMode="labeled"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:menu="#menu/bottom_navigation" />
It feels like I am missing something simple, but I cannot figure out what. Thanks!
You have many options to communicate betwean fragments - activity and between fragment's itself..
You should not try access activity views from fragment.
Solution 1: Share data with the host activity
class ItemViewModel : ViewModel() {
private val mutableSelectedItem = MutableLiveData<Item>()
val selectedItem: LiveData<Item> get() = mutableSelectedItem
fun selectItem(item: Item) {
mutableSelectedItem.value = item
}
}
class MainActivity : AppCompatActivity() {
// Using the viewModels() Kotlin property delegate from the activity-ktx
// artifact to retrieve the ViewModel in the activity scope
private val viewModel: ItemViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel.selectedItem.observe(this, Observer { item ->
// Perform an action with the latest item data
})
}
}
class ListFragment : Fragment() {
// Using the activityViewModels() Kotlin property delegate from the
// fragment-ktx artifact to retrieve the ViewModel in the activity scope
private val viewModel: ItemViewModel by activityViewModels()
// Called when the item is clicked
fun onItemClicked(item: Item) {
// Set a new item
viewModel.selectItem(item)
}
}
Solution 2: Receive results in the host activity
button.setOnClickListener {
val result = "result"
// Use the Kotlin extension in the fragment-ktx artifact
setFragmentResult("requestKey", bundleOf("bundleKey" to result))
}
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
supportFragmentManager
.setFragmentResultListener("requestKey", this) { requestKey, bundle ->
// We use a String here, but any type that can be put in a Bundle is supported
val result = bundle.getString("bundleKey")
// Do something with the result
}
}
}
There is many more ways but these are latest approaches from Google.
Check this reference: https://developer.android.com/guide/fragments/communicate
You can access the activity from its fragment by casting activity to your activity class, and inflate the views then.
bottomNavigation = (activity as MyActivityName).findViewById(R.id.bottom_navigation_view)

How to call navigation action after click on item with RecyclerView and DataBinding?

I have problem with call navigation action defined in nav_graph in my adpter.
I tried to call Navigation.findNavController(v).navigate(id_myaction) in the bind function of the ViewHolder but it doesnt work and I got
error : " View
androidx.constraintlayout.widget.ConstraintLayout{96d8d23 V.E...C..
...P.... 0,471-1080,628} does not have a NavController set"
It's my action:
<fragment
android:id="#+id/eventListFragment"
android:name="EventListFragment"
android:label="fragment_event_list"
tools:layout="#layout/fragment_event_list" >
<action
android:id="#+id/action_eventListFragment_to_startEventFragment"
app:destination="#id/startEventFragment" />
</fragment>
It's my adapter:
class EventListAdapter(val findNavController: NavController) : RecyclerView.Adapter<EventListAdapter.ViewHolder>() {
private lateinit var eventList:List<Event>
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): EventListAdapter.ViewHolder {
val binding: EventListItemBinding = DataBindingUtil.inflate(LayoutInflater.from(parent.context), R.layout.event_list_item, parent, false)
return ViewHolder(binding,findNavController)
}
override fun onBindViewHolder(holder: EventListAdapter.ViewHolder, position: Int) {
holder.bind(eventList[position])
}
override fun getItemCount(): Int {
return if(::eventList.isInitialized) eventList.size else 0
}
fun updateEventList(eventList:List<Event>){
this.eventList = eventList
notifyDataSetChanged()
}
class ViewHolder(private val binding: EventListItemBinding, val findNavController: NavController):RecyclerView.ViewHolder(binding.root){
private val viewModel = EventListItemViewModel()
fun bind(event:Event){
viewModel.bind(event)
binding.root.setOnClickListener {v -> Navigation.findNavController(v).navigate(R.id.action_eventListFragment_to_startEventFragment)}
binding.viewModel = viewModel
}
}
}
I would to start startEventFragment definied in my action after click on item.
Based on the official tutorial, you may require to change your code as follows,
binding.root.setOnClickListener {v ->
v.findNavController().navigate(R.id.action_eventListFragment_to_startEventFragment)
}
Update
If you read the documentation, you will find the following note,
Note: The Navigation component is designed for apps that have one main
activity with multiple fragment destinations. The main activity is
associated with a navigation graph and contains a NavHostFragment that
is responsible for swapping destinations as needed. In an app with
multiple activity destinations, each activity has its own navigation
graph.
So, you need to make sure that the both fragments belong to a same activity and the activity is associated with navigation graph and contains a NavHostFragment. Please check if your activity's layout contains the following fragment tag,
<fragment
android:id="#+id/nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:defaultNavHost="true"
app:navGraph="#navigation/nav_graph_id" />
The same problem, nothing helps me ...
java.lang.IllegalStateException: View com.google.android.material.card.MaterialCardView{a1d8ff1 VFE...C.. ...P.... 16,492-708,738 #7f0801ff app:id/tripLayout} does not have a NavController set
Solved
It was a problem with supportFragmentManager.beginTransaction.apply{}... in my MainActivity. Be careful. NavGaraph doesn't work if you have conflict between 2 types of fragments ...

Categories

Resources