【Kotlin 2.4】ついに登場!コレクションリテラル(実験的サポート)と型推論の仕組みを徹底解説

Kotlin開発者の皆さん、お待たせしました!

2026年6月にリリースされた Kotlin 2.4 にて、待望の「コレクションリテラル(Collection Literals)」が実験的(Experimental)にサポートされました。

これまで listOf()mutableListOf()arrayOf() などの関数を使って記述していた配列やリストの生成が、ついにスクエアブラケット [] を使って、よりシンプルかつ直感的に書けるようになります。

この記事では、コレクションリテラルの導入方法から、最も重要な挙動である「文脈依存の型推論(Context-sensitive Type Inference)」について詳しく解説します。

 

🧑🏻‍💻 コレクションリテラルとは?

Kotlin 2.4.0からは、以下のようにブラケット [] を使ってコレクション(配列やリストなど)を簡潔に作成できるようになります。


// Kotlin 2.4からの新しい書き方(リテラル表記)
val names = ["Joe", "Alice"]

従来の listOf("Joe", "Alice") と比べてタイピング量が減り、他言語(Javaの配列リテラルや、JavaScript/TypeScript/Pythonなどの配列・リスト表記)に慣れている開発者にとっても、より親しみやすいコードになります。

 

🧑🏻‍💻 導入方法(実験的サポートの有効化)

Kotlin 2.4時点では、この機能はまだ実験的(Experimental)な位置づけです。そのため、プロジェクトで利用するにはコンパイラオプションで明示的に機能を有効化する必要があります。

build.gradle.kts に以下の設定を追加してください。


kotlin {
    jvmToolchain(21)
    compilerOptions {
        // コレクションリテラルを有効化するコンパイラ引数
        freeCompilerArgs.add("-Xcollection-literals")
    }
}

 

🧑🏻‍💻 注目すべき「型推論」の挙動

コレクションリテラルを使う上で、最も興味深いのが「コンパイラがどのように型を推論するか」という点です。

Kotlinのコレクションリテラルは、単一の固定された型を表すのではなく、周囲の文脈(期待される型)に応じて最終的な型が変化する「文脈依存(Context-sensitive)」の性質を持っています。

具体的なコードで比較してみましょう。

1. 明示的な型指定がない場合

ターゲットとなる型を何も指定せずにリテラルを書いた場合、コンパイラはデフォルトで Array(配列) と推論します。


val names = ["Joe", "Alice"]
// 推論される型: Array<String>

2. 期待される型(型注釈)がある場合

変数の型を明示的に指定すると、リテラルの中身は同じ [] であっても、コンパイラが文脈を読み取って適切なコレクション型へと変換してくれます。


val names: MutableList<String> = ["Joe", "Alice"]
// 推論される型: MutableList<String>

このように、左辺の MutableList<String> という情報をコンパイラが解釈し、[] の部分を適切に MutableList として扱ってくれるのが、今回の型推論の面白いところです。

 

🧑🏻‍💻 まとめ:Kotlinのコードはさらに洗練される

Kotlin 2.4のコレクションリテラルは、ただの「書き方の省略」にとどまらず、Kotlinの強力な型推論エンジンとシームレスに融合している点が大きな特徴です。

現在はまだ実験的な機能であるため、プロダクション環境への導入には慎重になる必要がありますが、将来的に正式機能となれば、Kotlinのコードをさらにモダンでスッキリとしたものに変えてくれることは間違いありません。

興味のある方は、ぜひコンパイラオプションを追加して、手元のプロジェクトで新しい書き味を試してみてください!


Kotlin by JetBrains「Jake Wharton | KotlinConfersations'26」の文字起こし

Android開発やKotlinコミュニティのキーパーソンであるジェイク・ウォートン(Jake Wharton)氏へのインタビューです。

 

🤔 1. 自己紹介とKotlinとの歩み [00:26]

経歴:
長年Androidデベロッパーとして活動。Square(現Block)、Cash Appを経て、現在はSkylightに在籍。

Kotlinとの出会い:
Square時代、Java 7の機能不足やGoogleのツール進化の遅さに悩んでいた「プレ1.0(正式リリース前)」の頃にKotlinに注目。社内向けに導入提案書(プロポーザル)を作成。

反響:
その提案書を公開したところコミュニティで大きな話題となり、SquareでのKotlin導入だけでなく、エコシステム全体の盛り上がりに繋がった。

 

🤔 2. AndroidにおけるKotlinとコミュニティの力 [01:50]

Googleでの活動:
一時期Googleに籍を置き、AndroidにおけるKotlinの公式サポート(KTXライブラリなどの立ち上げ)を支援。

エコシステムの進化:
KTXライブラリのコード自体は現在通常のライブラリに統合されて役目を終えたが、それは言語が成熟した良い証拠であると語る。

Apple(Swift)とのアプローチの違い:
iOSのSwiftがAppleという中央集権からトップダウンで提供されたのに対し、AndroidにおけるKotlinは「草の根(グラスルーツ)的なコミュニティの熱意」から始まり、最終的にGoogleが公式サポートせざるを得ない流れを作った点がユニークである。

