Androidで文字列を扱う場合、以下の2パターンのいずれかであることが多いと思います。
- クライアント側で定義した文字列リソースを使用する
- サーバー側から受け取った文字列を使用する
これらをActivityやFragmentで一括して扱うために、以下のようなsealed classを用意すると便利でした。
sealed class StringHolder : Parcelable { abstract fun getString(context: Context): String @Parcelize data class Plain(private val value: String) : StringHolder() { override fun getString(context: Context): String = value } @Parcelize class Resource( @StringRes private val resId: Int, private vararg val formatArgs: @RawValue Any ) : StringHolder() { override fun getString(context: Context): String { return if (formatArgs.isNotEmpty()) { context.getString(resId, *formatArgs) } else { context.getString(resId) } } // auto generated override fun equals(other: Any?): Boolean { if (this === other) return true if (javaClass != other?.javaClass) return false other as Resource if (resId != other.resId) return false if (!formatArgs.contentEquals(other.formatArgs)) return false return true } // auto generated override fun hashCode(): Int { var result = resId result = 31 * result + formatArgs.contentHashCode() return result } } }
data classのprimary constructorではvararg
が使用できなかったため、通常のclassを使用しています。
また、Android Studioの「equals() and hashCode()」 actionでequals()
とhashCode()
を自動生成しました。
そして、画面間での受け渡しや画面内でのsave / restoreを考慮し、kotlin-parcelize pluginの@Parcelize
アノテーションを用いてParcelable
の実装を自動生成しています。
formatArgs
には@RawValue
アノテーションを付与しています。@RawValue
アノテーションは、marshallingするときにParcel#writeValue(v: Any)
を使用することをコンパイラに指示するためのアノテーションです。
したがって、formatArgs
に渡すパラメータの型によってはRuntimeExceptionが発生する可能性があるのですが、Parcel#writeValue(v: Any)が対応している型を見る限りはおそらく問題なさそうでした(時間があるときにテストコードを追加しようと思います🙏)。
このsealed classを使うとViewModelとActivityの実装は以下のようになり、Activityでは文字列のパターンを意識する必要がなくなります。
// MainViewModel.kt class MainViewModel: ViewModel() { private val _textForPlain = MutableLiveData<StringHolder>() val textForPlain: LiveData<StringHolder> get() = _textForPlain private val _textForResource = MutableLiveData<StringHolder>() val textForResource: LiveData<StringHolder> get() = _textForResource init { // emit dummy data _textForPlain.value = StringHolder.Plain("Hello World!") _textForResource.value = StringHolder.Resource(R.string.app_name) } } // MainActivity.kt class MainActivity : AppCompatActivity() { private lateinit var binding: ActivityMainBinding private lateinit var viewModel: MainViewModel override fun onCreate(savedInstanceState: Bundle?) { ... viewModel.textForPlain.observe(this) { binding.textViewForPlain.text = it.getString(this) } viewModel.textForResource.observe(this) { binding.textViewForResource.text = it.getString(this) } } }
サンプルコードは以下になります。
github.com