Finally set up proper dependency injection

This commit is contained in:
Z. Charles Dziura 2024-10-26 22:32:08 -04:00
parent 5b8179ade1
commit f9b3e35939
32 changed files with 203 additions and 174 deletions

View file

@ -6,6 +6,8 @@ plugins {
alias(libs.plugins.compose.compiler) alias(libs.plugins.compose.compiler)
alias(libs.plugins.serialization) alias(libs.plugins.serialization)
alias(libs.plugins.protobuf) alias(libs.plugins.protobuf)
id("kotlin-kapt")
alias(libs.plugins.hilt)
} }
protobuf { protobuf {
@ -76,6 +78,10 @@ android {
} }
} }
kapt {
correctErrorTypes = true
}
dependencies { dependencies {
implementation(libs.androidx.activity.compose) implementation(libs.androidx.activity.compose)
implementation(libs.androidx.appcompat) implementation(libs.androidx.appcompat)
@ -100,6 +106,9 @@ dependencies {
implementation(libs.okhttp.logging.interceptor) implementation(libs.okhttp.logging.interceptor)
implementation(libs.protobuf.javalite) implementation(libs.protobuf.javalite)
implementation(libs.protobuf.kotlinlite) implementation(libs.protobuf.kotlinlite)
implementation(libs.hilt)
kapt(libs.hilt.kapt)
implementation(libs.hilt.compose)
testImplementation(libs.junit) testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.junit)

View file

@ -5,6 +5,7 @@
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<application <application
android:name=".app.android.DebtPirateApplication"
android:allowBackup="true" android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules" android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules" android:fullBackupContent="@xml/backup_rules"
@ -15,9 +16,8 @@
android:theme="@style/Theme.DebtPirate" android:theme="@style/Theme.DebtPirate"
android:networkSecurityConfig="@xml/network_security_config"> android:networkSecurityConfig="@xml/network_security_config">
<activity <activity
android:name=".MainActivity" android:name=".app.host.MainActivity"
android:exported="true" android:exported="true"
android:label="@string/title_activity_main"
android:theme="@style/Theme.DebtPirate"> android:theme="@style/Theme.DebtPirate">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />

View file

@ -1,30 +0,0 @@
package ing.bikeshedengineer.debtpirate
import android.content.Context
import androidx.datastore.core.CorruptionException
import androidx.datastore.core.DataStore
import androidx.datastore.core.Serializer
import androidx.datastore.dataStore
import com.google.protobuf.InvalidProtocolBufferException
import java.io.InputStream
import java.io.OutputStream
object StoreSerializer : Serializer<Store> {
override val defaultValue: Store
get() = Store.getDefaultInstance()
override suspend fun readFrom(input: InputStream): Store {
try {
return Store.parseFrom(input)
} catch (exception: InvalidProtocolBufferException) {
throw CorruptionException("Cannot read proto file.", exception)
}
}
override suspend fun writeTo(t: Store, output: OutputStream) = t.writeTo(output)
}
val Context.dataStore: DataStore<Store> by dataStore(
fileName = "store.proto",
serializer = StoreSerializer
)

View file

@ -0,0 +1,8 @@
package ing.bikeshedengineer.debtpirate.app.android
import android.app.Application
import dagger.hilt.android.HiltAndroidApp
@HiltAndroidApp
class DebtPirateApplication : Application() {
}

View file

@ -1,35 +1,33 @@
package ing.bikeshedengineer.debtpirate package ing.bikeshedengineer.debtpirate.app.host
import android.os.Bundle import android.os.Bundle
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.compose.NavHost import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController import androidx.navigation.compose.rememberNavController
import ing.bikeshedengineer.debtpirate.screens.auth.AuthScreen import dagger.hilt.android.AndroidEntryPoint
import ing.bikeshedengineer.debtpirate.screens.auth.AuthScreenViewModel import ing.bikeshedengineer.debtpirate.presentation.ui.auth.AuthScreen
import ing.bikeshedengineer.debtpirate.ui.theme.DebtPirateTheme import ing.bikeshedengineer.debtpirate.presentation.theme.DebtPirateTheme
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@Serializable @Serializable
object AuthRoute object AuthRoute
@AndroidEntryPoint
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
enableEdgeToEdge() enableEdgeToEdge()
setContent { setContent {
val navController = rememberNavController() val navController = rememberNavController()
DebtPirateTheme { DebtPirateTheme {
NavHost(navController = navController, startDestination = AuthRoute) { NavHost(navController = navController, startDestination = AuthRoute) {
composable<AuthRoute> { composable<AuthRoute> {
AuthScreen( AuthScreen( )
viewModel = viewModel<AuthScreenViewModel>()
)
} }
} }
} }

View file

@ -0,0 +1,31 @@
package ing.bikeshedengineer.debtpirate.data.pref
import android.content.Context
import androidx.datastore.core.CorruptionException
import androidx.datastore.core.DataStore
import androidx.datastore.core.Serializer
import androidx.datastore.dataStore
import com.google.protobuf.InvalidProtocolBufferException
import ing.bikeshedengineer.debtpirate.PrefsDataStore
import java.io.InputStream
import java.io.OutputStream
object PrefsDataStoreSerializer : Serializer<PrefsDataStore> {
override val defaultValue: PrefsDataStore
get() = PrefsDataStore.getDefaultInstance()
override suspend fun readFrom(input: InputStream): PrefsDataStore {
try {
return PrefsDataStore.parseFrom(input)
} catch (exception: InvalidProtocolBufferException) {
throw CorruptionException("Cannot read proto file.", exception)
}
}
override suspend fun writeTo(t: PrefsDataStore, output: OutputStream) = t.writeTo(output)
}
val Context.dataStore: DataStore<PrefsDataStore> by dataStore(
fileName = "prefs_data_store.proto",
serializer = PrefsDataStoreSerializer
)

View file

@ -0,0 +1,4 @@
package ing.bikeshedengineer.debtpirate.data.remote.client
class AuthEndpointClient {
}

View file

@ -0,0 +1,13 @@
package ing.bikeshedengineer.debtpirate.data.remote.endpoint
import ing.bikeshedengineer.debtpirate.data.remote.model.ApiResponse
import ing.bikeshedengineer.debtpirate.data.remote.model.auth.AuthLoginRequest
import ing.bikeshedengineer.debtpirate.data.remote.model.auth.AuthLoginResponse
import retrofit2.http.POST
import retrofit2.Response
import retrofit2.http.Body
interface AuthEndpoint {
@POST("auth/login")
suspend fun submitLoginRequest(@Body credentials: AuthLoginRequest): Response<ApiResponse<AuthLoginResponse>>
}

View file

@ -0,0 +1,3 @@
package ing.bikeshedengineer.debtpirate.data.remote.model
data class ApiResponse<T>(val data: T?, val err: Any?)

View file

@ -1,3 +1,3 @@
package ing.bikeshedengineer.debtpirate.repositories.auth.models package ing.bikeshedengineer.debtpirate.data.remote.model.auth
data class AuthLoginRequest(val username: String, val password: String) data class AuthLoginRequest(val username: String, val password: String)

View file

@ -0,0 +1,5 @@
package ing.bikeshedengineer.debtpirate.data.remote.model.auth
data class AuthLoginResponse(val userId: Int, val session: AuthLoginTokenData, val auth: AuthLoginTokenData)
data class AuthLoginTokenData(val token: String, val expiresAt: String)

View file

@ -0,0 +1,9 @@
package ing.bikeshedengineer.debtpirate.data.remote.repository
import ing.bikeshedengineer.debtpirate.data.remote.model.ApiResponse
import ing.bikeshedengineer.debtpirate.data.remote.model.auth.AuthLoginRequest
import ing.bikeshedengineer.debtpirate.data.remote.model.auth.AuthLoginResponse
interface AuthRepository {
suspend fun submitLoginRequest(credentials: AuthLoginRequest): Result<AuthLoginResponse>
}

View file

@ -0,0 +1,40 @@
package ing.bikeshedengineer.debtpirate.di.modules
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import ing.bikeshedengineer.debtpirate.BuildConfig
import ing.bikeshedengineer.debtpirate.data.remote.repository.AuthRepository
import ing.bikeshedengineer.debtpirate.domain.repository.AuthRepositoryImpl
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
object AuthRepositoryModule {
@Provides
@Singleton
fun provideHttpClient(): Retrofit {
return Retrofit.Builder()
.baseUrl(BuildConfig.API_BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.client(OkHttpClient.Builder()
.addInterceptor(HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.BODY
})
.build()
)
.build()
}
@Provides
@Singleton
fun provideAuthRepository(httpClient: Retrofit): AuthRepository {
return AuthRepositoryImpl(httpClient)
}
}

View file

@ -0,0 +1,8 @@
package ing.bikeshedengineer.debtpirate.domain.model
import java.time.ZonedDateTime
import java.time.format.DateTimeFormatter.ISO_INSTANT
data class Token(val token: String, val expiresAt: ZonedDateTime) {
constructor(token: String, expiresAtStr: String): this(token, ZonedDateTime.parse(expiresAtStr, ISO_INSTANT))
}

View file

@ -0,0 +1,29 @@
package ing.bikeshedengineer.debtpirate.domain.repository
import ing.bikeshedengineer.debtpirate.data.remote.endpoint.AuthEndpoint
import ing.bikeshedengineer.debtpirate.data.remote.model.ApiResponse
import ing.bikeshedengineer.debtpirate.data.remote.model.auth.AuthLoginRequest
import ing.bikeshedengineer.debtpirate.data.remote.model.auth.AuthLoginResponse
import ing.bikeshedengineer.debtpirate.data.remote.repository.AuthRepository
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import retrofit2.Retrofit
import kotlin.Result
class AuthRepositoryImpl constructor(private val httpClient: Retrofit): AuthRepository {
private val authEndpoint: AuthEndpoint = this.httpClient.create(AuthEndpoint::class.java)
override suspend fun submitLoginRequest(credentials: AuthLoginRequest): Result<AuthLoginResponse> {
return withContext(Dispatchers.IO) {
val response = authEndpoint.submitLoginRequest(credentials)
if (response.isSuccessful) {
val body = response.body()
return@withContext Result.success(body?.data!!)
} else {
val error = response.errorBody()
return@withContext Result.failure(Throwable(error.toString()))
}
}
}
}

View file

@ -1,3 +0,0 @@
package ing.bikeshedengineer.debtpirate.http
data class ApiResponse<T>(val data: T, val err: Any)

View file

@ -1,19 +0,0 @@
package ing.bikeshedengineer.debtpirate.http
import ing.bikeshedengineer.debtpirate.BuildConfig
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
private val okHttpClient = OkHttpClient.Builder()
.addInterceptor(HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.BODY
})
.build()
val httpClientService = Retrofit.Builder()
.baseUrl(BuildConfig.API_BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.client(okHttpClient)
.build()

View file

@ -1,4 +1,4 @@
package ing.bikeshedengineer.debtpirate.ui.theme package ing.bikeshedengineer.debtpirate.presentation.theme
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color

View file

@ -1,4 +1,4 @@
package ing.bikeshedengineer.debtpirate.ui.theme package ing.bikeshedengineer.debtpirate.presentation.theme
import android.os.Build import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.isSystemInDarkTheme

View file

@ -1,4 +1,4 @@
package ing.bikeshedengineer.debtpirate.ui.theme package ing.bikeshedengineer.debtpirate.presentation.theme
import androidx.compose.material3.Typography import androidx.compose.material3.Typography
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle

View file

@ -1,4 +1,4 @@
package ing.bikeshedengineer.debtpirate.screens.auth package ing.bikeshedengineer.debtpirate.presentation.ui.auth
import android.annotation.SuppressLint import android.annotation.SuppressLint
import androidx.compose.foundation.background import androidx.compose.foundation.background
@ -35,14 +35,14 @@ import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter") @SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
@Composable @Composable
fun AuthScreen( fun AuthScreen(
viewModel: AuthScreenViewModel viewModel: AuthScreenViewModel = hiltViewModel<AuthScreenViewModel>()
) { ) {
Scaffold( Scaffold(
modifier = Modifier.fillMaxSize() modifier = Modifier.fillMaxSize()

View file

@ -1,21 +1,25 @@
package ing.bikeshedengineer.debtpirate.screens.auth package ing.bikeshedengineer.debtpirate.presentation.ui.auth
import android.util.Log import android.util.Log
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import ing.bikeshedengineer.debtpirate.repositories.auth.models.AuthLoginRequest import dagger.hilt.android.lifecycle.HiltViewModel
import ing.bikeshedengineer.debtpirate.screens.auth.usecases.LoginUsecase import ing.bikeshedengineer.debtpirate.data.remote.model.auth.AuthLoginRequest
import ing.bikeshedengineer.debtpirate.data.remote.repository.AuthRepository
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import javax.inject.Inject
class AuthScreenViewModel(private val loginRequest: LoginUsecase = LoginUsecase()) : @HiltViewModel
ViewModel() { class AuthScreenViewModel @Inject constructor(private val authRepository: AuthRepository) : ViewModel() {
private val tag: String private val tag: String
get() { get() {
return javaClass.simpleName return javaClass.simpleName
} }
// private val storeLoginData = StoreLoginDataUseCase(dataStore)
private val _username = MutableStateFlow("") private val _username = MutableStateFlow("")
val username = _username.asStateFlow() val username = _username.asStateFlow()
@ -33,15 +37,12 @@ class AuthScreenViewModel(private val loginRequest: LoginUsecase = LoginUsecase(
fun submitLoginRequest() { fun submitLoginRequest() {
val credentials = AuthLoginRequest(this._username.value, this._password.value) val credentials = AuthLoginRequest(this._username.value, this._password.value)
viewModelScope.launch { viewModelScope.launch {
val response = loginRequest(credentials) try {
when (response.isSuccessful) { val response = authRepository.submitLoginRequest(credentials)
true -> { Log.i(tag, "Login successful! $response")
Log.i(tag, "Login successful! ${response.body()}") } catch (err: Throwable) {
} // TODO...
Log.e(tag, err.message!!)
false -> {
Log.e(tag, "Login unsuccessful! ${response.body()}")
}
} }
} }
} }

View file

@ -1,21 +0,0 @@
package ing.bikeshedengineer.debtpirate.repositories.auth
import ing.bikeshedengineer.debtpirate.http.httpClientService
import ing.bikeshedengineer.debtpirate.repositories.auth.models.AuthLoginRequest
import ing.bikeshedengineer.debtpirate.repositories.auth.models.AuthLoginResponse
import retrofit2.Response
import retrofit2.http.Body
import retrofit2.http.POST
class AuthRemoteDataSource(
private val authApi: AuthApi = httpClientService.create(AuthApi::class.java)
) {
suspend fun submitLoginRequest(credentials: AuthLoginRequest) =
authApi.submitLoginRequest(credentials)
}
interface AuthApi {
@POST("auth/login")
suspend fun submitLoginRequest(@Body credentials: AuthLoginRequest): Response<AuthLoginResponse>
}

View file

@ -1,14 +0,0 @@
package ing.bikeshedengineer.debtpirate.repositories.auth
import ing.bikeshedengineer.debtpirate.repositories.auth.models.AuthLoginRequest
import ing.bikeshedengineer.debtpirate.repositories.auth.models.AuthLoginResponse
import retrofit2.Response
class AuthRepository(
private val remoteDataSource: AuthRemoteDataSource = AuthRemoteDataSource()
) {
suspend fun submitLoginRequest(credentials: AuthLoginRequest): Response<AuthLoginResponse> {
return remoteDataSource.submitLoginRequest(credentials)
}
}

View file

@ -1,13 +0,0 @@
package ing.bikeshedengineer.debtpirate.repositories.auth.models
import ing.bikeshedengineer.debtpirate.http.ApiResponse
typealias AuthLoginResponse = ApiResponse<AuthLoginResponseData>
data class AuthLoginResponseData(
val userId: Number,
val session: AuthLoginResponseTokenData,
val auth: AuthLoginResponseTokenData
)
data class AuthLoginResponseTokenData(val token: String, val expiresAt: String)

View file

@ -1,3 +0,0 @@
package ing.bikeshedengineer.debtpirate.repositories.users
class UsersRemoteDataSource

View file

@ -1,10 +0,0 @@
package ing.bikeshedengineer.debtpirate.repositories.users
import kotlinx.serialization.Serializable
class UsersRepository(private val usersRemoteDataSource: UsersRemoteDataSource = UsersRemoteDataSource()) {
fun insertUser(newUser: NewUser) {}
}
@Serializable
data class NewUser(val displayName: String, val emailAddress: String)

View file

@ -1,20 +0,0 @@
package ing.bikeshedengineer.debtpirate.screens.auth.usecases
import ing.bikeshedengineer.debtpirate.repositories.auth.AuthRepository
import ing.bikeshedengineer.debtpirate.repositories.auth.models.AuthLoginRequest
import ing.bikeshedengineer.debtpirate.repositories.auth.models.AuthLoginResponse
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import retrofit2.Response
class LoginUsecase(
private val authRepository: AuthRepository = AuthRepository(),
private val defaultDispatcher: CoroutineDispatcher = Dispatchers.IO
) {
suspend operator fun invoke(credentials: AuthLoginRequest): Response<AuthLoginResponse> =
withContext(defaultDispatcher) {
val response = authRepository.submitLoginRequest(credentials)
response
}
}

View file

@ -1,3 +0,0 @@
package ing.bikeshedengineer.debtpirate.screens.auth.usecases
class RegisterNewAccountUseCase

View file

@ -8,7 +8,7 @@ message Token {
int64 expires_at = 2; int64 expires_at = 2;
} }
message Store { message PrefsDataStore {
int32 user_id = 1; int32 user_id = 1;
Token auth_token = 2; Token auth_token = 2;
Token session_token = 3; Token session_token = 3;

View file

@ -5,4 +5,5 @@ plugins {
alias(libs.plugins.compose.compiler) apply false alias(libs.plugins.compose.compiler) apply false
alias(libs.plugins.serialization) apply false alias(libs.plugins.serialization) apply false
alias(libs.plugins.protobuf) apply false alias(libs.plugins.protobuf) apply false
alias(libs.plugins.hilt) apply false
} }

View file

@ -1,11 +1,12 @@
[versions] [versions]
activityCompose = "1.9.2" activityCompose = "1.9.3"
agp = "8.7.0" agp = "8.7.1"
appcompat = "1.7.0" appcompat = "1.7.0"
composeBom = "2024.09.03" composeBom = "2024.10.00"
coreKtx = "1.13.1" coreKtx = "1.13.1"
datastore = "1.1.1" datastore = "1.1.1"
espressoCore = "3.6.1" espressoCore = "3.6.1"
hilt = "2.51.1"
iconsExtended = "1.7.3" iconsExtended = "1.7.3"
junit = "4.13.2" junit = "4.13.2"
junitVersion = "1.2.1" junitVersion = "1.2.1"
@ -17,11 +18,12 @@ lifecycleViewModelKtx = "2.8.6"
lifecycleViewmodelCompose = "2.8.6" lifecycleViewmodelCompose = "2.8.6"
material = "1.12.0" material = "1.12.0"
material3 = "1.3.0" material3 = "1.3.0"
navigation = "2.8.2" navigation = "2.8.3"
protobuf = "0.9.4" protobuf = "0.9.4"
protoLite = "3.21.11" protoLite = "3.21.11"
okhttp = "4.10.0" okhttp = "4.10.0"
retrofit = "2.9.0" retrofit = "2.9.0"
hiltNavigationCompose = "1.2.0"
[libraries] [libraries]
androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" } androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
@ -54,10 +56,14 @@ okhttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhtt
okhttp-logging-interceptor = { group = "com.squareup.okhttp3", name = "logging-interceptor", version.ref = "okhttp" } okhttp-logging-interceptor = { group = "com.squareup.okhttp3", name = "logging-interceptor", version.ref = "okhttp" }
protobuf-javalite = { group = "com.google.protobuf", name = "protobuf-javalite", version.ref = "protoLite" } protobuf-javalite = { group = "com.google.protobuf", name = "protobuf-javalite", version.ref = "protoLite" }
protobuf-kotlinlite = { group = "com.google.protobuf", name = "protobuf-kotlin-lite", version.ref = "protoLite" } protobuf-kotlinlite = { group = "com.google.protobuf", name = "protobuf-kotlin-lite", version.ref = "protoLite" }
hilt = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt" }
hilt-kapt = { group = "com.google.dagger", name = "hilt-android-compiler", version.ref = "hilt" }
hilt-compose = { group = "androidx.hilt", name = "hilt-navigation-compose", version.ref = "hiltNavigationCompose" }
[plugins] [plugins]
android-application = { id = "com.android.application", version.ref = "agp" } android-application = { id = "com.android.application", version.ref = "agp" }
jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
protobuf = { id = "com.google.protobuf", version.ref = "protobuf" } protobuf = { id = "com.google.protobuf", version.ref = "protobuf" }
hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" }