Jetpack Composeで DisposableEffect に任せるべき処理

Jetpack Compose では UI のライフサイクル管理がかなり簡単になりました。

しかし、それでも明示的な後始末が必要な処理があります。

たとえば、

・ Listener を登録する
・ Connection を開始する
・ 外部リソースを取得する
・ 後から停止する必要がある処理を開始する
・ UI が消えたときに登録解除やリソース解放を行う

といった処理です。

このような処理を Composition のライフサイクルに結び付けるのが DisposableEffect です。

基本的には、次のように考えると分かりやすいです。


Composition に入る
       ↓
register / start / acquire
       ↓
Composition に存在している間
       ↓
Composition から出る
       ↓
onDispose
       ↓
unregister / stop / release

 

🤔 remember はリソースを解放しない

例えば、次のコードを考えてみます。


@Composable
fun PlayerScreen() {
    val player = remember {
        MediaPlayer()
    }
}

remember は再コンポジションをまたいでオブジェクトを保持します。

しかし、MediaPlayer が何なのか、いつ release() すべきなのかまでは知りません。

明示的な解放の API を持つオブジェクトなら、その解放処理を適切なライフサイクルに結び付ける必要があります。


@Composable
fun PlayerScreen() {
    val player = remember {
        MediaPlayer()
    }

    DisposableEffect(player) {
        onDispose {
            player.release()
        }
    }
}

ここで重要なのは、


remember
  ↓
オブジェクトを保持する


DisposableEffect
  ↓
オブジェクトに関連する副作用のライフサイクルを管理する

という違いです。

つまり、remember 自体がメモリーリークを起こすわけではありません。

問題になるのは、明示的な解放が必要なリソースを、適切なタイミングで終了させないことです。

 

🤔 よくあるパターン

DisposableEffect を使う処理は、ほとんど同じ形になります。


DisposableEffect(key) {

    resource.register()

    onDispose {
        resource.unregister()
    }
}

名前は変わっても、考え方は同じです。


// LifecycleObserver

DisposableEffect(lifecycle) {
    lifecycle.addObserver(observer)

    onDispose {
        lifecycle.removeObserver(observer)
    }
}


// BroadcastReceiver

DisposableEffect(receiver) {
    context.registerReceiver(receiver, filter)

    onDispose {
        context.unregisterReceiver(receiver)
    }
}


// Sensor

DisposableEffect(sensor) {
    sensorManager.registerListener(
        listener,
        sensor,
        SensorManager.SENSOR_DELAY_NORMAL
    )

    onDispose {
        sensorManager.unregisterListener(listener)
    }
}


// Location

DisposableEffect(locationCallback) {
    locationClient.requestLocationUpdates(
        request,
        locationCallback,
        Looper.getMainLooper()
    )

    onDispose {
        locationClient.removeLocationUpdates(locationCallback)
    }
}


// NetworkCallback

DisposableEffect(networkCallback) {
    connectivityManager.registerNetworkCallback(
        request,
        networkCallback
    )

    onDispose {
        connectivityManager.unregisterNetworkCallback(networkCallback)
    }
}


// MediaPlayer

val player = remember {
    MediaPlayer()
}

DisposableEffect(player) {
    onDispose {
        player.release()
    }
}


// WebSocket

val socket = remember {
    createWebSocket()
}

DisposableEffect(socket) {
    socket.connect()

    onDispose {
        socket.close()
    }
}


// Listener

DisposableEffect(manager) {
    manager.addListener(listener)

    onDispose {
        manager.removeListener(listener)
    }
}


// TextWatcher

DisposableEffect(textView) {
    textView.addTextChangedListener(watcher)

    onDispose {
        textView.removeTextChangedListener(watcher)
    }
}

 

🤔 ViewModel の場合はどうなる?

Composition が所有するリソースなら、DisposableEffect で解放できます。


Composition
    │
    └── resource
          │
          └── DisposableEffect
                  ↓
               onDispose()

一方、ViewModel が所有するリソースは、ViewModel のライフサイクルに従います。


