Androidクリーンアーキテクチャのメリットをご覧ください
公開: 2022-03-11あなたは何を好みますか:ひどいアーキテクチャで非常にうまく機能しているアプリに新しい機能を追加するか、うまく設計されているがバグのあるAndroidアプリケーションのバグを修正しますか? 個人的には、間違いなく2番目のオプションを選択します。 すべてのクラスのすべてからのすべての依存関係を考慮すると、新しい機能を追加することは、単純なものであっても、アプリで非常に面倒になる可能性があります。 私のAndroidプロジェクトの1つを覚えています。そこでは、プロジェクトマネージャーから、データをダウンロードして新しい画面に表示するなどの小さな機能を追加するように求められました。 それは、新しい仕事を見つけた私の同僚の1人によって書かれたアプリでした。 この機能は、1営業日の半分以上かかることはありません。 私はとても楽観的でした…
アプリがどのように機能するか、どのモジュールがあり、それらがどのように相互に通信するかを7時間調査した後、この機能のいくつかの試用実装を行いました。 地獄だった。 データモデルの小さな変更により、ログイン画面が大幅に変更されました。 ネットワークリクエストを追加するには、ほぼすべての画面とGodOnlyKnowsWhatThisClassDoes
クラスの実装を変更する必要がありました。 ボタンの色の変更により、データをデータベースに保存するときに奇妙な動作が発生したり、アプリ全体がクラッシュしたりしました。 翌日の半ば、私はプロジェクトマネージャーに次のように話しました。「この機能を実装するには2つの方法があります。 最初に、私はそれにさらに3日を費やすことができ、最終的には非常に汚い方法でそれを実装し、次のすべての機能またはバグ修正の実装時間は指数関数的に増加します。 または、アプリを書き直すこともできます。 これには2、3週間かかりますが、将来のアプリの変更に備えて時間を節約できます。」幸い、彼は2番目のオプションに同意しました。 アプリの優れたソフトウェアアーキテクチャ(非常に小さなものでも)がなぜ重要なのか疑問に思ったことがある場合、このアプリはそれらを完全に排除しました。 しかし、このような問題を回避するには、どのAndroidアーキテクチャパターンを使用する必要がありますか?
この記事では、Androidアプリのクリーンなアーキテクチャの例を紹介します。 ただし、このパターンの主なアイデアは、すべてのプラットフォームと言語に適合させることができます。 優れたアーキテクチャは、プラットフォーム、言語、データベースシステム、入力、出力などの詳細から独立している必要があります。
サンプルアプリ
次の機能を使用して現在地を登録するための簡単なAndroidアプリを作成します。
- ユーザーは名前でアカウントを作成できます。
- ユーザーはアカウント名を編集できます。
- ユーザーはアカウントを削除できます。
- ユーザーはアクティブなアカウントを選択できます。
- ユーザーは場所を保存できます。
- ユーザーは、ユーザーのロケーションリストを表示できます。
- ユーザーはユーザーのリストを見ることができます。
クリーンなアーキテクチャ
レイヤーは、クリーンなアーキテクチャの主要なコアです。 このアプリでは、プレゼンテーション、ドメイン、モデルの3つのレイヤーを使用します。 各レイヤーは分離する必要があり、他のレイヤーについて知る必要はありません。 それはそれ自身の世界に存在し、せいぜい通信するための小さなインターフェースを共有するべきです。
レイヤーの責任:
- ドメイン:アプリのビジネスルールが含まれています。 アプリの機能を反映したユースケースを提供する必要があります。
- プレゼンテーション:ユーザーにデータを提示し、ユーザー名などの必要なデータも収集します。 これは一種の入出力です。
- モデル:アプリのデータを提供します。 外部ソースからデータを取得し、データベースやクラウドサーバーなどに保存する責任があります。
どのレイヤーが他のレイヤーについて知っておくべきですか? 答えを得る最も簡単な方法は、変更について考えることです。 プレゼンテーション層を見てみましょう。ユーザーに何かを提示します。 プレゼンテーションで何かを変更する場合、モデルレイヤーも変更する必要がありますか? ユーザーの名前と最後の場所が表示された「ユーザー」画面があるとします。 1つだけではなく、ユーザーの最後の2つの場所を表示する場合は、モデルに影響を与えないでください。 したがって、最初の原則があります。プレゼンテーション層はモデル層について知りません。
そして、その逆です。モデル層はプレゼンテーション層について知っている必要がありますか? 繰り返しになりますが、たとえば、データベースからネットワークにデータのソースを変更した場合、UIの内容は変更されないためです(ここでローダーを追加することを検討した場合)。はい。ただし、UIローダーを使用することもできます。データベースを使用する場合)。 したがって、2つのレイヤーは完全に分離されています。 すごい!
ドメインレイヤーはどうですか? すべての主要なビジネスロジックが含まれているため、これは最も重要なものです。 これは、データをモデルレイヤーに渡す前、またはユーザーに提示する前にデータを処理する場所です。 他のレイヤーから独立している必要があります。データベース、ネットワーク、またはユーザーインターフェイスについては何も知りません。 これがコアであるため、他のレイヤーはこのレイヤーとのみ通信します。 なぜこれを完全に独立させたいのですか? ビジネスルールは、おそらくUIデザインや、データベースやネットワークストレージ内の何かよりも頻繁に変更されることはありません。 提供されているいくつかのインターフェースを介してこのレイヤーと通信します。 具体的なモデルやUIの実装は使用しません。 これらは詳細であり、覚えておいてください。詳細は変更されます。 優れたアーキテクチャは細部にとらわれません。
今のところ十分な理論。 コーディングを始めましょう! この記事はコードを中心に展開しているため、理解を深めるために、GitHubからコードをダウンロードして、中身を確認する必要があります。 作成された3つのGitタグ(architecture_v1、architecture_v2、architecture_v3)があり、これらは記事の部分に対応しています。
アプリテクノロジー
アプリでは、依存性注入にKotlinとDagger2を使用しています。 ここではKotlinもDagger2も必要ありませんが、これにより作業がはるかに簡単になります。 私がRxJava(またはRxKotlin)を使用していないことに驚かれるかもしれませんが、ここでは使用できませんでした。ライブラリが最上位にあり、誰かが必須だと言っているという理由だけで、ライブラリを使用するのは好きではありません。 私が言ったように、言語とライブラリは詳細なので、好きなものを使用できます。 一部のAndroidユニットテストライブラリも使用されます:JUnit、Robolectric、およびMockito。
ドメイン
Androidアプリケーションアーキテクチャの設計で最も重要なレイヤーはドメインレイヤーです。 それから始めましょう。 これは、ビジネスロジックと他のレイヤーと通信するためのインターフェイスが配置される場所です。 主なコアはUseCase
であり、ユーザーがアプリでできることを反映しています。 それらの抽象化を準備しましょう:
abstract class UseCase<out Type, in Params> { private var job: Deferred<OneOf<Failure, Type>>? = null abstract suspend fun run(params: Params): OneOf<Failure, Type> fun execute(params: Params, onResult: (OneOf<Failure, Type>) -> Unit) { job?.cancel() job = async(CommonPool) { run(params) } launch(UI) { val result = job!!.await() onResult(result) } } open fun cancel() { job?.cancel() } open class NoParams }
ここでKotlinのコルーチンを使用することにしました。 各UseCase
は、データを提供するためにrunメソッドを実装する必要があります。 このメソッドはバックグラウンドスレッドで呼び出され、結果を受け取った後、UIスレッドで配信されます。 返されるタイプはOneOf<F, T>
です。データでエラーまたは成功を返すことができます。
sealed class OneOf<out E, out S> { data class Error<out E>(val error: E) : OneOf<E, Nothing>() data class Success<out S>(val data: S) : OneOf<Nothing, S>() val isSuccess get() = this is Success<S> val isError get() = this is Error<E> fun <E> error(error: E) = Error(error) fun <S> success(data: S) = Success(data) fun oneOf(onError: (E) -> Any, onSuccess: (S) -> Any): Any = when (this) { is Error -> onError(error) is Success -> onSuccess(data) } }
ドメインレイヤーには独自のエンティティが必要なので、次のステップはそれらを定義することです。 今のところ、 User
とUserLocation
の2つのエンティティがあります。
data class User(var id: Int? = null, val name: String, var isActive: Boolean = false) data class UserLocation(var id: Int? = null, val latitude: Double, val longitude: Double, val time: Long, val userId: Int)
返すデータがわかったので、データプロバイダーのインターフェイスを宣言する必要があります。 これらはIUsersRepository
とILocationsRepository
になります。 これらはモデルレイヤーに実装する必要があります。
interface IUsersRepository { fun setActiveUser(userId: Int): OneOf<Failure, User> fun getActiveUser(): OneOf<Failure, User?> fun createUser(user: User): OneOf<Failure, User> fun removeUser(userId: Int): OneOf<Failure, User?> fun editUser(user: User): OneOf<Failure, User> fun users(): OneOf<Failure, List<User>> } interface ILocationsRepository { fun locations(userId: Int): OneOf<Failure, List<UserLocation>> fun addLocation(location: UserLocation): OneOf<Failure, UserLocation> }
この一連のアクションは、アプリに必要なデータを提供するのに十分なはずです。 この段階では、データの保存方法は決定していません。これは、独立させたい詳細です。 今のところ、私たちのドメインレイヤーはそれがAndroid上にあることさえ知りません。 この状態を維持しようとします(並べ替え。後で説明します)。
最後の(またはほぼ最後の)ステップは、プレゼンテーションデータで使用されるUseCase
の実装を定義することです。 それらはすべて非常に単純です(アプリとデータが単純であるように)。これらの操作は、リポジトリから適切なメソッドを呼び出すように制限されています。例:
class GetLocations @Inject constructor(private val repository: ILocationsRepository) : UseCase<List<UserLocation>, UserIdParams>() { override suspend fun run(params: UserIdParams): OneOf<Failure, List<UserLocation>> = repository.locations(params.userId) }
Repository
の抽象化により、 UseCases
のテストが非常に簡単になります。ネットワークやデータベースを気にする必要はありません。 これはどのような方法でもモックできるため、単体テストでは実際のユースケースをテストし、他の無関係なクラスはテストしません。 これにより、単体テストがシンプルかつ高速になります。
@RunWith(MockitoJUnitRunner::class) class GetLocationsTests { private lateinit var getLocations: GetLocations private val locations = listOf(UserLocation(1, 1.0, 1.0, 1L, 1)) @Mock private lateinit var locationsRepository: ILocationsRepository @Before fun setUp() { getLocations = GetLocations(locationsRepository) } @Test fun `should call getLocations locations`() { runBlocking { getLocations.run(UserIdParams(1)) } verify(locationsRepository, times(1)).locations(1) } @Test fun `should return locations obtained from locationsRepository`() { given { locationsRepository.locations(1) }.willReturn(OneOf.Success(locations)) val returnedLocations = runBlocking { getLocations.run(UserIdParams(1)) } returnedLocations shouldEqual OneOf.Success(locations) } }
これで、ドメインレイヤーは終了です。
モデル
Android開発者は、データを保存するための新しいAndroidライブラリであるRoomを選択するでしょう。 しかし、管理者がRoom、Realm、およびいくつかの新しい超高速ストレージライブラリのいずれかを決定しようとしているため、プロジェクトマネージャーがデータベースに関する決定を延期できるかどうか尋ねたと想像してみてください。 UIの操作を開始するにはデータが必要なので、今はそれをメモリに保持します。
class MemoryLocationsRepository @Inject constructor(): ILocationsRepository { private val locations = mutableListOf<UserLocation>() override fun locations(userId: Int): OneOf<Failure, List<UserLocation>> = OneOf.Success(locations.filter { it.userId == userId }) override fun addLocation(location: UserLocation): OneOf<Failure, UserLocation> { val addedLocation = location.copy(id = locations.size + 1) locations.add(addedLocation) return OneOf.Success(addedLocation) } }
プレゼンテーション
2年前、Android向けの非常に優れたアプリ構造としてMVPに関する記事を書きました。 GoogleがAndroidアプリケーション開発をはるかに簡単にする優れたアーキテクチャコンポーネントを発表したとき、MVPは不要になり、MVVMに置き換えることができます。 ただし、このパターンのいくつかのアイデアは、ダムビューに関するもののように、依然として非常に役立ちます。 彼らはデータの表示だけを気にする必要があります。 これを実現するために、ViewModelとLiveDataを利用します。
アプリのデザインは非常にシンプルです。1つのアクティビティには下部ナビゲーションがあり、2つのメニューエントリにlocations
のフラグメントまたはusers
のフラグメントが表示されます。 これらのビューでは、ViewModelsを使用しますUseCase
はドメインレイヤーのUseCaseを使用して、通信をきちんとシンプルに保ちます。 たとえば、 LocationsViewModel
は次のとおりです。
class LocationsViewModel @Inject constructor(private val getLocations: GetLocations, private val saveLocation: SaveLocation) : BaseViewModel() { var locations = MutableLiveData<List<UserLocation>>() fun loadLocations(userId: Int) { getLocations.execute(UserIdParams(userId)) { it.oneOf(::handleError, ::handleLocationsChange) } } fun saveLocation(location: UserLocation, onSaved: (UserLocation) -> Unit) { saveLocation.execute(UserLocationParams(location)) { it.oneOf(::handleError) { location -> handleLocationSave(location, onSaved) } } } private fun handleLocationSave(location: UserLocation, onSaved: (UserLocation) -> Unit) { val currentLocations = locations.value?.toMutableList() ?: mutableListOf() currentLocations.add(location) this.locations.value = currentLocations onSaved(location) } private fun handleLocationsChange(locations: List<UserLocation>) { this.locations.value = locations } }
ViewModelsに慣れていない人のための簡単な説明—データはlocations変数に保存されます。 getLocations
ユースケースからデータを取得すると、それらはLiveData
値に渡されます。 この変更により、オブザーバーに通知され、オブザーバーが反応してデータを更新できるようになります。 フラグメント内のデータのオブザーバーを追加します。
class LocationsFragment : BaseFragment() { ... private fun initLocationsViewModel() { locationsViewModel = ViewModelProviders.of(activity!!, viewModelFactory)[LocationsViewModel::class.java] locationsViewModel.locations.observe(this, Observer<List<UserLocation>> { showLocations(it ?: emptyList()) }) locationsViewModel.error.observe(this, Observer<Failure> { handleError(it) }) } private fun showLocations(locations: List<UserLocation>) { locationsAdapter.locations = locations } private fun handleError(error: Failure?) { toast(R.string.user_fetch_error).show() } }
場所が変更されるたびに、リサイクラービューに割り当てられたアダプターに新しいデータを渡すだけです。これが、リサイクラービューにデータを表示するための通常のAndroidフローです。

