Android Jetpack Compose Mvvm Example Clean Code

Bahadireray
21 min readJul 26, 2024

--

Herkese merhaba sizlere bu yazımda android compose ile temiz bir mimarı kurup bunun içerisinde bir örneğin bulunduğu projemi anlatacağım. Biraz uzun bir yazı olacaktır :)

Project

Şimdi sizlere projedeki yapımı anlatayım,

Solda ViewModel, sağda MainActivity bulunuyor. ViewModel, UI’nin durumunu (UIState) ve bu duruma etki eden olayları (UIEvent) yönetiyor. MainActivity ise Compose fonksiyonlarını kullanarak UI’yi oluşturuyor ve ViewModel ile iletişim kuruyor.

İşte temel adımlar:

  1. MainActivity, ViewModel’e state listener ekler.
  2. ViewModel, UIState’i günceller.
  3. State listener, UIState değişikliğini algılar ve UI’yi günceller.
  4. MainActivity, UI’de gerçekleşen olayları (tıklama vb.) ViewModel’e gönderir.
  5. ViewModel, UIEvent’i işler ve UIState’i günceller.

Bu şekilde, ViewModel, UI’nin durumunu yönetmek için merkezi bir nokta sunar ve MainActivity’nin daha temiz ve basit bir yapıya sahip olmasını sağlar.

Yazılım geliştirmede yaygın olarak kullanılan bir mimari kalıbı olan MVC (Model-View-Controller)’nin MVVM (Model-View-ViewModel) ile birleşik bir şekilde gösterilmesini amaçlıyor. MVVM, kullanıcı arayüzü (UI) geliştirme, iş mantığı ve veri erişimi katmanlarını ayırmak için kullanılan bir kalıptır.

1. Presentation:
* View: Kullanıcı arayüzünü temsil eder. Bu genellikle kullanıcıların etkileşim kurabileceği görsel öğeleri içerir.
* ViewModel: View ile veri ve iş mantığı arasındaki köprü görevi görür. View’ın ihtiyaç duyduğu verileri hazırlar, iş mantığını yönetir ve View’e veri akışını yönetir.

2. Domain:
* UseCase: Uygulamanın temel iş mantığını kapsar. Belirli görevleri (örneğin, veri alma, veri kaydetme) yerine getirir.
* Repository: Veri erişim işlemlerini gerçekleştirir. Verileri veri kaynağı (örneğin, veritabanı, API) üzerinden alır veya kaydeder.

3. Data:
* Factory: Veri kaynakları (örneğin, Room veritabanı, Retrofit API) için nesne oluşturma işlemini kolaylaştırır.
* Network (Retrofit): Ağ tabanlı veri erişimini yönetir (örneğin, API çağrıları).
* Local (Room): Yerel veritabanı erişimini yönetir.

Görselde gösterilen etkileşimler şunlardır:

  • View, ViewModel’e bir function (fonksiyon) göndererek veri isteyebilir veya bir olay bildirebilir.
  • ViewModel, suspend function (askıda fonksiyon) kullanarak UseCase ile iletişim kurar ve iş mantığına erişir.
  • UseCase, suspend function ile Repository aracılığıyla veri erişimini yönetir.
  • Repository, Factory’yi kullanarak veri kaynaklarından (örneğin, Room, Retrofit) veri alır veya kaydeder.

Bu mimari sayesinde:

  • View, UI ile doğrudan veri erişimi veya iş mantığı ile ilgilenmez, sadece ViewModel’e bağlıdır.
  • ViewModel, View’ı iş mantığı ayrıntılarından soyutlar.
  • UseCase, uygulamanın temel iş mantığını yönetmek için merkezi bir yer sağlar.
  • Repository, veri erişimini tek bir yerde yönetmeyi kolaylaştırır.

Bu mimari, uygulamanın bakımını, test edilmesini ve yeniden kullanılmasını kolaylaştırır.

Şimdi ilgili kodlarımı adım adım oluşturup anlatmaya çalışacağım.

[versions]

agp = "8.3.0"
kotlin = "1.9.21"
core-ktx = "1.12.0"
library = "4.0.0"
lifecycle-runtime-ktx = "2.7.0"
activity-compose = "1.8.2"
compose-compiler = "1.5.10"
ksp = "1.8.21-1.0.11"

#Compose
compose-bom = "2024.02.01"
androidx-compose-material3 = "1.2.0"
accompanist = "0.30.0"
compose-pagination = "1.0.0-beta01"

#More UI
androidx-paging = "3.2.0"
material3Icons = "1.9.0"
navigationFragment = "2.7.7"
timber = "5.0.1"

#Navigation
androidx-navigation = "2.7.7"
androidx-compose-hilt-navigation = "1.0.0"

#Coroutines
kotlinx-coroutines = "1.6.4"

#Serialization and more related
kotlinx-datetime = "0.4.0"
kotlinx-serializationJson = "1.5.0"
kotlinx-collectionsImmutable = "0.3.6"

#Image loading
coil = "2.4.0"

#Network
okhttp = "5.0.0-alpha.10"
retrofit = "2.9.0"
moshi = "1.15.0"

#Dependency injection
hilt = "2.49"
hilt-ext = "1.2.0"
hilt-compose-nav = "1.2.0"
#Database
room = "2.6.1"

#Android Chart
vico = "2.0.0-alpha.10"

#Tests
junit = "4.13.2"
androidx-test-ext-junit = "1.1.5"
espresso-core = "3.5.1"
appcompat = "1.6.1"
material = "1.11.0"
mockk = "1.12.0" # Mockk için son sürüm
assertj = "3.19.0"
playServicesMeasurementApi = "22.0.2" # AssertJ için son sürüm

[libraries]
#Core libraries
androidx-material3-icons = { module = "androidx.compose.material3:material3-icons", version.ref = "material3Icons" }
androidx-navigation-fragment = { module = "androidx.navigation:navigation-fragment", version.ref = "navigationFragment" }
library = { module = "com.github.chuckerteam.chucker:library", version.ref = "library" }
timber = { group = "com.jakewharton.timber", name = "timber", version.ref = "timber" }
core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "core-ktx" }
lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycle-runtime-ktx" }
activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activity-compose" }
compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "compose-bom" }
vico-compose-m3 = { group = "com.patrykandpatrick.vico", name = "compose-m3", version.ref = "vico" }
mockk = { module = "io.mockk:mockk", version = "1.13.7" }

assertj-core = { group = "org.assertj", name = "assertj-core", version.ref = "assertj" }



#Compose UI
compose-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
compose-runtime = { group = "androidx.compose.runtime", name = "runtime" }
compose-foundation = { group = "androidx.compose.foundation", name = "foundation" }
compose-material-iconsExtended = { group = "androidx.compose.material", name = "material-icons-extended" }
compose-animation-graphics = { group = "androidx.compose.animation", name = "animation-graphics" }
compose-ui = { group = "androidx.compose.ui", name = "ui" }
compose-material = { group = "androidx.compose.material", name = "material" }
compose-material3 = { group = "androidx.compose.material3", name = "material3", version.ref = "androidx-compose-material3" }
compose-material3-windowSizeClass = { group = "androidx.compose.material3", name = "material3-window-size-class", version.ref = "androidx-compose-material3" }

