使用具有簡潔架構的 MVVM 更好的 Android 應用程序

已發表: 2022-03-11

如果您沒有為您的 Android 項目選擇正確的架構,那麼隨著代碼庫的增長和團隊的擴展,您將很難維護它。

這不僅僅是一個 Android MVVM 教程。 在本文中,我們將結合 MVVM(Model-View-ViewModel 或有時風格化的“ViewModel 模式”)與 Clean Architecture。 我們將看到如何使用這種架構來編寫解耦、可測試和可維護的代碼。

為什麼選擇具有乾淨架構的 MVVM?

MVVM 將您的視圖(即ActivityFragment )與您的業務邏輯分開。 MVVM 對於小型項目來說已經足夠了,但是當你的代碼庫變得龐大時,你的ViewModel就會開始膨脹。 分離職責變得困難。

在這種情況下,具有 Clean Architecture 的 MVVM 非常好。 它在分離代碼庫的職責方面更進了一步。 它清楚地抽象了可以在您的應用程序中執行的操作的邏輯。

注意:您也可以將 Clean Architecture 與 model-view-presenter (MVP) 架構結合起來。 但是由於 Android 架構組件已經提供了一個內置的ViewModel類,我們將使用 MVVM 而不是 MVP——不需要 MVVM 框架!

使用乾淨架構的優勢

  • 你的代碼比普通的 MVVM 更容易測試。
  • 您的代碼進一步解耦(最大的優勢。)
  • 包結構更易於瀏覽。
  • 該項目更易於維護。
  • 您的團隊可以更快地添加新功能。

清潔架構的缺點

  • 它有一個稍微陡峭的學習曲線。 所有層如何協同工作可能需要一些時間才能理解,特別是如果您來自簡單的 MVVM 或 MVP 等模式。
  • 它增加了很多額外的類,所以它不適合低複雜度的項目。

我們的數據流將如下所示:

具有 Clean Architecture 的 MVVM 的數據流。數據從 View 流向 ViewModel,再到 Domain,再到 Data Repository,然後流向數據源(本地或遠程)。

我們的業務邏輯與我們的 UI 完全解耦。 它使我們的代碼非常易於維護和測試。

我們將要看到的例子非常簡單。 它允許用戶創建新帖子並查看他們創建的帖子列表。 為了簡單起見,在這個示例中我沒有使用任何第三方庫(如 Dagger、RxJava 等)。

具有乾淨架構的 MVVM 層

代碼分為三個獨立的層:

  1. 表示層
  2. 領域層
  3. 數據層

我們將在下面詳細介紹每一層。 現在,我們生成的包結構如下所示:

具有 Clean Architecture 包結構的 MVVM。

即使在我們使用的 Android 應用架構中,也有很多方法可以構建文件/文件夾層次結構。 我喜歡根據功能對項目文件進行分組。 我覺得它簡潔明了。 您可以自由選擇適合您的任何項目結構。

表示層

這包括我們的ActivityFragmentViewModelActivity應該盡可能的愚蠢。 永遠不要將您的業務邏輯放在Activity中。

Activity將與ViewModel對話, ViewModel將與域層對話以執行操作。 ViewModel從不直接與數據層對話。

在這裡,我們將一個UseCaseHandler和兩個UseCase傳遞給我們的ViewModel 。 我們很快就會更詳細地討論這個問題,但在這個架構中, UseCase是一個定義ViewModel如何與數據層交互的操作。

