AndroidのActivityやFragmentで文字列と文字列リソースを一括して扱うためのsealed classの作成

Androidで文字列を扱う場合、以下の2パターンのいずれかであることが多いと思います。

  1. クライアント側で定義した文字列リソースを使用する
  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

参考資料