Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ on:
workflow_dispatch:

jobs:
# 1) JVM/unit tests (core + compose)
# 1) JVM/unit tests (core + compose + test)
unit:
runs-on: ubuntu-latest
steps:
Expand All @@ -23,7 +23,8 @@ jobs:
run: |
./gradlew clean \
:core:testDebugUnitTest \
:compose:testDebugUnitTest
:compose:testDebugUnitTest \
:test:testDebugUnitTest

# 2) Instrumented tests on API 32
androidTest:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ jobs:
fi

- name: Assemble release artifacts (no tests)
run: ./gradlew --no-daemon :core:assembleRelease :compose:assembleRelease -x test -x check
run: ./gradlew --no-daemon :core:assembleRelease :compose:assembleRelease :test:assembleRelease -x test -x check

- name: Publish to Maven Central
env:
Expand Down
84 changes: 76 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

[![Maven Central – core](https://img.shields.io/maven-central/v/io.github.dkmarkell/textresource-core?label=textresource-core)](https://central.sonatype.com/artifact/io.github.dkmarkell/textresource-core)
[![Maven Central – compose](https://img.shields.io/maven-central/v/io.github.dkmarkell/textresource-compose?label=textresource-compose)](https://central.sonatype.com/artifact/io.github.dkmarkell/textresource-compose)
[![Maven Central – test](https://img.shields.io/maven-central/v/io.github.dkmarkell/textresource-test?label=textresource-test)](https://central.sonatype.com/artifact/io.github.dkmarkell.textresource/textresource-test)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
![minSdk](https://img.shields.io/badge/minSdk-21-blue)
![Kotlin](https://img.shields.io/badge/Kotlin-1.9.25-blue)
Expand All @@ -20,6 +21,31 @@ dependencies {
}
```

### Testing (Robolectric)

```kotlin
// build.gradle.kts (module where your tests run)
dependencies {
testImplementation("io.github.dkmarkell.textresource:textresource-test:<version>")
testImplementation("org.robolectric:robolectric:<version>")
}

android {
testOptions { unitTests.isIncludeAndroidResources = true }
}
```

#### Robolectric SDK cap (e.g., targetSdk = 36)

If your app targets a newer SDK than Robolectric supports, set the test SDK so unit tests run:

**Per test class**
```kotlin
@RunWith(org.robolectric.RobolectricTestRunner::class)
@org.robolectric.annotation.Config(sdk = [35])
class MyTest { /* ... */ }
```

## Quick Start

```kotlin
Expand All @@ -39,7 +65,7 @@ Check out the [Sample app](./sample) for a complete demo.

In a clean architecture, your ViewModel (or presenter) should decide what text is displayed, but not actually need to hold a Context to do it. With Android’s resource system, resolving strings usually requires a Context — which is either unavailable or awkward to inject.

TextResource solves this by:
**TextResource** solves this by:

- Deferring resolution — store the definition of a string (e.g., resource ID + arguments) until it’s actually displayed.
- Keeping formatting and pluralization logic out of presentation components — no need for Composables or Views to assemble text from parts.
Expand Down Expand Up @@ -203,13 +229,37 @@ private fun HomeScreen(
}
```

## API Overview
## Testing with TextResourceTest

**Factories**
- `TextResource.raw(value: String)`
- `TextResource.simple(@StringRes resId: Int, vararg args: Any)`
- `TextResource.plural(@PluralsRes resId: Int, quantity: Int, vararg args: Any)`
`textresource-test` provides a tiny helper that resolves `TextResource` in **local unit tests** (Robolectric), so your specs don’t need to wire a `Context` each time.

### Examples
```kotlin
// Basic
val tr = TextResource.simple(R.string.greeting, "Derek")
assertEquals("Hello, Derek", TextResourceTest.resolve(tr))

// Force locale
val tr = TextResource.simple(R.string.greeting, "Derek")
assertEquals("Bonjour, Derek", TextResourceTest.resolve(tr, Locale.FRANCE))

// Plurals
val apples = TextResource.plural(R.plurals.apples_count, 2, 2)
assertEquals("2 apples", TextResourceTest.resolve(apples))
```

## API Overview

### Constructing
**Core**
- **Factories (value-based)**
- `TextResource.raw(value: String)`
- `TextResource.simple(@StringRes resId: Int, vararg args: Any)`
- `TextResource.plural(@PluralsRes resId: Int, quantity: Int, vararg args: Any)`
- **SAM initializer (functional interface)**
- `TextResource { context -> /* resolve to a String using context */ }`

### Resolving
**Core**
```kotlin
fun TextResource.resolveString(context: Context): String
Expand All @@ -219,6 +269,11 @@ fun TextResource.resolveString(context: Context): String
```kotlin
@Composable
fun TextResource.resolveString(): String
```

### Helpers
**Compose**
```kotlin
@Composable
fun rememberTextResource(factory: () -> TextResource): TextResource
@Composable
Expand All @@ -227,6 +282,19 @@ fun rememberTextResource(key1: Any?, factory: () -> TextResource): TextResource
fun rememberTextResource(vararg keys: Any?, factory: () -> TextResource): TextResource
```

### Testing
**Test**
```kotlin
object TextResourceTest {
@JvmStatic
fun resolve(tr: TextResource, locale: Locale = Locale.US): String
}
```

### Equality semantics (important)
- Factory-created instances compare by **value** (same inputs → `==` is `true`)
- SAM-created instances compare by **reference** (each lambda is a new object)

## FAQ

**Q: Should I use the factory functions or the functional interface (SAM) initializer?**
Expand All @@ -243,11 +311,11 @@ The SAM initializer (`TextResource { ... }`) creates an anonymous object. Each c
```kotlin
val a = TextResource.simple(R.string.greeting, "Derek")
val b = TextResource.simple(R.string.greeting, "Derek")
println(a == b) // true value-based
println(a == b) // true -> value-based

val x = TextResource { "Hello, Derek" }
val y = TextResource { "Hello, Derek" }
println(x == y) // false reference-based
println(x == y) // false -> reference-based
```

## License
Expand Down
9 changes: 3 additions & 6 deletions compose/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,10 @@ android {
namespace = "io.github.dkmarkell.textresource.compose"
compileSdk = 36

defaultConfig { testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" }

defaultConfig {
minSdk = 21
consumerProguardFiles("proguard-rules.pro") // create at compose/proguard-rules.pro
consumerProguardFiles("proguard-rules.pro")
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}

publishing {
Expand All @@ -24,8 +23,6 @@ android {
}
}

buildFeatures { compose = true }

buildFeatures {
compose = true
}
Expand Down Expand Up @@ -114,8 +111,8 @@ dependencies {
implementation(platform(libs.compose.bom))
implementation(libs.compose.runtime)
implementation(libs.compose.ui)
implementation(libs.androidx.runner)

androidTestImplementation(libs.androidx.runner)
androidTestImplementation(libs.androidx.test.ext.junit)
androidTestImplementation(libs.androidx.test.core.ktx)
androidTestImplementation(platform(libs.compose.bom))
Expand Down
30 changes: 0 additions & 30 deletions compose/src/main/res/drawable-v24/ic_launcher_foreground.xml

This file was deleted.

Loading
Loading