Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Redux + (RxKotlin | RxSwift) = Awesome Native Mobile Apps — Router — Part 6

DZone's Guide to

Redux + (RxKotlin | RxSwift) = Awesome Native Mobile Apps — Router — Part 6

Learn how to use Redux and Redux-Router in iOS and Android apps to achieve separation of concerns by externalizing the routing logic.

· Mobile Zone ·
Free Resource

These libraries can be a boon for native app developers. See how they change the developer experience in part six of this series.

Image title

We have learned about the unit testing in Part 5, now let’s learn about the router.

Why We Need the Router

When there is a need to change the flow or introduce a new screen in the existing flow in an app, the changes are cumbersome. To understand that, let’s work an example — say we have a simple app that has 4 screens.

Image title

Now we have a need to introduce a new screen in the existing flow, such as

Image title

Think about the changes we need to make. 

Both in iOS and Android, we need to make the changes in the classes that trigger the new screens; typically those classes are Activity or Fragment in Android and ViewController in iOS. Also, we need to carry and forward the data screen B is sending to screen C when we introduce a new screen, B1.

This needs a lot of code changes. Painstaking work.

Using Redux

If the app is built using Redux, you may not need to pass the data between the screens, as the data can be stored as the state(s).

Image titleTo learn more about Redux in native apps, go here.

Now that we have achieved a greater level of separation of concerns (SOC) with the help of Redux, let’s move the routing logic out of Activities/Fragments or ViewControllers, respecting the principle of SOC.

What are the libs we have to achieve this?

We will use rekotlin-router or ReSwift-Router, depending upon the platform.

Redux Helps the Routing or Navigation

Redux Router takes advantage of the redux state to define the navigation. It uses a state called NavigationState. Using the NavigationState, the router externalizes the logic of navigation and moves the code out of Activities/Fragment or ViewControllers.

Android developers, the examples used in this post use Activity for the sake of simplicity, but you can achieve the same routing when the app uses Fragments as well. This will be explained later, so hang on.

How Does Router Work?


When the RoutingAction is dispatched by Activity/Fragment or ViewController, it is intercepted by the navigationReducer provided by the Router.

The Router uses the Routableof the dispatching Activity or ViewController to derive the next possible Activity or ViewController. Apart from creating the Activity or ViewController, the Routable creates the Routable for the next Activity or ViewController.

For example, when an ActivityA dispatches a RoutingAction that has two routes — ActivityA and ActivityB — the Router creates an instance of ActivityB and the routable for ActivityB, namely RoutableB. Now ActivityB is the active Activity in your app.

Routable creating another Activity and Routable

What happens when ActivityB decides to change the screen?

When ActivityB dispatches a routing change, it will be the responsibility of RoutableB to decide the next possible Activity.

Yes, each Activity or ViewController must have a Routable Class defined. The Routable class separates the navigation logic from the Activity or ViewController class.

Show Me the Code 

Let’s start with adding the dependencies, as shown below:

implementation 'org.rekotlinrouter:rekotlin-router:0.1.9'

(iOS developers, refer to this link)

Let’s update the state to implement the interfaceNavigationState 

data class AppState(override var navigationState: NavigationState,
                         // other application states such as....
                          var authenticationState: AuthenticationState,
                          var repoListState: RepoListState): 
                                     StateType, HasNavigationState

Once the navigation state is defined in the AppState, we need to create an object of Router. TheRouter takes care of the navigation, as long as the Routable is defined for each Activity or ViewController.

In order for Routable to work, we need to create an instance of Router and initialize it. 

Where should we do that?

Remember where we created the Store as a global variable in part 2 of this series?

Yes, we created them in AppController. Let’s update the class to accommodate the Router

var mainStore = Store(state = null,
        reducer = ::appReducer,
        middleware = arrayListOf(gitHubMiddleware))

var router: Router<GitHubAppState>? = null

class AppController : Application() { 
override fun onCreate() {
        super.onCreate()
       // create the App state
        val state = GitHubAppState(navigationState = NavigationState(),
                authenticationState = authenticationState,
                repoListState = RepoListState())
         // create the store
        mainStore = Store(state = state,
                reducer = ::appReducer,
                middleware = arrayListOf(gitHubMiddleware),
                automaticallySkipRepeats = true)
          // create the router
        router = Router(store = mainStore,
                rootRoutable = RootRoutable(context = applicationContext),
                stateTransform = { subscription -> 
                                  subscription.select { stateType ->
                                                        stateType.navigationState
                                                         }
                })

    }
}

Router initialization takes three inputs:

  • Store
  • Root routable
  • A lambda expression that describes how to access the navigationState of the application state

What Is Root Routable?

It is the first routable in the series of Routable's that takes care of the navigation. It is routable for the first Activity or the ViewController that is presented by the app.

Now let’s look at the Routable interface

class RootRoutable(val context: Context): Routable {
    override fun popRouteSegment(routeElementIdentifier: RouteElementIdentifier,
                                 animated: Boolean,
                                 completionHandler: RoutingCompletionHandler) {
    }

