Activity,Fragment で Koin 2.2の Scope を利用する

公開:2020/12/06
更新:2020/12/21
9 min読了の目安(約8200字TECH技術記事

Koin のバージョンが 2.2 になって、Android 向けの Scope 実装が大きく変わったようです。
Activity,Fragment 向けに提供されている機能を利用してみたところ、若干ハマったところがあったので使い方のメモとして残します。
また、この記事は AndroidX 向けライブラリ(org.koin:koin-androidx-scope) についての内容です。

対象読者

  • Koin 2.2 について調べている
  • Koin 2.1 についてある程度理解がある
    • 基本的な利用方法
    • Scope
  • AndroidX の Lifecycle,ViewModel についてある程度理解がある

Scopeについて

公式ドキュメントには、オブジェクトの保持と取得を特定の期間内のみ有効にする機能であると記載されています。

Scope is a fixed duration of time or method calls in which an object exists.

Android で利用する場合は、 Activity や Fragment が生成されてから破棄されるまでに利用するクラス (ViewModel など) を管理する用途で使われることが多いと思われます。

Scope の利用

module での Scope 定義

Koin 2.1, 2.2 で実装方法は変化しないです。

module {
    // SampleActivityクラスに関連づける場合
    scope<SampleActivity> {
        scoped { SampleClass() }
        viewModel { SampleViewModel() }
    }
}

おさらい: Koin 2.1 までの Scope 利用方法

LifecycleOwner の拡張関数として定義されていた lifecycleScope を使うことで、 LifecycleOwner に紐づいた Scope を生成でき、module の scope 定義を読み込むことができました。

また、通常 Scope は利用が終わったタイミングで close メソッドを呼び出す必要がありますが、 lifecycleScope を利用すると自動的に close が実行されます。
lifecycleScope 利用時に、close を実行する LifecycleObserver を LifecycleOwner に登録することで実現しているようです。

class SampleActivity : AppCompatActivity {
    private val sampleClass by lifecycleScope.inject<SampleClass>()
    private val sampleViewModel by lifecycleScope.viewModel<SampleViewModel>()

~

Koin 2.2 からの Scope 利用方法

Activity, Fragment での Scope を利用するための方法としては、以下の2パターンがあります。

  • ScopeActivity, ScopeFragment を利用する
  • KoinScopeComponent を利用する

ScopeActivity,ScopeFragment を利用する

ScopeActivity は androidx.appcompat.app.AppCompatActivity を、ScopeFragment は AndroidX の androidx.fragment.app.Fragment を継承したクラスです。

それぞれのクラスで、 Scope と、 Scope を参照する get,inject 関数が定義されてます。
継承することで、コードを書くときに Scope を意識せず扱えるようになります。
また、 viewModel() 関数は ScopeActivity,ScopeFragment の拡張関数として実装されています。

// Fragment の場合は ScopeFragment  を継承
class SampleActivity : ScopeActivity() {
    private val sampleClass by inject<SampleClass>()
    private val sampleViewModel by viewModel<SampleViewModel>()
    
~

Scope の close も自動で行われます。
詳しくは、後述する activityScope() 関数と一緒に説明します。

KoinScopeComponent を利用する

実装については Activity,Fragment 以外で Koin 2.2 の Scope を利用する方法とほぼ同じです。
Scope の生成方法が異なり、 Activity で Scope を生成する場合は activityScope() 関数を実行します。

class SampleActivity : AppCompatActivity(), KoinScopeComponent {
    // Fragmentの場合は fragmentScope() を利用
    override val scope: Scope by lazy { activityScope() }

    private val sampleClass by inject<SampleClass>()
    
    // Scope向けviewModel関数が存在しない
    // また、onCreate以前にscopeが評価されると RuntimeException が発生するため
    // lazyで評価を遅延させる
    private val sampleViewModel by lazy<SampleViewModel> {
        scope.getViewModel(owner = { ViewModelOwner.from(this, this) })
    } 
~

この関数は、 Activity 向けの Scope を自動生成してかつ、 ScopeHandlerViewModel という ViewModel の生成と Activity への紐づけを実行してくれます。
ScopeHandlerViewModel は、 Scope の close を onCleared のタイミングで実行します。

ScopeActivity 内部での Scope も、この activityScope() で生成されているため、 close が不要になります。

ScopeActivity を継承する方法と比較して面倒なこと

ScopeActivity では、拡張関数として Scope を意識せずに実装可能な viewModel() 関数が用意されているのですが、 KoinScopeComponent では同等の処理が存在しません。
そのため、現時点の Koin の実装では scope.getViewModel() のように scope 変数を直接参照する必要があります。

現状の実装だと、 KoinScopeComponent を実装しているクラスが Activity なのか Fragment なのか、そのどちらでもないのかを拡張関数などで正確に判定することが難しいため、用意されていないのだと予想しています。

Koin 2.2 からの Scope 利用方法 (Java)

そもそも利用できるのか?と思い試しに実装してみたら、 inject が使えないなどの制約ができてしまうものの、なんとか利用できるようです。
若干複雑なコードになってしまうので、 Java では書きたくないですね。

ScopeActivity,ScopeFragment を利用する

ScopeActivity に定義されている関数がインライン関数であるため、 Java から参照できません。
そのため、 scope 変数を直接参照して Scope#get を実行しています。 Scope#inject も残念ながらインライン関数でした...
ViewModel のほうは、拡張関数を使うことで Scope を直接参照せずに実行できていますが、 Java から参照した場合はデフォルト引数が設定されないため、デフォルト引数と同様の内容を渡す必要があったりとややこしいです。

public class SampleActivity extends ScopeActivity {
    private SampleClass sampleClass;
    private SampleViewModel sampleViewModel;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_sample_layout);

        // Scope の inject は inline 関数なので Java から参照できない
        sampleClass = getScope().get(SampleClass.class);
        sampleViewModel = ScopedActivityExtKt.getViewModel(
                this,
                null,
                null,
                () -> ViewModelOwner.Companion.from(
                        SampleActivity.this,
                        SampleActivity.this
                ),
                JvmClassMappingKt.getKotlinClass(SampleViewModel.class),
                null
        );
~	

ScopeActivity,ScopeFragment を利用しない場合

scope 変数や拡張関数を直接呼び出すので、 KoinScopeComponent は不要です。
ScopeActivity を継承した場合と異なるのは、 ScopeExtKt#getViewModel を利用しているところです。

public class SampleActivity extends AppCompatActivity {

    private Scope scope;

    private SampleClass sampleClass;
    private SampleViewModel sampleViewModel;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_sample_layout);

        // Fragmentの場合はFragmentExtkt.fragmentScope() を利用
        scope = ComponentActivityExtKt.activityScope(this);

        sampleClass = scope.get(SampleClass.class);
        sampleViewModel = ScopeExtKt.getViewModel(
                scope,
                null,
                null,
                () -> ViewModelOwner.Companion.from(
                        SampleActivity.this,
                        SampleActivity.this
                ),
                JvmClassMappingKt.getKotlinClass(SampleViewModel.class),
                null
        );
~

[追記] Koin 2.2 からの新機能

[Koin 2.2.2] ScopeActivity, ScopeFragment を利用すると自動的に Scope Linking が実行される

ScopeFragment を継承した Fragment の親 Activity が ScopeActivity を継承している場合、 Activity,Fragment 間で自動的に Scope Linking が実行されます。
実装サンプルはこちら

Koin 2.2 での注意点

onCreate 以前のタイミングで Scope を生成すると RuntimeException が発生する

前提として、onCreate 以前のタイミングで ViewModel を生成しようとすると RuntimeException が発生します。
Koin 2.2 では Scope を ViewModel で管理する仕様となっているため、 onCreate 以前のタイミングで Scope を生成しようとすると同様の問題が発生してしまいます。

そのため、コンストラクタで get など Scope を参照して即時評価させてしまう処理を実装してしまった場合は、クラッシュしてしまいます。
特に注意が必要なのは、Activity,Fragment で KoinScopeComponent を利用して、 ViewModel を生成する場合だと思っています。
sampleViewModel by scope.viewModel<SampleViewModel>() のような実装を書いてしまうとクラッシュしてしまうのです。

一見 sampleViewModel が遅延評価されるので問題なさそうにも見えますが、 scope 変数についてはコンストラクタで評価されてしまうため、onCreate 以前のタイミングで ViewModel が生成されてしまいます。
対処方法としては、サンプルコードとして書いてある通り lazy でラップして Scope の評価を遅延させることが挙げられます。

ちなみに、ScopeActivity,ScopeFragment や拡張関数として定義されている inject 関数を利用する場合は Scope も含め遅延評価されるため気にする必要ありません。

Scope が自動的に close されるタイミングが Koin 2.1と異なる

Koin 2.1

対象 Activity,Fragment が onDestroy に入ったタイミングで close が実行されます。

Koin 2.2

ViewModel#onCleared で実行されるため、 2.1 と比較して Scope の生存期間が長くなります。

Koin 2.2.1 時点では AndroidX の beta 版ライブラリが利用されている

org.koin:koin-androidx-scopeandroidx.activity:activity:1.2.0-beta01 が利用されているようです。

プロジェクトですでに Scope を多用していて、beta 版ライブラリを利用したくない場合は、標準の Scope 機能を利用するしかなさそうですね。

補足

Service の Scope 実装も存在している

ScopeService という Service クラス向けの実装も増えています。
コードを見る感じでは、 Activity,Fragment に比べてやっていることはシンプルですね。

動作確認用のリポジトリを作りました

ほぼサンプルコードそのままですが、実際に動きを確認できるリポジトリを用意しました。

参考