お久しぶりです。
この1年ほどjava100%のandroidアプリをkotlin化していくというお仕事をしていました。
最近の発表 -> マルチモジュールでandroidアプリを救う
javaとkotlinは非常に相性が良く、段階的な移行を大きな問題なく進めることが出来ました。
一方で、一番苦労した点は肥大化したBaseActivityの移行です。
BaseActivityは様々な要因から肥大化しがちです。
今回は、実務で実際に行ったBaseAdtivityの整理方法についてまとめたいと思います。
1. 今までのBaseActivityをLegacyBaseActivityとする。
2. 新しく新BaseActivityを作り、LegacyBaseActivityに継承させる。
3. 新BaseActivityに本当に必要な処理を移す。
4. LegacyBaseActivityに取り残された処理をよしなに分解する。
ここまで読んで、そもそも新BaseActivityは必要なのか?と思う方も多いと思います。
BaseActivityの不必要論は以前から議論されている内容だとは思いますし、僕もできる限り作らないほうが良いと思っています。
しかし、今回は lifecycleに関係したいくつかの処理 を 必ず全てのactivityで 行わなければならなかったため、BaseActivityを用意しました。
逆に、それ以外の処理はBaseActivityに書かないように(可能な限り)努めています。
具体的にBaseActivityに残したのは以下のような処理です。
実際の処理はLifecycleObserver等を使い、ViewModelに移してあり、BaseActivity自体はできるだけシンプルになるよう努めてあります。
abstract class BaseActivity : AppCompatActivity() {
private val viewModel: AppViewModel by viewModel()
@CallSuper
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycle.addObserver(viewModel.activityLifecycleObserver)
}
class AppViewModel : ViewModel() {
val activityLifecycleObserver = object : LifecycleObserver {
@OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
fun handleResume() {
// onResumeでやりたい処理
// プレミアム会員のチェック等
}
}
先程、lifecycleに関係した必ず全てのactivityで行う処理のみBaseActivityで書くと言いましたが、それ以外の処理について、どう分解していったのかまとめていきたいと思います。
主なBaseActivityの肥大化の理由はこれだと思います。
このActivityでもこのActivityでもこのような処理をしたいという要求から、BaseActivityがあるとついついそこに書きたくなります。
一方、その処理を行わないActivityも存在したり、例外的な処理等を考慮するようになると、BaseActivityが子Activityを知らないといけないという最悪の自体が発生します。
ぐっと我慢してUtilに切り出しましょう。
object WindowUtil {
fun makeStatusBarTransparent(
window: Window,
statusBarColor: Int = Color.TRANSPARENT
) {
window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or
View.SYSTEM_UI_FLAG_LAYOUT_STABLE
window.statusBarColor = statusBarColor
}
}
拡張関数も使えると思います。
fun Window.makeStatusBarTransparent(statusBarColor: Int = Color.TRANSPARENT) {
this.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or
View.SYSTEM_UI_FLAG_LAYOUT_STABLE
this.statusBarColor = statusBarColor
}
次に多いケースがこれだと思います。
interfaceを使うか、Activity scopeのViewModelを使うののが良いと思います。
使い分けは、ロジックを含む場合はviewmodel経由で、ただのviewのイベント通知はinterfaceでやってたりします。
interface LoginHandler {
fun handleLogin()
}
class HogeActivity : BaseActivity(), LoginHandler {
override fun handleLogin() {
// ログイン時の処理
}
}
class FugaFragment : Fragment() {
private fun onLogin() {
val activity = activity ?: return
if (activity is LoginHandler) {
activity.handleLogin()
}
}
}
class ErrorVewModel {
private val _error = MediatorLiveData<AppError>()
val error: LiveData<AppError> get() = _error
fun set(throwable: Throwable) {
_error.value = AppError.of(throwable)
}
}
class HogeActivity : BaseActivity() {
private val errorViewModel: ErrorVewModel by viewModel()
override fun onCreate(savedInstanceState: Bundle?) {
errorViewModel.error.observe(this) {
// エラーの表示
}
}
}
class FugaFragment : Fragment() {
private val errorViewModel: ErrorViewModel by sharedViewModel()
private fun onError(e: Throwable) {
errorViewModel.set(e)
}
}
少し特殊なケースですが、少し前まではbase activityでactiveなactivityを数えるくらいしか方法がなかった気がします。
最近は ProcessLifecycleOwner というやつがいるので、それを使いましょう。
class AppLifecycleObserver : LifecycleObserver {
@OnLifecycleEvent(Lifecycle.Event.ON_START)
fun handleStart() {
// アプリがフォアグラウンドになった場合の処理
}
@OnLifecycleEvent(Lifecycle.Event.ON_STOP)
fun onStop() {
// アプリがバックグラウンドになった場合の処理
}
}
class App : Application() {
override fun onCreate() {
super.onCreate()
ProcessLifecycleOwner.get().lifecycle.addObserver(AppLifecycleObserver())
}
}
今回、どうしてもBaseActivityからは分離できないが、新アーキテクチャではできる限り使いたくない!というケースがいくつかありました。
特にケース2のfragment間の通信は密結合になっていたため、引き剥がしに苦労しました。
僕はinterfaceに切ってDeprecatedをつけるようにしています。
今後改めて時間をとって再整理したいと思います。
@Deprecated(“Use only in legacy modules”)
interface LegacyActivityCallback {
@Deprecated(“Use only in legacy modules”)
fun showActiveMessage(text: String?) {
}
@Deprecated(“Use only in legacy modules”)
fun showErrorMessage(text: String?) {
}
@Deprecated(“Use only in legacy modules”)
fun onRefresh() {
}
}
abstract class BaseActivity : AppCompatActivity(), LegacyActivityCallback {
…
}
class HogeActivity : BaseActivity {
override fun showErrorMessage() {
// エラー表示
}
}
BaseActivityの分解は、ケースバイケースで解決しないといけないことが多く、目の前のことを一つ一つ着実に進めることが大事だなと改めて思いました。
今後少しずつ、リアーキテクチャに関する話題をまとめていきます。