ViewModel
    │
    └── resource
          │
          └── onCleared()

例えば、


class PlayerViewModel : ViewModel() {

    private val player = MediaPlayer()

    override fun onCleared() {
        player.release()
        super.onCleared()
    }
}

ViewModel がリソースを保持すること自体に問題があるわけではありません。

重要なのは、

「このリソースの所有者は誰で、その所有者の寿命はいつ終わるのか?」

ということです。

MediaPlayerが 画面に属するなら、DisposableEffect は自然な選択です。

また、Composition が破棄されても再生を継続したいなど、MediaPlayer を ViewModel の寿命まで保持したいのであれば、ViewModel が所有する設計も考えられます。

 

🤔 メモリーリークとは別の話

ここで、リソースリーク と メモリーリーク を混同しないことも重要です。

典型的なメモリーリークは、長く生存するオブジェクトが、本来なら破棄されるはずのオブジェクトを参照し続けることで起こります。

例えば、ViewModel が Activity への参照を保持しているとします。

Activity が画面から破棄された後も ViewModel がその Activity を参照していれば、Activity を GC できなくなる可能性があります。

これはメモリーリークです。

一方、リソースリークは少し違います。


Object
   │
   └──→ MediaPlayer
            │
            └── 外部リソース

もう使わないのに release() を呼ばなければ、外部リソースを明示的に解放していないことが問題になります。

両者が同時に発生することはありますが、同じものではありません。

 

🤔 気づくための簡単なルール

Composable の中に、次のような処理があったら考えてみてください。

・ register()
・ addListener()
・ connect()
・ start()
・ acquire()

そして、

「これに対応するクリーンアップはどこにある?」

と考えます。

例えば、


register()    →  unregister()
addListener() →  removeListener()
connect()     →  close()
start()       →  stop()
acquire()     →  release()

です。

もし、その処理が Composable が Composition に存在している間だけ必要なら、DisposableEffect は有力な選択肢です。

 

🤔 DisposableEffect の本質

DisposableEffect は、Composition のライフサイクルに合わせて副作用の開始と終了を管理します。

remember:
「再コンポジションをまたいで オブジェクトを保持する」

DisposableEffect:
「Compositionの存在期間に合わせて副作用を管理する」

onDispose:
「もう必要ないので明示的に後始末する」

結局、DisposableEffect を使うかどうかを考えるときに重要なのは、

「このリソースの所有者は誰で、その寿命はいつ終わるのか?」

という1つの質問です。

この視点を持つと、DisposableEffect を単なる API としてではなく、リソースのライフサイクルを Composition に接続するための仕組みとして理解できます。


iPhone・Android・PC 間でデータを共有するなら、Google Keep が便利

 

🧑🏻‍💻 iPhone・Android・PC でデータを送りたい

スマートフォンとPCの間で、ちょっとしたデータを送りたいことがあります。

例えば、


iPhone → Android
Android → iPhone
iPhone → Windows PC
Android → Mac
PC → iPhone
PC → Android

といったケースです。

同じOSなら、AirDropやQuick Share など便利な方法があります。

しかし、iPhone・Android・Windows・Mac が混在すると、少し面倒です。

そこで便利なのが、Google Keep です。

 

🧑🏻‍💻 Google KeepならOSを気にしなくていい

Google Keep は、Android だけのアプリではありません。

スマートフォンだけでなく、PCのブラウザからも利用できます。


          Google Keep
               │
   ┌───────────┼───────────┐
   │           │           │
Android      iPhone      PC / Mac
   │           │           │
   └───────────┼───────────┘
               │
         Google Account

つまり、Google Keepを共有場所にすることで、OSの違いをあまり意識せずに済みます。

 

🧑🏻‍💻 Googleアカウントがあればいい

Google Keep の大きなメリットは、Google アカウントを使えることです。

例えば PC で調べた URL をスマートフォンに送りたいとします。


PC
 │
 │ URLをKeepに保存
 ▼
Google Keep
 │
 │ 同期
 ▼
iPhone / Android

スマートフォン側でGoogle Keepを開けば、そのURLを確認できます。

