一键导入
kmp-data-layer
Kotlin data layer patterns: data classes in shared-core, repository pattern, StateFlow, CatalogService, and content wiring
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Kotlin data layer patterns: data classes in shared-core, repository pattern, StateFlow, CatalogService, and content wiring
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
Template repo file structure: where screens, navigation, theme, data, and platform entries live in the monorepo
Android TV build and D-pad QA using Android CLI first, with bounded Gradle and ADB fallbacks
Expo Application Services cloud build configuration for TV app APK and IPA generation
Expo TV configuration: app.json plugins, prebuild settings, platform-specific config for react-native-tvos
Fire TV leanback manifest configuration: banner icons, LEANBACK_LAUNCHER intent filter, TV-specific Android settings
Gradle build commands: assembleDebug, assembleRelease, APK output paths, and emulator installation
| name | kmp-data-layer |
| description | Kotlin data layer patterns: data classes in shared-core, repository pattern, StateFlow, CatalogService, and content wiring |
| applies_to | ["content","scaffold","screens"] |
| load_when | injecting content data, adding new models, or wiring data to screens |
Content flows: JSON catalog -> CatalogService -> ContentRepository -> UI (Compose/SwiftUI). The shared-core module owns all data logic. Platform apps only consume repositories via ServiceLocator.
JSON / API
|
CatalogService (implements CatalogSource interface)
|
ContentRepository (caches + queries)
|
ServiceLocator (provides singleton instances)
|
UI Layer (MainActivity / SwiftUI views)
// 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,
)
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(),
)
enum class ContentType { Video, Audio, Image, Mixed }
// 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>
}
ContentRepositoryImpl wraps a CatalogSource and adds:
Mutex protects the content mappriority descendingsealed 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.
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:
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")
}
}
}
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:
ktor-client-okhttpktor-client-darwinBoth are declared in shared-core/build.gradle.kts under androidMain and iosMain source sets.
// 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
}
}
To inject a user's content manifest:
List<ContentItem> using kotlinx-serialization// In Application.onCreate() or before setContent {}
val catalog = parseContentJson(jsonString)
ServiceLocator.configure(
catalogSource = object : CatalogSource {
override suspend fun fetchCatalog() = Result.Success(catalog)
}
)
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(),
)
}
}
@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" }
}
shared-core/src/commonMain/kotlin/models/@Serializable annotation if it will be parsed from JSONServiceLocatorshared-core/src/commonTest/shared-core/commonMain so both platforms use the same types.LaunchedEffect or rememberCoroutineScope().launch.Result<T>, UI handles Success/Error.listOf(ContentItem(...)) in a screen, the next content swap requires hunting through UI code.