👋

Androidで華麗に文字列を扱う Sealed interfaces + Extension functions

2024/12/17に公開

はじめに

クライアント開発において何らかの文字列を表示する際、大きく分けると「サーバーから受け取った文字列」と「クライアントで保持している文字列」があります。
Android開発では、特に後者の文字列には一意のIDが割り当てられ、別のファイルで保持されます。開発中はAndroidのビルドシステムが割り当てたこれらの文字列はInt型のIDを通して取得します。

つまり、『サーバーから受け取った文字列』は String 型であるのに対し、『クライアントで保持している文字列』は Int 型として扱う必要があります。
UI側でこれらを区別すると本質ではない部分の処理が煩雑になるため、避けたいと思う方は少なくないと思います。

対策としては様々な方法があります、AndroidDagashi#349 2024-10-20Clean Strings Handling in Androidの方法は、個人的には好きな一つです。

別の方法として、ApplicationContext などを使ってIDから文字列を返却可能なクラスをViewModelなどへDIする方法も考えられます。

記事で紹介されている実装方法

具体的には以下のようなSealed interfaceを定義します。(命名などは少し変更しています。)

sealed interface StringWrapper {
    val text: String
        @Composable
        get() = when(this) {
            is Str -> value
            is Res -> stringResource(resId)
        }
    fun getString(context: Context): String = when (this) {
        is Str -> value
        is Res -> context.getString(resId)
    }
    data class Str(val value: String) : StringWrapper
    data class Res(@StringRes val resId: Int) : StringWrapper
}

Composable内では以下のようなコードを利用できます。

@Composable
fun MyText(
    stringWrapper: StringWrapper
) {
    Text(text = stringWrapper.text)
}

またFragment内でも以下のようなコードで文字列を取得することができます。

class MyFragment: Fragment() {
    fun doSomething(stringWrapper: StringWrapper) {
        val str = stringWrapper.getString(requireContext())
        println(str)
    }
}

これによりUI側では、その文字列が『サーバーから受け取った文字列』か『クライアントで保持している文字列』かを気にせず使用できます。

Contextを渡すのがイマイチ

正直ここまでの内容で全く問題ないのですが、以下の部分で引数にContextを渡しているのがイマイチしっくりきません。

fun getString(context: Context): String = when (this) {
    is Str -> value
    is Res -> context.getString(resId)
}

拡張関数とスコープ関数の1つである with を使えば、よりスマートに書くことができます。
まずはFragment用の拡張関数を使い、以下のように定義します。

sealed interface StringWrapper {
    val text: String
        @Composable
        get() = when(this) {
            is Str -> value
            is Res -> stringResource(resId)
        }
-     fun getString(context: Context): String = when (this) {
-         is Str -> value
-         is Res -> context.getString(resId)
-    }
+    fun Fragment.getString(): String
+    data class Str(val value: String) : StringWrapper {
+        override fun Fragment.getString(): String = value
+    }
+    data class Res(@StringRes val resId: Int) : StringWrapper {
+        override fun Fragment.getString(): String {
+            return getString(resId)
+        }
+    }
}

呼び出し側のFragmentでは以下のように記述できます。

class MyFragment: Fragment() {
    fun doSomething(stringWrapper: StringWrapper) {
-        val str = stringWrapper.getString(requireContext())
+        val str = with(stringWrapper) { getString() }
        println(str)
    }
}

以上、小さなTipsと見せかけて記述量は減らず、メリットがあるかは微妙。自己満足でした。

GitHubで編集を提案

Discussion