逆に、


iPhone
   │
   │ テキストを保存
   ▼
Google Keep
   │
   │ 同期
   ▼
Windows PC

のように、スマートフォンからPCへ送ることもできます。

 

🧑🏻‍💻 「自分の端末間」で使うのが簡単

実は、Google Keep の便利さは自分の複数端末をつなぐ用途で特に分かりやすいです。

例えば、


       ┌──── iPhone
       │
       ├──── Android
Google ┤
Keep   ├──── Mac
       │
       └──── Windows PC

すべて同じ Google アカウントでログインしておけば、Keep のメモを各端末から確認できます。

「自分の iPhone から自分の Mac へ URL を送りたい」といった用途なら、かなり手軽です。

 

🧑🏻‍💻 他の人とも共有できる

Google Keep は、自分の端末間だけでなく他の人との共有にも使えます。

共有相手の Google アカウントを指定すれば、同じメモを共同編集できます。


      Google Keep
      /         \
     /           \
自分の Google    相手の Google
  Account         Account
     │               │
  Android          iPhone
     │               │
    PC              PC

そのため、

・ 自分の Android → 家族の iPhone
・ 自分の Mac → 同僚の Windows
・ 自分の iPhone → 自分の PC

のような使い方もできます。

 

🧑🏻‍💻 「Google アカウントは今どき持っている」がポイント

iPhone ユーザーでも Google アカウントを持っている人は珍しくありません。

Android ユーザーなら、Google アカウントを利用しているケースはさらに多いでしょう。

PC でもブラウザから Google Keep を利用できます。

そのため、特別なアプリや専用の転送ケーブルを用意しなくても、OSの違いを超えて情報を共有できます。

 

🧑🏻‍💻 まとめ

iPhone・Android・Windows・Mac が混在する環境では、データ共有の方法に悩みがちです。

Google Keepなら、

・ Android
・ iPhone / iPad
・ Windows
・ Mac
・ Chromebook

などから同じデータにアクセスできます。

特に、

・ URL
・ テキスト
・ メモ
・ チェックリスト
・ 小さな画像

のようなちょっとしたデータを別の端末へ渡す用途に向いています。

「iPhone か Android か」ではなく、「Google Keep を共有場所にする」と考えると、端末間のデータ共有がかなりシンプルになります。


Jetpack Compose で巨大化した UiState をどう整理するか

見通しが悪くなった UiState を、責務ごとに整理する7つの方法

 

🧑🏻‍💻 1. Loading / Error / Success が混ざっているなら sealed interface にする

まず、ありがちなパターンです。

変更前:


data class UiState(
    val isLoading: Boolean,
    val error: String?,
    val items: List<Item>
)

- isLoading、error、items の組み合わせで画面の状態を表しています。
- しかし、これでは「Loading なのに items が入っている」「Error なのに isLoading が true」といった、あり得ない状態も作れてしまいます。

変更後:


sealed interface UiState {
    data object Loading : UiState
    data class Error(val message: String) : UiState
    data class Success(val items: List<Item>) : UiState
}

- Loading、Error、Success を、そのまま画面の状態として表現します。
- 状態を Boolean や nullable なプロパティの組み合わせで表現する必要がなくなります。

 

🧑🏻‍💻 2. フィールドが増えすぎたら、Sub-Stateにまとめる

UiState にプロパティを追加していった結果、何がどこに関係しているのか分からなくなることがあります。

変更前:


data class UiState(
    val title: String,
    val query: String,
    val items: List<Item>,
    val selectedCategory: Category?
)

変更後:


data class UiState(
    val header: HeaderState,
    val filter: FilterState,
    val list: ListState
)

data class HeaderState(
    val title: String
)

data class FilterState(
    val query: String,
    val category: Category?
)

data class ListState(
    val items: List<Item>
)


-「ヘッダー」「検索・フィルター」「リスト」という画面上のまとまりで整理します。
- UiState の中身を見ただけで、どの状態がどの役割なのか分かるようになります。

 

🧑🏻‍💻 3. UIだけで使う状態はComposableに閉じ込める

