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

Expedited Shipping: Accelerating iOS developmen...

Sponsored · SiteGround - Reliable hosting with speed, security, and support you can count on.

Expedited Shipping: Accelerating iOS development with KMP at Amazon

This deck is for a talk given at KotlinConf 2026. The following is the abstract:

This talk covers developing with Kotlin Multiplatform and iOS at scale using App Platform, an open-source lightweight application framework for state and memory management. Amazon Delivery ships a variety of applications spanning multiple device types and platforms that help facilitate the delivery process of millions of packages every day. These applications often talk to each other and share similar features in different form factors. We want to ship fast while maintaining feature parity across applications, and Kotlin Multiplatform is playing a big role in helping us accomplish this!

We've been using App Platform for a few years now in Android and JVM/Linux, but recently took the initiative to flesh out iOS support. iOS integration came with its own challenges, and we learned a lot. We’ll briefly introduce App Platform and where we have adopted it, then take a deeper look at our journey to iOS integration. It will cover how we introduced support for SwiftUI, defined patterns for navigation, onboarded real-world use cases that now serve hundreds of thousands of delivery drivers, and how we are tackling development at scale in an organization with hundreds of developers and dozens of teams.

Avatar for Jessalyn

Jessalyn

June 02, 2026

Other Decks in Technology

