android-sdd 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +143 -0
- package/package.json +27 -0
- package/skills/Android Ecosystem/Baseline Profile Generator/SKILL.md +277 -0
- package/skills/Android Ecosystem/Glance/SKILL.md +315 -0
- package/skills/Android Platform/Configuration/SKILL.md +201 -0
- package/skills/Android Platform/Filesystem/SKILL.md +216 -0
- package/skills/Android Platform/Lifecycle/SKILL.md +233 -0
- package/skills/Android Platform/Manifest/SKILL.md +226 -0
- package/skills/Android Platform/Process Death Recovery/SKILL.md +214 -0
- package/skills/Android Platform/Resources/SKILL.md +234 -0
- package/skills/Android Platform/SavedStateHandle/SKILL.md +217 -0
- package/skills/Android Platform/State Restoration/SKILL.md +210 -0
- package/skills/Architecture/Bounded Context/SKILL.md +207 -0
- package/skills/Architecture/Clean Architecture/SKILL.md +229 -0
- package/skills/Architecture/Domain Modeling/SKILL.md +236 -0
- package/skills/Architecture/Entity Design/SKILL.md +243 -0
- package/skills/Architecture/Feature Isolation/SKILL.md +216 -0
- package/skills/Architecture/MVI/SKILL.md +224 -0
- package/skills/Architecture/MVVM/SKILL.md +198 -0
- package/skills/Architecture/Modularization/SKILL.md +194 -0
- package/skills/Architecture/Offline First/SKILL.md +249 -0
- package/skills/Architecture/Repository Pattern/SKILL.md +216 -0
- package/skills/Architecture/Side Effect Management/SKILL.md +278 -0
- package/skills/Architecture/State Management/SKILL.md +229 -0
- package/skills/Architecture/Unidirectional Data Flow/SKILL.md +196 -0
- package/skills/Architecture/Use Case Design/SKILL.md +244 -0
- package/skills/Architecture/Value Object/SKILL.md +226 -0
- package/skills/Build Infrastructure/Build Orchestration/SKILL.md +257 -0
- package/skills/Build Infrastructure/Dependency Compatibility Resolver/SKILL.md +259 -0
- package/skills/Build Infrastructure/Environment Validator/SKILL.md +311 -0
- package/skills/Build System/Build Cache/SKILL.md +233 -0
- package/skills/Build System/Build Flavor Strategy/SKILL.md +171 -0
- package/skills/Build System/Build Variant/SKILL.md +215 -0
- package/skills/Build System/Convention Plugin/SKILL.md +288 -0
- package/skills/Build System/Dependency Management/SKILL.md +261 -0
- package/skills/Build System/Gradle/SKILL.md +284 -0
- package/skills/Build System/Incremental Build/SKILL.md +199 -0
- package/skills/Build System/KAPT/SKILL.md +198 -0
- package/skills/Build System/KSP/SKILL.md +263 -0
- package/skills/Build System/Module Dependency Graph Validation/SKILL.md +223 -0
- package/skills/Build System/Specialized/C++/SKILL.md +308 -0
- package/skills/Build System/Specialized/JNI/SKILL.md +306 -0
- package/skills/Build System/Specialized/NDK/SKILL.md +264 -0
- package/skills/Build System/Version Catalog/SKILL.md +304 -0
- package/skills/Concurrency/Background Processing/SKILL.md +185 -0
- package/skills/Concurrency/Channel/SKILL.md +207 -0
- package/skills/Concurrency/Coroutine/SKILL.md +200 -0
- package/skills/Concurrency/Flow/SKILL.md +179 -0
- package/skills/Concurrency/Mutex Strategy/SKILL.md +185 -0
- package/skills/Concurrency/SharedFlow/SKILL.md +171 -0
- package/skills/Concurrency/StateFlow/SKILL.md +175 -0
- package/skills/Concurrency/Structured Concurrency/SKILL.md +197 -0
- package/skills/Concurrency/Synchronization Policy/SKILL.md +192 -0
- package/skills/Core Language/Annotation Processing/SKILL.md +224 -0
- package/skills/Core Language/DSL/SKILL.md +186 -0
- package/skills/Core Language/Extension Functions Design/SKILL.md +191 -0
- package/skills/Core Language/Immutability/SKILL.md +156 -0
- package/skills/Core Language/KMP/SKILL.md +182 -0
- package/skills/Core Language/Kotlin/SKILL.md +187 -0
- package/skills/Core Language/Reactive State Management/SKILL.md +228 -0
- package/skills/Core Language/Reactive Streams/SKILL.md +235 -0
- package/skills/Core Language/Serialization/SKILL.md +191 -0
- package/skills/Data Layer/Cache Strategy/SKILL.md +261 -0
- package/skills/Data Layer/Conflict Resolution/SKILL.md +248 -0
- package/skills/Data Layer/DAO/SKILL.md +225 -0
- package/skills/Data Layer/DTO Mapping/SKILL.md +269 -0
- package/skills/Data Layer/DataStore/SKILL.md +264 -0
- package/skills/Data Layer/Database Versioning Strategy/SKILL.md +215 -0
- package/skills/Data Layer/Encrypted Database/SKILL.md +212 -0
- package/skills/Data Layer/File Storage/SKILL.md +247 -0
- package/skills/Data Layer/Indexing/SKILL.md +184 -0
- package/skills/Data Layer/Key-Value Store Strategy/SKILL.md +185 -0
- package/skills/Data Layer/Merge Strategy/SKILL.md +240 -0
- package/skills/Data Layer/Migration/SKILL.md +243 -0
- package/skills/Data Layer/Paging/SKILL.md +264 -0
- package/skills/Data Layer/Proto DataStore/SKILL.md +250 -0
- package/skills/Data Layer/Room/SKILL.md +244 -0
- package/skills/Data Layer/SQLite/SKILL.md +255 -0
- package/skills/Data Layer/Sync Engine/SKILL.md +268 -0
- package/skills/Dependency Injection/Dagger/SKILL.md +283 -0
- package/skills/Dependency Injection/Hilt/SKILL.md +345 -0
- package/skills/Dependency Injection/Koin/SKILL.md +282 -0
- package/skills/Developer Experience/Detekt/SKILL.md +272 -0
- package/skills/Developer Experience/Lint Rule/SKILL.md +281 -0
- package/skills/Google Ecosystem/Analytics/SKILL.md +281 -0
- package/skills/Google Ecosystem/Crashlytics/SKILL.md +234 -0
- package/skills/Google Ecosystem/Firebase/SKILL.md +200 -0
- package/skills/Google Ecosystem/Firebase Messaging/SKILL.md +266 -0
- package/skills/Media/Audio/SKILL.md +257 -0
- package/skills/Media/Camera/SKILL.md +229 -0
- package/skills/Media/CameraX/SKILL.md +295 -0
- package/skills/Media/ExoPlayer/SKILL.md +258 -0
- package/skills/Media/Video/SKILL.md +228 -0
- package/skills/Meta Skills/Domain Error Model/SKILL.md +238 -0
- package/skills/Meta Skills/Error Handling/SKILL.md +255 -0
- package/skills/Meta Skills/Error Mapping/SKILL.md +232 -0
- package/skills/Meta Skills/Failure Strategy/SKILL.md +294 -0
- package/skills/Meta Skills/Migration Strategy/SKILL.md +305 -0
- package/skills/Meta Skills/User Friendly Errors/SKILL.md +334 -0
- package/skills/Navigation/Deep Navigation/SKILL.md +209 -0
- package/skills/Navigation/Navigation/SKILL.md +215 -0
- package/skills/Navigation/Nested Navigation/SKILL.md +214 -0
- package/skills/Networking/API Contract/SKILL.md +220 -0
- package/skills/Networking/Authentication/SKILL.md +210 -0
- package/skills/Networking/Certificate Pinning/SKILL.md +167 -0
- package/skills/Networking/Fallback Strategy/SKILL.md +182 -0
- package/skills/Networking/Ktor/SKILL.md +219 -0
- package/skills/Networking/Multipart Upload/SKILL.md +213 -0
- package/skills/Networking/OkHttp/SKILL.md +193 -0
- package/skills/Networking/REST/SKILL.md +178 -0
- package/skills/Networking/Rate Limiting/SKILL.md +170 -0
- package/skills/Networking/Retrofit/SKILL.md +241 -0
- package/skills/Networking/Retry-Backoff/SKILL.md +181 -0
- package/skills/Networking/Server-Sent Events (SSE)/SKILL.md +196 -0
- package/skills/Networking/WebSocket/SKILL.md +224 -0
- package/skills/Observability/Crash Reporting/SKILL.md +219 -0
- package/skills/Observability/Logging/SKILL.md +168 -0
- package/skills/Observability/Metrics/SKILL.md +227 -0
- package/skills/Observability/Structured Logging/SKILL.md +234 -0
- package/skills/Performance/ANR Prevention/SKILL.md +192 -0
- package/skills/Performance/Allocation Optimization/SKILL.md +179 -0
- package/skills/Performance/App Startup/SKILL.md +183 -0
- package/skills/Performance/Baseline Profile/SKILL.md +205 -0
- package/skills/Performance/Battery Optimization/SKILL.md +192 -0
- package/skills/Performance/Benchmark/SKILL.md +182 -0
- package/skills/Performance/Bitmap Optimization/SKILL.md +178 -0
- package/skills/Performance/Compose Optimization/SKILL.md +187 -0
- package/skills/Performance/Heap Management/SKILL.md +184 -0
- package/skills/Performance/Macrobenchmark/SKILL.md +214 -0
- package/skills/Performance/Memory Leak Prevention/SKILL.md +218 -0
- package/skills/Performance/Rendering Performance/SKILL.md +205 -0
- package/skills/Performance/Startup Optimization/SKILL.md +219 -0
- package/skills/Security/Biometric/SKILL.md +224 -0
- package/skills/Security/Certificate Transparency/SKILL.md +158 -0
- package/skills/Security/Cryptography/SKILL.md +244 -0
- package/skills/Security/Encrypted Storage/SKILL.md +273 -0
- package/skills/Security/Frida Detection/SKILL.md +230 -0
- package/skills/Security/Hook Detection/SKILL.md +197 -0
- package/skills/Security/Keystore/SKILL.md +272 -0
- package/skills/Security/Network Security Config/SKILL.md +186 -0
- package/skills/Security/Obfuscation/SKILL.md +226 -0
- package/skills/Security/Proguard/SKILL.md +202 -0
- package/skills/Security/R8/SKILL.md +234 -0
- package/skills/Security/Reverse Engineering Resistance/SKILL.md +267 -0
- package/skills/Security/Root Detection/SKILL.md +220 -0
- package/skills/Security/Secure Networking/SKILL.md +220 -0
- package/skills/System Integration/AlarmManager/SKILL.md +182 -0
- package/skills/System Integration/App Widget/SKILL.md +182 -0
- package/skills/System Integration/Deep Link/SKILL.md +187 -0
- package/skills/System Integration/Foreground Service/SKILL.md +212 -0
- package/skills/System Integration/Notification/SKILL.md +237 -0
- package/skills/System Integration/WorkManager/SKILL.md +256 -0
- package/skills/System Integration/clipboard/SKILL.md +155 -0
- package/skills/System Integration/share-intent/SKILL.md +182 -0
- package/skills/Testing/Compose Testing/SKILL.md +296 -0
- package/skills/Testing/Espresso/SKILL.md +292 -0
- package/skills/Testing/Fake Data/SKILL.md +245 -0
- package/skills/Testing/Integration Testing/SKILL.md +288 -0
- package/skills/Testing/Mocking/SKILL.md +229 -0
- package/skills/Testing/Snapshot Testing/SKILL.md +259 -0
- package/skills/Testing/UI Testing/SKILL.md +293 -0
- package/skills/Testing/Unit Testing/SKILL.md +309 -0
- package/skills/UI System/Bottom Sheet Patterns/SKILL.md +279 -0
- package/skills/UI System/Compose/SKILL.md +296 -0
- package/skills/UI System/Compose Animation/SKILL.md +281 -0
- package/skills/UI System/Compose Multiplatform/SKILL.md +261 -0
- package/skills/UI System/Compose Navigation/SKILL.md +255 -0
- package/skills/UI System/Compose Performance/SKILL.md +274 -0
- package/skills/UI System/Design System/SKILL.md +217 -0
- package/skills/UI System/Empty State Strategy/SKILL.md +208 -0
- package/skills/UI System/Keyboard Navigation/SKILL.md +214 -0
- package/skills/UI System/Loading Strategy/SKILL.md +254 -0
- package/skills/UI System/Material 3/SKILL.md +279 -0
- package/skills/UI System/RTL/SKILL.md +179 -0
- package/src/index.ts +182 -0
- package/tsconfig.json +19 -0
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: paging
|
|
3
|
+
description: >
|
|
4
|
+
Jetpack Paging 3 setup and patterns for Android.
|
|
5
|
+
Load this skill when loading large datasets incrementally, implementing
|
|
6
|
+
infinite scroll, paginating network or database results, or combining
|
|
7
|
+
paged network data with a local Room cache.
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Paging
|
|
11
|
+
|
|
12
|
+
## Overview
|
|
13
|
+
Jetpack Paging 3 enables loading large datasets in pages — from network, database, or both. It integrates with Room, Retrofit, and Compose. The library handles load state, error retry, and list diffing automatically.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Core Principles
|
|
18
|
+
|
|
19
|
+
- Use Paging when a list **could grow unbounded** — don't load everything at once
|
|
20
|
+
- **PagingSource** defines how to load a single page
|
|
21
|
+
- **RemoteMediator** coordinates network + Room cache for offline-first paging
|
|
22
|
+
- **PagingData** is consumed only once — use `cachedIn(viewModelScope)` to survive rotation
|
|
23
|
+
- Never collect `PagingData` directly in ViewModel — use `collectAsLazyPagingItems` in Compose
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Setup
|
|
28
|
+
|
|
29
|
+
```toml
|
|
30
|
+
[versions]
|
|
31
|
+
paging = "3.3.2"
|
|
32
|
+
|
|
33
|
+
[libraries]
|
|
34
|
+
paging-runtime = { module = "androidx.paging:paging-runtime", version.ref = "paging" }
|
|
35
|
+
paging-compose = { module = "androidx.paging:paging-compose", version.ref = "paging" }
|
|
36
|
+
paging-room = { module = "androidx.room:room-paging", version.ref = "room" }
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## PagingSource — Network Only
|
|
42
|
+
|
|
43
|
+
```kotlin
|
|
44
|
+
// ✅ Network-only PagingSource
|
|
45
|
+
class UserPagingSource(
|
|
46
|
+
private val api: UserApi,
|
|
47
|
+
private val query: String
|
|
48
|
+
) : PagingSource<Int, UserDto>() {
|
|
49
|
+
|
|
50
|
+
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, UserDto> {
|
|
51
|
+
val page = params.key ?: 1
|
|
52
|
+
|
|
53
|
+
return try {
|
|
54
|
+
val response = api.getUsers(
|
|
55
|
+
query = query,
|
|
56
|
+
page = page,
|
|
57
|
+
pageSize = params.loadSize
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
LoadResult.Page(
|
|
61
|
+
data = response.users,
|
|
62
|
+
prevKey = if (page == 1) null else page - 1,
|
|
63
|
+
nextKey = if (response.users.isEmpty()) null else page + 1
|
|
64
|
+
)
|
|
65
|
+
} catch (e: IOException) {
|
|
66
|
+
LoadResult.Error(e)
|
|
67
|
+
} catch (e: HttpException) {
|
|
68
|
+
LoadResult.Error(e)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
override fun getRefreshKey(state: PagingState<Int, UserDto>): Int? {
|
|
73
|
+
return state.anchorPosition?.let { anchor ->
|
|
74
|
+
state.closestPageToPosition(anchor)?.prevKey?.plus(1)
|
|
75
|
+
?: state.closestPageToPosition(anchor)?.nextKey?.minus(1)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## Repository — Network-Only Paging
|
|
84
|
+
|
|
85
|
+
```kotlin
|
|
86
|
+
class UserRepository @Inject constructor(
|
|
87
|
+
private val api: UserApi,
|
|
88
|
+
private val mapper: UserMapper
|
|
89
|
+
) {
|
|
90
|
+
fun searchUsers(query: String): Flow<PagingData<User>> = Pager(
|
|
91
|
+
config = PagingConfig(
|
|
92
|
+
pageSize = 20,
|
|
93
|
+
prefetchDistance = 5,
|
|
94
|
+
enablePlaceholders = false
|
|
95
|
+
),
|
|
96
|
+
pagingSourceFactory = { UserPagingSource(api, query) }
|
|
97
|
+
).flow.map { pagingData ->
|
|
98
|
+
pagingData.map { mapper.toDomain(it) }
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## Room + Paging (Database-Only)
|
|
106
|
+
|
|
107
|
+
```kotlin
|
|
108
|
+
// ✅ Room generates PagingSource automatically
|
|
109
|
+
@Dao
|
|
110
|
+
interface UserDao {
|
|
111
|
+
@Query("SELECT * FROM users ORDER BY name ASC")
|
|
112
|
+
fun pagingSource(): PagingSource<Int, UserEntity>
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Repository
|
|
116
|
+
class UserRepository @Inject constructor(private val userDao: UserDao) {
|
|
117
|
+
fun observeUsersPaged(): Flow<PagingData<User>> = Pager(
|
|
118
|
+
config = PagingConfig(pageSize = 20),
|
|
119
|
+
pagingSourceFactory = { userDao.pagingSource() }
|
|
120
|
+
).flow.map { it.map(UserEntity::toDomain) }
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
## RemoteMediator — Network + Room Cache
|
|
127
|
+
|
|
128
|
+
```kotlin
|
|
129
|
+
// ✅ RemoteMediator: network fills Room, Room provides paged data
|
|
130
|
+
@OptIn(ExperimentalPagingApi::class)
|
|
131
|
+
class UserRemoteMediator(
|
|
132
|
+
private val api: UserApi,
|
|
133
|
+
private val db: AppDatabase
|
|
134
|
+
) : RemoteMediator<Int, UserEntity>() {
|
|
135
|
+
|
|
136
|
+
override suspend fun load(
|
|
137
|
+
loadType: LoadType,
|
|
138
|
+
state: PagingState<Int, UserEntity>
|
|
139
|
+
): MediatorResult {
|
|
140
|
+
|
|
141
|
+
val page = when (loadType) {
|
|
142
|
+
LoadType.REFRESH -> 1
|
|
143
|
+
LoadType.PREPEND -> return MediatorResult.Success(endOfPaginationReached = true)
|
|
144
|
+
LoadType.APPEND -> {
|
|
145
|
+
val lastItem = state.lastItemOrNull()
|
|
146
|
+
?: return MediatorResult.Success(endOfPaginationReached = false)
|
|
147
|
+
getNextPageForItem(lastItem)
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return try {
|
|
152
|
+
val response = api.getUsers(page = page, pageSize = state.config.pageSize)
|
|
153
|
+
|
|
154
|
+
db.withTransaction {
|
|
155
|
+
if (loadType == LoadType.REFRESH) {
|
|
156
|
+
db.userDao().deleteAll()
|
|
157
|
+
}
|
|
158
|
+
db.userDao().upsertAll(response.users.map { it.toEntity() })
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
MediatorResult.Success(endOfPaginationReached = response.users.isEmpty())
|
|
162
|
+
} catch (e: IOException) {
|
|
163
|
+
MediatorResult.Error(e)
|
|
164
|
+
} catch (e: HttpException) {
|
|
165
|
+
MediatorResult.Error(e)
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
private suspend fun getNextPageForItem(item: UserEntity): Int {
|
|
170
|
+
// derive page number from item position
|
|
171
|
+
return (db.userDao().getItemIndex(item.id) / 20) + 2
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// ✅ Pager with RemoteMediator
|
|
176
|
+
@OptIn(ExperimentalPagingApi::class)
|
|
177
|
+
fun usersPaged(): Flow<PagingData<User>> = Pager(
|
|
178
|
+
config = PagingConfig(pageSize = 20),
|
|
179
|
+
remoteMediator = UserRemoteMediator(api, db),
|
|
180
|
+
pagingSourceFactory = { db.userDao().pagingSource() }
|
|
181
|
+
).flow.map { it.map(UserEntity::toDomain) }
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
|
|
186
|
+
## ViewModel
|
|
187
|
+
|
|
188
|
+
```kotlin
|
|
189
|
+
@HiltViewModel
|
|
190
|
+
class UserListViewModel @Inject constructor(
|
|
191
|
+
private val repository: UserRepository
|
|
192
|
+
) : ViewModel() {
|
|
193
|
+
|
|
194
|
+
private val _query = MutableStateFlow("")
|
|
195
|
+
|
|
196
|
+
// ✅ cachedIn — survives rotation without re-fetching
|
|
197
|
+
val users: Flow<PagingData<User>> = _query
|
|
198
|
+
.debounce(300)
|
|
199
|
+
.flatMapLatest { query -> repository.searchUsers(query) }
|
|
200
|
+
.cachedIn(viewModelScope)
|
|
201
|
+
|
|
202
|
+
fun onQueryChanged(query: String) { _query.value = query }
|
|
203
|
+
}
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
---
|
|
207
|
+
|
|
208
|
+
## Compose UI
|
|
209
|
+
|
|
210
|
+
```kotlin
|
|
211
|
+
@Composable
|
|
212
|
+
fun UserListScreen(viewModel: UserListViewModel = hiltViewModel()) {
|
|
213
|
+
val users = viewModel.users.collectAsLazyPagingItems()
|
|
214
|
+
|
|
215
|
+
LazyColumn {
|
|
216
|
+
items(
|
|
217
|
+
count = users.itemCount,
|
|
218
|
+
key = users.itemKey { it.id }
|
|
219
|
+
) { index ->
|
|
220
|
+
val user = users[index]
|
|
221
|
+
if (user != null) UserCard(user = user)
|
|
222
|
+
else UserCardPlaceholder()
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// ✅ Load state handling
|
|
226
|
+
when (val state = users.loadState.append) {
|
|
227
|
+
is LoadState.Loading -> item { LoadingIndicator() }
|
|
228
|
+
is LoadState.Error -> item {
|
|
229
|
+
ErrorItem(
|
|
230
|
+
message = state.error.message ?: "Error",
|
|
231
|
+
onRetry = { users.retry() }
|
|
232
|
+
)
|
|
233
|
+
}
|
|
234
|
+
else -> Unit
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// ✅ Refresh state
|
|
239
|
+
when (users.loadState.refresh) {
|
|
240
|
+
is LoadState.Loading -> FullScreenLoading()
|
|
241
|
+
is LoadState.Error -> FullScreenError(onRetry = { users.refresh() })
|
|
242
|
+
else -> Unit
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
---
|
|
248
|
+
|
|
249
|
+
## Anti-Patterns
|
|
250
|
+
|
|
251
|
+
- Loading entire dataset without Paging when the list can grow unbounded
|
|
252
|
+
- Not using `cachedIn(viewModelScope)` — re-fetches from page 1 on rotation
|
|
253
|
+
- Collecting `PagingData` in ViewModel with `collect {}` — it's a one-shot stream
|
|
254
|
+
- Missing load state handling — no loading indicator or error state shown
|
|
255
|
+
- Using `enablePlaceholders = true` without proper placeholder UI
|
|
256
|
+
|
|
257
|
+
---
|
|
258
|
+
|
|
259
|
+
## Related Skills
|
|
260
|
+
- `room` — Room PagingSource generation
|
|
261
|
+
- `retrofit` — network data source for PagingSource
|
|
262
|
+
- `repository-pattern` — Pager lives in repository
|
|
263
|
+
- `compose` — LazyColumn with LazyPagingItems
|
|
264
|
+
- `cache-strategy` — offline-first paging with RemoteMediator
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: proto-datastore
|
|
3
|
+
description: >
|
|
4
|
+
Proto DataStore setup and usage for typed object persistence in Android.
|
|
5
|
+
Load this skill when storing complex structured data with type safety,
|
|
6
|
+
using Protobuf schemas, or when Preferences DataStore is insufficient.
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Proto DataStore
|
|
10
|
+
|
|
11
|
+
## Overview
|
|
12
|
+
Proto DataStore stores typed objects defined by Protobuf schemas. Unlike Preferences DataStore (which uses string keys), Proto DataStore is strongly typed, schema-enforced, and forward-compatible by design. It is ideal for storing structured settings or state that evolves over time.
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Core Principles
|
|
17
|
+
|
|
18
|
+
- Use Proto DataStore when **type safety and schema evolution** matter
|
|
19
|
+
- Protobuf schemas define the contract — never change field numbers once released
|
|
20
|
+
- Like Preferences DataStore — **one instance per file**, inject via Hilt
|
|
21
|
+
- Always handle **IOException** in Flow collection
|
|
22
|
+
- Add new fields to the proto — never remove or renumber existing fields
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Setup
|
|
27
|
+
|
|
28
|
+
```toml
|
|
29
|
+
# libs.versions.toml
|
|
30
|
+
[versions]
|
|
31
|
+
datastore = "1.1.1"
|
|
32
|
+
protobuf = "4.26.1"
|
|
33
|
+
protobuf-plugin = "0.9.4"
|
|
34
|
+
|
|
35
|
+
[libraries]
|
|
36
|
+
datastore-proto = { module = "androidx.datastore:datastore", version.ref = "datastore" }
|
|
37
|
+
protobuf-javalite = { module = "com.google.protobuf:protobuf-javalite", version.ref = "protobuf" }
|
|
38
|
+
protobuf-kotlin-lite = { module = "com.google.protobuf:protobuf-kotlin-lite", version.ref = "protobuf" }
|
|
39
|
+
|
|
40
|
+
[plugins]
|
|
41
|
+
protobuf = { id = "com.google.protobuf", version.ref = "protobuf-plugin" }
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
```kotlin
|
|
45
|
+
// build.gradle.kts
|
|
46
|
+
plugins {
|
|
47
|
+
alias(libs.plugins.protobuf)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
dependencies {
|
|
51
|
+
implementation(libs.datastore.proto)
|
|
52
|
+
implementation(libs.protobuf.javalite)
|
|
53
|
+
implementation(libs.protobuf.kotlin.lite)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
protobuf {
|
|
57
|
+
protoc {
|
|
58
|
+
artifact = "com.google.protobuf:protoc:${libs.versions.protobuf.get()}"
|
|
59
|
+
}
|
|
60
|
+
generateProtoTasks {
|
|
61
|
+
all().forEach { task ->
|
|
62
|
+
task.builtins {
|
|
63
|
+
create("java") { option("lite") }
|
|
64
|
+
create("kotlin") { option("lite") }
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## Proto Schema Definition
|
|
74
|
+
|
|
75
|
+
```protobuf
|
|
76
|
+
// src/main/proto/user_preferences.proto
|
|
77
|
+
syntax = "proto3";
|
|
78
|
+
option java_package = "com.example.app.datastore";
|
|
79
|
+
option java_multiple_files = true;
|
|
80
|
+
|
|
81
|
+
message UserPreferences {
|
|
82
|
+
bool is_dark_mode = 1;
|
|
83
|
+
string language = 2;
|
|
84
|
+
int32 font_size = 3;
|
|
85
|
+
SortOrder sort_order = 4;
|
|
86
|
+
int64 last_sync_timestamp = 5;
|
|
87
|
+
// ✅ New fields added here — never remove or renumber existing
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
enum SortOrder {
|
|
91
|
+
SORT_ORDER_UNSPECIFIED = 0;
|
|
92
|
+
SORT_ORDER_ASC = 1;
|
|
93
|
+
SORT_ORDER_DESC = 2;
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## Serializer
|
|
100
|
+
|
|
101
|
+
```kotlin
|
|
102
|
+
// ✅ Implement Serializer for the proto type
|
|
103
|
+
object UserPreferencesSerializer : Serializer<UserPreferences> {
|
|
104
|
+
|
|
105
|
+
override val defaultValue: UserPreferences = UserPreferences.getDefaultInstance()
|
|
106
|
+
|
|
107
|
+
override suspend fun readFrom(input: InputStream): UserPreferences {
|
|
108
|
+
try {
|
|
109
|
+
return UserPreferences.parseFrom(input)
|
|
110
|
+
} catch (exception: InvalidProtocolBufferException) {
|
|
111
|
+
throw CorruptionException("Cannot read proto.", exception)
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
override suspend fun writeTo(t: UserPreferences, output: OutputStream) {
|
|
116
|
+
t.writeTo(output)
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
## Creating DataStore
|
|
124
|
+
|
|
125
|
+
```kotlin
|
|
126
|
+
// ✅ Create via extension property
|
|
127
|
+
val Context.userPreferencesDataStore: DataStore<UserPreferences> by dataStore(
|
|
128
|
+
fileName = "user_preferences.pb",
|
|
129
|
+
serializer = UserPreferencesSerializer
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
// ✅ Provide via Hilt
|
|
133
|
+
@Module
|
|
134
|
+
@InstallIn(SingletonComponent::class)
|
|
135
|
+
object DataStoreModule {
|
|
136
|
+
|
|
137
|
+
@Provides
|
|
138
|
+
@Singleton
|
|
139
|
+
fun provideUserPreferencesDataStore(
|
|
140
|
+
@ApplicationContext context: Context
|
|
141
|
+
): DataStore<UserPreferences> = context.userPreferencesDataStore
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
## Repository
|
|
148
|
+
|
|
149
|
+
```kotlin
|
|
150
|
+
class UserPreferencesRepository @Inject constructor(
|
|
151
|
+
private val dataStore: DataStore<UserPreferences>
|
|
152
|
+
) {
|
|
153
|
+
|
|
154
|
+
// ✅ Read — expose as Flow
|
|
155
|
+
val userPreferences: Flow<UserPreferences> = dataStore.data
|
|
156
|
+
.catch { e ->
|
|
157
|
+
if (e is IOException) emit(UserPreferences.getDefaultInstance())
|
|
158
|
+
else throw e
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
val isDarkMode: Flow<Boolean> = userPreferences.map { it.isDarkMode }
|
|
162
|
+
val language: Flow<String> = userPreferences.map { it.language }
|
|
163
|
+
|
|
164
|
+
// ✅ Write — use updateData or update (proto-specific)
|
|
165
|
+
suspend fun setDarkMode(enabled: Boolean) {
|
|
166
|
+
dataStore.updateData { current ->
|
|
167
|
+
current.toBuilder()
|
|
168
|
+
.setIsDarkMode(enabled)
|
|
169
|
+
.build()
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
suspend fun setLanguage(language: String) {
|
|
174
|
+
dataStore.updateData { current ->
|
|
175
|
+
current.toBuilder()
|
|
176
|
+
.setLanguage(language)
|
|
177
|
+
.build()
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ✅ Kotlin DSL style (with protobuf-kotlin-lite)
|
|
182
|
+
suspend fun updatePreferences(update: UserPreferences.Builder.() -> Unit) {
|
|
183
|
+
dataStore.updateData { current ->
|
|
184
|
+
current.copy(update)
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Usage:
|
|
189
|
+
// preferencesRepository.updatePreferences {
|
|
190
|
+
// isDarkMode = true
|
|
191
|
+
// language = "fa"
|
|
192
|
+
// }
|
|
193
|
+
}
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
---
|
|
197
|
+
|
|
198
|
+
## Schema Evolution Rules
|
|
199
|
+
|
|
200
|
+
```protobuf
|
|
201
|
+
// ✅ Safe changes
|
|
202
|
+
// - Add new optional fields with new field numbers
|
|
203
|
+
// - Add new enum values (with default 0 for unknown)
|
|
204
|
+
|
|
205
|
+
// ❌ Breaking changes — NEVER do these to a released schema
|
|
206
|
+
// - Remove a field
|
|
207
|
+
// - Rename a field (the name is cosmetic in proto3, but confusing)
|
|
208
|
+
// - Change a field's type
|
|
209
|
+
// - Reuse a field number for a different field
|
|
210
|
+
|
|
211
|
+
// ✅ Example: safe evolution
|
|
212
|
+
message UserPreferences {
|
|
213
|
+
bool is_dark_mode = 1;
|
|
214
|
+
string language = 2;
|
|
215
|
+
int32 font_size = 3; // existing
|
|
216
|
+
SortOrder sort_order = 4; // existing
|
|
217
|
+
// New field in v2 — safe to add
|
|
218
|
+
bool notifications_enabled = 5;
|
|
219
|
+
string theme_color = 6;
|
|
220
|
+
}
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
---
|
|
224
|
+
|
|
225
|
+
## Proto DataStore vs Preferences DataStore
|
|
226
|
+
|
|
227
|
+
| | Proto DataStore | Preferences DataStore |
|
|
228
|
+
|--|----------------|----------------------|
|
|
229
|
+
| Type safety | ✅ Strongly typed | ⚠️ Key-based |
|
|
230
|
+
| Schema evolution | ✅ Protobuf rules | ❌ Manual |
|
|
231
|
+
| Setup complexity | ⚠️ Higher | ✅ Simple |
|
|
232
|
+
| Best for | Structured objects | Simple settings |
|
|
233
|
+
|
|
234
|
+
---
|
|
235
|
+
|
|
236
|
+
## Anti-Patterns
|
|
237
|
+
|
|
238
|
+
- Reusing or renumbering proto field numbers — corrupts data for existing users
|
|
239
|
+
- Removing fields from proto — breaks deserialization of existing data
|
|
240
|
+
- Not providing a default value in Serializer — crashes on empty/new install
|
|
241
|
+
- Multiple DataStore instances for the same file — data corruption
|
|
242
|
+
- Storing large binary data in proto — use files or Room instead
|
|
243
|
+
|
|
244
|
+
---
|
|
245
|
+
|
|
246
|
+
## Related Skills
|
|
247
|
+
- `datastore` — Preferences DataStore for simple key-value storage
|
|
248
|
+
- `hilt` — singleton DataStore provision
|
|
249
|
+
- `repository-pattern` — wrapping DataStore in a Repository
|
|
250
|
+
- `key-value-store-strategy` — choosing between DataStore variants
|