KOTLIN MULTIPLATFORM IN ACTION

Transcription

KOTLIN MULTIPLATFORM IN ACTIONMORE THAN 10 PROJECTS FOR IOS AND ANDROID WITH SHARED CODEALEXANDR POGREBNYAK@KotlinMPPCopenhagenDenmark

About us Our experience in Kotlin Multiplatform moko.icerock.dev – a set of multiplatform libraries moko-widgets2

5 years in mobile development 80 mobile projects (iOS & Android) 40 developers in 2 offices3

Focus on:Avoid logic reduplication andkeep UI native4

Code Sharing in IceRockHot reloadNative UIReact NativeAsynchronous work with OSFlutterNeed to adopt Java-code to ObjCNew programming languageIntegration with OS via middle layerJ2ObjCNon-native UILegacy tech stacks5

Code Sharing at IceRockKotlin looks like SwiftInner libraries already in KotlinShift to native env at any momentFamiliar programming languageNative UIKotlin Multiplatform Kotlin/NativeFull access to Android OS and iOS featuresand SDKsThere are some limitations in Kotlin Swift directionDevs worry about integration6

Kotlin Multiplatform at IceRockUber-like appBMWApatris.io - finance service appBangkok Taxi ServiceeCommerceFirst KMPprojectSocial appGetChallenge - insta challengesResearchVekaFirst integrationVkusmen - delivery appBeGreat - habits tracking appArtPhoto - social appJ'JO - finance service AugSepOctNov7

IceRock KMP - 2018 Q3 Technical research First project to tryFirst KMP project Technical base:Research network serialization2 Jul2 Aug4 Sep key-value storage string resources access8

IceRock KMP - 2018 Q4 First internal KMP libsfor reuse Start two large-size projectson KMPApatris.iofinance service appeCommerceVekaFirst integration New features are in common: socket26 Sep9 Nov26 Nov28 Dec date/time formatting9

IceRock KMP - 2019 Q1 More developers triedKMP projects More internal KMP libsApatris.io - finance service app26 NoveCommerce28 DecSocial appBeGreat - habits tracking app New features in common: local storage - database9 Jan7 Mar3 Mar10

IceRock KMP - 2019 Q2 All new projects on Android iOS built with KMPApatris.ioGetChallenge26 NoveCommerce First attempts to share UI code first version of “widgets”28 Dec New features in share code:3 Mar permissions widgetsJ'JO - financeservice appSocial appArtPhotosocial appBeGreatUber-like app7 Mar1 Apr5 Apr1 May10 Jun10 May11

IceRock KMP - 2019 Q3Apatris.io26 Nov Added first styles to widgets(more customizations incommon code UI) Published our experience toworld - Medium posts28 Dec10 Jun10 May10 MayeCommerceGetChallenge - insta challengesJ'JO - finance service appArtPhoto - social appVkusmanUber-like app1 May1 Jul16 Aug3 Aug5 Sep25 Aug12

IceRock KMP - 2019 Q4eCommerce Started publishing internalKMP libs to OpenSource MOKO Large update and publishedwidgets to OpenSource target is full application from“shared code”28 Dec10 Jun10 May10 May1 MayGetChallenge - insta challengesJ'JO - finance service appArtPhoto - social appUber-like appBMW MotorradVkusmen - delivery app5 Sep11 NovNow13

Technical issues we faced1.Updates to libraries/Kotlin2.Suspend-functions in Kotlin3.Abstract in Kotlin4.in/out generics5.No generics in interface and function6.Multithreading with Kotlin/Native limitations7.Lack of incremental compilation in K/N, so it takes longer to compile8.Breakpoints through an xCode plug-in / lldbDetails on Medium: part 1, part 214

Communitymoko.icerock.dev15

moko.icerock.dev16

moko.icerock.dev17

Fast mp.icerock.dev18

A moko-template (sample project)Features: Shared business logicKotlin Gradle DSLModular-based architectureIndependent feature and domain modulesViewModels, LiveData, Resource management, Runtime permissions access,Media access, UI from shared code, Network layer generation from OpenAPI Install:git cloneapp nameapp o-template.gitDone!19

Result20

icerockdev/moko-templatewidgets branch21

goalsGoal #11 Kotlin developer 2 native mobile apps22

goalsGoal #2Fast start the codebase should grow withoutrewrites23

conceptsThe basic architecture concepts:1.2.3.4.compliance with platform rulesdeclare structure, not renderingcompile-time safetyreactive data handling24

