【モバイルSuica】新機能「teppay(テッペイ)」は不要?電車に乗るだけならスルーでOKな理由

2026年10月からモバイルSuicaで新たにスタートしたコード決済機能「teppay(テッペイ)」。

「Suicaの中に新しい決済機能が増えたみたいだけど、使ったほうがいいの?」
「設定とか面倒くさそうだけど、使わないと損する?」

そんな疑問を持っている方も多いのではないでしょうか。

結論から言うと、「電車やバスに乗るのがメイン」「普段の買い物はクレカや既存のPayで十分」という方は、teppayを使う必要は一切ありません。

この記事では、なぜ「普通の人」はteppayを使わなくて大丈夫なのか、その理由を分かりやすく解説します。

 

🤔 理由1:改札を通るときは結局「Suica(タッチ)」だから

teppayはPayPayや楽天ペイのような「バーコード・QRコード決済」です。

お店での買い物や送金には使えますが、電車の改札機をピッとタッチして通る機能は、これまで通りの「Suica」の役割です。

teppayを設定したからといって改札が便利になるわけではないため、電車移動がメインの人にはメリットがほぼありません。

 

🤔 理由2:普段のチャージなら「2万円上限」で十分

teppayの大きな特徴のひとつに「チャージ上限が最大30万円まで増える」という点があります。

ですが、毎日の電車代やちょっとしたコンビニの買い物程度なら、これまでのSuicaの上限である「2万円」があれば十分に足ります。

「30万円もスマホの残高に入れて持ち歩きたくない…」という人にとっても、無理に導入する理由は見当たりません。

 

🤔 理由3:画面の切り替えや操作が増えて面倒になる

これまでモバイルSuicaは、アプリを開かなくてもスマホをかざすだけで使えました。

しかしteppayで支払いをしようとすると、アプリを開いて「Suica画面」から「teppay画面」へ切り替え、バーコードを提示するといった手順が必要になります。

「かざすだけ」のシンプルさに慣れている人からすると、かえって手間が増えてしまいます。

 

🤔 理由4:ポイ活目的なら他のPayやクレカでOK

teppayを使うと「teppayポイント」という新しいポイントが貯まりますが、すでにPayPay、楽天ペイ、d払いなどの既存コード決済を使っている人や、クレジットカードのポイントを貯めている人にとっては、管理するポイントが増えて煩雑になるだけです。

無理に新しいポイントプログラムに乗り換える必要はありません。

 

🤔 まとめ:これまで通り「ピッとタッチ」で快適に使おう!

teppayは「高額な定期券を残高で買いたい人」や「Suicaアプリ内で友達と送金し合いたい人」などに向けた新機能です。

・ 電車やバスに乗るのがメイン

・ チャージは2万円以下で問題ない

・ 支払いはかざすだけの電子マネーが良い

これらに当てはまる方は、teppayの機能を気にせず、これまで通りのモバイルSuicaの使い方を続けて全く問題ありません。

便利な機能が増えるのは良いことですが、自分に不要な機能は無理に使わず、シンプルで快適なキャッシュレス生活を送りましょう!


uiState を宣言的に組み立てる Flow オペレーター ベスト10

みんなはいくつ使っていますか。

 

🧑🏻‍💻 1. stateIn: Flow を StateFlow に変換する

宣言的な uiState の締めくくりです。WhileSubscribed(5_000) にしておくと、画面回転では再購読せず、バックグラウンドでは上流を止められます。


val uiState: StateFlow<UiState> = flow
    .stateIn(
        scope = viewModelScope,
        started = SharingStarted.WhileSubscribed(5_000),
        initialValue = UiState.Loading
    )

 

🧑🏻‍💻 2. combine: 複数ソースを 1 つの UiState に合成する

MutableStateFlow を何本も手で更新する代わりに、「ソースの合成結果 = UiState」と宣言できます。


combine(repo.items, query, filter) { items, q, f ->
    UiState.Success(items.filter { it.matches(q, f) })
}

 

🧑🏻‍💻 3. map: ドメインモデルから UI モデルへの変換

整形や表示用の変換はここに集約します。


repo.user.map { it.toUiModel() }

 

🧑🏻‍💻 4. flatMapLatest: 入力の変化で上流を切り替える

検索クエリや選択 ID が変わると、前のリクエストを自動キャンセルして新しい Flow に切り替えます。


query.flatMapLatest { q -> repo.search(q) }

 

🧑🏻‍💻 5. catch: エラーを UiState に変換する

例外を UI の状態として扱えます。上流のエラーだけを捕捉する点が try/catch との違いです。


.catch { emit(UiState.Error(it.message)) }

 

🧑🏻‍💻 6. onStart: Loading の初期値を流す

購読開始時に Loading を流す定番の書き方です。


repo.items
    .map<_, UiState> { UiState.Success(it) }
    .onStart { emit(UiState.Loading) }

 

🧑🏻‍💻 7. distinctUntilChanged: 無駄な再描画を防ぐ

StateFlow 自体も同値を弾きますが、中間の Flow で重い処理の前に挟むと効果的です。


