Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Minor changes
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="DialogFragmentNavigator">
<attr name="android:name"/>
</declare-styleable>
</resources>
package com.geekorum.geekdroid.navigation
import android.content.Context
import android.os.Bundle
import android.util.AttributeSet
import androidx.core.content.res.use
import androidx.core.os.bundleOf
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import androidx.navigation.NavDestination
import androidx.navigation.NavOptions
import androidx.navigation.Navigator
import androidx.navigation.fragment.FragmentNavigator
import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.plusAssign
import com.geekorum.geekdroid.R
import java.util.ArrayDeque
import java.util.Deque
/**
* Allows to navigate to some [DialogFragment].
*
* Usage: add some dialog element in your navigation graph
* ```
* <dialog
* android:id="@+id/my_dialog"
* android:name="com.exemple.MyDialogFragment"
* tools:layout="@layout/fragment_my_dialog" />
*
* ```
* Use [MyNavHostFragment] as your [androidx.navigation.NavHost] in your layout
*/
@Navigator.Name("dialog")
class DialogFragmentNavigator(
private val fragmentManager: FragmentManager
) : Navigator<DialogFragmentNavigator.Destination>() {
private var lastBackStackEntry: FragmentManager.BackStackEntry? = null
private val backstack: Deque<Int> = ArrayDeque()
private var pendingPopBackStack = false
private val onBackstackChangedListener: FragmentManager.OnBackStackChangedListener =
FragmentManager.OnBackStackChangedListener {
if (pendingPopBackStack) {
val entry = fragmentManager.findLastBackStackEntry { it.name == FRAGMENT_BACKSTACK_NAME }
pendingPopBackStack = (entry != null && entry == lastBackStackEntry)
lastBackStackEntry = entry
return@OnBackStackChangedListener
}
if (lastBackStackEntry != null && fragmentManager.noneBackStackEntry { it == lastBackStackEntry }) {
backstack.removeLast()
}
lastBackStackEntry = fragmentManager.findLastBackStackEntry { it.name == FRAGMENT_BACKSTACK_NAME }
}
override fun navigate(
destination: Destination, args: Bundle?, navOptions: NavOptions?, navigatorExtras: Extras?
): NavDestination? {
val lastDialogFragment = instantiateFragment(destination.className!!).apply { arguments = args }
val tr = fragmentManager.beginTransaction().addToBackStack(FRAGMENT_BACKSTACK_NAME)
lastDialogFragment.show(tr, destination.id.toString())
backstack.addLast(destination.id)
// we don't want the destination to be added to the NavController stack,
// because it will update the whole
// navigation chrome (global AppBar, NavigationView, etc)
return null
}
private fun instantiateFragment(className: String): DialogFragment {
val clazz = Class.forName(className)
return fragmentManager.fragmentFactory
.instantiate(clazz.classLoader!!, className) as DialogFragment
}
override fun createDestination(): Destination = Destination(this)
override fun popBackStack(): Boolean {
val lastDialogFragment =
fragmentManager.findFragmentByTag(backstack.lastOrNull()?.toString()) as? DialogFragment ?: return false
lastDialogFragment.dismiss()
backstack.removeLast()
pendingPopBackStack = true
return true
}
/* These 2 lifecycle methods should be handled in the NavHost of this navigator.
* When the NavHost add them it should configure them or call some method so
* that they can configure there listeners. However, this make a strong coupling between a Navigator and
* its host implementation.
*
* The NavigatorProvider of NavController add a Navigator.OnBackStackChangedListener
* who calls these methods */
override fun onBackPressAdded() {
fragmentManager.addOnBackStackChangedListener(onBackstackChangedListener)
}
override fun onBackPressRemoved() {
fragmentManager.removeOnBackStackChangedListener(onBackstackChangedListener)
}
override fun onSaveState(): Bundle? {
return bundleOf(KEY_BACKSTACK_ID to backstack.toIntArray())
}
override fun onRestoreState(savedState: Bundle) {
savedState.getIntArray(KEY_BACKSTACK_ID)?.let {
backstack.clear()
for (id in it) {
backstack.addLast(id)
}
}
lastBackStackEntry = fragmentManager.findLastBackStackEntry { it.name == FRAGMENT_BACKSTACK_NAME }
}
class Destination(navigator: DialogFragmentNavigator) : NavDestination(navigator) {
var className: String? = null
get() = checkNotNull(field) { "Dialog name was not set" }
override fun onInflate(context: Context, attrs: AttributeSet) {
super.onInflate(context, attrs)
context.resources.obtainAttributes(attrs, R.styleable.DialogFragmentNavigator).use {
className = it.getString(R.styleable.DialogFragmentNavigator_android_name)
}
}
}
companion object {
private const val KEY_BACKSTACK_ID = "com.geekorum.geekdroid:navigation:backstack_ids"
private const val FRAGMENT_BACKSTACK_NAME = "com.geekorum.geekdroid:navigation:backstack"
}
private inline fun FragmentManager.findLastBackStackEntry(
predicate: (FragmentManager.BackStackEntry) -> Boolean
): FragmentManager.BackStackEntry? {
for (i in backStackEntryCount - 1 downTo 0) {
val backStackEntry = getBackStackEntryAt(i)
if (predicate(backStackEntry)) {
return backStackEntry
}
}
return null
}
private inline fun FragmentManager.noneBackStackEntry(
predicate: (FragmentManager.BackStackEntry) -> Boolean
): Boolean {
for (i in 0 until backStackEntryCount) {
val backStackEntry = getBackStackEntryAt(i)
if (predicate(backStackEntry)) {
return false
}
}
return true
}
}
/**
* A [NavHostFragment] who supports navigation to [DialogFragment]s.
*/
class MyNavHostFragment : NavHostFragment() {
override fun createFragmentNavigator(): Navigator<out FragmentNavigator.Destination> {
navController.navigatorProvider += DialogFragmentNavigator(childFragmentManager)
return super.createFragmentNavigator()
}
}
Copy link

