diff --git a/README.md b/README.md index 25cf4dc..f6f623e 100644 --- a/README.md +++ b/README.md @@ -33,16 +33,18 @@ Gears could be used together or alone. - [![Version](https://img.shields.io/maven-central/v/com.redmadrobot.extensions/resources-ktx?style=flat-square&label=resources-ktx)][resources-ktx] — A set of extensions for accessing resources - [![Version](https://img.shields.io/maven-central/v/com.redmadrobot.extensions/viewbinding-ktx?style=flat-square&label=viewbinding-ktx)][viewbinding-ktx] — A set of extensions for dealing with ViewBinding -### :mag_right: **[ViewModelEvents](viewmodelevents/)** +### :mag_right: **[ViewModelEvents](viewmodelevents/)** [![Version](https://img.shields.io/maven-central/v/com.redmadrobot.gears/kotlin?style=flat-square&label=viewmodelevents) -- [![Version](https://img.shields.io/maven-central/v/com.redmadrobot.gears/kotlin?style=flat-square&label=viewmodelevents-compose)][viewmodelevents-compose] — A set of extensions for dealing with ViewModelEvents inside `@Composable` functions -- [![Version](https://img.shields.io/maven-central/v/com.redmadrobot.gears/kotlin?style=flat-square&label=viewmodelevents-flow)][viewmodelevents-flow] — An implementation of ViewModelEvents via `Flow` -- [![Version](https://img.shields.io/maven-central/v/com.redmadrobot.gears/kotlin?style=flat-square&label=viewmodelevents-livedata)][viewmodelevents-livedata] — An implementation of ViewModelEvents via `LiveData` +`ViewModelEvents` addresses the challenge of buffering and consuming one-time events: ### :hourglass_flowing_sand: **[Result Flow](resultflow/)** ![Version](https://img.shields.io/maven-central/v/com.redmadrobot.gears/resultflow?style=flat-square) A couple of extensions to convert long operations into `Flow>`. +### :speech_balloon: **[TextValue](textvalue/)** ![Version](https://img.shields.io/maven-central/v/com.redmadrobot.textvalue/textvalue?style=flat-square) + +An abstraction over Android text + ## Why Gears? The goal of this mono-repository is to simplify the creation and publication of libraries. @@ -71,9 +73,5 @@ For major changes, open a [discussion][discussions] first to discuss what you wo [gears-compose]: gears/gears-compose [gears-kotlin]: gears/gears-kotlin -[viewmodelevents-compose]: viewmodelevents/viewmodelevents-compose/ -[viewmodelevents-flow]: viewmodelevents/viewmodelevents-flow/ -[viewmodelevents-livedata]: viewmodelevents/viewmodelevents-livedata/ - [ci]: https://github.com/RedMadRobot/gears-android/actions?query=branch%3Amain++ [discussions]: https://github.com/RedMadRobot/gears-android/discussions diff --git a/settings.gradle.kts b/settings.gradle.kts index e539fbb..44382f7 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -61,4 +61,6 @@ include( ":viewmodelevents:viewmodelevents-flow", ":viewmodelevents:viewmodelevents-livedata", ":resultflow", + ":textvalue:textvalue", + ":textvalue:textvalue-compose" ) diff --git a/textvalue/CHANGELOG.md b/textvalue/CHANGELOG.md new file mode 100644 index 0000000..fcb31eb --- /dev/null +++ b/textvalue/CHANGELOG.md @@ -0,0 +1,23 @@ +## [Unreleased] + +*No changes* + +## [1.0.2] + +- Add default value to formatArgs field in Resource constructor + +## [1.0.1] + +- Add ability to use arguments with text from resources +- Update version of AGP, plugins and libraries +- Update publishing logic to Maven Central, because [OSSRH](https://central.sonatype.org/pages/ossrh-eol/) has been shut down +- ⚠️ Breaking changes: new parameter formatArgs in TextValue.Resource constructor without default value + +## [1.0.0] + +- Public release textvalue library and textvalue-compose extensions library + +[unreleased]: https://github.com/RedMadRobot/TextValue/compare/1.0.2...main +[1.0.2]: https://github.com/RedMadRobot/TextValue/compare/1.0.1...1.0.2 +[1.0.1]: https://github.com/RedMadRobot/TextValue/compare/1.0.0...1.0.1 +[1.0.0]: https://github.com/RedMadRobot/TextValue/compare/d5d1d9...1.0.0 diff --git a/textvalue/README.md b/textvalue/README.md new file mode 100644 index 0000000..bc88165 --- /dev/null +++ b/textvalue/README.md @@ -0,0 +1,78 @@ +# TextValue + +[![Version](https://img.shields.io/maven-central/v/com.redmadrobot.textvalue/textvalue?style=flat-square)][mavenCentral] +[![License](https://img.shields.io/github/license/RedMadRobot/gears-android?style=flat-square)][license] + +TextValue is an abstraction allowing to work with a `String` and a string resource ID the same way. + +--- + + + +- [Installation](#installation) +- [Usage](#usage) +- [Contributing](#contributing) + + + +## Installation + +Add the dependency: +```groovy +repositories { + mavenCentral() + google() +} + +dependencies { + // Views version + implementation("com.redmadrobot.textvalue:textvalue:") + + // Compose extensions for textvalue + implementation("com.redmadrobot.textvalue:textvalue-compose:") +} +``` + +## Usage + +**TextValue** is a wrapper to make it possible to work with plain `String` and `StringRes` in the same way. +It may be useful for cases when you want to fallback to `StringRes` if desired string value is `null`. + +You can wrap `String` and `StringRes` with `TextValue` using `TextValue(String)`, `TextValue(Int)` or `TextValue(String?, Int))`, and use method `TextValue.get(Resource)` to retrieve `String`: + +```kotlin +// in some place where we can't access Context +val errorMessage = TextValue(exception.message, defaultResourceId = R.string.unknown_error) +showMessage(errorMessage) + +// in Activity, Fragment or View +fun showMessage(text: TextValue) { + val messageText = text.get(resources) + //... +} +``` + +`TextValue` also could be used with Jetpack Compose: + +```kotlin +// in Composable functions +@Composable +fun Screen(title: TextValue) { + // Remember to add com.redmadrobot.textvalue:textvalue-compose dependency + Text(text = stringResource(title)) +} +``` + +There are extensions to work with `TextValue` like with `StringRes`: + +- `Context.getString(text: TextValue): String` +- `View.getString(text: TextValue): String` +- `Resources.getString(text: TextValue): String` + +## Contributing + +Merge requests are welcome. +For major changes, please open an issue first to discuss what you would like to change. + +[mavenCentral]: https://central.sonatype.com/artifact/com.redmadrobot.textvalue/textvalue +[license]: ../LICENSE diff --git a/textvalue/build.gradle.kts b/textvalue/build.gradle.kts new file mode 100644 index 0000000..9790631 --- /dev/null +++ b/textvalue/build.gradle.kts @@ -0,0 +1,3 @@ +// For some reason gradle.properties in this project doesn't affect its subprojects +val textValueGroup = group +subprojects { group = textValueGroup } diff --git a/textvalue/gradle.properties b/textvalue/gradle.properties new file mode 100644 index 0000000..ef08f59 --- /dev/null +++ b/textvalue/gradle.properties @@ -0,0 +1,2 @@ +group=com.redmadrobot.textvalue +version=1.0.2 diff --git a/textvalue/textvalue-compose/build.gradle.kts b/textvalue/textvalue-compose/build.gradle.kts new file mode 100644 index 0000000..a34b83a --- /dev/null +++ b/textvalue/textvalue-compose/build.gradle.kts @@ -0,0 +1,19 @@ +plugins { + convention.library.android + alias(stack.plugins.kotlin.compose) +} + +description = "Compose extensions for TextValue" + +dependencies { + api(project(":textvalue:textvalue")) + api(androidx.compose.ui) +} + +android { + namespace = "$group.compose" + + buildFeatures { + compose = true + } +} diff --git a/textvalue/textvalue-compose/src/main/kotlin/StringResources.kt b/textvalue/textvalue-compose/src/main/kotlin/StringResources.kt new file mode 100644 index 0000000..166a321 --- /dev/null +++ b/textvalue/textvalue-compose/src/main/kotlin/StringResources.kt @@ -0,0 +1,28 @@ +package com.redmadrobot.textvalue + +import android.content.res.Resources +import androidx.compose.runtime.Composable +import androidx.compose.runtime.ReadOnlyComposable +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.platform.LocalContext + +/** + * Unwraps and returns a string for the given [text]. + * @see TextValue + */ +@Composable +@ReadOnlyComposable +public fun stringResource(text: TextValue): String { + return resources().getString(text) +} + +/** + * A composable function that returns the [Resources]. It will be recomposed when [Configuration] + * gets updated. + */ +@Composable +@ReadOnlyComposable +private fun resources(): Resources { + LocalConfiguration.current + return LocalContext.current.resources +} diff --git a/textvalue/textvalue/build.gradle.kts b/textvalue/textvalue/build.gradle.kts new file mode 100644 index 0000000..fef1abd --- /dev/null +++ b/textvalue/textvalue/build.gradle.kts @@ -0,0 +1,16 @@ +plugins { + convention.library.android + id("kotlin-parcelize") +} + +description = "TextValue is an abstraction over Android text" + +android { + namespace = "$group" +} + +dependencies { + api(kotlin("stdlib")) + api(androidx.annotation) + compileOnly(androidx.compose.runtime) +} diff --git a/textvalue/textvalue/src/main/kotlin/TextValue.kt b/textvalue/textvalue/src/main/kotlin/TextValue.kt new file mode 100644 index 0000000..06b5ae5 --- /dev/null +++ b/textvalue/textvalue/src/main/kotlin/TextValue.kt @@ -0,0 +1,115 @@ +package com.redmadrobot.textvalue + +import android.content.Context +import android.content.res.Resources +import android.os.Parcelable +import android.view.View +import androidx.annotation.StringRes +import androidx.compose.runtime.Immutable +import kotlinx.parcelize.Parcelize +import kotlinx.parcelize.RawValue + +/** + * Wrapper to make it possible to work with plain [String] and [StringRes] in the same way. + * + * ``` + * // in some place where we can't access Context + * val errorMessage = TextValue(exception.message, defaultResourceId= R.string.unknown_error) + * showMessage(errorMessage) + * + * // in Activity, Fragment or View + * val messageText = getString(message) + * ``` + */ +@Immutable +public sealed interface TextValue : Parcelable { + + /** Retrieves [String] using the given [resources]. */ + public fun get(resources: Resources): String + + override fun equals(other: Any?): Boolean + override fun hashCode(): Int + + /** Plain string. */ + @Parcelize + public data class Plain(public val string: String) : TextValue { + override fun get(resources: Resources): String = string + } + + /** String resource, requires [Resources] to get [String]. */ + @Parcelize + public data class Resource( + @StringRes public val resourceId: Int, + public val formatArgs: @RawValue Array = emptyArray() + ) : TextValue { + @Suppress("SpreadOperator") + override fun get(resources: Resources): String { + return resources.getString(resourceId, *formatArgs) + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as Resource + + if (resourceId != other.resourceId) return false + if (!formatArgs.contentEquals(other.formatArgs)) return false + + return true + } + + override fun hashCode(): Int { + var result = resourceId + result = 31 * result + formatArgs.contentHashCode() + return result + } + } + + public companion object { + + /** Empty [TextValue]. */ + public val EMPTY: TextValue = TextValue("") + } +} + +/** Creates [TextValue] from the given [resourceId] and [formatArgs]. */ +public fun TextValue(@StringRes resourceId: Int, vararg formatArgs: Any): TextValue { + return TextValue.Resource(resourceId, formatArgs) +} + +/** Creates [TextValue] from the given [string]. */ +public fun TextValue(string: String): TextValue = TextValue.Plain(string) + +/** Creates [TextValue] from the given [string], or from the [defaultResourceId] if string is `null`. */ +public fun TextValue(string: String?, @StringRes defaultResourceId: Int, vararg formatArgs: Any): TextValue { + return if (string != null) { + TextValue.Plain(string) + } else { + TextValue.Resource(defaultResourceId, formatArgs) + } +} + +/** + * Unwraps and returns a string for the given [text]. + * @see TextValue + */ +public fun Context.getString(text: TextValue): String = resources.getString(text) + +/** + * Unwraps and returns a string for the given [text]. + * @see TextValue + */ +public fun View.getString(text: TextValue): String = resources.getString(text) + +/** + * Unwraps and returns a string for the given [text]. + * @see TextValue + */ +public fun Resources.getString(text: TextValue): String = text.get(this) + +/** + * Returns TextValue itself if it is not `null`, or the [TextValue.EMPTY] otherwise. + * @see TextValue + */ +public fun TextValue?.orEmpty(): TextValue = this ?: TextValue.EMPTY diff --git a/viewmodelevents/README.md b/viewmodelevents/README.md index 48a0a63..ad89d3b 100644 --- a/viewmodelevents/README.md +++ b/viewmodelevents/README.md @@ -42,7 +42,7 @@ dependencies { ## Usage One-time events (or single events) are a common pattern to display messages or errors in UI. -`ViewModelEvents` addresses the challenge of buffering and consuming one-time events: +`ViewModelEvents` addresses the challenge of buffering and consuming one-time events. - **Buffering:** When there are no subscribers to `ViewModelEvents`, emitted events are stored in a buffer. All buffered events are then delivered sequentially as soon as you subscribe to the ViewModelEvents