Overview
Kotlin Native 예제 Hello (Platform)!
예제 Networking and Data Storage
Conclusion
참고

Overview

Kotlin의 경우 현재 Android 개발에 많이 쓰이고 있습니다. Kotlin 언어 가이드에 따르면 현재
아래와 같은 플랫폼을 지원한다고 안내하고 있습니다.

  • iOS (arm32, arm64, simulator x86_64)
  • macOS (x86_64)
  • watchOS (arm32, arm64, x86)
  • tvOS (arm64, x86_64)
  • Android (arm32, arm64, x86, x86_64)
  • Windows (mingw x86_64, x86)
  • Linux (x86_64, arm32, arm64, MIPS, MIPS little endian)
  • WebAssembly (wasm32)

지원 목록 중에 눈에 띄는 iOS에 대하여 Kotlin으로 어느 정도까지 iOS App 개발이 가능하며, 어떤 방식으로 개발이 가능 한지 알아보기 위해, 아래 공식 문서에서 제공하는 샘플을 먼저 살펴 보았습니다.

Kotlin Native

kotlin

멀티 플랫폼은 아직은 Alpha 버전이어서 자주 바뀔 수 있음을 경고하고 있습니다.

kotlin
출처 : kotlinlang.org


Kotlin 공통 코드를 사용하여 같은 라이브러리 지원을 받고, 플랫폼을 지원하는 컴파일러 위에서 코드를 작성하는 것임을 알 수 있습니다.

kotlin


kotlin

Android와 iOS 구현시 common에 expected class를 정의하고, 각 플랫폼별 actual class 를 통하여 실제 구현을 하도록 합니다.

Kotlin으로 전체 코드를 작성하는 것으로 예상 하였으나, 공식문서를 대략적으로 파악해보니 공통의

코드를 정의하고 상세 코드의 경우 개별적 작성을 하는 방식임을 알 수 있습니다.

예제 Hello (Platform)!

대략적인 흐름은 파악 하였으니, 이제 예제를 기반으로 실제 구현된 모습을 살펴 보겠습니다.

일반적으로 프로그래밍 언어를 배울 때 가장 처음으로 해보는 것은 역시나 hello world이기 때문에

가이드에서 제공하는 hello world 프로젝트를 우선 분석해 보겠습니다.

kotlin

Android Studio 최신 업데이트 이후, 프로젝트 생성 시 아래 쪽에 멀티플랫폼을 지원하는 Template이 추가되었음을 확인할 수 있습니다.

kotlin

프로젝트 생성 시 AndroidApp, commonMain, iOSMain의 3개로 구분된 프로젝트가 생성됩니다.
더불어 hello를 출력하기 위한 기본 코드 역시 생성되어 있습니다.

commonMain의 Platform

expect class Platform() {
    val platform: String
}

androidMain의 Platform

actual class Platform actual constructor() {
    actual val platform: String = "Android ${android.os.Build.VERSION.SDK_INT}"
}

iosMain의 Platform

actual class Platform actual constructor() {
    actual val platform: String = UIDevice.currentDevice.systemName() + " " + UIDevice.currentDevice.systemVersion
}

commonMain의 expect class를 각 플랫폼별 Main에서 actual class 형태로 구현 되어 있습니다.

androidApp의 MainActivity

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import com.example.demo.shared.Greeting
import android.widget.TextView

fun greet(): String {
    return Greeting().greeting()
}

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val tv: TextView = findViewById(R.id.text_view)
        tv.text = greet()
    }
}

iosApp의 ContentView

import SwiftUI
import shared

func greet() -> String {

  return Greeting().greeting()

}

struct ContentView: View {

var body:some View {

     Text(greet())

   }

}

struct ContentView_Previews: PreviewProvider {

static var previews: some View {

      ContentView()
   }
}

각 플랫폼 별로 구현된 main을 모듈로 사용하는 app을 구현합니다. Android는 Kotlin을 사용하여 ios는 xcode로 swift를 사용하여 UI 표현 합니다.

kotlin
kotlin

이후 각 플랫폼 별 시뮬레이터 실행했을 때, 정상적으로 출력 되는 것을 확인할 수 있습니다.

그동안 테스트 할 때 보았던 hello world가 아닌 hello + “플랫폼 정보” 이긴 하지만, Kotlin을 이용하여 ios 출력해볼 수 있다는 것이 굉장히 흥미로운 부분입니다.

예제 Networking and Data Storage