    override fun pushRouteSegment(routeElementIdentifier: RouteElementIdentifier,
                                  animated: Boolean,
                                  completionHandler: RoutingCompletionHandler): Routable {
        if(routeElementIdentifier == loginRoute) {
            return LoginRoutable(context)
        } else if (routeElementIdentifier == welcomeRoute) {
            return RoutableHelper.createWelcomeRoutable(context)
        }

        return LoginRoutable(context)
    }

    override fun changeRouteSegment(from: RouteElementIdentifier,
                                    to: RouteElementIdentifier,
                                    animated: Boolean,
                                    completionHandler: RoutingCompletionHandler): Routable {
       TODO("not implemented")
    }

When the opposite happens, for example when the route changes from ["Home", "User"] to ["Home"], Router will execute the function popRouteSegment of Routable User.

Image title

The code pretty much does nothing, as Android takes care of dismissing the current Activity. You may have some post-dismissing work in completionHandler

override fun popRouteSegment(routeElementIdentifier: RouteElementIdentifier,
                                 animated: Boolean,
                                 completionHandler: RoutingCompletionHandler) {
        completionHandler()
  }

In the case of iOS, you dismiss the current ViewController.

func popRouteSegment(
        _ routeElementIdentifier: RouteElementIdentifier,
        animated: Bool,
        completionHandler: @escaping RoutingCompletionHandler) {
        if routeElementIdentifier == oAuthRoute {
            self.viewController.dismiss(animated: true, completion: completionHandler)
        }
    }

What happens when we want to change the routes for example from ["Home", "User"] to ["Home", "Detail"]?

The Router will execute the function changeRouteSegment of Routable User.

class UserRoutable(val context: Context) : Routable {

    override fun changeRouteSegment(from: RouteElementIdentifier, 
                                    to: RouteElementIdentifier, 
                                    animated: Boolean, 
                                    completionHandler: RoutingCompletionHandler): Routable {
        if (from == userRoute && to == detailRoute) {
            val detailIntent = Intent(context, DetailActivity::class.java)
            context.startActivity(detailIntent)
             return DetailRoutable(context) 
        } else{
            return this
        }

     }
    }

To see the complete example using Router, refer to the Android example using Activity here and the iOS example here. You may like to clone and run the projects.

Router Using Fragments

You can dispatch the actions that invoke the Fragments similar to Activity.

val routes = arrayListOf(mainActivityRoute, backStackActivityRoute, oneFragmentRoute)
val action = SetRouteAction(route = routes)
val actionData = SetRouteSpecificData(route= routes,
                                      data = FragmentDataValue(activity, true))
mainStore.dispatch(actionData)
mainStore.dispatch(action)

The function SetRouteSpecificData passes the data to Router to create the fragment.

We need the reference of Activity to create the Fragments. We will pass the Activity as WeakReference

We will use a class FragmentDataValue to pass such data

class FragmentDataValue(val activity: WeakReference<AppCompatActivity>, 
                        val addToBackStack: Boolean)

Routable That Creates Fragments

Let’s override the functions to create the Fragments, similar to how we do it when using Router with Activity:

override fun pushRouteSegment(routeElementIdentifier: RouteElementIdentifier,
                                  animated: Boolean,
                                  completionHandler: RoutingCompletionHandler): Routable {
        when (routeElementIdentifier) {
            oneFragmentRoute -> {
                return RoutableHelper.backStackFragmentRoutable(fragment = OneFragment(),
                        tag = "OneFragment")
            }
            twoFragmentRoute -> {
                return RoutableHelper.backStackFragmentRoutable(fragment = TwoFragment(),
                        tag = "TwoFragment")
            }
        }
    }

Let’s use the WeakReference of Activity to create the Fragment:

fun backStackFragmentRoutable(fragment: Fragment,tag: String): FragmentRoutable {
        val currentRoute = mainStore.state.navigationState.route
        val intentData: FragmentDataValue =
                mainStore.state.navigationState
                        .getRouteSpecificState<FragmentDataValue>(currentRoute)!!
        val activity = intentData.activity.get()!!
        val addToBackStack = intentData.addToBackStack
        addFragment(fragment, 
                    activity, 
                    addToBackStack, 
                    tag,
                    R.id.container_frame_back_stack)
        //changeFragment(fragment,activity,addToBackStack,tag,R.id.container_frame_back)
        return FragmentRoutable(activity.applicationContext)
}

It does the following:

  • Gets the current route from navigationState
  • From the current route, gets the fragment’s intentData
  • With the intentData, gets the activity from its WeakReference and Boolean value whether to add to the stack
  • Calls the function addFragment, a helper method to create the fragment.

To see the example of Android using Fragments, go here.

Conclusion

I hope you are able to appreciate the fact that the routing logic is externalized completely using Redux and Redux-Router, achieving SOC.

Topics:
redux ,routing ,android ,mobile ,mobile app development ,tutorial ,swift ,ios

Published at DZone with permission of

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}