|
| 1 | +package com.kryptkode.navigation |
| 2 | + |
| 3 | +import android.app.Activity |
| 4 | +import android.content.Intent |
| 5 | +import android.os.Parcelable |
| 6 | +import androidx.core.app.NavUtils |
| 7 | +import androidx.fragment.app.DialogFragment |
| 8 | +import androidx.fragment.app.Fragment |
| 9 | +import androidx.fragment.app.FragmentActivity |
| 10 | +import com.kryptkode.commonandroid.extension.illegal |
| 11 | +import com.kryptkode.commonandroid.extension.unsupported |
| 12 | +import com.kryptkode.commonandroid.livedata.extension.observeEvent |
| 13 | +import com.kryptkode.navigation.direction.NavDirection |
| 14 | +import com.kryptkode.navigation.dsl.doWhen |
| 15 | +import com.kryptkode.navigation.ktx.getFragmentTag |
| 16 | +import com.kryptkode.navigation.ktx.requireDialogFragment |
| 17 | +import com.kryptkode.navigation.ktx.requireIntent |
| 18 | +import com.kryptkode.navigation.ktx.startForResultCodeWithData |
| 19 | +import com.kryptkode.navigation.model.NavigationCommand |
| 20 | +import com.kryptkode.navigation.model.NavigationResult |
| 21 | +import com.kryptkode.navigation.model.NavigationResult.Companion.RESULT_PARAM |
| 22 | +import kotlin.coroutines.Continuation |
| 23 | +import kotlin.coroutines.resume |
| 24 | +import kotlinx.coroutines.CoroutineScope |
| 25 | +import kotlinx.coroutines.coroutineScope |
| 26 | +import kotlinx.coroutines.launch |
| 27 | + |
| 28 | +/** |
| 29 | + * This interface is used to centralize & encapsulate the navigation logic. |
| 30 | + * Mainly implemented by [Fragment] or [FragmentActivity] |
| 31 | + */ |
| 32 | +interface Navigable { |
| 33 | + |
| 34 | + /** |
| 35 | + * Convenient method that will be used by any [Fragment] or [FragmentActivity] |
| 36 | + * to properly observe the [NavigableViewModel.router]. |
| 37 | + * |
| 38 | + * Set [delegate] if you want to delegate the navigation actions to another [Navigable]. |
| 39 | + */ |
| 40 | + fun <T : NavDirection> NavigableViewModel<T>.observeNavigation(delegate: Navigable? = null) { |
| 41 | + router.observeEvent(getLifecycleOwner()) { |
| 42 | + val navigable = delegate ?: this@Navigable |
| 43 | + it.handleNavigation(navigableScope, navigable) |
| 44 | + } |
| 45 | + } |
| 46 | + |
| 47 | + /** |
| 48 | + * Convenient method which help the [NavigableViewModel.router] to navigate to a screen. |
| 49 | + */ |
| 50 | + fun navigateTo(direction: NavDirection) { |
| 51 | + direction.doWhen { |
| 52 | + isFragmentDialog { |
| 53 | + navigateToDialogFragment(requireDialogFragment()) |
| 54 | + } |
| 55 | + isFragment { |
| 56 | + illegal( |
| 57 | + "You should manually implement " + |
| 58 | + "'navigateTo()'" + |
| 59 | + " if you want to navigate to a Fragment." |
| 60 | + ) |
| 61 | + } |
| 62 | + isIntent { |
| 63 | + navigateToActivity(requireIntent()) |
| 64 | + } |
| 65 | + otherDirection { unsupported() } |
| 66 | + } |
| 67 | + } |
| 68 | + |
| 69 | + /** |
| 70 | + * Convenient method which help the [NavigableViewModel.router] |
| 71 | + * to navigate to a screen |
| 72 | + * and expecting a [NavigationResult] returned by it. |
| 73 | + */ |
| 74 | + suspend fun navigateToForResult( |
| 75 | + direction: NavDirection, |
| 76 | + continuation: Continuation<NavigationResult> |
| 77 | + ) { |
| 78 | + coroutineScope { |
| 79 | + direction.doWhen { |
| 80 | + isFragment { |
| 81 | + illegal( |
| 82 | + "navigateToForResult()" + |
| 83 | + " is only available from [Fragment]" + |
| 84 | + " & [FragmentActivity]" |
| 85 | + ) |
| 86 | + } |
| 87 | + isIntent { |
| 88 | + launch { |
| 89 | + val result = navigateToActivityForResult(requireIntent()) |
| 90 | + continuation.resume( |
| 91 | + NavigationResult(code = result.first, data = result.second) |
| 92 | + ) |
| 93 | + } |
| 94 | + } |
| 95 | + otherDirection { unsupported() } |
| 96 | + } |
| 97 | + } |
| 98 | + } |
| 99 | + |
| 100 | + /** |
| 101 | + * Convenient method which help the [NavigableViewModel.router] to navigate |
| 102 | + * to a previous screen. |
| 103 | + * This method will be used mainly through a funnel. |
| 104 | + * |
| 105 | + * Note that this method will not working with any shared transition |
| 106 | + * (expect if you override it). |
| 107 | + */ |
| 108 | + fun navigateToPrevious(direction: NavDirection) { |
| 109 | + direction.doWhen { |
| 110 | + isFragment { |
| 111 | + illegal( |
| 112 | + "You should manually implement 'navigateToPrevious()'" + |
| 113 | + " to navigate to a previous Fragment." |
| 114 | + ) |
| 115 | + } |
| 116 | + isIntent { |
| 117 | + NavUtils.navigateUpTo(requireActivity(), requireIntent()) |
| 118 | + } |
| 119 | + otherDirection { unsupported() } |
| 120 | + } |
| 121 | + } |
| 122 | + |
| 123 | + /** |
| 124 | + * Convenient method which help the [NavigableViewModel.router] to navigate back |
| 125 | + * (for example, when user clicks on the back button). |
| 126 | + */ |
| 127 | + fun navigateBack() { |
| 128 | + requireActivity().onBackPressed() |
| 129 | + } |
| 130 | + |
| 131 | + /** |
| 132 | + * Convenient method which help the [NavigableViewModel.router] to finish the current screen. |
| 133 | + * (for example, when user clicks on any close button) |
| 134 | + */ |
| 135 | + fun navigateFinish(results: Any?) { |
| 136 | + requireActivity().run { |
| 137 | + results?.let { |
| 138 | + val parcelables = when (it) { |
| 139 | + is Parcelable -> listOf(it) |
| 140 | + is List<*> -> it.filterIsInstance<Parcelable>() |
| 141 | + else -> illegal( |
| 142 | + errorMessage = "You should pass to the finish() method " + |
| 143 | + "only 'Parcelable' or 'List<Parcelable>' variable types." |
| 144 | + ) |
| 145 | + } |
| 146 | + val intent = |
| 147 | + Intent().putParcelableArrayListExtra(RESULT_PARAM, ArrayList(parcelables)) |
| 148 | + setResult(Activity.RESULT_OK, intent) |
| 149 | + } |
| 150 | + finishAfterTransition() |
| 151 | + } |
| 152 | + } |
| 153 | + |
| 154 | + // region Utils |
| 155 | + |
| 156 | + private fun NavigationCommand.handleNavigation( |
| 157 | + scope: CoroutineScope, |
| 158 | + navigable: Navigable |
| 159 | + ) { |
| 160 | + when (this) { |
| 161 | + is NavigationCommand.To -> navigable.navigateTo(direction) |
| 162 | + is NavigationCommand.ToPrevious -> navigable.navigateToPrevious(direction) |
| 163 | + is NavigationCommand.ForResult -> scope.launch { |
| 164 | + navigable.navigateToForResult( |
| 165 | + direction, |
| 166 | + continuation |
| 167 | + ) |
| 168 | + } |
| 169 | + is NavigationCommand.Back -> navigable.navigateBack() |
| 170 | + is NavigationCommand.Finish -> navigable.navigateFinish(results) |
| 171 | + } |
| 172 | + } |
| 173 | + |
| 174 | + private fun getLifecycleOwner() = when (this) { |
| 175 | + is FragmentActivity -> this |
| 176 | + is Fragment -> viewLifecycleOwner |
| 177 | + else -> illegal(CONTEXT_ERROR) |
| 178 | + } |
| 179 | + |
| 180 | + private fun requireActivity(): Activity = when (this) { |
| 181 | + is FragmentActivity -> this |
| 182 | + is Fragment -> this.requireActivity() |
| 183 | + else -> illegal(CONTEXT_ERROR) |
| 184 | + } |
| 185 | + |
| 186 | + private fun navigateToActivity(intent: Intent) = when (this) { |
| 187 | + is FragmentActivity -> startActivity(intent) |
| 188 | + is Fragment -> startActivity(intent) |
| 189 | + else -> illegal(CONTEXT_ERROR) |
| 190 | + } |
| 191 | + |
| 192 | + private suspend fun navigateToActivityForResult(intent: Intent) = when (this) { |
| 193 | + is FragmentActivity -> startForResultCodeWithData(intent) |
| 194 | + is Fragment -> startForResultCodeWithData(intent) |
| 195 | + else -> illegal( |
| 196 | + "navigateToForResult() is only available" + |
| 197 | + " from [Fragment] & [FragmentActivity]" |
| 198 | + ) |
| 199 | + } |
| 200 | + |
| 201 | + private fun navigateToDialogFragment(dialogFragment: DialogFragment) { |
| 202 | + val fragmentManager = when (this) { |
| 203 | + is FragmentActivity -> supportFragmentManager |
| 204 | + is Fragment -> parentFragmentManager |
| 205 | + else -> illegal(CONTEXT_ERROR) |
| 206 | + } |
| 207 | + fragmentManager.beginTransaction().run { |
| 208 | + dialogFragment.show(this, dialogFragment.getFragmentTag()) |
| 209 | + } |
| 210 | + } |
| 211 | + |
| 212 | + companion object { |
| 213 | + private const val CONTEXT_ERROR = |
| 214 | + "This context is not handled... The Navigable interface supports " + |
| 215 | + "only [Fragment] & [FragmentActivity]" + |
| 216 | + " (or [ContextAware] objects if they only want to launch an activity)" |
| 217 | + } |
| 218 | + |
| 219 | + // endregion |
| 220 | +} |
0 commit comments