codecommonclass App : BaseApplication() {override fun setup() {val theme Theme()}}registerScreenFactory(MainScreen::class) { MainScreen(theme) }override fun getRootScreen(): KClass out Screen Args.Empty {return MainScreen::class}25

codeandroidclass MainApplication : Application() {override fun onCreate() {super.onCreate()}}mppApplication App().apply {setup()}class MainActivity : HostActivity() {override val application: BaseApplicationget() MainApplication.mppApplication}companion object {lateinit var mppApplication: App}26

codeios@UIApplicationMainclass 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 urn true27

codecommonclass 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()))}}}28

codecommonclass 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()))}}}29

codecommonclass 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()))}}}30

codecommonclass 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()))}}}31

codecommonclass 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()))}}}32

codecommonclass 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()))}}}33

code34

codecommonclass 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}35

codecommonval theme Theme {textFactory FactoryBase.Style(textStyle TextStyle(size 24,color Colors.black),padding PaddingValues(padding 16f)))}36

codecommonval theme Theme {textFactory FactoryBase.Style(textStyle TextStyle(size 24,color Colors.black),padding PaddingValues(padding 16f)))}37

codecommonval theme Theme {textFactory FactoryBase.Style(textStyle TextStyle(size 24,color Colors.black),padding PaddingValues(padding 16f)))}38

code39

codeSimple is simple.How about some complexity?40

code41

codeimage centerbetween2 fields and a buttonin the center42

codecommonclass LoginScreen(private val theme: Theme) : WidgetScreen Args.Empty () {}override fun createContentWidget() with(theme) {constraint(size WidgetSize.AsParent) {// .}}43

codecommonoverride 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)))}}44

codecommonconstraint(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)45

codecommonconstraint(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)46

codecommonconstraint(size WidgetSize.AsParent) {val logoImage image(.)val emailInput input(.)val passwordInput input(.)val loginButton button(.)constraints {passwordInput centerYToCenterY rootpasswordInput leftRightToLeftRight rootemailInput bottomToTop passwordInputemailInput leftRightToLeftRight rootloginButton topToBottom passwordInputloginButton leftRightToLeftRight root}logoImage centerXToCenterX rootlogoImage.verticalCenterBetween(top root.top,bottom emailInput.top)47

codecommonconstraint(size WidgetSize.AsParent) {val logoImage image(.)val emailInput input(.)val passwordInput input(.)val loginButton button(.)constraints {passwordInput centerYToCenterY rootpasswordInput leftRightToLeftRight rootemailInput bottomToTop passwordInputemailInput leftRightToLeftRight rootloginButton topToBottom passwordInputloginButton leftRightToLeftRight root}logoImage centerXToCenterX rootlogoImage.verticalCenterBetween(top root.top,bottom emailInput.top)48

codecommonconstraint(size WidgetSize.AsParent) {val logoImage image(.)val emailInput input(.)val passwordInput input(.)val loginButton button(.)constraints {passwordInput centerYToCenterY rootpasswordInput leftRightToLeftRight rootemailInput bottomToTop passwordInputemailInput leftRightToLeftRight rootloginButton topToBottom passwordInputloginButton leftRightToLeftRight root}logoImage centerXToCenterX rootlogoImage.verticalCenterBetween(top root.top,bottom emailInput.top)49

codecommonconstraint(size WidgetSize.AsParent) {val logoImage image(.)val emailInput input(.)val passwordInput input(.)val loginButton button(.)constraints {passwordInput centerYToCenterY rootpasswordInput leftRightToLeftRight rootemailInput bottomToTop passwordInputemailInput leftRightToLeftRight rootloginButton topToBottom passwordInputloginButton leftRightToLeftRight root}logoImage centerXToCenterX rootlogoImage.verticalCenterBetween(top root.top,bottom emailInput.top)50

code51

code52

codecommonval loginTheme Theme(theme) {constraintFactory ntWidgetViewFactoryBase.Style(padding PaddingValues(16f),background Background(fill Fill.Solid(Colors.white))))}53

codecommonval loginTheme Theme(theme) {constraintFactory DefaultConstraintWidgetViewFactory(.)}imageFactory ewFactoryBase.Style(scaleType 4

codecommonval loginTheme Theme(theme) {constraintFactory DefaultConstraintWidgetViewFactory(.)imageFactory DefaultImageWidgetViewFactory(.)val corners platformSpecific(android 8f, ios 25f)}inputFactory ewFactoryBase.Style(margins MarginValues(bottom 8f),underLineColor Color(0xe5e6eeFF),labelTextStyle TextStyle(color Color(0x777889FF))))55

codecommonval loginTheme Theme(theme) {constraintFactory DefaultConstraintWidgetViewFactory(.)imageFactory DefaultImageWidgetViewFactory(.)val corners platformSpecific(android 16f, ios 25f)}inputFactory ewFactoryBase.Style(margins MarginValues(bottom 8f),underLineColor Color(0xe5e6eeFF),labelTextStyle TextStyle(color Color(0x777889FF))))56

codecommonval loginTheme Theme(theme) {constraintFactory DefaultConstraintWidgetViewFactory(.)imageFactory DefaultImageWidgetViewFactory(.)val corners .inputFactory DefaultInputWidgetViewFactory(.)}buttonFactory ViewFactoryBase.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)))57

