Blog

BaseActivityを解体しよう

お久しぶりです。

この1年ほどjava100%のandroidアプリをkotlin化していくというお仕事をしていました。

最近の発表 -> マルチモジュールでandroidアプリを救う

javaとkotlinは非常に相性が良く、段階的な移行を大きな問題なく進めることが出来ました。

一方で、一番苦労した点は肥大化したBaseActivityの移行です。

BaseActivityは様々な要因から肥大化しがちです。

今回は、実務で実際に行ったBaseAdtivityの整理方法についてまとめたいと思います。

基本戦略

1. 今までのBaseActivityをLegacyBaseActivityとする。
2. 新しく新BaseActivityを作り、LegacyBaseActivityに継承させる。
3. 新BaseActivityに本当に必要な処理を移す。
4. LegacyBaseActivityに取り残された処理をよしなに分解する。

新BaseActivityに必要な処理とは

ここまで読んで、そもそも新BaseActivityは必要なのか?と思う方も多いと思います。

BaseActivityの不必要論は以前から議論されている内容だとは思いますし、僕もできる限り作らないほうが良いと思っています。

しかし、今回は lifecycleに関係したいくつかの処理必ず全てのactivityで 行わなければならなかったため、BaseActivityを用意しました。

逆に、それ以外の処理はBaseActivityに書かないように(可能な限り)努めています。

具体的にBaseActivityに残したのは以下のような処理です。

  • プレミアム会員のチェック
  • 強制アップデート等の表示
  • analyticsへのイベント送信
  • ログの保存

実際の処理はLifecycleObserver等を使い、ViewModelに移してあり、BaseActivity自体はできるだけシンプルになるよう努めてあります。

LifecycleObserverの利用イメージ

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でやりたい処理
            // プレミアム会員のチェック等
        }
}

BaseActivityの分解術

先程、lifecycleに関係した必ず全てのactivityで行う処理のみBaseActivityで書くと言いましたが、それ以外の処理について、どう分解していったのかまとめていきたいと思います。

ケース1: 処理の共通化をしたい

主な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
}

ケース2: FragmentとActivity、Fragment間でデータのやり取りがしたい。

次に多いケースがこれだと思います。

interfaceを使うか、Activity scopeのViewModelを使うののが良いと思います。

使い分けは、ロジックを含む場合はviewmodel経由で、ただのviewのイベント通知はinterfaceでやってたりします。

例: 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()
        }
    }
}

例: activity scopeのview modelを使う

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)
    }
}

ケース3: アプリのバックグラウンド、フォアグラウンドを検知したい。

少し特殊なケースですが、少し前までは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())
    }
}

ケース4: どうにも分離できてねぇ!!!

今回、どうしても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の分解は、ケースバイケースで解決しないといけないことが多く、目の前のことを一つ一つ着実に進めることが大事だなと改めて思いました。

今後少しずつ、リアーキテクチャに関する話題をまとめていきます。

人気の記事

kotlin coroutinesのFlow, SharedFlow, StateFlowを整理する

Jetpack ComposeとKotlin Coroutinesを連携させる

Layout Composableを使って複雑なレイアウトを組む【Jetpack Compose】

テスト用Dispatcherの使い分け【Kotlin Coroutines】

Flow.combineの内部実装がすごい話

Jetpack ComposeのRippleエフェクトを深堀り、カスタマイズも