Skip to content

Instantly share code, notes, and snippets.

@ElianFabian
Last active March 30, 2024 19:26
Show Gist options
  • Save ElianFabian/8b6bcb0d2ed5dff1e30ea64d15f0fd76 to your computer and use it in GitHub Desktop.
Save ElianFabian/8b6bcb0d2ed5dff1e30ea64d15f0fd76 to your computer and use it in GitHub Desktop.
Class to access resources inside ViewModels without worrying about Context.
import android.graphics.drawable.Drawable
import android.os.Build
import androidx.annotation.ArrayRes
import androidx.annotation.BoolRes
import androidx.annotation.ColorRes
import androidx.annotation.DimenRes
import androidx.annotation.DrawableRes
import androidx.annotation.IntegerRes
import androidx.annotation.PluralsRes
import androidx.annotation.RequiresApi
import androidx.annotation.StringRes
import kotlinx.coroutines.flow.StateFlow
/**
* This interface is used to provide resources to the application.
*
* It can safely be used in classes like ViewModels.
*/
interface ResourceHelper {
fun getInt(@IntegerRes id: Int): Int
fun getBoolean(@BoolRes id: Int): Boolean
fun getColor(@ColorRes id: Int): Int
fun getDimension(@DimenRes id: Int): Float
fun getDimensionPixelOffset(@DimenRes id: Int): Int
fun getDimensionPixelSize(@DimenRes id: Int): Int
@RequiresApi(Build.VERSION_CODES.Q)
fun getFloat(@DimenRes id: Int): Float
fun getString(@StringRes id: Int): String
fun getQuantityString(@PluralsRes id: Int, quantity: Int): String
fun getText(@StringRes id: Int): CharSequence
fun getQuantityText(@PluralsRes id: Int, quantity: Int): CharSequence
fun getIntArray(@ArrayRes id: Int): IntArray
fun getStringArray(@ArrayRes id: Int): Array<String>
fun getTextArray(@ArrayRes id: Int): Array<CharSequence>
fun getDrawable(@DrawableRes id: Int): Drawable?
fun getIntStateFlow(@IntegerRes id: Int): StateFlow<Int>
fun getBooleanStateFlow(@BoolRes id: Int): StateFlow<Boolean>
fun getColorStateFlow(@ColorRes id: Int): StateFlow<Int>
fun getDimensionStateFlow(@DimenRes id: Int): StateFlow<Float>
fun getDimensionPixelOffsetStateFlow(@DimenRes id: Int): StateFlow<Int>
fun getDimensionPixelSizeStateFlow(@DimenRes id: Int): StateFlow<Int>
@RequiresApi(Build.VERSION_CODES.Q)
fun getFloatStateFlow(@DimenRes id: Int): StateFlow<Float>
fun getStringStateFlow(@StringRes id: Int): StateFlow<String>
fun getQuantityStringStateFlow(@PluralsRes id: Int, quantity: Int): StateFlow<String>
fun getTextStateFlow(@StringRes id: Int): StateFlow<CharSequence>
fun getQuantityTextStateFlow(@PluralsRes id: Int, quantity: Int): StateFlow<CharSequence>
fun getIntArrayStateFlow(@ArrayRes id: Int): StateFlow<IntArray>
fun getStringArrayStateFlow(@ArrayRes id: Int): StateFlow<Array<String>>
fun getTextArrayStateFlow(@ArrayRes id: Int): StateFlow<Array<CharSequence>>
fun getDrawableStateFlow(@DrawableRes id: Int): StateFlow<Drawable?>
}
import android.content.ComponentCallbacks
import android.content.Context
import android.content.res.Configuration
import android.graphics.drawable.Drawable
import android.os.Build
import androidx.annotation.ArrayRes
import androidx.annotation.BoolRes
import androidx.annotation.ColorRes
import androidx.annotation.DimenRes
import androidx.annotation.DrawableRes
import androidx.annotation.IntegerRes
import androidx.annotation.PluralsRes
import androidx.annotation.RequiresApi
import androidx.annotation.StringRes
import androidx.core.content.ContextCompat
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
class ResourceHelperImpl(
private val context: Context,
) : ResourceHelper {
private val _stateFlowByResource = mutableMapOf<TypedResource, MutableStateFlow<in Any?>>()
init {
context.registerComponentCallbacks(object : ComponentCallbacks {
override fun onConfigurationChanged(newConfig: Configuration) {
update()
}
override fun onLowMemory() {}
})
}
override fun getInt(@IntegerRes id: Int): Int {
return context.resources.getInteger(id)
}
override fun getBoolean(@BoolRes id: Int): Boolean {
return context.resources.getBoolean(id)
}
override fun getColor(@ColorRes id: Int): Int {
return ContextCompat.getColor(context, id)
}
override fun getDimension(@DimenRes id: Int): Float {
return context.resources.getDimension(id)
}
override fun getDimensionPixelOffset(@DimenRes id: Int): Int {
return context.resources.getDimensionPixelOffset(id)
}
override fun getDimensionPixelSize(@DimenRes id: Int): Int {
return context.resources.getDimensionPixelSize(id)
}
@RequiresApi(Build.VERSION_CODES.Q)
override fun getFloat(@DimenRes id: Int): Float {
return context.resources.getFloat(id)
}
override fun getString(@StringRes id: Int): String {
return context.resources.getString(id)
}
override fun getQuantityString(
@PluralsRes
id: Int,
quantity: Int,
): String {
return context.resources.getQuantityString(id, quantity)
}
override fun getText(@StringRes id: Int): CharSequence {
return context.resources.getText(id)
}
override fun getQuantityText(
@PluralsRes
id: Int,
quantity: Int,
): CharSequence {
return context.resources.getQuantityText(id, quantity)
}
override fun getIntArray(@ArrayRes id: Int): IntArray {
return context.resources.getIntArray(id)
}
override fun getStringArray(@ArrayRes id: Int): Array<String> {
return context.resources.getStringArray(id)
}
override fun getTextArray(@ArrayRes id: Int): Array<CharSequence> {
return context.resources.getTextArray(id)
}
override fun getDrawable(@DrawableRes id: Int): Drawable? {
return ContextCompat.getDrawable(context, id)
}
override fun getIntStateFlow(@IntegerRes id: Int) = getOrCreateResourceStateFlow(
typedResource = TypedResource.Integer(id),
getResource = ::getInt,
)
override fun getBooleanStateFlow(@BoolRes id: Int) = getOrCreateResourceStateFlow(
typedResource = TypedResource.Boolean(id),
getResource = ::getBoolean,
)
override fun getColorStateFlow(@ColorRes id: Int) = getOrCreateResourceStateFlow(
typedResource = TypedResource.Color(id),
getResource = ::getColor,
)
override fun getDimensionStateFlow(@DimenRes id: Int) = getOrCreateResourceStateFlow(
typedResource = TypedResource.Dimension(id),
getResource = ::getDimension,
)
override fun getDimensionPixelOffsetStateFlow(@DimenRes id: Int) = getOrCreateResourceStateFlow(
typedResource = TypedResource.DimensionPixelOffset(id),
getResource = ::getDimensionPixelOffset,
)
override fun getDimensionPixelSizeStateFlow(@DimenRes id: Int) = getOrCreateResourceStateFlow(
typedResource = TypedResource.DimensionPixelSize(id),
getResource = ::getDimensionPixelSize,
)
@RequiresApi(Build.VERSION_CODES.Q)
override fun getFloatStateFlow(@DimenRes id: Int) = getOrCreateResourceStateFlow(
typedResource = TypedResource.Float(id),
getResource = ::getFloat,
)
override fun getStringStateFlow(@StringRes id: Int) = getOrCreateResourceStateFlow(
typedResource = TypedResource.String(id),
getResource = ::getString,
)
override fun getQuantityStringStateFlow(
@PluralsRes
id: Int,
quantity: Int,
) = getOrCreateResourceStateFlow(
typedResource = TypedResource.QuantityString(id, quantity),
getResource = { getQuantityString(id, quantity) },
)
override fun getTextStateFlow(@StringRes id: Int) = getOrCreateResourceStateFlow(
typedResource = TypedResource.Text(id),
getResource = ::getText,
)
override fun getQuantityTextStateFlow(
@PluralsRes
id: Int,
quantity: Int,
) = getOrCreateResourceStateFlow(
typedResource = TypedResource.QuantityText(id, quantity),
getResource = { getQuantityText(id, quantity) },
)
override fun getIntArrayStateFlow(@ArrayRes id: Int) = getOrCreateResourceStateFlow(
typedResource = TypedResource.IntArray(id),
getResource = ::getIntArray,
)
override fun getStringArrayStateFlow(@ArrayRes id: Int) = getOrCreateResourceStateFlow(
typedResource = TypedResource.StringArray(id),
getResource = ::getStringArray,
)
override fun getTextArrayStateFlow(@ArrayRes id: Int) = getOrCreateResourceStateFlow(
typedResource = TypedResource.TextArray(id),
getResource = ::getTextArray,
)
override fun getDrawableStateFlow(@DrawableRes id: Int) = getOrCreateResourceStateFlow(
typedResource = TypedResource.Drawable(id),
getResource = ::getDrawable,
)
private fun update() {
_stateFlowByResource.forEach { (typedResource, stateFlow) ->
val id = typedResource.id
stateFlow.value = when (typedResource) {
is TypedResource.Integer -> getInt(id)
is TypedResource.Boolean -> getBoolean(id)
is TypedResource.Color -> getColor(id)
is TypedResource.Dimension -> getDimension(id)
is TypedResource.DimensionPixelOffset -> getDimensionPixelOffset(id)
is TypedResource.DimensionPixelSize -> getDimensionPixelSize(id)
is TypedResource.Float -> {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
return
}
getFloat(id)
}
is TypedResource.String -> getString(id)
is TypedResource.QuantityString -> getQuantityString(id, typedResource.quantity)
is TypedResource.Text -> getText(id)
is TypedResource.QuantityText -> getQuantityText(id, typedResource.quantity)
is TypedResource.IntArray -> getIntArray(id)
is TypedResource.StringArray -> getStringArray(id)
is TypedResource.TextArray -> getTextArray(id)
is TypedResource.Drawable -> getDrawable(id)
}
}
}
private inline fun <T> getOrCreateResourceStateFlow(
typedResource: TypedResource,
getResource: (id: Int) -> T,
): StateFlow<T> {
if (typedResource in _stateFlowByResource) {
return getResourceStateFlow(typedResource)
}
val value = getResource(typedResource.id)
val stateFlow = MutableStateFlow(value)
addResourceStateFlow(typedResource, stateFlow)
return stateFlow
}
@Suppress("NOTHING_TO_INLINE", "UNCHECKED_CAST")
private inline fun addResourceStateFlow(typedResource: TypedResource, stateFlow: MutableStateFlow<*>) {
_stateFlowByResource[typedResource] = stateFlow as MutableStateFlow<in Any?>
}
@Suppress("NOTHING_TO_INLINE", "UNCHECKED_CAST")
private inline fun <T> getResourceStateFlow(typedResource: TypedResource): StateFlow<T> {
return _stateFlowByResource[typedResource] as StateFlow<T>
}
}
private sealed interface TypedResource {
val id: Int
class Integer(@IntegerRes override val id: Int) : TypedResource
class Boolean(@BoolRes override val id: Int) : TypedResource
class Color(@ColorRes override val id: Int) : TypedResource
class Dimension(@DimenRes override val id: Int) : TypedResource
class DimensionPixelOffset(@DimenRes override val id: Int) : TypedResource
class DimensionPixelSize(@DimenRes override val id: Int) : TypedResource
class Float(@DimenRes override val id: Int) : TypedResource
class String(@StringRes override val id: Int) : TypedResource
class QuantityString private constructor(
private val packedValue: Long
) : TypedResource {
@get:PluralsRes
override val id: Int get() = packedValue.toInt()
val quantity: Int get() = (packedValue shr 32).toInt()
constructor(
@PluralsRes
id: Int,
quantity: Int,
) : this(
packedValue = id.toLong() shl 32 or quantity.toLong(),
)
}
class Text(@StringRes override val id: Int) : TypedResource
class QuantityText private constructor(
private val packedValue: Long
) : TypedResource {
@get:PluralsRes
override val id: Int get() = packedValue.toInt()
val quantity: Int get() = (packedValue shr 32).toInt()
constructor(
@PluralsRes
id: Int,
quantity: Int,
) : this(
packedValue = id.toLong() shl 32 or quantity.toLong(),
)
}
class IntArray(@ArrayRes override val id: Int) : TypedResource
class StringArray(@ArrayRes override val id: Int) : TypedResource
class TextArray(@ArrayRes override val id: Int) : TypedResource
class Drawable(@DrawableRes override val id: Int) : TypedResource
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment