Temiz Mimari ile MVVM Kullanan Daha İyi Android Uygulamaları
Yayınlanan: 2022-03-11Android projeniz için doğru mimariyi seçmezseniz, kod tabanınız büyüdükçe ve ekibiniz genişledikçe onu sürdürmekte zorlanacaksınız.
Bu sadece bir Android MVVM öğreticisi değil. Bu yazıda, MVVM'yi (Model-View-ViewModel veya bazen stilize edilmiş “ViewModel deseni”) Temiz Mimari ile birleştireceğiz. Bu mimarinin ayrıştırılmış, test edilebilir ve bakımı yapılabilir kod yazmak için nasıl kullanılabileceğini göreceğiz.
Neden Temiz Mimari ile MVVM?
MVVM, görünümünüzü (yani Activity
ve Fragment
) iş mantığınızdan ayırır. MVVM, küçük projeler için yeterlidir, ancak kod tabanınız büyüdüğünde, ViewModel
başlar. Sorumlulukları ayırmak zorlaşıyor.
Temiz Mimari ile MVVM bu gibi durumlarda oldukça iyidir. Kod tabanınızın sorumluluklarını ayırmada bir adım daha ileri gider. Uygulamanızda gerçekleştirilebilecek eylemlerin mantığını açıkça özetler.
Not: Temiz Mimariyi model-görünüm-sunucu (MVP) mimarisiyle de birleştirebilirsiniz. Ancak Android Mimari Bileşenleri zaten yerleşik bir ViewModel
sınıfı sağladığından, MVP üzerinden MVVM ile gidiyoruz - MVVM çerçevesi gerekmez!
Temiz Mimari Kullanmanın Avantajları
- Kodunuz, düz MVVM'den daha kolay test edilebilir.
- Kodunuz daha da ayrıştırılmıştır (en büyük avantaj.)
- Paket yapısında gezinmek daha da kolaydır.
- Projenin bakımı daha da kolaydır.
- Ekibiniz yeni özellikleri daha da hızlı ekleyebilir.
Temiz Mimarinin Dezavantajları
- Biraz dik bir öğrenme eğrisine sahiptir. Özellikle basit MVVM veya MVP gibi kalıplardan geliyorsanız, tüm katmanların birlikte nasıl çalıştığını anlamak biraz zaman alabilir.
- Çok fazla ekstra sınıf ekler, bu nedenle karmaşıklığı düşük projeler için ideal değildir.
Veri akışımız şöyle görünecek:
İş mantığımız, kullanıcı arayüzümüzden tamamen ayrılmıştır. Kodumuzun bakımını ve test edilmesini çok kolaylaştırır.
Göreceğimiz örnek oldukça basit. Kullanıcıların yeni gönderiler oluşturmasına ve onlar tarafından oluşturulan gönderilerin bir listesini görmesine olanak tanır. Bu örnekte basitlik adına herhangi bir üçüncü taraf kitaplığı (Dagger, RxJava, vb.) kullanmıyorum.
Temiz Mimari ile MVVM Katmanları
Kod üç ayrı katmana ayrılmıştır:
- Sunum Katmanı
- Etki Alanı Katmanı
- Veri Katmanı
Aşağıda her katman hakkında daha fazla ayrıntıya gireceğiz. Şimdilik, sonuçtaki paket yapımız şöyle görünüyor:
Kullandığımız Android uygulama mimarisinde bile dosya/klasör hiyerarşinizi yapılandırmanın birçok yolu vardır. Proje dosyalarını özelliklere göre gruplamayı seviyorum. Düzgün ve özlü buluyorum. Size uygun olan proje yapısını seçmekte özgürsünüz.
Sunum Katmanı
Buna Activity
s, Fragment
s ve ViewModel
s dahildir. Bir Activity
mümkün olduğunca aptal olmalıdır. Faaliyet mantığınızı asla Activity
s'ye koymayın.
Bir Activity
, bir ViewModel
ile konuşacak ve bir ViewModel
, eylemleri gerçekleştirmek için etki alanı katmanıyla konuşacaktır. Bir ViewModel
, hiçbir zaman doğrudan veri katmanıyla konuşmaz.
Burada ViewModel
bir UseCaseHandler
ve iki UseCase
. Bunu yakında daha ayrıntılı olarak ele alacağız, ancak bu mimaride UseCase
, ViewModel
veri katmanıyla nasıl etkileşime girdiğini tanımlayan bir eylemdir.
Kotlin kodumuz şöyle görünür:
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) } }) } }
Etki Alanı Katmanı
Etki alanı katmanı, uygulamanızın tüm kullanım durumlarını içerir. Bu örnekte, soyut bir sınıf olan UseCase
. Tüm UseCase
bu sınıfı genişletecektir.
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) } }
Ve UseCaseHandler
, bir UseCase
yürütülmesini yönetir. Veritabanından veya uzak sunucumuzdan veri getirdiğimizde UI'yi asla engellememeliyiz. Bu, UseCase
bir arka plan iş parçacığında yürütmeye ve ana iş parçacığında yanıtı almaya karar verdiğimiz yerdir.
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!! } } }
Adından da anlaşılacağı gibi, GetPosts
UseCase
, bir kullanıcının tüm gönderilerini almaktan sorumludur.
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
s'nin amacı, ViewModel
'leriniz ve Repository
'leriniz arasında arabulucu olmaktır.

