Use when implementing async operations with Kotlin coroutines, Flow, StateFlow, or managing concurrency in Android apps.
Provides Kotlin coroutine patterns for Android async operations, including Flow, StateFlow, and lifecycle-aware collection. Triggers when you implement async operations, handle concurrency, or work with reactive data streams in Android apps.
/plugin marketplace add TheBushidoCollective/han/plugin install jutsu-android@hanThis skill is limited to using the following tools:
Asynchronous programming patterns using Kotlin coroutines and Flow in Android.
// Launching coroutines
class UserViewModel : ViewModel() {
fun loadUser(id: String) {
// viewModelScope is automatically cancelled when ViewModel is cleared
viewModelScope.launch {
try {
val user = userRepository.getUser(id)
_uiState.value = UiState.Success(user)
} catch (e: Exception) {
_uiState.value = UiState.Error(e.message)
}
}
}
// For operations that return a value
fun fetchUserAsync(id: String): Deferred<User> {
return viewModelScope.async {
userRepository.getUser(id)
}
}
}
// Suspend functions
suspend fun fetchUserFromNetwork(id: String): User {
return withContext(Dispatchers.IO) {
api.getUser(id)
}
}
// Main - UI operations
withContext(Dispatchers.Main) {
textView.text = "Updated"
}
// IO - Network, database, file operations
withContext(Dispatchers.IO) {
val data = api.fetchData()
database.save(data)
}
// Default - CPU-intensive work
withContext(Dispatchers.Default) {
val result = expensiveComputation(data)
}
// Custom dispatcher for limited parallelism
val limitedDispatcher = Dispatchers.IO.limitedParallelism(4)
// Creating flows
fun getUsers(): Flow<List<User>> = flow {
while (true) {
val users = api.getUsers()
emit(users)
delay(30_000) // Poll every 30 seconds
}
}
// Flow from Room
@Dao
interface UserDao {
@Query("SELECT * FROM users")
fun getAllUsers(): Flow<List<UserEntity>>
}
// Collecting flows
viewModelScope.launch {
userRepository.getUsers()
.catch { e -> _uiState.value = UiState.Error(e) }
.collect { users ->
_uiState.value = UiState.Success(users)
}
}
class SearchViewModel : ViewModel() {
// StateFlow - always has a current value
private val _searchQuery = MutableStateFlow("")
val searchQuery: StateFlow<String> = _searchQuery.asStateFlow()
// SharedFlow - for events without initial value
private val _events = MutableSharedFlow<UiEvent>()
val events: SharedFlow<UiEvent> = _events.asSharedFlow()
// Derived state from flow
val searchResults: StateFlow<List<Item>> = _searchQuery
.debounce(300)
.filter { it.length >= 2 }
.flatMapLatest { query ->
searchRepository.search(query)
}
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = emptyList()
)
fun updateQuery(query: String) {
_searchQuery.value = query
}
fun sendEvent(event: UiEvent) {
viewModelScope.launch {
_events.emit(event)
}
}
}
// Good: Using coroutineScope for parallel operations
suspend fun loadDashboard(): Dashboard = coroutineScope {
val userDeferred = async { userRepository.getUser() }
val ordersDeferred = async { orderRepository.getOrders() }
val notificationsDeferred = async { notificationRepository.getNotifications() }
// All complete or all fail together
Dashboard(
user = userDeferred.await(),
orders = ordersDeferred.await(),
notifications = notificationsDeferred.await()
)
}
// With timeout
suspend fun loadWithTimeout(): Data {
return withTimeout(5000) {
api.fetchData()
}
}
// Or with nullable result on timeout
suspend fun loadWithTimeoutOrNull(): Data? {
return withTimeoutOrNull(5000) {
api.fetchData()
}
}
// Using runCatching
suspend fun safeApiCall(): Result<User> = runCatching {
api.getUser()
}
// Handling in ViewModel
fun loadUser() {
viewModelScope.launch {
safeApiCall()
.onSuccess { user ->
_uiState.value = UiState.Success(user)
}
.onFailure { error ->
_uiState.value = UiState.Error(error.message)
}
}
}
// SupervisorJob for independent child failures
class MyViewModel : ViewModel() {
private val supervisorJob = SupervisorJob()
private val scope = CoroutineScope(Dispatchers.Main + supervisorJob)
fun loadMultiple() {
scope.launch {
// This failure won't cancel other children
userRepository.getUser()
}
scope.launch {
// This continues even if above fails
orderRepository.getOrders()
}
}
}
// Transformation operators
userRepository.getUsers()
.map { users -> users.filter { it.isActive } }
.distinctUntilChanged()
.collect { activeUsers -> updateUI(activeUsers) }
// Combining flows
val combined: Flow<Pair<User, Settings>> = combine(
userRepository.getUser(),
settingsRepository.getSettings()
) { user, settings ->
Pair(user, settings)
}
// FlatMapLatest for search
searchQuery
.debounce(300)
.flatMapLatest { query ->
if (query.isEmpty()) flowOf(emptyList())
else searchRepository.search(query)
}
.collect { results -> updateResults(results) }
// Retry with exponential backoff
api.fetchData()
.retry(3) { cause ->
if (cause is IOException) {
delay(1000 * (2.0.pow(retryCount)).toLong())
true
} else false
}
// In Compose - collectAsStateWithLifecycle
@Composable
fun UserScreen(viewModel: UserViewModel = hiltViewModel()) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
UserContent(uiState)
}
// In Activity/Fragment - repeatOnLifecycle
class UserFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.uiState.collect { state ->
updateUI(state)
}
}
}
}
}
// Multiple flows
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
launch {
viewModel.users.collect { updateUserList(it) }
}
launch {
viewModel.events.collect { handleEvent(it) }
}
}
}
class UserRepository(
private val api: UserApi,
private val dao: UserDao,
private val dispatcher: CoroutineDispatcher = Dispatchers.IO
) {
fun getUser(id: String): Flow<User> = flow {
// Emit cached data first
dao.getUser(id)?.let { emit(it.toDomain()) }
// Fetch from network
val networkUser = api.getUser(id)
dao.insertUser(networkUser.toEntity())
emit(networkUser.toDomain())
}
.flowOn(dispatcher)
.catch { e ->
// Log error, emit from cache if available
dao.getUser(id)?.let { emit(it.toDomain()) }
?: throw e
}
suspend fun refreshUsers() {
withContext(dispatcher) {
val users = api.getUsers()
dao.deleteAll()
dao.insertAll(users.map { it.toEntity() })
}
}
}
suspend fun downloadFile(url: String): ByteArray {
return withContext(Dispatchers.IO) {
val connection = URL(url).openConnection()
connection.inputStream.use { input ->
val buffer = ByteArrayOutputStream()
val data = ByteArray(4096)
while (true) {
// Check for cancellation
ensureActive()
val count = input.read(data)
if (count == -1) break
buffer.write(data, 0, count)
}
buffer.toByteArray()
}
}
}
// Cancellable flow
fun pollData(): Flow<Data> = flow {
while (currentCoroutineContext().isActive) {
emit(api.fetchData())
delay(5000)
}
}
// Debounce - wait for pause in emissions
@Composable
fun SearchField(onSearch: (String) -> Unit) {
var query by remember { mutableStateOf("") }
LaunchedEffect(query) {
delay(300) // Debounce
if (query.isNotEmpty()) {
onSearch(query)
}
}
TextField(value = query, onValueChange = { query = it })
}
// In ViewModel
private val _searchQuery = MutableStateFlow("")
val searchResults = _searchQuery
.debounce(300)
.distinctUntilChanged()
.flatMapLatest { query ->
searchRepository.search(query)
}
.stateIn(viewModelScope, SharingStarted.Lazily, emptyList())
Bad:
GlobalScope.launch { // Never cancelled, leaks memory
fetchData()
}
Good:
viewModelScope.launch { // Properly scoped
fetchData()
}
Bad:
fun loadData() {
runBlocking { // Blocks main thread!
api.fetchData()
}
}
Good:
fun loadData() {
viewModelScope.launch {
withContext(Dispatchers.IO) {
api.fetchData()
}
}
}
Bad:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
lifecycleScope.launch {
viewModel.uiState.collect { // Collects even when in background
updateUI(it)
}
}
}
Good:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.uiState.collect { updateUI(it) }
}
}
}
Bad:
// Creates new flow each time
fun getUsers(): Flow<List<User>> = userDao.getAllUsers()
// Called multiple times, multiple database subscriptions
Good:
// Shared flow, single subscription
val users: StateFlow<List<User>> = userDao.getAllUsers()
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())
This skill should be used when the user asks to "create an agent", "add an agent", "write a subagent", "agent frontmatter", "when to use description", "agent examples", "agent tools", "agent colors", "autonomous agent", or needs guidance on agent structure, system prompts, triggering conditions, or agent development best practices for Claude Code plugins.
This skill should be used when the user asks to "create a slash command", "add a command", "write a custom command", "define command arguments", "use command frontmatter", "organize commands", "create command with file references", "interactive command", "use AskUserQuestion in command", or needs guidance on slash command structure, YAML frontmatter fields, dynamic arguments, bash execution in commands, user interaction patterns, or command development best practices for Claude Code.
This skill should be used when the user asks to "create a hook", "add a PreToolUse/PostToolUse/Stop hook", "validate tool use", "implement prompt-based hooks", "use ${CLAUDE_PLUGIN_ROOT}", "set up event-driven automation", "block dangerous commands", or mentions hook events (PreToolUse, PostToolUse, Stop, SubagentStop, SessionStart, SessionEnd, UserPromptSubmit, PreCompact, Notification). Provides comprehensive guidance for creating and implementing Claude Code plugin hooks with focus on advanced prompt-based hooks API.