#Compose UI Navigation
compose-navigation = { group = "androidx.navigation", name = "navigation-compose", version.ref = "androidx-navigation" }

#Kotlin Immutable Collections
kotlinx-collections-immutable = { group = "org.jetbrains.kotlinx", name = "kotlinx-collections-immutable", version.ref = "kotlinx-collectionsImmutable" }

#Accompanist
accompanist-navigation-animation = { group = "com.google.accompanist", name = "accompanist-navigation-animation", version.ref = "accompanist" }
accompanist-permissions = { group = "com.google.accompanist", name = "accompanist-permissions", version.ref = "accompanist" }
accompanist-systemuicontroller = { group = "com.google.accompanist", name = "accompanist-systemuicontroller", version.ref = "accompanist" }
accompanist-webview = { group = "com.google.accompanist", name = "accompanist-webview", version.ref = "accompanist" }
accompanist-pager-layouts = { group = "com.google.accompanist", name = "accompanist-pager", version.ref = "accompanist" }
accompanist-pager-indicators = { group = "com.google.accompanist", name = "accompanist-pager-indicators", version.ref = "accompanist" }
accompanist-flowlayout = { group = "com.google.accompanist", name = "accompanist-flowlayout", version.ref = "accompanist" }

#Accompanist Material components
accompanist-material = { group = "com.google.accompanist", name = "accompanist-navigation-material", version.ref = "accompanist" }

#Image loading
compose-coil = { group = "io.coil-kt", name = "coil-compose", version.ref = "coil" }

#Network
moshi = { group = "com.squareup.moshi", name = "moshi", version.ref = "moshi" }
moshi-kotlin = { group = "com.squareup.moshi", name = "moshi-kotlin", version.ref = "moshi" }
okhttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhttp" }
okhttp-loggingInterceptor = { group = "com.squareup.okhttp3", name = "logging-interceptor", version.ref = "okhttp" }
retrofit = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit" }
retrofit-moshi = { group = "com.squareup.retrofit2", name = "converter-moshi", version.ref = "retrofit" }
retrofit-gson = { group = "com.squareup.retrofit2", name = "converter-gson", version.ref = "retrofit" }

#Dependency injection
compose-hilt-navigation = { group = "androidx.hilt", name = "hilt-navigation-compose", version.ref = "hilt-compose-nav" }
hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt" }
hilt-compiler = { group = "com.google.dagger", name = "hilt-android-compiler", version.ref = "hilt" }
hilt-ext-compiler = { group = "androidx.hilt", name = "hilt-compiler", version.ref = "hilt-ext" }

#Databases
room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room" }
room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "room" }
room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room" }

#Compose tooling and tests
compose-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
compose-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
compose-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
compose-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }

#Testing
junit = { group = "junit", name = "junit", version.ref = "junit" }
androidx-test-ext-junit = { group = "androidx.test.ext", name = "junit", version.ref = "androidx-test-ext-junit" }
espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espresso-core" }
appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
material = { group = "com.google.android.material", name = "material", version.ref = "material" }
play-services-measurement-api = { group = "com.google.android.gms", name = "play-services-measurement-api", version.ref = "playServicesMeasurementApi" }

[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" }
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
kotlin-gradlePlugin = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
kotlin-kapt = { id = "org.jetbrains.kotlin.kapt", version.ref = "kotlin" }
kotlin-ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
com-android-library = { id = "com.android.library", version.ref = "agp" }

[bundles]
core = ["core-ktx", "lifecycle-runtime-ktx", "activity-compose"]
accompanist = ["accompanist-navigation-animation", "accompanist-permissions", "accompanist-systemuicontroller", "accompanist-webview", "accompanist-pager-layouts", "accompanist-pager-indicators", "accompanist-flowlayout", "accompanist-material"]
compose = ["compose-graphics", "compose-runtime", "compose-foundation", "compose-material-iconsExtended", "compose-animation-graphics", "compose-ui", "compose-material", "compose-material3", "compose-material3-windowSizeClass"]
hilt = ["compose-hilt-navigation", "hilt-android", "hilt-compiler", "hilt-ext-compiler"]
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
id("com.google.dagger.hilt.android")
id("kotlin-kapt")
id("kotlin-parcelize")
id("com.google.gms.google-services")
id("com.google.firebase.crashlytics")

}

android {
namespace = "com.bahadireray.findbankapp"
compileSdk = 34

defaultConfig {
applicationId = "com.bahadireray.findbankapp"
minSdk = 24
targetSdk = 34
versionCode = 1
versionName = "1.0"

testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
useSupportLibrary = true
}
}

buildTypes {
debug {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}

release {
isMinifyEnabled = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = "17"
}
buildFeatures {
compose = true
}
composeOptions {
kotlinCompilerExtensionVersion = "1.5.1"
}
packaging {
resources {
excludes += "META-INF/LICENSE.md"
excludes += "META-INF/LICENSE-notice.md"
excludes += "META-INF/gradle/incremental.annotation.processors"
}
}
}

dependencies {
implementation(libs.compose.runtime)
implementation(libs.compose.ui)
implementation(libs.activity.compose)
implementation(libs.compose.material3)
implementation(libs.compose.graphics)
implementation(libs.compose.tooling)
implementation(libs.compose.navigation)
implementation(libs.compose.coil)
implementation(platform(libs.compose.bom))
implementation(libs.compose.tooling.preview)
implementation(libs.compose.test.manifest)
implementation(libs.compose.test.junit4)
implementation(libs.compose.hilt.navigation)
implementation(libs.accompanist.pager.layouts)
implementation(libs.accompanist.pager.indicators)
implementation(libs.moshi)
implementation(libs.moshi.kotlin)
implementation(libs.retrofit)
implementation(libs.retrofit.moshi)
implementation(libs.retrofit.gson)
implementation(libs.room.ktx)
implementation(libs.room.runtime)
implementation(libs.play.services.measurement.api)
kapt(libs.room.compiler)
implementation(libs.okhttp)
implementation(libs.okhttp.loggingInterceptor)
implementation(libs.core.ktx)
implementation(libs.timber)
implementation(libs.junit)
implementation(libs.espresso.core)
implementation(libs.hilt.android)
implementation(libs.hilt.compiler)
implementation(libs.vico.compose.m3)
implementation(libs.mockk)
implementation(libs.assertj.core)
implementation ("com.google.firebase:firebase-database-ktx")
implementation(platform("com.google.firebase:firebase-bom:32.5.0"))
implementation("com.google.firebase:firebase-crashlytics")
debugImplementation(libs.library)
}

Bu bölüm, projede kullanılan eklentileri tanımlar:

  • com.android.application: Android uygulaması oluşturmak için gerekli eklenti.
  • org.jetbrains.kotlin.android: Kotlin dil desteği ekler.
  • com.google.dagger.hilt.android: Hilt kullanarak bağımlılık enjeksiyonu ekler.
  • kotlin-kapt: Kotlin için annotation processing (örneğin, Room ve Hilt için).
  • kotlin-parcelize: Parcelable arayüzü kolayca uygulamak için Kotlin eklentisi.
  • com.google.gms.google-services: Firebase ve Google hizmetlerini kullanmak için gerekli eklenti.
  • com.google.firebase.crashlytics: Firebase Crashlytics'i kullanmak için gerekli eklenti.
