はじめに
アーキテクチャと依存性逆転の続きです。アーキテクチャをいじって遊ぶ感じの記事です。
完成品はgithubに上げてあります
Observer
Observerパターンと合わせることができそうですね。
やってみましょう。
まずはCoroutineを入れます。RxJavaでもいいですが自分はCoroutineが好きです。
Lifecycle上でCoroutineをうまく使うためにLifeCycleExtensionsも追加しておきます。
plugins {id 'com.android.application'id 'org.jetbrains.kotlin.android'id 'kotlin-kapt'id 'dagger.hilt.android.plugin'}android {compileSdk 31defaultConfig {applicationId "com.shirobutton.dependency_inversion"minSdk 26targetSdk 31versionCode 1versionName "1.0"testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"}buildTypes {release {minifyEnabled falseproguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'}}compileOptions {sourceCompatibility JavaVersion.VERSION_1_8targetCompatibility JavaVersion.VERSION_1_8}kotlinOptions {jvmTarget = '1.8'}buildFeatures {viewBinding true}}dependencies {implementation 'androidx.core:core-ktx:1.7.0'implementation 'androidx.appcompat:appcompat:1.4.1'implementation 'com.google.android.material:material:1.5.0'implementation 'androidx.constraintlayout:constraintlayout:2.1.3'// Coroutinesimplementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2"implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2"// LifeCycleExtensionsimplementation "androidx.lifecycle:lifecycle-runtime-ktx:2.4.1"// Fragment KTXimplementation "androidx.fragment:fragment-ktx:1.4.1"// Hiltimplementation "com.google.dagger:hilt-android:$hilt_version"kapt "com.google.dagger:hilt-compiler:$hilt_version"testImplementation 'junit:junit:4.13.2'androidTestImplementation 'androidx.test.ext:junit:1.1.3'androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'}
PresenterのFlowをMainActivityでObserveする形にしてみましょう。
まずはPresenterにFlowを追加します。
Presenterの実装ではSharedFlowを使ってFlowを作っています。
SharedFlowに新しい値を流すためには、
suspend関数のemitを使用するのでCoroutineScopeを作ってその中でemitします。
なぜStateFlowでないかというとここに状態を持ちたくないからです。
これでPresenterはMainReceiverすら知らなくなりました。
interface MainPresenter: IncrementUseCaseOutputBoundary {val numberFlow: Flow<Int>}
class MainPresenterImpl @Inject constructor(): MainPresenter {private val mutableNumberFlow = MutableSharedFlow<Int>()override val numberFlow get() = mutableNumberFlowprivate val coroutineScope = CoroutineScope(Dispatchers.Default)override fun incrementOutput(number: Int) {coroutineScope.launch {mutableNumberFlow.emit(number)}}}
MainActivityにPresenterをInjectしてFlowをObserveしてみましょう。
FlowをObserveする方法はcollectなどがあります。
また、ActivityでViewを扱うCoroutineを使う場合、
repeatOnLifecycleを使ってActivityのLifecycleに合わせる必要があります。
この辺りはFragmentならViewLifecycleScopeを使うだけなのでFragmentのほうが楽ですね。
MainReceiverとDriverModuleは必要なくなったので削除しましょう
@AndroidEntryPointclass MainActivity : AppCompatActivity() {@Injectlateinit var controller: MainController@Injectlateinit var presenter: MainPresenterlateinit var binding: ActivityMainBindingvar number: Int = 0override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)binding = ActivityMainBinding.inflate(layoutInflater)binding.textView.text = number.toString()binding.button.setOnClickListener {controller.onIncrementButtonClick(number)}lifecycleScope.launch {repeatOnLifecycle(Lifecycle.State.STARTED) {presenter.numberFlow.collect(::onReceiveNumber)}}setContentView(binding.root)}private fun onReceiveNumber(number: Int){this.number = numberbinding.textView.text = number.toString()}}
UseCaseからPresenterについてもOvserverにできそうですね。
ですが、UseCase自体にFlowを持たせるのは変なので、
application層に新しくFlowを管理するオブジェクトを追加したほうがよさそうです。
Subjectなるものをapplication層に追加してみます。
UseCaseから更新するSubjectと、
Presenterが取得するSubjectが、
別のオブジェクトにならないように@ActivityScoped
をつけます。
また、Presenter側からSubjectの値を変更できたら意味ないので、
Subjectの実装にUseCaseOutputBoundaryを付けてUseCaseから値を変更します。
今回はしませんがMutableSubjectみたいに別のInterfaceにしたほうがいいかもしれないですね。
interface NumberSubject {val flow: Flow<Int>}
@ActivityScopedclass NumberSubjectImpl @Inject constructor(): NumberSubject, IncrementUseCaseOutputBoundary {private val mutableNumberFlow = MutableSharedFlow<Int>()override val flow: Flow<Int> get() = mutableNumberFlowprivate val coroutineScope = CoroutineScope(Dispatchers.Default)override fun incrementOutput(number: Int) {coroutineScope.launch {mutableNumberFlow.emit(number)}}}
@Module@InstallIn(ActivityComponent::class)abstract class ApplicationModule {@Bindsabstract fun bindIncrementUseCase(incrementUseCase: IncrementUseCaseInteractor): IncrementUseCase@Bindsabstract fun bindNumberSubject(numberSubjectImpl: NumberSubjectImpl): NumberSubject@Bindsabstract fun bindIncrementUseCaseOutputBoundary(numberSubjectImpl: NumberSubjectImpl): IncrementUseCaseOutputBoundary}
@Module@InstallIn(ActivityComponent::class)abstract class AdapterModule {@ActivityScoped@Bindsabstract fun bindMainController(mainController: MainControllerImpl): MainController@ActivityScoped@Bindsabstract fun bindMainPresenter(mainPresenterImpl: MainPresenterImpl): MainPresenter}
interface MainPresenter {val numberFlow: Flow<Int>}
class MainPresenterImpl @Inject constructor(private val numberSubject: NumberSubject): MainPresenter {override val numberFlow get() = numberSubject.flow}
Flowを使って下位レイヤーから上位レイヤーにデータを渡すことができました。
この時にinterfaceを使っていたときと同じく、
下位レイヤーは上位レイヤーのことを何も知らないことがポイントですね。
ViewModel
そういえば、Activityが変数を保持していましたね。
AndroidではViewModelを使ってView用のデータを保持します。
ViewModel自体はadapter層の物っぽいですが、
Androidのフレームワークと密接にかかわっているので、
driver層に追加したいと思います。
そして普通にActivityで使います。
@HiltViewModelclass MainViewModel @Inject constructor(presenter: MainPresenter): ViewModel() {val numberFlow = presenter.numberFlow.stateIn(viewModelScope, SharingStarted.Lazily, 0)val number get() = numberFlow.value}
@AndroidEntryPointclass MainActivity : AppCompatActivity() {@Injectlateinit var controller: MainControllerprivate val viewModel: MainViewModel by viewModels()lateinit var binding: ActivityMainBindingoverride fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)binding = ActivityMainBinding.inflate(layoutInflater)binding.textView.text = viewModel.number.toString()binding.button.setOnClickListener {controller.onIncrementButtonClick(viewModel.number)}lifecycleScope.launch {repeatOnLifecycle(Lifecycle.State.STARTED) {viewModel.numberFlow.collect(::onReceiveNumber)}}setContentView(binding.root)}private fun onReceiveNumber(number: Int){binding.textView.text = number.toString()}}
stateInというのはFlowをStateFlowにしてくれるものです。
これで起動するとHilt周りでエラーしてしまいますね。
PresenterがActivityComponentに入っているのでViewModelで使う事ができないというものですね。
なのでPresenterをViewModelComponentに入れます。
しかしこれでもエラーします。
次はSubjectがないというものですね。
これも同じような理由ですが、
SubjectはUseCaseで使うのでActivityComponentにも入っている必要があるので、
SingletonComponentに入れます。
@Module@InstallIn(ActivityComponent::class)abstract class InputAdapterModule {@ActivityScoped@Bindsabstract fun bindMainController(mainController: MainControllerImpl): MainController}@Module@InstallIn(ViewModelComponent::class)abstract class OutputAdapterModule {@ViewModelScoped@Bindsabstract fun bindMainPresenter(mainPresenterImpl: MainPresenterImpl): MainPresenter}
@Module@InstallIn(SingletonComponent::class)abstract class ApplicationModule {@Bindsabstract fun bindIncrementUseCase(incrementUseCase: IncrementUseCaseInteractor): IncrementUseCase@Bindsabstract fun bindNumberSubject(numberSubjectImpl: NumberSubjectImpl): NumberSubject@Bindsabstract fun bindIncrementUseCaseOutputBoundary(numberSubjectImpl: NumberSubjectImpl): IncrementUseCaseOutputBoundary}
@Singletonclass NumberSubjectImpl @Inject constructor(): NumberSubject, IncrementUseCaseOutputBoundary {private val mutableNumberFlow = MutableSharedFlow<Int>()override val flow: Flow<Int> get() = mutableNumberFlowprivate val coroutineScope = CoroutineScope(Dispatchers.Default)override fun incrementOutput(number: Int) {coroutineScope.launch {mutableNumberFlow.emit(number)}}}
これでエラーせず起動できると思います。
図にするとこんな感じです。
おわりに
アーキテクチャをいじって遊ぶのは楽しいですね。
続き書くかわからないですが、もうすこしこれで遊べそうです。
アプリ全体で状態を一つにするRedux的なこともできそうです。
Subject周りが微妙なのでそのあたりがきれいになりそうです。