すべての状態をViewModelの UiState に入れる必要はありません。

変更前:


data class UiState(
    val items: List<Item>,
    val isExpanded: Boolean
)

変更後:


data class UiState(
    val items: List<Item>
)

@Composable
fun Screen(state: UiState) {
    var isExpanded by rememberSaveable {
        mutableStateOf(false)
    }
}

- isExpanded が画面の一時的な表示状態にすぎないなら、ViewModelに持たせる必要はありません。
- remember / rememberSaveable に移すことで、UiState をシンプルにできます。

例えば次のようなものです。

・一時的な展開状態
・アニメーションの状態
・Tooltipの表示状態
・フォーカス状態
・スクロール位置

ただし、スクロール位置などを画面復元やビジネスロジック上の理由で ViewModel 側に保持する必要がある場合は例外です。

 

🧑🏻‍💻 4. 計算できる値はStateとして持たない

「元のStateから計算できる値」まで UiState に入れてしまうと、Stateがどんどん増えていきます。

変更前:


data class UiState(
    val items: List<Item>,
    val filteredItems: List<Item>,
    val isEmpty: Boolean
)

変更後:


data class UiState(
    val items: List<Item>,
    val query: String
)

val filteredItems =
    state.items.filter { it.name.contains(state.query) }

val isEmpty = filteredItems.isEmpty()

- filteredItems は items と query から計算できます。
- isEmpty も filteredItems から計算できます。
- つまり、わざわざStateとして保存する必要がありません。
- ViewModelで StateFlow を組み合わせるなら combine や map、Compose 内で Compose State から値を導出するなら derivedStateOf などを使います。

 

🧑🏻‍💻 5. ViewModelまで巨大になったら、ViewModelを分ける

UiState が巨大になった原因が、そもそもViewModelがいろいろな責務を持ちすぎているケースもあります。

変更前:


class MainViewModel : ViewModel() {
    val headerState = ...
    val searchState = ...
    val contentState = ...
}

Stateを分けていても、ViewModel自体がすべてを管理しています。

変更後:


class HeaderViewModel : ViewModel() {
    val state = ...
}

class ContentViewModel : ViewModel() {
    val state = ...
}

@Composable
fun MainScreen(
    header: HeaderViewModel,
    content: ContentViewModel
) {
    HeaderSection(header.state)
    ContentSection(content.state)
}

- 「State を分割する」だけではなく、責務そのものを分けるという考え方です。
- ただし、Composable ごとに ViewModel を作る、という意味ではありません。
- 独立した責務やライフサイクルがある場合に検討します。

 

🧑🏻‍💻 6. 大量のデータを UiState に詰め込まない

数千件、数万件のデータをそのまま UiState に持たせるケースでは、そもそもStateの持ち方を見直します。

変更前:


data class UiState(
    val items: List<Item>
)

val uiState = repository.loadAllItems()

変更後:


data class UiState(
    val query: String
)
val items =
    repository.items()
        .cachedIn(viewModelScope)

- 検索条件などの画面状態と、大量のデータを分離します。
- Paging が適しているケースでは、Paging を利用して必要なデータを段階的に扱います。

 

🧑🏻‍💻 7. 1つの巨大な StateFlow に全部詰め込まない

すべての状態を1つの UiState にまとめることが、かえって分かりにくくなるケースもあります。

変更前:


data class UiState(
    val header: HeaderState,
    val search: SearchState,
    val content: ContentState
)

val uiState: StateFlow<UiState> = ...

変更後:


val headerState: StateFlow<HeaderState> = ...
val searchState: StateFlow<SearchState> = ...
val contentState: StateFlow<ContentState> = ...

@Composable
fun Screen(viewModel: MainViewModel) {
    Header(viewModel.headerState)
    Search(viewModel.searchState)
    Content(viewModel.contentState)
}

- 各 State が本当に独立しているなら、無理に1つへまとめる必要はありません。
- それぞれの UI が必要な State だけを受け取る形にできます。
- ただし、何でも StateFlow に分ければいいわけではありません。

 

