Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .agent/skills/android-coroutines/.openskills.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"source": "https://github.com/new-silvermoon/awesome-android-agent-skills.git",
"sourceType": "git",
"repoUrl": "https://github.com/new-silvermoon/awesome-android-agent-skills.git",
"subpath": ".github/skills/android-coroutines",
"installedAt": "2026-02-08T13:21:45.307Z"
}
139 changes: 139 additions & 0 deletions .agent/skills/android-coroutines/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
---
name: android-coroutines
description: Authoritative rules and patterns for production-quality Kotlin Coroutines onto Android. Covers structured concurrency, lifecycle integration, and reactive streams.
---

# Android Coroutines Expert Skill

This skill provides authoritative rules and patterns for writing production-quality Kotlin Coroutines code on Android. It enforces structured concurrency, lifecycle safety, and modern best practices (2025 standards).

## Responsibilities

* **Asynchronous Logic**: Implementing suspend functions, Dispatcher management, and parallel execution.
* **Reactive Streams**: Implementing `Flow`, `StateFlow`, `SharedFlow`, and `callbackFlow`.
* **Lifecycle Integration**: Managing scopes (`viewModelScope`, `lifecycleScope`) and safe collection (`repeatOnLifecycle`).
* **Error Handling**: Implementing `CoroutineExceptionHandler`, `SupervisorJob`, and proper `try-catch` hierarchies.
* **Cancellability**: Ensuring long-running operations are cooperative using `ensureActive()`.
* **Testing**: Setting up `TestDispatcher` and `runTest`.

## Applicability

Activate this skill when the user asks to:
* "Fetch data from an API/Database."
* "Perform background processing."
* "Fix a memory leak" related to threads/tasks.
* "Convert a listener/callback to Coroutines."
* "Implement a ViewModel."
* "Handle UI state updates."

## Critical Rules & Constraints

### 1. Dispatcher Injection (Testability)
* **NEVER** hardcode Dispatchers (e.g., `Dispatchers.IO`, `Dispatchers.Default`) inside classes.
* **ALWAYS** inject a `CoroutineDispatcher` via the constructor.
* **DEFAULT** to `Dispatchers.IO` in the constructor argument for convenience, but allow it to be overridden.

```kotlin
// CORRECT
class UserRepository(
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
) { ... }

// INCORRECT
class UserRepository {
fun getData() = withContext(Dispatchers.IO) { ... }
}
```

### 2. Main-Safety
* All suspend functions defined in the Data or Domain layer must be **main-safe**.
* **One-shot calls** should be exposed as `suspend` functions.
* **Data changes** should be exposed as `Flow`.
* The caller (ViewModel) should be able to call them from `Dispatchers.Main` without blocking the UI.
* Use `withContext(dispatcher)` inside the repository implementation to move execution to the background.

### 3. Lifecycle-Aware Collection
* **NEVER** collect a flow directly in `lifecycleScope.launch` or `launchWhenStarted` (deprecated/unsafe).
* **ALWAYS** use `repeatOnLifecycle(Lifecycle.State.STARTED)` for collecting flows in Activities or Fragments.

```kotlin
// CORRECT
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.uiState.collect { ... }
}
}
```

### 4. ViewModel Scope Usage
* Use `viewModelScope` for initiating coroutines in ViewModels.
* Do not expose suspend functions from the ViewModel to the View. The ViewModel should expose `StateFlow` or `SharedFlow` that the View observes.

### 5. Mutable State Encapsulation
* **NEVER** expose `MutableStateFlow` or `MutableSharedFlow` publicly.
* Expose them as read-only `StateFlow` or `Flow` using `.asStateFlow()` or upcasting.

### 6. GlobalScope Prohibition
* **NEVER** use `GlobalScope`. It breaks structured concurrency and leads to leaks.
* If a task must survive the current scope, use an injected `applicationScope` (a custom scope tied to the Application lifecycle).

### 7. Exception Handling
* **NEVER** catch `CancellationException` in a generic `catch (e: Exception)` block without rethrowing it.
* Use `runCatching` only if you explicitly rethrow `CancellationException`.
* Use `CoroutineExceptionHandler` only for top-level coroutines (inside `launch`). It has no effect inside `async` or child coroutines.

