Jetpack Compose Foundation サンプル目次リンク

Jetpack Composeの核心を担う androidx.compose.foundation

そのサンプルコード群は、Googleのエンジニアが「正しい書き方」を提示している宝庫です。

今回は、これらを実務での利用頻度とモダンな設計(2026年現在のトレンド)に基づいてグループ分けしました。

 

🧑🏻‍💻 1. インタラクション & ジェスチャー(操作感のキモ)

ユーザーが画面に触れた時の挙動を制御する、最も重要なグループです。

 

🧑🏻‍💻 2. スクロール & リスト(データの表示)

効率的にスクロールさせるためのテクニック集です。

 

🧑🏻‍💻 3. テキスト & 入力(文字の表示と編集)

2026年のトレンドである「次世代入力」が含まれます。

 

🧑🏻‍💻 4. 描画 & 視覚効果(見た目のクオリティ)

 

🧑🏻‍💻 5. 高度なシステム統合・同期

 

🧑🏻‍💻 これだけは読んでおくべきトップ5

1. ClickableSamples.kt(すべての基本)
2. LazyDslSamples.kt(リスト表示の要)
3. AnchoredDraggableSample.kt(モダンなUIに必須)
4. BasicTextFieldSamples.kt(入力の実装)
5. CanvasSamples.kt(カスタムUIの第一歩)

ぐらいか。


[Jetpack Compose] Implement "Pull-to-Refresh" with the New PullToRefreshBox

The "Pull-to-Refresh" gesture is a staple in Android app UI.

While we previously relied on Modifier.pullRefresh, Jetpack Compose has introduced PullToRefreshBox in Material 3 as the new standard. It's more intuitive and requires much less boilerplate code.

In this post, we’ll quickly cover everything from basic implementation to customization!

 

🧑🏻‍💻 1. Prerequisites

PullToRefreshBox is available in Material 3 (version 1.3.0 or later).

Make sure to check your build.gradle dependencies:


dependencies {
    implementation("androidx.compose.material3:material3:1.3.0")
}

 

🧑🏻‍💻 2. Basic Implementation Pattern

The best part about PullToRefreshBox is that it encapsulates both the refresh logic and the indicator UI into a single component.


@Composable
fun RefreshableListScreen() {
    var isRefreshing by remember { mutableStateOf(false) }
    val scope = rememberCoroutineScope()
    val items = remember { mutableStateListOf("Initial Item A", "Initial Item B") }

    PullToRefreshBox(
        isRefreshing = isRefreshing,
        onRefresh = {
            scope.launch {
                isRefreshing = true
                // Perform your refresh logic (e.g., API calls)
                delay(2000) 
                items.add(0, "New Item ${items.size + 1}")
                isRefreshing = false
            }
        }
    ) {
        LazyColumn(Modifier.fillMaxSize()) {
            items(items) { item ->
                ListItem(headlineContent = { Text(item) })
            }
        }
    }
}

Key Highlights

  • isRefreshing: A boolean that controls the visibility of the refresh indicator.
  • onRefresh: The callback triggered when the user performs the pull gesture.
  • Content Size: Ensure your scrollable content (like LazyColumn) uses Modifier.fillMaxSize() so the pull gesture is detectable across the entire area.

 

🧑🏻‍💻 3. Practical Usage with ViewModel

In a production environment, it's best practice to let a ViewModel handle the state.


class MyViewModel : ViewModel() {
    var isRefreshing by mutableStateOf(false)
        private set

    fun refreshData() {
        viewModelScope.launch {
            isRefreshing = true
            // Simulate network call
            isRefreshing = false
        }
    }
}

val viewModel: MyViewModel = viewModel()
PullToRefreshBox(
    isRefreshing = viewModel.isRefreshing,
    onRefresh = { viewModel.refreshData() }
) {
    // ... Content
}

 

🧑🏻‍💻 4. Customizing the Design

If you want to change the indicator's color to match your brand, use the indicator parameter.


PullToRefreshBox(
    isRefreshing = isRefreshing,
    onRefresh = { /* ... */ },
    indicator = {
        PullToRefreshDefaults.Indicator(
            state = it,
            isRefreshing = isRefreshing,
            containerColor = Color.DarkGray, // Background color
            color = Color.Cyan              // Progress spinner color
        )
    }
) {
    // ...
}

 

🧑🏻‍💻 Conclusion: Simplified Refresh Logic

With the arrival of PullToRefreshBox, implementing this common UI pattern has never been easier.

  • Use Material 3 1.3.0+.
  • Pass the state (isRefreshing).
  • Handle the logic in onRefresh.

That’s it! You now have a modern, native-feeling refresh experience.


Mastering Screen Lifecycle in Jetpack Compose Navigation

When developing with Jetpack Compose, a common challenge is detecting when a screen becomes visible (to refresh data) or when it moves to the background (to pause a video).

While Compose provides onDispose, this only triggers when a Composable is completely removed from the UI tree. It cannot detect when a screen is still in the backstack but no longer visible to the user.