下面是我們的 Kotlin 代碼的樣子:

 class PostListViewModel( val useCaseHandler: UseCaseHandler, val getPosts: GetPosts, val savePost: SavePost): ViewModel() { fun getAllPosts(userId: Int, callback: PostDataSource.LoadPostsCallback) { val requestValue = GetPosts.RequestValues(userId) useCaseHandler.execute(getPosts, requestValue, object : UseCase.UseCaseCallback<GetPosts.ResponseValue> { override fun onSuccess(response: GetPosts.ResponseValue) { callback.onPostsLoaded(response.posts) } override fun onError(t: Throwable) { callback.onError(t) } }) } fun savePost(post: Post, callback: PostDataSource.SaveTaskCallback) { val requestValues = SavePost.RequestValues(post) useCaseHandler.execute(savePost, requestValues, object : UseCase.UseCaseCallback<SavePost.ResponseValue> { override fun onSuccess(response: SavePost.ResponseValue) { callback.onSaveSuccess() } override fun onError(t: Throwable) { callback.onError(t) } }) } }

領域層

領域層包含應用程序的所有用例。 在這個例子中,我們有一個抽像類UseCase 。 我們所有的UseCase都將擴展這個類。

 abstract class UseCase<Q : UseCase.RequestValues, P : UseCase.ResponseValue> { var requestValues: Q? = null var useCaseCallback: UseCaseCallback<P>? = null internal fun run() { executeUseCase(requestValues) } protected abstract fun executeUseCase(requestValues: Q?) /** * Data passed to a request. */ interface RequestValues /** * Data received from a request. */ interface ResponseValue interface UseCaseCallback<R> { fun onSuccess(response: R) fun onError(t: Throwable) } }

並且UseCaseHandler處理UseCase的執行。 當我們從數據庫或遠程服務器獲取數據時,我們不應該阻塞 UI。 這是我們決定在後台線程上執行UseCase並在主線程上接收響應的地方。

 class UseCaseHandler(private val mUseCaseScheduler: UseCaseScheduler) { fun <T : UseCase.RequestValues, R : UseCase.ResponseValue> execute( useCase: UseCase<T, R>, values: T, callback: UseCase.UseCaseCallback<R>) { useCase.requestValues = values useCase.useCaseCallback = UiCallbackWrapper(callback, this) mUseCaseScheduler.execute(Runnable { useCase.run() }) } private fun <V : UseCase.ResponseValue> notifyResponse(response: V, useCaseCallback: UseCase.UseCaseCallback<V>) { mUseCaseScheduler.notifyResponse(response, useCaseCallback) } private fun <V : UseCase.ResponseValue> notifyError( useCaseCallback: UseCase.UseCaseCallback<V>, t: Throwable) { mUseCaseScheduler.onError(useCaseCallback, t) } private class UiCallbackWrapper<V : UseCase.ResponseValue>( private val mCallback: UseCase.UseCaseCallback<V>, private val mUseCaseHandler: UseCaseHandler) : UseCase.UseCaseCallback<V> { override fun onSuccess(response: V) { mUseCaseHandler.notifyResponse(response, mCallback) } override fun onError(t: Throwable) { mUseCaseHandler.notifyError(mCallback, t) } } companion object { private var INSTANCE: UseCaseHandler? = null fun getInstance(): UseCaseHandler { if (INSTANCE == null) { INSTANCE = UseCaseHandler(UseCaseThreadPoolScheduler()) } return INSTANCE!! } } }