code58

codehow add differentbutton?59

codeconstraint(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 loginButtonregisterButton rightToRight root60

codeconstraint(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 loginButtonregisterButton rightToRight root61

codeclass LoginScreen(.) : WidgetScreen Args.Empty () {override fun createContentWidget() .object Id {.object RegisterButtonId : ButtonWidget.Id}}62

codecommonval loginTheme Theme(theme) {// efaultButtonWidgetViewFactoryBase.Style(// .)),LoginScreen.Id.RegistrationButtonId)63

(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)))64

(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)))65

(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)))66

(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)))67

code68

codeMultiple screens?69

results70

results71

results72

codeLists?73

results74

conceptsThe basic architecture concepts:1.2.3.4.compliance with platform rulesdeclare structure, not renderingcompile-time safetyreactive data handling75

conceptsCompliance with platform rules:1. Activity recreation on Android2. Save instance state on Android3. All elements are native (UI/UX)76

conceptsDeclare structure, not rendering77

conceptsCompile-time safety:1.2.3.4.Type match of WidgetFactory and WidgetChild Widgets sizes compile-time checksWidgets Id match to Widget typeArguments in Screens78

conceptsType match of WidgetFactory and Widgetval theme Theme {textFactory DefaultTextWidgetViewFactory()}val theme Theme {textFactory DefaultContainerWidgetViewFactory()}79

conceptsChild Widgets sizes compile-time checksoverride fun createContentWidget() with(theme) {container(size WidgetSize.AsParent) {}}override fun createContentWidget() with(theme) {container(size WidgetSize.WrapContent) {}}80

conceptsChild Widgets sizes compile-time checksfun createContentWidget(): Widget WidgetSize.Const SizeSpec.AsParent,SizeSpec.AsParent 81

conceptsWidgets Id match to Widget typeobject RootContainerId: d)82

conceptsArguments in Screensclass NoArgsScreen : WidgetScreen Args.Empty ()routeToScreen(NoArgsScreen::class)83

conceptsArguments in Screensclass ArgsScreen : WidgetScreen Args.Parcel ArgsScreen.Arg ()routeToScreen(ArgsScreen::class, )84

conceptsReactive data handling:1. One-way binding via LiveData2. Two-way binding via MutableLiveData85

conceptsOne-way binding via LiveDataclass TimerViewModel : ViewModel() {val text: LiveData StringDesc }val viewModel getViewModel { TimerViewModel() }container(size WidgetSize.AsParent) {center {text(size WidgetSize.WrapContent, text viewModel.text)}}86

conceptsTwo-way binding via MutableLiveDataclass 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)87

conceptsTwo-way binding via MutableLiveDataclass FormField D, E {val data: MutableLiveData D val error: LiveData E? val isValid: LiveData Boolean }88

current list of widgetswith(theme) (.)singleChoice(.)switch(.)89

current list of widgetspublic 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 }90

references Widgets declare screens and structure of screens,but rendering is native Jetpack Compose & SwiftUI can be used to renderwidgets91

benefits 1 kotlin developer can create for both platformsnatively for developer and customerno limits with any modifications (feature, screen, )MVP quickly must redo natively in the future92

roadmapWhat’s available today (December, 5): Base widgets setBase navigation patternsBase styles for default widget-viewsA set of samplesActual version – 0.1.0-dev-593

roadmapDecember-January: Implement in production project Check flexibility of API to detect and fix limits – youcan help with it.Just try self and send feedback to github issues Screens actions API design (show toast, alert, route) More documentation and samples94

roadmapFebruary-March: Release 0.1.0 (with flexible API for customization) Codelabs and Medium posts with details of newversion95

roadmap2020 Q2-Q3: New widgetsMore variations of widgets rendersMore stylesProduction usage96

THANK YOUANDREMEMBERTO VOTEAlexandr Pogrebnyak @KotlinMPP#KotlinConf

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