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. 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
  2. • 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
  3. • Enable multi-module navigation • Make deep link handling more

    easier and more modular • Standardize the navigation system Action Items
  4. // 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
  5. // Feature B - BetaActivity class BetaActivity : AppCompatActivity {

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

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

    fun navigateToAlpha(id: String) { AlphaActivity.start(this, id) } } Activity Navigation Unresolved reference
  8. :app :feature_a :feature_b :lib_router class TiketRouter : Router { fun

    navigateToAlpha(...) { AlphaActivity.start(...) } fun navigateToBeta(...) { BetaActivity.start(...) } } interface Router { fun navigateToAlpha(...) fun navigateToBeta(...) }
  9. • Enable multi-module navigation • Make deep link handling more

    easier and more modular • Standardize the navigation system Action Items 🤔
  10. interface Router { fun navigateToAlpha(...) fun navigateToBeta(...) fun navigateToCharlie(...) fun

    navigateToDelta(...) fun navigateToEcho(...) ... } Router Interface Different caller? Tracking? Redirection?
  11. @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
  12. @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
  13. // Identifier for destination object AlphaRoute : Route<Param>("https://example.com/{id}") { data

    class Param(val id: String) fun mapUri(uri: Uri): Param = Param.from(uri) } Router API Design...
  14. // 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
  15. // Facade for navigation system interface Router { fun <P>

    go(route: Route<P>, param: P) : Boolean fun go(uri: String) : Boolean fun <P> register(router: Route<P>, onNavigate: (Payload) => Unit) } Router API Design
  16. class TiketRouter : Router { private val map = mutableMapOf<Route,

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

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

    (Payload) => Unit>() private val uriMap = mutableMapOf<Uri, Route>() fun <P> go(route: Route<P>, 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 <P> register(router: Route<P>, onNavigate: (Payload) => Unit) { ... } }
  19. class TiketRouter : Router { private val map = mutableMapOf<Route,

    (Payload) => Unit>() private val uriMap = mutableMapOf<Uri, Route>() fun <P> go(route: Route<P>, 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 <P> register(router: Route<P>, onNavigate: (Payload) => Unit) { map[route] = onNavigate uriMap[Uri.from(route.uri)] = route } }
  20. 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<MiddleWare>) = TiketAppRouter(middleWares)
  21. • Enable multi-module navigation • Make deep link handling more

    easier and more modular • Standardize the navigation system Action Items
  22. class DeepLinkHandler : Activity { fun onCreate() { val uri

    = intent?.data?.toString() val isHandled = router.go(uri) if (!isHandled) { router.go(HomeRoute) } } } Deep link handling
  23. • Enable multi-module navigation • Make deep link handling more

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

    easier and more modular • Standardize the navigation system Action Items
  25. • 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
  26. • 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
  27. 1. Universal Router - https://github.com/esafirm/universal-router 2. Scaling Android App Developer

    @ tiket.com - https://www.youtube.com/watch?v=qhkNL0o7x3o References