Skip to content

Instantly share code, notes, and snippets.

@thegarlynch
Last active June 22, 2020 15:56
Show Gist options
  • Save thegarlynch/5821b1fc46b2bf87054d6fc885e5d5d8 to your computer and use it in GitHub Desktop.
Save thegarlynch/5821b1fc46b2bf87054d6fc885e5d5d8 to your computer and use it in GitHub Desktop.
Navigation Gateway in navigation component
import android.os.Bundle
import androidx.annotation.IdRes
import androidx.navigation.*
/**
*
* Act as gateway between current fragment to it's destination
*
* Each [NavigationGateway] may have only one gatewayId (either a NavGraph or Destination)
*
* @sample
*
* class AuthenticatorNavigationGateway @Inject constructor(
* private val fragment : Fragment,
* private val sessionStorage : SessionStorage
* ) : NavigationGateway(fragment.findNavController()){
*
* override fun getGatewayId() = R.id.auth_nav_graph
* override fun canProceed() = sessionStorage.currentSession != null
*
* }
*
* class LoginFragment {
*
* val gateway by gateway()
*
* override fun onViewCreated(view : View?){
* if(LoginSuccess){
* gateway.proceed()
* }
* }
*
* }
*
*/
abstract class NavigationGateway constructor(
private val navController: NavController
) {
@IdRes
abstract fun getGatewayId() : Int
abstract fun canProceed() : Boolean
private fun requireGateway(){
navController.graph.findNode(getGatewayId())
?: throw IllegalArgumentException("Destination with id : ${getGatewayId()} cannot be found in this graph")
}
private fun requireAllowedOptions(options: NavOptions){
if(options.popUpTo == -1) return
val gatewayDestination = navController.graph.findNode(getGatewayId())!!
if(gatewayDestination is NavGraph){
gatewayDestination.forEach {
if(it.id == options.popUpTo){
throw IllegalArgumentException("You cannot popUpTo any destination in this navGraph : ${options.popUpTo}")
}
}
}else{
if(options.popUpTo == getGatewayId()){
throw IllegalArgumentException("You cannot popUpTo Gateway : ${options.popUpTo}")
}
}
}
private fun initializeSavedState(destinationId: Int, args: Bundle?, options: NavOptions){
navController.currentBackStackEntry!!.savedStateHandle.apply {
set(DESTINATION_ARGUMENTS, args)
set(DESTINATION_ID, destinationId)
set(POP_UP_INCLUSIVE, options.isPopUpToInclusive)
set(POP_UP_TO, options.popUpTo)
set(POPUP_ENTER_ANIM, options.popEnterAnim)
set(POPUP_EXIT_ANIM, options.popExitAnim)
set(ENTER_ANIM, options.enterAnim)
set(EXIT_ANIM, options.exitAnim)
set(LAUNCH_SINGLE_TOP, options.shouldLaunchSingleTop())
set(GATEWAY_ID, getGatewayId())
}
}
private fun requireAction(direction: NavDirections) : NavAction{
return navController.currentDestination!!.getAction(direction.actionId)
?: navController.graph.getAction(direction.actionId)
?: throw IllegalArgumentException("direction must be in graph $direction")
}
fun navigate(
direction : NavDirections,
gatewayArgs: Bundle? = null
){
requireGateway()
val action = requireAction(direction)
if(action.navOptions != null){
requireAllowedOptions(action.navOptions!!)
}
if(canProceed()){
navController.navigate(direction)
return
}
val newArgs = (gatewayArgs ?: Bundle()).apply { putInt(SAVED_STATE_HOLDER_ID, navController.currentDestination!!.id) }
val navOptions = action.navOptions ?: navOptions { }
initializeSavedState(action.destinationId, direction.arguments ,navOptions)
navController.navigate(getGatewayId(), newArgs, defaultGatewayOptions(inherit = navOptions))
}
fun navigate(
@IdRes destinationId : Int,
args : Bundle?,
navOptions : NavOptions = navOptions { },
gatewayArgs : Bundle? = null
){
requireGateway()
requireAllowedOptions(navOptions)
if(canProceed()){
navController.navigate(destinationId, args, navOptions)
return
}
val newArgs = (gatewayArgs ?: Bundle()).apply { putInt(SAVED_STATE_HOLDER_ID, navController.currentDestination!!.id) }
initializeSavedState(destinationId, args, navOptions)
navController.navigate(getGatewayId(), newArgs, defaultGatewayOptions(inherit = navOptions))
}
companion object {
private const val PREFIX = "com.ibima.elearning.helper.navigation.gateway.NavigationGateway"
internal const val DESTINATION_ARGUMENTS = "${PREFIX}.destination_arguments"
internal const val DESTINATION_ID = "${PREFIX}.destinationId"
internal const val POP_UP_INCLUSIVE = "${PREFIX}.popUpInclusive"
internal const val POP_UP_TO = "${PREFIX}.popUpTo"
internal const val LAUNCH_SINGLE_TOP = "${PREFIX}.launchSingleTop"
internal const val ENTER_ANIM = "${PREFIX}.enterAnim"
internal const val EXIT_ANIM = "${PREFIX}.exitAnim"
internal const val POPUP_ENTER_ANIM = "${PREFIX}.popUpEnterAnim"
internal const val POPUP_EXIT_ANIM = "${PREFIX}.popUpExitAnim"
internal const val SAVED_STATE_HOLDER_ID = "${PREFIX}.savedStateHolderId"
internal const val GATEWAY_ID = "${PREFIX}.gatewayId"
}
}
private fun defaultGatewayOptions(inherit : NavOptions) = navOptions {
this.launchSingleTop = true
this.anim {
enter = inherit.enterAnim
popEnter = inherit.popEnterAnim
exit = inherit.exitAnim
popExit = inherit.popExitAnim
}
}
import android.os.Bundle
import androidx.fragment.app.Fragment
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleObserver
import androidx.lifecycle.OnLifecycleEvent
import androidx.lifecycle.SavedStateHandle
import androidx.navigation.NavController
import androidx.navigation.fragment.findNavController
import androidx.navigation.navOptions
class GatewayAction internal constructor(
private val navController: NavController,
private val handle : SavedStateHandle
) : LifecycleObserver {
private val destinationId : Int = handle.get<Int>(NavigationGateway.DESTINATION_ID)
?: throw IllegalStateException("You can only proceed if gateway is initialized")
private val destinationArgs = handle.get<Bundle?>(NavigationGateway.DESTINATION_ARGUMENTS)
private val navOptions = navOptions {
launchSingleTop = handle.get<Boolean>(NavigationGateway.LAUNCH_SINGLE_TOP)!!
val whatToPopUpTo = handle.get<Int>(NavigationGateway.POP_UP_TO)!!
if(whatToPopUpTo == -1){
// if there is no destination to popUpTo -> remove Gateway
popUpTo(handle.get<Int>(NavigationGateway.GATEWAY_ID)!!){ inclusive = true }
}else {
popUpTo(handle.get<Int>(NavigationGateway.POP_UP_TO)!!) {
inclusive = handle.get<Boolean>(NavigationGateway.POP_UP_INCLUSIVE)!!
}
}
anim {
enter = handle.get<Int>(NavigationGateway.ENTER_ANIM)!!
exit = handle.get<Int>(NavigationGateway.EXIT_ANIM)!!
popEnter = handle.get<Int>(NavigationGateway.POPUP_ENTER_ANIM)!!
popExit = handle.get<Int>(NavigationGateway.POPUP_EXIT_ANIM)!!
}
}
@OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
internal fun invalidateSavedState(){
handle.remove<Int>(NavigationGateway.DESTINATION_ID)
handle.remove<Bundle?>(NavigationGateway.DESTINATION_ARGUMENTS)
handle.remove<Boolean>(NavigationGateway.LAUNCH_SINGLE_TOP)
handle.remove<Int>(NavigationGateway.POP_UP_TO)
handle.remove<Boolean>(NavigationGateway.POP_UP_INCLUSIVE)
handle.remove<Int>(NavigationGateway.ENTER_ANIM)
handle.remove<Int>(NavigationGateway.EXIT_ANIM)
handle.remove<Int>(NavigationGateway.POPUP_ENTER_ANIM)
handle.remove<Int>(NavigationGateway.POPUP_EXIT_ANIM)
handle.remove<Int>(NavigationGateway.GATEWAY_ID)
}
fun proceed(){
navController.navigate(destinationId, destinationArgs, navOptions)
invalidateSavedState()
}
}
fun Fragment.gateway() : Lazy<GatewayAction>{
return lazy {
val navController = findNavController()
val savedStateHolderId = navController.currentBackStackEntry!!.arguments!!.getInt(NavigationGateway.SAVED_STATE_HOLDER_ID, -1)
if(savedStateHolderId == -1){
throw IllegalStateException("Argument ${NavigationGateway.SAVED_STATE_HOLDER_ID} does not exist")
}
val savedStateHolderEntry = navController.getBackStackEntry(savedStateHolderId)
val action = GatewayAction(navController, savedStateHolderEntry.savedStateHandle)
lifecycle.addObserver(action)
action
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment