kmm으로 앱을 만들면 shared 모듈에서 공통 로직을 사용할것이다.
그런데 사용하다보면 문자열이나 이미지 등을 공유하여 사용하고 싶은데 마땅한 방법이 없다.
현재(2023.05.09)까진 공식적으로 지원하는 기능은 없고(베타니 당연한가..)
moko-resources 라는 것을 이용하여 리소스를 공유할수있다.
This is a Kotlin MultiPlatform library (and Gradle plugin) that provides access to the resources on macOS, iOS, Android the JVM and JS/Browser with the support of the default system localization.
android/ios 말고도 JS/Browser도 지원하는 좋은 라이브러이이다.
아래와 같은 빌드 환경에서 진행 하였다.
다음과 같은 과정을 거친다.
buildscript {
dependencies {
classpath("dev.icerock.moko:resources-generator:0.22.0")
}
}
...
plugins {
...
id("dev.icerock.mobile.multiplatform-resources")
}
kotlin {
...
cocoapods {
...
framework {
...
isStatic = true
export("dev.icerock.moko:resources:0.22.0")
export("dev.icerock.moko:graphics:0.9.0") // toUIColor here
}
}
sourceSets {
val commonMain by getting {
dependencies {
api("dev.icerock.moko:resources:0.22.0")
}
}
...
}
}
...
multiplatformResources {
// (필수) 반드시 입력
multiplatformResourcesPackage = "com.wonddak.mokoresources"
// (선택) 기본값 MR
multiplatformResourcesClassName = "SharedRes"
// (선택) 기본값 Public
// multiplatformResourcesVisibility = MRVisibility.Internal
// (선택) 기본값 en
iosBaseLocalizationRegion = "ko"
// (선택) 기본값 "commonMain"
// multiplatformResourcesSourceSet = "commonClientMain"
}
info.plist에 localizations 정보를 추가해준다.
<key>CFBundleLocalizations</key>
<array>
<string>ko</string>
<string>en</string>
</array>
Build Phase의 가장 마지막에 스크립트를 추가한다.
"$SRCROOT/../gradlew" -p "$SRCROOT/../" :shared:copyFrameworkResourcesToApp \
-Pmoko.resources.BUILT_PRODUCTS_DIR=$BUILT_PRODUCTS_DIR \
-Pmoko.resources.CONTENTS_FOLDER_PATH=$CONTENTS_FOLDER_PATH\
-Pkotlin.native.cocoapods.platform=$PLATFORM_NAME \
-Pkotlin.native.cocoapods.archs="$ARCHS" \
-Pkotlin.native.cocoapods.configuration=$CONFIGURATION
다음은 사용 방법이다.
shared/src/commonMain/resources/MR 경로를 만들자
그리고 base 폴더와 추가적인 지역문자열을 이름으로 폴더를 만들자.
위에서 기본 base를 ko로 설정했고 추가로 en폴더를 만들었다.
그리고 strings.xml을 만들어 다음과같이 추가하자.
<?xml version="1.0" encoding="UTF-8" ?>
<resources>
<string name="test">테스트 문자열</string>
<string name="test_format">%s 테스트 문자열</string>
</resources>
<?xml version="1.0" encoding="UTF-8" ?>
<resources>
<string name="test">test string</string>
<string name="test_format">%s test string</string>
</resources>
commomMain/kotlin//Strings.kt 파일에 함수를 작성하고 아래와 같이 작성하고
import dev.icerock.moko.resources.desc.StringDesc
import dev.icerock.moko.resources.desc.desc
import dev.icerock.moko.resources.format
fun getMyDesc(): StringDesc {
return SharedRes.strings.test.desc()
}
fun getMyFormatDesc(input: String): StringDesc {
return SharedRes.strings.test_format.format(input)
}
StringDesc형태로 리턴을 하기 때문에,
android에서는 toString(context)
ios에서는 localized()
를 뒤에 붙혀주셔야 됩니다.
..
import com.wonddak.mokoresources.getMyDesc
import com.wonddak.mokoresources.getMyFormatDesc
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MyApplicationTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colors.background
) {
val context = LocalContext.current
Column() {
Text(text = getMyDesc().toString(context))
Text(text = getMyFormatDesc("이것은").toString(context))
}
}
}
}
}
}
import SwiftUI
import shared
struct ContentView: View {
var body: some View {
VStack {
Text(StringsKt.getMyDesc().localized())
Text(StringsKt.getMyFormatDesc(input: "이것은").localized())
}
}
}
잘 적용된 모습입니다.
아래와 같이 User class가 있고
data class User(
val name : String,
val age : Int
)
user가 있으면 name을 없으면 "empty"를 반환하고 싶다고 하면 다음과 같이 사용해봅시다.
//Strings.kt
...
fun getUserName(user: User?): StringDesc {
return StringDesc.Raw(user?.name ?: "empty")
}
아래과 같이 compose에 Text를 추가해보면
Text(text = getUserName(null).toString(context))
Text(text = getUserName(User("wonddak",27)).toString(context))
설정한대로 null이면 empty를 아니면 user.name을 반환해주는 모습입니다.
추가로 테스트할때
StringDesc.localeType = StringDesc.LocaleType.Custom("en")
StringDesc.localeType = StringDesc.LocaleType.System()
강제로 바꿀수 있습니다.
문자열과 거의 동일하다
shared/src/commonMain/resources/MR/colors 경로를 만들자
그리고 colors.xml파일을 생성하자
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="valueColor">#B02743FF</color>
<color name="referenceColor">@color/valueColor</color>
<color name="themedColor">
<light>0xB92743FF</light>
<dark>7CCFEEFF</dark>
</color>
<color name="themedReferenceColor">
<light>@color/valueColor</light>
<dark>@color/referenceColor</dark>
</color>
</resources>
<color name="valueColor">#B02743FF</color>
지원하는 형식은 다음중 하나의 형태로 작성한다.([AA] - optional)
<color name="referenceColor">@color/valueColor</color>
@color/[name]형태로 사용한다.
<color name="themedColor">
<light>0xB92743FF</light>
<dark>7CCFEEFF</dark>
</color>
또한 값을 참조하여 사용할 수도 있다.
<color name="themedReferenceColor">
<light>@color/valueColor</light>
<dark>@color/referenceColor</dark>
</color>
commomMain/kotlin//Colors.kt 파일에 함수를 작성하고 아래와 같이 작성하고
package com.wonddak.mokoresources
import dev.icerock.moko.resources.ColorResource
fun getValueColor() : ColorResource {
return SharedRes.colors.valueColor
}
fun getReferenceColor() : ColorResource {
return SharedRes.colors.referenceColor
}
fun getThemeColor() : ColorResource {
return SharedRes.colors.themedColor
}
ColorResource형태로 리턴을 하기 때문에,
android에서는 getColor(context)
ios에서는 getUIColor()
를 뒤에 붙혀주셔야 됩니다.
확장 함수 추가
@Composable
fun ColorResource.toColor() : Color {
return colorResource(this.resourceId)
}
추가해주시면 사용하기 편합니다
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MyApplicationTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colors.background
) {
...
Text(
text = getMyDesc().toString(context),
color = getValueColor().toColor()
)
Text(
text = getMyFormatDesc("이것은").toString(context),
color = getThemeColor().toColor()
)
...
}
}
}
}
}
}
확장 함수 추가
extension ColorResource {
func toColor() -> SwiftUI.Color {
return SwiftUI.Color(getUIColor())
}
}
추가해주세용
struct ContentView: View {
var body: some View {
VStack {
Text(StringsKt.getMyDesc().localized())
.foregroundColor(ColorsKt.getValueColor().toColor())
Text(StringsKt.getMyFormatDesc(input: "이것은").localized())
.foregroundColor(ColorsKt.getThemeColor().toColor())
...
}
}
}
shared/src/commonMain/resources/MR/images 경로를 만들자
png 테스트 파일 다운로드
svg 테스트 파일 다운로드
이미지 파일을 추가하자
파일명은 다음과 같이 사용한다.
파일명[@0x].jpg
shared/src/commonMain/resources/MR/images/에 파일을 추가한다.
파일명에 대응되는 해상도는 다음과 같다.
@0.75x | @1x | @1.5x | @2x | @3x | @4x | |
---|---|---|---|---|---|---|
Android | ldpi | mdpi | hdpi | xhdpi | xxhdpi | xxxhdpi |
Ios | - | 1x | - | 2x | - | 3x |
경로에 그냥 추가해준다.
commomMain/kotlin//Images.kt 파일에 함수를 작성하고 아래와 같이 작성하고
import dev.icerock.moko.resources.ImageResource
fun getImage() : ImageResource {
return SharedRes.images.image
}
fun getClose() : ImageResource {
return SharedRes.images.close
}
ImageResource형태로 리턴을 하기 때문에,
android에서는 drawableResId 로 id를 가져올수 있고
ios에서는 toUIImage() 로 UiImage를 반환 할수 있습니다.
를 뒤에 붙혀주셔야 됩니다.
확장 함수 추가
@Composable
fun ImageResource.toResource() : Painter {
return painterResource(id = this.drawableResId)
}
사용하기~
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MyApplicationTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colors.background
) {
val context = LocalContext.current
Column() {
...
Image(painter = getImage().toResource(), contentDescription = "")
Icon(painter = getClose().toResource(), contentDescription = "")
}
}
}
}
}
}
struct ContentView: View {
var body: some View {
VStack {
...
Image(uiImage: ImagesKt.getImage().toUIImage()!)
Image(uiImage: ImagesKt.getClose().toUIImage()!)
}
}
}
이외에도 font 혹은 json/txt 파일등을 공유 할 수 있습니다.(시간나면 폰트와 파일부분도 추가로 올리겠습니다)
GIT에서 코드 확인이 가능합니다.
좋은 글 감사합니다~