In this post, we’ll explore how to leverage the fact that Navigation Compose uses NavBackStackEntry as a LifecycleOwner to perfectly manage screen-level events.

 

🧑🏻‍💻 1. The Core: Who is the LocalLifecycleOwner?

In Compose, you can access the current lifecycle via LocalLifecycleOwner.current. However, its identity changes depending on your app's architecture:

  • Directly under an Activity: LifecycleOwner = Activity
  • Using Navigation Compose: LifecycleOwner = NavBackStackEntry

When using Navigation Compose, each destination is wrapped in a NavBackStackEntry. When you navigate from Screen A to Screen B, the Activity remains RESUMED, but Screen A’s NavBackStackEntry transitions to the STOPPED state.

By monitoring this, you can capture lifecycle events specific to that individual screen.

 

🧑🏻‍💻 2. The Solution: Implementing ScreenLifecycleObserver

To make this reusable, we can create a custom Composable function that observes these state changes safely.

 

🧑🏻‍💻 3. Real-World Patterns

A. Refreshing Data on Screen Return

Using LaunchedEffect(Unit) only runs once when the screen is first created. If you want to refresh data every time a user navigates back to the screen, use ON_RESUME.

B. Pausing and Resuming Video

Automatically pause video when the user navigates away or minimizes the app, and resume it when they return.

C. Tracking Screen Time (Analytics)

Start a timer on ON_RESUME and send the duration on ON_PAUSE.

 

🧑🏻‍💻 Summary: When to Use What?

Finally, let's distinguish between onDispose and Lifecycle events:

If you are using Navigation Compose, LocalLifecycleOwner.current is a powerful tool. Using it correctly ensures a robust app that respects system resources and provides a seamless user experience.

I hope this guide helps you manage screen lifecycles in your Compose projects!


Ending the Event Management Debate in ViewModel: The "MVI-style" Best Practice using StateFlow and Channel

 

🧑🏻‍💻 Introduction

When streaming data from a ViewModel to the UI, do you ever struggle with how to handle one-time events like "screen navigation" or "showing a Toast"?

It’s tempting to think, "Why not just combine everything into one state?" However, this often leads to a common pitfall: event re-emission bugs.

Today, I’ll introduce a robust, boilerplate-friendly design pattern: "State = combine / Effect = merge."

 

🧑🏻‍💻 1. Separating State from Effect

First, let’s categorize UI elements into two distinct types based on their behavior:

  • UiState (State): Represents the current look of the screen. It must always hold a "latest value" (e.g., loading flags, usernames, input fields).
  • UiEffect (Side Effect): Represents momentary occurrences. These should be processed once and then forgotten (e.g., navigation, error alerts, snackbars).

 

🧑🏻‍💻 2. ViewModel Implementation: Choosing between combine and merge

In the ViewModel, we use different operators depending on the nature of the data flow.


class UserProfileViewModel(private val repository: UserRepository) : ViewModel() {

    // --- [State] Synthesizing the latest state ---
    // We combine multiple sources (Loading, User data, etc.) 
    // to ensure the UI always has a consistent "single frame" of data.
    private val _isLoading = MutableStateFlow(false)
    val uiState: StateFlow<UserProfileState> = combine(
        _isLoading, 
        repository.userData // Flow<User>
    ) { loading, user ->
        UserProfileState(userName = user.name, isLoading = loading)
    }.stateIn(
        scope = viewModelScope, 
        started = SharingStarted.WhileSubscribed(5000), 
        initialValue = UserProfileState()
    )

    // --- [Effect] Integrating independent events ---
    // Use a Channel for one-shot events and merge them into a single flow 
    // to pipe everything through a single "event bus" to the UI.
    private val navigationEvents = Channel<UserProfileEffect.Navigate>()
    private val toastEvents = Channel<UserProfileEffect.ShowToast>()

    val uiEffect: Flow<UserProfileEffect> = merge(
        navigationEvents.receiveAsFlow(),
        toastEvents.receiveAsFlow()
    )

    fun onUpdateClick() {
        viewModelScope.launch {
            _isLoading.value = true
            if (repository.update()) {
                navigationEvents.send(UserProfileEffect.Navigate("home"))
            } else {
                toastEvents.send(UserProfileEffect.ShowToast("Update failed"))
            }
            _isLoading.value = false
        }
    }
}

Why differentiate them?

  • Why combine for State? The UI must always be consistent. Even if only one value changes, combine re-emits the set of all "latest values," preventing the UI from showing incomplete data.
  • Why merge for Effect? If you use combine for events, a simple update to a State (like a loading spinner) would trigger a re-emission of the previous navigation event. merge ensures that only the event that just happened gets triggered.

 

🧑🏻‍💻 3. Handling Events in the View (Compose)

On the UI side, we handle these flows using methods tailored to their specific lifecycles.


