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.
Files changed (176) hide show
  1. package/dist/index.js +143 -0
  2. package/package.json +27 -0
  3. package/skills/Android Ecosystem/Baseline Profile Generator/SKILL.md +277 -0
  4. package/skills/Android Ecosystem/Glance/SKILL.md +315 -0
  5. package/skills/Android Platform/Configuration/SKILL.md +201 -0
  6. package/skills/Android Platform/Filesystem/SKILL.md +216 -0
  7. package/skills/Android Platform/Lifecycle/SKILL.md +233 -0
  8. package/skills/Android Platform/Manifest/SKILL.md +226 -0
  9. package/skills/Android Platform/Process Death Recovery/SKILL.md +214 -0
  10. package/skills/Android Platform/Resources/SKILL.md +234 -0
  11. package/skills/Android Platform/SavedStateHandle/SKILL.md +217 -0
  12. package/skills/Android Platform/State Restoration/SKILL.md +210 -0
  13. package/skills/Architecture/Bounded Context/SKILL.md +207 -0
  14. package/skills/Architecture/Clean Architecture/SKILL.md +229 -0
  15. package/skills/Architecture/Domain Modeling/SKILL.md +236 -0
  16. package/skills/Architecture/Entity Design/SKILL.md +243 -0
  17. package/skills/Architecture/Feature Isolation/SKILL.md +216 -0
  18. package/skills/Architecture/MVI/SKILL.md +224 -0
  19. package/skills/Architecture/MVVM/SKILL.md +198 -0
  20. package/skills/Architecture/Modularization/SKILL.md +194 -0
  21. package/skills/Architecture/Offline First/SKILL.md +249 -0
  22. package/skills/Architecture/Repository Pattern/SKILL.md +216 -0
  23. package/skills/Architecture/Side Effect Management/SKILL.md +278 -0
  24. package/skills/Architecture/State Management/SKILL.md +229 -0
  25. package/skills/Architecture/Unidirectional Data Flow/SKILL.md +196 -0
  26. package/skills/Architecture/Use Case Design/SKILL.md +244 -0
  27. package/skills/Architecture/Value Object/SKILL.md +226 -0
  28. package/skills/Build Infrastructure/Build Orchestration/SKILL.md +257 -0
  29. package/skills/Build Infrastructure/Dependency Compatibility Resolver/SKILL.md +259 -0
  30. package/skills/Build Infrastructure/Environment Validator/SKILL.md +311 -0
  31. package/skills/Build System/Build Cache/SKILL.md +233 -0
  32. package/skills/Build System/Build Flavor Strategy/SKILL.md +171 -0
  33. package/skills/Build System/Build Variant/SKILL.md +215 -0
  34. package/skills/Build System/Convention Plugin/SKILL.md +288 -0
  35. package/skills/Build System/Dependency Management/SKILL.md +261 -0
  36. package/skills/Build System/Gradle/SKILL.md +284 -0
  37. package/skills/Build System/Incremental Build/SKILL.md +199 -0
  38. package/skills/Build System/KAPT/SKILL.md +198 -0
  39. package/skills/Build System/KSP/SKILL.md +263 -0
  40. package/skills/Build System/Module Dependency Graph Validation/SKILL.md +223 -0
  41. package/skills/Build System/Specialized/C++/SKILL.md +308 -0
  42. package/skills/Build System/Specialized/JNI/SKILL.md +306 -0
  43. package/skills/Build System/Specialized/NDK/SKILL.md +264 -0
  44. package/skills/Build System/Version Catalog/SKILL.md +304 -0
  45. package/skills/Concurrency/Background Processing/SKILL.md +185 -0
  46. package/skills/Concurrency/Channel/SKILL.md +207 -0
  47. package/skills/Concurrency/Coroutine/SKILL.md +200 -0
  48. package/skills/Concurrency/Flow/SKILL.md +179 -0
  49. package/skills/Concurrency/Mutex Strategy/SKILL.md +185 -0
  50. package/skills/Concurrency/SharedFlow/SKILL.md +171 -0
  51. package/skills/Concurrency/StateFlow/SKILL.md +175 -0
  52. package/skills/Concurrency/Structured Concurrency/SKILL.md +197 -0
  53. package/skills/Concurrency/Synchronization Policy/SKILL.md +192 -0
  54. package/skills/Core Language/Annotation Processing/SKILL.md +224 -0
  55. package/skills/Core Language/DSL/SKILL.md +186 -0
  56. package/skills/Core Language/Extension Functions Design/SKILL.md +191 -0
  57. package/skills/Core Language/Immutability/SKILL.md +156 -0
  58. package/skills/Core Language/KMP/SKILL.md +182 -0
  59. package/skills/Core Language/Kotlin/SKILL.md +187 -0
  60. package/skills/Core Language/Reactive State Management/SKILL.md +228 -0
  61. package/skills/Core Language/Reactive Streams/SKILL.md +235 -0
  62. package/skills/Core Language/Serialization/SKILL.md +191 -0
  63. package/skills/Data Layer/Cache Strategy/SKILL.md +261 -0
  64. package/skills/Data Layer/Conflict Resolution/SKILL.md +248 -0
  65. package/skills/Data Layer/DAO/SKILL.md +225 -0
  66. package/skills/Data Layer/DTO Mapping/SKILL.md +269 -0
  67. package/skills/Data Layer/DataStore/SKILL.md +264 -0
  68. package/skills/Data Layer/Database Versioning Strategy/SKILL.md +215 -0
  69. package/skills/Data Layer/Encrypted Database/SKILL.md +212 -0
  70. package/skills/Data Layer/File Storage/SKILL.md +247 -0
  71. package/skills/Data Layer/Indexing/SKILL.md +184 -0
  72. package/skills/Data Layer/Key-Value Store Strategy/SKILL.md +185 -0
  73. package/skills/Data Layer/Merge Strategy/SKILL.md +240 -0
  74. package/skills/Data Layer/Migration/SKILL.md +243 -0
  75. package/skills/Data Layer/Paging/SKILL.md +264 -0
  76. package/skills/Data Layer/Proto DataStore/SKILL.md +250 -0
  77. package/skills/Data Layer/Room/SKILL.md +244 -0
  78. package/skills/Data Layer/SQLite/SKILL.md +255 -0
  79. package/skills/Data Layer/Sync Engine/SKILL.md +268 -0
  80. package/skills/Dependency Injection/Dagger/SKILL.md +283 -0
  81. package/skills/Dependency Injection/Hilt/SKILL.md +345 -0
  82. package/skills/Dependency Injection/Koin/SKILL.md +282 -0
  83. package/skills/Developer Experience/Detekt/SKILL.md +272 -0
  84. package/skills/Developer Experience/Lint Rule/SKILL.md +281 -0
  85. package/skills/Google Ecosystem/Analytics/SKILL.md +281 -0
  86. package/skills/Google Ecosystem/Crashlytics/SKILL.md +234 -0
  87. package/skills/Google Ecosystem/Firebase/SKILL.md +200 -0
  88. package/skills/Google Ecosystem/Firebase Messaging/SKILL.md +266 -0
  89. package/skills/Media/Audio/SKILL.md +257 -0
  90. package/skills/Media/Camera/SKILL.md +229 -0
  91. package/skills/Media/CameraX/SKILL.md +295 -0
  92. package/skills/Media/ExoPlayer/SKILL.md +258 -0
  93. package/skills/Media/Video/SKILL.md +228 -0
  94. package/skills/Meta Skills/Domain Error Model/SKILL.md +238 -0
  95. package/skills/Meta Skills/Error Handling/SKILL.md +255 -0
  96. package/skills/Meta Skills/Error Mapping/SKILL.md +232 -0
  97. package/skills/Meta Skills/Failure Strategy/SKILL.md +294 -0
  98. package/skills/Meta Skills/Migration Strategy/SKILL.md +305 -0
  99. package/skills/Meta Skills/User Friendly Errors/SKILL.md +334 -0
  100. package/skills/Navigation/Deep Navigation/SKILL.md +209 -0
  101. package/skills/Navigation/Navigation/SKILL.md +215 -0
  102. package/skills/Navigation/Nested Navigation/SKILL.md +214 -0
  103. package/skills/Networking/API Contract/SKILL.md +220 -0
  104. package/skills/Networking/Authentication/SKILL.md +210 -0
  105. package/skills/Networking/Certificate Pinning/SKILL.md +167 -0
  106. package/skills/Networking/Fallback Strategy/SKILL.md +182 -0
  107. package/skills/Networking/Ktor/SKILL.md +219 -0
  108. package/skills/Networking/Multipart Upload/SKILL.md +213 -0
  109. package/skills/Networking/OkHttp/SKILL.md +193 -0
  110. package/skills/Networking/REST/SKILL.md +178 -0
  111. package/skills/Networking/Rate Limiting/SKILL.md +170 -0
  112. package/skills/Networking/Retrofit/SKILL.md +241 -0
  113. package/skills/Networking/Retry-Backoff/SKILL.md +181 -0
  114. package/skills/Networking/Server-Sent Events (SSE)/SKILL.md +196 -0
  115. package/skills/Networking/WebSocket/SKILL.md +224 -0
  116. package/skills/Observability/Crash Reporting/SKILL.md +219 -0
  117. package/skills/Observability/Logging/SKILL.md +168 -0
  118. package/skills/Observability/Metrics/SKILL.md +227 -0
  119. package/skills/Observability/Structured Logging/SKILL.md +234 -0
  120. package/skills/Performance/ANR Prevention/SKILL.md +192 -0
  121. package/skills/Performance/Allocation Optimization/SKILL.md +179 -0
  122. package/skills/Performance/App Startup/SKILL.md +183 -0
  123. package/skills/Performance/Baseline Profile/SKILL.md +205 -0
  124. package/skills/Performance/Battery Optimization/SKILL.md +192 -0
  125. package/skills/Performance/Benchmark/SKILL.md +182 -0
  126. package/skills/Performance/Bitmap Optimization/SKILL.md +178 -0
  127. package/skills/Performance/Compose Optimization/SKILL.md +187 -0
  128. package/skills/Performance/Heap Management/SKILL.md +184 -0
  129. package/skills/Performance/Macrobenchmark/SKILL.md +214 -0
  130. package/skills/Performance/Memory Leak Prevention/SKILL.md +218 -0
  131. package/skills/Performance/Rendering Performance/SKILL.md +205 -0
  132. package/skills/Performance/Startup Optimization/SKILL.md +219 -0
  133. package/skills/Security/Biometric/SKILL.md +224 -0
  134. package/skills/Security/Certificate Transparency/SKILL.md +158 -0
  135. package/skills/Security/Cryptography/SKILL.md +244 -0
  136. package/skills/Security/Encrypted Storage/SKILL.md +273 -0
  137. package/skills/Security/Frida Detection/SKILL.md +230 -0
  138. package/skills/Security/Hook Detection/SKILL.md +197 -0
  139. package/skills/Security/Keystore/SKILL.md +272 -0
  140. package/skills/Security/Network Security Config/SKILL.md +186 -0
  141. package/skills/Security/Obfuscation/SKILL.md +226 -0
  142. package/skills/Security/Proguard/SKILL.md +202 -0
  143. package/skills/Security/R8/SKILL.md +234 -0
  144. package/skills/Security/Reverse Engineering Resistance/SKILL.md +267 -0
  145. package/skills/Security/Root Detection/SKILL.md +220 -0
  146. package/skills/Security/Secure Networking/SKILL.md +220 -0
  147. package/skills/System Integration/AlarmManager/SKILL.md +182 -0
  148. package/skills/System Integration/App Widget/SKILL.md +182 -0
  149. package/skills/System Integration/Deep Link/SKILL.md +187 -0
  150. package/skills/System Integration/Foreground Service/SKILL.md +212 -0
  151. package/skills/System Integration/Notification/SKILL.md +237 -0
  152. package/skills/System Integration/WorkManager/SKILL.md +256 -0
  153. package/skills/System Integration/clipboard/SKILL.md +155 -0
  154. package/skills/System Integration/share-intent/SKILL.md +182 -0
  155. package/skills/Testing/Compose Testing/SKILL.md +296 -0
  156. package/skills/Testing/Espresso/SKILL.md +292 -0
  157. package/skills/Testing/Fake Data/SKILL.md +245 -0
  158. package/skills/Testing/Integration Testing/SKILL.md +288 -0
  159. package/skills/Testing/Mocking/SKILL.md +229 -0
  160. package/skills/Testing/Snapshot Testing/SKILL.md +259 -0
  161. package/skills/Testing/UI Testing/SKILL.md +293 -0
  162. package/skills/Testing/Unit Testing/SKILL.md +309 -0
  163. package/skills/UI System/Bottom Sheet Patterns/SKILL.md +279 -0
  164. package/skills/UI System/Compose/SKILL.md +296 -0
  165. package/skills/UI System/Compose Animation/SKILL.md +281 -0
  166. package/skills/UI System/Compose Multiplatform/SKILL.md +261 -0
  167. package/skills/UI System/Compose Navigation/SKILL.md +255 -0
  168. package/skills/UI System/Compose Performance/SKILL.md +274 -0
  169. package/skills/UI System/Design System/SKILL.md +217 -0
  170. package/skills/UI System/Empty State Strategy/SKILL.md +208 -0
  171. package/skills/UI System/Keyboard Navigation/SKILL.md +214 -0
  172. package/skills/UI System/Loading Strategy/SKILL.md +254 -0
  173. package/skills/UI System/Material 3/SKILL.md +279 -0
  174. package/skills/UI System/RTL/SKILL.md +179 -0
  175. package/src/index.ts +182 -0
  176. package/tsconfig.json +19 -0