@HiltAndroidApp
class FindBankApp : Application() {
override fun onCreate() {
super.onCreate()
Timber.plant(Timber.DebugTree())
}
}

Bu kod, FindBankApp sınıfını bir Application sınıfı olarak tanımlar ve Hilt'in bağımlılık enjeksiyonu için gerekli olan başlangıç işlemlerini yapar. Aynı zamanda Timber kütüphanesini kullanarak debug loglamayı etkinleştirir. Bu, uygulamanın başlangıcında gerekli yapılandırmaların yapılmasını sağlar ve geliştiriciye logları takip etme olanağı sunar.

open class BaseViewModel : ViewModel() {

private val _uiEvent = Channel<UiEvent>()
val uiEvent = _uiEvent.receiveAsFlow()

protected fun sendUiEvent(event: UiEvent) {
viewModelScope.launch {
_uiEvent.send(event)
}
}

protected fun handleException(exception: Throwable) {
sendUiEvent(UiEvent.ShowError(exception))
}
}

Bu kod, bir Android uygulamasında kullanılmak üzere genel bir ViewModel sınıfı olan BaseViewModel sınıfını tanımlar. Bu sınıf, UI (Kullanıcı Arayüzü) olaylarını yönetmek ve hataları ele almak için temel işlevler sağlar.

  • Channel<UiEvent>(): Kotlin Coroutines'den gelen bir kanal. Kanallar, farklı coroutine'ler arasında veri iletmek için kullanılır.
  • _uiEvent.receiveAsFlow(): Kanalı bir Flow'a dönüştürür. Flow, bir veri akışını temsil eder ve asenkron veri yayılımı için kullanılır. Burada, _uiEvent kanalı üzerinden gönderilen olaylar dinlenebilir hale gelir.
  • protected: Bu metodun yalnızca bu sınıf veya alt sınıfları tarafından erişilebilir olduğunu belirtir.
  • viewModelScope.launch: Yeni bir coroutine başlatır ve ViewModelin yaşam döngüsüne bağlıdır. ViewModel yok edildiğinde, bu coroutine otomatik olarak iptal edilir.
  • _uiEvent.send(event): Verilen UI olayını kanala gönderir.
  • sendUiEvent(UiEvent.ShowError(exception)): Hata durumunda, UiEvent.ShowError olayını gönderir. Bu olay, bir hata mesajı göstermek için UI tarafından yakalanabilir.
object Constants {
const val BASE_URL = "https://raw.githubusercontent.com/Bahadireray/ApiExample/main/"
const val API_SERVICE_TIMEOUT: Long = 60
}

@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {

@Provides
@Singleton
fun getGson(): Gson {
return GsonBuilder().serializeNulls().setLenient().create()
}

@Singleton
@Provides
fun provideHttpLoggingInterceptor() =
HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.BODY
}

@Singleton
@Provides
fun provideDynamicBaseUrlInterceptor(): DynamicBaseUrlInterceptor = DynamicBaseUrlInterceptor()

@Provides
@Singleton
fun provideOkHttpClient(
@ApplicationContext context: Context,
httpLoggingInterceptor: HttpLoggingInterceptor,
dynamicBaseUrlInterceptor: DynamicBaseUrlInterceptor
): OkHttpClient {
val chuckerInterceptor = ChuckerInterceptor(context)
return OkHttpClient.Builder()
.readTimeout(Constants.API_SERVICE_TIMEOUT, TimeUnit.SECONDS)
.connectTimeout(Constants.API_SERVICE_TIMEOUT, TimeUnit.SECONDS)
.addInterceptor(dynamicBaseUrlInterceptor)
.addInterceptor(httpLoggingInterceptor)
.addInterceptor(chuckerInterceptor)
.build()
}

@Singleton
@Provides
fun provideRetrofit(
okHttpClient: OkHttpClient,
gson: Gson
): Retrofit = Retrofit.Builder()
.baseUrl(Constants.BASE_URL)
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create(gson))
.build()

@Singleton
@Provides
fun provideApiService(retrofit: Retrofit): BankService {
return retrofit.create(BankService::class.java)
}
}

Bu kod, Android uygulamanızda ağ isteklerini yapılandırmak ve sağlamak için Hilt Dependency Injection (Bağımlılık Enjeksiyonu) kütüphanesini kullanarak bir NetworkModule tanımlar. Bu modül, Retrofit, OkHttp, Gson ve diğer ağ bileşenlerini sağlamak için kullanılır.

  • BASE_URL: API isteklerinin temel URL'si.
  • API_SERVICE_TIMEOUT: API servis istekleri için zaman aşımı süresi (saniye cinsinden).
  • @Module: Hilt'in bu sınıfı bir modül olarak tanımasını sağlar. Modüller, bağımlılık sağlayıcılarını (provider) içerir.
  • @InstallIn(SingletonComponent::class): Bu modülün uygulama yaşam döngüsü boyunca var olacağını belirtir (SingletonComponent).
    @Provides
@Singleton
fun getGson(): Gson {
return GsonBuilder().serializeNulls().setLenient().create()
}
  • @Provides: Hilt'e bu metodun bir bağımlılık sağlayıcı olduğunu belirtir.
  • @Singleton: Bu bağımlılığın uygulama boyunca tek bir örneği olacağını belirtir.
  • Gson: JSON verilerini serileştirmek ve serileştirilmiş JSON verilerini nesnelere dönüştürmek için kullanılan kütüphane.
    @Singleton
@Provides
fun provideHttpLoggingInterceptor() =
HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.BODY
}
  • HttpLoggingInterceptor: HTTP istek ve yanıtlarını loglamak için kullanılan bir interceptor.
  • Level.BODY: İstek ve yanıtların gövdesinin loglanmasını sağlar.
    @Singleton
@Provides
fun provideDynamicBaseUrlInterceptor(): DynamicBaseUrlInterceptor = DynamicBaseUrlInterceptor()
  • DynamicBaseUrlInterceptor: Dinamik olarak temel URL'yi değiştirmek için kullanılan özel bir interceptor.
    @Provides
@Singleton
fun provideOkHttpClient(
@ApplicationContext context: Context,
httpLoggingInterceptor: HttpLoggingInterceptor,
dynamicBaseUrlInterceptor: DynamicBaseUrlInterceptor
): OkHttpClient {
val chuckerInterceptor = ChuckerInterceptor(context)
return OkHttpClient.Builder()
.readTimeout(Constants.API_SERVICE_TIMEOUT, TimeUnit.SECONDS)
.connectTimeout(Constants.API_SERVICE_TIMEOUT, TimeUnit.SECONDS)
.addInterceptor(dynamicBaseUrlInterceptor)
.addInterceptor(httpLoggingInterceptor)
.addInterceptor(chuckerInterceptor)
.build()
}
  • OkHttpClient: HTTP isteklerini yöneten istemci.
  • ChuckerInterceptor: HTTP trafiğini izlemek ve hata ayıklamak için kullanılan bir interceptor.
  • readTimeout ve connectTimeout: Okuma ve bağlantı zaman aşım sürelerini ayarlar.
  • addInterceptor: Interceptor'ları ekler (DynamicBaseUrlInterceptor, HttpLoggingInterceptor, ChuckerInterceptor).
    @Singleton
