Android Jetpack Compose — Projenin Temellerini Oluşturma
Öncelikler herkese selamlar. Buradaki yazımızda Jetpack Compose kullanarak bir Android projesinin temellerini oluşturmaya çalışacağız.
Projeyi oluştururken Google’ın bizlere sunduğu örnek proje olan NowInAndroid uygulamasını referans alacağız. Projenin temellerini oluşturduktan sonra, temel yapıyı koruyarak, kendi ihtiyacınıza göre ekranları ekleyip projenizi ilerletebileceksiniz.
Kısaca projeden bahsedecek olursam; projemizde genelde uygulamalarda kullandığımız yapıları kullanacağız. Projemizde bir tane BottomAppBar ve TopAppBar olacak ve 3 adet ekran içerecektir.
1- Kütüphanelerin eklenmesi
Dependency işlemleri için Hilt kütüphanesini, navigasyon işlemleri için Jetpack Navigation kütüphanesini ekleyeceğiz. app level build.gradle’ı açıp, dependencies içerisine kütüphaneleri ekliyoruz. Hilt kurulumu için ise ek olarak aynı sayfa içerisinde plugins bloğuna eklemeler yapıyoruz. İşlemler sonunda gradle dosyanız aşağıdaki gibi gözükecektir.
Son olarak project level build.gradle dosyamızı açıyoruz ve yine plugins bloğu içerisine Hilt için gerekli olan son eklemeyi gerçekleştiriyoruz. İşlem sonucunda gradle dosyanız aşağıdaki gibi gözükecektir.
2- Hilt kurulumu
Projemizi oluşturmaya başlamadan önce Hilt kütüphanesi için gerekli olan kurulumları yapalım. Application() class’ını extend eden bir sınıfa ihtiyacımız var. application adında bir package oluşturalım ve içerisine ___Application isminde bir class yaratalım. Buradaki prefix’in ne olacağı size kalmış. Ben, oluşturacağımız uygulama için DemoAppApplication isimlendirmesini kullanacağım.
Sınıfı oluşturduktan sonra sınıfın başına @HiltAndroidApp notasyonunu ekleyeceğiz. Bu notasyon ile ilgili daha detaylı bilgiler için Hilt kütüphanesini inceleyebilirsiniz.
Bu işlemlerden sonra kodunuz aşağıdaki gibi gözükmesi gerekmektedir:
Son olarak eklediğimiz application sınıfını uygulamamızın manifestine tanıtmamız gerekiyor. name tag’ını kullanarak ekleyeceğiz. Manifest dosyasınızın son hali aşağıdaki gibi olmalıdır:
Hilt kurulumumuz tamamlandı. Kod yazmaya başlayabiliriz.
3- İlk ekranı oluşturma
Uygulamamızda ekranlarımız presentation katmanında olacağı için presentation adında bir package oluşturuyoruz. İlk ekranımız Home ekranı olacak ve uygulama açıldığında ilk bu ekran gözükecek. Temiz bir yapı kurmak istediğimiz için presentation package’i içerisine bir de home adında bir package oluşturalım.
Daha sonra HomeScreen adında bir Kotlin dosyası oluşturuyoruz.
Burada iki tane Composable fonksiyon oluşturacağız. Bir tanesi ekranda göstereceğimiz içeriği(UI components) barındırırken, diğer fonksiyonumuz içerikte gösterilecek datayı(state) pasladığımız ve kullanıcı etkileşimlerini(events) alıp business logic’e gönderdiğimiz, aynı zamanda navigasyonda kullanacağımız parça olacak.
Peki bu yapının bize faydası nedir? Öncelikle bu yapı, state hoisting dediğimiz bir programlama kalıbını takip ederek oluşturduğumuz bir yapı. Bu yapı sayesinde içeriği gösterdiğimiz composable metotlarımız Screen UI state’e bağımlı olmuyor. Bu sayede ise bu composable metodu daha rahat test edebiliyor, tekrar kullanabiliyor ve bug oluşturma eğiliminin de önüne geçmiş bulunuyoruz. Google’ın da önerdiği state hoisting ile alakalı daha fazla bilgi almak isterseniz takip eden uzantılardan ulaşabilirsiniz: Referans-1, Referans-2, Referans-3
HomeRoute ve HomeScreen adında iki tane composable metot oluşturalım:
Yukarıda bahsettiğim gibi HomeRoute metodu içerisinde Screen UI state’i tutacak ve bunu HomeScreen adı verdiğimiz, Screen UI state bağımsız, içerisinde UI componentleri bulunan metodumuza paslayacak. Aynı zamanda HomeScreen tarafından yakalanan eventleri de business logic kısmına iletmemize aracılık edecek.
3–1- Home Screen’in navigasyona hazır hale getirilmesi:
Ekran kurulumu tamamlandı. Son olarak HomeScreen’i navigasyonda kullanabilir hale getireceğiz ve bu ekran bizim için hazır hale gelecek. presentation/home paketi içerisinde navigation adında bir paket daha oluşturuyoruz. Bu paket içerisine de HomeNavigation adında bir Kotlin dosyası oluşturalım ve içini aşağıdaki gibi dolduralım:
Öncelikle Home ekranı için navigasyon esnasında kullacağımız bir string nesnesine ihtiyacımız olacak. Bunu ilk olarak tanımlıyoruz.
Daha sonra 2 tane extension metodumuz var. Bunlardan ilki NavController için yazılmış. Bu extension sayesinde Home ekranına, navConroller nesnesi olan her yerden kolaylıkla geçiş yapabiliriz. Burada default değeri null olan NavOptions’ı da dilediğimiz zaman kullanabiliriz.
Diğer extension ise NavGraphBuilder için yazılmış. Burada oluşturduğumuz ekranı compose navigation’a ekliyoruz diyebiliriz. Yukarıda da görüldüğü üzere öncelikle ilgili ekranın route’unu belirliyoruz. Daha sonra içerideki blokta da ilgili ekranı NavGraphBuilder’a paslıyoruz. Bu yaptığımız işlem sayesinde NavGraphBuilder bu route üzerinde hangi ekrana gitmesi gerektiğini biliyor. Dikkat edeceğiniz üzere ekranı paslarken HomeScreen yerine HomeRoute’u kullandık. Bunun sebebi yukarıda da bahsettiğim gibi, state hoisting yaparak HomeScreen’i state bağımsız ve tekrar kullanılabilir hale getirdik. Bundan sonra bu ekran için tek muhattabımız HomeRoute olacak.
İlk ekranımızı oluşturduk. Buna ek olarak Profile ve Favorites ekranlarını da oluşturacağız. Yazının uzunluğu sebebiyle bu ekranların oluşturulmasının detaylarını göstermeyeceğim. Uygulamanın belirli bir pattern’i takip etmesi önemli, bu sebeple diğer ekranları da yukarıda anlattığım yapıyı takip ederek oluşturabilirsiniz. Son olarak package yapısı aşağıdaki gibi gözükmelidir:
Burada oluşturulan ViewModel sınıfları kafanızı karıştırmasın. Hilt’e uygun şekilde yaratılmış ve içleri boştur. Hilt viewModel kurulumunu incelemek için takip eden uzantıya bakabilirsiniz. Referans-4
4- BottomAppBar ve TopAppBar oluşturulması
4–1 - BottomAppBar oluşturulması
BottomAppBar’a Home ve Profile ekranlarını ekleyeceğiz. Yani bu ekranlar için bize bir title, selectedIconId ve unselectedIconId gerekli. Şimdi bunları oluşturmaya başlayabiliriz. İlk önce yeni bir paket oluşturuyoruz ve buna navigation ismini veriyoruz. Uygulama içerisinde sınıfların sorumlulukları ile alakalı isimlendirmeler alan paketler içinde bulunması önemli. Bu bize daha kolay bir yönetim sağlayacak. Bu paket içerisine TopLevelDestinations adında bir enum class oluşturuyoruz.
Bu enum’u doldurmadan önce bu sınıfın altına aşağıdaki sealed class’ı ekliyoruz:
Bu sealed class NowInAndroid projesinde bulunmaktadır. Burada kullanacağımız ikonların farklı tiplerde olsa da yönetimini kolaylaştıran bir ortak tip olarak düşünebilirsiniz.
TopLevelDestinations enum’a bir constructor ekleyeceğiz. Burada oluşturduğumuz item’ları BottomNavigationItem olarak kullanacağımız için bize title, selectedIconId ve unselectedIconId olacak şekilde üç tane değişken gerekmekte. Home ve Profile ekranlarını aşağıdaki gibi enum class içerisinde tanımlıyoruz ve sınıfımız son halini alıyor.
Yukarıda da göreceğiniz üzere Icon sealed class’ı sayesinde, Home için material ikonları kullanırken, Profile için drawable üzerinden vektör kullanabildik.
Daha sonra ilk proje oluşturulduğunda gelen ui paketi içerisine gidip component adında bir paket oluşturabiliriz. Bu paketin içerisinde __BottomAppBar adında bir Kotlin dosyası oluşturuyoruz. Burada prefix size kalmış. Ben DemoAppBottomAppBar isimlendirmesini yapacağım.
Yine aynı ismi kullanarak bir composable fonksiyon oluşturacağız. Bu fonksiyon üç tane parametre alacak; 1- Liste halinde TopLevelDestination içerisinde oluşturduğumuz ekranları, 2- Mevcut ekranı, 3- BottomNavigationItem tıklandığı zaman aksiyon almak için, tıklanan ekranın bilgisini paslayan bir Kotlin function type parametresi. İlgili component’in son hali aşağıdaki gibi olmalıdır:
Peki burada ne yapıyoruz? Aslında karmaşık bir durum yok. Compose component’i olan BottomAppBar’ı oluşturuyoruz. Daha sonra TopLevelDestinations içerisinde bulunan ekranlarımız için bir BottomNavigationItem oluşturuyoruz. Seçili olup-olmama durumu için gerekli logic’i ayarlıyoruz. Tıklanma durumunda ise hangi item’a tıklandı bilgisini Kotlin function type parametresini kullanarak bir üst tarafa aktarıyoruz. Bu kısmı ilerleyen kısımlarda daha iyi anlayacaksınız.
En altta bulunan extension metodu karmaşık gözükse de mantığı basit. Mevcut destination, bizim tanımladığımız TopLevelDestination’lar içerisinde var mı kontrolü yapılıyor. Buradan dönen değere göre BotttomAppBar’daki item’lar üzerinde selected-unselected durumu ayarlanıyor.
4–2- TopAppBar oluşturulması
TopAppBar için de oluşturduğumuz ui/component paketi içerisine __TopAppBar isminde bir Kotlin dosyası oluşturuyoruz. Prefix size kalmış, ben DemoAppTopAppBar isimlendirmesini yapacağım. Dosya adıyla aynı şekilde DemoAppTopAppBar isminde bir composable fonksiyon oluşturalım. TopAppBar üzerinde sadece ilgili ekranın title değerini göstereceğimiz için burada ekstra bir logic kurmayacağız. Sınıfın son hali aşağıdaki gibi olmalıdır:
Sınıfların, sorumlulukları ile alakalı isimlendirmeler alan paketlerin içinde olmasının yanı sıra component’lerin de ayrı sınıflarda olması, proje büyüdükten sonra yönetimi kolaylaştıracaktır.
5- AppState oluşturulması
Öncelikle AppState nedir ve bize faydaları nelerdir kısaca buna değinmeye çalışacağım. AppState, içerisinde UI state ve UI logic’i barındıran, Activity lifecycle’ına bağımlı (Activity tekrar yaratıldığında tekrar yaratılan) bir state holder sınıfıdır. Burada oluşturulan UI scoped datalar aynı lifecycle’ı paylaştıkları için güvenle tutulabilir.
AppState sınıfını oluşturarak uygulama iskeletinde göstereceğimiz component’leri tek bir yerden rahatlıkla yönetebileceğiz. Örneğin, BottomAppBar görünürlüğünü ve navigasyonunu, aynı zamanda toolbar title’ını buradan yöneteceğiz. Daha kompleks UI state ve UI logic işlemlerini de buraya ekleyip uygulamamızı kolaylıkla genişletebileceğiz.
Öncelikle ui paketi içerisine gideceğiz ve burada __AppState isminde bir Kotlin dosyası oluşturacağız. Prefix’i dilediğiniz gibi yapabilirsiniz, ben DemoAppState olarak oluşturacağım. AppState’imiz bir state holder sınıfı olduğu için DemoAppState adında bir sınıf oluşturalım. Bu sınıf constructor’da bir NavHostController değişkeni alacaktır. Yukarıda da yazdığım gibi BottomAppBar navigasyonunu buradan yöneteceğiz ve uygulamanın diğer noktalarında ihtiyacımız olacak UI state datalarını buradan sağlayacağız.
Sınıfı oluşturduktan sonra bir tane composable metoda ihtiyacımız olacak. Bu sınıf bir state holder sınıfı ama bizim bu sınıfı çağırıldığı yerlere state olarak sağlamamız gerekli. Örneğin LazyLisState sınıfını ele alalım. Kendisi bir state holder sınıfı ve listenin state’ini tutmakta. Biz bu sınıfı composable metotlarımız içerisinde nasıl çağırıyoruz? rememberLazyListState() metodunu kullanarak.
Bu amaçla rememberDemoAppState adında bir composable metot oluşturuyoruz. Bu metodun dönüş değeri ise DemoAppState olmalı. Fakat return ederken composition cycle’a eklemek için remember{} içerisinde return edeceğiz. Aynı zamanda DemoAppState sınıfımız bir NavHostController değişkenine ihtiyaç duyduğu için bu metodumuz da parametre olarak bu değişkeni alacak ve sınıfa paslayacak. Yazıda hayal etmesi zor olabilir ama yaptığımız işlem aslında basit. Günün sonunda composable metodunu aşağıdaki gibi gözükecektir:
Metodu oluşturduk, class’ı da oluşturup üzerinden bir geçelim. AppState class’ınız aşağıdaki gibi olacaktır:
UI state ve UI logic diye iki tane kavramdan bahsettim. Nedir bunlar diye aklınıza geldiyse burada netleşeceğiniz düşünüyorum. AppState sınıfımız UI ile alakalı ve bu sınıf içerisinde oluşturduğumuz değişkenler UI state’leri temsil ederken; bu sınıf içerisindeki metotlar UI logic’leri temsil etmektedir.
Peki buradaki değişkenlerimiz nedir? İsimlendirmelerden de anlaşılacağı gibi currentDestination mevcut konumu, currentTopLevelDestination değişkeninde de navigasyonda BottomAppBar’da gösterilen mevcut item’ı tutuyoruz. TopLevelDestinations listesi ise BottomAppBar’da gösterilecek itemler tutuluyor. Bu değişkeni BottomAppBar’ı oluştururken kullanacağız.
Peki buradaki metotlarımız ne yapıyor? onBackPressed() adına uygun bir şekilde backPress event’lerinde çağrılmakta. navigateTopLevelDestinations() metodu ise BottomAppBar’da gezinirken yapılan navigasyon işlemini yönettiğimiz metot olarak karşımıza çıkmaktadır. Metodun detayları da içerisinde açıklama olarak mevcuttur.
Günün sonunda AppState sınıfını ve bunu provide ettiğimiz composable metodumuzu oluşturduk.
Daha kompleks AppState işlemleri için referans projemiz NowInAndroid’i inceleyebilirsiniz. Referans-5
6- NavHost’un oluşturulması
Uygulamayı tamamlamamıza az kaldı. Şimdi bize bir adet NavHost gerekli. NavHost’u xml’li yapılarda kullandığımız FragmentContainerView olarak düşünebilirsiniz. Uygulamada göstereceğimiz bütün ekranları buraya ekleyecek ve BottomAppBar navigasyon işlemleri dışında kalan navigasyon işlemlerini burada gerçekleştireceğiz.
Daha önce oluşturduğumuz navigation paketi içerisinde gidip __NavHost adında bir Kotlin dosyası oluşturuyoruz. Ben DemoAppNavHost isimlendirmesini kullanacağım. Bu dosya içerisinde DemoAppNavHost adında bir composable fonksiyon oluştuyoruz. Bu fonksiyon parametre olarak Modifier, NavHostController ve onBackClick adında 3 tane parametre alacak. Daha sonra fonksiyon bloğu içerisine navigation compose component olan NavHost oluşturup parametre olarak modifier, navHostController ve start destination’ı tanımlıyoruz. NavGraphBuilder kısmında ise ekranlarımızı ekliyoruz. Dosyanın son hali aşağıdaki gibi olacaktır:
Her ekranımızı oluştururken bir de navigation paketi oluşturup, içerisinde __Navigation sınıfları oluşturmuştuk ve NavGraphBuilder bir extension yazıp ekranın tanıtılmasını burada gerçekleştirmiştik. NavHost içerisinde bu extension’ı kullanıyoruz. Bu sayede son derece sade, anlaşılır ve yönetimi kolay bir yapı elde ettik.
Burada Profile ekranında Favorites ekranına bir geçiş olduğunu farkedeceksiniz. Bunu da property drilling dediğimiz işlem kullanarak gerçekleştiriyoruz.
6–1 Profile -> Favorites navigasyonu
Profile ekranında bir butonumuz mevcut. Bu butona tıklandığında Favorites ekranına geçiş yapıyoruz. Peki kurduğumuz bu yapıda bunu nasıl gerçekleştiriyoruz.
Öncelikle buton click event’ini Screen state’ini tutan ProfileRoute composable fonksiyona döndürmemiz gerekiyor. Bu tarz işlemler için ise en iyi yardımcımız Kotlin function type parametreler olarak karşımıza çıkıyor. Function Type parametreler hakkında daha detaylı bilgi almak için takip eden uzantıya gidebilirsiniz. Referans-6
Screen State’i tutan __Route isimlendirmeli fonskiyondan da NavGraphBuilder’a yazdığımız extension’a bu event’i paslıyoruz. En sonunda da buton click event’ini NavHost içerinde yakalayıp aksiyon alabiliyoruz. Bu parametre içerisinde değer de taşıyıp ekranlar arası veri de aktarma yapabiliriz. Bu işlem property drilling’e örnektir.
Görsel olarak property drilling işlemi aşağıdaki gibi olmaktadır.
7- App content’in oluşturulması
Oluşturmamız gereken yapıları tamamladık. Şimdi uygulama iskeletimizi oluşturacağız. BottomAppBar ve TopAppBar’ı ekleyecek; AppState’i bağlayacak ve uygulamayı kullanıma hazır hale getireceğiz.
ui paketi altına gidip __App adında bir Kotlin dosyası oluşturuyoruz. Ben DemoApp ismini kullanacağım. Bundan sonra DemoApp adında bir composable fonksiyon oluşturalım.
Bu fonksiyon appState’i parametre olarak alacak ve default değeri tanımladığımız rememberAppState() fonksiyonu olacak. Bu sayede appState’i bu composable’a state olarak provide etmiş oluyoruz.
Fonksiyon bloğu içerisinde bir Scaffold oluşturacak ve TopAppBar, BottomAppBar ve NavHost component’lerimizi ilgili yerlere ekleyeceğiz. Fonksiyonun son hali aşağıdaki gibi gözükmelidir:
Burada component’lere ek logic’ler de ekledik. Biz sadece TopLevelDestination’da tanımladığımız ekranlar için BottomAppBar ve TopAppBar göstermek istiyoruz. Aynı zamanda TopAppBar’daki title değişkenini de TopLevelDestinations’tan almak istiyoruz. Buradaki UI logic ve UI state işlemlerini de AppState içerisinde oluşturup yönetiyoruz.
En sonunda MainActivity’e gidip oluşturduğumuz App content’i ekliyoruz ve uygulamamız çalışmaya hazır hale gelmiş oluyor. MainActivity sınıfınız aşağıdaki gibi gözükmelidir:
MainActivity sınıfına @AndroidEntryPoint notasyonunu eklemeyi unutmayın.
ÖZET
Özet olarak bu yazımızda Compose kullanarak, anlaşılır, yönetimi ve bakımı kolay olan bir Android uygulamasının temellerini attık. Burada kullandığımız yapılar Google’ın örnek proje olarak yayınladığı NowInAndroid isimli uygulamadan alınmıştır.
Uygulamamızda BottomAppBar üzerinde iki ekranımız, bir tane de detay ekranımız bulunmaktadır. Temellerin işlendiği yazımızı referans alarak, kendi ihtiyaçlarınız doğrultusunda uygulamanızı genişletebilirsiniz.
Bir sonraki yazımızda, bu projeyi temel alarak screen state yönetimi hakkında denemeler yapacağız. Umarım faydalı bir yazı olmuştur. Vakit ayırdığınız için teşekkürler.
Proje Repo uzantıları:
Yazı içerisindeki referanslar:
- State and Jetpack Compose — Referans-1-buraya tıklayın
- State Hoisting —Referans-2- buraya tıklayın
- State Holders and UI State —Referans-3-buraya tıklayın
- Dependency Injection with Hilt —Referans-4- buraya tıklayın
- NowInAndroid AppState —Referans-5-buraya tıklayın
- Kotlin High-order Functions and Lambdas —Referans-6-buraya tıklayın