@Composable
fun UserProfileScreen(viewModel: UserProfileViewModel) {
    // 1. Observe State: Automatically updates the UI and respects lifecycle
    val uiState by viewModel.uiState.collectAsStateWithLifecycle()

    // 2. Consume Effect: Use LaunchedEffect to handle events exactly once
    LaunchedEffect(viewModel.uiEffect) {
        viewModel.uiEffect.collect { effect ->
            when (effect) {
                is UserProfileEffect.Navigate -> navController.navigate(effect.route)
                is UserProfileEffect.ShowToast -> showToast(effect.message)
            }
        }
    }

    // 3. Render UI: Simply follow the uiState
    ProfileContent(uiState) 
}

 

🧑🏻‍💻 Summary: Why this Pattern Wins

  • Unidirectional Data Flow (UDF): It clearly separates "State flowing down" from "Events flowing up."
  • Bug Prevention: It structurally prevents issues like "Toasts reappearing on screen rotation" or "double navigation."
  • Clean Code: The UI processes all events in a single when block, and the ViewModel keeps concerns neatly separated.

If you find your event management getting messy, give the State (combine) & Effect (merge) pattern a try!

👉 Android アーキテクチャの現在地:Google が推奨する UDF と、現場が選ぶ「MVI 風 MVVM」
👉 モダンUI開発の決定版: State / Effect / Event で作る「迷わない」画面実装


Why are updates to Kotlin, Compose, and KSP such a hassle?

In Android development, you're constantly dealing with the same set of three: Kotlin, the Compose Compiler, and KSP.

They seem like a friendly group, but their update schedules are always completely different! You upgrade Kotlin, and the Compose Compiler isn't compatible. You change something, and KSP throws a build error because of an internal API change...

To better manage this "dependency triangle" situation, the main idea is to use Renovate's configuration to treat them as a single unit. The simple plan is: "Raise all Kotlin ecosystem dependencies at the same time!"

 

🧑🏻‍💻 Brief overview of the renovate.json file


{
  "$schema": "https://docs.renovatebot.com/renovate-schema.json",
  "extends": [
    "config:base",
    "group:all",
    ":dependencyDashboard",
    "schedule:daily"
  ],
  "baseBranches": ["main"],
  "commitMessageExtra": "{{{currentValue}}} to {{#if isPinDigest}}{{{newDigestShort}}}{{else}}{{#if isMajor}}{{prettyNewMajor}}{{else}}{{#if isSingleVersion}}{{prettyNewVersion}}{{else}}{{#if newValue}}{{{newValue}}}{{else}}{{{newDigestShort}}}{{/if}}{{/if}}{{/if}}{{/if}}",
  "packageRules": [
    {
      "matchPackagePatterns": ["androidx.compose.compiler:compiler"],
      "groupName": "kotlin"
    },
    {
      "matchPackagePatterns": ["org.jetbrains.kotlin.*"],
      "groupName": "kotlin"
    },
    {
      "matchPackagePatterns": ["com.google.devtools.ksp"],
      "groupName": "kotlin"
    }
  ]
}

👉 architecture-samples/renovate.json at main · android/architecture-samples

Roughly summarized, here are the key points:

  • groupName: "kotlin" to bundle dependencies This setting specifies that the three elements—the Compose Compiler, Kotlin, and KSP—should be treated as belonging to the "same group." This allows Renovate to update them all together at once.
  • schedule: daily for a calm update pace This checks for updates once a day. You'll receive pull requests (PRs) on a daily basis, preventing a huge influx of dependency updates all at once, which makes things much easier to manage.
  • commitMessageExtra to see changes at a glance The version difference, like "2.0.10 → 2.0.20," is automatically added to the PR title. It's a small tweak, but surprisingly useful.

Setting up your configuration this way significantly reduces the tragedy of "Kotlin got updated, but Compose broke..."

 

🧑🏻‍💻 What We Found While Using It

Once this setup is in place, you can feel much more confident testing updates for everything Kotlin-related. Renovate diligently checks daily, automatically creating a PR whenever a new version drops.

But there's one small warning:

The Compose Compiler sometimes takes a little extra time to catch up to the latest Kotlin version. So, don't just merge the PR when you see it—it's highly recommended to verify the CI status first.

KSP is similar; because it depends on Kotlin's internal workings, it's safer to update it along with Kotlin and run your tests together.

 

🧑🏻‍💻 Summary: Teach Renovate that "These Three Are a Set"

The configuration we discussed treats the trio of Kotlin, the Compose Compiler, and KSP as a single group.

  • Bundle all Kotlin-related dependencies for simultaneous updates.
  • Check for updates at a manageable daily pace.
  • See version differences directly in the PR title.

Just implementing this significantly reduces the problems caused by versions getting out of sync and breaking your build.

💡 Key Takeaway: Use Renovate less as an "automatic update tool" and more as a "dependency rulebook."

We simply need to tell Kotlin, Compose, and KSP to cooperate and "work together."

👉 Kotlin・Compose・KSP の更新、どうしてこんなに面倒なの?