@Provides
fun provideRetrofit(
okHttpClient: OkHttpClient,
gson: Gson
): Retrofit = Retrofit.Builder()
.baseUrl(Constants.BASE_URL)
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create(gson))
.build()
  • Retrofit: HTTP isteklerini gerçekleştirmek için kullanılan kütüphane.
  • baseUrl: Temel URL'yi ayarlar.
  • client: OkHttp istemcisini ekler.
  • addConverterFactory: JSON verilerini dönüştürmek için Gson dönüştürücüsünü ekler.
    @Singleton
@Provides
fun provideApiService(retrofit: Retrofit): BankService {
return retrofit.create(BankService::class.java)
}
  • BankService: API isteklerini tanımlayan bir arayüz.
  • retrofit.create(BankService::class.java): Retrofit kullanarak BankService arayüzünün bir örneğini oluşturur.

Bu NetworkModule, uygulamanızda ağ isteklerini yapılandırmak ve sağlamak için gerekli olan bileşenleri (Gson, OkHttpClient, Retrofit vb.) sağlar. Bu modül, Hilt kullanarak bağımlılık enjeksiyonunu kolaylaştırır ve ağ isteklerinin yönetimini basitleştirir.

Aşağıdaki kod, Android uygulamanızda Hilt kullanarak bağımlılıkları nasıl sağladığınızı ve yönettiğinizi gösteren üç farklı modül (DomainModule, BankModule, DatabaseModule) tanımlar. Her bir modül, farklı bileşenler ve sınıflar için bağımlılıkları sağlar. İşte bu modüllerin detaylı açıklaması:

@Module
@InstallIn(ViewModelComponent::class)
object DomainModule {

@ViewModelScoped
@Provides
fun provideGetBankListUseCase(
bankRepository: BankRepository,
bankDataRepository: BankDataRepository
): GetBankListUseCase {
return GetBankListUseCase(
bankRepository,
bankDataRepository
)
}
}
  • @Module: Hilt'e bu sınıfın bir modül olduğunu belirtir.
  • @InstallIn(ViewModelComponent::class): Bu modülün ViewModelComponent yaşam döngüsüne sahip olacağını belirtir. Bu, bağımlılıkların ViewModel yaşam döngüsü boyunca sağlanacağı anlamına gelir.
  • @ViewModelScoped: Bu bağımlılığın ViewModel yaşam döngüsü boyunca tek bir örneğe sahip olacağını belirtir.
  • provideGetBankListUseCase: GetBankListUseCase sınıfının bir örneğini sağlar. Bu kullanım durumu, bankaları listelemek için gerekli bağımlılıkları (bankRepository ve bankDataRepository) alır ve bir GetBankListUseCase örneği döndürür.
@Module
@InstallIn(SingletonComponent::class)
object BankModule {

@Provides
@Singleton
fun provideBankRepository(
service: BankService,
): BankRepository = BankRepositoryImpl(service)
}
  • @Module: Hilt'e bu sınıfın bir modül olduğunu belirtir.
  • @InstallIn(SingletonComponent::class): Bu modülün SingletonComponent yaşam döngüsüne sahip olacağını belirtir. Bu, bağımlılıkların uygulama yaşam döngüsü boyunca sağlanacağı anlamına gelir.
  • @Singleton: Bu bağımlılığın uygulama boyunca tek bir örneğe sahip olacağını belirtir.
  • provideBankRepository: BankRepository sınıfının bir örneğini sağlar. BankRepositoryImpl, BankService bağımlılığı ile oluşturulur ve bir BankRepository örneği döndürür.
@Module
@InstallIn(SingletonComponent::class)
object DatabaseModule {

@Provides
fun provideBankDao(bankDatabase: BankDatabase): BankInfoDao {
return bankDatabase.bankInfoDao()
}

@Provides
@Singleton
fun provideAppDatabase(@ApplicationContext appContext: Context): BankDatabase {
return Room.databaseBuilder(
appContext.applicationContext,
BankDatabase::class.java,
"bank_database"
).build()
}
}
  • @Module: Hilt'e bu sınıfın bir modül olduğunu belirtir.
  • @InstallIn(SingletonComponent::class): Bu modülün SingletonComponent yaşam döngüsüne sahip olacağını belirtir.
  • provideBankDao: BankInfoDao sınıfının bir örneğini sağlar. BankDatabase bağımlılığı ile bankInfoDao metodunu çağırarak bir BankInfoDao örneği döndürür.
  • provideAppDatabase: Room veritabanının bir örneğini sağlar. Room.databaseBuilder kullanarak veritabanı oluşturulur ve "bank_database" adı ile bir BankDatabase örneği döndürülür. @Singleton anotasyonu, bu veritabanı örneğinin uygulama boyunca tek bir örnek olmasını sağlar.
  • DomainModule: GetBankListUseCase kullanım durumunu sağlar ve ViewModel yaşam döngüsüne sahiptir.
  • BankModule: BankRepository sınıfını sağlar ve uygulama boyunca tek bir örneğe sahiptir.
  • DatabaseModule: Room veritabanı bileşenlerini sağlar ve uygulama boyunca tek bir örneğe sahiptir.

Bu modüller, Hilt Dependency Injection kullanarak bağımlılıkları yönetir ve uygulamanın çeşitli bileşenlerine enjekte edilmesini sağlar.

sealed class ResultState<out T> {

data class Success<out T>(val data: T) : ResultState<T>()
data class Error<out T>(val exception: Throwable) : ResultState<T>()
object Complete : ResultState<Nothing>()

override fun toString(): String {
return when (this) {
is Success<*> -> "Success[data=$data]"
is Error -> "Error[exception=$exception]"
Complete -> "Complete"
}
}
}

Bu kod, genelleştirilmiş bir sonuç durum sınıfı (ResultState) tanımlar. Bu sınıf, bir işlemin sonucunu temsil etmek için kullanılabilir ve işlemin başarılı olup olmadığını veya bir hata meydana gelip gelmediğini belirtir. ResultState sınıfı, üç farklı durum türü içerir:

  1. Success: İşlemin başarılı olduğunu ve sonuç verisini içerdiğini belirtir.
  2. Error: İşlemin başarısız olduğunu ve bir hata meydana geldiğini belirtir.
  3. Complete: İşlemin tamamlandığını, ancak herhangi bir veri veya hata içermediğini belirtir.

Şimdi proje içinde RoomDb kullanmak için gerekli sınıfları oluşturalım.

Bir Room veritabanı kullanarak bankaların bilgilerini saklama ve yönetme işlevselliği sağlar. Kod, aşağıdaki bileşenleri içerir: BankInfoEntity veri sınıfı, BankDatabase veritabanı sınıfı, BankDataRepository veri deposu sınıfı ve BankInfoDao DAO (Data Access Object) arabirimi.

@Parcelize
@Entity(tableName = "bank_info")
data class BankInfoEntity(
@PrimaryKey(autoGenerate = true)
@ColumnInfo(name = "id")
val id: Int = 0,
@ColumnInfo(name = "dc_SEHIR")
val city: String,
@ColumnInfo(name = "dc_ILCE")
val district: String,
.
.
.
) : Parcelable
  • Bu sınıf, bank_info tablosundaki her satırı temsil eder.
  • @Entity anotasyonu, bu sınıfın bir Room veritabanı tablosu olduğunu belirtir.
  • @PrimaryKey ve @ColumnInfo anotasyonları, bu sınıfın alanlarının tablodaki sütunlara nasıl eşleneceğini belirtir.
  • Parcelable arayüzü, bu sınıfın Android'de bir yerden bir yere taşınabilmesi için serileştirilebilir olduğunu belirtir.
