8- Infrastructure in QNB Android Mobile📢
Herkese selamlar, ben Melike 👋🏻
Bu yazıda sizlere sürekli yeni özellikler eklediğimiz ve giderek dinamik bir şekilde büyüyen QNB Android Mobil projemizdeki infrastructure’dan bahsedeceğim. Böyle büyük bir uygulamanın performansını ve verimliliğini korumak, projeyi sürdürülebilir kılmak için her geçen gün güncel teknolojileri projemize entegre etmeye çalışıyoruz.
Bir süredir, kullandığımız teknolojileri, son zamanlarda yaptığımız refactor çalışmalarını sizlerle paylaşmak adına ekip olarak birçok makale yayımladık. Ben de bu yazımda hem projemizde kullandığımız teknolojileri, hem geliştirme süreçlerimizi hem de refactor çalışmalarımızı bir özet niteliğinde sizlerle paylaşmak istedim. Umarım paylaştığımız tecrübeler sizler için de faydalı olur. Keyifli okumalar🚀 😊.
Uygulama Mimarisi 👷🏻♀️
Projemizin mimarisini, Google tarafından tavsiye edilen katmanlı mimari yapısını ve MVVM (Model-View-ViewModel) prensiplerini benimseyerek oluşturduk. Bu yapı sayesinde kullanıcı arayüzü (UI) ve veri kaynağı (Data) birbirlerinden net bir şekilde ayrılıyor. Kodumuzun daha düzenli ve anlaşılır olmasına olanak sağlıyor. Her katmanın sorumluluklarının birbirinden ayrılmış olması kodumuzun test edilebilirliğini artırıyor. Hataları hızlı bir şekilde tespit edebiliyoruz. Aynı zamanda yeni özellikler eklerken ve refactor çalışmalarımızı sürdürürken mevcut işleyişi bozmadan kolaylıkla ilerleyebiliyoruz. Ek olarak kullanıcı deneyiminde tutarlılığı sağlayarak performanslı bir uygulama sunmamıza destek oluyor. Kullandığımız mimaride temel olarak iki katmanımız var: Data ve UI.
UI Katmanı, kullanıcıya veriyi göstermekten sorumlu katmanımız. Bu katmanı, Activity/Fragment, ViewModel ve UIState sınıfları oluşturuyor. Activity ve Fragment sınıflarını, view component’lerinden oluşan kullanıcıların karşılaştığı ekranlar olarak nitelendirebiliriz. Bu sınıfların tek görevi kullanıcıya veriyi göstermek ve kullanıcıların girdiği inputları (örneğin bir butona tıklamak) ViewModel’e iletmek. ViewModel’lerin görevi ise kullanıcı arayüzündeki etkileşimleri data katmanına ileterek data katmanında gerekli güncellemelerin yapılmasını ve bu güncellemelerin tekrardan UI’a yansıtılarak kullanıcıya sunulmasını sağlamak. UIState sınıfları için de kısaca ViewModel üzerinden gelen verinin Activity veya Fragment’larda kullanıcıya ne şekilde sunulacağını tanımlayan sınıflar diyebiliriz. UI Stateler sayesinde, XML veya ViewModel sınıfları içinde UI logic yazımının da önüne geçiyoruz. Data katmanından ViewModel’e gelen verileri, UIState sınıfları aracılığıyla tasarımlarımızı tutan XML sınıflarına Data Binding kütüphanesini kullanarak bağlıyoruz.
Data Katmanı, projeye veri akışının sağlanmasından sorumlu sınıflarımızı barındıran katman. Network üzerinden veri sağladığımız Remote-Data Source, lokal veri saklama yapıları (Room, Shared Preferences, DataStore) üzerinden veri sağladığımız Local-Data Source sınıflarının implementasyonları, repository sınıflarımız ve data modellerimiz data katmanınımızı oluşturuyor.
Multi-Module Yapı 🧩
Modüler mimari, code-base’i modül olarak isimlendirdiğimiz daha küçük, bağımsız ve yönetilebilir parçalara bölünmesidir. Bu yapı ile, kodun yeniden kullanılabilirliğini arttırmayı, projenin farklı bileşenlerini birbirinden bağımsız olarak geliştirmeyi ve test etmeyi mümkün kılabiliyoruz. Her modülün belirli bir işlevi ve sorumluluğu yerine getirmesi kodun okunabilirliğini arttırdığı gibi, bakım maliyetimizi de düşürüyor. Ek olarak kalabalık bir ekip olarak geliştirdiğimiz projemizde, belirli modüllerde değişiklikler yapılırken diğer modüllerin bu değişikliklerden etkilenme riskini de azaltıyor. Takım olarak paralel bir şekilde çalışmamızı kolaylaştırıyor ve geliştirme sürecimizi hızlandırıyor. Ayrıca kurum içinde yeni bir android projesi açılması durumunda multi-module yapımızdaki atomik modülleri kullanarak geliştirme sürecini hızlandırmak da hedeflerimiz arasında 🎯.
Son zamanlarda projemizi sürdürülebilir kılmak için yaptığımız en büyük çalışmalardan biri, sorumlulukları birbirinden ayrılmayan, yanlış yapılandırmadan dolayı sürekli şişen ve birbirlerine olan bağımlılıkları yüzünden build süremizi oldukça arttıran modül yapımızı, Google tarafından önerilen modüler mimari yaklaşımlarını inceleyerek (Now in Android) yeni bir modül yapısına taşımak oldu. Projemizi bütün hatlarıyla tekrar tasarlayıp, sağlam bir zemine oturtarak baştan yarattık diyebiliriz 🥳 . Geçiş sürecimizi ve yeni modüler yapımızı merak ediyorsanız yayımladığımız Modularization in QNB Android Mobile makalesini incelemenizi tavsiye ediyorum 😊.
Dependency Injection (Hilt) 🔗🤝
Nesne tabanlı programlamada sınıflar sıklıkla birbirlerinin referanslarına ihtiyaç duyarak birbirlerine bağımlı bir şekilde çalışırlar. Dependency Injection (DI) bu bağımlılıkları yönetmek için kullanılan bir programlama metodudur. DI ile sınıflar bağımlılıklarını kendi içinde oluşturmazlar, implementasyonları ile ilgilenemezler yalnızca onları dışardan alıp kullanırlar. Bu sayede component’lerin birbiriyle olan bağımlılığı oldukça azaldığı gibi gerektiği zaman herhangi bir yeni entegrasyona geçilmesi de kolaylaşır.
Yakın zamanda projemizde DI yönetimi için kullandığımız Koin kütüphanesini Hilt ile değiştirdik. Hilt, bağımlılıkları compile-time’da çözdüğü için run-time’da karşılaşabileceğimiz potansiyel sorunlardan bizi kurtardı. Aynı zamanda Google tarafından da desteklenen Hilt’in projemizde kullandığımız Jetpack component’leriyle olan uyumlu çalışma avantajlarını da kullanmak istedik. Yaptığımız bu geçiş süreci ve projemizdeki Hilt kütüphanesi ile kurduğumuz DI yapımız hakkında daha detaylı bilgi sahibi olmak isterseniz ekip arkadaşlarımın daha önce yayımladığı Koin to Hilt Migration in QNB Android Mobile makalesini inceleyebilirsiniz 😊.
Asenkron Yapılar (Kotlin Flow-Coroutines) ⏳
Uygulamamızda servis çağrıları, veritabanı üzerinden yapılan sorgular gibi uzun zaman alan işlemler sırasında kullanıcı deneyimini kesintisiz ve hızlı tutmak adına asenkron yapılara başvuruyoruz. Bu asenkron işlemlerimizi projemizde, Google’ın da android geliştirme ekosisteminde desteklediği Kotlin-Flow ve Coroutines’i kullanarak yönetiyoruz.
Coroutines kullanımı, bizi geleneksel asenkron programlamada sıklıkla karşılaşılan kodun okunabilirliğini ve yönetimini azaltan, karmaşıklığını arttıran callback’lerden suspend fonksiyonlar ile kurtarıyor. Performans açısından da light-weight threads olarak adlandırılan coroutines, thread’lerle kıyaslandığında çok daha az maliyetli ve verimli bir yapı sunuyor. Düşük bellek kullanımı sayesinde sınırlı kaynaklara sahip mobil cihazlar bile birçok işlemi eş zamanlı olarak daha performanslı çalıştırıyor.
Kotlin-Flow içinse Coroutines kütüphanesinin bize sunduğu reaktif veri akışını yönetmemizi sağlayan bir API diyebiliriz. Reaktif programlama dinamik veri akışlarına uygulamanın seri bir şekilde tepki vermesini sağlayan bir programlama modeli. Uygulamayı kullanan kullanıcılara sunduğumuz arayüzün (UI) kullanıcının input’larına duyarlı ve oldukça akıcı olması hedefimiz. Asenkron çalışan coroutines üzerine kurulan flow sayesinde, data kaynağından gelen sürekli güncellemeleri işleyip arayüze main-thread’i bloklamadan yansıtıyoruz. Data ve UI katmanı arasındaki bu sürekli iletişim sırasında çeşitli senaryolara göre State Flow, Shared Flow ve Channel yapılarını aktif olarak kullanıyoruz. Ek olarak flow’un bize sunduğu operatörler ile (map, filter, combine vs.) veri kaynağından gelen veriler üzerinde manipülasyonlara ihtiyaç duyduğumuzda oldukça kolay bir şekilde bu işlemi gerçekleştirebiliyoruz.
Statik Kod Analizi (Detekt) 🔎
Büyük bir ekip olarak çalışmanın zorluklarından birisi de kod standartlarını yönetmek. Aynı projede geliştirme yapan tüm developer’ların farklı kod yazma tarzlarına çok müdahale etmeden projede tutarlı bir code-base’e sahip olmak istiyoruz. Bu yüzden oluşturduğumuz ortak standartlarımız bulunuyor. Statik code analizi de belirlediğimiz temel kod standartlarımızı bizim için inceleyip, uyumsuzlukları tespit ediyor ve code review sürecimizi oldukça kolaylaştırıyor. Projemizde yakın zamanda statik code analizi için Ktlint ve SonarQube kullanımından, Kotlin base olan Detekt kullanımına geçtik. Bu geçiş sürecimizi, Detekt’i neden ve nasıl kullandığımız ile ilgili detaylı bilgi için daha önce yazdığımız Static Code Analysis Detekt in QNB Android Mobile makalemizi incelemenizi tavsiye ediyorum 😊.
Unit Testler👮 🚨
Unit testler ile code-base’imizdeki android framework’ünden bağımsız çalışan fonksiyonlarımızın istenilen şekilde çıktılar ürettiğinden emin olmaya çalışıyoruz. Yazdığımız testler sayesinde ilerleyen zamanlarda oluşabilecek olası hataları erkenden tespit edip düzeltebiliyoruz. Aynı zamanda yazığımız unit testler, yapacağımız yeni geliştirmelerin veya refactor çalışmalarının, daha önceden yazılan kodların mevcut işlevini bozmadığını da bizim için sürekli olarak kontrol ediyor. Bir nevi kodumuzun koruyucusu diyebiliriz 👮. Ek olarak bir projeye unit test yazıyor olmamız, developer olarak bizleri geliştirme yaparken clean-code prensiplerine uymamız için de teşvik ediyor.
Projemizde, ViewModel sınıflarına eklediğimiz logic içeren fonksiyonlara unit test yazmak ekip olarak önem verdiğimiz bir konu. Code-Review aşamasında unit test yazımını da kontrol ederek takım olarak birbirimizi sürekli motive ediyoruz.
Test yazımı için, ortak bir paydada buluşmak için belirli standartlar oluşturduk. Bütün test senaryolarımızı Given (Test edilen fonksiyonun başlangıç koşulu) — When (Test edilen işlev) — Then (Beklenen sonuç) aşamalarından oluşacak şekilde kurguluyoruz. Test yazdığımız fonksiyonun olası bütün case’lerine ayrı ayrı testleri ekleyerek, kodumuzun farklı durumlarda doğru çıktılar ürettiğinden emin olmaya çalışıyoruz.
Son zamanlarda, Gemini’ın yardımıyla unit test yazımı için harcadığımız zamanı oldukça minimize ettik. Gemini, verdiğimiz kodu analiz ederek, kodun olası senaryolarını belirliyor ve bizim istediğimiz standartlarda otomatik testler oluştuyor. Ayrıca Gemini’ı takım olarak yazılım geliştirme süreçlerimizde oldukça fazla kullanıyoruz. Bu kullanım detaylarımızı da anlatacağımız bir yazıyı da ilerleyen zamanlarda paylaşacağız.
Java ve Kotlin dillerine test yazımı için geliştirilmiş olan JUnit kütüphanesini, testlerimizde mock nesneler oluşturmak için MockK kütüphanesini, Flow yapılarını test edebilmek için Turbine kütüphanesini ve Coroutines ile yazdığımız kodları test edebilmek için TestCoroutines kütüphanesini kullanıyoruz.
Code Review Sürecimiz 👀 ✅
Code review süreçlerimizi Azure DevOps üzerinden ilerletiyoruz. Developer olarak geliştirmelerimiz test süreçlerine hazır olduğunda Azure üzerinden pull — request oluşturuyoruz ve ekip arkadaşlarımızdan code review talep ediyoruz. Bu aşamada bazen ekip içerisinde yorumlar alıyoruz, görüşleri tartışıyoruz ve düzenlemeler yapabiliyoruz 🧐. Pull-request’lerimizin tamamlanması için min iki reviewer, maks ise geliştirme yaptığımız modul sayısına göre artan reviewer bir yapımız var. Onaylardan birisini kod eklemek istediğimiz modülden sorumlu olan takım arkadaşlarımızdan alıyoruz, ikincisini ise projenin bütününe hakim olan daha tecrübeli takım arkadaşlarımızdan alıyoruz. Gradle, proguard vb dosyalarda yaptığımız değişikliklerde ise takım liderimizden onay alıyoruz.
Ayrıca, PR aşamasında yine Azure Devops üzerinden oluşturduğumuz policy’ler sayesinde istenmeyen durumları kontrol edebiliyoruz. Örnek olarak projemize uygulama boyutumuzu yükselten ve performans kaybı yaşatan, JPEG ve PNG formatlarında görseller eklememeye çalışıyoruz. Açılan PR’da böyle bir ekleme varsa policy bizleri uyarıyor.
Bu yazıyla beraber, QNB Android Mobile projemizde kullandığımız temel yapıları açıklamaya çalıştım. Okuduğunuz için çok teşekkür ederim. Yorumlarınız ve merak ettikleriniz için bana ve ekip arkadaşlarıma dilediğiniz zaman ulaşabilirsiniz 😊. Bir sonraki yazımız CI/CD Processes using Azure Devops in QNB Android Mobile ‘da görüşmek üzere 🙋🏻♀️.