단순 텍스트 출력인 hello 예제 외에 좀 더 본격적인 예제를 살펴 보았습니다. 이제 네트워크를 통하여 api 호출 후 list를 통하여 출력하는 예제를 보여드리겠습니다.

commonMain의 DatabaseDriverFactory

import com.squareup.sqldelight.db.SqlDriver

expect class DatabaseDriverFactory {
    fun createDriver(): SqlDriver
}

androidMain의 DatabaseDriverFactory

import android.content.Context
import com.jetbrains.handson.kmm.shared.cache.AppDatabase
import com.squareup.sqldelight.android.AndroidSqliteDriver
import com.squareup.sqldelight.db.SqlDriver

actual class DatabaseDriverFactory(private val context: Context) {
    actual fun createDriver(): SqlDriver {
        return AndroidSqliteDriver(AppDatabase.Schema, context, "test.db")
    }
}

iosMain의 DatabaseDriverFactory

import com.squareup.sqldelight.db.SqlDriver
import com.squareup.sqldelight.drivers.native.NativeSqliteDriver

actual class DatabaseDriverFactory {
    actual fun createDriver(): SqlDriver {
        return NativeSqliteDriver(AppDatabase.Schema, "test.db")
    }
}

DatabaseDriverFactory class의 경우, 각 플랫폼별로 구현합니다.

RocketLaunch Data class

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
data class RocketLaunch(
    @SerialName("flight_number")
    val flightNumber: Int,
    @SerialName("mission_name")
    val missionName: String,
    @SerialName("launch_year")
    val launchYear: Int,
    @SerialName("launch_date_utc")
    val launchDateUTC: String,
    @SerialName("rocket")
    val rocket: Rocket,
    @SerialName("details")
    val details: String?,
    @SerialName("launch_success")
    val launchSuccess: Boolean?,
    @SerialName("links")
    val links: Links
)

@Serializable
data class Rocket(
    @SerialName("rocket_id")
    val id: String,
    @SerialName("rocket_name")
    val name: String,
    @SerialName("rocket_type")
    val type: String
)

@Serializable
data class Links(
    @SerialName("mission_patch")
    val missionPatchUrl: String?,
    @SerialName("article_link")
    val articleUrl: String?
)

data class를 비롯한 domain 관련 된 부분은 Kotlin으로 작성된 공용 코드를 사용하는 구조입니다.

여기서 눈 여겨 볼 부분은, SqlDriver 라이브러리의 경우 Android는 AndroidSqliteDriver iOS는 NativeSqliteDriver를 사용하고 있다는 것입니다.

androidApp의 MainActivity

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.widget.FrameLayout
import android.widget.Toast
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import com.jetbrains.handson.kmm.shared.SpaceXSDK
import com.jetbrains.handson.kmm.shared.cache.DatabaseDriverFactory
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch
import androidx.core.view.isVisible
import kotlinx.coroutines.cancel

class MainActivity : AppCompatActivity() {
    private val mainScope = MainScope()

    private lateinit var launchesRecyclerView: RecyclerView
    private lateinit var progressBarView: FrameLayout
    private lateinit var swipeRefreshLayout: SwipeRefreshLayout

    private val sdk = SpaceXSDK(DatabaseDriverFactory(this))

    private val launchesRvAdapter = LaunchesRvAdapter(listOf())

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        title = "SpaceX Launches"
        setContentView(R.layout.activity_main)

        launchesRecyclerView = findViewById(R.id.launchesListRv)
        progressBarView = findViewById(R.id.progressBar)
        swipeRefreshLayout = findViewById(R.id.swipeContainer)

        launchesRecyclerView.adapter = launchesRvAdapter
        launchesRecyclerView.layoutManager = LinearLayoutManager(this)

        swipeRefreshLayout.setOnRefreshListener {
            swipeRefreshLayout.isRefreshing = false
            displayLaunches(true)
        }

        displayLaunches(false)
    }

    override fun onDestroy() {
        super.onDestroy()
        mainScope.cancel()
    }

    private fun displayLaunches(needReload: Boolean) {
        progressBarView.isVisible = true
        mainScope.launch {
            kotlin.runCatching {
                sdk.getLaunches(needReload)
            }.onSuccess {
                launchesRvAdapter.launches = it
                launchesRvAdapter.notifyDataSetChanged()
            }.onFailure {
                Toast.makeText(this@MainActivity, it.localizedMessage, Toast.LENGTH_SHORT).show()
            }
            progressBarView.isVisible = false
        }
    }
}

