Navigation3 時代の Destination 設計:sealed interface による型安全な実装パターンと使い分け

モダンな Android 開発において、Navigation はもはや単なる「画面の切り替え機」ではありません。

Destinationは、UIの状態やラベル、アイコンといったメタ情報を内包した、純粋な「型」として定義されるべきです。

ここでは、最新の Navigation ライブラリが目指す方向性に沿った、sealed interface による Destination 設計を提案します。

「シンプルさと拡張性」

このトレードオフをどう乗り越えるか、具体的なコード例と共に見ていきましょう。

 

🤔 共通の考え方:Destination = 型 + UIメタ情報

これまでの Navigation では String ベースの Route 管理が主流でしたが、これからの設計は

「型そのものに UI のメタ情報(ラベルやアイコンなど)を持たせる」

のが基本スタイルになります。

 

🤔 パターン 1:ネストする sealed interface

すべての Destination を一つの親インターフェースの中に閉じ込めるスタイルです。

実装イメージ

NavHost では AppDestination.xxx という形で指定します。

特徴

  • ◎ 視認性: 全ての画面遷移先が 1 ファイルにまとまっており、全体像を把握しやすい。
  • ◎ シンプル: 小〜中規模のアプリであれば、管理コストが最小限で済みます。
  • △ 拡張性: 全てが AppDestination に依存するため、機能(Feature)ごとにモジュールを分割しようとすると、循環参照が発生しやすくなります。

 

🤔 パターン 2:ネストしない(トップレベル) sealed interface

インターフェースを定義しつつ、各 Destination は独立したクラスとして定義するスタイルです。

実装イメージ

NavHost での記述はよりフラットになります。

特徴

  • ◎ 疎結合: 各 Destination を別ファイルや別モジュールに切り出しやすいため、Feature 単位の分割に強い。
  • ◎ 大規模向き: チーム開発でコンフリクトを避けやすく、ビルド速度向上のためのマルチモジュール化にも適しています。
  • △ 記述量: クラス名が重複しないよう xxxDestination と命名する必要があり、少し冗長に感じることがあります。

 

🤔 どちらを選ぶべきか?

設計の選択基準は非常にシンプルです。

 

🤔 まとめ

Navigation3 時代の Destination 設計の肝は
「型自体にメタ情報を持たせること」
です。

  • とりあえず作り始めるなら「ネスト型」
  • 将来的な機能拡張やモジュール化を見越すなら「非ネスト型」

アプリの規模と、将来どこまで成長させるかに合わせて選んでみてください。


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の第一歩)

ぐらいか。


5-Minute TLS/SSL Troubleshooting Playbook - IP-direct access only (curl / openssl)

 

🧑🏻‍💻 Introduction

When you go through DNS, you can be misled by:

  • caching
  • load balancers / CDNs
  • name-resolution mistakes

This guide standardizes all commands to IP-direct access + correct SNI so you can isolate the real cause quickly.

 

🧑🏻‍💻 Prerequisite Variables


DOMAIN=example.com 
IP=1.2.3.4

 

🧑🏻‍💻 Overall Flow


① Check reachability with curl (IP direct) 
    ↓ 
② Read certificate verification result 
    ↓ 
③ Get raw TLS data with openssl 
    ↓ 
④ Check certificate expiration 
    ↓ 
⑤ Verify SAN 
    ↓ 
⑥ Check intermediate certificate 
    ↓ 
⑦ Verify TLS versions

 

🧑🏻‍💻 ① HTTP Reachability (IP direct + SNI)


curl -v https://$DOMAIN \
 --resolve $DOMAIN:443:$IP \
 -o /dev/null

OK


* Connected to example.com (1.2.3.4) port 443
* SSL certificate verify ok.
< HTTP/1.1 200 OK

Failure


Connection refused

  • nginx / apache not running
  • closed port
  • firewall

 

🧑🏻‍💻 ③ Raw TLS Layer Information


openssl s_client \
 -connect $IP:443 \
 -servername $DOMAIN

OK


CONNECTED(00000003)
New, TLSv1.3, Cipher is TLS_AES_256_GCM_SHA384
Verify return code: 0 (ok)

 

🧑🏻‍💻 ④ Certificate Expiration


openssl s_client \
 -connect $IP:443 \
 -servername $DOMAIN 2>/dev/null \
 | openssl x509 -noout -dates


notAfter=May 2 23:59:59 2026 GMT

 

🧑🏻‍💻 ⑤ SAN (Domain Match)


openssl s_client \
 -connect $IP:443 \
 -servername $DOMAIN \
 | openssl x509 -noout -ext subjectAltName


DNS:example.com
DNS:www.example.com

 

🧑🏻‍💻 ⑥ Missing Intermediate Certificate Check


openssl s_client \
 -connect $IP:443 \
 -servername $DOMAIN \
 -showcerts

OK


Certificate chain
 0 s:CN = example.com
 1 s:C = US, O = Let's Encrypt, CN = R3

