Skip to content

Resources access for mobile (android & ios) Kotlin Multiplatform development

License

Notifications You must be signed in to change notification settings

Qawaz/moko-resources

Repository files navigation

moko-resources
GitHub licenseDownloadkotlin-version badge badge badge badge badge badge badge badge

Mobile Kotlin resources

This is a Kotlin MultiPlatform library that provides access to the resources on macOS, iOS, Android the JVM and JS/Browser with the support of the default system localization.

Table of Contents

Features

  • Strings, Plurals, Images, Fonts, Filesto access the corresponding resources from common code;
  • Colorswith light/dark mode support;
  • StringDescfor lifecycle-aware access to resources and unified localization on both platforms;
  • StaticiOS frameworks support;
  • Fat and XCframeworks support.

Requirements

  • Gradle version 6.8.3+
  • Android API 16+
  • iOS version 11.0+

Installation

root build.gradle

buildscript {
repositories {
gradlePluginPortal()
}

dependencies {
classpath"dev.icerock.moko:resources-generator:0.20.1"
}
}


allprojects {
repositories {
mavenCentral()
}
}

project build.gradle

applyplugin:"dev.icerock.mobile.multiplatform-resources"

dependencies {
commonMainApi("dev.icerock.moko:resources:0.20.1")
androidMainApi("dev.icerock.moko:resources-compose:0.20.1")
jvmMainApi("dev.icerock.moko:resources-compose:0.20.1")
commonTestImplementation("dev.icerock.moko:resources-test:0.20.1")
}

multiplatformResources {
multiplatformResourcesPackage="org.example.library"//required
multiplatformResourcesClassName="SharedRes"//optional, default MR
multiplatformResourcesVisibility=MRVisibility.Internal//optional, default Public
iosBaseLocalizationRegion="en"//optional, default "en"
multiplatformResourcesSourceSet="commonClientMain"//optional, default "commonMain"
}

To usetoUIColor(),toUIImage(),desc()and other iOS extensions from Swift - you shouldaddexportdeclarations:

framework {
export( "dev.icerock.moko:resources:0.20.1" )
export( "dev.icerock.moko:graphics:0.9.0" ) // toUIColor here
}

If your project includes a build type, for examplestagingwhich isn't in moko-resources. That isn't an issue. Use matchingFallbacks to specify alternative matches for a given build type, as shown below

buildTypes {
staging {
initWith debug
matchingFallbacks = ['debug']
}
}

ios-app Info.plist:

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

in array should be added all used languages.

JS/Browser generates json files which is included in webpack by default. For more details about JS seethisexample

Static kotlin frameworks support

If project configured with static framework output (for example byorg.jetbrains.kotlin.native.cocoapodsplugin) in Xcode project should be addedBuild Phase(at end of list) with script:

"$SRCROOT/../gradlew"-p"$SRCROOT/../":yourframeworkproject:copyFrameworkResourcesToApp \
-Pmoko.resources.PLATFORM_NAME=$PLATFORM_NAME\
-Pmoko.resources.CONFIGURATION=$CONFIGURATION\
-Pmoko.resources.BUILT_PRODUCTS_DIR=$BUILT_PRODUCTS_DIR\
-Pmoko.resources.CONTENTS_FOLDER_PATH=$CONTENTS_FOLDER_PATH

Please replace:yourframeworkprojectto kotlin project gradle path, and set correct relative path ($SRCROOT/../in example).
This phase will copy resources into application, because static frameworks can't have resources.

To disable warnings about static framework in gradle set flag:

multiplatformResources {
disableStaticFrameworkWarning=true
}

With Pods dependencies in Kotlin

When you useorg.jetbrains.kotlin.native.cocoapodsplugin and also kotlin module depends to Pods - you also need to pass extra properties:

"$SRCROOT/../gradlew"-p"$SRCROOT/../":shared:copyFrameworkResourcesToApp \
-Pmoko.resources.PLATFORM_NAME=$PLATFORM_NAME\
-Pmoko.resources.CONFIGURATION=$CONFIGURATION\
-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

iOS executable

When you useexecutablekotlin target you should add custom build phase to xcode, after kotlin compilation:

"$SRCROOT/../gradlew"-p"$SRCROOT/../":shared:copyResourcesDebugExecutableIosSimulatorArm64 \
-Pmoko.resources.BUILT_PRODUCTS_DIR=$BUILT_PRODUCTS_DIR\
-Pmoko.resources.CONTENTS_FOLDER_PATH=$CONTENTS_FOLDER_PATH

copyResourcesDebugExecutableIosSimulatorArm64should be configured depends on target.

Configured sample you can see insample/ios-app-TestKotlinApptarget

Usage

Example 1 - simple localization string

The first step is a create a filestrings.xmlincommonMain/resources/MR/basewith the following content:

<?xmlversion="1.0"encoding="UTF-8"?>
<resources>
<stringname="my_string">My default localization string</string>
</resources>

Next - create a filestrings.xmlwith localized strings incommonMain/resource/MR/<languageCode>.Here's an example of creatingcommonMain/resource/MR/rufor a Russian localization:

<?xmlversion="1.0"encoding="UTF-8"?>
<resources>
<stringname="my_string">Моя строка локализации по умолчанию</string>
</resources>

After adding the resources we can call a gradle sync or execute a gradle taskgenerateMRcommonMain.This will generate aMRclass containingMR.strings.my_string,which we can use incommonMain:

fungetMyString():StringDesc{
returnStringDesc.Resource(MR.strings.my_string)
}

After this we can use our functions on the platform side:
Android:

valstring=getMyString().toString(context=this)

iOS:

letstring=getMyString().localized()

JS:

valstrings=MR.stringsLoader.getOrLoad()//loading localization from a remote file
valstring=getMyString().localized(strings)

Note:StringDescis a multiple-source container for Strings: in StringDesc we can use a resource, plurals, formatted variants, or raw string. To convertStringDesctoStringon Android calltoString(context)(a context is required for the resources usage), on iOS - calllocalized().

MR directly from native side

Android:

valstring=MR.strings.my_string.desc().toString(context=this)

iOS:

letstring=MR.strings().my_string.desc().localized()

Get resourceId for Jetpack Compose / SwiftUI

Android:

valresId=MR.strings.my_string.resourceId

for example in Compose:

text=stringResource(id=MR.strings.email.resourceId)

iOS:

LocalizedStringKey(MR.strings().email.resourceId)

Note: more info in issue#126.

Example 2 - formatted localization string

IncommonMain/resources/MR/base/strings.xmladd:

<?xmlversion="1.0"encoding="UTF-8"?>
<resources>
<stringname="my_string_formatted">My format \'%s\'</string>
</resources>

Then add the localized values for other languages like in example #1. Now create the following function incommonMain:

fungetMyFormatDesc(input:String):StringDesc{
returnStringDesc.ResourceFormatted(MR.strings.my_string_formatted, input)
}

To create formatted strings from resources you can also use extensionformat:

fungetMyFormatDesc(input:String):StringDesc{
returnMR.strings.my_string_formatted.format(input)
}

Now add support on the platform side like in example #1:
Android:

valstring=getMyFormatDesc("hello").toString(context=this)

iOS:

letstring=getMyFormatDesc(input:"hello").localized()

Warning: Do no mix positioned placeholders with unpositioned ones within a string, as this may lead to different behaviour on different platforms. Stick to one style for each string.

Example 3 - plural string

The first step is to create a fileplurals.xmlincommonMain/resources/MR/basewith the following content:

<?xmlversion="1.0"encoding="UTF-8"?>
<resources>
<pluralname="my_plural">
<itemquantity="zero">zero</item>
<itemquantity="one">one</item>
<itemquantity="two">two</item>
<itemquantity="few">few</item>
<itemquantity="many">many</item>
<itemquantity="other">other</item>
</plural>
</resources>

Then add the localized values for other languages like in example #1.
Next, create a function incommonMain:

fungetMyPluralDesc(quantity:Int):StringDesc{
returnStringDesc.Plural(MR.plurals.my_plural, quantity)
}

Now add support on the platform side like in example #1:
Android:

valstring=getMyPluralDesc(10).toString(context=this)

iOS:

letstring=getMyPluralDesc(quantity:10).localized()

Example 4 - plural formatted string

The first step is to create fileplurals.xmlincommonMain/resources/MR/basewith the following content:

