android-jetpack-compose について
Android 開発者向けの Jetpack Compose スキルです。宣言型 UI の状態管理やコンポーネント設計、ナビゲーション、Material 3 のテーマ設定を扱います。
主な機能
- 01Compose の状態管理
- 02状態の引き上げと再利用可能なコンポーネント
- 03ViewModel 連携
- 04効率的な再コンポーズと副作用
- 05ナビゲーションと Material 3 のテーマ
活用シーン
- 01状態を持つ Android UI の構築
- 02再利用可能なステートレスコンポーネントの設計
- 03UI の非同期読み込み処理
- 04アプリのナビゲーションとテーマの実装
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ expo/ agent-templates --skill android-jetpack-composeインストール後、Claude Code で/android-jetpack-composeを入力して読み込まれたか確認します。
詳細
Android - Jetpack Compose
ネイティブ Android インターフェースを構築するための、モダンな宣言型 UI ツールキットです。
主な概念
状態管理
Compose には、状態を管理する方法がいくつか用意されています。
- remember:再コンポジション後も保持されます
- rememberSaveable:構成変更後も保持されます
- mutableStateOf:監視可能な状態を作成します
- derivedStateOf:依存関係が変わると更新される計算済みの状態です
@Composable
fun Counter() {
var count by remember { mutableStateOf(0) }
Column {
Text("Count: $count")
Button(onClick = { count++ }) {
Text("Increment")
}
}
}
// With saveable for configuration changes
@Composable
fun SearchField() {
var query by rememberSaveable { mutableStateOf("") }
TextField(
value = query,
onValueChange = { query = it },
placeholder = { Text("Search...") }
)
}
状態のホイスティング
状態を上位に移動して、コンポーザブルをステートレスかつ再利用可能にします。
// Stateless composable
@Composable
fun NameInput(
name: String,
onNameChange: (String) -> Unit,
modifier: Modifier = Modifier
) {
TextField(
value = name,
onValueChange = onNameChange,
label = { Text("Name") },
modifier = modifier
)
}
// Stateful parent
@Composable
fun UserForm() {
var name by remember { mutableStateOf("") }
NameInput(
name = name,
onNameChange = { name = it }
)
}
ViewModel との統合
class UserViewModel : ViewModel() {
private val _uiState = MutableStateFlow(UserUiState())
val uiState: StateFlow<UserUiState> = _uiState.asStateFlow()
fun updateName(name: String) {
_uiState.update { it.copy(name = name) }
}
fun saveUser() {
viewModelScope.launch {
_uiState.update { it.copy(isLoading = true) }
try {
userRepository.save(_uiState.value.toUser())
_uiState.update { it.copy(isLoading = false, isSaved = true) }
} catch (e: Exception) {
_uiState.update { it.copy(isLoading = false, error = e.message) }
}
}
}
}
@Composable
fun UserScreen(viewModel: UserViewModel = viewModel()) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
UserContent(
uiState = uiState,
onNameChange = viewModel::updateName,
onSave = viewModel::saveUser
)
}
ベストプラクティス
コンポーザブル関数のガイドライン
// Use Modifier as first optional parameter
@Composable
fun CustomCard(
title: String,
modifier: Modifier = Modifier,
onClick: () -> Unit = {}
) {
Card(
modifier = modifier.clickable(onClick = onClick)
) {
Text(
text = title,
modifier = Modifier.padding(16.dp)
)
}
}
// Use slot APIs for flexible content
@Composable
fun CustomScaffold(
topBar: @Composable () -> Unit = {},
bottomBar: @Composable () -> Unit = {},
content: @Composable (PaddingValues) -> Unit
) {
Scaffold(
topBar = topBar,
bottomBar = bottomBar,
content = content
)
}
効率的な再コンポジション
// Use keys for list items
@Composable
fun UserList(users: List<User>) {
LazyColumn {
items(
items = users,
key = { it.id } // Stable key for efficient updates
) { user ->
UserItem(user)
}
}
}
// Use derivedStateOf for expensive computations
@Composable
fun FilteredList(items: List<Item>, query: String) {
val filteredItems by remember(items, query) {
derivedStateOf {
items.filter { it.name.contains(query, ignoreCase = true) }
}
}
LazyColumn {
items(filteredItems) { item ->
ItemRow(item)
}
}
}
副作用
// LaunchedEffect for coroutine-based side effects
@Composable
fun UserProfile(userId: String, viewModel: UserViewModel) {
LaunchedEffect(userId) {
viewModel.loadUser(userId)
}
// UI content
}
// DisposableEffect for cleanup
@Composable
fun LifecycleAwareComponent(lifecycle: Lifecycle) {
DisposableEffect(lifecycle) {
val observer = LifecycleEventObserver { _, event ->
// Handle lifecycle events
}
lifecycle.addObserver(observer)
onDispose {
lifecycle.removeObserver(observer)
}
}
}
// SideEffect for non-suspend side effects
@Composable
fun AnalyticsScreen(screenName: String) {
SideEffect {
analytics.logScreenView(screenName)
}
}
よく使われるパターン
Navigation Compose によるナビゲーション
@Composable
fun AppNavigation() {
val navController = rememberNavController()
NavHost(navController = navController, startDestination = "home") {
composable("home") {
HomeScreen(
onNavigateToDetail = { id ->
navController.navigate("detail/$id")
}
)
}
composable(
route = "detail/{itemId}",
arguments = listOf(navArgument("itemId") { type = NavType.StringType })
) { backStackEntry ->
val itemId = backStackEntry.arguments?.getString("itemId")
DetailScreen(itemId = itemId)
}
}
}
Material 3 のテーマ設定
@Composable
fun AppTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
content: @Composable () -> Unit
) {
val colorScheme = when {
darkTheme -> darkColorScheme(
primary = Purple80,
secondary = PurpleGrey80,
tertiary = Pink80
)
else -> lightColorScheme(
primary = Purple40,
secondary = PurpleGrey40,
tertiary = Pink40
)
}
MaterialTheme(
colorScheme = colorScheme,
typography = Typography,
content = content
)
}
// Using theme values
@Composable
fun ThemedCard() {
Card(
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceVariant
)
) {
Text(
text = "Themed content",
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
リストとグリッド
@Composable
fun ProductGrid(products: List<Product>) {
LazyVerticalGrid(
columns = GridCells.Adaptive(minSize = 160.dp),
contentPadding = PaddingValues(16.dp),
horizontalArrangement = Arrangement.spacedBy(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
items(products, key = { it.id }) { product ->
ProductCard(product)
}
}
}
// Sticky headers
@Composable
fun ContactList(contacts: Map<Char, List<Contact>>) {
LazyColumn {
contacts.forEach { (initial, contactsForInitial) ->
stickyHeader {
Text(
text = initial.toString(),
modifier = Modifier
.fillMaxWidth()
.background(MaterialTheme.colorScheme.surface)
.padding(16.dp),
style = MaterialTheme.typography.titleMedium
)
}
items(contactsForInitial) { contact ->
ContactItem(contact)
}
}
}
}
アンチパターン
コンポジション内での副作用を避ける
悪い例:
@Composable
fun BadExample(viewModel: ViewModel) {
viewModel.loadData() // Called on every recomposition!
Text("Data loaded")
}
良い例:
@Composable
fun GoodExample(viewModel: ViewModel) {
LaunchedEffect(Unit) {
viewModel.loadData()
}
Text("Data loaded")
}
remember ブロック内で状態を読み取らない
悪い例:
@Composable
fun BadCounter(initial: Int) {
// Won't update when initial changes
var count by remember { mutableStateOf(initial) }
}
良い例:
@Composable
fun GoodCounter(initial: Int) {
var count by remember(initial) { mutableStateOf(initial) }
}
コンポジション中の負荷の高い計算を避ける
悪い例:
@Composable
fun BadList(items: List<Item>) {
// Runs on every recomposition
val sorted = items.sortedBy { it.name }
LazyColumn { /* ... */ }
}
良い例:
@Composable
fun GoodList(items: List<Item>) {
val sorted by remember(items) {
derivedStateOf { items.sortedBy { it.name } }
}
LazyColumn { /* ... */ }
}
関連スキル
- android-architecture:MVVM とクリーンアーキテクチャのパターン
- android-kotlin-coroutines:Compose での非同期処理
コメント
Markdown 対応