Missing


Certificate chain
 0 s:CN = example.com

→ fullchain.pem not configured

 

🧑🏻‍💻 ⑦ TLS Version Restrictions

TLS 1.2


curl --tlsv1.2 -v https://$DOMAIN \
 --resolve $DOMAIN:443:$IP \
 -o /dev/null

TLS 1.3


curl --tlsv1.3 -v https://$DOMAIN \
 --resolve $DOMAIN:443:$IP \
 -o /dev/null


unsupported protocol

→ ssl_protocols misconfiguration

 

🧑🏻‍💻 ⑧ Detect SNI Misconfiguration (intentionally omit it)


openssl s_client -connect $IP:443


subject=CN = default.example.net

→ default certificate returned
→ virtual host configuration issue

 

🧑🏻‍💻 Copy-Paste 5-Minute Diagnosis Set



DOMAIN=example.com 
IP=1.2.3.4 

curl -v https://$IP \
 -H "Host: $DOMAIN"\
 -o /dev/null 

openssl s_client -connect $IP:443 \
 -servername $DOMAIN -brief 

openssl s_client -connect $IP:443 \
 -servername $DOMAIN 2>/dev/null \
 | openssl x509 -noout -dates 

openssl s_client -connect $IP:443 \
 -servername $DOMAIN \
 | openssl x509 -noout -ext subjectAltName

 

🧑🏻‍💻 Root-Cause Shortcut Map


Cannot connect even with IP direct
 → server or firewall 

Verify error 
 → intermediate certificate 

Expired
 → certificate renewal missed

SAN mismatch
 → wrong certificate selected 

Different cert without SNI
 → virtual host configuration 

Only one of TLS1.2 / 1.3 fails
 → protocol restriction

 

🧑🏻‍💻 Summary

By eliminating DNS and fixing:

  • IP-direct access
  • correct SNI

your TLS troubleshooting speed improves dramatically.

This workflow is ready to copy-paste in real incidents.

👉 openssl-s_client - OpenSSL Documentation
👉 curl - SSL CA Certificates


[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.


Modernizing Android Build Scripts: Moving from "android { ... }" to "configure { ... }"

In the world of Android development, Kotlin DSL has become the standard for writing build scripts.

While the familiar android { ... } block works perfectly for simple projects, as your project grows and you start sharing build logic across multiple modules (e.g., using Convention Plugins), you might find it a bit limiting.

Today, we’ll look at why and how to switch to the more explicit and scalable configure<ApplicationExtension> syntax.

 

🧑🏻‍💻 1. Why Make the Switch?

The standard android { ... } block in build.gradle.kts is actually a "shorthand" provided by the Android Gradle Plugin (AGP). While convenient, using configure<T> offers several advantages:

  • Better Type Safety: By explicitly telling Gradle that "this block is an ApplicationExtension," the IDE (Android Studio) can provide more accurate code completion and error highlighting.
  • Scalable Build Logic: If you are moving common logic into buildSrc or external plugins to keep your Gradle files DRY (Don't Repeat Yourself), using the explicit extension type becomes essential for writing clean, reusable functions.

 

🧑🏻‍💻 2. The Transformation: Before vs. After

Let’s compare the standard approach with the explicit configuration style for an App module.

Before: The Standard android Block


// app/build.gradle.kts
android {
    compileSdk = 35
    defaultConfig {
        applicationId = "com.example.myapp"
        minSdk = 26
        targetSdk = 35
    }
}

After: Using configure<ApplicationExtension>
Note that you will need to import the ApplicationExtension class explicitly.


// app/build.gradle.kts
import com.android.build.api.dsl.ApplicationExtension

configure<ApplicationExtension> {
    compileSdk = 35
    defaultConfig {
        applicationId = "com.example.myapp"
        minSdk = 26
        targetSdk = 35
        // ...
    }
}

 

🧑🏻‍💻 3. Choosing the Right Extension Type

Not every module is an "Application."

You should choose the extension type that matches your module's purpose:

[!TIP]
Use CommonExtension when writing shared logic that applies to both your App and Library modules (like Java versioning or Compose options).

 

🧑🏻‍💻 4. Practical Implementation: Reusable Build Logic

The true power of this syntax shines when you extract common configurations into a function, such as in buildSrc.


// Example of a shared configuration function in buildSrc
import com.android.build.api.dsl.ApplicationExtension
import org.gradle.api.Project

fun Project.configureAndroidApplication() {
    extensions.configure<ApplicationExtension> {
        compileSdk = 35
        defaultConfig {
            minSdk = 26
            // ...other shared settings
        }
    }
}

By defining your build logic this way, your module-level Gradle files stay thin and highly maintainable.

 

🧑🏻‍💻 Conclusion

The traditional android { ... } block is great for its brevity. However, once your project reaches a certain scale and you start treating your build configuration as "real code," switching to configure is the way to go.

It brings better IDE support, type safety, and makes your build logic much easier to share across modules.