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

Scaling Android App Development at tiket.com

Scaling Android App Development at tiket.com

By : Esa Firman, Sr Android Engineer at tiket.com
Live : https://youtu.be/qhkNL0o7x3o?t=360

Tweet

More Decks by Android Enthusiast Jakarta

Other Decks in Technology

Transcript

  1. Agenda Why Bother? Intro to why we need to scale

    our app development 1 Modularization Strategies, conventions, problems and how we solve it. 2 Looking Forward Possible changes, challenges, things to consider 3
  2. Why Bother? • Growth of codes and or features •

    Supporting Multiple App / Configuration • Requirement of new features • Change of organization
  3. Why Bother? • Growth of codes and or features •

    Supporting Multiple App / Configuration • Requirement of new features • Change of organization
  4. Pros and Cons Pros • Code is more portable •

    Code is easier to maintain • Code is easier to test • Clear separation of concern • Clear ownership Cons • More complicated
  5. Goals • We want to be productive • We want

    our architecture to be based on a plugin-like system from code to module declaration • We want strong ownership of the modules • We don’t want a complex setup as our achievement
  6. What goes where? • Monorepo or multi repo? Mono for

    easier readability, simpler collaboration and CI/CD* • We separate our modules to three categories ◦ App - wiring, app configuration ◦ Feature - feature specific component (ex: feature screen, feature repository, feature specific view, feature dagger module) ◦ Library - base classes, common functions
  7. What goes where? • HotelActivity → Feature • FlightSeatMiniMapView →

    Feature • TiketWebView → Library • AppRatingViewModel → Feature • AppStateMiddleware → Library
  8. // app module dependencies { implementation project(':features:feature_inbox') implementation project(':features:feature_login') implementation

    project(':features:feature_hotel') } // feature inbox dependencies { implementation project('libraries:lib_analytic') implementation deps.glide } // library analytic dependencies { api project('libraries:lib_network') implementation deps.googleAnalytic }
  9. Communication between features • Dependency Inversion • The interface moved

    to library module ◦ feature_auth → lib_common_auth • The implementation will be provided by feature module through dagger
  10. interface AccountDataSource { fun getSessionToken(): String } class AccountRepository(store: AccountStore):

    AccountDataSource { fun getSessionToken() = store.getSessionToken() } class AccountPublicModule { @Provides fun provideAccountDataSource(store: AccountStore) { return AccountRepository(store) } }
  11. interface AccountDataSource { fun getSessionToken(): String } class AccountRepository(store: AccountStore):

    AccountDataSource { fun getSessionToken() = store.getSessionToken() } class AccountPublicModule { @Provides fun provideAccountDataSource(store: AccountStore) { return AccountRepository(store) } } lib_common_auth feature_auth
  12. // app module dependencies { implementation project('features:feature_inbox') implementation project('features:feature_auth') }

    // feature inbox dependencies { implementation project('libraries:lib_common_auth') }
  13. 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 appRouter.push(TrainSearch) // Handle DeepLink / URI appRouter.goTo("https://tiket.com/train") // Instance creation @Provides fun provideRouter(middleWares: Set<MiddleWare>) = TiketAppRouter(middleWares) // Optional configuration RouterPlugin.logger = { Log.d("router", it) }
  14. Route Redirection interface LoggedInRoute : Parcelable object OrderRoute : BaseRoute(),

    LoggedInRoute class LoginCheckMiddleWare : MiddleWare { override fun onRouting(route: BaseRoute<*>, routeParam: RouteParam) { // Check if is current not logged in if(route is LoggedInRoute && !isLoggedIn) { return MiddleWareResult( LoginRoute, LoginRouteParam(createRedirectParam() ) } return MiddleWareResult(route, routeParam) } }
  15. Route Redirection interface LoggedInRoute : Parcelable object OrderRoute : BaseRoute(),

    LoggedInRoute class LoginCheckMiddleWare : MiddleWare { override fun onRouting(route: BaseRoute<*>, routeParam: RouteParam) { // Check if is current not logged in if(route is LoggedInRoute && !isLoggedIn) { return MiddleWareResult( LoginRoute, LoginRouteParam(createRedirectParam() ) } return MiddleWareResult(route, routeParam) } }
  16. Route Redirection interface LoggedInRoute : Parcelable object OrderRoute : BaseRoute(),

    LoggedInRoute class LoginCheckMiddleWare : MiddleWare { override fun onRouting(route: BaseRoute<*>, routeParam: RouteParam) { // Check if is current not logged in if(route is LoggedInRoute && !isLoggedIn) { return MiddleWareResult( LoginRoute, LoginRouteParam(createRedirectParam() ) } return MiddleWareResult(route, routeParam) } }
  17. Route Redirection interface LoggedInRoute : Parcelable object OrderRoute : BaseRoute(),

    LoggedInRoute class LoginCheckMiddleWare : MiddleWare { override fun onRouting(route: BaseRoute<*>, routeParam: RouteParam) { // Check if is current not logged in if(route is LoggedInRoute && !isLoggedIn) { return MiddleWareResult( LoginRoute, LoginRouteParam(createRedirectParam() ) } return MiddleWareResult(route, routeParam) } }
  18. Route Redirection interface LoggedInRoute : Parcelable object OrderRoute : BaseRoute(),

    LoggedInRoute class LoginCheckMiddleWare : MiddleWare { override fun onRouting(route: BaseRoute<*>, routeParam: RouteParam) { // Check if is current not logged in if(route is LoggedInRoute && !isLoggedIn) { return MiddleWareResult( LoginRoute, LoginRouteParam(createRedirectParam() ) } return MiddleWareResult(route, routeParam) } }
  19. Route Initialization class TrainSearchRouterInitializer : RouterInitializer() { override fun onInit(appContext:

    Context) { // bind the route to the router TrainSearch.register { // call activity / fragment } } }
  20. Wiring & Configuration • Using Dagger to wire it all

    up • App wide configuration class that contains dagger’s app component for escape hatch
  21. object AppConfig : AppConfigInterface { private lateinit var internalConfig: AppConfigInterface

    val appComponent: AppComponent get() = internalConfig.appComponent } // Usage val feature = LegacyFeature(AppConfig.appComponent.context)
  22. Problem: Hilt in transitive deps If you use @InstallIn in

    transitive down in dependency tree and you use implementation in gradle, it will not gonna included in the graph
  23. Listening to App Event interface AppEventListener { fun onEvent(appEvent: AppEvent)

    } interface AppEvent { class OnAppReadyEvent(app: Application) : AppEvent }
  24. Listening to App Event object AppEventListenerManager { private val appEventListeners

    by lazy { entryPoint.eventListeners() } fun dispatchEvent(appEvent: AppEvent) { appEventListeners.forEach { it.onEvent(appEvent) } } }
  25. Listening to App Event object AppEventListenerManager { private val appEventListeners

    by lazy { entryPoint.eventListeners() } fun dispatchEvent(appEvent: AppEvent) { appEventListeners.forEach { it.onEvent(appEvent) } } }
  26. Listening to App Event class TiketApp: Application @Inject appEventListener :

    AppEventListener fun onCreate() { ... appEventListener.dispatchEvent(OnAppReadyEvent(this)) } }
  27. Iteration Speed • One of the promise of modularization is

    build speed, and in software development build speed is greatly affect iteration speed • Even if the modularization done right, the actual gain is on parallel computation and more effective incremental build • No to mention… ◦ Times to take us from the start of our app to our feature ◦ Creating specific environment for our development
  28. // build.gradle apply from: "deps.${properties.get('miniapp')}.gradle" // deps.inbox.gradle depedendecies { implementation

    project(':features:feature_inbox') implementation project(':features:feature_login') } // local.properties miniapp=inbox
  29. Dealing with legacy module :app feature train feature hotel base

    classes :common :app feature train :lib_base :feature_hotel :common :lib_common_data common data
  30. • Separate public and private APIs ◦ Feature hotel →

    :feature_hotel:impl, :feature_hotel:public • Separate wiring ◦ Feature hotel → :feature_hotel:wiring, :feature_hotel:fake_wiring • No more common modules, separated into tiny specific modules • Separated route module? Even more modules • Might be overkill 🤔 • But….
  31. More complicated? • Handling dynamic module? • Swap out file

    dependency with aar on-demand ◦ Gradle’s Include build?
  32. References • Android at Scale @Square: https://www.droidcon.com/media-detail?video=380843878 • Universal Router:

    https://github.com/esafirm/universal-router • Include build: https://publicobject.com/2021/03/11/includebuild/
  33. Si Uya Tinggal di Kenya Beli Rumah Banyak Mainan, Udah

    Ya Materinya Sekarang Waktunya Pertanyaan Esa Firman Sr Android Engineer at tiket.com Esa Firman esafirm