iosApp의 ContentView

import SwiftUI
import shared

struct ContentView: View {
  @ObservedObject private(set) var viewModel: ViewModel

    var body: some View {
        NavigationView {
            listView()
            .navigationBarTitle("SpaceX Launches")
            .navigationBarItems(trailing:
                Button("Reload") {
                    self.viewModel.loadLaunches(forceReload: true)
            })
        }
    }

    private func listView() -> AnyView {
        switch viewModel.launches {
        case .loading:
            return AnyView(Text("Loading...").multilineTextAlignment(.center))
        case .result(let launches):
            return AnyView(List(launches) { launch in
                RocketLaunchRow(rocketLaunch: launch)
            })
        case .error(let description):
            return AnyView(Text(description).multilineTextAlignment(.center))
        }
    }
}

extension ContentView {

    enum LoadableLaunches {
        case loading
        case result([RocketLaunch])
        case error(String)
    }

    class ViewModel: ObservableObject {
        let sdk: SpaceXSDK
        @Published var launches = LoadableLaunches.loading

        init(sdk: SpaceXSDK) {
            self.sdk = sdk
            self.loadLaunches(forceReload: false)
        }

        func loadLaunches(forceReload: Bool) {
            self.launches = .loading
            sdk.getLaunches(forceReload: forceReload, completionHandler: { launches, error in
                if let launches = launches {
                    self.launches = .result(launches)
                } else {
                    self.launches = .error(error?.localizedDescription ?? "error")
                }
            })
        }
    }
}

extension RocketLaunch: Identifiable { }

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        /*@START_MENU_TOKEN@*/Text("Hello, World!")/*@END_MENU_TOKEN@*/
    }
}

hello 예제와 동일하게 각 플랫폼 별 list 라이브러리를 이용하여 ui를 표현해 줍니다.

kotlin
kotlin

실행 결과 각 플랫폼 별로 리스트가 출력됩니다.

Conclusion

Reactive Native 와 flutter를 비롯한 멀티플랫폼의 경우 native에 비하여 느리거나 기존 native 개발자들이 새롭게 언어를 배워야 하는 문제가 있습니다. Kotlin의 경우 기존 Android 개발자가 추가 학습 없이 개발이 가능하기 때문에 쉽게 iOS App 개발이 가능하지 않을까, 라고 생각하였습니다.

의문에 대한 답을 찾기 위해 가볍게 살펴 본 Kotlin Native는, 기존 생각과 달리 모든 부분을 Kotlin 으로 작성 하는 것은 아니었습니다. Kotlin 개발자가 iOS 개발을 한다기보다는 공통 domain 영역을 Kotlin으로 작성하고, 이 부분을 라이브러리처럼 사용하면서 UI를 해당 플랫폼의 언어로 개발하는 형태입니다.

대다수 Android와 iOS App을 병행 개발 한다고 할 때 domain 부분에 대한 처리를 양쪽이 동일한 형태로 만들 수 있는 장점이 있습니다. iOS쪽은 UI부분에 좀 더 집중할 수 있을 것이고요. 플랫폼 별 UI 구현 역시 native 속도 또한 잃지 않을 수 있습니다. 하지만 이것은 happy case에 대한 가정입니다. 현재 간단한 예제를 따라 만들어 보면 Kotlin Native는 아직 Alpha이기 때문이라는 점, 더불어 문서에 최신 tool의 내용이 미반영 되어 있다는 것, bug인지 알수 없는 tool상의 오류, ios의 경우 공통 영역의 디버깅이 어렵다는 점 등과 참고 자료 역시 충분하지 않다는 문제점이 보입니다.

향후 정식 버전까지 문제점에 대하여 지속적인 수정과 보안이 이루어진다면 분명 Kotlin Native 역시 멀티 플랫폼의 한가지 대안이 될수 있다고 생각합니다. Swift 또한 단순히 iOS 개발만을 위한 언어가 아니기 때문에 ‘Swift로 Android 지원이 가능해진다면 어떨까?’하는 생각을 하며 Kotlin Native에 대한 글을 마무리 합니다.

참고

https://kotlinlang.org/docs
https://play.kotlinlang.org/hands-on/Networking and Data Storage with Kotlin Multiplatfrom Mobile/01_Introduction
https://www.raywenderlich.com/7357-ios-app-with-kotlin-native-getting-started


이재민 팀장 | MA팀
leejm4@brandi.co.kr
브랜디, 오직 예쁜 옷만