Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Scaling The Navigation System in tiket

Scaling The Navigation System in tiket

Navigation is a core part of a mobile application. There are screen-to-screen navigation, deep-link, handling in-app browser links, and handling redirection after log-in. Things get more complicated if modularization is introduced. So how we can keep the navigation not just under our control but also easier?

Esa Firman

June 07, 2022
Tweet

More Decks by Esa Firman

Other Decks in Programming

Transcript

  1. Esa Firman
    Mobile Apps Developer
    Scaling The Navigation
    System in tiket

    View full-size slide

  2. Esa Firman
    Sr Software Engineer @ tiket.com
    Esa Firman esafirm

    View full-size slide

  3. Agenda
    How it started
    Background, problems to solve
    1
    Implementation
    Design & problems implementing
    the system
    2
    Going Forward
    More problems, what can be
    improved
    3

    View full-size slide

  4. How it started

    View full-size slide

  5. ● We have 20 Android engineers separated into 6 teams
    ● There are ~45 PRs every week
    ○ We use squash merge...
    ● We have 200+ Activity
    ● And all of that Activity is in one module
    ○ We have multi module setup…
    ● Deeplink handling logic is centralized
    ○ Don’t get us wrong, we use deeplink dispatch library
    ● Navigation logic is scattered and doesn’t have standard
    ● All of this is leads to many merge conflict, unclear code ownership, and bad build speed
    Problems

    View full-size slide

  6. ● Enable multi-module navigation
    ● Make deep link handling more easier and more modular
    ● Standardize the navigation system
    Action Items

    View full-size slide

  7. Implementation

    View full-size slide

  8. :app
    :feature_a
    :feature_b

    View full-size slide

  9. // Feature A - AlphaActivity
    companion object {
    private const val ARG_ID = "ID"
    fun start(activity: Activity, id: String) {
    activity.startActivity(Intent(activity, AlphaActivity::class).apply {
    putExtra(ARG_ID, id)
    })
    }
    }
    Activity Navigation

    View full-size slide

  10. // Feature B - BetaActivity
    class BetaActivity : AppCompatActivity {
    fun navigateToAlpha(id: String) {
    AlphaActivity.start(this, id)
    }
    }
    Activity Navigation

    View full-size slide

  11. :app
    :feature_a :feature_b

    View full-size slide

  12. // Feature B - BetaActivity
    class BetaActivity : AppCompatActivity {
    fun navigateToAlpha(id: String) {
    AlphaActivity.start(this, id)
    }
    }
    Activity Navigation

    View full-size slide

  13. // Feature B - BetaActivity
    class BetaActivity : AppCompatActivity {
    fun navigateToAlpha(id: String) {
    AlphaActivity.start(this, id)
    }
    }
    Activity Navigation
    Unresolved
    reference

    View full-size slide

  14. :app
    :feature_a :feature_b
    :lib_router

    View full-size slide

  15. :app
    :feature_a :feature_b
    :lib_router
    class TiketRouter : Router {
    fun navigateToAlpha(...) {
    AlphaActivity.start(...)
    }
    fun navigateToBeta(...) {
    BetaActivity.start(...)
    }
    }
    interface Router {
    fun navigateToAlpha(...)
    fun navigateToBeta(...)
    }

    View full-size slide

  16. ● Enable multi-module navigation
    ● Make deep link handling more easier and more modular
    ● Standardize the navigation system
    Action Items
    🤔

    View full-size slide

  17. interface Router {
    fun navigateToAlpha(...)
    fun navigateToBeta(...)
    fun navigateToCharlie(...)
    fun navigateToDelta(...)
    fun navigateToEcho(...)
    ...
    }
    Router Interface
    Different caller?
    Tracking?
    Redirection?

    View full-size slide

  18. @DeepLink({"https://example.com/a/{id}", "app:beta"})
    class DeepLinkHandler : Activity {
    fun onCreate() {
    val uri = intent?.extras?.getString(DeepLink.URI)
    when {
    DeepLinkUtils.isAlpha(uri) -> {
    val id = DeepLinkUtils.getId(stringUri)
    AlphaActivity.start(id)
    }
    DeepLinkUtils.isBeta(uri) -> BetaActivity.start()
    else -> router.navigateToHome()
    }
    ...
    }
    }
    Deep link handling

    View full-size slide

  19. @DeepLink({"https://example.com/a/{id}", "app:beta"})
    class DeepLinkHandler : Activity {
    fun onCreate() {
    val uri = intent?.extras?.getString(DeepLink.URI)
    when {
    DeepLinkUtils.isAlpha(uri) -> {
    val id = DeepLinkUtils.getId(stringUri)
    AlphaActivity.start(id)
    }
    DeepLinkUtils.isBeta(uri) -> BetaActivity.start()
    else -> router.navigateToHome()
    }
    ...
    }
    }
    Deep link handling

    View full-size slide

  20. // Identifier for destination
    object AlphaRoute : Route("https://example.com/{id}") {
    data class Param(val id: String)
    fun mapUri(uri: Uri): Param = Param.from(uri)
    }
    Router API Design...

    View full-size slide

  21. // Call without URI
    router.go(AlphaRoute, AlphaRoute.Param("1"))
    // Call with URI
    router.go("https://example.com/1")
    // Register
    router.register(AlphaRoute) { payload ->
    val (caller, param) = payload
    AlphaActivity.start(caller, param.id)
    }
    Router API Design

    View full-size slide

  22. // Facade for navigation system
    interface Router {
    fun go(route: Route, param: P) : Boolean
    fun go(uri: String) : Boolean
    fun register(router: Route, onNavigate: (Payload) => Unit)
    }
    Router API Design

    View full-size slide

  23. class TiketRouter : Router {
    private val map = mutableMapOf Unit>()
    private val uriMap = mutableMapOf()
    fun go(route: Route, param: P): Boolean {
    ...
    }
    fun go(uri: String): Boolean {
    ...
    }
    fun register(router: Route, onNavigate: (Payload) => Unit) {
    map[route] = onNavigate
    uriMap[Uri.from(route.uri)] = route
    }
    }

    View full-size slide

  24. class TiketRouter : Router {
    private val map = mutableMapOf Unit>()
    private val uriMap = mutableMapOf()
    fun go(route: Route, param: P): Boolean {
    map[route]?.invoke(Payload(param)) ?: return false
    return true
    }
    fun go(uri: String): Boolean {
    ...
    }
    fun register(router: Route, onNavigate: (Payload) => Unit) {
    ...
    }
    }

    View full-size slide

  25. class TiketRouter : Router {
    private val map = mutableMapOf Unit>()
    private val uriMap = mutableMapOf()
    fun go(route: Route, param: P): Boolean {
    ...
    }
    fun go(uri: String): Boolean {
    val route = uriMap.keys.find { it.match(uri) }
    val param = route.mapUri(uri)
    return go(route, param)
    }
    fun register(router: Route, onNavigate: (Payload) => Unit) {
    ...
    }
    }

    View full-size slide

  26. class TiketRouter : Router {
    private val map = mutableMapOf Unit>()
    private val uriMap = mutableMapOf()
    fun go(route: Route, param: P) {
    map[route]?.invoke(Payload(param)) ?: return false
    return true
    }
    fun go(uri: String) {
    val route = uriMap.keys.find { it.match(uri) }
    val param = route.mapUri(uri)
    return go(route, param)
    }
    fun register(router: Route, onNavigate: (Payload) => Unit) {
    map[route] = onNavigate
    uriMap[Uri.from(route.uri)] = route
    }
    }

    View full-size slide

  27. go("https://example.com")
    go(AlphaRoute, AlphaRoute.Param("1")) Resolve lambda Invoke Navigation
    Resolve
    Identifier
    Apply Middlewares
    Context provider
    Tracker
    Redirection
    Create Payload
    Caller info

    View full-size slide

  28. app
    feature_hotel
    feature_train
    lib_router
    object HotelDetail : Route()
    object TrainSearch : Route("https://tiket.com/train")
    // Register
    TrainSearch.Register {
    startActivity(this, TrainActivity::class.java)
    }
    // Navigate to other screen
    router.go(TrainSearch)
    // Handle DeepLink / URI
    router.goTo("https://tiket.com/train")
    // Instance creation
    @Provides fun provideRouter(middleWares: Set) =
    TiketAppRouter(middleWares)

    View full-size slide

  29. ● Enable multi-module navigation
    ● Make deep link handling more easier and more modular
    ● Standardize the navigation system
    Action Items

    View full-size slide

  30. class DeepLinkHandler : Activity {
    fun onCreate() {
    val uri = intent?.data?.toString()
    val isHandled = router.go(uri)
    if (!isHandled) {
    router.go(HomeRoute)
    }
    }
    }
    Deep link handling

    View full-size slide

  31. ● Enable multi-module navigation
    ● Make deep link handling more easier and more modular
    ● Standardize the navigation system
    Action Items

    View full-size slide

  32. ● Enable multi-module navigation
    ● Make deep link handling more easier and more modular
    ● Standardize the navigation system
    Action Items

    View full-size slide

  33. Going Forward

    View full-size slide

  34. ● It still doesn’t support single activity navigation
    ● register() speed does not scale well
    ○ It currently took 200-300 ms for ~4000 URI
    ● Centralized route module could be a problem
    Problems

    View full-size slide

  35. ● Modular project structure is necessary to create better and faster iteration environment
    ● We need to adjust our navigation/routing system to support our modular application
    ● Sometime we need to zoom out to see the whole picture and create a better solution
    ● Standardization can remove confusion, overthinking, over-engineering and other stressful stuff
    ● Scalability is an ongoing process and we need to adjust accordingly
    Key Takeaways

    View full-size slide

  36. 1. Universal Router - https://github.com/esafirm/universal-router
    2. Scaling Android App Developer @ tiket.com -
    https://www.youtube.com/watch?v=qhkNL0o7x3o
    References

    View full-size slide