mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-03-30 13:43:26 +08:00
feat: add Kotlin, Android, and KMP rules, agent, skills, and command
This commit is contained in:
86
rules/kotlin/coding-style.md
Normal file
86
rules/kotlin/coding-style.md
Normal file
@@ -0,0 +1,86 @@
|
||||
---
|
||||
paths:
|
||||
- "**/*.kt"
|
||||
- "**/*.kts"
|
||||
---
|
||||
# Kotlin Coding Style
|
||||
|
||||
> This file extends [common/coding-style.md](../common/coding-style.md) with Kotlin specific content.
|
||||
|
||||
## Formatting
|
||||
|
||||
- **ktlint** or **Detekt** for style enforcement
|
||||
- Official Kotlin code style (`kotlin.code.style=official` in `gradle.properties`)
|
||||
|
||||
## Immutability
|
||||
|
||||
- Prefer `val` over `var` — default to `val` and only use `var` when mutation is required
|
||||
- Use `data class` for value types; use immutable collections (`List`, `Map`, `Set`) in public APIs
|
||||
- Copy-on-write for state updates: `state.copy(field = newValue)`
|
||||
|
||||
## Naming
|
||||
|
||||
Follow Kotlin conventions:
|
||||
- `camelCase` for functions and properties
|
||||
- `PascalCase` for classes, interfaces, objects, and type aliases
|
||||
- `SCREAMING_SNAKE_CASE` for constants (`const val` or `@JvmStatic`)
|
||||
- Prefix interfaces with behavior, not `I`: `Clickable` not `IClickable`
|
||||
|
||||
## Null Safety
|
||||
|
||||
- Never use `!!` — prefer `?.`, `?:`, `requireNotNull()`, or `checkNotNull()`
|
||||
- Use `?.let {}` for scoped null-safe operations
|
||||
- Return nullable types from functions that can legitimately have no result
|
||||
|
||||
```kotlin
|
||||
// BAD
|
||||
val name = user!!.name
|
||||
|
||||
// GOOD
|
||||
val name = user?.name ?: "Unknown"
|
||||
val name = requireNotNull(user) { "User must be set before accessing name" }.name
|
||||
```
|
||||
|
||||
## Sealed Types
|
||||
|
||||
Use sealed classes/interfaces to model closed state hierarchies:
|
||||
|
||||
```kotlin
|
||||
sealed interface UiState<out T> {
|
||||
data object Loading : UiState<Nothing>
|
||||
data class Success<T>(val data: T) : UiState<T>
|
||||
data class Error(val message: String) : UiState<Nothing>
|
||||
}
|
||||
```
|
||||
|
||||
Always use exhaustive `when` with sealed types — no `else` branch.
|
||||
|
||||
## Extension Functions
|
||||
|
||||
Use extension functions for utility operations, but keep them discoverable:
|
||||
- Place in a file named after the receiver type (`StringExt.kt`, `FlowExt.kt`)
|
||||
- Keep scope limited — don't add extensions to `Any` or overly generic types
|
||||
|
||||
## Scope Functions
|
||||
|
||||
Use the right scope function:
|
||||
- `let` — null check + transform: `user?.let { greet(it) }`
|
||||
- `run` — compute a result using receiver: `service.run { fetch(config) }`
|
||||
- `apply` — configure an object: `builder.apply { timeout = 30 }`
|
||||
- `also` — side effects: `result.also { log(it) }`
|
||||
- Avoid deep nesting of scope functions (max 2 levels)
|
||||
|
||||
## Error Handling
|
||||
|
||||
- Use `Result<T>` or custom sealed types
|
||||
- Use `runCatching {}` for wrapping throwable code
|
||||
- Never catch `CancellationException` — always rethrow it
|
||||
- Avoid `try-catch` for control flow
|
||||
|
||||
```kotlin
|
||||
// BAD — using exceptions for control flow
|
||||
val user = try { repository.getUser(id) } catch (e: NotFoundException) { null }
|
||||
|
||||
// GOOD — nullable return
|
||||
val user: User? = repository.findUser(id)
|
||||
```
|
||||
139
rules/kotlin/patterns.md
Normal file
139
rules/kotlin/patterns.md
Normal file
@@ -0,0 +1,139 @@
|
||||
---
|
||||
paths:
|
||||
- "**/*.kt"
|
||||
- "**/*.kts"
|
||||
---
|
||||
# Kotlin Patterns
|
||||
|
||||
> This file extends [common/patterns.md](../common/patterns.md) with Kotlin and Android/KMP specific content.
|
||||
|
||||
## Dependency Injection
|
||||
|
||||
Prefer constructor injection. Use Koin (KMP) or Hilt (Android-only):
|
||||
|
||||
```kotlin
|
||||
// Koin — declare modules
|
||||
val dataModule = module {
|
||||
single<ItemRepository> { ItemRepositoryImpl(get(), get()) }
|
||||
factory { GetItemsUseCase(get()) }
|
||||
viewModelOf(::ItemListViewModel)
|
||||
}
|
||||
|
||||
// Hilt — annotations
|
||||
@HiltViewModel
|
||||
class ItemListViewModel @Inject constructor(
|
||||
private val getItems: GetItemsUseCase
|
||||
) : ViewModel()
|
||||
```
|
||||
|
||||
## ViewModel Pattern
|
||||
|
||||
Single state object, event sink, one-way data flow:
|
||||
|
||||
```kotlin
|
||||
data class ScreenState(
|
||||
val items: List<Item> = emptyList(),
|
||||
val isLoading: Boolean = false
|
||||
)
|
||||
|
||||
class ScreenViewModel(private val useCase: GetItemsUseCase) : ViewModel() {
|
||||
private val _state = MutableStateFlow(ScreenState())
|
||||
val state = _state.asStateFlow()
|
||||
|
||||
fun onEvent(event: ScreenEvent) {
|
||||
when (event) {
|
||||
is ScreenEvent.Load -> load()
|
||||
is ScreenEvent.Delete -> delete(event.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Repository Pattern
|
||||
|
||||
- `suspend` functions return `Result<T>` or custom error type
|
||||
- `Flow` for reactive streams
|
||||
- Coordinate local + remote data sources
|
||||
|
||||
```kotlin
|
||||
interface ItemRepository {
|
||||
suspend fun getById(id: String): Result<Item>
|
||||
fun observeAll(): Flow<List<Item>>
|
||||
}
|
||||
```
|
||||
|
||||
## UseCase Pattern
|
||||
|
||||
Single responsibility, `operator fun invoke`:
|
||||
|
||||
```kotlin
|
||||
class GetItemsUseCase(private val repository: ItemRepository) {
|
||||
suspend operator fun invoke(filter: Filter): Result<List<Item>> {
|
||||
return repository.getAll(filter)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## expect/actual (KMP)
|
||||
|
||||
Use for platform-specific implementations:
|
||||
|
||||
```kotlin
|
||||
// commonMain
|
||||
expect fun platformName(): String
|
||||
expect class SecureStorage {
|
||||
fun save(key: String, value: String)
|
||||
fun get(key: String): String?
|
||||
}
|
||||
|
||||
// androidMain
|
||||
actual fun platformName(): String = "Android"
|
||||
actual class SecureStorage {
|
||||
actual fun save(key: String, value: String) { /* EncryptedSharedPreferences */ }
|
||||
actual fun get(key: String): String? { /* ... */ }
|
||||
}
|
||||
|
||||
// iosMain
|
||||
actual fun platformName(): String = "iOS"
|
||||
actual class SecureStorage {
|
||||
actual fun save(key: String, value: String) { /* Keychain */ }
|
||||
actual fun get(key: String): String? { /* ... */ }
|
||||
}
|
||||
```
|
||||
|
||||
## Coroutine Patterns
|
||||
|
||||
- Use `viewModelScope` in ViewModels, `coroutineScope` for structured child work
|
||||
- Use `stateIn(WhileSubscribed(5_000))` for StateFlow from cold Flows
|
||||
- Use `supervisorScope` when child failures should be independent
|
||||
|
||||
## Builder Pattern with DSL
|
||||
|
||||
```kotlin
|
||||
class HttpClientConfig {
|
||||
var baseUrl: String = ""
|
||||
var timeout: Long = 30_000
|
||||
private val interceptors = mutableListOf<Interceptor>()
|
||||
|
||||
fun interceptor(block: () -> Interceptor) {
|
||||
interceptors.add(block())
|
||||
}
|
||||
}
|
||||
|
||||
fun httpClient(block: HttpClientConfig.() -> Unit): HttpClient {
|
||||
val config = HttpClientConfig().apply(block)
|
||||
return HttpClient(config)
|
||||
}
|
||||
|
||||
// Usage
|
||||
val client = httpClient {
|
||||
baseUrl = "https://api.example.com"
|
||||
timeout = 15_000
|
||||
interceptor { AuthInterceptor(tokenProvider) }
|
||||
}
|
||||
```
|
||||
|
||||
## References
|
||||
|
||||
See skill: `kotlin-coroutines-flows` for detailed coroutine patterns.
|
||||
See skill: `android-clean-architecture` for module and layer patterns.
|
||||
82
rules/kotlin/security.md
Normal file
82
rules/kotlin/security.md
Normal file
@@ -0,0 +1,82 @@
|
||||
---
|
||||
paths:
|
||||
- "**/*.kt"
|
||||
- "**/*.kts"
|
||||
---
|
||||
# Kotlin Security
|
||||
|
||||
> This file extends [common/security.md](../common/security.md) with Kotlin and Android/KMP specific content.
|
||||
|
||||
## Secrets Management
|
||||
|
||||
- Never hardcode API keys, tokens, or credentials in source code
|
||||
- Use `local.properties` (git-ignored) for local development secrets
|
||||
- Use `BuildConfig` fields generated from CI secrets for release builds
|
||||
- Use `EncryptedSharedPreferences` (Android) or Keychain (iOS) for runtime secret storage
|
||||
|
||||
```kotlin
|
||||
// BAD
|
||||
val apiKey = "sk-abc123..."
|
||||
|
||||
// GOOD — from BuildConfig (generated at build time)
|
||||
val apiKey = BuildConfig.API_KEY
|
||||
|
||||
// GOOD — from secure storage at runtime
|
||||
val token = secureStorage.get("auth_token")
|
||||
```
|
||||
|
||||
## Network Security
|
||||
|
||||
- Use HTTPS exclusively — configure `network_security_config.xml` to block cleartext
|
||||
- Pin certificates for sensitive endpoints using OkHttp `CertificatePinner` or Ktor equivalent
|
||||
- Set timeouts on all HTTP clients — never leave defaults (which may be infinite)
|
||||
- Validate and sanitize all server responses before use
|
||||
|
||||
```xml
|
||||
<!-- res/xml/network_security_config.xml -->
|
||||
<network-security-config>
|
||||
<base-config cleartextTrafficPermitted="false" />
|
||||
</network-security-config>
|
||||
```
|
||||
|
||||
## Input Validation
|
||||
|
||||
- Validate all user input before processing or sending to API
|
||||
- Use parameterized queries for Room/SQLDelight — never concatenate user input into SQL
|
||||
- Sanitize file paths from user input to prevent path traversal
|
||||
|
||||
```kotlin
|
||||
// BAD — SQL injection
|
||||
@Query("SELECT * FROM items WHERE name = '$input'")
|
||||
|
||||
// GOOD — parameterized
|
||||
@Query("SELECT * FROM items WHERE name = :input")
|
||||
fun findByName(input: String): List<ItemEntity>
|
||||
```
|
||||
|
||||
## Data Protection
|
||||
|
||||
- Use `EncryptedSharedPreferences` for sensitive key-value data on Android
|
||||
- Use `@Serializable` with explicit field names — don't leak internal property names
|
||||
- Clear sensitive data from memory when no longer needed
|
||||
- Use `@Keep` or ProGuard rules for serialized classes to prevent name mangling
|
||||
|
||||
## Authentication
|
||||
|
||||
- Store tokens in secure storage, not in plain SharedPreferences
|
||||
- Implement token refresh with proper 401/403 handling
|
||||
- Clear all auth state on logout (tokens, cached user data, cookies)
|
||||
- Use biometric authentication (`BiometricPrompt`) for sensitive operations
|
||||
|
||||
## ProGuard / R8
|
||||
|
||||
- Keep rules for all serialized models (`@Serializable`, Gson, Moshi)
|
||||
- Keep rules for reflection-based libraries (Koin, Retrofit)
|
||||
- Test release builds — obfuscation can break serialization silently
|
||||
|
||||
## WebView Security
|
||||
|
||||
- Disable JavaScript unless explicitly needed: `settings.javaScriptEnabled = false`
|
||||
- Validate URLs before loading in WebView
|
||||
- Never expose `@JavascriptInterface` methods that access sensitive data
|
||||
- Use `WebViewClient.shouldOverrideUrlLoading()` to control navigation
|
||||
127
rules/kotlin/testing.md
Normal file
127
rules/kotlin/testing.md
Normal file
@@ -0,0 +1,127 @@
|
||||
---
|
||||
paths:
|
||||
- "**/*.kt"
|
||||
- "**/*.kts"
|
||||
---
|
||||
# Kotlin Testing
|
||||
|
||||
> This file extends [common/testing.md](../common/testing.md) with Kotlin and Android/KMP specific content.
|
||||
|
||||
## Test Framework
|
||||
|
||||
- **kotlin.test** for multiplatform (KMP) — `@Test`, `assertEquals`, `assertTrue`
|
||||
- **JUnit 4/5** for Android-specific tests
|
||||
- **Turbine** for testing Flows and StateFlow
|
||||
- **kotlinx-coroutines-test** for coroutine testing (`runTest`, `TestDispatcher`)
|
||||
|
||||
## ViewModel Testing with Turbine
|
||||
|
||||
```kotlin
|
||||
@Test
|
||||
fun `loading state emitted then data`() = runTest {
|
||||
val repo = FakeItemRepository(items = listOf(testItem))
|
||||
val viewModel = ItemListViewModel(GetItemsUseCase(repo))
|
||||
|
||||
viewModel.state.test {
|
||||
assertEquals(ItemListState(), awaitItem()) // initial state
|
||||
viewModel.onEvent(ItemListEvent.Load)
|
||||
assertTrue(awaitItem().isLoading) // loading
|
||||
assertEquals(listOf(testItem), awaitItem().items) // loaded
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Fakes Over Mocks
|
||||
|
||||
Prefer hand-written fakes over mocking frameworks:
|
||||
|
||||
```kotlin
|
||||
class FakeItemRepository : ItemRepository {
|
||||
private val items = mutableListOf<Item>()
|
||||
var fetchError: Throwable? = null
|
||||
|
||||
override suspend fun getAll(): Result<List<Item>> {
|
||||
fetchError?.let { return Result.failure(it) }
|
||||
return Result.success(items.toList())
|
||||
}
|
||||
|
||||
override fun observeAll(): Flow<List<Item>> = flowOf(items.toList())
|
||||
|
||||
fun addItem(item: Item) { items.add(item) }
|
||||
}
|
||||
```
|
||||
|
||||
## Coroutine Testing
|
||||
|
||||
```kotlin
|
||||
@Test
|
||||
fun `parallel operations complete`() = runTest {
|
||||
val repo = FakeRepository()
|
||||
val result = loadDashboard(repo)
|
||||
advanceUntilIdle()
|
||||
assertNotNull(result.items)
|
||||
assertNotNull(result.stats)
|
||||
}
|
||||
```
|
||||
|
||||
Use `runTest` — it auto-advances virtual time and provides `TestScope`.
|
||||
|
||||
## Ktor MockEngine
|
||||
|
||||
```kotlin
|
||||
val mockEngine = MockEngine { request ->
|
||||
when (request.url.encodedPath) {
|
||||
"/api/items" -> respond(
|
||||
content = Json.encodeToString(testItems),
|
||||
headers = headersOf(HttpHeaders.ContentType, ContentType.Application.Json.toString())
|
||||
)
|
||||
else -> respondError(HttpStatusCode.NotFound)
|
||||
}
|
||||
}
|
||||
|
||||
val client = HttpClient(mockEngine) {
|
||||
install(ContentNegotiation) { json() }
|
||||
}
|
||||
```
|
||||
|
||||
## Room/SQLDelight Testing
|
||||
|
||||
- Room: Use `Room.inMemoryDatabaseBuilder()` for in-memory testing
|
||||
- SQLDelight: Use `JdbcSqliteDriver(JdbcSqliteDriver.IN_MEMORY)` for JVM tests
|
||||
|
||||
```kotlin
|
||||
@Test
|
||||
fun `insert and query items`() = runTest {
|
||||
val driver = JdbcSqliteDriver(JdbcSqliteDriver.IN_MEMORY)
|
||||
Database.Schema.create(driver)
|
||||
val db = Database(driver)
|
||||
|
||||
db.itemQueries.insert("1", "Sample Item", "description")
|
||||
val items = db.itemQueries.getAll().executeAsList()
|
||||
assertEquals(1, items.size)
|
||||
}
|
||||
```
|
||||
|
||||
## Test Naming
|
||||
|
||||
Use backtick-quoted descriptive names:
|
||||
|
||||
```kotlin
|
||||
@Test
|
||||
fun `search with empty query returns all items`() = runTest { }
|
||||
|
||||
@Test
|
||||
fun `delete item emits updated list without deleted item`() = runTest { }
|
||||
```
|
||||
|
||||
## Test Organization
|
||||
|
||||
```
|
||||
src/
|
||||
├── commonTest/kotlin/ # Shared tests (ViewModel, UseCase, Repository)
|
||||
├── androidTest/kotlin/ # Android unit tests (JUnit)
|
||||
├── androidInstrumentedTest/kotlin/ # Instrumented tests (Room, UI)
|
||||
└── iosTest/kotlin/ # iOS-specific tests
|
||||
```
|
||||
|
||||
Minimum test coverage: ViewModel + UseCase for every feature.
|
||||
Reference in New Issue
Block a user