@Database(entities = [BankInfoEntity::class], version = 1, exportSchema = false)
abstract class BankDatabase : RoomDatabase() {
abstract fun bankInfoDao(): BankInfoDao
}
  • Bu sınıf, Room veritabanını temsil eder.
  • @Database anotasyonu, veritabanının BankInfoEntity tablosunu içerdiğini ve sürümünün 1 olduğunu belirtir.
  • bankInfoDao yöntemi, DAO'ya erişim sağlar.
class BankDataRepository @Inject constructor(private val bankInfoDao: BankInfoDao) {
fun allList(): Flow<List<BankInfoEntity>> {
return bankInfoDao.readAllBankInfoData()
}

suspend fun addAll(bankInfo: List<BankInfoEntity>) {
bankInfoDao.addAllBankInfo(bankInfo)
}

suspend fun updateBankInfo(bankInfo: BankInfoEntity) {
bankInfoDao.updateBankInfo(bankInfo)
}

suspend fun deleteAllBankInfo() {
bankInfoDao.deleteAllBankInfo()
}
}
  • Bu sınıf, veritabanı işlemlerini gerçekleştirmek için DAO’yu kullanır.
  • @Inject anotasyonu, Hilt tarafından bu sınıfın bağımlılıklarının sağlanmasını sağlar.
  • allList yöntemi, tüm bankaların bilgilerini döndürür.
  • addAll, updateBankInfo, ve deleteAllBankInfo yöntemleri, veritabanında veri ekleme, güncelleme ve silme işlemlerini gerçekleştirir.
@Dao
interface BankInfoDao {
@Update
suspend fun updateBankInfo(bankInfo: BankInfoEntity)

@Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun addAllBankInfo(bankInfo: List<BankInfoEntity>)

@Query("SELECT * FROM bank_info")
fun readAllBankInfoData(): Flow<List<BankInfoEntity>>

@Query("DELETE FROM bank_info")
suspend fun deleteAllBankInfo()
}
  • Bu arayüz, veritabanı işlemlerini tanımlar.
  • @Dao anotasyonu, bu arayüzün bir DAO olduğunu belirtir.
  • @Update, @Insert, ve @Query anotasyonları, DAO yöntemlerinin nasıl çalışacağını belirtir.
  • readAllBankInfoData yöntemi, tüm bankaların bilgilerini içeren bir Flow döndürür.
  • addAllBankInfo ve deleteAllBankInfo yöntemleri, veri ekleme ve silme işlemlerini gerçekleştirir.

Bu kod yapısı, Android uygulamanızın veritabanı işlemlerini basit ve anlaşılır bir şekilde yönetmenize yardımcı olur. Veritabanı işlemlerini katmanlı bir şekilde ayrıştırarak, kodunuzu daha modüler ve test edilebilir hale getirir.

Aşağıda şimdi yapmış olduğumuz işlemleri repository ve useCase oluşturup viewModel tarafından adım adım işlenebilir duruma getireceğiz. Bir bankaların listesini API'den almak, işlemek ve veritabanına eklemek için kullanılır. Ayrıca, bu işlemleri ViewModel'da kullanmak için bir kullanım durumu (use case) tanımlar. Aşağıda adım adım açıklamalara yer veriyorum:

 interface BankService {
@GET("projectBankApi.json")
suspend fun getBankList(): List<BankInfo>
}
  • Açıklama: BankService, Retrofit ile HTTP isteklerini gerçekleştiren bir arayüzdür. @GET anotasyonu ile belirtilen URL'den veri çeker.
  • Amaç: API’den bankaların listesini almak için bir GET isteği yapar ve List<BankInfo> döner.
class BankRepositoryImpl @Inject constructor(
private val bankService: BankService
) : BankRepository {

override suspend fun getBankList(): Flow<List<BankInfo>> = flow {
val response = bankService.getBankList()
emit(response)
}.flowOn(Dispatchers.IO)
}
  • Açıklama: BankRepositoryImpl, BankRepository arayüzünü uygular ve BankService kullanarak API isteklerini gerçekleştirir.
  • Amaç: API’den alınan bankalar listesini bir Flow olarak döner ve bu işlem IO iş parçacığında gerçekleştirilir (flowOn(Dispatchers.IO)).
class GetBankListUseCase @Inject constructor(
private val bankRepository: BankRepository,
private val bankDataRepository: BankDataRepository
) {
suspend operator fun invoke(onResult: (ResultState<List<BankInfoEntity>>) -> Unit) {
bankRepository.getBankList()
.catch { exception ->
onResult(ResultState.Error(exception))
}
.collect { bankList ->
val newList = convertResponseToUI(bankList)
bankDataRepository.addAll(newList)
onResult(ResultState.Success(newList))
}
}

private fun convertResponseToUI(response: List<BankInfo>): List<BankInfoEntity> {
return response.map { item ->
with(item) {
BankInfoEntity(
id = ID,
city = dc_SEHIR,
district = dc_ILCE,
bankBranch = dc_BANKA_SUBE,
bankType = dc_BANKA_TIPI,
bankCode = dc_BANK_KODU,
addressName = dc_ADRES_ADI,
address = dc_ADRES,
postalCode = dc_POSTA_KODU,
onlineStatus = dc_ON_OFF_LINE,
onSiteStatus = dc_ON_OFF_SITE,
regionalCoordination = dc_BOLGE_KOORDINATORLUGU,
nearestAtm = dc_EN_YAKIM_ATM
)
}
}
}
}
  • Açıklama: GetBankListUseCase, BankRepository ve BankDataRepository kullanarak bankalar listesini API'den alır, işler ve veritabanına ekler.
  • Amaç: API’den alınan bankalar listesini alır, dönüştürür ve veritabanına ekler. İşlem sırasında hata olursa ResultState.Error ile döner, başarılı olursa ResultState.Success ile döner.
class BankDataRepository @Inject constructor(private val bankInfoDao: BankInfoDao) {

fun allList(): Flow<List<BankInfoEntity>> {
return bankInfoDao.readAllBankInfoData()
}

suspend fun addAll(bankInfo: List<BankInfoEntity>) {
bankInfoDao.addAllBankInfo(bankInfo)
}

suspend fun updateBankInfo(bankInfo: BankInfoEntity) {
bankInfoDao.updateBankInfo(bankInfo)
}

suspend fun deleteAllBankInfo() {
bankInfoDao.deleteAllBankInfo()
}
}
  • Açıklama: BankDataRepository, BankInfoDao'yu kullanarak veritabanı işlemlerini gerçekleştirir.
  • Amaç: Veritabanına bankalar listesini ekler, günceller, siler ve tüm bankalar listesini döner.
