Android MVVM Architecture : Antipatterns

Osman Korcan Andaç
lTunes Tribe
Published in
5 min readJul 13, 2020

Bu yazıda, Model — View — ViewModel (MVVM) mimarisi üzerinde yapılmaması gereken ancak farkında olmadan veya bilmeden uyguladığımız bazı tasarım hatalardan bahsedeceğim.

Öncelikle bilmemiz gereken şey, ViewModel sınıfılarımızın android framework’üne dahil, neredeyse hiçbir şeyi kullanmamasıdır. android.* ile ilgili hiçbir import bulunmamalıdır(istisna olarak; androidx.lifecycle.* ). Eğer android framewok’üne dahil bir componenti referans alarak kullanırsak uygulamamıza test sınıfları yazmamız çok zorlaşır, Memory Leak riski artar ve modülerliği düşer.

‼️ Viewmodel sınıfları kesinlikle Android framework’ü hakkında bilgi sahibi olmamalı!

Yukarıdaki örnekte olduğu gibi, eğer UI tarafında kullanılan bir componenti veya yukarıda olduğu gibi binding nesnesini viewmodel kullanılacak şekilde yazarsak, Viewmodel sınıfını yazmamızın hiçbir mantığı olmaz. İlk olarak VehicleViewModel sınıfımızda yer alan myCarListLiveData adında vehicle listemizi tuttuğumuz LiveData’yı MyCarFragment dışında hiçbir yerde kullanamayız. 2. olarak yazacağımız unit testing için, binding nesnesini mocklamamız gerekir ki test yazmak bir zulme dönüşecektir.

‼️ Activity ve Fragment sınıflarımızda yapılacak kontroller minimum olmalı!

View öğeleri verinin veya bileşenin nasıl görüneceği ve nasıl davranacağı ile ilgili sorumlulukları barındırmalıdır.

Örnek olarak bir proje üzerinde açıklamak gerekirse:

Günlük kullanıcıların önceden girdikleri ve takviminde yer alan günlük toplantı planlarını görüntüleyebildiği bir sayfa düşünelim. Servis kısmında ise tarih, başlık şeklinde gelen 2 alan var. Servis konusunda isteyebileceğimiz herhangi bir durumumuz yok ve bu sayfayı oluşturmamız istendi. İlk olarak yapmamız gereken tarihsel olarak (datetime) listemizi sıralamamız (I❤️kotlin), ki bunu kolayca yapabiliyoruz. Bu listeyi bir Recyclerview de göstereceğimizi düşünelim (örnek olarak bu konudan ilerledim. Daha farklı ve etkin bir biçimde düşünülürse yapılabilir tabiki. sadece konuya odaklanalım). Recyclerview de aynı gün olan aktivitelerde saat olarak ilkinin başında gün yazacak şekilde bir render mekanizması kurmamız gerekli. Yani Pazartesi 08:00–08:30 arası yer alan toplantımın başında Pazartesi yazacak fakat 11:00–11:30 arasındaki toplantımın başında bu alan görünür olmayacak. Bunu adapter içinde de kontrol ederek yapabiliriz. Fakat bu tür kontrolleri viewmodel içine taşıyıp calender isimli model sınıfımıza isFirstActivity tarzında bir alan ekleyerek adapter içinde gün alanının gösterildiği textview’in görünürlüğü ile oynayarak ilerlememiz MVVM açısından doğru bir yaklaşım olacaktır. Çünkü View içinde kontrollerimizi, ViewModel sınıfımıza taşımış oluruz.

‼️ View içinden ViewModel’e referans vermeyin.

Bu yapılabilecek ölümcül 7 günahtan biri gibi bir şey. İlk verdiğim kod örneğinde görülebileceği gibi bir view’in componentleri viewmodel’e geçiriliyor. Bir view(fragment, activity), lifecycle olarak çok farklı durumlarda olurken ViewModel çalışır durumdadır. API’a istek atılmış olabilir ve o anda viewmodel çalışır durumdayken belki de fragment ondestroy olmuş olabilir(geri butonuna tıkladık ve kapattık). Uygulamamız kaçınılmaz bir biçimde crash alır. Crash almadığı zamanlarda bile memory leak oluşması kaçınılmazdır.

MVVM mimarisi aslında bize kuvvetler ayrılığı ilkesi gibi düşündüğümüzde büyük kolaylıklar sağlar. Örneğin yukarıda verdiğim View öğelerimizin UI tarafındaki operasyonlardan sorumlu olması, veri ile alakalı durumlarda da sorumluluğun ViewModel’de bulunması gibi.

‼️ View içinden ViewModel mümkün mertebe çağırılmamalı veya minimum çağırım olmalı.

Az önce verdiğim lifecycle şemasında da görülebileceği gibi viewlarımız herhangi lifecycle statüsünde olabilir. Diyelim ki bir günah işledik ve onresume’da bir api call’umuz var. view her rotation change olduğunda veya farklı bir view çağırıp geri döndüğümüzde o call yapılacak.

Bırakalım Viewmodel oluştuğunda o servis çağrısını yapsın.

Not: Bir button’ın action’ına koyduğumuz post api call’ları tabiki bu duruma dahil değil!

Veri gereksinimlerinizi LiveData’ları observe ederek karşılayabilirsiniz. ViewModel içinden direkt olarak veri tipi erişim tanımlarımızda public kullanmamamız gerekiyor.

MVVM mimarisinde observer pattern için en sevdiğim konu, Viewmodel provider olarak fragment içinde eğer activity kullanırsanız child fragment içinde de viewmodel’imizin içinde bulunan verileri kullanabilir/observe yapabilirsiniz. Yani bazı durumlarda fragment içinden child fragment’imizi açarken bundle aracılığla veri aktarmamız gerekirken viewmodel’imizin içinde bulunan bir veri ise buna gerek kalmayacaktır.

