A context aware string abstraction for Android
Ask yourself a fairly standard question for any interview to fill an Android position: How would you provide a String from a ViewModel to display it with the View? This is not only a theoretical problem, but rather an applied one which surfaces in every project. Most solutions are fairly simple as well and result in either sending the
String
directly or providing a @StringRes int
string resource reference to resolve with the View, where both have their specific downsides and shortcomings. If you need the String
to be locale aware, the ViewModel requires access to the Context
(or rather the application context), while string resources only provide static values predefined and localized with the application.
When the requirements start to exceed these simple use-cases, you’ll find yourself writing abstractions for and around these solutions. You might need to format a string with a string resource, you might need to provide plurals depending on quantities provided by your API or you might even need to mix both, strings provided as an error from a backend and string resources for predefined or unknown error states. Eventually your ViewModel will know about the Context
or your View needs to learn how to handle these abstractions, which you’d like to avoid (or rather I’d like to avoid in my projects).
The Java String type
The simple and fairly static solution is to provide a simple CharSequence
like a String
or Spannable
and set it as text.
textView.setText("string")
There’s no big magic going on here. Depending on the kind of text view, though the CharSequence
might get wrapped into a more specific type like Editable
.
The Android String resource
Text views and similar widgets also provide overloads with @StringRes int
parameters instead of the standard CharSequence
.
textView.setText(R.string.resource)
The magic involved here is that these methods just wrap the context.getString(stringRes)
method to create the CharSequence
to provide to the original method.
Android String resource abstraction
When it comes to providing a mix of String
and string resources a common pattern is to provide both with a default value within the same entity. Depending on the availability of the provided property, your View would just use one or the other then.
data class ErrorMessage( @StringRes val messageId: Int = 0, val message: String? = null, ) fun ErrorMessage.getMessage(context: Context) = message ?: context.getString(messageId)
To support formatting argument or quantities, the entity could be extended further. Alternatively it would be possible to provide a sealed class to handle all the different use-cases here depending on the actual implementation.
Job Offers
The Android String type
To generalize the approach Android took for string resources, I created the context aware parcelable string type abstraction AString with the single function
invoke(Context): CharSequence?
– written in Java for Kotlin.
The core library itself implements types to support null-values, simple Java CharSequence
types (including String
and Spannable
), as well as string and text resources, with and without formatting arguments or quantities.
Furthermore I defined Kotlin extension functions as overloads for all methods usually taking a CharSequence
directly. So it’s not necessary to provide a CharSequence
or string resource directly with a view model to be used within the view implementation.
textView.setText("value".asAString()) textView.setText(StringResource(R.string.value))
The AString
interface is defined to be extensible, so that it’s also possible to encapsulate formatting logic and small conversions or access information directly from the Context
provided to the function.
@Parcelize data class LocalDateFormatAString( private val date: LocalDate, private val pattern: String, ) : AString { override fun invoke(context: Context) = date.format(DateTimeFormatter.ofPattern(pattern)) } @Parcelize object LocaleAString : AString { override fun invoke(context: Context) = context.resources.configuration.locales[0].toString() }
So there’s only a single type now to provide to the View and use as if it was a simple string.