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

View file

@ -5,6 +5,7 @@
<uses-permission android:name="android.permission.INTERNET" />
<application
android:name=".app.android.DebtPirateApplication"
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
@ -15,9 +16,8 @@
android:theme="@style/Theme.DebtPirate"
android:networkSecurityConfig="@xml/network_security_config">
<activity
android:name=".MainActivity"
android:name=".app.host.MainActivity"
android:exported="true"
android:label="@string/title_activity_main"
android:theme="@style/Theme.DebtPirate">
<intent-filter>
<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 androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import ing.bikeshedengineer.debtpirate.screens.auth.AuthScreen
import ing.bikeshedengineer.debtpirate.screens.auth.AuthScreenViewModel
import ing.bikeshedengineer.debtpirate.ui.theme.DebtPirateTheme
import dagger.hilt.android.AndroidEntryPoint
import ing.bikeshedengineer.debtpirate.presentation.ui.auth.AuthScreen
import ing.bikeshedengineer.debtpirate.presentation.theme.DebtPirateTheme
import kotlinx.serialization.Serializable
@Serializable
object AuthRoute
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
val navController = rememberNavController()
DebtPirateTheme {
NavHost(navController = navController, startDestination = AuthRoute) {
composable<AuthRoute> {
AuthScreen(
viewModel = viewModel<AuthScreenViewModel>()
)
AuthScreen( )
}
}
}

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)

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

View file

@ -1,4 +1,4 @@
package ing.bikeshedengineer.debtpirate.ui.theme
package ing.bikeshedengineer.debtpirate.presentation.theme
import android.os.Build
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.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 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.KeyboardType
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.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
@Composable
fun AuthScreen(
viewModel: AuthScreenViewModel
viewModel: AuthScreenViewModel = hiltViewModel<AuthScreenViewModel>()
) {
Scaffold(
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 androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import ing.bikeshedengineer.debtpirate.repositories.auth.models.AuthLoginRequest
import ing.bikeshedengineer.debtpirate.screens.auth.usecases.LoginUsecase
import dagger.hilt.android.lifecycle.HiltViewModel
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.asStateFlow
import kotlinx.coroutines.launch
import javax.inject.Inject
class AuthScreenViewModel(private val loginRequest: LoginUsecase = LoginUsecase()) :
ViewModel() {
@HiltViewModel
class AuthScreenViewModel @Inject constructor(private val authRepository: AuthRepository) : ViewModel() {
private val tag: String
get() {
return javaClass.simpleName
}
// private val storeLoginData = StoreLoginDataUseCase(dataStore)
private val _username = MutableStateFlow("")
val username = _username.asStateFlow()
@ -33,15 +37,12 @@ class AuthScreenViewModel(private val loginRequest: LoginUsecase = LoginUsecase(
fun submitLoginRequest() {
val credentials = AuthLoginRequest(this._username.value, this._password.value)
viewModelScope.launch {
val response = loginRequest(credentials)
when (response.isSuccessful) {
true -> {
Log.i(tag, "Login successful! ${response.body()}")
}
false -> {
Log.e(tag, "Login unsuccessful! ${response.body()}")
}
try {
val response = authRepository.submitLoginRequest(credentials)
Log.i(tag, "Login successful! $response")
} catch (err: Throwable) {
// TODO...
Log.e(tag, err.message!!)
}
}
}

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;
}
message Store {
message PrefsDataStore {
int32 user_id = 1;
Token auth_token = 2;
Token session_token = 3;

View file

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

View file

@ -1,11 +1,12 @@
[versions]
activityCompose = "1.9.2"
agp = "8.7.0"
activityCompose = "1.9.3"
agp = "8.7.1"
appcompat = "1.7.0"
composeBom = "2024.09.03"
composeBom = "2024.10.00"
coreKtx = "1.13.1"
datastore = "1.1.1"
espressoCore = "3.6.1"
hilt = "2.51.1"
iconsExtended = "1.7.3"
junit = "4.13.2"
junitVersion = "1.2.1"
@ -17,11 +18,12 @@ lifecycleViewModelKtx = "2.8.6"
lifecycleViewmodelCompose = "2.8.6"
material = "1.12.0"
material3 = "1.3.0"
navigation = "2.8.2"
navigation = "2.8.3"
protobuf = "0.9.4"
protoLite = "3.21.11"
okhttp = "4.10.0"
retrofit = "2.9.0"
hiltNavigationCompose = "1.2.0"
[libraries]
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" }
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" }
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]
android-application = { id = "com.android.application", version.ref = "agp" }
jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", 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" }