あなたは何を好みますか:ひどいアーキテクチャで非常にうまく機能しているアプリに新しい機能を追加するか、うまく設計されているがバグのあるAndroidアプリケーションのバグを修正しますか?個人的には、間違いなく2番目のオプションを選択します。すべてのクラスのすべてからのすべての依存関係を考慮すると、新しい機能を追加することは、単純なものであっても、アプリで非常に面倒になる可能性があります。私のAndroidプロジェクトの1つを覚えています。そこでは、プロジェクトマネージャーから、データをダウンロードして新しい画面に表示するなどの小さな機能を追加するように求められました。それは、新しい仕事を見つけた私の同僚の一人によって書かれたアプリでした。この機能は、半営業日以上かかることはありません。私はとても楽観的でした…
お金でクレジットカード番号を漏らした
アプリがどのように機能するか、どのモジュールがあり、それらがどのように相互に通信するかを7時間調査した後、この機能のいくつかの試行的な実装を行いました。地獄だった。データモデルの小さな変更により、ログイン画面が大幅に変更されました。ネットワークリクエストを追加するには、ほぼすべての画面とGodOnlyKnowsWhatThisClassDoes
の実装を変更する必要がありましたクラス。ボタンの色の変更により、データをデータベースに保存するときに奇妙な動作が発生したり、アプリ全体がクラッシュしたりしました。翌日の半ば、私はプロジェクトマネージャーに次のように話しました。「この機能を実装するには2つの方法があります。最初に、私はそれにさらに3日を費やすことができ、最終的には非常に汚い方法でそれを実装し、次のすべての機能またはバグ修正の実装時間は指数関数的に増加します。または、アプリを書き直すこともできます。これには2、3週間かかりますが、将来のアプリの変更に備えて時間を節約できます。」幸い、彼は2番目のオプションに同意しました。アプリの優れたソフトウェアアーキテクチャ(非常に小さなものでも)がなぜ重要なのか疑問に思ったことがある場合、このアプリはそれらを完全に排除しました。しかし、このような問題を回避するには、どのAndroidアーキテクチャパターンを使用する必要がありますか?
この記事では、Androidアプリのクリーンなアーキテクチャの例を紹介します。ただし、このパターンの主なアイデアは、すべてのプラットフォームと言語に適合させることができます。優れたアーキテクチャは、プラットフォーム、言語、データベースシステム、入力、出力などの詳細から独立している必要があります。
次の機能を使用して現在地を登録するための簡単なAndroidアプリを作成します。
レイヤーは、クリーンなアーキテクチャの主要なコアです。このアプリでは、プレゼンテーション、ドメイン、モデルの3つのレイヤーを使用します。各レイヤーは分離する必要があり、他のレイヤーについて知る必要はありません。それはそれ自身の世界に存在し、せいぜい、通信するための小さなインターフェースを共有するべきです。
レイヤーの責任:
どのレイヤーが他のレイヤーについて知っておくべきですか?答えを得る最も簡単な方法は、変更について考えることです。プレゼンテーション層を見てみましょう。ユーザーに何かを提示します。プレゼンテーションで何かを変更した場合、モデルレイヤーも変更する必要がありますか?ユーザーの名前と最後の場所が表示された「ユーザー」画面があるとします。 1つだけではなく、ユーザーの最後の2つの場所を表示する場合は、モデルに影響を与えないでください。したがって、最初の原則があります。 プレゼンテーション層はモデル層を認識していません。
そして、その逆です。モデル層はプレゼンテーション層について知っている必要がありますか?繰り返しになりますが、たとえば、データベースからネットワークにデータのソースを変更した場合、UIの内容は変更されないはずです(ここにローダーを追加することを検討した場合)はい。ただし、UIローダーを使用することもできます。データベースを使用する場合)。したがって、2つの層は完全に分離されています。すごい!
ドメインレイヤーはどうですか?すべての主要なビジネスロジックが含まれているため、これは最も重要なものです。これは、データをモデルレイヤーに渡す前、またはユーザーに提示する前にデータを処理する場所です。他のレイヤーから独立している必要があります。データベース、ネットワーク、またはユーザーインターフェイスについては何も知りません。これがコアであるため、他のレイヤーはこのレイヤーとのみ通信します。なぜこれを完全に独立させたいのですか?ビジネスルールは、UIデザインや、データベースやネットワークストレージ内の何かよりも頻繁に変更されることはないでしょう。提供されているいくつかのインターフェースを介してこのレイヤーと通信します。具体的なモデルやUIの実装は使用しません。これらは詳細であり、覚えておいてください。詳細は変更されます。優れたアーキテクチャは細部にとらわれません。
今のところ十分な理論。コーディングを始めましょう!この記事はコードを中心に展開しているため、理解を深めるために、からコードをダウンロードする必要があります。 GitHub 中身を確認してください。作成されるGitタグは、architecture_v1、architecture_v2、architecture_v3の3つで、記事の各部分に対応しています。
アプリでは、依存性注入にKotlinとDagger2を使用しています。ここではKotlinもDagger2も必要ありませんが、作業がはるかに簡単になります。私がRxJava(またはRxKotlin)を使用していないことに驚かれるかもしれませんが、ここでは使用できませんでした。ライブラリが一番上にあり、誰かが必須だと言っているという理由だけで、ライブラリを使用するのは好きではありません。私が言ったように、言語とライブラリは詳細なので、好きなものを使用できます。 JUnit、Robolectric、MockitoなどのAndroidユニットテストライブラリも使用されます。
Androidアプリケーションアーキテクチャの設計で最も重要なレイヤーはドメインレイヤーです。それから始めましょう。これは、他のレイヤーと通信するためのビジネスロジックとインターフェイスが配置される場所です。主なコアはUseCase
sで、これはユーザーがアプリでできることを反映しています。それらの抽象化を準備しましょう:
abstract class UseCase { private var job: Deferred? = null abstract suspend fun run(params: Params): OneOf fun execute(params: Params, onResult: (OneOf) -> 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
—データでエラーまたは成功を返すことができます。
sealed class OneOf { data class Error(val error: E) : OneOf() data class Success(val data: S) : OneOf() val isSuccess get() = this is Success val isError get() = this is Error fun error(error: E) = Error(error) fun success(data: S) = Success(data) fun oneOf(onError: (E) -> Any, onSuccess: (S) -> Any): Any = when (this) { is Error -> onError(error) is Success -> onSuccess(data) } }
ドメインレイヤーには独自のエンティティが必要なので、次のステップはそれらを定義することです。今のところ2つのエンティティがあります:User
およびUserLocation
:
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 fun getActiveUser(): OneOf fun createUser(user: User): OneOf fun removeUser(userId: Int): OneOf fun editUser(user: User): OneOf fun users(): OneOf } interface ILocationsRepository { fun locations(userId: Int): OneOf fun addLocation(location: UserLocation): OneOf }
この一連のアクションは、アプリに必要なデータを提供するのに十分なはずです。この段階では、データの保存方法は決定していません。これは、独立させたい詳細です。今のところ、ドメインレイヤーはAndroid上にあることすら知りません。この状態を維持しようとします(並べ替え。後で説明します)。
最後の(またはほぼ最後の)ステップは、プレゼンテーションデータで使用されるUseCase
の実装を定義することです。それらはすべて非常に単純です(アプリとデータが単純であるように)。これらの操作は、リポジトリから適切なメソッドを呼び出すように制限されています。例:
class GetLocations @Inject constructor(private val repository: ILocationsRepository) : UseCase() { override suspend fun run(params: UserIdParams): OneOf = 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() override fun locations(userId: Int): OneOf = OneOf.Success(locations.filter { it.userId == userId }) override fun addLocation(location: UserLocation): OneOf { val addedLocation = location.copy(id = locations.size + 1) locations.add(addedLocation) return OneOf.Success(addedLocation) } }
2年前、私は 論文 Android向けの非常に優れたアプリ構造としてのMVPについて。 GoogleがAndroidアプリケーションの開発をはるかに容易にする優れたアーキテクチャコンポーネントを発表したとき、MVPは不要になり、MVVMに置き換えることができます。ただし、このパターンからのいくつかのアイデアは、ダムビューに関するもののように、依然として非常に役立ちます。彼らはデータの表示だけを気にする必要があります。これを実現するために、ViewModelとLiveDataを利用します。
書体の2つのカテゴリは、サンセリフと
アプリのデザインは非常にシンプルです。1つのアクティビティには下部ナビゲーションがあり、2つのメニューエントリにlocations
が表示されます。フラグメントまたはusers
断片。これらのビューでは、ViewModelsを使用します。ViewModelsはドメインレイヤーのUseCase
sを使用して、通信をきちんとシンプルに保ちます。たとえば、ここにLocationsViewModel
があります。
class LocationsViewModel @Inject constructor(private val getLocations: GetLocations, private val saveLocation: SaveLocation) : BaseViewModel() { var locations = MutableLiveData() 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) { 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 { showLocations(it ?: emptyList()) }) locationsViewModel.error.observe(this, Observer { handleError(it) }) } private fun showLocations(locations: List) { 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(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です。 ViewModelの実装は、ユースケースがドメイン用であるのと同じです。言い換えると、クリーンなアーキテクチャはタマネギのようなものです。レイヤーがあり、レイヤーにもレイヤーを含めることができます。
アーキテクチャのすべてのクラスを作成しましたが、もう1つやるべきことがあります。それは、すべてを接続する何かが必要なことです。プレゼンテーション、ドメイン、およびモデルレイヤーはクリーンに保たれますが、ダーティモジュールであり、すべてについてすべてを知っている1つのモジュールが必要です。この知識によって、レイヤーを接続できるようになります。それを作成する最良の方法は、一般的なデザインパターンの1つ(SOLIDで定義されたクリーンなコード原則の1つ)を使用することです。依存性注入は、適切なオブジェクトを作成し、それらを目的の依存性に注入します。ここではDagger2を使用しました(プロジェクトの途中で、ボイラープレートの少ない2.16にバージョンを変更しました)が、任意のメカニズムを使用できます。最近、コインライブラリーで少し遊んだので、試してみる価値もあると思います。ここで使用したかったのですが、テスト時にViewModelをモックするのに多くの問題がありました。それらをすばやく解決する方法を見つけたいと思います。そうであれば、KoinとDagger2を使用するときにこのアプリの違いを提示できます。
タグarchitecture_v1を使用して、GitHubでこの段階のアプリを確認できます。
レイヤーを完成させ、アプリをテストしました。すべてが機能しています。 1つを除いて、PMが使用するデータベースを知る必要があります。彼らがあなたのところに来て、経営陣がRoomの使用に同意したと言ったと仮定しますが、将来的には最新の超高速ライブラリを使用する可能性があるため、潜在的な変更を念頭に置く必要があります。また、利害関係者の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
。ここに小さな問題があります— ILocationsRepository
UserLocation
を返す必要がありますが、UserLocationEntity
を受け取りますデータベースから。 User
関連のクラスについても同じです。反対方向に、UserLocation
を渡します。データベースにUserLocationEntity
が必要な場合。これを解決するには、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
を削除しますドメイン層から。
最後に行うことは、依存性注入モジュールを変更して、新しく作成されたRepository
を提供することです。以前のMemoryRepository
の代わりの実装。アプリをビルドして実行した後、PMに移動して、Roomデータベースで動作中のアプリを表示できます。また、ネットワークの追加にそれほど時間はかからないこと、および管理者が必要とする任意のストレージライブラリを利用できることをPMに通知することもできます。変更されたファイルを確認できます。モデルレイヤー内のファイルのみです。私たちのアーキテクチャは本当にきれいです!次のすべてのストレージタイプは、リポジトリを拡張して適切な実装を提供するだけで、同じ方法で構築できます。もちろん、データベースやネットワークなど、複数のデータソースが必要になる可能性があります。では、どうしますか?何もしません。3つのリポジトリ実装を作成する必要があります。1つはネットワーク用、もう1つはデータベース用、そしてメインの実装で、正しいデータソースが選択されます(たとえば、ネットワークがある場合は、ネットワーク、そうでない場合はデータベースからロード)。
タグarchitecture_v2を使用して、GitHubでこのステージのアプリをチェックアウトできます。
これで、1日がほぼ終わりました。コーヒーを飲みながらパソコンの前に座っていると、アプリをGoogle Playに送信する準備が整いました。突然、プロジェクトマネージャーがあなたのところに来て、「機能を追加してくれませんか? GPSからユーザーの現在地を保存できますか?」
すべてが変わります…特にソフトウェア。これが、クリーンなコードとクリーンなアーキテクチャが必要な理由です。ただし、考えずにコーディングすると、最もクリーンなものでも汚れることがあります。 GPSから位置を取得することを実装するときの最初の考えは、アクティビティにすべての位置認識コードを追加し、それをSaveLocationDialogFragment
で実行することです。新しいUserLocation
を作成します対応するデータで。これが最速の方法かもしれません。しかし、私たちのクレイジーなPMが私たちのところに来て、位置情報の取得をGPSから他のプロバイダー(Bluetoothやネットワークなど)に変更するように要求した場合はどうなりますか?変更はすぐに手に負えなくなるでしょう。どうすればきれいな方法でそれを行うことができますか?
ユーザーの場所はデータです。また、場所の取得はUseCase
であるため、ドメインレイヤーとモデルレイヤーもここに含める必要があると思います。したがって、もう1つUseCase
があります。実装する— GetCurrentLocation
。また、場所を提供するもの、つまりILocationProvider
も必要です。インターフェース、UseCase
を作成するGPSセンサーのような詳細に依存しない:
interface ILocationProvider { fun getLocation(): OneOf fun cancel() } class GetCurrentLocation @Inject constructor(private val locationProvider: ILocationProvider) : UseCase() { override suspend fun run(params: NoParams): OneOf = 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 = runBlocking { val grantedResult = getLocationPermissions() if (grantedResult.isError) { val error = (grantedResult as OneOf.Error).error OneOf.Error(error) } else { getLocationFromGPS() } } private suspend fun getLocationPermissions(): OneOf = suspendCoroutine { Dexter.withActivity(activity) .withPermission(Manifest.permission.ACCESS_FINE_LOCATION) .withListener(PermissionsCallback(it)) .check() } private suspend fun getLocationFromGPS(): OneOf = 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
sのrunメソッドはバックグラウンドスレッドで呼び出されるため、スレッドを確実に適切にマークする必要があります。ご覧のとおり、ここでアクティビティを渡す必要があります。メモリリークを回避するために、更新をキャンセルし、リスナーが不要になったときにリスナーから登録を解除することが非常に重要です。 ILocationProvider
を実装しているため、将来、他のプロバイダーに簡単に変更できます。また、携帯電話でGPSを有効にしなくても、現在地の処理を(自動または手動で)簡単にテストできます。実装を置き換えて、ランダムに構築された場所を返すだけです。それを機能させるには、新しく作成したUseCase
を追加する必要がありますLocationsViewModel
に。次に、ViewModelには、実際にユースケースを呼び出す新しいメソッドgetCurrentLocation
が必要です。それを呼び出してGPSProviderをDaggerに登録するための、わずかなUIの変更だけで、アプリは完成です。
メンテナンス、テスト、変更が簡単なAndroidアプリを開発する方法を紹介しようとしていました。また、理解しやすいものにする必要があります。新しい人があなたの仕事に来たとしても、データフローや構造を理解するのに問題はないはずです。アーキテクチャがクリーンであることに気付いていれば、UIの変更がモデル内の何にも影響を与えず、新しい機能の追加に予想以上の時間がかからないことを確信できます。しかし、これで旅は終わりではありません。うまく構造化されたアプリを持っていたとしても、「ほんの少しの間、ただ機能するために」厄介なコード変更によってそれを壊すのは非常に簡単です。覚えておいてください。「今のところ」コードはありません。ルールに違反する各コードはコードベースに存続する可能性があり、将来のより大きな違反の原因となる可能性があります。わずか1週間後にそのコードにたどり着くと、誰かがそのコードに強い依存関係を実装しているように見えます。それを解決するには、アプリの他の多くの部分を掘り下げる必要があります。優れたコードアーキテクチャは、プロジェクトの開始時だけでなく、Androidアプリの存続期間のどの部分にとっても課題です。コードを考えてチェックすることは、何かが変わるたびに考慮されるべきです。これを覚えておくために、たとえば、Androidアーキテクチャ図を印刷してハングアップすることができます。ドメインモジュールが他のモジュールを認識せず、プレゼンテーションモジュールとモデルモジュールが相互に使用しない3つのGradleモジュールにレイヤーを分離することで、レイヤーの独立性を少し強制することもできます。しかし、これでさえ、アプリコードの混乱が私たちが最も期待していないときに私たちに復讐するという認識に取って代わることはできません。
Daggerは、コード生成とアノテーションを使用する依存性注入ライブラリです。依存関係を管理するために使用され、コードのテストと保守が容易になります。
Googleは、I / O 2017でAndroid上のKotlinの公式サポートを発表しました。そのプラットフォームでは最新のJava機能を利用できないため、最新のKotlin言語はAndroidに非常に適しています。
今年のI / O 2018で、GoogleはLiveData、ViewModel、Roomなどの新しいAndroidアーキテクチャコンポーネントを発表しました。これらは、SQLiteの操作を簡素化し、データ変更の処理を容易にし、開発者がライフサイクルを処理するのを支援するために作成されています。