2025年4月5日土曜日

Kotlin Multiplatformで日時をフォーマットする

Kotlin Multiplatformで日付を言語ロケールに合わせてフォーマットしたい場合はどうしたらいいでしょうか。 Kotlin Multiplatformで日時を扱うには kotlinx-datetime ライブラリを利用することが一般的だと思います。このライブラリは基本的な日時操作機能を提供していますが、ロケールに合わせたフォーマット機能はありません。

expect/actualで実装する

現状では expect/actual 機構を使って、各プラットフォーム固有の日時フォーマットを実装するのが現実的です。 以下はその実装例です。

共通モジュールでの定義

まず、共通モジュールに機能を定義します。

// 共通モジュールに定義
expect class DateTimeFormatter() {
    fun format(instant: Instant, timeZone: TimeZone): String
}

JVM(Android)実装

JVMプラットフォームではJava標準ライブラリの日時フォーマット機能を使います。

2025年3月22日土曜日

Jetpack Composeのアニメーションとライフサイクル管理

Jetpack Composeでアニメーションを使用する場合、アプリがバックグラウンドの時でもアニメーションが動いたままでバッテリーを消費してしまうのではないかと心配してしまうかもしれません。
この記事ではアニメーションと画面のライフサイクルの関係について説明します。

アニメーションの動作

Jetpack ComposeのアニメーションはActivityのライフサイクルと連動して動作します。Activityがpause状態になるとその中に含まれるComposableのアニメーションも停止します。これは以下の流れで動作します。

  • Composableがアクティブな間はアニメーションが正常に動作します。この時Composeのアニメーションエンジンはフレームごとにアニメーション値を更新します。
  • Composableが一時停止状態(pause)に移行するとアニメーションも一時停止します。
  • Composableが再開(resume)するとアニメーションも再開します。

この仕組みによりJetpack Composeアプリケーションは効率的にリソースを管理しながらも滑らかなアニメーションを提供できます。

Composableのpause状態の検知方法

アニメーションの挙動をより細かく制御したい場合は、以下の方法でComposableのライフサイクル状態を検知できます。

2025年3月14日金曜日

Jetpack Compose: mutableStateOf の変更の検出の方法を変えるには

はじめに

Jetpack Composeで状態を管理する際に mutableStateOf をよく使うと思います。 その mutableStateOf を作成する際に、policy というパラメータを指定できることをご存知でしょうか?

val state = remember {
    mutableStateOf(initialValue, policy = structuralEqualityPolicy()) // policyを指定
}

この policy パラメータは、Stateオブジェクトの値が変更されたとみなす条件、つまり状態の変更をどのように検出するかを制御することができます。

policy を適切に設定することで不要な再コンポーズを避け、パフォーマンスを向上できる可能性があります。逆に policy を理解せずにデフォルトのまま使用していると、意図しない再コンポーズが発生しパフォーマンスに影響を与えるかもしれません。

この記事では、mutableStateOfpolicy で選択できる3つのポリシーを解説します。

mutableStateOfpolicy で選択できる3つのポリシー

mutableStateOfpolicy パラメータには以下の3つのポリシーを設定できます。

  1. structuralEqualityPolicy(): 構造的等価性ポリシー (デフォルト)
  2. referentialEqualityPolicy(): 参照的等価性ポリシー
  3. neverEqualPolicy(): 常に非等価ポリシー

それぞれ詳しく見ていきましょう。

2025年3月1日土曜日

KotlinのSharedFlowで、コレクターが存在するときだけ値を流す方法

KotlinのshareIn()はコールドFlowをホットFlowにすることができますが、

  • SharedFlowを使用し、コレクターが存在する場合にのみデータを流したい
  • 不要な処理を避け、パフォーマンスを最適化したい

といったケースでは、適切なフローの仕組みを選択する必要があります。

SharedFlowとは?

SharedFlowは、複数のコレクターが同じデータを受け取れるホットFlowの一種です。しかし、デフォルトではコレクターの有無にかかわらずデータが流れ続けるため、「コレクターがいないときは値を流さない」ように制御したほうがいい場合もあります。