顧名思義, GetPosts UseCase負責獲取用戶的所有帖子。

 class GetPosts(private val mDataSource: PostDataSource) : UseCase<GetPosts.RequestValues, GetPosts.ResponseValue>() { protected override fun executeUseCase(requestValues: GetPosts.RequestValues?) { mDataSource.getPosts(requestValues?.userId ?: -1, object : PostDataSource.LoadPostsCallback { override fun onPostsLoaded(posts: List<Post>) { val responseValue = ResponseValue(posts) useCaseCallback?.onSuccess(responseValue) } override fun onError(t: Throwable) { // Never use generic exceptions. Create proper exceptions. Since // our use case is different we will go with generic throwable useCaseCallback?.onError(Throwable("Data not found")) } }) } class RequestValues(val userId: Int) : UseCase.RequestValues class ResponseValue(val posts: List<Post>) : UseCase.ResponseValue }

UseCase的目的是成為您的ViewModelRepository之間的中介。

假設將來您決定添加“編輯帖子”功能。 您所要做的就是添加一個新的EditPost UseCase ,它的所有代碼都將與其他UseCase完全分離並解耦。 我們都見過很多次:引入了新功能,它們無意中破壞了現有代碼中的某些內容。 創建一個單獨的UseCase有助於極大地避免這種情況。

當然,您不能 100% 消除這種可能性,但您肯定可以將其最小化。 這就是 Clean Architecture 與其他模式的區別:代碼是如此解耦,以至於您可以將每一層都視為一個黑盒子。

數據層

這具有域層可以使用的所有存儲庫。 該層向外部類公開數據源 API:

 interface PostDataSource { interface LoadPostsCallback { fun onPostsLoaded(posts: List<Post>) fun onError(t: Throwable) } interface SaveTaskCallback { fun onSaveSuccess() fun onError(t: Throwable) } fun getPosts(userId: Int, callback: LoadPostsCallback) fun savePost(post: Post) }

PostDataRepository實現PostDataSource 。 它決定我們是從本地數據庫還是從遠程服務器獲取數據。

 class PostDataRepository private constructor( private val localDataSource: PostDataSource, private val remoteDataSource: PostDataSource): PostDataSource { companion object { private var INSTANCE: PostDataRepository? = null fun getInstance(localDataSource: PostDataSource, remoteDataSource: PostDataSource): PostDataRepository { if (INSTANCE == null) { INSTANCE = PostDataRepository(localDataSource, remoteDataSource) } return INSTANCE!! } } var isCacheDirty = false override fun getPosts(userId: Int, callback: PostDataSource.LoadPostsCallback) { if (isCacheDirty) { getPostsFromServer(userId, callback) } else { localDataSource.getPosts(userId, object : PostDataSource.LoadPostsCallback { override fun onPostsLoaded(posts: List<Post>) { refreshCache() callback.onPostsLoaded(posts) } override fun onError(t: Throwable) { getPostsFromServer(userId, callback) } }) } } override fun savePost(post: Post) { localDataSource.savePost(post) remoteDataSource.savePost(post) } private fun getPostsFromServer(userId: Int, callback: PostDataSource.LoadPostsCallback) { remoteDataSource.getPosts(userId, object : PostDataSource.LoadPostsCallback { override fun onPostsLoaded(posts: List<Post>) { refreshCache() refreshLocalDataSource(posts) callback.onPostsLoaded(posts) } override fun onError(t: Throwable) { callback.onError(t) } }) } private fun refreshLocalDataSource(posts: List<Post>) { posts.forEach { localDataSource.savePost(it) } } private fun refreshCache() { isCacheDirty = false } }

代碼大部分是不言自明的。 這個類有兩個變量, localDataSourceremoteDataSource 。 它們的類型是PostDataSource ,所以我們不關心它們實際上是如何實現的。

以我個人的經驗,這種架構已被證明是無價的。 在我的一個應用程序中,我從後端的 Firebase 開始,它非常適合快速構建​​您的應用程序。 我知道最終我必須轉移到我自己的服務器上。

當我這樣做時,我所要做的就是更改RemoteDataSource中的實現。 即使發生瞭如此巨大的變化,我也不必接觸任何其他課程。 這就是解耦代碼的優勢。 更改任何給定的類不應影響代碼的其他部分。

我們有一些額外的課程:

 interface UseCaseScheduler { fun execute(runnable: Runnable) fun <V : UseCase.ResponseValue> notifyResponse(response: V, useCaseCallback: UseCase.UseCaseCallback<V>) fun <V : UseCase.ResponseValue> onError( useCaseCallback: UseCase.UseCaseCallback<V>, t: Throwable) } class UseCaseThreadPoolScheduler : UseCaseScheduler { val POOL_SIZE = 2 val MAX_POOL_SIZE = 4 val TIMEOUT = 30 private val mHandler = Handler() internal var mThreadPoolExecutor: ThreadPoolExecutor init { mThreadPoolExecutor = ThreadPoolExecutor(POOL_SIZE, MAX_POOL_SIZE, TIMEOUT.toLong(), TimeUnit.SECONDS, ArrayBlockingQueue(POOL_SIZE)) } override fun execute(runnable: Runnable) { mThreadPoolExecutor.execute(runnable) } override fun <V : UseCase.ResponseValue> notifyResponse(response: V, useCaseCallback: UseCase.UseCaseCallback<V>) { mHandler.post { useCaseCallback.onSuccess(response) } } override fun <V : UseCase.ResponseValue> onError( useCaseCallback: UseCase.UseCaseCallback<V>, t: Throwable) { mHandler.post { useCaseCallback.onError(t) } } }

UseCaseThreadPoolScheduler負責使用ThreadPoolExecuter異步執行任務。

 class ViewModelFactory : ViewModelProvider.Factory { override fun <T : ViewModel> create(modelClass: Class<T>): T { if (modelClass == PostListViewModel::class.java) { return PostListViewModel( Injection.provideUseCaseHandler() , Injection.provideGetPosts(), Injection.provideSavePost()) as T } throw IllegalArgumentException("unknown model class $modelClass") } companion object { private var INSTANCE: ViewModelFactory? = null fun getInstance(): ViewModelFactory { if (INSTANCE == null) { INSTANCE = ViewModelFactory() } return INSTANCE!! } } }

這是我們的ViewModelFactory 。 您必須創建它才能在ViewModel構造函數中傳遞參數。

依賴注入

我將用一個例子來解釋依賴注入。 如果您查看我們的PostDataRepository類,它有兩個依賴項, LocalDataSourceRemoteDataSource 。 我們使用Injection類將這些依賴項提供給PostDataRepository類。

注入依賴有兩個主要優點。 一種是您可以從一個中心位置控制對象的實例化,而不是將其分散到整個代碼庫中。 另一個是這將幫助我們為PostDataRepository編寫單元測試,因為現在我們可以只將LocalDataSourceRemoteDataSource的模擬版本傳遞給PostDataRepository構造函數,而不是實際值。

 object Injection { fun providePostDataRepository(): PostDataRepository { return PostDataRepository.getInstance(provideLocalDataSource(), provideRemoteDataSource()) } fun provideViewModelFactory() = ViewModelFactory.getInstance() fun provideLocalDataSource(): PostDataSource = LocalDataSource.getInstance() fun provideRemoteDataSource(): PostDataSource = RemoteDataSource.getInstance() fun provideGetPosts() = GetPosts(providePostDataRepository()) fun provideSavePost() = SavePost(providePostDataRepository()) fun provideUseCaseHandler() = UseCaseHandler.getInstance() }

注意:我更喜歡在復雜項目中使用 Dagger 2 進行依賴注入。 但是由於其極其陡峭的學習曲線,它超出了本文的範圍。 因此,如果您有興趣深入了解,我強烈推薦 Hari Vignesh Jayapalan 對 Dagger 2 的介紹。

具有乾淨架構的 MVVM:一個可靠的組合

我們這個項目的目的是了解具有 Clean Architecture 的 MVVM,因此我們跳過了一些您可以嘗試進一步改進它的事情:

  1. 使用 LiveData 或 RxJava 刪除回調並使其更整潔。
  2. 使用狀態來表示您的 UI。 (為此,請查看 Jake Wharton 的精彩演講。)
  3. 使用 Dagger 2 注入依賴項。

這是適用於 Android 應用程序的最佳和最具可擴展性的架構之一。 我希望你喜歡這篇文章,我期待聽到你如何在自己的應用程序中使用這種方法!

相關: Xamarin Forms、MVVMCross 和 SkiaSharp:跨平台應用程序開發的三位一體