◦ 개발 기간 1주일 ▪ KotlinContest 출품작 ▪ 대회 일정으로 인한 짧은 개발 기간 • 이번 Devfest 발표를 위해 ◦ Kotlin/Compose 최신 버전 마이그레이션 ◦ iOS 플랫폼 지원 https://github.com/kisa002/transer Git Repository
supportedLanguages: List<Language>, selectedSourceLanguage: String, selectedTargetLanguage: String, onSelectedSourceLanguage: (Language) -> Unit, onSelectedTargetLanguage: (Language) -> Unit, onClickClearData: () -> Unit, onClickContact: () -> Unit, onNotifyVisibleSelect: (Boolean) -> Unit = {} ) { ... } 상태는 아래로
◦ CashApp에서 만든 오픈소스 라이브러리 • Kotlin Multiplatform 지원 ◦ Android ◦ iOS ◦ Desktop ◦ … • SQLite, MySQL, PostgreSQL, HSQL 지원 CREATE TABLE recentTranslate ( idx INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, originalText TEXT NOT NULL, translatedText TEXT NOT NULL ); selectAll: SELECT * FROM recentTranslate ORDER BY idx DESC; insert: INSERT INTO recentTranslate (originalText, translatedText) VALUES (?, ?); deleteByIdx: DELETE FROM recentTranslate WHERE idx = ?; deleteByTranslatedText: DELETE FROM recentTranslate WHERE translatedText = ?; deleteAll: DELETE FROM recentTranslate;
◦ CashApp에서 만든 오픈소스 라이브러리 • Kotlin Multiplatform 지원 ◦ Android ◦ iOS ◦ Desktop ◦ … • SQLite, MySQL, PostgreSQL, HSQL 지원 CREATE TABLE recentTranslate ( idx INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, originalText TEXT NOT NULL, translatedText TEXT NOT NULL ); selectAll: SELECT * FROM recentTranslate ORDER BY idx DESC; insert: INSERT INTO recentTranslate (originalText, translatedText) VALUES (?, ?); deleteByIdx: DELETE FROM recentTranslate WHERE idx = ?; deleteByTranslatedText: DELETE FROM recentTranslate WHERE translatedText = ?; deleteAll: DELETE FROM recentTranslate;
◦ CashApp에서 만든 오픈소스 라이브러리 • Kotlin Multiplatform 지원 ◦ Android ◦ iOS ◦ Desktop ◦ … • SQLite, MySQL, PostgreSQL, HSQL 지원 CREATE TABLE recentTranslate ( idx INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, originalText TEXT NOT NULL, translatedText TEXT NOT NULL ); selectAll: SELECT * FROM recentTranslate ORDER BY idx DESC; insert: INSERT INTO recentTranslate (originalText, translatedText) VALUES (?, ?); deleteByIdx: DELETE FROM recentTranslate WHERE idx = ?; deleteByTranslatedText: DELETE FROM recentTranslate WHERE translatedText = ?; deleteAll: DELETE FROM recentTranslate;
◦ CashApp에서 만든 오픈소스 라이브러리 • Kotlin Multiplatform 지원 ◦ Android ◦ iOS ◦ Desktop ◦ … • SQLite, MySQL, PostgreSQL, HSQL 지원 CREATE TABLE recentTranslate ( idx INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, originalText TEXT NOT NULL, translatedText TEXT NOT NULL ); selectAll: SELECT * FROM recentTranslate ORDER BY idx DESC; insert: INSERT INTO recentTranslate (originalText, translatedText) VALUES (?, ?); deleteByIdx: DELETE FROM recentTranslate WHERE idx = ?; deleteByTranslatedText: DELETE FROM recentTranslate WHERE translatedText = ?; deleteAll: DELETE FROM recentTranslate;
◦ CashApp에서 만든 오픈소스 라이브러리 • Kotlin Multiplatform 지원 ◦ Android ◦ iOS ◦ Desktop ◦ … • SQLite, MySQL, PostgreSQL, HSQL 지원 CREATE TABLE recentTranslate ( idx INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, originalText TEXT NOT NULL, translatedText TEXT NOT NULL ); selectAll: SELECT * FROM recentTranslate ORDER BY idx DESC; insert: INSERT INTO recentTranslate (originalText, translatedText) VALUES (?, ?); deleteByIdx: DELETE FROM recentTranslate WHERE idx = ?; deleteByTranslatedText: DELETE FROM recentTranslate WHERE translatedText = ?; deleteAll: DELETE FROM recentTranslate;
◦ CashApp에서 만든 오픈소스 라이브러리 • Kotlin Multiplatform 지원 ◦ Android ◦ iOS ◦ Desktop ◦ … • SQLite, MySQL, PostgreSQL, HSQL 지원 CREATE TABLE recentTranslate ( idx INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, originalText TEXT NOT NULL, translatedText TEXT NOT NULL ); selectAll: SELECT * FROM recentTranslate ORDER BY idx DESC; insert: INSERT INTO recentTranslate (originalText, translatedText) VALUES (?, ?); deleteByIdx: DELETE FROM recentTranslate WHERE idx = ?; deleteByTranslatedText: DELETE FROM recentTranslate WHERE translatedText = ?; deleteAll: DELETE FROM recentTranslate;
• single ◦ 한 번만 생성 • factory ◦ 호출마다 생성 • viewModel ◦ Android ViewModel • get ◦ 의존성 주입 • singleOf, factoryOf, viewModelOf ◦ Constructor DSL ◦ get 생략 가능 • bind ◦ 정의에 대한 바인딩
PRIMARY KEY DEFAULT 0, sourceLanguage TEXT NOT NULL, sourceName TEXT NOT NULL, targetLanguage TEXT NOT NULL, targetName TEXT NOT NULL ); select: SELECT sourceLanguage, sourceName, targetLanguage, targetName FROM preferences; set: REPLACE INTO preferences (id, sourceLanguage, sourceName, targetLanguage, targetName) VALUES (0, ?, ?, ?, ?);
onCleared() } commonMain/BaseViewModel actual open class BaseViewModel actual constructor() { actual val viewModelScope: CoroutineScope = CoroutineScope(Dispatchers.Default) actual fun onCleared() { viewModelScope.cancel() } } desktopMain/BaseViewModel actual open class BaseViewModel : ViewModel() { actual val viewModelScope get() = (this as ViewModel).viewModelScope actual override fun onCleared() { super.onCleared() } } androidMain/BaseViewModel
onCleared() } commonMain/BaseViewModel actual open class BaseViewModel actual constructor() { actual val viewModelScope: CoroutineScope = CoroutineScope(Dispatchers.Default) actual fun onCleared() { viewModelScope.cancel() } } desktopMain/BaseViewModel actual open class BaseViewModel : ViewModel() { actual val viewModelScope get() = (this as ViewModel).viewModelScope actual override fun onCleared() { super.onCleared() } } androidMain/BaseViewModel Expect로 선언한 클래스/함수/객체 등은 각 플랫폼 Android/iOS/Desktop 에서 Actual을 통해 구현됨
WindowState = rememberWindowState( position = WindowPosition(BiasAlignment(0f, -.3f)), size = DpSize(width = 400.dp, height = 560.dp) ) ) { ... } 생성되는 위치 및 크기 등 윈도우 상태
private val getPreferencesUseCase: GetPreferencesUseCase ) { ... private fun makeSourceTargetPair(language: String, target: String, source: String): Pair<String, String> = when(language) { "und" -> target to source target -> source to target !in listOf(target, source) -> source to language else -> target to source } } 상황에 맞는 번역 언어 설정 로직
private val getPreferencesUseCase: GetPreferencesUseCase ) { suspend operator fun invoke(q: String) = detectLanguageUseCase(q).language.let { language -> val (source, target) = getPreferencesUseCase().firstOrNull() ?: throw NullPointerException("Preferences is null") makeSourceTargetPair(language, target.language, source.language).let { (target, source) -> translationRepository.translate(q = q, target = target, source = source) } } ... } Android/iOS/Desktop 등 여러 플랫폼에서 사용 가능한 번역 UseCase. API 통신, 언어 변환 공식 등 코틀린 하나로.
delay(700) runCatching { val (originalText, translatedText) = text to translateUseCase(text).translatedText.decodeHtmlEntities() emit(translatedText) ... }.onFailure { ... } } else { emit("") } }.stateIn(scope = viewModelScope, started = SharingStarted.WhileSubscribed(5000), initialValue = "") Android와 동일한 번역 UseCase 사용
delay(700) runCatching { val (originalText, translatedText) = text to translateUseCase(text).translatedText.decodeHtmlEntities() emit(translatedText) ... }.onFailure { ... } } else { emit("") } }.stateIn(scope = viewModelScope, started = SharingStarted.WhileSubscribed(5000), initialValue = "") 번역 결과를 한번 더 가공해주는 확장 함수
◦ 모듈화/의존성만 고려하면 큰 문제 없다. • Desktop ◦ 별다른 지식이 없어도 개발이 가능하다. ◦ AWT 경험이 있다면 조금 더 자유로운 개발이 가능하다. • iOS ◦ 기본적인 iOS, Xcode 지식을 알아야한다. ◦ KMP를 하는건지 iOS를 공부하는건지 라는 생각이 들 수 있다.
지옥이었다. ◦ 특히 Desktop의 경우 Memory 저장 내용만 있어 로컬에 저장하기 위해 많은 고생을 했다. GitHub 코드 검색해도 안 나왔음… • 지금은 조금 괜찮은 것 같습니다. ◦ 이제는 초기와 비교하면 KMP 관련 레퍼런스가 몇 배는 많아진 것 같다. ◦ 무엇보다 당시에는 Beta였다면 지금은 Stable이다.
가능합니다. ◦ Jetpack Compose에는 Android 의존성이 걸린 컴포저블이 존재한다. 만약 해당 컴포저블을 사용하였다면 해체해야 한다. ◦ 의존성 걸린 컴포저블이 없다면 그대로 사용 가능하다. • 지원되는 컴포저블이 늘어납니다. ◦ Compose Multiplatform 버전에 따라 사용가능한 요소도 늘어나고 있다. ◦ 한 예시로 기존에는 AlertDialog, DropdownMenu 사용이 불가능하였지만, 1.5.0으로 올라오면서 사용이 가능하다.