非Androidへの広がり:
「KotlinはGoogle製でも、Android専用でもない」という点が、15年経った今ではAndroid以外の開発者にも広く認知されるようになった。

 

🤔 3. Kotlin Multiplatform (KMP) の挑戦 [05:35]

これまでのクロスプラットフォーム:
過去のXamarinやPhoneGap、現代のReact NativeやFlutterなどを評価した上で、Cash App時代にKotlin Multiplatform(KMP)とCompose Multiplatformを選択。

アプローチの特徴:
UI層はiOSならUIKit/SwiftUI、WebならHTML DOM、AndroidならCompose UIといった「各プラットフォーム独自のネイティブビュー」を尊重し、バックエンドのビジネスロジックやプレゼンターロジックのみを共通化(シェア)する方針をとった。

KMPの強み:
他のクロスプラットフォーム言語と異なり、WebならJavaScript、iOSならネイティブコード、AndroidならJavaバイトコードと、ターゲットごとに最適な形へコンパイルされるため、メモリ空間の競合や相互運用の壁(Interop layer)が少ない。

エコシステムへの貢献:
5年前に始めた当時はJetBrainsのComposeも初期段階だったため、自分たちで足りないターゲットを作るなどしてエコシステムを強力にプッシュした。

 

🤔 4. オープンソース(OSS)の重要性 [10:53]

キャリアへの影響:
オープンソースに貢献することで、自身のスキルアップだけでなく、新しい人との出会いや転職の機会など、キャリアの節目で何度も救われた。

企業とOSSの関係:
REST APIとの通信や画像読み込み、依存関係の解決(DI)など、どのアプリでも共通する「ビジネスの知的財産(IP)ではない部分」のコードはオープンにすべき。結果的に会社の採用活動(「OSSを見て応募した」という優秀な人材の獲得)など、数字に表れにくい大きなリターン(無形の資産)をもたらす。

 

🤔 5. 生成AI(LLM)とライブラリの未来 [15:11]

「AIがコード生成できるならライブラリ投資は不要か?」という問いへの持論:
「コードを書くこと(Writing code)」自体は、ソフトウェア開発において決して最も難しい部分ではない。AIは既存のスキルを加速させる(アクセラレーター)ツールとして使うべきであり、それなしではコードが書けないような依存の仕方はリスクがある(将来的なコスト高騰も含めて)。