<?xmlversion="1.0"encoding="UTF-8"?>
<resources>
<pluralname="my_plural">
<itemquantity="zero">no items</item>
<itemquantity="one">%d item</item>
<itemquantity="two">%d items</item>
<itemquantity="few">%d items</item>
<itemquantity="many">%d items</item>
<itemquantity="other">%d items</item>
</plural>
</resources>

Then add the localized values for other languages like in example #1.
Next, create a function incommonMain:

fungetMyPluralFormattedDesc(quantity:Int):StringDesc{
//we pass quantity as selector for correct plural string and for pass quantity as argument for formatting
returnStringDesc.PluralFormatted(MR.plurals.my_plural, quantity, quantity)
}

To create formatted plural strings from resources you can also use extensionformat:

fungetMyPluralFormattedDesc(quantity:Int):StringDesc{
//we pass quantity as selector for correct plural string and for pass quantity as argument for formatting
returnMR.plurals.my_plural.format(quantity, quantity)
}

And like in example #1, add the platform-side support:
Android:

valstring=getMyPluralFormattedDesc(10).toString(context=this)

iOS:

letstring=getMyPluralFormattedDesc(quantity:10).localized()

Example 5 - pass raw string or resource

If we already use some resources as a placeholder value, we can useStringDescto change the string source:

fungetUserName(user:User?):StringDesc{
if(user!=null) {
returnStringDesc.Raw(user.name)
}else{
returnStringDesc.Resource(MR.strings.name_placeholder)
}
}

And just like in example 1 usage on platform side:
Android:

valstring1=getUserName(user).toString(context=this)//we got name from User model
valstring2=getUserName(null).toString(context=this)//we got name_placeholder from resources

iOS:

letstring1=getUserName(user:user).localized()// we got name from User model
letstring2=getUserName(user:null).localized()// we got name_placeholder from resources

Example 6 - Select localization in runtime

You can forceStringDescto use preferred localization in common code:

StringDesc.localeType=StringDesc.LocaleType.Custom("es")

and return to system behaviour (when localization depends on device settings):

StringDesc.localeType=StringDesc.LocaleType.System()

Example 7 - pass image

Image resources directory iscommonMain/resources/MR/imageswith support of nested directories.
Image name should be end with one of:

  • @0.75x- android ldpi;
  • @1x- android mdpi, ios 1x;
  • @1.5x- android hdpi;
  • @2x- android xhdpi, ios 2x;
  • @3x- android xxhdpi, ios 3x;
  • @4x- android xxxhdpi. Supportedpngandjpgresources for now.

If we add tocommonMain/resources/MR/imagesfiles:

We got autogeneratedMR.images.home_black_18ImageResourcein code, that we can use:

  • Android:imageView.setImageResource(image.drawableResId)
  • iOS:imageView.image = image.toUIImage()

You can get images by their name too

incommonMaincreate aResources.ktfile with the content below

fungetImageByFileName(name:String):ImageResource{
valfallbackImage=MR.images.transparent
returnMR.images.getImageByFileName(name)?:fallbackImage
}
  • Android:imageView.setImageResource(getDrawableByFileName( "image_name" ))
  • iOS:imageView.image = ResourcesKt.getDrawableByFileName(name: "image_name" ).toUIImage()!

Example 8 - pass font

Fonts resources directory iscommonMain/resources/MR/fonts.
Font name should be this pattern:<fontFamily>-<fontStyle>like:

  • Raleway-Bold.ttf
  • Raleway-Regular.ttf
  • Raleway-Italic.ttf Supportsttfandotfresources.

If we add tocommonMain/resources/MR/fontsfiles:

  • Raleway-Bold.ttf
  • Raleway-Regular.ttf
  • Raleway-Italic.ttf

We got autogeneratedMR.fonts.Raleway.italic,MR.fonts.Raleway.regular,MR.fonts.Raleway.boldFontResourcein code, that we can use:

  • Android:textView.typeface = font.getTypeface(context = this)
  • iOS:textView.font = font.uiFont(withSize: 14.0)

Example 9 - pass colors

Colors resources directory iscommonMain/resources/MR/colors.
Colors files isxmlwith format:

<?xmlversion="1.0"encoding="utf-8"?>
<resources>
<!--format: #RRGGBB[AA] or 0xRRGGBB[AA] or RRGGBB[AA] where [AA] - optional-->
<colorname="valueColor">#B02743FF</color>
<colorname="referenceColor">@color/valueColor</color>
<colorname="themedColor">
<light>0xB92743FF</light>
<dark>7CCFEEFF</dark>
</color>
<colorname="themedReferenceColor">
<light>@color/valueColor</light>
<dark>@color/referenceColor</dark>
</color>
</resources>

If you want use one color without light/dark theme selection:

<colorname="valueColor">#B02743FF</color>

If you want use value of other color - use references:

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

If you want different colors in light/dark themes:

<colorname="themedColor">
<light>0xB92743FF</light>
<dark>7CCFEEFF</dark>
</color>

Also themed colors can be referenced too:

<colorname="themedReferenceColor">
<light>@color/valueColor</light>
<dark>@color/referenceColor</dark>
</color>

Colors available in common code insodeMR.colors.**asColorResource.
ColorResourcecan beColorResource.Single- simple color without theme selection.
And can beColorResource.Themedwith colors for each mode.

You can read colors value from common code:

valcolor:Color=MR.colors.valueColor.color

but if you useColorResource.Themedyou can get current theme color only from platfrom side. Android:

valcolor:Color=MR.colors.valueColor.getColor(context=this)

iOS:

val color:UIColor=MR.colors.valueColor.getColor(UIScreen.main.traitCollection.userInterfaceStyle)

// If your SwiftUI View can not handle the run time dark/light mode changes for colors
// add this line on top of the View it will make it aware of dark/light mode changes
@Environment(\.colorScheme)varcolorScheme

You can get Color from resource on IOS with toUIColor For use it you should export moko-resources library to IOS

framework {
export(libs.mokoResources)
}

Example 10 - plain file resource access

The first step is a create a resource filetest.txtfor example, incommonMain/resources/MR/files After gradle sync we can get file by idMR.files.test Moko-resources has out of box implementation function for read text files from common code -readText()

Usage on Android:

val text = MR.files.test.getText(context = this)

Usage on Apple:

val text = MR.files.test.readText()

If you want to read files not as text, add your own implementation to expect/actual FileResource

Example 11 - assets access

Assets allow you save directories hierarchy (in files structure is plain). Locate files tocommonMain/resources/MR/assetsand access to it byMR.assets.*

Creating Fat Framework with resources

Just useFatFrameworkTaskfrom kotlin plugin .

Creating XCFramework with resources

Just useXCFrameworkfrom kotlin plugin .

But if you usestatic frameworksrequired additional setup - add to Xcode build phase (at end):

"$SRCROOT/../gradlew"-p"$SRCROOT/../":shared:copyResourcesMPLReleaseXCFrameworkToApp \
-Pmoko.resources.BUILT_PRODUCTS_DIR=$BUILT_PRODUCTS_DIR\
-Pmoko.resources.CONTENTS_FOLDER_PATH=$CONTENTS_FOLDER_PATH

Details you can check in sample TestStaticXCFramework in ios-app. In this sample used mpp-hierarhical kotlin module with XCFramework.

Samples

Please see more examples in thesample directory.

Samplempp-hierarhicalcontains usage oforg.jetbrains.kotlin.native.cocoapodsplugin and unit tests with resources usage. Jvm-sampleto run it you should use IntelliJ IDEA.
macOS-sampleit contains two schemes. TestProj is the sample app and TestHierarchical is a splash-screen.
android-sampleTestHierarchical creates two launchers, the first one starts the sample-app at once, the second one allows to choose language before starting the sample.

Set Up Locally

Contributing

All development (both new features and bug fixes) is performed in thedevelopbranch. This waymasteralways contains the sources of the most recently released version. Please send PRs with bug fixes to thedevelopbranch. Documentation fixes in the markdown files are an exception to this rule. They are updated directly inmaster.

Thedevelopbranch is pushed tomasteron release.

For more details on contributing please see thecontributing guide.

License

Copyright 2019 IceRock MAG Inc.

Licensed under the Apache License, Version 2.0 (the "License" );
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http:// apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

About

Resources access for mobile (android & ios) Kotlin Multiplatform development

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Languages

  • Kotlin 100.0%