query.distinctUntilChanged()
    .flatMapLatest { repo.search(it) }

 

🧑🏻‍💻 8. debounce: 入力の連打を間引く

検索ボックスなどで使います。debounce は同値の弾き込みはしないため、distinctUntilChanged と併用します。


query.debounce(300)
    .distinctUntilChanged()

 

🧑🏻‍💻 9. scan / runningFold: 状態を畳み込む

ページング風の追加読み込みや、イベントから状態を積み上げる MVI 的な設計で使います。


events.scan(UiState()) { state, event -> reduce(state, event) }

 

🧑🏻‍💻 10. flowOn: 上流の実行スレッドを指定する

重い変換や IO を Main から外します。上流にだけ効く点に注意してください。


repo.items
    .map { heavyTransform(it) }
    .flowOn(Dispatchers.Default)

 

🧑🏻‍💻 よくある組み合わせ例


class SearchViewModel(repo: SearchRepository) : ViewModel() {
    private val query = MutableStateFlow("")
    private val filter = MutableStateFlow(Filter.All)

    val uiState: StateFlow<UiState> = combine(
        query.debounce(300).distinctUntilChanged()
            .flatMapLatest { repo.search(it) },
        filter
    ) { results, f ->
        UiState.Success(results.filter(f::accepts)) as UiState
    }
        .onStart { 
            emit(UiState.Loading) 
        }
        .catch { 
            emit(UiState.Error(it.message)) 
        }
        .flowOn(Dispatchers.Default)
        .stateIn(
            viewModelScope, 
            SharingStarted.WhileSubscribed(5_000), 
            UiState.Loading
        )

    fun onQueryChange(q: String) { 
        query.value = q 
    }
}

 

🧑🏻‍💻 参考


UI Action は UiState を直接変更しない

UiState は入力から導出する

UI = f(State) は、宣言的 UI の基本的な考え方です。

では、UIからのActionによってStateをどう変えるのでしょうか?

ルールはシンプルです。

UI Action から public な UiState を直接変更しない。

UI Action は ViewModel が持つ private な入力を更新します。

そして、UiState はその入力から導出します。


UI Action
  ↓
Private Flow
  ↓
UiState
  ↓
UI

例えば、検索画面なら次のように書けます。


private val _query = MutableStateFlow("")

val uiState = combine(
    _query,
    repository.items
) { query, items ->
    UiState(
        query = query,
        items = items
    )
}

fun onQueryChanged(query: String) {
    _query.value = query
}

次のように UiState を直接変更することもできます。


fun onQueryChanged(query: String) {
    _uiState.update {
        it.copy(query = query)
    }
}

この場合、UI Action 自身が画面の State を管理することになります。

一方、private な入力を更新するだけなら、責務が明確になります。


UI Action
    ↓
入力を変更する
    ↓
UiState を導出する
    ↓
UI を描画する

つまり、


UiState = f(Inputs)

であり、


UI Action → UiState を変更する

ではありません。

UI Action の役割は、「何が起きたか」を ViewModel に伝えることです。

そして、「その結果どのようなUiStateになるか」は Flow の依存関係によって決まります。

UI Action は UiState を変更する命令ではなく、入力を変更するトリガーである。

 

🧑🏻‍💻 参考


UiState で UiEvent を扱う3つのパターン

「AndroidのUI設計って、イベント駆動から状態駆動に完全にシフトしたよね」という話の続きです。

ViewModelから画面側(Compose)へ単発のイベントを伝えるとき、SharedFlowやChannelを使わずにUiStateだけで閉じた設計にしようとすると、だいたい次の3つのアプローチに行き着きます。

実務での使い分けを踏まえて整理してみました。

 

🤔 1. Single Event (nullable)

UiState の中に event: UiEvent? をひとつ持たせる一番シンプルで直感的なパターンです。


sealed interface UiEvent {
    data class ShowSnackbar(val message: String) : UiEvent
    data class NavigateToDetail(val id: String) : UiEvent
    data class ShowDialog(val type: DialogType) : UiEvent
}

data class UiState(
    val isLoading: Boolean = false,
    val items: List<Item> = emptyList(),
    val event: UiEvent? = null,
)

// ViewModel
private val _uiState = MutableStateFlow(UiState())
val uiState: StateFlow<UiState> = _uiState.asStateFlow()

fun onSaveClicked() {
    _uiState.update { it.copy(event = UiEvent.ShowSnackbar("保存しました")) }
}

fun onEventConsumed() {
    _uiState.update { it.copy(event = null) }
}

// UI (Compose)
LaunchedEffect(uiState.event) {
    when (val event = uiState.event) {
        is UiEvent.ShowSnackbar -> {
            snackbarHostState.showSnackbar(event.message)
            viewModel.onEventConsumed()
        }
        is UiEvent.NavigateToDetail -> {
            navController.navigate("detail/${event.id}")
            viewModel.onEventConsumed()
        }
        is UiEvent.ShowDialog -> viewModel.onEventConsumed()
        null -> Unit
    }
}