### 8. Cancellability
* Coroutines feature **cooperative cancellation**. They don't stop immediately unless they check for cancellation.
* **ALWAYS** call `ensureActive()` or `yield()` in tight loops (e.g., processing a large list, reading files) to check for cancellation.
* Standard functions like `delay()` and `withContext()` are already cancellable.

### 9. Callback Conversion
* Use `callbackFlow` to convert callback-based APIs to Flow.
* **ALWAYS** use `awaitClose` at the end of the `callbackFlow` block to unregister listeners.

## Code Patterns

### Repository Pattern with Flow

```kotlin
class NewsRepository(
private val remoteDataSource: NewsRemoteDataSource,
private val externalScope: CoroutineScope, // For app-wide events
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
) {
val newsUpdates: Flow<List<News>> = flow {
val news = remoteDataSource.fetchLatestNews()
emit(news)
}.flowOn(ioDispatcher) // Upstream executes on IO
}
```

### Parallel Execution

```kotlin
suspend fun loadDashboardData() = coroutineScope {
val userDeferred = async { userRepo.getUser() }
val feedDeferred = async { feedRepo.getFeed() }

// Wait for both
DashboardData(
user = userDeferred.await(),
feed = feedDeferred.await()
)
}
```

### Testing with runTest

```kotlin
@Test
fun testViewModel() = runTest {
val testDispatcher = StandardTestDispatcher(testScheduler)
val viewModel = MyViewModel(testDispatcher)

viewModel.loadData()
advanceUntilIdle() // Process coroutines

assertEquals(expectedState, viewModel.uiState.value)
}
```
7 changes: 7 additions & 0 deletions .agent/skills/android-gradle-logic/.openskills.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"source": "https://github.com/new-silvermoon/awesome-android-agent-skills.git",
"sourceType": "git",
"repoUrl": "https://github.com/new-silvermoon/awesome-android-agent-skills.git",
"subpath": ".github/skills/android-gradle-logic",
"installedAt": "2026-02-08T13:21:45.308Z"
}
126 changes: 126 additions & 0 deletions .agent/skills/android-gradle-logic/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
---
name: android-gradle-logic
description: Expert guidance on setting up scalable Gradle build logic using Convention Plugins and Version Catalogs.
---

# Android Gradle Build Logic & Convention Plugins

This skill helps you configure a scalable, maintainable build system for Android apps using **Gradle Convention Plugins** and **Version Catalogs**, following the "Now in Android" (NiA) architecture.

## Goal
Stop copy-pasting code between `build.gradle.kts` files. Centralize build logic (Compose setup, Kotlin options, Hilt, etc.) in reusable plugins.

## Project Structure

Ensure your project has a `build-logic` directory included in `settings.gradle.kts` as a composite build.

```text
root/
├── build-logic/
│ ├── convention/
│ │ ├── src/main/kotlin/
│ │ │ └── AndroidApplicationConventionPlugin.kt
│ │ └── build.gradle.kts
│ ├── build.gradle.kts
│ └── settings.gradle.kts
├── gradle/
│ └── libs.versions.toml
├── app/
│ └── build.gradle.kts
└── settings.gradle.kts
```

## Step 1: Configure `settings.gradle.kts`

Include the `build-logic` as a plugin management source.

```kotlin
// settings.gradle.kts
pluginManagement {
includeBuild("build-logic")
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
}
}
```

## Step 2: Define Dependencies in `libs.versions.toml`

Use the Version Catalog for both libraries *and* plugins.

```toml
[versions]
androidGradlePlugin = "8.2.0"
kotlin = "1.9.20"

[libraries]
# ...

[plugins]
android-application = { id = "com.android.application", version.ref = "androidGradlePlugin" }
android-library = { id = "com.android.library", version.ref = "androidGradlePlugin" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
# Define your own plugins here
nowinandroid-android-application = { id = "nowinandroid.android.application", version = "unspecified" }
```

## Step 3: Create a Convention Plugin

Inside `build-logic/convention/src/main/kotlin/AndroidApplicationConventionPlugin.kt`:

```kotlin
import com.android.build.api.dsl.ApplicationExtension
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.kotlin.dsl.configure

class AndroidApplicationConventionPlugin : Plugin<Project> {
override fun apply(target: Project) {
with(target) {
with(pluginManager) {
apply("com.android.application")
apply("org.jetbrains.kotlin.android")
}

extensions.configure<ApplicationExtension> {
defaultConfig.targetSdk = 34
// Configure common options here
}
}
}
}
```