🧑🏻‍💻 まとめ - どう整理するか


巨大な UiState
      │
      ├─ Loading / Error / Success が混在
      │       → sealed interface
      │
      ├─ フィールドが増えすぎた
      │       → Sub-State
      │
      ├─ UIだけで使う状態
      │       → remember / rememberSaveable
      │
      ├─ 計算すれば求められる値
      │       → Derived State
      │
      ├─ ViewModelの責務が多すぎる
      │       → ViewModelを分割
      │
      ├─ 大量のデータを保持している
      │       → Pagingなどを検討
      │
      └─ 独立した状態が混在している
              → StateFlowを分割

重要なのは、「UiStateの行数を減らす」ことではありません。

まず、今 UiState に入っているものを見て、


これは本当にStateなのか?
        ↓
UIだけのStateでは?
        ↓
計算できる値では?
        ↓
別の責務では?
        ↓
大量データでは?

と整理していくことです。

つまり、巨大な UiState は、Stateの責務を見直すきっかけと考えると分かりやすいです。

注意点:

- UiState が大きいこと自体が悪いわけではありません。
- StateFlow を細かく分けすぎると、今度は State 同士の関係が分かりにくくなります。
- sealed interface は、Loading / Error / Successのように本当に相互排他的な状態に使います。
- rememberSaveable に移すか ViewModel に残すかは、画面再生成やプロセス終了後にも復元する必要があるかも判断材料になります。
- Pagingも「List が大きいから必ず使う」というものではありません。

 

🤔 参考


ComposableView の闇

Jetpack Compose を導入したからといって、アプリが「Compose アプリ」になったわけではありません。

多くのプロジェクトでは、XML を ComposeView に置き換えただけで移行を終えています。

しかし、その状態は Compose の世界と View の世界の両方を同時に抱える アーキテクチャです。

一見すると移行できたように見えますが、実際には複雑さが増えていることも少なくありません。

 

🧑🏻‍💻 Compose に見えて、実は View

ComposeView は名前の通り View です。


Activity
└── Fragment
    └── ComposeView
        └── Composition

つまり、Compose を書いていても、

- Activity のライフサイクル
- Fragment のライフサイクル
- View のライフサイクル
- Composition のライフサイクル

という複数のライフサイクルの上で動いています。

Compose 本来のシンプルさは、この時点ではまだ得られていません。

 

🧑🏻‍💻 View のルールから逃げられない

Compose だけなら意識しなくて済むことも、ComposeView では必要になります。

例えば Composition の破棄タイミング。


composeView.setViewCompositionStrategy(
    ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed
)

これを適切に設定しなければ、Composition が期待どおり破棄されないケースがあります。

Compose アプリでは考えなくてよいことを、ComposeView では考える必要があります。

 

🧑🏻‍💻 Fragment は消えていない

ComposeView を使う構成は、多くの場合こうなります。


Fragment
    ↓
ComposeView
    ↓
Composable

これは

Fragment を Compose に置き換えた

のではありません。

実際には

Fragment の中へ Compose を埋め込んだ

だけです。

Fragment の責務もライフサイクルも Navigation も、そのまま残っています。

 

🧑🏻‍💻 「View を Compose に置き換える」は移行の半分

Compose 化というと、多くの人は View を Composable に書き換えることを思い浮かべます。

しかし、本当に変わるのは UI だけではありません。

Compose First の設計では、

- Fragment が不要になる
- XML が不要になる
- ViewBinding が不要になる
- FragmentManager への依存が減る
- Navigation の考え方が変わる
- State の持ち方も変わる

つまり、アーキテクチャ全体が変わります。

 

🧑🏻‍💻 Navigation 3 が示している未来

Navigation 3 のサンプルを見ると、Fragment は登場しません。


Activity
    ↓
NavDisplay
        ↓
Composable Screen

画面そのものが Composable になり、状態は Compose の仕組みを中心に管理されます。

ここでは ComposeView は必要ありません。

Compose がアプリの土台になっています。

 

🧑🏻‍💻 ComposeView は悪者ではない