interface BankRepository {
suspend fun getBankList(): Flow<List<BankInfo>>
}
  • Açıklama: BankRepository arayüzü, API'den alınan bankalar listesini döner.
  • Amaç: BankRepositoryImpl tarafından uygulanacak olan yöntemleri tanımlar.
  1. API İsteği: BankService arayüzü ile API'den bankalar listesini al.
  2. Depo İşlemleri: BankRepositoryImpl sınıfı ile API'den alınan verileri Flow olarak döner.
  3. Kullanım Durumu: GetBankListUseCase sınıfı ile verileri al, dönüştür ve veritabanına ekle.
  4. Veritabanı İşlemleri: BankDataRepository sınıfı ile veritabanına ekle, güncelle, sil ve oku.

Bu yapı, temiz mimari (clean architecture) ilkelerine uygun olarak veri katmanını soyutlar ve bağımlılık enjeksiyonu ile modüler ve test edilebilir bir hale getirir.

@HiltViewModel
class BankListViewModel @Inject constructor(
private val getBankListUseCase: GetBankListUseCase,
private val bankDataRepository: BankDataRepository
) : BaseViewModel() {

private val _uiState = MutableStateFlow(BankListUiState())
val uiState: StateFlow<BankListUiState> = _uiState.asStateFlow()

init {
fetchBankList()
}

private fun fetchBankList() {
viewModelScope.launch(ExceptionHandler.handler) {
updatePageLoading(true)
getBankListUseCase { result ->
when (result) {
is ResultState.Success -> {
viewModelScope.launch {
val uniqueData = result.data.distinctBy { it.id }

val existingList = bankDataRepository.allList().firstOrNull()

val updatedData = uniqueData.map { newItem ->
val existingItem = existingList?.find { it.id == newItem.id }
newItem.copy(isFavorite = existingItem?.isFavorite ?: false)
}.distinctBy { it.id }
bankDataRepository.addAll(updatedData)
_uiState.update { currentState ->
currentState.copy(allList = updatedData, isLoading = false)
}
}
}
is ResultState.Error -> {
handleException(result.exception)
updatePageLoading(false)
}
ResultState.Complete -> TODO()
}
}
}
}

fun onEvent(event: BankListUiEvent) {
when (event) {
is BankListUiEvent.OnBankItemClicked -> {
onBankItemClick(event.item)
}
is BankListUiEvent.OnFavoriteClicked -> {
updateFavoriteBank(event.item)
}
}
}

private fun onBankItemClick(bankInfo: BankInfoEntity) {
viewModelScope.launch(ExceptionHandler.handler) {
sendUiEvent(
UiEvent.Navigate(
navigationType = NavigationType.Navigate(Route.BankDetailScreen),
data = mapOf(
"bankInfo" to bankInfo
)
)
)
}
}

private fun updateFavoriteBank(item: BankInfoEntity) {
viewModelScope.launch {
val updatedItem = withContext(Dispatchers.IO) {
val updated = item.copy(isFavorite = !item.isFavorite!!)
bankDataRepository.updateBankInfo(updated)
updated
}

_uiState.update { currentState ->
val updatedList = currentState.allList.map { if (it.bankBranch == updatedItem.bankBranch) updatedItem else it }
currentState.copy(allList = updatedList)
}
}
}

private fun updatePageLoading(isLoading: Boolean) {
viewModelScope.launch(ExceptionHandler.handler) {
_uiState.update { currentState ->
currentState.copy(isLoading = isLoading)
}
}
}
}
  • BankListViewModel, bankalar listesini almak ve UI durumunu güncellemek için kullanılan bir ViewModel'dir. GetBankListUseCase kullanarak bankalar listesini API'den alır ve BankDataRepository ile veritabanına ekler.
  • Başlangıçta: fetchBankList fonksiyonu çağrılır ve bankalar listesi alınır.
  • fetchBankList Fonksiyonu: API'den bankalar listesini alır, mevcut verilerle karşılaştırır, veritabanını günceller ve UI durumunu günceller.
  • onEvent Fonksiyonu: UI'dan gelen olayları işler (örneğin, bir banka öğesine tıklama veya favori durumunu güncelleme).
  • updateFavoriteBank Fonksiyonu: Bir bankanın favori durumunu günceller.
  • updatePageLoading Fonksiyonu: Sayfanın yüklenme durumunu günceller.
data class BankListUiState(
val isLoading: Boolean = false,
val allList: List<BankInfoEntity> = arrayListOf(),
val favoriteList: List<BankInfoEntity> = arrayListOf(),
)
  • UI durumunu temsil eden veri sınıfıdır. Yüklenme durumu (isLoading), tüm bankalar listesi (allList) ve favori bankalar listesi (favoriteList) içerir.
sealed class BankListUiEvent {
data class OnBankItemClicked(val item: BankInfoEntity) : BankListUiEvent()
data class OnFavoriteClicked(val item: BankInfoEntity) : BankListUiEvent()
}

UI’dan gelen olayları temsil eden mühendislik sınıfıdır. Banka öğesine tıklama ve favori durumu güncelleme gibi olayları içerir.

@Composable
fun BankListScreen(
uiState: BankListUiState,
uiEvent: Flow<UiEvent>,
onEvent: (BankListUiEvent) -> Unit,
onUiEvent: (UiEvent) -> Unit,
) {
var searchQuery by remember { mutableStateOf("") }

LaunchedEffect(key1 = true) {
uiEvent.collect { event ->
onUiEvent(event)
}
}

Box(
modifier = Modifier
.fillMaxSize()
.background(Color.Black),
contentAlignment = Alignment.Center
) {
if (uiState.isLoading) {
CircularProgressIndicator(color = Color.White)
}
Column(modifier = Modifier.padding(top = 32.dp)) {
Text(
modifier = Modifier.padding(horizontal = 12.dp),
text = "Favorite Banks",
color = Color.White,
fontWeight = FontWeight.Bold,
style = MaterialTheme.typography.bodyLarge
)
LazyRow(
modifier = Modifier
.padding(horizontal = 12.dp)
.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
items(uiState.allList.filter { it.isFavorite == true }) { item ->
FavoriteBankItem(
item = item,
onItemClicked = {
onEvent(
BankListUiEvent.OnBankItemClicked(
item = item
)
)
},
)
}
}

CustomOutlinedTextField(
value = searchQuery,
onValueChange = { query ->
searchQuery = query
},
placeholder = "Search by City",
modifier = Modifier
.fillMaxWidth()
.padding(top = 8.dp, bottom = 4.dp)
)

LazyColumn(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.spacedBy(4.dp),
horizontalAlignment = Alignment.Start
) {
item {
Spacer(modifier = Modifier.height(12.dp))
}
if (uiState.allList.isNotEmpty()) {
item {
Text(
modifier = Modifier.padding(horizontal = 12.dp),
text = "All Banks",
color = Color.White,
fontWeight = FontWeight.Bold,
style = MaterialTheme.typography.bodyLarge
)
Spacer(modifier = Modifier.height(10.dp))
}

items(uiState.allList.filter {
it.city.contains(searchQuery, ignoreCase = true)
}) { item ->
BankListItem(
item = item,
onItemClicked = {
AnalyticsHelper.logServiceReguest ("Bank İtem","${item.bankCode}")
onEvent(BankListUiEvent.OnBankItemClicked(item))
},
onFavoriteClick = {
AnalyticsHelper.logFavoriteItem ("Bank İtem","${item.bankCode}",true)
onEvent(BankListUiEvent.OnFavoriteClicked(item))
}
)
Divider(
modifier = Modifier
.fillMaxWidth()
.height(0.5.dp)
.background(color = Color.White)
)
}
}
}
}
}
}
  • BankListScreen, bankalar listesini görüntüleyen bir Compose ekranıdır.
  1. Yüklenme Durumu: CircularProgressIndicator gösterir, yüklenme durumunda ekranı göstermek için kullanılır.
  2. Favori Bankalar: LazyRow içinde favori bankalar listesi gösterilir.
  3. Arama: CustomOutlinedTextField kullanılarak şehir bazında arama yapılabilir.
  4. Tüm Bankalar: LazyColumn içinde bankalar listesi gösterilir, arama sorgusuna göre filtrelenir.
  5. Olay Yönetimi: onEvent ve onUiEvent fonksiyonları ile kullanıcı etkileşimleri işlenir.