@@ -0,0 +1,309 @@
1
+ ---
2
+ name: unit-testing
3
+ description: >
4
+ Unit testing for Android with JUnit5, MockK, and Kotlin coroutines.
5
+ Load this skill when writing unit tests for ViewModels, UseCases,
6
+ Repositories, or any pure logic class, testing coroutines and flows,
7
+ or structuring test classes and assertions.
8
+ ---
9
+
10
+ # Unit Testing
11
+
12
+ ## Overview
13
+ Unit tests verify a single class or function in isolation. Dependencies are replaced with fakes or mocks. In Android, unit tests run on the JVM (not a device), making them fast. ViewModels, UseCases, Repositories, and domain logic are the primary targets.
14
+
15
+ ---
16
+
17
+ ## Core Principles
18
+
19
+ - **Test behavior, not implementation** — test what a class does, not how
20
+ - **One assertion per test** — focused tests are easier to diagnose
21
+ - **AAA structure** — Arrange, Act, Assert
22
+ - **Fakes over mocks** — fakes are more maintainable; use mocks for verification
23
+ - **Test edge cases** — empty lists, null values, errors, boundary conditions
24
+ - **Fast and deterministic** — no network, no disk, no sleep()
25
+
26
+ ---
27
+
28
+ ## Dependencies
29
+
30
+ ```kotlin
31
+ // build.gradle.kts (app module)
32
+ dependencies {
33
+ testImplementation(libs.junit)
34
+ testImplementation(libs.junit.params) // parameterized tests
35
+ testImplementation(libs.mockk) // mocking
36
+ testImplementation(libs.kotlinx.coroutines.test)
37
+ testImplementation(libs.turbine) // Flow testing
38
+ testImplementation(libs.truth) // Google Truth assertions
39
+ }
40
+ ```
41
+
42
+ ```toml
43
+ # libs.versions.toml
44
+ [versions]
45
+ junit = "5.10.2"
46
+ mockk = "1.13.10"
47
+ coroutines-test = "1.8.0"
48
+ turbine = "1.1.0"
49
+ truth = "1.4.2"
50
+
51
+ [libraries]
52
+ junit = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit" }
53
+ junit-params = { module = "org.junit.jupiter:junit-jupiter-params", version.ref = "junit" }
54
+ mockk = { module = "io.mockk:mockk", version.ref = "mockk" }
55
+ kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines-test" }
56
+ turbine = { module = "app.cash.turbine:turbine", version.ref = "turbine" }
57
+ truth = { module = "com.google.truth:truth", version.ref = "truth" }
58
+ ```
59
+
60
+ ---
61
+
62
+ ## Basic Test Structure
63
+
64
+ ```kotlin
65
+ // ✅ AAA pattern with JUnit5
66
+ class GetUserUseCaseTest {
67
+
68
+ // Fake instead of mock for repository
69
+ private val repository = FakeUserRepository()
70
+ private val useCase = GetUserUseCase(repository)
71
+
72
+ @Test
73
+ fun `returns user when found`() {
74
+ // Arrange
75
+ val expected = User(id = "1", name = "Ali", email = Email("ali@test.com"))
76
+ repository.emit(listOf(expected))
77
+
78
+ // Act
79
+ val result = runBlocking { useCase("1") }
80
+
81
+ // Assert
82
+ assertThat(result.isSuccess).isTrue()
83
+ assertThat(result.getOrNull()).isEqualTo(expected)
84
+ }
85
+
86
+ @Test
87
+ fun `returns failure when user not found`() {
88
+ // Arrange
89
+ repository.emit(emptyList())
90
+
91
+ // Act
92
+ val result = runBlocking { useCase("nonexistent") }
93
+
94
+ // Assert
95
+ assertThat(result.isFailure).isTrue()
96
+ }
97
+ }
98
+ ```
99
+
100
+ ---
101
+
102
+ ## Testing ViewModels
103
+
104
+ ```kotlin
105
+ // ✅ ViewModel test with TestCoroutineScheduler
106
+ class UserListViewModelTest {
107
+
108
+ @get:Rule
109
+ val mainDispatcherRule = MainDispatcherRule() // replaces Main dispatcher
110
+
111
+ private val getUsersUseCase = mockk<GetUsersUseCase>()
112
+ private lateinit var viewModel: UserListViewModel
113
+
114
+ @BeforeEach
115
+ fun setup() {
116
+ viewModel = UserListViewModel(getUsersUseCase)
117
+ }
118
+
119
+ @Test
120
+ fun `state is Loading initially`() {
121
+ // Initial state before any collection
122
+ assertThat(viewModel.state.value).isEqualTo(UserListUiState.Loading)
123
+ }
124
+
125
+ @Test
126
+ fun `state is Success when use case returns users`() = runTest {
127
+ // Arrange
128
+ val users = listOf(
129
+ User(id = "1", name = "Ali", email = Email("ali@test.com"))
130
+ )
131
+ every { getUsersUseCase() } returns flowOf(users)
132
+
133
+ // Re-create ViewModel after stubbing
134
+ viewModel = UserListViewModel(getUsersUseCase)
135
+
136
+ // Act + Assert
137
+ viewModel.state.test {
138
+ assertThat(awaitItem()).isEqualTo(UserListUiState.Loading)
139
+ assertThat(awaitItem()).isEqualTo(UserListUiState.Success(users))
140
+ cancelAndIgnoreRemainingEvents()
141
+ }
142
+ }
143
+
144
+ @Test
145
+ fun `state is Error when use case throws`() = runTest {
146
+ // Arrange
147
+ every { getUsersUseCase() } returns flow { throw Exception("Network error") }
148
+ viewModel = UserListViewModel(getUsersUseCase)
149
+
150
+ // Act + Assert
151
+ viewModel.state.test {
152
+ skipItems(1) // skip Loading
153
+ val error = awaitItem()
154
+ assertThat(error).isInstanceOf(UserListUiState.Error::class.java)
155
+ cancelAndIgnoreRemainingEvents()
156
+ }
157
+ }
158
+
159
+ @Test
160
+ fun `delete emits NavigateBack event on success`() = runTest {
161
+ // Arrange
162
+ val deleteUseCase = mockk<DeleteUserUseCase>()
163
+ coEvery { deleteUseCase("1") } returns Result.success(Unit)
164
+ viewModel = UserDetailViewModel(savedStateHandle, getUsersUseCase, deleteUseCase)
165
+
166
+ // Act + Assert
167
+ viewModel.events.test {
168
+ viewModel.onDeleteClick()
169
+ assertThat(awaitItem()).isEqualTo(UserDetailEvent.NavigateBack)
170
+ }
171
+ }
172
+ }
173
+ ```
174
+
175
+ ---
176
+
177
+ ## MainDispatcherRule
178
+
179
+ ```kotlin
180
+ // ✅ Replace Main dispatcher in tests (required for ViewModel tests)
181
+ class MainDispatcherRule(
182
+ private val dispatcher: TestDispatcher = UnconfinedTestDispatcher()
183
+ ) : TestWatcher() {
184
+
185
+ override fun starting(description: Description) {
186
+ Dispatchers.setMain(dispatcher)
187
+ }
188
+
189
+ override fun finished(description: Description) {
190
+ Dispatchers.resetMain()
191
+ }
192
+ }
193
+ ```
194
+
195
+ ---
196
+
197
+ ## Testing Flows with Turbine
198
+
199
+ ```kotlin
200
+ // ✅ Turbine — clean Flow assertion API
201
+ @Test
202
+ fun `observe users emits list updates`() = runTest {
203
+ val repository = FakeUserRepository()
204
+ val useCase = ObserveUsersUseCase(repository)
205
+
206
+ useCase().test {
207
+ // Initial empty state
208
+ assertThat(awaitItem()).isEmpty()
209
+
210
+ // Emit new data
211
+ val user = User(id = "1", name = "Ali", email = Email("ali@test.com"))
212
+ repository.emit(listOf(user))
213
+ assertThat(awaitItem()).containsExactly(user)
214
+
215
+ // Update existing
216
+ val updated = user.copy(name = "Ali Updated")
217
+ repository.emit(listOf(updated))
218
+ assertThat(awaitItem()).containsExactly(updated)
219
+
220
+ cancelAndIgnoreRemainingEvents()
221
+ }
222
+ }
223
+ ```
224
+
225
+ ---
226
+
227
+ ## Testing with MockK
228
+
229
+ ```kotlin
230
+ // ✅ MockK for mocking Kotlin classes
231
+ class UserRepositoryImplTest {
232
+
233
+ private val dao = mockk<UserDao>()
234
+ private val api = mockk<UserApiService>()
235
+ private val repository = UserRepositoryImpl(dao, api)
236
+
237
+ @Test
238
+ fun `getUser returns cached value when available`() = runTest {
239
+ // Arrange
240
+ val entity = UserEntity(id = "1", name = "Ali", email = "ali@test.com", role = "member")
241
+ coEvery { dao.getById("1") } returns entity
242
+
243
+ // Act
244
+ val result = repository.getUser("1")
245
+
246
+ // Assert
247
+ assertThat(result.isSuccess).isTrue()
248
+ coVerify(exactly = 0) { api.getUser(any()) } // ✅ API not called
249
+ }
250
+
251
+ @Test
252
+ fun `getUser fetches from API when not cached`() = runTest {
253
+ // Arrange
254
+ val dto = UserDto(id = "1", name = "Ali", email = "ali@test.com", role = "member")
255
+ coEvery { dao.getById("1") } returns null
256
+ coEvery { api.getUser("1") } returns dto
257
+ coEvery { dao.insert(any()) } just Runs
258
+
259
+ // Act
260
+ val result = repository.getUser("1")
261
+
262
+ // Assert
263
+ assertThat(result.isSuccess).isTrue()
264
+ coVerify(exactly = 1) { api.getUser("1") }
265
+ coVerify(exactly = 1) { dao.insert(any()) }
266
+ }
267
+ }
268
+ ```
269
+
270
+ ---
271
+
272
+ ## Parameterized Tests
273
+
274
+ ```kotlin
275
+ // ✅ Test multiple inputs with one test function
276
+ class EmailValidationTest {
277
+
278
+ @ParameterizedTest
279
+ @ValueSource(strings = ["ali@test.com", "user@domain.co.uk", "a@b.io"])
280
+ fun `valid email addresses are accepted`(email: String) {
281
+ assertThat(runCatching { Email(email) }.isSuccess).isTrue()
282
+ }
283
+
284
+ @ParameterizedTest
285
+ @ValueSource(strings = ["notanemail", "@nodomain", "missing@", ""])
286
+ fun `invalid email addresses throw`(email: String) {
287
+ assertThat(runCatching { Email(email) }.isFailure).isTrue()
288
+ }
289
+ }
290
+ ```
291
+
292
+ ---
293
+
294
+ ## Anti-Patterns
295
+
296
+ - Testing implementation details — if refactoring breaks tests without changing behavior, tests are wrong
297
+ - `Thread.sleep()` in tests — use `runTest` and `TestDispatcher`
298
+ - Not resetting mocks between tests — use `@BeforeEach` or `clearAllMocks()`
299
+ - Testing private methods — test through the public API instead
300
+ - One giant test class — split by the class under test
301
+
302
+ ---
303
+
304
+ ## Related Skills
305
+ - `mocking` — MockK patterns and verification
306
+ - `fake-data` — building test data with factories
307
+ - `integration-testing` — testing multiple components together
308
+ - `coroutine` — coroutine dispatcher setup for tests
309
+ - `flow` — Flow testing with Turbine
@@ -0,0 +1,279 @@
1
+ ---
2
+ name: bottom-sheet-pattern
3
+ description: >
4
+ Bottom sheet and side sheet patterns in Jetpack Compose with Material 3.
5
+ Load this skill when implementing modal bottom sheets, persistent bottom
6
+ sheets, side sheets, sheet state management, nested scrolling inside sheets,
7
+ or handling keyboard interaction with sheets.
8
+ ---
9
+
10
+ # Bottom Sheet / Side Sheet Patterns
11
+
12
+ ## Overview
13
+ Bottom sheets and side sheets are surface containers that slide into view from the edge of the screen. M3 provides `ModalBottomSheet` and `BottomSheetScaffold`. Side sheets are handled via custom `AnimatedVisibility` or `Drawer` depending on the use case.
14
+
15
+ ---
16
+
17
+ ## Core Principles
18
+
19
+ - Use `ModalBottomSheet` for transient, overlay content (actions, pickers)
20
+ - Use `BottomSheetScaffold` for persistent sheets that coexist with screen content
21
+ - Always hoist `SheetState` to the caller — never manage it inside the sheet
22
+ - Add `imePadding()` inside sheet content when it contains input fields
23
+ - Never put a bottom sheet inside another bottom sheet
24
+
25
+ ---
26
+
27
+ ## Modal Bottom Sheet
28
+
29
+ ```kotlin
30
+ // ✅ Basic modal bottom sheet
31
+ @Composable
32
+ fun UserActionsSheet(
33
+ onDismiss: () -> Unit,
34
+ onEdit: () -> Unit,
35
+ onDelete: () -> Unit
36
+ ) {
37
+ val sheetState = rememberModalBottomSheetState(
38
+ skipPartiallyExpanded = true // go directly to full height
39
+ )
40
+
41
+ ModalBottomSheet(
42
+ onDismissRequest = onDismiss,
43
+ sheetState = sheetState
44
+ ) {
45
+ Column(
46
+ modifier = Modifier
47
+ .fillMaxWidth()
48
+ .padding(bottom = AppSpacing.lg)
49
+ ) {
50
+ ListItem(
51
+ headlineContent = { Text("Edit") },
52
+ leadingContent = { Icon(Icons.Default.Edit, null) },
53
+ modifier = Modifier.clickable { onEdit(); onDismiss() }
54
+ )
55
+ ListItem(
56
+ headlineContent = { Text("Delete") },
57
+ leadingContent = { Icon(Icons.Default.Delete, null) },
58
+ modifier = Modifier.clickable { onDelete(); onDismiss() }
59
+ )
60
+ }
61
+ }
62
+ }
63
+
64
+ // ✅ Show/hide from parent
65
+ @Composable
66
+ fun UserScreen() {
67
+ var showSheet by remember { mutableStateOf(false) }
68
+
69
+ Scaffold { padding ->
70
+ // screen content
71
+ }
72
+
73
+ if (showSheet) {
74
+ UserActionsSheet(
75
+ onDismiss = { showSheet = false },
76
+ onEdit = { /* ... */ },
77
+ onDelete = { /* ... */ }
78
+ )
79
+ }
80
+ }
81
+ ```
82
+
83
+ ---
84
+
85
+ ## Sheet with Input Fields (Keyboard)
86
+
87
+ ```kotlin
88
+ // ✅ imePadding + windowInsetsPadding inside sheet with text fields
89
+ @Composable
90
+ fun AddItemSheet(
91
+ onDismiss: () -> Unit,
92
+ onAdd: (String) -> Unit
93
+ ) {
94
+ val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
95
+ var text by remember { mutableStateOf("") }
96
+ val focusRequester = remember { FocusRequester() }
97
+
98
+ ModalBottomSheet(
99
+ onDismissRequest = onDismiss,
100
+ sheetState = sheetState,
101
+ windowInsets = WindowInsets.ime // ✅ sheet moves above keyboard
102
+ ) {
103
+ Column(
104
+ modifier = Modifier
105
+ .fillMaxWidth()
106
+ .padding(AppSpacing.md)
107
+ .imePadding() // ✅ extra safety for content inside
108
+ ) {
109
+ OutlinedTextField(
110
+ value = text,
111
+ onValueChange = { text = it },
112
+ label = { Text("Item name") },
113
+ modifier = Modifier
114
+ .fillMaxWidth()
115
+ .focusRequester(focusRequester),
116
+ keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
117
+ keyboardActions = KeyboardActions(onDone = { onAdd(text) })
118
+ )
119
+ Spacer(Modifier.height(AppSpacing.md))
120
+ Button(
121
+ onClick = { onAdd(text) },
122
+ modifier = Modifier.fillMaxWidth()
123
+ ) {
124
+ Text("Add")
125
+ }
126
+ }
127
+ }
128
+
129
+ LaunchedEffect(Unit) {
130
+ focusRequester.requestFocus()
131
+ }
132
+ }
133
+ ```
134
+
135
+ ---
136
+
137
+ ## Scrollable Content Inside Sheet
138
+
139
+ ```kotlin
140
+ // ✅ Nested scroll in modal bottom sheet
141
+ @Composable
142
+ fun FilterSheet(
143
+ filters: List<Filter>,
144
+ onDismiss: () -> Unit
145
+ ) {
146
+ val sheetState = rememberModalBottomSheetState()
147
+
148
+ ModalBottomSheet(
149
+ onDismissRequest = onDismiss,
150
+ sheetState = sheetState
151
+ ) {
152
+ LazyColumn(
153
+ modifier = Modifier
154
+ .fillMaxWidth()
155
+ .heightIn(max = 400.dp), // ✅ cap height for large lists
156
+ contentPadding = PaddingValues(
157
+ start = AppSpacing.md,
158
+ end = AppSpacing.md,
159
+ bottom = AppSpacing.lg
160
+ )
161
+ ) {
162
+ items(filters) { filter ->
163
+ FilterItem(filter)
164
+ }
165
+ }
166
+ }
167
+ }
168
+ ```
169
+
170
+ ---
171
+
172
+ ## Persistent Bottom Sheet (BottomSheetScaffold)
173
+
174
+ ```kotlin
175
+ // ✅ Persistent sheet coexists with screen content
176
+ @Composable
177
+ fun MapScreen() {
178
+ val scaffoldState = rememberBottomSheetScaffoldState(
179
+ bottomSheetState = rememberStandardBottomSheetState(
180
+ initialValue = SheetValue.PartiallyExpanded
181
+ )
182
+ )
183
+
184
+ BottomSheetScaffold(
185
+ scaffoldState = scaffoldState,
186
+ sheetContent = {
187
+ Column(
188
+ modifier = Modifier
189
+ .fillMaxWidth()
190
+ .padding(AppSpacing.md)
191
+ ) {
192
+ Text("Location Details", style = MaterialTheme.typography.titleMedium)
193
+ // sheet content
194
+ }
195
+ },
196
+ sheetPeekHeight = 120.dp // ✅ height when partially expanded
197
+ ) { padding ->
198
+ // main screen content (map)
199
+ Box(modifier = Modifier.padding(padding)) { ... }
200
+ }
201
+ }
202
+ ```
203
+
204
+ ---
205
+
206
+ ## Side Sheet (Custom)
207
+
208
+ ```kotlin
209
+ // ✅ Side sheet via AnimatedVisibility — for tablets or landscape
210
+ @Composable
211
+ fun SideSheet(
212
+ visible: Boolean,
213
+ onDismiss: () -> Unit,
214
+ content: @Composable () -> Unit
215
+ ) {
216
+ AnimatedVisibility(
217
+ visible = visible,
218
+ enter = slideInHorizontally(initialOffsetX = { it }),
219
+ exit = slideOutHorizontally(targetOffsetX = { it })
220
+ ) {
221
+ Surface(
222
+ modifier = Modifier
223
+ .fillMaxHeight()
224
+ .width(320.dp)
225
+ .align(Alignment.End), // in a Box parent
226
+ tonalElevation = 4.dp,
227
+ shadowElevation = 4.dp
228
+ ) {
229
+ Column(modifier = Modifier.padding(AppSpacing.md)) {
230
+ content()
231
+ }
232
+ }
233
+ }
234
+ }
235
+
236
+ // ✅ Wrap in a Box at screen level
237
+ Box(modifier = Modifier.fillMaxSize()) {
238
+ MainContent()
239
+ SideSheet(visible = showSideSheet, onDismiss = { showSideSheet = false }) {
240
+ FilterPanel()
241
+ }
242
+ }
243
+ ```
244
+
245
+ ---
246
+
247
+ ## Programmatic Sheet Control
248
+
249
+ ```kotlin
250
+ // ✅ Expand / collapse programmatically
251
+ val sheetState = rememberModalBottomSheetState()
252
+ val scope = rememberCoroutineScope()
253
+
254
+ Button(onClick = {
255
+ scope.launch { sheetState.expand() }
256
+ }) { Text("Expand") }
257
+
258
+ Button(onClick = {
259
+ scope.launch { sheetState.hide() }
260
+ }) { Text("Hide") }
261
+ ```
262
+
263
+ ---
264
+
265
+ ## Anti-Patterns
266
+
267
+ - Managing `SheetState` inside the sheet composable — hoist it to the caller
268
+ - Putting a `ModalBottomSheet` inside another `ModalBottomSheet`
269
+ - Not adding `imePadding()` or `windowInsets = WindowInsets.ime` in sheets with text fields
270
+ - Using `Dialog` instead of `ModalBottomSheet` for action menus on mobile
271
+ - Not capping height on scrollable sheet content — sheet can fill the entire screen
272
+
273
+ ---
274
+
275
+ ## Related Skills
276
+ - `compose` — Compose layout and animation fundamentals
277
+ - `keyboard-navigation` — IME handling inside sheets
278
+ - `material3` — M3 surface and elevation tokens
279
+ - `navigation` — navigating to/from content shown in sheets