kmm에서 resource 공유하기

WonDDak·2023년 5월 9일
1

KMP- Kotlin MultiPlatform

목록 보기
4/12

들어가며

kmm으로 앱을 만들면 shared 모듈에서 공통 로직을 사용할것이다.

그런데 사용하다보면 문자열이나 이미지 등을 공유하여 사용하고 싶은데 마땅한 방법이 없다.

현재(2023.05.09)까진 공식적으로 지원하는 기능은 없고(베타니 당연한가..)
moko-resources 라는 것을 이용하여 리소스를 공유할수있다.

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도 지원하는 좋은 라이브러이이다.

빌드 환경

아래와 같은 빌드 환경에서 진행 하였다.

  • Android Studio : Android Studio Flamingo | 2022.2.1 Patch 1
  • kmm plugin : 0.5.3(222)-12
  • jdk : 17
  • 프로젝트는 cocoapods사용으로 생성
  • gradle : 7.4.1
  • kotlin : 1.8.0

gradle 설정

다음과 같은 과정을 거친다.

build.gradle.kts (project)

buildscript {
    dependencies {
        classpath("dev.icerock.moko:resources-generator:0.22.0")
    }
}
...

build.gradle.kts (shared)

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"
}

Xcode 설정

localizations 설정

info.plist에 localizations 정보를 추가해준다.

<key>CFBundleLocalizations</key>
	<array>
		<string>ko</string>
		<string>en</string>
	</array>

Build Phase 추가

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을 만들어 다음과같이 추가하자.

파일 생성

base/strings.xml

<?xml version="1.0" encoding="UTF-8" ?>
<resources>
    <string name="test">테스트 문자열</string>
    <string name="test_format">%s 테스트 문자열</string>
</resources>

en/strings.xml

<?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()
를 뒤에 붙혀주셔야 됩니다.

Android

..
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))
                    }
                }
            }
        }
    }
}

ios

import SwiftUI
import shared

struct ContentView: View {
	var body: some View {
        VStack {
            Text(StringsKt.getMyDesc().localized())
            Text(StringsKt.getMyFormatDesc(input: "이것은").localized())
        }
	}
}

잘 적용된 모습입니다.

string 자체를 반환할 경우

아래와 같이 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을 반환해주는 모습입니다.


Runtime 변경

추가로 테스트할때

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)

  • #RRGGBB[AA]
  • 0xRRGGBB[AA]
  • RRGGBB[AA]

다른 값을 참조하는 경우

	<color name="referenceColor">@color/valueColor</color>

@color/[name]형태로 사용한다.

light/dark 모드에서 색상을 다르게 할경우

 	<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()
를 뒤에 붙혀주셔야 됩니다.

Android

확장 함수 추가

@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()
                        )
       					...
                    }
                }
            }
        }
    }
}

ios

확장 함수 추가

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 테스트 파일 다운로드

이미지 추가

이미지 파일을 추가하자

png,jpg 파일의 경우

파일명은 다음과 같이 사용한다.

파일명[@0x].jpg

shared/src/commonMain/resources/MR/images/에 파일을 추가한다.
파일명에 대응되는 해상도는 다음과 같다.

@0.75x@1x@1.5x@2x@3x@4x
Androidldpimdpihdpixhdpixxhdpixxxhdpi
Ios-1x-2x-3x

svg 파일의 경우

경로에 그냥 추가해준다.

함수 추가

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를 반환 할수 있습니다.
를 뒤에 붙혀주셔야 됩니다.

Android

확장 함수 추가

@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 = "")
                    }
                }
            }
        }
    }
}

ios

struct ContentView: View {
    var body: some View {
        VStack {
        	...
            Image(uiImage: ImagesKt.getImage().toUIImage()!)
            Image(uiImage: ImagesKt.getClose().toUIImage()!)
        }
    }
}


이외에도 font 혹은 json/txt 파일등을 공유 할 수 있습니다.(시간나면 폰트와 파일부분도 추가로 올리겠습니다)

GIT에서 코드 확인이 가능합니다.

profile
안녕하세요. 원딱입니다.

1개의 댓글

comment-user-thumbnail
2024년 1월 3일

좋은 글 감사합니다~

답글 달기