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

KOTLIN MULTIPLATFORM IN ACTION

KOTLIN MULTIPLATFORM IN ACTION

MORE THAN 10 PROJECTS FOR IOS AND ANDROID WITH SHARED CODE

IceRock moko - Mobile Kotlin project aimed at simplifying and accelerating the development of native mobile applications for Android and iOS using Kotlin Multiplatform technology.

Over the past year, we have successfully completed more than 10 projects with Kotlin Multiplatform , with a significant reduction in development time and full realization of the benefits of the new environment. Come to this talk to learn more about how we're doing this.

https://kotlinconf.com/talks/5-dec/151717

Avatar for Aleksey Mikhailov

Aleksey Mikhailov

December 05, 2019
Tweet

Other Decks in Programming

Transcript

  1. Copenhagen Denmark KOTLIN MULTIPLATFORM IN ACTION MORE THAN 10 PROJECTS

    FOR IOS AND ANDROID WITH SHARED CODE ALEXANDR POGREBNYAK @KotlinMPP
  2. • About us • Our experience in Kotlin Multiplatform •

    moko.icerock.dev – a set of multiplatform libraries • moko-widgets 2
  3. • 5 years in mobile development • 80+ mobile projects

    (iOS & Android) • 40+ developers in 2 offices 3
  4. 5 Code Sharing in IceRock Flutter React Native J2ObjC New

    programming language Integration with OS via middle layer Non-native UI Legacy tech stacks Asynchronous work with OS Need to adopt Java-code to ObjC Native UI Hot reload
  5. 6 Kotlin Multiplatform + Kotlin/Native Code Sharing at IceRock Kotlin

    looks like Swift Inner libraries already in Kotlin Shift to native env at any moment Familiar programming language Native UI Full access to Android OS and iOS features and SDKs There are some limitations in Kotlin > Swift direction Devs worry about integration
  6. 7 Kotlin Multiplatform at IceRock 2018 2019 Nov Sep Oct

    Dec Jul Aug May Mar Apr Jun Jan Feb Nov Sep Oct Jul Aug Research First KMP project eCommerce Apatris.io - finance service app BeGreat - habits tracking app Social app Uber-like app J'JO - finance service app Bangkok Taxi Service ArtPhoto - social app BMW Vkusmen - delivery app GetChallenge - insta challenges Veka First integration
  7. • Technical research • First project to try • Technical

    base: ◦ network ◦ serialization ◦ key-value storage ◦ string resources access 8 IceRock KMP - 2018 Q3 2 Jul 2 Aug First KMP project 4 Sep Research
  8. 9 IceRock KMP - 2018 Q4 9 Nov 26 Sep

    26 Nov 28 Dec eCommerce Apatris.io finance service app Veka First integration • First internal KMP libs for reuse • Start two large-size projects on KMP • New features are in common: ◦ socket ◦ date/time formatting
  9. 10 IceRock KMP - 2019 Q1 • More developers tried

    KMP projects • More internal KMP libs • New features in common: ◦ local storage - database Apatris.io - finance service app BeGreat - habits tracking app Social app 9 Jan 7 Mar 3 Mar eCommerce 28 Dec 26 Nov
  10. 11 IceRock KMP - 2019 Q2 • All new projects

    on Android + iOS built with KMP • First attempts to share UI code - first version of “widgets” • New features in share code: ◦ permissions ◦ widgets BeGreat 1 Apr 5 Apr Apatris.io 26 Nov eCommerce 28 Dec 1 May 10 Jun 7 Mar 10 May Uber-like app ArtPhoto social app Social app 3 Mar J'JO - finance service app GetChallenge
  11. 12 IceRock KMP - 2019 Q3 • Added first styles

    to widgets (more customizations in common code UI) • Published our experience to world - Medium posts Apatris.io 26 Nov eCommerce 28 Dec GetChallenge - insta challenges 10 Jun J'JO - finance service app 10 May ArtPhoto - social app 10 May Uber-like app 1 May 1 Jul 16 Aug 5 Sep 3 Aug 25 Aug Vkusman
  12. 13 IceRock KMP - 2019 Q4 • Started publishing internal

    KMP libs to OpenSource - MOKO • Large update and published widgets to OpenSource - target is full application from “shared code” eCommerce 28 Dec GetChallenge - insta challenges 10 Jun J'JO - finance service app 10 May ArtPhoto - social app 10 May Uber-like app 1 May 11 Nov BMW Motorrad Vkusmen - delivery app 5 Sep Now
  13. 14 Technical issues we faced 1. Updates to libraries/Kotlin 2.

    Suspend-functions in Kotlin 3. Abstract in Kotlin 4. in/out generics 5. No generics in interface and function 6. Multithreading with Kotlin/Native limitations 7. Lack of incremental compilation in K/N, so it takes longer to compile 8. Breakpoints through an xCode plug-in / lldb Details on Medium: part 1, part 2
  14. 19 A moko-template (sample project) https://github.com/icerockdev/moko-template.git Features: Install: git clone

    Done! app_name app_icon applicationId • Shared business logic • Kotlin Gradle DSL • Modular-based architecture • Independent feature and domain modules • ViewModels, LiveData, Resource management, Runtime permissions access, Media access, UI from shared code, Network layer generation from OpenAPI…
  15. The basic architecture concepts: 1. compliance with platform rules 2.

    declare structure, not rendering 3. compile-time safety 4. reactive data handling 24 concepts
  16. 25 code class App : BaseApplication() { override fun setup()

    { val theme = Theme() registerScreenFactory(MainScreen::class) { MainScreen(theme) } } override fun getRootScreen(): KClass<out Screen<Args.Empty>> { return MainScreen::class } } common
  17. 26 code class MainApplication : Application() { override fun onCreate()

    { super.onCreate() mppApplication = App().apply { setup() } } companion object { lateinit var mppApplication: App } } class MainActivity : HostActivity() { override val application: BaseApplication get() = MainApplication.mppApplication } android
  18. 27 code @UIApplicationMain class AppDelegate: NSObject, UIApplicationDelegate { var window:

    UIWindow? func application(_ application: ..., didFinishLaunchingWithOptions ...) -> Bool { let app = App() app.setup() let screen = app.createRootScreen() let rootViewController = screen.createViewController() window = UIWindow(frame: UIScreen.main.bounds) window?.rootViewController = rootViewController window?.makeKeyAndVisible() return true } } ios
  19. 28 code class MainScreen( private val theme: Theme ) :

    WidgetScreen<Args.Empty>() { override fun createContentWidget() = with(theme) { container(size = WidgetSize.AsParent) { center { text( size = WidgetSize.WrapContent, text = const(MR.strings.hello_world.desc()) ) } } } } common
  20. 29 code class MainScreen( private val theme: Theme ) :

    WidgetScreen<Args.Empty>() { override fun createContentWidget() = with(theme) { container(size = WidgetSize.AsParent) { center { text( size = WidgetSize.WrapContent, text = const(MR.strings.hello_world.desc()) ) } } } } common
  21. 30 code class MainScreen( private val theme: Theme ) :

    WidgetScreen<Args.Empty>() { override fun createContentWidget() = with(theme) { container(size = WidgetSize.AsParent) { center { text( size = WidgetSize.WrapContent, text = const(MR.strings.hello_world.desc()) ) } } } } common
  22. 31 code class MainScreen( private val theme: Theme ) :

    WidgetScreen<Args.Empty>() { override fun createContentWidget() = with(theme) { container(size = WidgetSize.AsParent) { center { text( size = WidgetSize.WrapContent, text = const(MR.strings.hello_world.desc()) ) } } } } common
  23. 32 code class MainScreen( private val theme: Theme ) :

    WidgetScreen<Args.Empty>() { override fun createContentWidget() = with(theme) { container(size = WidgetSize.AsParent) { center { text( size = WidgetSize.WrapContent, text = const(MR.strings.hello_world.desc()) ) } } } } common
  24. 33 code class MainScreen( private val theme: Theme ) :

    WidgetScreen<Args.Empty>() { override fun createContentWidget() = with(theme) { container(size = WidgetSize.AsParent) { center { text( size = WidgetSize.WrapContent, text = const(MR.strings.hello_world.desc()) ) } } } } common
  25. 35 code class App : BaseApplication() { override fun setup()

    { val theme = Theme { // custom styles here } registerScreenFactory(MainScreen::class) { MainScreen(theme) } } override fun getRootScreen(): KClass<out Screen<Args.Empty>> { return MainScreen::class } } common
  26. 36 code val theme = Theme { textFactory = DefaultTextWidgetViewFactory(

    DefaultTextWidgetViewFactoryBase.Style( textStyle = TextStyle( size = 24, color = Colors.black ), padding = PaddingValues(padding = 16f) ) ) } common
  27. 37 code val theme = Theme { textFactory = DefaultTextWidgetViewFactory(

    DefaultTextWidgetViewFactoryBase.Style( textStyle = TextStyle( size = 24, color = Colors.black ), padding = PaddingValues(padding = 16f) ) ) } common
  28. 38 code val theme = Theme { textFactory = DefaultTextWidgetViewFactory(

    DefaultTextWidgetViewFactoryBase.Style( textStyle = TextStyle( size = 24, color = Colors.black ), padding = PaddingValues(padding = 16f) ) ) } common
  29. DEVELOPMENT 43 code class LoginScreen( private val theme: Theme )

    : WidgetScreen<Args.Empty>() { override fun createContentWidget() = with(theme) { constraint(size = WidgetSize.AsParent) { // ... } } } common
  30. DEVELOPMENT 44 code override fun createContentWidget() = with(theme) { constraint(size

    = WidgetSize.AsParent) { val logoImage = +image( size = WidgetSize.Const(SizeSpec.WrapContent, SizeSpec.WrapContent), image = const(Image.resource(MR.images.logo)) ) } } common
  31. DEVELOPMENT 45 code constraint(size = WidgetSize.AsParent) { val logoImage =

    +image(...) val emailInput = +input( size = WidgetSize.WidthAsParentHeightWrapContent, id = Id.EmailInputId, label = const("Email".desc() as StringDesc), field = viewModel.emailField ) val passwordInput = +input( size = WidgetSize.WidthAsParentHeightWrapContent, id = Id.PasswordInputId, label = const("Password".desc() as StringDesc), field = viewModel.passwordField ) } common
  32. DEVELOPMENT 46 code constraint(size = WidgetSize.AsParent) { val logoImage =

    +image(...) val emailInput = +input(...) val passwordInput = +input(...) val loginButton = +button( size = WidgetSize.Const(SizeSpec.AsParent, SizeSpec.Exact(50f)), text = const("Login".desc() as StringDesc), onTap = viewModel::onLoginPressed ) } common
  33. DEVELOPMENT 47 code constraint(size = WidgetSize.AsParent) { val logoImage =

    +image(...) val emailInput = +input(...) val passwordInput = +input(...) val loginButton = +button(...) constraints { passwordInput centerYToCenterY root passwordInput leftRightToLeftRight root emailInput bottomToTop passwordInput emailInput leftRightToLeftRight root loginButton topToBottom passwordInput loginButton leftRightToLeftRight root logoImage centerXToCenterX root logoImage.verticalCenterBetween( top = root.top, bottom = emailInput.top ) } common
  34. DEVELOPMENT 48 code constraint(size = WidgetSize.AsParent) { val logoImage =

    +image(...) val emailInput = +input(...) val passwordInput = +input(...) val loginButton = +button(...) constraints { passwordInput centerYToCenterY root passwordInput leftRightToLeftRight root emailInput bottomToTop passwordInput emailInput leftRightToLeftRight root loginButton topToBottom passwordInput loginButton leftRightToLeftRight root logoImage centerXToCenterX root logoImage.verticalCenterBetween( top = root.top, bottom = emailInput.top ) } common
  35. DEVELOPMENT 49 code constraint(size = WidgetSize.AsParent) { val logoImage =

    +image(...) val emailInput = +input(...) val passwordInput = +input(...) val loginButton = +button(...) constraints { passwordInput centerYToCenterY root passwordInput leftRightToLeftRight root emailInput bottomToTop passwordInput emailInput leftRightToLeftRight root loginButton topToBottom passwordInput loginButton leftRightToLeftRight root logoImage centerXToCenterX root logoImage.verticalCenterBetween( top = root.top, bottom = emailInput.top ) } common
  36. DEVELOPMENT 50 code constraint(size = WidgetSize.AsParent) { val logoImage =

    +image(...) val emailInput = +input(...) val passwordInput = +input(...) val loginButton = +button(...) constraints { passwordInput centerYToCenterY root passwordInput leftRightToLeftRight root emailInput bottomToTop passwordInput emailInput leftRightToLeftRight root loginButton topToBottom passwordInput loginButton leftRightToLeftRight root logoImage centerXToCenterX root logoImage.verticalCenterBetween( top = root.top, bottom = emailInput.top ) } common
  37. DEVELOPMENT 53 code val loginTheme = Theme(theme) { constraintFactory =

    DefaultConstraintWidgetViewFactory( DefaultConstraintWidgetViewFactoryBase.Style( padding = PaddingValues(16f), background = Background( fill = Fill.Solid(Colors.white) ) ) ) } common
  38. DEVELOPMENT 54 code val loginTheme = Theme(theme) { constraintFactory =

    DefaultConstraintWidgetViewFactory(...) imageFactory = DefaultImageWidgetViewFactory( DefaultImageWidgetViewFactoryBase.Style( scaleType = DefaultImageWidgetViewFactoryBase.ScaleType.FIT ) ) } common
  39. DEVELOPMENT 55 code val loginTheme = Theme(theme) { constraintFactory =

    DefaultConstraintWidgetViewFactory(...) imageFactory = DefaultImageWidgetViewFactory(...) val corners = platformSpecific(android = 8f, ios = 25f) inputFactory = DefaultInputWidgetViewFactory( DefaultInputWidgetViewFactoryBase.Style( margins = MarginValues(bottom = 8f), underLineColor = Color(0xe5e6eeFF), labelTextStyle = TextStyle( color = Color(0x777889FF) ) ) ) } common
  40. DEVELOPMENT 56 code val loginTheme = Theme(theme) { constraintFactory =

    DefaultConstraintWidgetViewFactory(...) imageFactory = DefaultImageWidgetViewFactory(...) val corners = platformSpecific(android = 16f, ios = 25f) inputFactory = DefaultInputWidgetViewFactory( DefaultInputWidgetViewFactoryBase.Style( margins = MarginValues(bottom = 8f), underLineColor = Color(0xe5e6eeFF), labelTextStyle = TextStyle( color = Color(0x777889FF) ) ) ) } common
  41. DEVELOPMENT 57 code val loginTheme = Theme(theme) { constraintFactory =

    DefaultConstraintWidgetViewFactory(...) imageFactory = DefaultImageWidgetViewFactory(...) val corners = ... inputFactory = DefaultInputWidgetViewFactory(...) buttonFactory = DefaultButtonWidgetViewFactory( DefaultButtonWidgetViewFactoryBase.Style( margins = MarginValues(top = 32f), background = StateBackground( normal = Background( fill = Fill.Solid(Color(0x6770e0FF)), shape = Shape.Rectangle(cornerRadius = corners) ), pressed = Background(...), disabled = Background(...) ), textStyle = TextStyle(color = Colors.white) ) ) } common
  42. DEVELOPMENT 60 code constraint(size = WidgetSize.AsParent) { val logoImage =

    +image(...) val emailInput = +input(...) val passwordInput = +input(...) val loginButton = +button(...) val registerButton = +button( id = Id.RegistrationButtonId, size = WidgetSize.Const(SizeSpec.WrapContent, SizeSpec.Exact(40f)), text = const("Registration".desc() as StringDesc), onTap = viewModel::onRegistrationPressed ) constraints { // ... registerButton topToBottom loginButton registerButton rightToRight root } }
  43. DEVELOPMENT 61 code constraint(size = WidgetSize.AsParent) { val logoImage =

    +image(...) val emailInput = +input(...) val passwordInput = +input(...) val loginButton = +button(...) val registerButton = +button( id = Id.RegistrationButtonId, size = WidgetSize.Const(SizeSpec.WrapContent, SizeSpec.Exact(40f)), text = const("Registration".desc() as StringDesc), onTap = viewModel::onRegistrationPressed ) constraints { // ... registerButton topToBottom loginButton registerButton rightToRight root } }
  44. DEVELOPMENT 62 code class LoginScreen(...) : WidgetScreen<Args.Empty>() { override fun

    createContentWidget() = ... object Id { ... object RegisterButtonId : ButtonWidget.Id } }
  45. DEVELOPMENT 63 code val loginTheme = Theme(theme) { // …

    setButtonFactory( DefaultButtonWidgetViewFactory( DefaultButtonWidgetViewFactoryBase.Style( // ... ) ), LoginScreen.Id.RegistrationButtonId ) } common
  46. DEVELOPMENT 64 code DefaultButtonWidgetViewFactoryBase.Style( margins = MarginValues(top = 16f), padding

    = platformSpecific( ios = PaddingValues(start = 16f, end = 16f), android = null ), background = StateBackground( normal = Background( fill = Fill.Solid(Colors.white), border = Border( color = Color(0xF2F2F8FF), width = 2f ), shape = Shape.Rectangle(cornerRadius = corners) ), pressed = Background(...), disabled = Background(...) ), textStyle = TextStyle(color = Color(0x777889FF)) ) common
  47. DEVELOPMENT 65 code DefaultButtonWidgetViewFactoryBase.Style( margins = MarginValues(top = 16f), padding

    = platformSpecific( ios = PaddingValues(start = 16f, end = 16f), android = null ), background = StateBackground( normal = Background( fill = Fill.Solid(Colors.white), border = Border( color = Color(0xF2F2F8FF), width = 2f ), shape = Shape.Rectangle(cornerRadius = corners) ), pressed = Background(...), disabled = Background(...) ), textStyle = TextStyle(color = Color(0x777889FF)) ) common
  48. DEVELOPMENT 66 code DefaultButtonWidgetViewFactoryBase.Style( margins = MarginValues(top = 16f), padding

    = platformSpecific( ios = PaddingValues(start = 16f, end = 16f), android = null ), background = StateBackground( normal = Background( fill = Fill.Solid(Colors.white), border = Border( color = Color(0xF2F2F8FF), width = 2f ), shape = Shape.Rectangle(cornerRadius = corners) ), pressed = Background(...), disabled = Background(...) ), textStyle = TextStyle(color = Color(0x777889FF)) ) common
  49. DEVELOPMENT 67 code DefaultButtonWidgetViewFactoryBase.Style( margins = MarginValues(top = 16f), padding

    = platformSpecific( ios = PaddingValues(start = 16f, end = 16f), android = null ), background = StateBackground( normal = Background( fill = Fill.Solid(Colors.white), border = Border( color = Color(0xF2F2F8FF), width = 2f ), shape = Shape.Rectangle(cornerRadius = corners) ), pressed = Background(...), disabled = Background(...) ), textStyle = TextStyle(color = Color(0x777889FF)) ) common
  50. DEVELOPMENT The basic architecture concepts: 1. compliance with platform rules

    2. declare structure, not rendering 3. compile-time safety 4. reactive data handling 75 concepts
  51. DEVELOPMENT 76 concepts Compliance with platform rules: 1. Activity recreation

    on Android 2. Save instance state on Android 3. All elements are native (UI/UX)
  52. DEVELOPMENT Compile-time safety: 1. Type match of WidgetFactory and Widget

    2. Child Widgets sizes compile-time checks 3. Widgets Id match to Widget type 4. Arguments in Screens 78 concepts
  53. DEVELOPMENT 79 concepts Type match of WidgetFactory and Widget val

    theme = Theme { textFactory = DefaultTextWidgetViewFactory() } val theme = Theme { textFactory = DefaultContainerWidgetViewFactory() }
  54. DEVELOPMENT 80 concepts Child Widgets sizes compile-time checks override fun

    createContentWidget() = with(theme) { container(size = WidgetSize.AsParent) { } } override fun createContentWidget() = with(theme) { container(size = WidgetSize.WrapContent) { } }
  55. DEVELOPMENT 81 concepts Child Widgets sizes compile-time checks fun createContentWidget():

    Widget<WidgetSize.Const<SizeSpec.AsParent, SizeSpec.AsParent>>
  56. DEVELOPMENT 82 concepts Widgets Id match to Widget type setContainerFactory(

    DefaultContainerWidgetViewFactory(), RootContainerId ) setTextFactory( DefaultTextWidgetViewFactory(), RootContainerId ) object RootContainerId: ContainerWidget.Id
  57. DEVELOPMENT Reactive data handling: 1. One-way binding via LiveData 2.

    Two-way binding via MutableLiveData 85 concepts
  58. DEVELOPMENT 86 concepts One-way binding via LiveData class TimerViewModel :

    ViewModel() { val text: LiveData<StringDesc> } val viewModel = getViewModel { TimerViewModel() } container(size = WidgetSize.AsParent) { center { text(size = WidgetSize.WrapContent, text = viewModel.text) } }
  59. DEVELOPMENT 87 concepts Two-way binding via MutableLiveData class InputViewModel :

    ViewModel() { val nameField: FormField<String, StringDesc> = FormField("") } val viewModel = getViewModel { InputViewModel() } input( size = WidgetSize.WidthAsParentHeightWrapContent, id = Id.NameInput, label = const(MR.strings.name_label.desc()), field = viewModel.nameField )
  60. DEVELOPMENT 88 concepts Two-way binding via MutableLiveData class FormField<D, E>

    { val data: MutableLiveData<D> val error: LiveData<E?> val isValid: LiveData<Boolean> }
  61. DEVELOPMENT 89 current list of widgets with(theme) { constraint(...) container(...)

    linear(...) scroll(...) stateful(...) tabs(...) clickable(...) image(...) text(...) input(...) button(...) list(...) collection(...) flatAlert(...) progressBar(...) singleChoice(...) switch(...) }
  62. DEVELOPMENT 90 current list of widgets public abstract class Widget<WS

    : WidgetSize> public constructor() { public abstract val size: WS public abstract fun buildView(viewFactoryContext: ViewFactoryContext): ViewBundle<WS> } public interface ViewFactory<W : Widget<out WidgetSize>> { public abstract fun <WS : WidgetSize> build(widget: W, size: WS, viewFactoryContext: ViewFactoryContext): ViewBundle<WS> }
  63. DEVELOPMENT • Widgets declare screens and structure of screens, but

    rendering is native • Jetpack Compose & SwiftUI can be used to render widgets 91 references
  64. DEVELOPMENT • 1 kotlin developer can create for both platforms

    • natively for developer and customer • no limits with any modifications (feature, screen, …) • MVP quickly ≠ must redo natively in the future 92 benefits
  65. DEVELOPMENT What’s available today (December, 5): • Base widgets set

    • Base navigation patterns • Base styles for default widget-views • A set of samples • Actual version – 0.1.0-dev-5 93 roadmap
  66. DEVELOPMENT 94 December-January: • Implement in production project • Check

    flexibility of API to detect and fix limits – you can help with it. Just try self and send feedback to github issues • Screens actions API design (show toast, alert, route) • More documentation and samples roadmap
  67. DEVELOPMENT February-March: • Release 0.1.0 (with flexible API for customization)

    • Codelabs and Medium posts with details of new version 95 roadmap
  68. DEVELOPMENT 2020 Q2-Q3: • New widgets • More variations of

    widgets renders • More styles • Production usage 96 roadmap