String resources in Kotlin Multi-platform

Multiple approaches to abstracting String resources for use in a Kotlin Multi-platform module.

String resources in Kotlin Multi-platform

Kotlin Multi-platform is a great feature that gives the ability to share code between the different parts of an application (mobile clients, web clients, and even the server). Using this feature often leads to the creation of a core module which contains the reusable code, such as, models and business logic. But most of the presentation layer can be written as Kotlin common code as well, barring the View implementations (Framework components, such as, Activities and Fragments in Android), including Presenters, Mappers, ViewModels, and View interfaces.

Instead of redundantly writing this code for each client, this code could be abstracted into another Kotlin Multi-platform module (e.g., presentation). But abstracting some of these components isn't so trivial. For instance, the Mappers, which are responsible for converting Models to ViewModels, usually need access to the system resources. Unfortunately, the accessing of resources for each system varies. So, an abstraction layer will have to be created for accessing resources. This article discusses abstraction approaches for accessing one of the most common resources: Strings.

TL;DR

There are multiple approaches to abstracting String resources for use in a Kotlin Multi-platform module.

Access by identifier

Creating an interface containing a function that retrieves a String by a provided identifier is one simple way to accessing String resources in a Kotlin common module. For instance:

interface StringRetriever {

    fun string(resourceId: Int): Lazy<String>
}

The implementation for the above interface on Android might look like this:

class StringProvider(private val context: Context) : StringRetriever {

    override fun string(resourceId: Int) = lazy { context.getString(resourceId) }
}

Then the Strings could be retrieved in a Kotlin common module Mapper:

class SomeMapper(providedStringRetriever: StringRetriever) : Mapper,
    StringRetriever by providedStringRetriever {
    
    private val myString by string(R.string.my_string)
}

One thing that should stand out with the above approach is that even though the retrieval of the String resource is abstracted, the identifier is not. The Android generated R class will not be available in a Kotlin common module.

Access by common identifier

Making an Enum Class whose properties represent the identifiers would allow the abstraction to work in a common module. But there would need to be some means of getting the actual identifier in each client module.

enum class ResourceIds {

    MY_STRING,
    ANOTHER_STRING
}

Which could be used like so:

class SomeMapper(providedStringRetriever: StringRetriever) : Mapper,
    StringRetriever by providedStringRetriever {
    
    private val myString by string(ResourceIds.MY_STRING)
}

Another class would have to be created, for each client, to map the ResourceIds values to the appropriate platform specific identifiers. Then the StringProvider implementation would have to be updated to reflect these changes:

class ResourceIdMapper {

    fun map(resourceId: ResourceIds): Int = when(resourceId) {
        MY_STRING -> R.string.my_string
        ANOTHER_STRING -> R.string.another_string
    }
}

interface StringRetriever {

    fun string(resourceId: ResourceIds): Lazy<String>
}

class StringProvider(
    private val context: Context,
    private val mapper: ResourceIdMapper
) : StringRetriever {

    override fun string(resourceId: ResourceIds) = lazy { context.getString(mapper.map(resourceId)) }
}

Access by interface

Another approach to abstracting the retrieval of String resources in a Kotlin Multi-platform module would be to define an interface with properties representing the available Strings. Then each implementation of the interface would provide the platform specific means of getting the Strings. For instance:

interface Strings {

    val myString: String
    val anotherString: String
}

Then an implementation of this on Android might look like this:

class StringsProvider(private val context: Context) : Strings {

    override val myString: String by lazy { context.getString(R.string.my_string) }
    
    override val anotherString: String by lazy { context.getString(R.string.another_string) }
}

Then the Strings could be retrieved like this:

class SomeMapper(providedStrings: Strings) : Mapper,
    Strings by providedStrings {
    
    fun map(someObject: SomeObject) = OtherObject(stringValue = myString)
}

Access with interface with abstracted implementation retrieval

In the preceeding approach, the StringsProvider could become rather verbose, especially, when there are a large quantity of Strings. This issue could be mitigated by using the retrieval mechanism from the first approach in the implementation:

class StringsProvider(private val stringRetriever: StringRetriever) : Strings,
    StringAccessor by stringRetriever {

    override val myString by string(R.string.my_string)
    
    override val anotherString by string(R.string.my_string)
}

End

There are numerous approaches which can be used to abstract the retrieval of String resources in Kotlin Multi-platform modules. The abstraction of String resources allows components dependent on those resources to be used in a platform agnostic manner which removes redundancy, improves consistency and testability, and provides flexibility for the implementation. This article discussed multiple approaches to retrieving a single String value with no arguments, but in a real world scenario, there might be a String which can be formatted, or represent a quantity value, by provided arguments. The approaches mentioned could be easily adapted to accommodate those scenarios as well.