Last active
May 14, 2025 11:35
-
-
Save ElianFabian/406bdbccda2d8fdcbac45200247452d3 to your computer and use it in GitHub Desktop.
A interface to share string resources from domain layer to presentation layer. Based on: https://github.com/philipplackner/UniversalStringResources/blob/final/app/src/main/java/com/plcoding/universalstringresources/UiText.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package packagename | |
// Validation functions that returns a lists of error messages as UiText. | |
private const val ValidPasswordSpecialCharacters = "!?\$&#._-" | |
private const val ValidUsernameSpecialCharacters = "$&._-" | |
private const val MinDigitCount = 1 | |
private const val MinSpecialCharacterCount = 1 | |
private const val MinPasswordLength = 8 | |
private const val MaxPasswordLength = 25 | |
private const val MinUsernameLength = 2 | |
private const val MaxUsernameLength = 25 | |
// Source: https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/util/Patterns.java#435 | |
private val EmailAddressRegex = """ | |
[a-zA-Z0-9\+\.\_\%\-]{1,256} | |
@ | |
[a-zA-Z0-9][a-zA-Z0-9\-]{0,64} | |
(\.[a-zA-Z0-9][a-zA-Z0-9\-]{0,25})+ | |
""".trimIndent() | |
.replace("\n", "") | |
.toRegex() | |
fun validateEmail(email: String): List<UiText> { | |
val trimmedEmail = email.trim() | |
if (trimmedEmail.isBlank()) { | |
return listOf(UiText(R.string.Error_CantBeEmpty)) | |
} | |
if (!trimmedEmail.matches(EmailAddressRegex)) { | |
val errorMessage = UiText( | |
resId = R.string.Error_InvalidEmail, | |
args = uiTextArgsOf( | |
stringResArg(R.string.Value_EmailExample) | |
), | |
) | |
return listOf(errorMessage) | |
} | |
return emptyList() | |
} | |
fun validateUsername(username: String): List<UiText> { | |
val trimmedUsername = username.trim() | |
if (trimmedUsername.isBlank()) { | |
return listOf(UiText(R.string.Error_CantBeEmpty)) | |
} | |
return buildList { | |
if (trimmedUsername.length < MinUsernameLength) { | |
add(UiText(R.string.Error_TooShort, args = uiTextArgsOf(MinUsernameLength))) | |
} | |
if (trimmedUsername.length > MaxUsernameLength) { | |
add(UiText(R.string.Error_TooLong, args = uiTextArgsOf(MaxUsernameLength))) | |
} | |
val areThereNoValidCharacters = trimmedUsername.any { char -> | |
!char.isLetterOrDigit() && char !in ValidUsernameSpecialCharacters | |
} | |
if (areThereNoValidCharacters) { | |
add( | |
UiText( | |
resId = R.string.Error_ItCanOnlyHaveLettersNumbersAndTheseCharacters_characters, | |
args = uiTextArgsOf(ValidUsernameSpecialCharacters), | |
) | |
) | |
} | |
} | |
} | |
fun validatePassword(password: String): List<UiText> { | |
val trimmedPassword = password.trim() | |
if (trimmedPassword.isBlank()) { | |
return listOf(UiText(R.string.Error_CantBeEmpty)) | |
} | |
return buildList { | |
if (trimmedPassword.length < MinPasswordLength) { | |
add(UiText(R.string.Error_TooShort, args = uiTextArgsOf(MinPasswordLength))) | |
} | |
if (trimmedPassword.length > MaxPasswordLength) { | |
add(UiText(R.string.Error_TooLong, args = uiTextArgsOf(MaxPasswordLength))) | |
} | |
val digitCount = trimmedPassword.count { it.isDigit() } | |
if (digitCount < MinDigitCount) { | |
add( | |
UiText( | |
resId = R.plurals.Error_MustContainAtLeast_digitCount_Digits, | |
quantity = MinDigitCount, | |
args = uiTextArgsOf(MinDigitCount), | |
) | |
) | |
} | |
val specialCharacterCount = trimmedPassword.count { char -> char in ValidPasswordSpecialCharacters } | |
if (specialCharacterCount < MinSpecialCharacterCount) { | |
add( | |
UiText( | |
resId = R.plurals.Error_MustContainAtLeast_characterCount_SpecialCharactersOfThese_characters, | |
quantity = MinSpecialCharacterCount, | |
args = uiTextArgsOf(MinSpecialCharacterCount, ValidPasswordSpecialCharacters), | |
) | |
) | |
} | |
} | |
} | |
fun validateConfirmPassword(confirmPassword: String, password: String): List<UiText> { | |
val trimmedConfirmPassword = confirmPassword.trim() | |
val trimmedPassword = password.trim() | |
if (trimmedPassword.isBlank()) { | |
return emptyList() | |
} | |
if (trimmedConfirmPassword.isBlank()) { | |
return listOf(UiText(R.string.Error_CantBeEmpty)) | |
} | |
if (trimmedConfirmPassword != trimmedPassword) { | |
return listOf(UiText(R.string.Error_PasswordsDontMatch)) | |
} | |
return emptyList() | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@file:Suppress("DEPRECATION", "DEPRECATION_ERROR") | |
package packagename | |
import android.content.Context | |
import android.os.Parcel | |
import android.os.Parcelable | |
import androidx.annotation.BoolRes | |
import androidx.annotation.IntegerRes | |
import androidx.annotation.PluralsRes | |
import androidx.annotation.StringRes | |
import java.io.Serializable | |
sealed interface UiText : Parcelable { | |
fun asString(context: Context): String | |
} | |
fun UiText(value: String): UiText = when { | |
value.isEmpty() -> EmptyUiText | |
else -> DynamicString(value) | |
} | |
fun UiText( | |
@StringRes | |
resId: Int, | |
args: UiTextArgList = EmptyUiArgs, | |
): UiText = StringResource( | |
resId = resId, | |
args = args.rawList, | |
) | |
fun UiText( | |
@PluralsRes | |
resId: Int, | |
quantity: Int, | |
args: UiTextArgList = EmptyUiArgs, | |
): UiText = PluralsResource( | |
resId = resId, | |
quantity = quantity, | |
args = args.rawList, | |
) | |
fun uiTextArgsOf(arg0: Any?, vararg args: Any?): UiTextArgList { | |
val uiArgs = buildList { | |
add(arg0?.asUiArg()) | |
for (arg in args) { | |
add(arg?.asUiArg()) | |
} | |
} | |
return UiTextArgListImpl(uiArgs) | |
} | |
fun stringResArg( | |
@StringRes | |
resId: Int, | |
args: UiTextArgList = EmptyUiArgs, | |
): UiTextArg = StringResourceArg(resId, args.rawList) | |
fun pluralsResArg( | |
@PluralsRes | |
resId: Int, | |
quantity: Int, | |
args: UiTextArgList = EmptyUiArgs, | |
): UiTextArg = PluralsResourceArg(resId, quantity, args.rawList) | |
fun integerResArg(@IntegerRes resId: Int): UiTextArg = IntegerResourceArg(resId) | |
fun booleanResArg(@BoolRes resId: Int): UiTextArg = BooleanResourceArg(resId) | |
private val EmptyUiText: UiText = DynamicString("") | |
private data class DynamicString(val value: String) : UiText { | |
constructor(parcel: Parcel) : this(parcel.readString().orEmpty()) | |
override fun asString(context: Context) = value | |
override fun writeToParcel(dest: Parcel, flags: Int) { | |
dest.writeString(value) | |
} | |
override fun describeContents() = 0 | |
companion object CREATOR : Parcelable.Creator<DynamicString> { | |
override fun createFromParcel(parcel: Parcel) = DynamicString(parcel) | |
override fun newArray(size: Int): Array<DynamicString?> = arrayOfNulls(size) | |
} | |
} | |
private data class StringResource( | |
@StringRes | |
val resId: Int, | |
val args: List<UiTextArg?>, | |
) : UiText { | |
constructor(parcel: Parcel) : this( | |
resId = parcel.readInt(), | |
args = parcel.readList(), | |
) | |
override fun asString(context: Context): String { | |
val arguments = args.map { arg -> | |
arg?.getValue(context) | |
}.toTypedArray() | |
return context.getString(resId, *arguments) | |
} | |
override fun writeToParcel(dest: Parcel, flags: Int) { | |
dest.writeInt(resId) | |
dest.writeList(args) | |
} | |
override fun describeContents() = 0 | |
companion object CREATOR : Parcelable.Creator<StringResource> { | |
override fun createFromParcel(parcel: Parcel) = StringResource(parcel) | |
override fun newArray(size: Int): Array<StringResource?> = arrayOfNulls(size) | |
} | |
} | |
private data class PluralsResource( | |
@PluralsRes | |
val resId: Int, | |
val quantity: Int, | |
val args: List<UiTextArg?>, | |
) : UiText { | |
constructor(parcel: Parcel) : this( | |
resId = parcel.readInt(), | |
quantity = parcel.readInt(), | |
args = parcel.readList(), | |
) | |
override fun asString(context: Context): String { | |
val arguments = args.map { arg -> | |
arg?.getValue(context) | |
}.toTypedArray() | |
return context.resources.getQuantityString(resId, quantity, *arguments) | |
} | |
override fun writeToParcel(dest: Parcel, flags: Int) { | |
dest.writeInt(resId) | |
dest.writeInt(quantity) | |
dest.writeList(args) | |
} | |
override fun describeContents() = 0 | |
companion object CREATOR : Parcelable.Creator<PluralsResource> { | |
override fun createFromParcel(parcel: Parcel) = PluralsResource(parcel) | |
override fun newArray(size: Int): Array<PluralsResource?> = arrayOfNulls(size) | |
} | |
} | |
@Deprecated("Hidden from intellisense", level = DeprecationLevel.HIDDEN) | |
sealed interface UiTextArgList : Parcelable { | |
private companion object | |
} | |
private class UiTextArgListImpl( | |
val sourceList: List<UiTextArg?>, | |
) : UiTextArgList, List<UiTextArg?> by sourceList { | |
constructor(parcel: Parcel) : this(parcel.readList()) | |
override fun toString(): String = sourceList.toString() | |
override fun hashCode(): Int = sourceList.hashCode() | |
override fun equals(other: Any?): Boolean { | |
if (this === other) { | |
return true | |
} | |
if (other is UiTextArgListImpl) { | |
return sourceList == other.sourceList | |
} | |
return false | |
} | |
override fun writeToParcel(dest: Parcel, flags: Int) { | |
dest.writeList(sourceList) | |
} | |
override fun describeContents() = 0 | |
companion object CREATOR : Parcelable.Creator<UiTextArgListImpl> { | |
override fun createFromParcel(parcel: Parcel) = UiTextArgListImpl(parcel) | |
override fun newArray(size: Int): Array<UiTextArgListImpl?> = arrayOfNulls(size) | |
} | |
} | |
private val UiTextArgList.rawList: List<UiTextArg?> | |
get() { | |
this as UiTextArgListImpl | |
return sourceList | |
} | |
private val EmptyUiArgs: UiTextArgList = UiTextArgListImpl(emptyList()) | |
private data class SerializableArg(val value: Serializable) : UiTextArg { | |
constructor(parcel: Parcel) : this(parcel.readSerializable() as Serializable) | |
override fun writeToParcel(dest: Parcel, flags: Int) { | |
dest.writeSerializable(value) | |
} | |
override fun describeContents() = 0 | |
override fun toString() = value.toString() | |
companion object CREATOR : Parcelable.Creator<SerializableArg> { | |
override fun createFromParcel(parcel: Parcel) = SerializableArg(parcel) | |
override fun newArray(size: Int): Array<SerializableArg?> = arrayOfNulls(size) | |
} | |
} | |
private data class ParcelableArg(val value: Parcelable) : UiTextArg { | |
constructor(parcel: Parcel) : this(parcel.readParcelable(Parcelable::class.java.classLoader)!!) | |
override fun writeToParcel(parcel: Parcel, flags: Int) { | |
parcel.writeParcelable(value, flags) | |
} | |
override fun describeContents() = 0 | |
override fun toString() = value.toString() | |
companion object CREATOR : Parcelable.Creator<ParcelableArg> { | |
override fun createFromParcel(parcel: Parcel) = ParcelableArg(parcel) | |
override fun newArray(size: Int): Array<ParcelableArg?> = arrayOfNulls(size) | |
} | |
} | |
@Deprecated("Hidden from intellisense", level = DeprecationLevel.HIDDEN) | |
sealed interface UiTextArg : Parcelable { | |
private companion object | |
} | |
private data class BooleanResourceArg(@BoolRes val resId: Int) : UiTextArg { | |
constructor(parcel: Parcel) : this(parcel.readInt()) | |
override fun writeToParcel(dest: Parcel, flags: Int) { | |
dest.writeInt(resId) | |
} | |
override fun describeContents() = 0 | |
override fun toString() = "booleanRes(id=$resId)" | |
companion object CREATOR : Parcelable.Creator<BooleanResourceArg> { | |
override fun createFromParcel(parcel: Parcel) = BooleanResourceArg(parcel) | |
override fun newArray(size: Int): Array<BooleanResourceArg?> = arrayOfNulls(size) | |
} | |
} | |
private data class IntegerResourceArg(@IntegerRes val resId: Int) : UiTextArg { | |
constructor(parcel: Parcel) : this(parcel.readInt()) | |
override fun writeToParcel(dest: Parcel, flags: Int) { | |
dest.writeInt(resId) | |
} | |
override fun describeContents() = 0 | |
override fun toString() = "integerRes(id=$resId)" | |
companion object CREATOR : Parcelable.Creator<IntegerResourceArg> { | |
override fun createFromParcel(parcel: Parcel) = IntegerResourceArg(parcel) | |
override fun newArray(size: Int): Array<IntegerResourceArg?> = arrayOfNulls(size) | |
} | |
} | |
private data class StringResourceArg( | |
@StringRes | |
val resId: Int, | |
val args: List<UiTextArg?>, | |
) : UiTextArg { | |
constructor(parcel: Parcel) : this( | |
resId = parcel.readInt(), | |
args = parcel.readList(), | |
) | |
override fun writeToParcel(dest: Parcel, flags: Int) { | |
dest.writeInt(resId) | |
dest.writeList(args) | |
} | |
override fun describeContents() = 0 | |
override fun toString() = "stringRes(id=$resId, args=$args)" | |
companion object CREATOR : Parcelable.Creator<StringResourceArg> { | |
override fun createFromParcel(parcel: Parcel) = StringResourceArg(parcel) | |
override fun newArray(size: Int): Array<StringResourceArg?> = arrayOfNulls(size) | |
} | |
} | |
private data class PluralsResourceArg( | |
@PluralsRes | |
val resId: Int, | |
val quantity: Int, | |
val args: List<UiTextArg?>, | |
) : UiTextArg { | |
constructor(parcel: Parcel) : this( | |
resId = parcel.readInt(), | |
quantity = parcel.readInt(), | |
args = parcel.readList(), | |
) | |
override fun writeToParcel(dest: Parcel, flags: Int) { | |
dest.writeInt(resId) | |
dest.writeInt(quantity) | |
dest.writeList(args) | |
} | |
override fun describeContents() = 0 | |
override fun toString() = "pluralsRes(id=$resId, quantity=$quantity, args=$args)" | |
companion object CREATOR : Parcelable.Creator<PluralsResourceArg> { | |
override fun createFromParcel(parcel: Parcel) = PluralsResourceArg(parcel) | |
override fun newArray(size: Int): Array<PluralsResourceArg?> = arrayOfNulls(size) | |
} | |
} | |
private fun UiTextArg.getValue(context: Context): Any { | |
val resources = context.resources | |
return when (this) { | |
is SerializableArg -> value | |
is ParcelableArg -> value | |
is BooleanResourceArg -> resources.getBoolean(resId) | |
is IntegerResourceArg -> resources.getInteger(resId) | |
is StringResourceArg -> { | |
val arguments = args.map { arg -> arg?.getValue(context) }.toTypedArray() | |
resources.getString(resId, *arguments) | |
} | |
is PluralsResourceArg -> { | |
val arguments = args.map { arg -> arg?.getValue(context) }.toTypedArray() | |
resources.getQuantityString(resId, quantity, *arguments) | |
} | |
} | |
} | |
private fun Any.asUiArg(): UiTextArg = when (this) { | |
is UiTextArg -> this | |
is Parcelable -> ParcelableArg(this) | |
is Serializable -> SerializableArg(this) | |
else -> throw IllegalArgumentException("Unsupported type: ${this::class}. Only Serializable and Parcelable types are supported.") | |
} | |
private inline fun <reified T> Parcel.readList(): List<T> { | |
val outList = mutableListOf<T>() | |
readList(outList, T::class.java.classLoader) | |
return outList | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package packagename | |
import androidx.compose.runtime.Composable | |
import androidx.compose.ui.platform.LocalContext | |
@Composable | |
fun UiText.asString(): String { | |
val context = LocalContext.current | |
return asString(context) | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import android.content.Context | |
fun Collection<UiText>.joinAsString( | |
context: Context, | |
separator: CharSequence = "\n", | |
prefix: CharSequence = "", | |
postfix: CharSequence = "", | |
limit: Int = -1, | |
truncated: CharSequence = "...", | |
): String { | |
if (isEmpty()) { | |
return "" | |
} | |
return joinToString( | |
separator = separator, | |
prefix = prefix, | |
postfix = postfix, | |
limit = limit, | |
truncated = truncated, | |
) { uiText -> | |
uiText.asString(context) | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package packagename | |
import android.os.Parcelable | |
import androidx.annotation.BoolRes | |
import androidx.annotation.IntegerRes | |
import androidx.annotation.PluralsRes | |
import androidx.annotation.StringRes | |
import java.io.Serializable | |
object JavaUiText { | |
@JvmStatic | |
fun newInstance(value: String) = UiText(value) | |
@JvmStatic | |
fun newInstance( | |
@StringRes | |
resId: Int, | |
args: UiTextArgList, | |
) = UiText(resId, args) | |
@JvmStatic | |
fun newInstance( | |
@StringRes | |
resId: Int, | |
) = UiText(resId) | |
@JvmStatic | |
fun newInstance( | |
@PluralsRes | |
resId: Int, | |
quantity: Int, | |
args: UiTextArgList, | |
) = UiText(resId, quantity, args) | |
@JvmStatic | |
fun newInstance( | |
@PluralsRes | |
resId: Int, | |
quantity: Int, | |
) = UiText(resId, quantity) | |
@JvmStatic | |
fun argBuilder() = UiTextArgBuilder() | |
} | |
class UiTextArgBuilder { | |
private val _args = mutableListOf<Any?>() | |
fun addStringRes( | |
@StringRes resId: Int, | |
args: UiTextArgList, | |
) = apply { | |
_args.add(stringResArg(resId, args)) | |
} | |
fun addStringRes( | |
@StringRes resId: Int, | |
) = apply { | |
_args.add(stringResArg(resId)) | |
} | |
fun addPluralsRes( | |
@PluralsRes resId: Int, | |
quantity: Int, | |
args: UiTextArgList, | |
) = apply { | |
_args.add(pluralsResArg(resId, quantity, args)) | |
} | |
fun addPluralsRes( | |
@PluralsRes resId: Int, | |
quantity: Int, | |
) = apply { | |
_args.add(pluralsResArg(resId, quantity)) | |
} | |
fun addIntegerRes(@IntegerRes resId: Int) = apply { | |
_args.add(integerResArg(resId)) | |
} | |
fun addBooleanRes(@BoolRes resId: Int) = apply { | |
_args.add(booleanResArg(resId)) | |
} | |
fun add(value: Parcelable) = apply { | |
_args.add(value) | |
} | |
fun add(value: Serializable) = apply { | |
_args.add(value) | |
} | |
fun add(value: Any?) = apply { | |
_args.add(value) | |
} | |
fun build(): UiTextArgList { | |
if (_args.isEmpty()) { | |
throw IllegalStateException("Can't create empty UiTextArgs") | |
} | |
val arg0 = _args.first() | |
val rest = _args.drop(1) | |
return uiTextArgsOf(arg0, *rest.toTypedArray()) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment