kmp-data-layer
Skill4 starsChanged 4 months agoMIT-0View on GitHub
Kotlin data layer patterns: data classes in shared-core, repository pattern, StateFlow, CatalogService, and content wiring
2 other projects use a version of this.
Adopted by
Most recently taken up by
- PL-Coding-GmbH/Skillstheir own version, first seen Nov 8, 2026
- Kushpawan/kmp-mobile-skillstheir own version, first seen Nov 1, 2026
Other versions2 projects use a version of this.
Word for word · 0
None found.
Edited · 0
None found.
Wrote their own version · 2
- PL-Coding-GmbH/Skills★ 18same name
- Kushpawan/kmp-mobile-skills★ 15same name
How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.
In this repository26 other files it gives its agents.
- AGENTS.mdAGENTS.md
- CLAUDE.mdCLAUDE.md
- 10ft-uiSkill
- amazon-devices-vega-app-performanceSkill
- amazon-devices-vega-best-practicesSkill
- amazon-devices-vega-setup-sdkSkill
- android-tv-testingSkill
- changelog-static-siteSkill
- creative-tv-uiSkill
- eas-buildSkill
- expo-tv-configSkill
- firetv-leanbackSkill
- kmp-build-commandsSkill
- kmp-compose-tvSkill
- kmp-navigationSkill
- kmp-template-anatomySkill
- kmp-themingSkill
- kmp-verify-patternsSkill
- metaSkill
- rn-manifest-wiringSkill
- rn-shared-ui-catalogSkill
- rn-spatial-navigationSkill
- rn-template-anatomySkill
- rn-themingSkill
- vega-sdkSkill
- video-playerSkill
The file
We split this skill into the rules it gives an agent
9 rules. About 0 are common, 4 are distinctive, and 5 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈4
Distinctive · estimate
This author's own choices. Worth a look.
5
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 184
Configure ServiceLocator: before the UI starts:
- Distinctive: line 198
If the user's JSON doesn't match ContentItem directly, write a transform:
- Very specific: line 245
Create the data class in shared-core/src/commonMain/kotlin/models/
- Very specific: line 246
Add @Serializable annotation if it will be parsed from JSON
- Distinctive: line 247
Create/update a repository interface and implementation
- Very specific: line 249
Add contract tests in shared-core/src/commonTest/
- Very specific: line 254
Calling suspend functions from composition directly: Always wrap in LaunchedEffect or rememberCoroutineScope().launch.
- Distinctive: line 256
Mutating the content list at the UI level: If you need user state (watchlist, progress), add a separate repository/store.
- Very specific: line 257
Hardcoding content in composables: All data flows through repositories.
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
KMP Data Layer
Content flows: JSON catalog -> CatalogService -> ContentRepository -> UI (Compose/SwiftUI). The shared-core module owns all data logic. Platform apps only consume repositories via ServiceLocator.
Architecture overview
JSON / API
|
CatalogService (implements CatalogSource interface)
|
ContentRepository (caches + queries)
|
ServiceLocator (provides singleton instances)
|
UI Layer (MainActivity / SwiftUI views)Core data model: ContentItem
// shared-core/src/commonMain/kotlin/models/ContentItem.kt
data class ContentItem(
val id: String,
val title: String,
val description: String? = null,
val thumbnailUrl: String? = null,
val contentType: ContentType, // Video, Audio, Image, Mixed
val metadata: ContentMetadata = ContentMetadata(),
val isOfflineAvailable: Boolean = false,
val lastAccessed: Long? = null,
val focusable: Boolean = true,
val collections: List<String> = emptyList(),
val tags: List<String> = emptyList(),
val priority: Int = 0, // Higher = more prominent in UI
val videoUrl: String? = null,
)ContentMetadata
data class ContentMetadata(
val genre: String? = null,
val rating: String? = null,
val releaseDate: String? = null,
val duration: Long? = null,
val director: String? = null,
val cast: List<String> = emptyList(),
)ContentType enum
enum class ContentType { Video, Audio, Image, Mixed }Repository pattern
Interface
// shared-core/src/commonMain/kotlin/repositories/ContentRepository.kt
interface ContentRepository {
suspend fun getContentItems(limit: Int = 50, offset: Int = 0): Result<List<ContentItem>>
suspend fun getContentItem(id: String): Result<ContentItem?>
suspend fun searchContent(query: String): Result<List<ContentItem>>
suspend fun markContentAccessed(contentId: String): Result<Unit>
}Implementation
ContentRepositoryImpl wraps a CatalogSource and adds:
- Lazy loading: catalog is fetched once, then cached in-memory
- Thread safety:
Mutexprotects the content map - Sorting: items returned sorted by
prioritydescending - Search: filters by title, description, and tags (case-insensitive)
Result type
sealed class Result<out T> {
data class Success<T>(val data: T) : Result<T>()
data class Error(val exception: Exception? = null, val message: String? = null) : Result<Nothing>()
}UI code uses onSuccess/onFailure extension methods for clean handling.
CatalogService — loading content
interface CatalogSource {
suspend fun fetchCatalog(): Result<List<ContentItem>>
}The default CatalogService implementation loads content from a JSON endpoint or bundled file. To wire custom content:
Option 1: Replace the JSON source
Implement CatalogSource to read from your content.json:
class CustomCatalogSource(private val jsonString: String) : CatalogSource {
override suspend fun fetchCatalog(): Result<List<ContentItem>> {
return try {
val items = Json.decodeFromString<List<ContentItem>>(jsonString)
Result.Success(items)
} catch (e: Exception) {
Result.Error(e, "Failed to parse catalog")
}
}
}Option 2: Remote API via Ktor
class RemoteCatalogSource(private val client: HttpClient) : CatalogSource {
override suspend fun fetchCatalog(): Result<List<ContentItem>> {
return try {
val items = client.get("https://api.example.com/catalog").body<List<ContentItem>>()
Result.Success(items)
} catch (e: Exception) {
Result.Error(e, "Network request failed")
}
}
}Ktor engines are platform-specific:
- Android:
ktor-client-okhttp - iOS:
ktor-client-darwin
Both are declared in shared-core/build.gradle.kts under androidMain and iosMain source sets.
ServiceLocator — dependency injection
// shared-core/src/commonMain/kotlin/di/ServiceLocator.kt
object ServiceLocator {
fun configure(
catalogSource: CatalogSource = CatalogService(),
authProvider: AuthProvider = AuthProvider { _, _ -> false },
) { /* stores instances */ }
fun contentRepository(): ContentRepository { /* lazy singleton */ }
fun sessionManager(): SessionManager { /* lazy singleton */ }
fun applicationManager(): TVApplicationManager { /* lazy singleton */ }
}Usage in Android TV app:
class MainActivity : ComponentActivity() {
private val contentRepository = ServiceLocator.contentRepository()
override fun onCreate(savedInstanceState: Bundle?) {
// Use contentRepository in composables via LaunchedEffect
}
}Wiring content.json into the data layer
To inject a user's content manifest:
- Parse the JSON into
List<ContentItem>using kotlinx-serialization - Create a CatalogSource that returns the parsed list
- Configure ServiceLocator before the UI starts:
// In Application.onCreate() or before setContent {}
val catalog = parseContentJson(jsonString)
ServiceLocator.configure(
catalogSource = object : CatalogSource {
override suspend fun fetchCatalog() = Result.Success(catalog)
}
)Mapping user manifest shapes
If the user's JSON doesn't match ContentItem directly, write a transform:
fun transformUserContent(raw: UserManifest): List<ContentItem> {
return raw.videos.map { video ->
ContentItem(
id = video.id,
title = video.title,
description = video.description,
thumbnailUrl = video.thumbnail_url,
contentType = ContentType.Video,
videoUrl = video.stream_url,
metadata = ContentMetadata(
genre = video.genre,
rating = video.rating,
releaseDate = video.year,
),
tags = video.tags ?: emptyList(),
)
}
}UI consumption pattern
@Composable
fun MainScreen() {
var contentItems by remember { mutableStateOf<List<ContentItem>>(emptyList()) }
var isLoading by remember { mutableStateOf(true) }
LaunchedEffect(Unit) {
contentRepository.getContentItems()
.onSuccess { items ->
contentItems = items
isLoading = false
}
.onFailure { /* show error */ }
}
// Render contentItems grouped by genre
val categories = contentItems.groupBy { it.metadata.genre ?: "Other" }
}Adding a new model
- Create the data class in
shared-core/src/commonMain/kotlin/models/ - Add
@Serializableannotation if it will be parsed from JSON - Create/update a repository interface and implementation
- Wire it in
ServiceLocator - Add contract tests in
shared-core/src/commonTest/
Anti-patterns
- Putting data classes in the platform app. Models belong in
shared-core/commonMainso both platforms use the same types. - Calling suspend functions from composition directly. Always wrap in
LaunchedEffectorrememberCoroutineScope().launch. - Skipping the Result wrapper. Bare try/catch in UI code mixes concerns. Repository returns
Result<T>, UI handlesSuccess/Error. - Mutating the content list at the UI level. If you need user state (watchlist, progress), add a separate repository/store. Don't modify the catalog items.
- Hardcoding content in composables. All data flows through repositories. If you write
listOf(ContentItem(...))in a screen, the next content swap requires hunting through UI code.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