SharedFlowの基本

2025年2月15日土曜日

Jetpack ComposeでのLaunchedEffectとwhileループの動作について

基本的な実装と動作

以下のようなコードでは、アプリ(Activity)がpause中でもwhileループは自動的には停止しません。

LaunchedEffect(key1 = Unit) {
     while (true) {
         indicatorAlpha.floatValue = 1f
         delay(1000L)
         indicatorAlpha.floatValue = 0f
         delay(1000L)
     }
}

なぜかというと、

  • LaunchedEffectはComposable関数のライフサイクルに連動するが、Activityのライフサイクルとは直接連動しない
  • whileループ内のdelayはsuspend関数だが、コルーチン自体は終了しない
  • ActivityがonPause()に入ってもコルーチンは実行を継続する

という理由があるためです。

ライフサイクルに応じた制御の実装

Activityのpause中にループを停止するには以下のような実装が必要です。

var isRunning by remember { mutableStateOf(true) }

LaunchedEffect(isRunning) {
    while (isRunning) {
        indicatorAlpha.floatValue = 1f
        delay(1000L)
        indicatorAlpha.floatValue = 0f
        delay(1000L)
    }
}

DisposableEffect(key1 = LocalLifecycleOwner.current) {
    val observer = LifecycleEventObserver { _, event ->
        if (event == Lifecycle.Event.ON_PAUSE) {
            isRunning = false
        } else if (event == Lifecycle.Event.ON_RESUME) {
            isRunning = true
        }
    }
    LocalLifecycleOwner.current.lifecycle.addObserver(observer)
    onDispose {
        LocalLifecycleOwner.current.lifecycle.removeObserver(observer)
    }
}

Decomposeを使用したMultiplatformアプリでの実装

Decomposeを使用している場合は、Decomposeのライフサイクルを使用することができます。

class YourComponent(
    componentContext: ComponentContext
) : ComponentContext by componentContext {

    private val _isRunning = MutableValue(true)
    val isRunning: Value<Boolean> = _isRunning

    init {
        lifecycle.doOnPause { _isRunning.value = false }
        lifecycle.doOnResume { _isRunning.value = true }
    }
}

まとめ

Jetpack ComposeでLaunchedEffectとwhileループを使用する際は、ActivityやDecomposeのライフサイクルを使って適切なタイミングでループを停止・再開することができます。 これにより、アプリがバックグラウンドに移行した際に不要な処理が実行され続けることを防いでバッテリー消費を抑えることができます。 回しっぱなしのループを作らないように心がけましょう。

2025年2月7日金曜日

MarkdownファイルをHTMLに変換してBloggerに掲載する方法

このブログではMarkdownファイルをローカルでHTMLに変換してBloggerにアップしています。その手順を説明します。

1. Pandocのインストール

私はMac miniをメインで使っているのでMacで作業する場合について説明します。 まずMarkdownファイルを変換するためのPandocをインストールします。 macOSではHomebrewを使用してPandocをインストールできます。

brew install pandoc

または公式サイトからダウンロードできます。

Pandoc 公式サイト

2. MarkdownをHTMLに変換する

最も基本的なコマンドでMarkdownをHTMLに変換できます。

pandoc -o output.html input.md

3. シンタックスハイライトを適用する

Bloggerでテーマの「HTMLを編集」を選択してテーマのHTMLを編集します。

<head> に以下を追加します。

<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/kotlin.min.js"></script>
<script>hljs.highlightAll();</script>

4. 記事をコピー

作成したHTMLファイルをエディタで開き、ファイルの内容をBloggerのHTMLビューへコピペします。最初のh1タグの内容はタイトルなので本文ではなくタイトルの方にコピペします(タグは不要)。

これでMarkdownファイルからBloggerの記事を作成することができます。

2025年2月1日土曜日

KotlinのElvis演算子でUnitはどう評価される?

概要

KotlinのElvis演算子ではUnit型はどのように扱われるでしょうか? この記事では具体的な挙動について説明します。

Unitとは