Bu yapılar, uygulamanızın kullanıcı arayüzünü ve veritabanı işlemlerini yönetmenizi sağlar. ViewModel, UI durumunu ve kullanıcı etkileşimlerini yönetirken, Composable fonksiyonu ekranı oluşturur ve günceller.

Şimdi detay için oluşturduğumuz screen’ı anlatacağım.

@Composable
fun BankDetailScreen(
uiEvent: Flow<UiEvent>,
onEvent: (BankDetailUiEvent) -> Unit,
viewModel: BankDetailViewModel,
bankInfo: BankInfoEntity?,
onUiEvent: (UiEvent) -> Unit,
) {

LaunchedEffect(key1 = true) {
uiEvent.collect { event ->
onUiEvent(event)
}
}

Column(modifier = Modifier.background(Color.Black)){
Icon(
imageVector = Icons.Default.ArrowBack,
contentDescription = "Cancel Icon",
tint = Color.White,
modifier = Modifier
.padding(top = 64.dp, start = 32.dp)
.clickable {
onEvent(BankDetailUiEvent.OnBackClicked)
}
)

Column(
modifier = Modifier
.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {

Card(
modifier = Modifier.padding(16.dp),
backgroundColor = Color.DarkGray,
elevation = 8.dp
) {

Column(
modifier = Modifier
.padding(16.dp)
.fillMaxWidth(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
bankInfo?.let { info ->
BankDetailText(
text = info.bankBranch,
fontSize = 24.sp
)
BankDetailText(
text = info.address,
fontSize = 18.sp
)

BankDetailText(
text = info.bankType,
fontSize = 16.sp
)

Button(
onClick = {
viewModel.onEvent(BankDetailUiEvent.OnLocationClicked(info))
},
modifier = Modifier.fillMaxWidth()
) {
Text(text = "Banka Konumuna Git")
}
}
}
}
}
}
}
  • BankDetailScreen fonksiyonu, bankanın detaylarını gösteren bir Compose ekranını tanımlar.
  • UI Event Koleksiyonu: LaunchedEffect kullanılarak uiEvent akışı dinlenir ve her bir UiEvent işlenir.
  • Back Butonu: Ekranın sol üst köşesinde, geri dönmek için kullanılan bir ok simgesi (ArrowBack) bulunur. Bu simgeye tıklanıldığında, BankDetailUiEvent.OnBackClicked olayı tetiklenir.
  • Bankanın Detayları: Eğer bankInfo verisi mevcutsa, bir Card içinde bankanın detayları (bankBranch, address, bankType) gösterilir.
  • Konum Butonu: “Banka Konumuna Git” butonuna tıklanıldığında, BankDetailUiEvent.OnLocationClicked olayı tetiklenir. Bu olay, bankanın konumunu harita uygulamasında açmak için ViewModel'e gönderilir.
sealed class BankDetailUiEvent {
data class OnLocationClicked(val bankInfo: BankInfoEntity) : BankDetailUiEvent()
data object OnBackClicked : BankDetailUiEvent()
}
  • BankDetailUiEvent sınıfı, BankDetailScreen içinde oluşabilecek olayları temsil eder.
  • OnLocationClicked: Kullanıcı bir banka konumuna gitmek istediğinde tetiklenir ve BankInfoEntity nesnesi içerir.
  • OnBackClicked: Kullanıcı geri gitmek istediğinde tetiklenir, herhangi bir ek veri içermez.
data class BankDetailUiState(
val isLoading: Boolean = false,
val bankInfo: BankInfoEntity? = null
)
  • UI durumunu temsil eden veri sınıfıdır.
  • isLoading: Verinin yüklenip yüklenmediğini belirtir.
  • bankInfo: Detayları gösterilecek bankanın bilgilerini içerir.
@HiltViewModel
class BankDetailViewModel @Inject constructor(
application: Application
) : AndroidViewModel(application) {

private val _uiState = MutableStateFlow(BankDetailUiState())
val uiState: StateFlow<BankDetailUiState> = _uiState.asStateFlow()

fun onEvent(event: BankDetailUiEvent) {
when (event) {
is BankDetailUiEvent.OnLocationClicked -> {
openMapForBankLocation(event.bankInfo)
}
BankDetailUiEvent.OnBackClicked -> {
onBackClick()
}
}
}

private fun openMapForBankLocation(bankInfo: BankInfoEntity) {
val gmmIntentUri = Uri.parse("geo:0,0?q=${bankInfo.address}")
val mapIntent = Intent(Intent.ACTION_VIEW, gmmIntentUri)
mapIntent.setPackage("com.google.android.apps.maps")
mapIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
getApplication<Application>().startActivity(mapIntent)
}

private val _uiEvent = Channel<UiEvent>()
val uiEvent = _uiEvent.receiveAsFlow()

private fun sendUiEvent(event: UiEvent) {
viewModelScope.launch {
_uiEvent.send(event)
}
}

private fun onBackClick() {
sendNavigationEvent(NavigationType.PopBack)
}

private fun sendNavigationEvent(navigationType: NavigationType, data: Map<String, Any?>? = emptyMap()) {
viewModelScope.launch {
sendUiEvent(UiEvent.Navigate(navigationType, data))
}
}
}
  • BankDetailViewModel, bankanın detaylarını yöneten ViewModel'dir.
  1. onEvent Fonksiyonu: Gelen olaylara göre işlemleri başlatır (örneğin, haritada banka konumunu açma).
  2. openMapForBankLocation Fonksiyonu: Banka adresini Google Haritalar uygulamasında açar.
  3. UI Event Gönderimi: UI olaylarını Channel aracılığıyla UI bileşenlerine gönderir.
  4. onBackClick Fonksiyonu: Geri dönüş navigasyonunu tetikler (PopBack).

Bu yapılar, bankanın detaylarını gösteren bir ekranı ve bu ekranın etkileşimlerini yönetmek için kullanılır. BankDetailViewModel, UI'dan gelen olayları işler ve gerekli işlemleri yapar. BankDetailScreen ise bu verileri gösterir ve kullanıcı etkileşimlerini ViewModel'e iletir.

Şimdi bütün bu kurduğumuz yapıyı MainActivity çağırı ve gerekli route yönlendirme ayarlarını yapmamız lazım :)

@OptIn(ExperimentalTextApi::class)
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
private lateinit var firebaseAnalytics: FirebaseAnalytics

@OptIn(ExperimentalComposeUiApi::class, ExperimentalMaterialApi::class)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
firebaseAnalytics = Firebase.analytics
AnalyticsHelper.initialize(firebaseAnalytics)
FirebaseCrashlytics.getInstance().setCrashlyticsCollectionEnabled(true)
FindBankAppTheme {
val navController = rememberNavController()
Navigation(
navController = navController,
)
}
}
}
}
  • @OptIn(ExperimentalTextApi::class): Bu anotasyon, Compose'da kullanılan deneysel API'lerin kullanılmasını belirten bir anotasyondur. ExperimentalTextApi gibi deneysel özelliklerin kullanıldığını belirtir.
  • @AndroidEntryPoint: Dagger Hilt'in Android uygulamalarında bağımlılık enjeksiyonu yapmak için kullanılan anotasyondur. Bu anotasyon, MainActivity sınıfının Hilt tarafından yönetilmesini sağlar.
  • @OptIn(ExperimentalComposeUiApi::class, ExperimentalMaterialApi::class): ExperimentalComposeUiApi ve ExperimentalMaterialApi gibi deneysel Compose API'lerinin kullanıldığını belirten anotasyonlar. Bu anotasyonlar, belirli Compose özelliklerinin deneysel olduğunu ve bu özelliklerin stabil sürümlerinin kullanılmayabileceğini belirtir.
  • super.onCreate(savedInstanceState): ComponentActivity sınıfının onCreate metodunu çağırarak, Activity'nin yaşam döngüsünü başlatır.
  • enableEdgeToEdge(): Bu yöntem muhtemelen ekran kenarlarını kullanarak uygulamanın tam ekran görünümünü etkinleştiren bir yardımcı fonksiyon. Bu fonksiyonun detayları kodda verilmemiş ama genellikle kenar boşluklarını kaldırarak uygulamanın ekranın tamamını kullanmasını sağlar.
  • setContent: Jetpack Compose ile UI'yi tanımlar ve oluşturur. setContent içinde, uygulamanızın UI bileşenlerini tanımlarsınız.
  • firebaseAnalytics = Firebase.analytics: Firebase Analytics örneğini alır ve firebaseAnalytics değişkenine atar.
  • AnalyticsHelper.initialize(firebaseAnalytics): AnalyticsHelper sınıfını başlatır ve Firebase Analytics örneğini ona iletir. Bu, genellikle uygulama analitiğini yapılandırmak için kullanılır.
  • FirebaseCrashlytics.getInstance().setCrashlyticsCollectionEnabled (true): Firebase Crashlytics'i etkinleştirir, yani uygulama çökme raporlarını toplamak için Crashlytics'i kullanır.
  • FindBankAppTheme: Uygulamanın teması olarak FindBankAppTheme'i kullanır. Bu, Compose'da uygulamanızın görsel stilini ve tema ayarlarını belirler.
  • val navController = rememberNavController(): Navigation component'ını kullanarak bir NavController oluşturur. Bu, ekranlar arasında navigasyonu yönetir.
  • Navigation(navController = navController): Navigation adlı bir Composable fonksiyonunu çağırır ve navController'ı ona iletir. Bu, uygulamanızın navigasyon yapısını tanımlar ve ekranlar arasında geçiş yapar.
