协作头脑风暴
brainstorming
在实现前澄清需求、探索设计选择,并形成清晰的开发方向。
android【开发与工程】Use this skill when the user asks about Android development, Kotlin, Jetpack Compose, Android architecture, MVVM, Clean Architecture, Room, Hilt, Retrofit, or Android testing.
独立 GitHub 快照 · 自动分类 · 以原作者文档为准
Implement Android native applications using Kotlin, Jetpack Compose, MVVM+Clean Architecture, Coroutines+Flow, Hilt DI, Room, and comprehensive testing.
User request includes: android, kotlin, jetpack, compose, android architecture, android testing, room, hilt.
A markdown document containing:
Produce the artifact directly. No preamble, no postamble, no explanations. No filler, no hedging, no transitions. Strip articles a/an/the where unambiguous. Compress output — why use many token when few do trick.
4096 tokens
Organize code with Clean Architecture layers: data, domain, ui, and di with feature-based packaging.
Define domain models, repository interfaces, and use cases independent of framework dependencies.
Set up Room database with DAOs and entities, Retrofit API service, and repository implementations bridging local and remote sources.
Set up Hilt modules for database, network, and repository bindings with appropriate scopes.
Create ViewModels with StateFlow, Compose screens with state collection, and handle loading/error/success states.
Cover domain use cases with JUnit + MockK unit tests and UI screens with Compose + Espresso UI tests.
App complexity?
├── Simple CRUD (<10 screens, no complex state)
│ → MVVM + Repository — ViewModel → Repository → API/DB
│ → No UseCase layer needed
├── Complex state (real-time, multi-step forms)
│ → MVI with sealed Intent/State classes, reducer pattern
│ → Single state object per screen
└── Large codebase (5+ teams, shared domain)
→ Clean Architecture with strict layer separation
→ Domain module shared across features
Team preference?
├── Annotation-based → Hilt
│ Pros: compile-time verification, @HiltViewModel, less boilerplate
│ Cons: slower build, kapt/ksp overhead
├── Manual DI → Koin
│ Pros: no annotation processing, fast builds, easy to learn
│ Cons: runtime errors for missing bindings
└── Compiler-assisted → Dagger (raw)
Pros: most control, mature
Cons: steep learning curve, verbose
Single-activity?
├── Yes → Jetpack Navigation Compose
│ NavHost with sealed Route class, type-safe arguments
├── Multi-activity → Navigation component with XML
└── Feature-modules → Dynamic feature navigation with NavDeepLink
Network images needed?
├── Yes, Coil — Kotlin coroutines, Compose integration, memory-efficient
└── Yes, Glide — Mature, animated GIF/WebP, disk cache, OK
Coil preferred for new projects — lighter (150KB vs ~500KB)
WorkManager vs Foreground Service vs AlarmManager
├── Deferrable, reliable → WorkManager (constraints, chaining, battery-optimized)
├── User-visible must continue → Foreground Service (notification required)
└── Exact timing needed → AlarmManager (limited, consider ScheduleTaskExecutor)
app/
├── src/main/java/com/example/app/
│ ├── App.kt
│ ├── MainActivity.kt
│ ├── data/
│ │ ├── local/
│ │ │ ├── AppDatabase.kt
│ │ │ ├── OrderDao.kt
│ │ │ └── OrderEntity.kt
│ │ ├── remote/
│ │ │ ├── ApiService.kt
│ │ │ └── OrderResponse.kt
│ │ └── repository/
│ │ └── OrderRepositoryImpl.kt
│ ├── di/
│ │ ├── AppModule.kt
│ │ ├── DatabaseModule.kt
│ │ └── NetworkModule.kt
│ ├── domain/
│ │ ├── model/
│ │ │ └── Order.kt
│ │ ├── repository/
│ │ │ └── OrderRepository.kt
│ │ └── usecase/
│ │ └── GetOrdersUseCase.kt
│ └── ui/
│ ├── orders/
│ │ ├── OrderListScreen.kt
│ │ ├── OrderDetailScreen.kt
│ │ └── OrderViewModel.kt
│ └── theme/
└── src/test/
└── ...
// domain/model/Order.kt
data class Order(
val id: String,
val customerName: String,
val total: Double,
val status: OrderStatus
)
enum class OrderStatus { PENDING, SHIPPED, DELIVERED }
// domain/repository/OrderRepository.kt
interface OrderRepository {
suspend fun getOrders(): Result<List<Order>>
suspend fun getOrder(id: String): Result<Order>
}
// domain/usecase/GetOrdersUseCase.kt
class GetOrdersUseCase(private val repo: OrderRepository) {
suspend operator fun invoke(): Result<List<Order>> = repo.getOrders()
}
// ui/orders/OrderViewModel.kt
@HiltViewModel
class OrderViewModel @Inject constructor(
private val getOrders: GetOrdersUseCase
) : ViewModel() {
private val _state = MutableStateFlow<OrderUiState>(OrderUiState.Loading)
val state: StateFlow<OrderUiState> = _state.asStateFlow()
fun loadOrders() {
viewModelScope.launch {
_state.value = OrderUiState.Loading
getOrders()
.onSuccess { _state.value = OrderUiState.Success(it) }
.onFailure { _state.value = OrderUiState.Error(it.message) }
}
}
}
sealed interface OrderUiState {
data object Loading : OrderUiState
data class Success(val orders: List<Order>) : OrderUiState
data class Error(val message: String?) : OrderUiState
}
// ui/orders/OrderListScreen.kt
@Composable
fun OrderListScreen(
viewModel: OrderViewModel = hiltViewModel(),
onOrderClick: (String) -> Unit
) {
val state by viewModel.state.collectAsStateWithLifecycle()
LaunchedEffect(Unit) { viewModel.loadOrders() }
when (val s = state) {
is OrderUiState.Loading -> CircularProgressIndicator()
is OrderUiState.Error -> ErrorMessage(s.message, onRetry = viewModel::loadOrders)
is OrderUiState.Success -> LazyColumn {
items(s.orders, key = { it.id }) { order ->
OrderCard(order = order, onClick = { onOrderClick(order.id) })
}
}
}
}
@Composable
fun OrderCard(order: Order, onClick: () -> Unit) {
Card(modifier = Modifier.clickable(onClick = onClick)) {
Column(modifier = Modifier.padding(16.dp)) {
Text(text = order.customerName, style = MaterialTheme.typography.titleMedium)
Text(text = "${order.total}", style = MaterialTheme.typography.bodyLarge)
}
}
}
// data/local/OrderEntity.kt
@Entity(tableName = "orders")
data class OrderEntity(
@PrimaryKey val id: String,
@ColumnInfo(name = "customer_name") val customerName: String,
val total: Double,
val status: String
)
// data/local/OrderDao.kt
@Dao
interface OrderDao {
@Query("SELECT * FROM orders")
fun getAll(): Flow<List<OrderEntity>>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertAll(orders: List<OrderEntity>)
@Query("DELETE FROM orders WHERE id = :id")
suspend fun deleteById(id: String)
}
// data/local/AppDatabase.kt
@Database(entities = [OrderEntity::class], version = 1, exportSchema = true)
abstract class AppDatabase : RoomDatabase() {
abstract fun orderDao(): OrderDao
}
// di/AppModule.kt
@Module
@InstallIn(SingletonComponent::class)
object AppModule {
@Provides @Singleton
fun provideOrderRepository(
api: ApiService,
dao: OrderDao
): OrderRepository = OrderRepositoryImpl(api, dao)
}
// di/NetworkModule.kt
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
@Provides @Singleton
fun provideApiService(): ApiService {
return Retrofit.Builder()
.baseUrl("https://api.example.com/")
.addConverterFactory(MoshiConverterFactory.create())
.addCallAdapterFactory(CoroutineCallAdapterFactory())
.client(OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.addInterceptor(HttpLoggingInterceptor().apply {
level = if (BuildConfig.DEBUG) BODY else NONE
})
.build())
.build()
.create(ApiService::class.java)
}
}
// di/DatabaseModule.kt
@Module
@InstallIn(SingletonComponent::class)
object DatabaseModule {
@Provides @Singleton
fun provideDatabase(@ApplicationContext ctx: Context): AppDatabase {
return Room.databaseBuilder(ctx, AppDatabase::class.java, "app-db")
.fallbackToDestructiveMigration()
.build()
}
@Provides fun provideOrderDao(db: AppDatabase): OrderDao = db.orderDao()
}
class GetOrdersUseCaseTest {
private val repo = mockk<OrderRepository>()
private val useCase = GetOrdersUseCase(repo)
@Test
fun `returns orders successfully`() = runTest {
val orders = listOf(Order("1", "Test", 100.0, OrderStatus.PENDING))
coEvery { repo.getOrders() } returns Result.success(orders)
val result = useCase()
assertTrue(result.isSuccess)
assertEquals(orders, result.getOrThrow())
}
@Test
fun `returns failure when repository fails`() = runTest {
coEvery { repo.getOrders() } returns Result.failure(IOException("Network error"))
val result = useCase()
assertTrue(result.isFailure)
assertTrue(result.exceptionOrNull() is IOException)
}
}
@RunWith(AndroidJUnit4::class)
class OrderListScreenTest {
@get:Rule
val composeTestRule = createComposeRule()
@Test
fun displaysOrders() {
val viewModel = mockk<OrderViewModel>()
every { viewModel.state } returns MutableStateFlow(
OrderUiState.Success(listOf(
Order("1", "Alice", 50.0, OrderStatus.PENDING)
))
).asStateFlow()
composeTestRule.setContent {
OrderListScreen(viewModel = viewModel, onOrderClick = {})
}
composeTestRule.onNodeWithText("Alice").assertIsDisplayed()
}
@Test
fun showsErrorState() {
val viewModel = mockk<OrderViewModel>()
every { viewModel.state } returns MutableStateFlow(
OrderUiState.Error("Failed to load")
).asStateFlow()
composeTestRule.setContent {
OrderListScreen(viewModel = viewModel, onOrderClick = {})
}
composeTestRule.onNodeWithText("Failed to load").assertIsDisplayed()
}
}
// Stable data classes — Compose skips recomposition when inputs are stable
@Stable
data class OrderUiModel(val id: String, val name: String, val total: Double)
// derivedStateOf for expensive computations
val totalPrice by remember(orders) {
derivedStateOf { orders.sumOf { it.total } }
}
// LazyColumn keys for stable identity
items(orders, key = { it.id }) { order -> OrderCard(order) }
// Remember lambda to avoid allocation on recomposition
val onItemClick = remember { { id: String -> onOrderClick(id) } }
// ContentType for list-item differentiation
items(orders, key = { it.id }, contentType = { "order" }) { ... }
size → padding → background → clickable (outer to inner)Modifier.composed for reusable modifier chainsModifier.absoluteOffset — prefer Modifier.offset for RTL supportModifier.clipToBounds() to prevent content overflowModifier.alpha(0f) vs Modifier.visible(false) — invisible keeps layout space, gone releases it@Keep annotations, Moshi adapters, Retrofit interfacesviewModelScope.launch for fire-and-forget operations (logging, analytics)viewModelScope.async for concurrent decomposition (parallel API calls)flatMapLatest for search-as-you-type debounced queriesstateIn(WhileSubscribed(5000)) to keep upstream active for 5s after last subscribercatch { emit(UiState.Error(it)) } for error recovery in Flow chainsRetrofit suspend functions are main-safe — no need to wrap with withContext(IO)Flow automatically runs on background threadDispatchers.IO for blocking operations, Dispatchers.Default for CPU-heavy workFeature-first (recommended for medium+ apps):
com.example.app/
├── feature/orders/
│ ├── data/ local/ remote/ repository/
│ ├── domain/ model/ repository/ usecase/
│ └── ui/ OrderListScreen OrderViewModel
├── core/ network/ database/ di/ testing/
Layer-first (simple apps):
com.example.app/
├── data/
├── domain/
├── ui/
└── di/
ApiResult or DbModel — map to domain modelsStateFlow, keep MutableStateFlow privateGlobalScope.launch — creates leaked coroutines tied to app processsuspend functions or Flowremember does notAsyncImage or rememberAsyncImagePainterHand off to mobile/universal/deployment/SKILL.md for deployment.