KotlinではUnitは値を返さない関数の戻り値として使用されます。Kotlinの型システムではUnitは非nullなので、nullとして扱われることはありません。

Elvis演算子の動作

Elvis演算子(?:)は、左辺の値がnullの場合に右辺の値を返します。しかし、Unitは常に非nullなため、Elvis演算子の左辺にUnitを返す関数を指定しても右辺の値は評価されません。

具体例

以下のコードの場合、

fun printMessage() {
    println("Hello, World!")
}

fun main() {
    val result = printMessage() ?: "Default Message"
    println(result)
}

printMessage()関数は常にUnitを返します。Elvis演算子の左辺はUnit(非null)であるため右辺の"Default Message"は評価されず、resultにはUnitが代入されます。その結果、println(result)kotlin.Unitと出力されます。

まとめ

  • UnitはKotlinの型システム上、常に非null
  • Elvis演算子は左辺がnullのときにのみ右辺を評価する
  • UnitをElvis演算子の左辺に置くと、右辺の評価は行われない

ちょっとあやふやなままElvis演算子を使っていたこともありましたが、Unitは値であると考えればすっきり理解できます。

2025年1月26日日曜日

Androidアプリのログの文字列を遅延評価する方法

Androidでログ出力を行う際、実際には出力されないログレベルの場合でも複雑な文字列の作成をしてしまっていることがあります。そこで、文字列を遅延評価して不要な文字列作成コストを削減する方法を解説します。

ログ出力のパフォーマンス問題

以下のコードで複雑なデータ構造を文字列化しているとします。

Log.d("TAG", "現在の状態: ${getComplexState()}")

getComplexState()関数が複雑な処理を行って文字列を生成する場合、ログ出力が行われなくても常にこの処理が実行されてしまいます。

遅延評価とは

遅延評価とは、処理が必要になるまでその実行を遅らせるというプログラミング方法のことです。Kotlinではlazyデリゲートを使って簡単に遅延評価できます。

val message by lazy { "現在の状態: ${getComplexState()}" }
Log.d("TAG", message)

この場合、message変数が実際に使われるまでgetComplexState()関数の呼び出しと文字列の作成は行われません。つまり、ログが出力されない場合はgetComplexState()関数は呼び出されず、無駄な処理をしなくて済みます。

遅延評価ログ関数を作る

遅延評価を簡単に行うためのログ関数を作成してみます。以下のコードはラムダ式を使って遅延評価を行う関数の例です。

inline fun logDebug(tag: String, message: () -> String) {
    if (Log.isLoggable(tag, Log.DEBUG)) {
        Log.d(tag, message())
    }
}

この関数ではログレベルがDEBUG以上の場合にのみ文字列が評価されます。

文字列を遅延評価できるログライブラリ

既存のログライブラリの中には文字列を遅延評価できるものもあります。

Timber

Androidでは非常に有名なログライブラリです。

Timber.d("現在の状態: %s", getComplexState())

この場合getComplexState()自体は常に実行されてしまうため、getComplexState()は文字列を作成するのではなく、データオブジェクトを返すだけにしたほうがいいです。文字列のフォーマットは必要な場合のみ行われます。

Timber: https://github.com/JakeWharton/timber

KmLogging

KmLoggingはKotlin Multiplatform対応の軽量なログライブラリです。こちらはラムダ式を使ってログメッセージを渡すことができます。

KmLogging: https://github.com/LighthouseGames/KmLogging

KmLogging.debug { "現在の状態: ${getComplexState()}" }

ラムダ式の中に複雑な処理を閉じ込めてしまえば必要以上に実行コストを使うことは無くなります。

Napier

NapierもKotlin Multiplatform対応のログライブラリです。文字列の遅延評価もサポートしています。

Napier.d { "現在の状態: ${getComplexState()}" }

Napier: https://github.com/LighthouseGames/KmLogging

まとめ

ログの文字列を遅延評価することでパフォーマンスの向上が期待できます。ついつい何も考えずにログに出力しがちですが、繰り返しが多い部分などは遅延評価を使うことによって本番コードの軽量化を図りましょう。