|
1 |
| -A declarative router for [ReKotlin](https://github.com/GeoThings/ReKotlin). Allows developers to declare routes in a similar manner as URLs are used on the web. |
| 1 | +A declarative router for [ReKotlin](https://github.com/GeoThings/ReKotlin). Allows developers to declare routes in a similar manner as URLs are used on the web. |
| 2 | + |
| 3 | +Using ReKotlinRouter you can navigate your app by defining the target location in the form of a URL-like sequence of identifiers: |
| 4 | + |
| 5 | +```Kotlin |
| 6 | +val routes = arrayListOf(loginRoute, repoListRoute, repoDetailRoute) |
| 7 | +val actionData = SetRouteSpecificData(route = routes, data = "somedata") |
| 8 | +val action = SetRouteAction(route = routes) |
| 9 | +mainStore.dispatch(actionData) |
| 10 | +mainStore.dispatch(action) |
| 11 | +``` |
| 12 | + |
| 13 | +# About ReKotlinRouter |
| 14 | + |
| 15 | + |
| 16 | +When building apps with ReKotlin you should aim to cause **all** state changes through actions - this includes changes to the navigation state. |
| 17 | + |
| 18 | +This requires to store the current navigation state within the app state and to use actions to trigger changes to that state - both is provided ReKotlinRouter. |
| 19 | + |
| 20 | +# Installation |
| 21 | +Add the following line along with ReKotlin dependencies in gradle file |
| 22 | + |
| 23 | +```Groovy |
| 24 | +implementation 'org.rekotlinrouter:rekotlin-router:0.1.0' |
| 25 | +``` |
| 26 | + |
| 27 | +# Configuration |
| 28 | + |
| 29 | +Extend your app state to include the navigation state: |
| 30 | + |
| 31 | +```Kotlin |
| 32 | +import org.rekotlinrouter.HasNavigationState |
| 33 | +import org.rekotlinrouter.NavigationState |
| 34 | +import tw.geothings.rekotlin.StateType |
| 35 | + |
| 36 | +data class AppState(override var navigationState: NavigationState, |
| 37 | + // other application states such as.... |
| 38 | + var authenticationState: AuthenticationState, |
| 39 | + var repoListState: RepoListState): StateType, HasNavigationState |
| 40 | + |
| 41 | +``` |
| 42 | + |
| 43 | +After you've initialized your store, create an instance of `Router`, passing in a reference to the store and to the root `Routable`. Additionally you will need to provide a closure that describes how to access the `navigationState` of your application state: |
| 44 | + |
| 45 | +```Kotlin |
| 46 | + router = Router(store = mainStore, |
| 47 | + rootRoutable = RootRoutable(context = applicationContext), |
| 48 | + stateTransform = { subscription -> |
| 49 | + subscription.select { stateType -> |
| 50 | + stateType.navigationState |
| 51 | + } |
| 52 | + }) |
| 53 | +``` |
| 54 | + |
| 55 | +We'll discuss `Routable` in the next main section. |
| 56 | + |
| 57 | +## Calling the Navigation Reducer |
| 58 | + |
| 59 | +The `NavigationReducer` is provided as part of `ReKotlinRouter`. You need to call it from within your top-level reducer. Here's a simple example from the specs: |
| 60 | + |
| 61 | +```Kotlin |
| 62 | + |
| 63 | +fun appReducer(action: Action, oldState: GitHubAppState?) : GitHubAppState { |
| 64 | + |
| 65 | + // if no state has been provided, create the default state |
| 66 | + val state = oldState ?: GitHubAppState( |
| 67 | + navigationState = NavigationReducer.handleAction(action = action, state = oldState?.navigationState), |
| 68 | + // other application state reducers such as.... |
| 69 | + authenticationState = AuthenticationState(loggedInState = LoggedInState.loggedIn, |
| 70 | + userName = ""), |
| 71 | + repoListState = RepoListState()) |
| 72 | + |
| 73 | + return state.copy( |
| 74 | + navigationState = (::navigationReducer)(action, state.navigationState), |
| 75 | + // other application state reducers such as.... |
| 76 | + authenticationState = (::authenticationReducer)(action, state.authenticationState), |
| 77 | + repoListState = (::repoListReducer)(action, state.repoListState)) |
| 78 | +} |
| 79 | + |
| 80 | +fun navigationReducer(action: Action, oldState: NavigationState?): NavigationState { |
| 81 | + val state = oldState ?: NavigationReducer.handleAction(action = action, state = oldState) |
| 82 | + when (action) { |
| 83 | + is SetRouteAction -> { |
| 84 | + return NavigationReducer.handleAction(action = action, state = state) |
| 85 | + } |
| 86 | + |
| 87 | + is SetRouteSpecificData -> { |
| 88 | + return NavigationReducer.handleAction(action = action, state = state) |
| 89 | + } |
| 90 | + } |
| 91 | + return state |
| 92 | +} |
| 93 | + |
| 94 | +``` |
| 95 | +This will make reducer handle all routing relevant actions. |
| 96 | + |
| 97 | +# Implementing `Routable` |
| 98 | + |
| 99 | +ReKotlinRouter works with routes that are defined, similar to URLs, as a sequence of identifiers e.g. `["Home", "User", "UserDetail"]`. It uses `Routable`s to implement that interaction. |
| 100 | + |
| 101 | +Each route segment is mapped to one responsible `Routable`. The `Routable` needs to be able to present a child, hide a child or replace a child with another child. |
| 102 | + |
| 103 | +Here is the `Routable` protocol with the methods you should implement: |
| 104 | + |
| 105 | +```Kotlin |
| 106 | +interface Routable { |
| 107 | + |
| 108 | + fun pushRouteSegment(routeElementIdentifier: RouteElementIdentifier, |
| 109 | + animated: Boolean, |
| 110 | + completionHandler: RoutingCompletionHandler): Routable |
| 111 | + |
| 112 | + fun popRouteSegment(routeElementIdentifier: RouteElementIdentifier, |
| 113 | + animated: Boolean, |
| 114 | + completionHandler: RoutingCompletionHandler) |
| 115 | + |
| 116 | + fun changeRouteSegment(from: RouteElementIdentifier, |
| 117 | + to: RouteElementIdentifier, |
| 118 | + animated: Boolean, |
| 119 | + completionHandler: RoutingCompletionHandler): Routable |
| 120 | +} |
| 121 | +``` |
| 122 | + |
| 123 | +As part of initializing `Router` you need to pass the first `Routable` as an argument. That root `Routable` will be responsible for the first route segment. |
| 124 | + |
| 125 | +If e.g. you set the route of your application to `["Home"]`, your root `Routable` will be asked to present the view that corresponds to the identifier `"Home"`. |
| 126 | + |
| 127 | + |
| 128 | +Whenever a `Routable` presents a new route segment, it needs to return a new `Routable` that will be responsible for managing the presented segment. If you want to navigate from `["Home"]` to `["Home", "Users"]` the `Routable` responsible for the `"Home"` segment will be asked to present the `"User"` segment. |
| 129 | + |
| 130 | +If your navigation stack uses a modal presentation for this transition, the implementation of `Routable` for the `"Root"` segment might look like this: |
| 131 | + |
| 132 | +```Kotlin |
| 133 | + |
| 134 | +class RootRoutable(val context: Context): Routable { |
| 135 | + override fun popRouteSegment(routeElementIdentifier: RouteElementIdentifier, |
| 136 | + animated: Boolean, |
| 137 | + completionHandler: RoutingCompletionHandler) { |
| 138 | + } |
| 139 | + |
| 140 | + override fun pushRouteSegment(routeElementIdentifier: RouteElementIdentifier, |
| 141 | + animated: Boolean, |
| 142 | + completionHandler: RoutingCompletionHandler): Routable { |
| 143 | + if(routeElementIdentifier == loginRoute) { |
| 144 | + return LoginRoutable(context) |
| 145 | + } else if (routeElementIdentifier == welcomeRoute) { |
| 146 | + return RoutableHelper.createWelcomeRoutable(context) |
| 147 | + } |
| 148 | + |
| 149 | + return LoginRoutable(context) |
| 150 | + } |
| 151 | + |
| 152 | + override fun changeRouteSegment(from: RouteElementIdentifier, |
| 153 | + to: RouteElementIdentifier, |
| 154 | + animated: Boolean, |
| 155 | + completionHandler: RoutingCompletionHandler): Routable { |
| 156 | + TODO("not implemented") |
| 157 | + } |
| 158 | + |
| 159 | +} |
| 160 | + |
| 161 | +object RoutableHelper { |
| 162 | + |
| 163 | + fun createWelcomeRoutable(context: Context): WelcomeRoutable { |
| 164 | + val welcomeIntent = Intent(context, WelcomeActivity::class.java) |
| 165 | + welcomeIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK) |
| 166 | + context.startActivity(welcomeIntent) |
| 167 | + return WelcomeRoutable(context) |
| 168 | + } |
| 169 | +} |
| 170 | + |
| 171 | +``` |
| 172 | + |
| 173 | +## Calling the Completion Handler within Routables |
| 174 | + |
| 175 | +//TODO: |
| 176 | +ReKotlinRouter needs to throttle the navigation actions, since many UI frameworks don't allow to perform multiple navigation steps in parallel. Therefor every method of `Routable` receives a `completionHandler`. The router will not perform any further navigation actions until the completion handler is called. |
| 177 | + |
| 178 | +# Changing the Current Route |
| 179 | + |
| 180 | +Currently the only way to change the current application route is by using the `SetRouteAction` and providing an absolute route. Here's a brief example: |
| 181 | + |
| 182 | +```Kotlin |
| 183 | + mEmailSignInButton.setOnClickListener { |
| 184 | + mainStore.dispatch(LoginAction(userName = mETEmail.text.toString(), |
| 185 | + password = mETPassword.text.toString())) |
| 186 | + } |
| 187 | +``` |
| 188 | +As development continues, support for changing individual route segments will be added. |
| 189 | + |
| 190 | +## Bugs and Feedback |
| 191 | + |
| 192 | +For bugs, feature requests, and discussion please use [GitHub Issues][issues]. |
| 193 | +For general usage questions please use the [mailing list][list] or [StackOverflow][so]. |
| 194 | + |
| 195 | +# Contributing |
| 196 | + |
| 197 | +## Compiling & Running tests |
| 198 | + |
| 199 | + To build or test any of the targets, run `gradle assemble`. |
| 200 | + |
| 201 | +## Credits |
| 202 | + |
| 203 | +- Many thanks to [Benjamin Encz](https://github.com/Ben-G) and other ReSwift contributors for buidling original [ReSwift](https://github.com/ReSwift/ReSwift) that we really enjoyed working with. |
| 204 | +- Also huge thanks to [Dan Abramov](https://github.com/gaearon) for building [Redux](https://github.com/reactjs/redux) - all ideas in here and many implementation details were provided by his library. |
0 commit comments