もちろん ComposeView が悪いわけではありません。

既存アプリを段階的に Compose へ移行するには、とても重要な仕組みです。

大規模アプリでは、一気に Compose へ移行できるケースはほとんどありません。

だから ComposeView は今でも価値があります。

問題なのは、

ComposeView を使っている状態を「Compose 化が完了した状態」と思ってしまうことです。

 

🧑🏻‍💻 本当の Compose 化とは

本当の Compose 化は、単に XML を消すことではありません。


XML
    ↓
ComposeView
    ↓
Fragment を減らす
    ↓
Navigation を Compose 中心へ
    ↓
State 管理を Compose に合わせる
    ↓
Single Activity

この変化によって初めて、

Compose のシンプルな設計思想をそのまま活かせるようになります。

 

🧑🏻‍💻 おわりに

「ComposeView を使っています。」

この一文だけでは、そのアプリが Compose アプリなのか、View アプリなのかは分かりません。

もし ComposeView が Fragment の中にあり、Fragment Navigation の上で動いているなら、それはまだ View ベースのアプリに Compose を埋め込んでいる状態です。

Compose の本当の価値は、UI を書き換えることではありません。

アーキテクチャそのものをシンプルにできること。

そこまで進んだとき、ようやく「Compose 化した」と言えるのではないでしょうか。


【Kotlin】コピペで使える「null」を消すテクニック頻出順40

 

🧑🏻‍💻 1. Non-null を基本にする


// 悪い
val name: String?

// 良い
val name: String

Nullable を作らないことが最も重要。

 

🧑🏻‍💻 2. DTOだけ Nullable


data class UserResponse(
    val name: String?
)

外部データだけ Nullable にする。

 

🧑🏻‍💻 3. Domain は Non-null


data class User(
    val name: String
)

アプリ内部では Non-null を維持する。

 

🧑🏻‍💻 4. Repositoryで吸収する


fun load(): User =
    api.load()?.toUser() ?: User.EMPTY

Repository より先へ Nullable を持ち込まない。

 

🧑🏻‍💻 5. デフォルト値を使う


data class User(
    val name: String = ""
)

初期値で null を不要にする。

 

🧑🏻‍💻 6. orEmpty()


val name = user.name.orEmpty()

null を空文字へ変換する。

 

🧑🏻‍💻 7. emptyList()


fun users(): List<User> = emptyList()

List? を返さない。

 

🧑🏻‍💻 8. emptyMap()


fun settings(): Map<String, String> = emptyMap()

Map? を返さない。

 

🧑🏻‍💻 9. emptySet()


fun tags(): Set<String> = emptySet()

Set? を返さない。

 

🧑🏻‍💻 10. ?: return


val user = repository.find(id) ?: return

null を早期に除去する。

 

🧑🏻‍💻 11. ?: continue


val name = user.name ?: continue

ループ中の null をスキップする。

 

🧑🏻‍💻 12. ?: break


val item = iterator.nextOrNull() ?: break

null を終了条件として扱う。

 

🧑🏻‍💻 13. ?: error()


val token = token ?: error("Token required")

あり得ない null は即座に失敗させる。

 

🧑🏻‍💻 14. mapNotNull()


val names = users.mapNotNull { it.name }

変換と null 除去を同時に行う。

 

🧑🏻‍💻 15. filterNotNull()


val names = names.filterNotNull()

Collection の null を取り除く。

 

🧑🏻‍💻 16. Flow filterNotNull()


val users: Flow<User> =
    repository.userFlow.filterNotNull()

Flow の null を流さない。

 

🧑🏻‍💻 17. firstOrNull() ?: return


val user = users.firstOrNull() ?: return

最初の要素が無ければ早期終了する。

 

🧑🏻‍💻 18. by lazy


private val repository by lazy {
    UserRepository()
}

遅延初期化で Nullable を不要にする。

 

🧑🏻‍💻 19. Smart Cast(ローカル変数へ退避)


val user = currentUser ?: return

println(user.name)

Smart Cast を有効にして !! を避ける。

 