Transcript

  1. Why KMP • Devices deeply interconnected • Share business logic,

    data, and state • Wanted to be flexible to support new form factors
  2. Why KMP • Devices deeply interconnected • Share business logic,

    data, and state • Wanted to be flexible to support new form factors • Developer imbalance • Features often disparate
  3. Why KMP • Devices deeply interconnected • Share business logic,

    data, and state • Wanted to be flexible to support new form factors • Developer imbalance • Features often disparate • Effort of bespoke implementations is too high
  4. KMP  Amazon Flex App • Larger application • Older

    code base • The only app that ships in iOS
  5. KMP  Amazon Flex App • Larger application • Older

    code base • The only app that ships in iOS • Requirements • Needed to keep UI native • APIs have to plug and play
  6. Business logic and UI • Presenters • Business logic that

    produces state as models interface Presenter<ModelT : BaseModel> { val model: StateFlow<ModelT> } interface MoleculePresenter<InputT : Any, ModelT : BaseModel> { @Composable fun present(input: InputT): ModelT }
  7. Business logic and UI • Presenters • Business logic that

    produces state as models interface Presenter<ModelT : BaseModel> { val model: StateFlow<ModelT> } interface MoleculePresenter<InputT : Any, ModelT : BaseModel> { @Composable fun present(input: InputT): ModelT } https://amzn.github.io/app-platform/presenter/
  8. Business logic and UI • Renderers • UI counterparts to

    presenters interface Renderer<in ModelT : BaseModel> { fun render(model: ModelT) } @ContributesRenderer class LoginRenderer : ComposeRenderer<Model>() { @Composable override fun Compose(model: Model) { if (model.loginInProgress) { CircularProgressIndicator() } else { Text("Login") } } }
  9. Business logic and UI • Renderers • UI counterparts to

    presenters • Support CMP and Android Views interface Renderer<in ModelT : BaseModel> { fun render(model: ModelT) } @ContributesRenderer class LoginRenderer : ComposeRenderer<Model>() { @Composable override fun Compose(model: Model) { if (model.loginInProgress) { CircularProgressIndicator() } else { Text("Login") } } } https://amzn.github.io/app-platform/renderer/
  10. Business logic and UI • Recap • Presenters contain business

    logic and produce a reactive flow of models • Renderers render views based on models interface LoginPresenter : MoleculePresenter<Unit, Model> { data class Model( val loginInProgress: Boolean, ) : BaseModel } @ContributesRenderer class LoginRenderer : ComposeRenderer<Model>() { @Composable override fun Compose(model: Model) { if (model.loginInProgress) { CircularProgressIndicator() } else { Text("Login") } } }
  11. Observing state public extension Kotlinx_coroutines_coreFlow { func values() -> AsyncThrowingStream<Any?,

    Error> { let collector = Kotlinx_coroutines_coreFlowCollectorImpl<Any?>() collect(collector: collector, completionHandler: collector.onComplete(_:)) return collector.values } }
  12. Observing state public extension Presenter { func viewModels<Model>(ofType type: Model.Type)

    -> AsyncThrowingStream<Model, Error> { model .values() .compactMap { $0 as? Model } .asAsyncThrowingStream() } } public extension Kotlinx_coroutines_coreFlow { func values() -> AsyncThrowingStream<Any?, Error> { let collector = Kotlinx_coroutines_coreFlowCollectorImpl<Any?>() collect(collector: collector, completionHandler: collector.onComplete(_:)) return collector.values } }
  13. Observing state @MainActor class ViewModelObserver: ObservableObject { @Published var viewModel:

    Model? private var task: Task<Void, Never>? = nil init<ViewModels: AsyncSequence>(viewModels: ViewModels, handleError: @escaping (Error) -> ()) where ViewModels.Element == Model { task = Task { @MainActor [weak self] in do { for try await viewModel in viewModels { self?.viewModel = viewModel } } catch { handleError(error) } } } deinit { task?.cancel() } }
  14. Observing state @MainActor class ViewModelObserver: ObservableObject { @Published var viewModel:

    Model? private var task: Task<Void, Never>? = nil init<ViewModels: AsyncSequence>(viewModels: ViewModels, handleError: @escaping (Error) -> ()) where ViewModels.Element == Model { task = Task { @MainActor [weak self] in do { for try await viewModel in viewModels { self?.viewModel = viewModel } } catch { handleError(error) } } } deinit { task?.cancel() } }
  15. Creating views Renderers: public interface RendererFactory { public fun <T

    : BaseModel> createRenderer(modelType: KClass<out T>): Renderer<T> public fun <T : BaseModel> getRenderer(modelType: KClass<out T>, rendererId: Int = 0): Renderer<T> }
  16. Creating views SwiftUI /// Displays the view model hierarchy from

    a root `Presenter`. struct PresenterView<Model: BaseModel>: View { ... }
  17. Creating views struct PresenterView<Model: BaseModel>: View { @StateObject var viewModelObserver:

    ViewModelObserver init(presenter: Presenter, viewModelType: Model.Type, handleViewModelError: @escaping (Error) -> ()) { self.init( viewModels: presenter.viewModels(ofType: viewModelType), handleViewModelError: handleViewModelError ) } init<ViewModels: AsyncSequence>(viewModels: ViewModels, handleViewModelError: @escaping (Error) -> ()) where ViewModels.Element == Model { self._viewModelObserver = StateObject(wrappedValue: ViewModelObserver( viewModels: viewModels, handleError: handleViewModelError )) } var body: some View { ... } @MainActor class ViewModelObserver: ObservableObject { ... } }
  18. Creating views struct PresenterView<Model: BaseModel>: View { @StateObject var viewModelObserver:

    ViewModelObserver init(presenter: Presenter, viewModelType: Model.Type, handleViewModelError: @escaping (Error) -> ()) { self.init( viewModels: presenter.viewModels(ofType: viewModelType), handleViewModelError: handleViewModelError ) } init<ViewModels: AsyncSequence>(viewModels: ViewModels, handleViewModelError: @escaping (Error) -> ()) where ViewModels.Element == Model { self._viewModelObserver = StateObject(wrappedValue: ViewModelObserver( viewModels: viewModels, handleError: handleViewModelError )) } var body: some View { ... } @MainActor class ViewModelObserver: ObservableObject { ... } }
  19. View “registering” @ContributesRenderer class LoginRenderer : ComposeRenderer<Model>() { @Composable override

    fun Compose(model: Model) { if (model.loginInProgress) { CircularProgressIndicator() } else { Text("Login") } } }
  20. View “registering” @ContributesRenderer class LoginRenderer : ComposeRenderer<Model>() { @Composable override

    fun Compose(model: Model) { if (model.loginInProgress) { CircularProgressIndicator() } else { Text("Login") } } }
  21. View “registering” public class PresenterViewRegistry { @MainActor private var registry:

    [ObjectIdentifier: (Any) -> AnyView] = [:] public init(registry: [ObjectIdentifier : (Any) -> AnyView] = [:]) { ... } public static var shared: PresenterViewRegistry = PresenterViewRegistry() } @MainActor public extension PresenterViewRegistry { func registerViewForModelType<Model, Content: View>(...) { ... } func makeViewForModel<Model>(...) -> some View { ... } }
  22. View “registering” • Protocol conformance protocol PresenterViewModel { associatedtype Renderer

    : View @ViewBuilder @MainActor func makeViewRenderer() -> Self.Renderer } extension BaseModel { @MainActor func getViewRenderer() -> AnyView { guard let viewModel = self as? (any PresenterViewModel) else { return fatalError("Error, some ViewModel was not implemented!") } return AnyView(viewModel.makeViewRenderer()) } }
  23. View “registering” struct PresenterView<Model: BaseModel>: View { @StateObject var viewModelObserver:

    ViewModelObserver init(presenter: Presenter, viewModelType: Model.Type, handleViewModelError: @escaping (Error) -> ()) { self.init(viewModels: presenter.viewModels(ofType: viewModelType), handleViewModelError: handleViewModelError) } init<ViewModels: AsyncSequence>(viewModels: ViewModels, handleViewModelError: @escaping (Error) -> ()) where ViewModels.Element == Model { self._viewModelObserver = StateObject(wrappedValue: ViewModelObserver( viewModels: viewModels, handleError: handleViewModelError )) } var body: some View { if let viewModel = viewModelObserver.viewModel { viewModel.getViewRenderer() } } @MainActor class ViewModelObserver: ObservableObject { ... } }
  24. Presenters and SwiftUI Views /// Displays the view model hieararchy

    from a root `Presenter`. struct PresenterView<Model: BaseModel>: View { ... } /// A protocol for models that create their own SwiftUI view representation. protocol PresenterViewModel { associatedtype Renderer : View @ViewBuilder @MainActor func makeViewRenderer() -> Self.Renderer } /// Extension to get the associated view from a model extension BaseModel { @MainActor func getViewRenderer() -> AnyView { ... }
  25. Presenters and SwiftUI Views struct RootPresenterView: View { var rootPresenter:

    Presenter var body: some View { PresenterView( presenter: rootPresenter, viewModelType: BaseModel.self, handleViewModelError: { error in fatalError("View model error occurred: \(error)") } ) } }
  26. Presenters and SwiftUI Views extension LoginPresenter.Model: PresenterViewModel { func makeViewRenderer()

    -> some View { LoginPresenterView(model: self) } } struct LoginPresenterView : View { var model: LoginPresenter.Model var body: some View { if model.loginInProgress { ProgressView("Loading...") } else { Text("Login") } } }
  27. Navigation and back gestures @Composable fun present(input: Unit): Model {

    BackHandlerPresenter { // Handle a back press. } PredictiveBackHandlerPresenter { progress: Flow<BackEventCompat> -> // Handle back gesture started. try { progress.collect { backevent -> // Handle progress. } // Handle completion. } catch (e: CancellationException) { // Handle cancellation. } } } https://amzn.github.io/app-platform/presenter/#back-gestures
  28. Navigation, back gestures, and SwiftUI • SwiftUI challenges • Pushes

    navigation to the UI layer • Handles back gestures in a relatively closed loop
  29. Navigation, back gestures, and SwiftUI private struct NavigationStackView: View {

    @State private var path: [String] = [] var body: some View { NavigationStack(path: $path) { VStack { Text("I am the root view!") Button("Go to A") { path.append("A") } } .navigationDestination(for: String.self) { destination in switch destination { case "A": VStack { Text("Destination A") Button("Go to B") { path.append("B") } } case "B": Text("Destination B") default: EmptyView() } } } } }
  30. Navigation, back gestures, and SwiftUI private struct NavigationStackView: View {

    @State private var path: [String] = [] var body: some View { NavigationStack(path: $path) { VStack { Text("I am the root view!") Button("Go to A") { path.append("A") } } .navigationDestination(for: String.self) { destination in switch destination { case "A": VStack { Text("Destination A") Button("Go to B") { path.append("B") } } case "B": Text("Destination B") default: EmptyView() } } } } }
  31. Navigation, back gestures, and SwiftUI private struct NavigationStackView: View {

    @State private var path: [String] = [] var body: some View { NavigationStack(path: $path) { VStack { Text("I am the root view!") Button("Go to A") { path.append("A") } } .navigationDestination(for: String.self) { destination in switch destination { case "A": VStack { Text("Destination A") Button("Go to B") { path.append("B") } } case "B": Text("Destination B") default: EmptyView() } } } } }
  32. Navigation, back gestures, and SwiftUI class RootPresenter : MoleculePresenter<Unit, Model>

    { @Composable override fun present(input: Unit): Model { val backstack = remember { mutableStateListOf<MoleculePresenter<Unit, out BaseModel>>().apply { add(LoginPresenter(backstack = this)) } } return Model(modelBackstack = backstack.map { it.present(Unit) }) { event -> when (event) { is Event.BackstackModificationEvent -> { // Update backstack } } } } }
  33. Navigation, back gestures, and SwiftUI class RootPresenter : MoleculePresenter<Unit, Model>

    { @Composable override fun present(input: Unit): Model { val backstack = remember { mutableStateListOf<MoleculePresenter<Unit, out BaseModel>>().apply { add(LoginPresenter(backstack = this)) } } return Model(modelBackstack = backstack.map { it.present(Unit) }) { event -> when (event) { is Event.BackstackModificationEvent -> { // Update backstack } } } } }
  34. Navigation, back gestures, and SwiftUI extension RootPresenter.Model { func pathBinding()

    -> Binding<[Int]> { .init { Array(self.modelBackstack.indices.dropFirst()) } set: { modifiedIndices in let indicesBackstack = [0] + modifiedIndices.map { $0.toKotlinInt() } self.onEvent( RootPresenterEventBackstackModificationEvent ( indicesBackstack: indicesBackstack ) ) } } }
  35. Navigation, back gestures, and SwiftUI struct RootPresenterView: View { var

    model: RootPresenter.Model var body: some View { NavigationStack(path: model.pathBinding()) { model.modelBackstack[0].getViewRenderer() .navigationDestination(for: Int.self) { index in model.modelBackstack[index].getViewRenderer() } } } }
  36. Bridging iOS - recap • Launch a presenter hierarchy within

    the lifecycle of a `View` via `PresenterView` • Conform to `PresenterViewModel` to “self-register” views • Navigation with `NavigationStack` • Pre-compute models for `View` creation • Embrace stack modification from the UI layer and propagate modification events to keep the `Presenter` stack in sync
  37. DI • kotlin-inject and kotlin-inject-anvil • Frequently need to provide

    native concrete implementations of a given interface
  38. DI interface IosNativeImplementations { val analyticsService: AnalyticsService val httpConfigurationContextProviderHelper: HttpConfigurationContextProviderHelper

    val ktorSessionConfiguration: NSURLSessionConfiguration.() -> Unit } class SwiftToKotlinInjector: IosNativeImplementations { var analyticsService: AnalyticsService = ... var httpConfigurationContextProviderHelper: any HttpConfigurationContextProviderHelper = ... var ktorSessionConfiguration: (URLSessionConfiguration) -> Void = ... } public var kotlinAppPlatformApplication: ToOne<AppPlatformApplication> { .lazyVar { let app = AppPlatformApplication() app.create(iosNativeImplementations: SwiftToKotlinInjector()) return app } }
  39. DI

  40. DI // Generated object AnalyticsServiceFactory { var bind: (AnalyticsServiceSwiftBinding) ->

    AnalyticsService = { throw NotImplementedError("Swift binding not registered") } } @ContributesTo(AppScope::class) interface AnalyticsService_SwiftBindingProvider { @Provides @SingleIn(AppScope::class) fun provideAnalyticsService( logger: Logger ): AnalyticsService = AnalyticsServiceFactory.bind( object : AnalyticsServiceSwiftBinding { override val logger = logger } ) }
  41. DI public protocol ContributesBinding { static func create() } extension

    AnalyticsServiceImpl: ContributesBinding { static func create() { AnalyticsServiceFactory.shared.bind = { binding in AnalyticsServiceImpl(logger: binding.logger) } } }
  42. DI // Generated @objc final class AnalyticsServiceImpl_Registrar: NSObject, KotlinInjectRegistrar {

    public static func registerBindings() { AnalyticsServiceImpl.create() } }
  43. Summary • Use KMP in many of our apps in

    Amazon Delivery • Built reusable patterns for view integration, navigation, and more