@ExperimentalTextApi
@ExperimentalComposeUiApi
@ExperimentalMaterialApi
@Composable
fun Navigation(
navController: NavHostController,
) {
NavHost(
navController = navController,
startDestination = Route.BankScreen.route
) {
composable(route = Route.BankScreen.route) {
val viewModel = hiltViewModel<BankListViewModel>()
val uiState by viewModel.uiState.collectAsState()
BankListScreen(
uiState = uiState,
onEvent = viewModel::onEvent,
uiEvent = viewModel.uiEvent,
onUiEvent = {
it.handleUiEvent(navController)
}
)
}

composable(route = Route.BankDetailScreen.route) {
val viewModel = hiltViewModel<BankDetailViewModel>()
val bankInfo = navController.currentBackStackEntry?.savedStateHandle?.get<BankInfoEntity>("bankInfo")
BankDetailScreen(
viewModel =viewModel,
bankInfo = bankInfo,
onEvent = viewModel::onEvent,
uiEvent = viewModel.uiEvent,
onUiEvent = {
it.handleUiEvent(navController)
}
)
}
}
}

Bu kod, Jetpack Compose kullanarak uygulamanızın navigasyon yapısını tanımlayan bir @Composable fonksiyon olan Navigation'ı içeriyor. Kod, NavHost ve composable kullanarak farklı ekranlar arasında geçişi yönetir. İşte kodun detaylı açıklaması:

  • @ExperimentalTextApi: Bu anotasyon, Jetpack Compose'da ExperimentalTextApi olarak işaretlenmiş deneysel API'lerin kullanılmasını belirtir.
  • @ExperimentalComposeUiApi: Bu anotasyon, ExperimentalComposeUiApi olarak işaretlenmiş deneysel Compose UI API'lerinin kullanılmasını belirtir.
  • @ExperimentalMaterialApi: Bu anotasyon, ExperimentalMaterialApi olarak işaretlenmiş deneysel Material API'lerinin kullanılmasını belirtir.
  • @Composable: Jetpack Compose'da UI bileşenlerini tanımlamak için kullanılan anotasyon. Bu, Navigation fonksiyonunun bir Compose bileşeni olduğunu belirtir.
  • navController: NavHostController: NavHostController, navigasyon işlemlerini yöneten bir kontrolördür. Ekranlar arasında geçiş yapmak için kullanılır.
  • NavHost: Navigasyon yapısını tanımlar. NavHost, ekranlar arasında geçiş yapmanızı sağlar.
  • navController: Navigasyon işlemlerini kontrol eden bir NavHostController nesnesi.
  • startDestination: Uygulama başlatıldığında hangi ekranın ilk olarak gösterileceğini belirtir. Burada, Route.BankScreen.route ile BankScreen ekranı başlangıç noktası olarak belirlenmiştir.
  • composable(route = Route.BankScreen.route): Bu blok, BankScreen rotasına sahip bir ekran tanımlar. NavHost'a bu rotayı ekler.
  • val viewModel = hiltViewModel<BankListViewModel>(): Hilt kullanarak BankListViewModel'i alır. Bu, bu ekranın ViewModel'ini sağlayan bir Compose fonksiyonudur.
  • val uiState by viewModel.uiState.collectAsState(): ViewModel'in UI durumunu collectAsState() ile alır ve Compose bileşenlerinde reaktif olarak kullanır.
  • BankListScreen: BankListScreen Composable fonksiyonunu çağırır. Bu fonksiyon, uiState, onEvent, uiEvent, ve onUiEvent parametrelerini alır ve ekranı oluşturur.
  • onUiEvent = { it.handleUiEvent(navController) }: UiEvent'leri alır ve handleUiEvent metodunu kullanarak uygun navigasyon işlemini gerçekleştirir.

Bu yapı, uygulamanızın navigasyon mantığını ve ekran yönetimini düzenli ve yönetilebilir bir şekilde yapmanızı sağlar.

Biraz fazla uzun bir yazı oldu. Çünkü fazlasıyla büyük bir yapı kurup anlatmak istedim. Aşağıda Github kodlarını bulabiliriz. Projeyi indirip daha detaylı incelebiliriniz.

https://github.com/Bahadireray/FindBankAppCompose

Daha güzel günlerde görüşmek üzere :)

--

--