コードは書かれる回数より、読まれる回数の方が10倍多い(Code is read 10 times more than it's written)。 要件は常に変わるため、全体を俯瞰して理解できるコード設計が重要。

ライブラリの役割:
ライブラリの真の価値は「再利用可能なコードの境界線(デリミテーション)」を明確に引き、人間の認知負荷を下げることにある。パレートの法則のように「80〜85%の共通ユースケース」を綺麗にカプセル化することが大切。人間にとって直感的で優れた設計は、言語モデル(AI)にとっても扱いやすいはずである。

 

🤔 6. 2026年現在のKotlinへの期待 [22:45]

K2コンパイラの恩恵:
長年開発が続けられてきた「K2コンパイラ」への移行(大きな山場)を乗り越えたことで、言語自体の進化スピードが再び加速している。

注目している新機能:
Rich Errors(エラー処理の改善)プロポーザルの刷新
未使用の戻り値チェッカー(Unused return value checker)
when 式のデフォルト網羅性(Exhaustive when by default)

現在の心境:
K2の開発に全力を注いでいた停滞期を抜け出し、標準ライブラリ(datetimeやco-routinesなど)や言語プロポーザルが活発に進化している今の状況は、初期のKotlinのワクワク感を思い出させる。

 

🤔 7. コミュニティへの参加に気後れしている人へのアドバイス [27:30]

提案書(KEEP)の難しさ:
KEEP(Kotlin Evolution and Enhancement Process)はコンパイラの構文解析や各プラットフォームのバイトコードまで考慮しなければならず、内容が非常に高度で圧倒されるのは当然。

おすすめの関わり方:
Kotlin公式Slack(Kotlinlang Slack)にある language-proposal や language-evolution チャンネルを覗いてみるのがおすすめ。そこでは、より人間的で身近な困りごとや質問が、消化しやすい形で活発に議論されている。


SavedInstanceState 不要!? Navigation3 の NavKey がもたらす Jetpack Compose 開発の変革

Android 開発者を長年悩ませてきた「画面回転」や「プロセス死」に伴う状態保存。SavedInstanceStateSavedStateHandle と格闘する日々は、もう過去のものになろうとしています。

Jetpack Navigation3 のソースコードを読み解くと、新しく登場した NavKey という仕組みが、Compose 時代の状態保存をいかにスマートに変革しようとしているかが見えてきました。

 

🤔 1. NavKey:単なる「マーカー」ではない、型安全の要


/**
 * Marker interface for keys.
 *
 * Objects and classes that extend this class must be marked with the [Serializable] annotation in
 * order to be saved with by the [rememberNavBackStack] function.
 *
 * This class is required because [Serializable] is only an annotation and does not provide a way to
 * link classes marked with the annotation together and provide a serializable that works with all
 * of them, resulting it making it impossible to properly save and restore.
 */
public interface NavKey

Navigation3 でバックスタックを管理する rememberNavBackStack を使う際、避けて通れないのが NavKey インターフェースの実装です。一見すると中身のないマーカーインターフェースですが、これこそが 「型安全な自動保存」 の鍵を握っています。

従来の Navigation では、引数の受け渡しや状態保存の裏側で Bundle が使われてきました。しかし、Bundle は何でも入る反面、型安全性が欠け、実行時のエラーを招きやすいという課題がありました。


/**
 * Provides a [NavBackStack] that is automatically remembered in the Compose hierarchy across
 * process death and configuration changes.
 *
 * This overload **does not take a [SavedStateConfiguration]**. It relies on the platform default:
 * on **Android**, state is saved/restored using a **reflection-based serializer**; on **other
 * platforms this will fail at runtime**. If you target non-Android platforms, use the overload that
 * accepts a [SavedStateConfiguration] and register your serializers explicitly.
 *
 * ### When to use this overload
 * - You are on **Android only** and want a simple API that uses reflection under the hood.
 * - Your back stack elements use **closed polymorphism** (sealed hierarchies) or otherwise work
 *   with Android’s reflective serializer.
 *
 * ### Serialization requirements
 * - Each element placed in the [NavBackStack] must be `@Serializable`.
 * - For **closed polymorphism** (sealed hierarchies), the compiler knows all subtypes and generates
 *   serializers; Android’s reflection will also work.
 * - For **open polymorphism** (interfaces or non-sealed hierarchies):
 *     - On Android, the reflection path can handle subtypes without manual registration.
 *     - On non-Android, this overload is **unsupported**; use the configuration overload and
 *       register all subtypes of [NavKey] in a [SerializersModule].
 *
 * @sample androidx.navigation3.runtime.samples.rememberNavBackStack_withReflection
 * @param elements The initial keys of this back stack.
 * @return A [NavBackStack] that survives process death and configuration changes on Android.
 * @see NavBackStackSerializer
 */
@Composable
public fun rememberNavBackStack(vararg elements: NavKey): NavBackStack<NavKey> {
    return rememberSerializable(
        serializer = NavBackStackSerializer(elementSerializer = NavKeySerializer())
    ) {
        NavBackStack(*elements)
    }
}

NavKey は、バックスタックに入るすべての要素が「シリアライズ可能であること」をコンパイル時に保証するための型上の制約として機能します。

 

🤔 2. なぜ「Serializable アノテーション」だけでは不十分か

Kotlin Serialization を使っているなら、@Serializable だけで十分だと思うかもしれません。しかし、ソースコードのコメントには重要な洞察が記されています。

Serializable はあくまでアノテーションであり、クラス同士をコード上でリンクさせる手段を提供しない。そのため、すべての要素を適切に保存・復元することが不可能になる。

NavKey という共通のインターフェースを介することで、Navigation 側は「このバックスタックの中身はすべて、共通の手順でシリアライズできる仲間である」と認識できるようになります。これが、「何も考えなくても状態が復元される」 という体験の裏側にあるロジックです。

 

🤔 3. Android 限定の「リフレクション」がもたらす魔法

Navigation3 の rememberNavBackStack には、Android 開発者にとって非常に強力な恩恵があります。

Android プラットフォーム上では、リフレクションベースのシライライザーがデフォルトで動作します。これにより、開発者はシリアライザーを個別に登録する手間(ボイラープレート)から解放されます。

  • 開発者: NavKey を継承し、@Serializable を付ける。
  • Navigation3: process death の際、リフレクションを使ってバックスタックをまるごと保存し、復帰時に自動で復元する。

これこそが、「さらば SavedInstanceState」と言える最大の理由です。

 

🤔 4. Navigation3 時代の実装スタイル

これからの Compose 開発では、以下のように NavKey を実装した sealed interface を定義するのが標準になるでしょう。


@Serializable
sealed interface Screen : NavKey {
    @Serializable
    data object Home : Screen

    @Serializable
    data class UserProfile(val userId: String) : Screen
}

// Composable 内での利用
val backStack = rememberNavBackStack(Screen.Home)

この実装だけで、userId を含めた遷移状態が、OSによるメモリ回収の後でも完璧に復元されます。もはや Bundle を意識するシーンはほとんどなくなるはずです。

 

🤔 まとめ

Navigation3 における NavKey の導入は、Google が掲げる開発思想の現れのように感じます。

内部の複雑なシリアライズ処理を NavKey という一つのインターフェースに集約し、開発者には最小限の実装(アノテーションと継承)だけで最大限の恩恵(自動状態保存)を与える。

「状態保存に怯える開発」はもう終わりです。Navigation3 を武器に、より本質的な 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.


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.