ビューでViewModelを使用しているため、その動作も簡単にテストできます。ViewModelをモックするだけで、データソース、ネットワーク、その他の要素を気にする必要はありません。
@RunWith(RobolectricTestRunner::class) @Config(application = TestRegistryRobolectricApplication::class) class LocationsFragmentTests { private var usersViewModel = mock(UsersViewModel::class.java) private var locationsViewModel = mock(LocationsViewModel::class.java) lateinit var fragment: LocationsFragment @Before fun setUp() { UsersViewModelMock.intializeMock(usersViewModel) LocationsViewModelMock.intializeMock(locationsViewModel) fragment = LocationsFragment() fragment.viewModelFactory = ViewModelUtils.createFactoryForViewModels(usersViewModel, locationsViewModel) startFragment(fragment) } @Test fun `should getActiveUser on start`() { Mockito.verify(usersViewModel).getActiveUser() } @Test fun `should load locations from active user`() { usersViewModel.activeUserId.value = 1 Mockito.verify(locationsViewModel).loadLocations(1) } @Test fun `should display locations`() { val date = Date(1362919080000)//10-03-2013 13:38 locationsViewModel.locations.value = listOf(UserLocation(1, 1.0, 2.0, date.time, 1)) val recyclerView = fragment.find<RecyclerView>(R.id.locationsRecyclerView) recyclerView.measure(100, 100) recyclerView.layout(0,0, 100, 100) val adapter = recyclerView.adapter as LocationsListAdapter adapter.itemCount `should be` 1 val viewHolder = recyclerView.findViewHolderForAdapterPosition(0) as LocationsListAdapter.LocationViewHolder viewHolder.latitude.text `should equal` "Lat: 1.0" viewHolder.longitude.text `should equal` "Lng: 2.0" viewHolder.locationDate.text `should equal` "10-03-2013 13:38" } }
プレゼンテーション層も明確な境界線を持つ小さな層に分離されていることに気付くかもしれません。 activities
、 fragments
、 ViewHolders
などのビューは、データの表示のみを担当します。 彼らはViewModelレイヤーについてのみ認識しており、ユーザーと場所を取得または送信するためにそれのみを使用します。 ドメインと通信するViewModelです。 ビューモデルの実装は、ユースケースがドメイン用であるのと同じです。 言い換えると、クリーンなアーキテクチャはタマネギのようなものです。レイヤーがあり、レイヤーにもレイヤーを含めることができます。
依存性注入
アーキテクチャのすべてのクラスを作成しましたが、もう1つやるべきことがあります。それは、すべてを接続する何かが必要なことです。 プレゼンテーション、ドメイン、およびモデルレイヤーはクリーンに保たれますが、ダーティモジュールであり、すべてについてすべてを知っている1つのモジュールが必要です。この知識によって、レイヤーを接続できるようになります。 それを作成する最良の方法は、一般的なデザインパターンの1つ(SOLIDで定義されたクリーンなコード原則の1つ)を使用することです。依存性注入は、適切なオブジェクトを作成し、それらを目的の依存性に注入します。 ここではDagger2を使用しました(プロジェクトの途中で、バージョンを2.16に変更しました。これにより、ボイラープレートが少なくなります)が、任意のメカニズムを使用できます。 最近、コインライブラリーで少し遊んだのですが、試してみる価値もあると思います。 ここで使用したかったのですが、テスト時にViewModelのモックを作成する際に多くの問題が発生しました。 それらをすばやく解決する方法を見つけたいと思います。そうであれば、KoinとDagger2を使用するときにこのアプリの違いを提示できます。
タグarchitecture_v1を使用して、GitHubでこの段階のアプリを確認できます。
変更点
レイヤーを完成させ、アプリをテストしました。すべてが機能しています。 1つを除いて、PMが使用するデータベースを知る必要があります。 彼らがあなたのところに来て、経営陣がRoomの使用に同意したと言ったと仮定しますが、将来的には最新の超高速ライブラリを使用する可能性があるため、潜在的な変更を念頭に置く必要があります。 また、利害関係者の1人が、データをクラウドに保存できるかどうかを尋ね、そのような変更のコストを知りたいと考えています。 したがって、これは、アーキテクチャが優れているかどうか、およびプレゼンテーションやドメイン層を変更せずにデータストレージシステムを変更できるかどうかを確認するときです。
変更1
Roomを使用するときの最初のことは、データベースのエンティティを定義することです。 すでにいくつかあります: User
とUserLocation
。 @Entity
や@PrimaryKey
などのアノテーションを追加するだけで、データベースを使用してモデルレイヤーで使用できます。 すごい! これは、維持したいすべてのアーキテクチャルールを破る優れた方法です。 実際、この方法でドメインエンティティをデータベースエンティティに変換することはできません。 ネットワークからデータをダウンロードしたいと想像してみてください。 さらにいくつかのクラスを使用してネットワーク応答を処理できます。つまり、単純なエンティティを変換して、データベースとネットワークで機能するようにします。 これが、将来の大惨事への最短経路です(そして、「誰がこのコードを書いたのか」と叫びます)。 使用するデータストレージタイプごとに個別のエンティティクラスが必要です。 それほど費用はかからないので、Roomエンティティを正しく定義しましょう。
@Entity data class UserEntity( @PrimaryKey(autoGenerate = true) var id: Long?, @ColumnInfo(name = "name") var name: String, @ColumnInfo(name = "isActive") var isActive: Boolean = false ) @Entity(foreignKeys = [ ForeignKey(entity = UserEntity::class, parentColumns = [ "id" ], childColumns = [ "userId" ], onDelete = CASCADE) ]) data class UserLocationEntity( @PrimaryKey(autoGenerate = true) var id: Long?, @ColumnInfo(name = "latitude") var latitude: Double, @ColumnInfo(name = "longitude") var longitude: Double, @ColumnInfo(name = "time") var time: Long, @ColumnInfo(name = "userId") var userId: Long )
ご覧のとおり、これらはドメインエンティティとほぼ同じであるため、それらをマージするという大きな誘惑があります。 これは単なる偶然です。データが複雑になると、類似性は小さくなります。
次に、 UserDAO
とUserLocationsDAO
、 AppDatabase
を実装し、最後にIUsersRepository
とILocationsRepository
の実装を実装する必要があります。 ここに小さな問題がありますUserLocation
はILocationsRepository
を返す必要がありますが、データベースからUserLocationEntity
を受け取ります。 User
関連のクラスについても同じです。 反対の方向では、データベースにUserLocationEntity
が必要な場合にUserLocation
を渡します。 これを解決するには、ドメインとデータエンティティの間にMappers
が必要です。 私はお気に入りのKotlin機能の1つである拡張機能を使用しました。 Mapper.kt
という名前のファイルを作成し、そこにクラス間でマッピングするためのすべてのメソッドを配置しました(もちろん、モデルレイヤーにあります。ドメインはそれを必要としません)。
fun User.toEntity() = UserEntity(id?.toLong(), name, isActive) fun UserEntity.toUser() = User(this.id?.toInt(), name, isActive) fun UserLocation.toEntity() = UserLocationEntity(id?.toLong(), latitude, longitude, time, userId.toLong()) fun UserLocationEntity.toUserLocation() = UserLocation(id?.toInt(), latitude, longitude, time, userId.toInt())
私が前に述べた小さな嘘は、ドメインエンティティに関するものです。 彼らはAndroidについて何も知らないと書いたが、これは完全に真実ではない。 @Parcelize
アノテーションをUser
エンティティに追加し、そこでParcelable
を拡張して、エンティティをフラグメントに渡すことができるようにしました。 より複雑な構造の場合は、ビューレイヤー自体のデータクラスを提供し、ドメインモデルとデータモデルの間のようにマッパーを作成する必要があります。 ドメインエンティティにParcelable
を追加することは、私があえて取る小さなリスクです。私はそれを認識しています。 User
エンティティが変更された場合は、プレゼンテーション用に個別のデータクラスを作成し、ドメインレイヤーからParcelable
を削除します。
最後に行うことは、依存性注入モジュールを変更して、以前のMemoryRepository
の代わりに新しく作成されたRepository
実装を提供することです。 アプリをビルドして実行したら、PMに移動して、Roomデータベースで動作中のアプリを表示できます。 また、ネットワークの追加にそれほど時間はかからないこと、および管理者が必要とする任意のストレージライブラリを利用できることをPMに通知することもできます。 変更されたファイルを確認できます。モデルレイヤー内のファイルのみです。 私たちのアーキテクチャは本当にきれいです! 次のすべてのストレージタイプは、リポジトリを拡張して適切な実装を提供するだけで、同じ方法で構築できます。 もちろん、データベースやネットワークなど、複数のデータソースが必要になる可能性があります。 では、どうしますか? 何もしません。3つのリポジトリ実装を作成する必要があります。1つはネットワーク用、もう1つはデータベース用、メインの実装では、正しいデータソースが選択されます(たとえば、ネットワークがある場合は、ネットワーク、そうでない場合はデータベースからロード)。
タグarchitecture_v2を使用して、GitHubでこのステージのアプリをチェックアウトできます。
これで、1日がほぼ終わりました。コーヒーを飲みながらパソコンの前に座っていると、アプリをGoogle Playに送信する準備が整いました。突然、プロジェクトマネージャーがあなたのところに来て、「 GPSからユーザーの現在地を保存できますか?」
変更2
すべてが変わります…特にソフトウェア。 これが、クリーンなコードとクリーンなアーキテクチャが必要な理由です。 ただし、考えずにコーディングしていると、最もクリーンなものでも汚れることがあります。 GPSから位置を取得することを実装するときの最初の考えは、アクティビティにすべての位置認識コードを追加し、それをUserLocation
SaveLocationDialogFragment
作成することです。 これが最速の方法かもしれません。 しかし、私たちのクレイジーなPMが私たちのところに来て、位置情報をGPSから他のプロバイダー(Bluetoothやネットワークなど)に変更するように要求した場合はどうなりますか? 変更はすぐに手に負えなくなるでしょう。 どうすればきれいな方法でそれを行うことができますか?
ユーザーの場所はデータです。 また、場所の取得はUseCase
です。したがって、ドメインレイヤーとモデルレイヤーもここに含める必要があると思います。 したがって、実装するGetCurrentLocation
ケースがもう1つありますUseCase
。 また、GPSセンサーなどの詳細からUseCase
を独立させるために、場所を提供するILocationProvider
インターフェイスも必要です。
interface ILocationProvider { fun getLocation(): OneOf<Failure, SimpleLocation> fun cancel() } class GetCurrentLocation @Inject constructor(private val locationProvider: ILocationProvider) : UseCase<SimpleLocation, UseCase.NoParams>() { override suspend fun run(params: NoParams): OneOf<Failure, SimpleLocation> = locationProvider.getLocation() override fun cancel() { super.cancel() locationProvider.cancel() } }
ここには、キャンセルという1つの追加の方法があることがわかります。 これは、GPS位置の更新をキャンセルする方法が必要なためです。 モデルレイヤーで定義されたProvider
の実装は、次のとおりです。
class GPSLocationProvider constructor(var activity: Activity) : ILocationProvider { private var locationManager: LocationManager? = null private var locationListener: GPSLocationListener? = null override fun getLocation(): OneOf<Failure, SimpleLocation> = runBlocking { val grantedResult = getLocationPermissions() if (grantedResult.isError) { val error = (grantedResult as OneOf.Error<Failure>).error OneOf.Error(error) } else { getLocationFromGPS() } } private suspend fun getLocationPermissions(): OneOf<Failure, Boolean> = suspendCoroutine { Dexter.withActivity(activity) .withPermission(Manifest.permission.ACCESS_FINE_LOCATION) .withListener(PermissionsCallback(it)) .check() } private suspend fun getLocationFromGPS(): OneOf<Failure, SimpleLocation> = suspendCoroutine { locationListener?.unsubscribe() locationManager = activity.getSystemService(Context.LOCATION_SERVICE) as LocationManager locationManager?.let { manager -> locationListener = GPSLocationListener(manager, it) launch(UI) { manager.requestLocationUpdates(LocationManager.NETWORK_PROVIDER, 0L, 0.0f, locationListener) } } } override fun cancel() { locationListener?.unsubscribe() locationListener = null locationManager = null } }
このプロバイダーは、Kotlinコルーチンと連携する準備ができています。 覚えているかと思いますが、 UseCase
のrunメソッドはバックグラウンドスレッドで呼び出されるため、スレッドを確認して適切にマークする必要があります。 ご覧のとおり、ここでアクティビティを渡す必要があります。メモリリークを回避するために、更新をキャンセルし、リスナーが不要になったときにリスナーの登録を解除することが非常に重要です。 ILocationProvider
を実装しているため、将来、他のプロバイダーに簡単に変更できます。 また、携帯電話でGPSを有効にしなくても、現在の場所の処理を(自動または手動で)簡単にテストできます。実装を置き換えて、ランダムに構築された場所を返すだけです。 これを機能させるには、新しく作成したUseCase
をLocationsViewModel
に追加する必要があります。 次に、ViewModelには、実際にユースケースを呼び出す新しいメソッドgetCurrentLocation
が必要です。 UIを少し変更するだけで、それを呼び出してGPSProviderをDaggerに登録できます。これで、アプリは完成です。
概要
メンテナンス、テスト、変更が簡単なAndroidアプリを開発する方法を紹介しようとしていました。 また、理解しやすいものにする必要があります。新しい人があなたの仕事に来たとしても、データフローや構造を理解するのに問題はないはずです。 アーキテクチャがクリーンであることを認識している場合は、UIの変更がモデル内の何にも影響を与えず、新しい機能の追加に予想以上の時間がかからないことを確認できます。 しかし、これで旅は終わりではありません。 うまく構造化されたアプリを持っていても、「ほんの一瞬、ただ動作する」という厄介なコード変更によって、それを簡単に破ることができます。 覚えておいてください。「今のところ」コードはありません。 ルールに違反する各コードはコードベースに存続する可能性があり、将来のより大きな違反の原因となる可能性があります。 ちょうど1週間後にそのコードにたどり着くと、誰かがそのコードにいくつかの強力な依存関係を実装しているように見えます。それを解決するには、アプリの他の多くの部分を掘り下げる必要があります。 優れたコードアーキテクチャは、プロジェクトの開始時だけでなく、Androidアプリの存続期間のどの部分にとっても課題です。 何かが変わるたびに、コードを考えてチェックする必要があります。 これを覚えておくために、たとえば、Androidアーキテクチャ図を印刷してハングアップすることができます。 また、レイヤーを3つのGradleモジュールに分離することで、レイヤーの独立性を少し強制することもできます。ドメインモジュールは他のモジュールを認識せず、プレゼンテーションモジュールとモデルモジュールは相互に使用しません。 しかし、これでさえ、アプリコードの混乱が私たちが最も期待していないときに私たちに復讐するという認識に取って代わることはできません。