メリット
・ 構造がとにかくシンプル。フィールドも1つ、消費用コールの記述も1つで済む。
・ StateFlow だけで完結するので、Channel 管理の煩わしさやリークのリスクを回避できる。
・ uiState.value.event をアサートするだけで単体テストが書ける。

デメリット
・ ほぼ同時に複数のイベントが発生すると、最後のやつで上書きされて途中のイベントが消える。
・ 全く同じ内容のイベントが連続すると、data class の同値判定(equals)に引っかかって LaunchedEffect が再発火しない場合がある。

 

🤔 2. Queue + drop(1)

イベントをQueue(List)で保持し、処理が終わった順に先頭を削っていく(drop(1))パターンです。


data class UiState(
    val isLoading: Boolean = false,
    val items: List<Item> = emptyList(),
    val eventQueue: List<UiEvent> = emptyList(),
)

// ViewModel
private fun sendEvent(event: UiEvent) {
    _uiState.update { it.copy(eventQueue = it.eventQueue + event) }
}

fun onSaveClicked() {
    sendEvent(UiEvent.ShowSnackbar("保存しました"))
    sendEvent(UiEvent.NavigateToDetail(savedId))
}

fun onEventConsumed() {
    _uiState.update { it.copy(eventQueue = it.eventQueue.drop(1)) }
}

// UI (Compose)
LaunchedEffect(uiState.eventQueue) {
    val event = uiState.eventQueue.firstOrNull() ?: return@LaunchedEffect
    when (event) {
        is UiEvent.ShowSnackbar -> snackbarHostState.showSnackbar(event.message)
        is UiEvent.NavigateToDetail -> navController.navigate("detail/${event.id}")
        is UiEvent.ShowDialog -> { /* ... */ }
    }
    viewModel.onEventConsumed()
}

メリット
・ 複数イベントの連打や同時発火に対応でき、発生した順番通りに処理できる。
・ when 式の型チェック(exhaustive)をそのまま活かせる。

デメリット
・ UI側で onEventConsumed() を呼び忘れるとキューの先頭が残り続け、後続のイベントがすべて詰まる。
・ 単純に drop(1) しているだけなので、「今処理したイベント」と「実際に削除されたイベント」がずれるリスクが残る(同一内容のイベントが連続した場合など)。

 

🤔 3. Queue + ID matching

キュー構造に加えて、イベントごとに一意のID(System.nanoTime() など)を付与し、ID指定でピンポイントに消費させるパターンです。


data class QueuedEvent(
    val id: Long = System.nanoTime(),
    val event: UiEvent,
)

data class UiState(
    val isLoading: Boolean = false,
    val items: List<Item> = emptyList(),
    val eventQueue: List<QueuedEvent> = emptyList(),
)

// ViewModel
private fun sendEvent(event: UiEvent) {
    _uiState.update { it.copy(eventQueue = it.eventQueue + QueuedEvent(event = event)) }
}

fun onSaveClicked() {
    sendEvent(UiEvent.ShowSnackbar("保存しました"))
    sendEvent(UiEvent.NavigateToDetail(savedId))
}

fun onEventConsumed(id: Long) {
    _uiState.update { state ->
        state.copy(eventQueue = state.eventQueue.filterNot { it.id == id })
    }
}

// UI (Compose)
LaunchedEffect(uiState.eventQueue.firstOrNull()?.id) {
    val queued = uiState.eventQueue.firstOrNull() ?: return@LaunchedEffect
    when (val event = queued.event) {
        is UiEvent.ShowSnackbar -> snackbarHostState.showSnackbar(event.message)
        is UiEvent.NavigateToDetail -> navController.navigate("detail/${event.id}")
        is UiEvent.ShowDialog -> { /* ... */ }
    }
    viewModel.onEventConsumed(queued.id)
}

メリット
・ 内容が全く同じイベントが並んでも、ID指定で削除するため誤削除が発生しない。
・ LaunchedEffect のキーにIDを渡せるため、二重処理や再発火漏れを極力防げる。

デメリット
・ ラッパークラスやID生成、filterNot の処理など、ボイラープレートコードが増える。
・ 通常のトースト表示や画面遷移程度なら、正味オーバースペック。

 

🤔 比較

 

🤔 現場での判断軸

プロダクト開発では、最初から凝りすぎず段階的に育てるアプローチが取りやすいです。

1, まずは「Single Event」で組む

8割方の画面はこれで十分事足ります。コードも簡潔で可読性が高く、チーム内での認知負荷も低く抑えられます。

2.「保存成功スナックバーを出しつつ元の画面に戻る」など、同時発生が要件になったら「Queue」へ移行

Single Eventの限界(上書き問題)が見えたタイミングでキュー構造にリファクタリングします。

3. 二重処理や損失が致命傷になる重要フロー(決済やアカウント削除など)のみ「ID matching」を入れる

堅牢性が必要な局所に絞ってピンポイントで採用するのが、過剰な抽象化(オーバーエンジニアリング)を防ぐコツです。

 

🤔 参考


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 に接続するための仕組みとして理解できます。