🧑🏻‍💻 20. StateFlow を Nullable にしない


private val uiState =
    MutableStateFlow(UserUiState())

StateFlow より状態オブジェクトを持つ。

 

🧑🏻‍💻 21. Compose State を Nullable にしない


var uiState by mutableStateOf(UserUiState())

mutableStateOf より状態オブジェクトを持つ。

 

🧑🏻‍💻 22. Java境界で Non-null 化する


val title: String =
    javaApi.title.orEmpty()

Platform Type (String!) を入口で閉じ込める。

 

🧑🏻‍💻 23. requireNotNull()


val user = requireNotNull(user)

引数や入力値の null を早期に拒否する。

 

🧑🏻‍💻 24. checkNotNull()


val database = checkNotNull(database)

オブジェクトの状態が不正なら失敗させる。

 

🧑🏻‍💻 25. Null Object Pattern


interface Logger

object EmptyLogger : Logger

Logger? の代わりに空実装を使う。

 

🧑🏻‍💻 26. sealed class / interface


sealed interface UiState {
    data object Loading : UiState
    data class Success(val users: List<User>) : UiState
    data class Error(val message: String) : UiState
}

null の代わりに状態を型で表現する。

 

🧑🏻‍💻 27. Result


fun load(): Result<User>

失敗を null ではなく Result で表現する。

 

🧑🏻‍💻 28. value class


@JvmInline
value class UserId(val value: Long)

IDなどを専用型にして null を減らす。

 

🧑🏻‍💻 29. takeIf()


val adult = user.takeIf { it.age >= 18 } ?: return

条件を満たさなければ早期終了する。

 

🧑🏻‍💻 30. firstNotNullOf()


val token = providers.firstNotNullOf {
    it.token
}

最初に見つかった Non-null 値を取得する。

 

🧑🏻‍💻 31. SharedFlow を使う


private val events = MutableSharedFlow<Event>(
    replay = 1
)

イベントでは StateFlow より SharedFlow が適している場合がある。

 

🧑🏻‍💻 32. Nothing を使う


fun fail(message: String): Nothing =
    throw IllegalStateException(message)

val token = token ?: fail("Token required")

Nothing を使うと独自のガード関数を作れる。

 

🧑🏻‍💻 33. !! を使わない


// 悪い
val user = user!!

// 良い
val user = requireNotNull(user)

!! より意図が明確な API を使う。

 

🧑🏻‍💻 34. let を減らす


// 悪い
user?.let {
    println(it.name)
}

// 良い
val user = user ?: return
println(user.name)

ネストを減らして読みやすくする。

 

🧑🏻‍💻 35. lateinit


private lateinit var repository: UserRepository

後から初期化する参照型では Nullable を避けられる。

 

🧑🏻‍💻 36. Delegates.notNull()


var count: Int by Delegates.notNull()

Int や Boolean など lateinit が使えない型向け。

 

🧑🏻‍💻 37. takeUnless()


val user = user.takeUnless { it.deleted } ?: return

条件を満たしたら除外したい場合に使う。

 

🧑🏻‍💻 38. firstNotNullOfOrNull()


val token = providers.firstNotNullOfOrNull {
    it.token
}

最初の Non-null を取得し、無ければ null を返す。

 

🧑🏻‍💻 39. Kotlin Contracts


@OptIn(ExperimentalContracts::class)
fun requireUser(user: User?) {
    contract {
        returns() implies (user != null)
    }
    requireNotNull(user)
}

独自関数でも Smart Cast を効かせられる。

 

🧑🏻‍💻 40. associateNotNull() パターン


val map = users
    .mapNotNull {
        it.id?.let { id -> id to it }
    }
    .toMap()

Map を作る途中で null を除外する。

 

🧑🏻‍💻 まとめ

「null」を未だに使ってるプロジェクト、実際現場では多いですよね。

「NullPointerExeption(ヌルポ)」の対応による開発時間の損失は、IT業界全体で年間数十兆円、個々の開発プロジェクトでも全体の数割の時間を奪うレベルの巨大なインパクトを持っています。