Don't forget to register it in `build-logic/convention/build.gradle.kts`:

```kotlin
gradlePlugin {
plugins {
register("androidApplication") {
id = "nowinandroid.android.application"
implementationClass = "AndroidApplicationConventionPlugin"
}
}
}
```

## Usage

Apply your custom plugin in your modules (e.g., `app/build.gradle.kts`):

```kotlin
plugins {
alias(libs.plugins.nowinandroid.android.application)
}
```

This drastically cleans up module-level build files.
7 changes: 7 additions & 0 deletions .agent/skills/android-testing/.openskills.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"source": "https://github.com/new-silvermoon/awesome-android-agent-skills.git",
"sourceType": "git",
"repoUrl": "https://github.com/new-silvermoon/awesome-android-agent-skills.git",
"subpath": ".github/skills/android-testing",
"installedAt": "2026-02-08T13:21:45.309Z"
}
102 changes: 102 additions & 0 deletions .agent/skills/android-testing/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
---
name: android-testing
description: Comprehensive testing strategy involving Unit, Integration, Hilt, and Screenshot tests.
---

# Android Testing Strategies

This skill provides expert guidance on testing modern Android applications, inspired by "Now in Android". It covers **Unit Tests**, **Hilt Integration Tests**, and **Screenshot Testing**.

## Testing Pyramid

1. **Unit Tests**: Fast, isolate logic (ViewModels, Repositories).
2. **Integration Tests**: Test interactions (Room DAOs, Retrofit vs MockWebServer).
3. **UI/Screenshot Tests**: Verify UI correctness (Compose).

## Dependencies (`libs.versions.toml`)

Ensure you have the right testing dependencies.

```toml
[libraries]
junit4 = { module = "junit:junit", version = "4.13.2" }
kotlinx-coroutines-test = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-test", version.ref = "kotlinxCoroutines" }
androidx-test-ext-junit = { group = "androidx.test.ext", name = "junit", version = "1.1.5" }
espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version = "3.5.1" }
compose-ui-test = { group = "androidx.compose.ui", name = "ui-test-junit4" }
hilt-android-testing = { group = "com.google.dagger", name = "hilt-android-testing", version.ref = "hilt" }
roborazzi = { group = "io.github.takahirom.roborazzi", name = "roborazzi", version.ref = "roborazzi" }
```

## Screenshot Testing with Roborazzi

Screenshot tests ensure your UI doesn't regress visually. NiA uses **Roborazzi** because it runs on the JVM (fast) without needing an emulator.

### Setup

1. Add the plugin to `libs.versions.toml`:
```toml
[plugins]
roborazzi = { id = "io.github.takahirom.roborazzi", version.ref = "roborazzi" }
```
2. Apply it in your module's `build.gradle.kts`:
```kotlin
plugins {
alias(libs.plugins.roborazzi)
}
```

### Writing a Screenshot Test

```kotlin
@RunWith(AndroidJUnit4::class)
@GraphicsMode(GraphicsMode.Mode.NATIVE)
@Config(sdk = [33], qualifiers = RobolectricDeviceQualifiers.Pixel5)
class MyScreenScreenshotTest {

@get:Rule
val composeTestRule = createAndroidComposeRule<ComponentActivity>()

@Test
fun captureMyScreen() {
composeTestRule.setContent {
MyTheme {
MyScreen()
}
}

composeTestRule.onRoot()
.captureRoboImage()
}
}
```

## Hilt Testing

Use `HiltAndroidRule` to inject dependencies in tests.

```kotlin
@HiltAndroidTest
class MyDaoTest {

@get:Rule
var hiltRule = HiltAndroidRule(this)

@Inject
lateinit var database: MyDatabase
private lateinit var dao: MyDao

@Before
fun init() {
hiltRule.inject()
dao = database.myDao()
}

// ... tests
}
```

## Running Tests

* **Unit**: `./gradlew test`
* **Screenshots**: `./gradlew recordRoborazziDebug` (to record) / `./gradlew verifyRoborazziDebug` (to verify)
Loading