ghost commented Mar 23, 2019

mine doesn't work. Unresolved reference: plusAssign and Line166: Unresolved reference. None of the following candidates is applicable because of receiver type mismatch

@deckyfx
Copy link

deckyfx commented Mar 25, 2019

example of how to use this gist, please?

@matpag
Copy link
Author

matpag commented Mar 25, 2019

@deckyfx @JackSparling Here the instructions: https://stackoverflow.com/a/55256858/2910520
Guys if you have problems, be sure to copy both the file and the style

Btw i've updated the gist fixing small typos

@nuhkocamobven
Copy link

nuhkocamobven commented Mar 31, 2019

@matpag,

I am getting the error below even though I add DialogNavigator to my provider:

Caused by: java.lang.IllegalStateException: Could not find Navigator with name "dialog". You must call NavController.addNavigator() for each navigation type.

@matpag
Copy link
Author

matpag commented Mar 31, 2019

@nuhkocamobven I think your problem is that in your XML you used the dialog tag instead of dialog-fragment tag.

You should have your dialogs in the navigation graph like this:

<dialog-fragment
        android:id="@+id/test_dialog"
        android:name="com.example.testapp.dev.ui.todoadd.TodoAddFragment"
        android:label="fragment_todolist"
        tools:layout="@layout/fragment_todo_add" />

@kuno
Copy link

kuno commented Apr 7, 2019

@matpag

thanks for the code, it basically works.

But I found a problem, if we set a dialog-fragment as start destination of a nested graph, after jumping from the main nav graph to this nested graph, if I want to nav to other destinations inside the same nested graph as the dialog-fragment, it will crash caused by
navigation xxxxxx is unknown to this NavController

@matpag
Copy link
Author

matpag commented Apr 8, 2019

@kuno
I don't think currently this code supports a dialog-fragment as start-destination.
I can see if I have time to dig into it more and try to fix that but currently is not working
PS: Remember that this is a temporary solution, for advanced cases like the one you mentioned, we probably should wait for the official implementation.

@matpag
Copy link
Author

matpag commented Apr 8, 2019

Updated the GIST to be used with:

  • Navigation 2.1.0-alpha02
  • Fragment 1.1.0-alpha06

@kuno
Copy link

kuno commented Apr 9, 2019

@matpag

Thanks for the reply and I fully agreed with u

@wilburt
Copy link

wilburt commented Apr 12, 2019

Thanks but you can't close the dialog by calling either:
findNavController().popBackStack(R.id.fragmentId, false)
or
findNavController().popBackStack(R.id.fragmentId, false)

Only the back button works for closing.

@matpag
Copy link
Author

matpag commented Apr 12, 2019

@Willburt I will check the behaviour when i'm back to home

@wilburt
Copy link

wilburt commented Apr 15, 2019

Please, any update on this?

@matpag
Copy link
Author

matpag commented Apr 15, 2019

@wilburt Yes,
the trick currently is to replace the last line of navigate method from
return null
to
return createDestination()

Then I used findNavController().popBackStack() and it worked fine.

If you are using NavigationUI with DrawerLayout or AppConfigurationBar probably you need manual handling when navigation is changed. Let me know how it goes :)

@wilburt
Copy link

wilburt commented Apr 15, 2019

Thanks, it worked. Lucky, I am neither using a DrawerLayout or AppConfigurationBar.

Thanks.

@matpag
Copy link
Author

matpag commented May 17, 2019

Navigation 2.1.0-alpha03 has been released with support to DialogFragment navigation.
Starting from now this gist is deprecated, move on to the official solution when you can.
Check here and here for more info

@mohodroid
Copy link

mohodroid commented May 22, 2019

Navigation 2.1.0-alpha03 has been released with support to DialogFragment navigation.
Starting from now this gist is deprecated, move on to the official solution when you can.
Check here and here for more info

Hii, i updated to latest version(2.1.0-alpha04), but i cant see tag, why?

@matpag
Copy link
Author

matpag commented May 22, 2019

What do you mean with tag? If you moved to the latest version of Navigation library, you need to remove all the stuff of this gist from your code and follow the official documentation on how to implement dialog-fragments

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment