Activity,Fragment で Koin 2.2の Scope を利用する
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
記載内容が誤っていたため修正しました。
ScopeActivity, ScopeFragment を継承している場合は Koin 2.1 と同じ。
KoinScopeComponent を実装する場合は、自動的に close が実行されません。
よって、Activity もしくは Fragment の onDestroy
で Scope#close
を実行する必要があります。
Koin 2.2.1 時点では AndroidX の beta 版ライブラリが利用されている
org.koin:koin-androidx-scope
で androidx.activity:activity:1.2.0-beta01
が利用されているようです。
プロジェクトですでに Scope を多用していて、beta 版ライブラリを利用したくない場合は、標準の Scope 機能を利用するしかなさそうですね。
補足
Service の Scope 実装も存在している
ScopeService という Service クラス向けの実装も増えています。
コードを見る感じでは、 Activity,Fragment に比べてやっていることはシンプルですね。
動作確認用のリポジトリを作りました
ほぼサンプルコードそのままですが、実際に動きを確認できるリポジトリを用意しました。
Discussion