ViewModel Büyümesi

Kuvvetler ayrılığı ile tüm veri sorumluluklarımızı Viewmodel’e yıkmamız ve view tarafının sadece UI ile ilgilenmesi beklenen ve ideal bir durum. Fakat tek bir Viewmodel için çok farklı işlemler yapmamız o Viewmodelin gereksiz karmaşıklaşmasına ve şişmesine neden olur. Mesela authentication işlemlerini yaptığım bir Viewmodel içine user profile işlemlerini de yazmam gibi. Domain bazlı bir ayrıma gitmek ve Viewmodel’in kod olarak çok şişmemesine sağlamak ana amaçlarımızdan biri olmalı. Hatta bu konu sadece bu yazı dizinin konusuyla ilgili değil, tüm kodlamalarımız için geçerli olmalı. Yoksa 129819 satırlık stored procedure kodları ile karşılaşabiliriz :(

Viewmodel’imizin şişmemesini sağlamak için başka bir yerde tutabileceğimiz bir verimizi “illaki Viewmodel’de tutmak istiyorum” mantığıyla da hareket etmemeliyiz. Mesela az önce verdiğim örnekteki userprofile verisi gibi. Uygulamaya giriş yapan kullanıcı bilgilerini tutan UserProfile nesnesi cihazın ön belleğinde tutulabilir ve ilgili ViewModel’e(aşağıdaki örnekteki ProfileViewModel sınıfı) Repository prensibine göre alınabilir.

✔️ Bir UIState yardımıyla API Call veya cache den okunan veriler için View’a statü beslemesi yapın.

Diyelim ki bir API Call yardımıyla kullanıcı bilgilerini çektik ve boş geldi. View tarafında bu boş bilgiler için hata mesajı veya yeniden dene button’unun gösterilmesi gibi aksiyonlar alınacak. Bunun için en ideal yöntem ViewModel’inizin bir UIState class’ının olması.

Yukarıdaki örnekte araç bilgilerini yüklediğim fonksiyonda loading ve error gibi aksiyonlarımı observe eden view tarafı, ona göre aksiyon alacaktır.

Activity State Kaydedilmesi

Bazı durumlarda View öğeleri yeniden oluşturulur ve bileşenlerin son durumlarını koruması gerekir. Textview’e atanmış bir isim soyisim bilgisi gibi. Rotation changed bu durumun akla gelen ilk örneğidir. Bu durum için veriler Viewmodel üzerinde tutulursa güvenli kalacağını ve verilerimizde değişim olmayacağından bahsetmiştim.

Fakat Viewmodel’in de kill edilip recreate olduğu memory’nin düşük kaldığı durumlar oluşabiliyor. View için bu durumların korunması adına onSaveInstanceState() kullanıyorduk. Aynı durum Viewmodel için de geçerli. Aşağıdaki kütüphaneyi ekleyerek Viewmodel’ler için o anki instance’ı koruyabilirsiniz.

Observe Sorunu!

Diyelim ki uygulamamızda bir profil ekranı var ve bu profil ekranında kullanıcı telefon numarasını değiştirebiliyor. Ve telefon numarasını değiştirdiği anda successlivedata ataması yaptık ve bunu dinleyen View tarafındaki kodumuz çalıştı ve bir bilgilendirme pop-up’ı çıkardı : “Bilgileriniz başarıyla kaydedildi!”

Buraya kadar her şey normal. Fakat bir anda ekranı yan döndürdü (rotation changed). Ve o da ne!! aynı pop-up tekrar çıktı : “Bilgileriniz başarıyla kaydedildi!”. Sizce burada neyi eksik yaptık?

Bu sorun View öğelerimiz yeniden oluşturulduğu zamanlar için sinir bozucu olabiliyor. Verilerimizi View tarafında dinlemeye ihtiyacımız var. Fakat view yeniden oluşturulduğunda observe fonksiyonumuz yeniden tetikleniyor ve kodumuz çalışıyor. Çözümümüz ise SingleLiveEvent. Event’lerimiz için mutlaka tetiklendiği anda observe edilmesi gerektiğini aklımızdan çıkarmamız gerekiyor.

Not: Bu konu çok önemli ve ayrıca başka bir yazıda ele alacağım. Bu not kendini imha edip yerini güzel bir medium linkine bırakacak.

ViewModel : Memory Leak!

Yukarıda verdiğim antipattern_mvvm_4 isimli kod parçacığında görebileceğimiz gibi bir api call yapıyorum. API call yaparken de RPP tarafını da dinliyorum. Fakat bu Viewmodeli kullanan View ile işim bittiğini ve ondestoyed safhasından geçtiğini düşünün. ViewModel hala daha RPP’yi dinlemeye devam eder. Bu işlemi onlarca kez yaptığımda veya bazen tek bir işlemde bile Memory Leak sebebiyeti oluşturmuş oluruz.

antipattern_mvvm_4 kod parçacığıma bir ekleme yaparak onCleared içinde callMyCars’ı bırakmasını söylüyorum. Bu tip işlemler için mutlaka onCleared içinde “cancel” yapmalıyız .

Şimdilik aklıma gelen MVVM mimarisi için küçük ipuçları ve yapılmaması gerekenler listesi bu şekilde. Fakat bu yazının 2.si de olacak gibi duruyor.

Son Not: Kotlin ile ilgili yazılarımda ayrıca bahsedeceğim fakat şimdiden bir ön not: Yukarıda verdiğim kod örneklerinde kullanılan !! işaretini ignore edebilirsiniz. !! operation’ı bir arkadaşımın da dediği gibi kotlin de hiç kullanılmamak üzere yaratılmış bir operation’dır.

--

--