Diyelim ki gelecekte bir "gönderiyi düzenle" özelliği eklemeye karar verdiniz. Tek yapmanız gereken yeni bir EditPost
UseCase
eklemek ve tüm kodu diğer UseCase
tamamen ayrı ve ayrıştırılmış olacaktır. Hepimiz bunu defalarca gördük: Yeni özellikler tanıtıldı ve yanlışlıkla önceden var olan koddaki bir şeyi bozuyorlar. Ayrı bir UseCase
oluşturmak, bundan kaçınmada son derece yardımcı olur.
Tabii ki, bu olasılığı yüzde 100 ortadan kaldıramazsınız, ancak kesinlikle en aza indirebilirsiniz. Temiz Mimariyi diğer kalıplardan ayıran şey budur: Kod o kadar ayrıştırılmıştır ki her katmanı bir kara kutu olarak değerlendirebilirsiniz.
Veri Katmanı
Bu, etki alanı katmanının kullanabileceği tüm depolara sahiptir. Bu katman, bir veri kaynağı API'sini dış sınıflara sunar:
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
öğesini uygular. Yerel bir veritabanından mı yoksa uzak bir sunucudan mı veri alacağımıza karar verir.
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 } }
Kod çoğunlukla açıklayıcıdır. Bu sınıfın localDataSource
ve remoteDataSource
olmak üzere iki değişkeni vardır. Türleri PostDataSource
, bu nedenle kaputun altında gerçekte nasıl uygulandıkları umurumuzda değil.
Kişisel deneyimime göre, bu mimarinin paha biçilmez olduğu kanıtlandı. Uygulamalarımdan birinde, uygulamanızı hızlı bir şekilde oluşturmak için harika olan arka uçta Firebase ile başladım. Sonunda kendi sunucuma geçmem gerektiğini biliyordum.
Yaptığımda, tek yapmam gereken RemoteDataSource
içindeki uygulamayı değiştirmekti. Bu kadar büyük bir değişiklikten sonra bile başka bir sınıfa dokunmam gerekmedi. Ayrılmış kodun avantajı budur. Herhangi bir sınıfı değiştirmek, kodunuzun diğer bölümlerini etkilememelidir.
Sahip olduğumuz ekstra sınıflardan bazıları:
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
kullanarak görevleri eşzamansız olarak yürütmekten sorumludur.
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!! } } }
Bu bizim ViewModelFactory
. ViewModel
argümanları iletmek için bunu oluşturmanız gerekir.
Bağımlılık Enjeksiyonu
Bağımlılık enjeksiyonunu bir örnekle açıklayacağım. PostDataRepository
sınıfımıza bakarsanız, LocalDataSource
ve RemoteDataSource
olmak üzere iki bağımlılığı vardır. Bu bağımlılıkları PostDataRepository
sınıfına sağlamak için Injection
sınıfını kullanıyoruz.
Enjeksiyon bağımlılığının iki ana avantajı vardır. Birincisi, nesnelerin örneğini tüm kod tabanına yaymak yerine merkezi bir yerden kontrol edebilmenizdir. Bir diğeri, bunun PostDataRepository
için birim testleri yazmamıza yardımcı olacağıdır, çünkü artık LocalDataSource
ve RemoteDataSource
alaylı sürümlerini gerçek değerler yerine PostDataRepository
yapıcısına iletebiliriz.
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() }
Not: Karmaşık projelerde bağımlılık enjeksiyonu için Dagger 2'yi kullanmayı tercih ederim. Ancak son derece dik öğrenme eğrisi ile bu makalenin kapsamı dışındadır. Bu yüzden daha derine inmekle ilgileniyorsanız, Hari Vignesh Jayapalan'ın Dagger 2'ye girişini şiddetle tavsiye ederim.
Temiz Mimari ile MVVM: Sağlam Bir Kombinasyon
Bu projedeki amacımız Temiz Mimari ile MVVM'yi anlamaktı, bu yüzden onu daha da geliştirmeye çalışabileceğiniz birkaç şeyi atladık:
- Geri aramaları kaldırmak ve biraz daha düzenli hale getirmek için LiveData veya RxJava kullanın.
- Kullanıcı arayüzünüzü temsil etmek için durumları kullanın. (Bunun için Jake Wharton'ın bu harika konuşmasına bakın.)
- Bağımlılıkları enjekte etmek için Dagger 2'yi kullanın.
Bu, Android uygulamaları için en iyi ve en ölçeklenebilir mimarilerden biridir. Umarım bu makaleyi beğenmişsinizdir ve bu yaklaşımı kendi uygulamalarınızda nasıl kullandığınızı öğrenmek için sabırsızlanıyorum!