diff --git a/README.md b/README.md new file mode 100644 index 0000000..7f093e1 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# AProSelection + +[![Android CI](https://github.com/stslex/AProSelection/actions/workflows/android_build.yml/badge.svg)](https://github.com/stslex/AProSelection/actions/workflows/android_build.yml) diff --git a/app/src/main/java/com/stslex/aproselection/ui/InitialApp.kt b/app/src/main/java/com/stslex/aproselection/ui/InitialApp.kt index 555c410..bbb3867 100644 --- a/app/src/main/java/com/stslex/aproselection/ui/InitialApp.kt +++ b/app/src/main/java/com/stslex/aproselection/ui/InitialApp.kt @@ -1,28 +1,14 @@ package com.stslex.aproselection.ui -import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect -import androidx.compose.ui.graphics.Color import androidx.compose.ui.tooling.preview.Preview import androidx.navigation.compose.rememberNavController -import com.google.accompanist.systemuicontroller.rememberSystemUiController -import com.stslex.aproselection.navigation.NavigationHost import com.stslex.aproselection.core.ui.theme.AppTheme +import com.stslex.aproselection.navigation.NavigationHost @Composable fun InitialApp() { val navController = rememberNavController() - val systemUiController = rememberSystemUiController() - val isDarkTheme = isSystemInDarkTheme() - - DisposableEffect(systemUiController, isDarkTheme) { - systemUiController.setSystemBarsColor( - color = Color.Transparent, - darkIcons = isDarkTheme.not(), - ) - onDispose {} - } NavigationHost(navController = navController) } diff --git a/app/src/main/java/com/stslex/aproselection/ui/MainActivity.kt b/app/src/main/java/com/stslex/aproselection/ui/MainActivity.kt index 2d53911..6a44d46 100644 --- a/app/src/main/java/com/stslex/aproselection/ui/MainActivity.kt +++ b/app/src/main/java/com/stslex/aproselection/ui/MainActivity.kt @@ -3,16 +3,12 @@ package com.stslex.aproselection.ui import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent -import androidx.core.view.WindowCompat import com.stslex.aproselection.core.ui.theme.AppTheme class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - - WindowCompat.setDecorFitsSystemWindows(window, false) - setContent { AppTheme { InitialApp() diff --git a/core/network/build.gradle.kts b/core/network/build.gradle.kts index 3650644..8d429d6 100644 --- a/core/network/build.gradle.kts +++ b/core/network/build.gradle.kts @@ -17,10 +17,13 @@ val key = properties["API_KEY"]?.toString() ?: throw IllegalStateException("API_KEY") val apiHost = properties["API_SERVER_HOST"]?.toString() ?: throw IllegalStateException("API_SERVER_HOST") +val apiVersion = properties["API_VERSION"]?.toString() + ?: throw IllegalStateException("API_VERSION") android { defaultConfig { buildConfigField("String", "API_KEY", key) buildConfigField("String", "API_SERVER_HOST", apiHost) + buildConfigField("String", "API_VERSION", apiVersion) } } diff --git a/core/network/src/main/java/com/stslex/aproselection/core/network/client/NetworkClientImpl.kt b/core/network/src/main/java/com/stslex/aproselection/core/network/client/NetworkClientImpl.kt index 8f925fc..31cd302 100644 --- a/core/network/src/main/java/com/stslex/aproselection/core/network/client/NetworkClientImpl.kt +++ b/core/network/src/main/java/com/stslex/aproselection/core/network/client/NetworkClientImpl.kt @@ -9,7 +9,9 @@ import io.ktor.client.plugins.logging.DEFAULT import io.ktor.client.plugins.logging.LogLevel import io.ktor.client.plugins.logging.Logger import io.ktor.client.plugins.logging.Logging +import io.ktor.client.request.bearerAuth import io.ktor.http.URLProtocol +import io.ktor.http.encodedPath import io.ktor.serialization.kotlinx.json.json import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.json.Json @@ -43,21 +45,11 @@ class NetworkClientImpl : NetworkClient { get() = client.config { defaultRequest { url { - host = "${BuildConfig.API_SERVER_HOST}$HOST_API_URL" + host = BuildConfig.API_SERVER_HOST + encodedPath = BuildConfig.API_VERSION protocol = URLProtocol.HTTP } -// headers { -// append( -// HEADER_AUTH, -// "$HEADER_AUTH_FIELD ${BuildConfig.API_KEY}" -// ) -// } + bearerAuth(BuildConfig.API_KEY) } } - - companion object { - private const val HOST_API_URL = "/api/v1" - private const val HEADER_AUTH = "Authorization" - private const val HEADER_AUTH_FIELD = "Client-ID" - } } \ No newline at end of file diff --git a/core/network/src/main/java/com/stslex/aproselection/core/network/clients/auth/AuthNetworkClient.kt b/core/network/src/main/java/com/stslex/aproselection/core/network/clients/auth/AuthNetworkClient.kt new file mode 100644 index 0000000..e133342 --- /dev/null +++ b/core/network/src/main/java/com/stslex/aproselection/core/network/clients/auth/AuthNetworkClient.kt @@ -0,0 +1,8 @@ +package com.stslex.aproselection.core.network.clients.auth + +import com.stslex.aproselection.core.network.clients.auth.model.HelloRequestModel + +interface AuthNetworkClient { + + suspend fun getHello(username: String): HelloRequestModel +} \ No newline at end of file diff --git a/core/network/src/main/java/com/stslex/aproselection/core/network/clients/auth/AuthNetworkClientImpl.kt b/core/network/src/main/java/com/stslex/aproselection/core/network/clients/auth/AuthNetworkClientImpl.kt new file mode 100644 index 0000000..7148185 --- /dev/null +++ b/core/network/src/main/java/com/stslex/aproselection/core/network/clients/auth/AuthNetworkClientImpl.kt @@ -0,0 +1,25 @@ +package com.stslex.aproselection.core.network.clients.auth + +import com.stslex.aproselection.core.network.client.NetworkClient +import com.stslex.aproselection.core.network.clients.auth.model.HelloRequestModel +import io.ktor.client.call.body +import io.ktor.client.request.get +import io.ktor.http.appendPathSegments +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +class AuthNetworkClientImpl( + private val networkClient: NetworkClient +) : AuthNetworkClient { + + override suspend fun getHello( + username: String + ): HelloRequestModel = withContext(Dispatchers.IO) { + networkClient + .apiClient + .get { + url.appendPathSegments("hello", username) + } + .body() + } +} \ No newline at end of file diff --git a/core/network/src/main/java/com/stslex/aproselection/core/network/clients/auth/model/HelloRequestModel.kt b/core/network/src/main/java/com/stslex/aproselection/core/network/clients/auth/model/HelloRequestModel.kt new file mode 100644 index 0000000..a19f3c3 --- /dev/null +++ b/core/network/src/main/java/com/stslex/aproselection/core/network/clients/auth/model/HelloRequestModel.kt @@ -0,0 +1,10 @@ +package com.stslex.aproselection.core.network.clients.auth.model + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class HelloRequestModel( + @SerialName("text") + val text: String +) diff --git a/core/network/src/main/java/com/stslex/aproselection/core/network/di/ModuleCoreNetwork.kt b/core/network/src/main/java/com/stslex/aproselection/core/network/di/ModuleCoreNetwork.kt index 0b870e6..65b6b0e 100644 --- a/core/network/src/main/java/com/stslex/aproselection/core/network/di/ModuleCoreNetwork.kt +++ b/core/network/src/main/java/com/stslex/aproselection/core/network/di/ModuleCoreNetwork.kt @@ -2,6 +2,8 @@ package com.stslex.aproselection.core.network.di import com.stslex.aproselection.core.network.client.NetworkClient import com.stslex.aproselection.core.network.client.NetworkClientImpl +import com.stslex.aproselection.core.network.clients.auth.AuthNetworkClient +import com.stslex.aproselection.core.network.clients.auth.AuthNetworkClientImpl import org.koin.core.module.dsl.bind import org.koin.core.module.dsl.singleOf import org.koin.dsl.module @@ -10,5 +12,6 @@ object ModuleCoreNetwork { val moduleCoreNetwork = module { singleOf(::NetworkClientImpl) { bind() } + singleOf(::AuthNetworkClientImpl) { bind() } } } \ No newline at end of file diff --git a/core/ui/src/main/java/com/stslex/aproselection/core/ui/base/BaseViewModel.kt b/core/ui/src/main/java/com/stslex/aproselection/core/ui/base/BaseViewModel.kt new file mode 100644 index 0000000..51832ab --- /dev/null +++ b/core/ui/src/main/java/com/stslex/aproselection/core/ui/base/BaseViewModel.kt @@ -0,0 +1,24 @@ +package com.stslex.aproselection.core.ui.base + +import android.util.Log +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.stateIn + +open class BaseViewModel : ViewModel() { + + fun Flow.stateIn( + initialValue: T + ): StateFlow = this.stateIn( + scope = viewModelScope, + started = SharingStarted.Lazily, + initialValue = initialValue + ) + + fun handleError(throwable: Throwable) { + Log.e(javaClass.simpleName, throwable.message, throwable) + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/stslex/aproselection/core/ui/theme/Theme.kt b/core/ui/src/main/java/com/stslex/aproselection/core/ui/theme/Theme.kt index b8e9bcd..de204ff 100644 --- a/core/ui/src/main/java/com/stslex/aproselection/core/ui/theme/Theme.kt +++ b/core/ui/src/main/java/com/stslex/aproselection/core/ui/theme/Theme.kt @@ -9,11 +9,12 @@ import androidx.compose.material3.dynamicDarkColorScheme import androidx.compose.material3.dynamicLightColorScheme import androidx.compose.material3.lightColorScheme import androidx.compose.runtime.Composable -import androidx.compose.runtime.SideEffect -import androidx.compose.ui.graphics.toArgb +import androidx.compose.runtime.DisposableEffect +import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalView import androidx.core.view.WindowCompat +import com.google.accompanist.systemuicontroller.rememberSystemUiController private val DarkColorScheme = darkColorScheme( primary = Purple80, @@ -55,10 +56,19 @@ fun AppTheme( } val view = LocalView.current if (!view.isInEditMode) { - SideEffect { - val window = (view.context as Activity).window - window.statusBarColor = colorScheme.primary.toArgb() - WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = darkTheme + val window = (view.context as Activity).window + val systemUiController = rememberSystemUiController() + val isDarkTheme = isSystemInDarkTheme() + + DisposableEffect(systemUiController, isDarkTheme) { + WindowCompat.setDecorFitsSystemWindows(window, false) + systemUiController.setSystemBarsColor( + color = Color.Transparent, + darkIcons = isDarkTheme.not(), + ) + onDispose { + WindowCompat.setDecorFitsSystemWindows(window, true) + } } } diff --git a/feature/auth/build.gradle.kts b/feature/auth/build.gradle.kts index 33dc149..4c98180 100644 --- a/feature/auth/build.gradle.kts +++ b/feature/auth/build.gradle.kts @@ -1,14 +1,11 @@ plugins { id("aproselection.android.library") id("aproselection.android.library.compose") - kotlin("plugin.serialization") } dependencies { implementation(project(":core:ui")) implementation(project(":core:network")) - implementation(libs.bundles.ktor) - implementation(libs.bundles.okhttp) } android.namespace = "com.stslex.aproselection.feature.auth" diff --git a/feature/auth/src/main/java/com/stslex/aproselection/feature/auth/data/repository/AuthRepository.kt b/feature/auth/src/main/java/com/stslex/aproselection/feature/auth/data/repository/AuthRepository.kt new file mode 100644 index 0000000..5b77795 --- /dev/null +++ b/feature/auth/src/main/java/com/stslex/aproselection/feature/auth/data/repository/AuthRepository.kt @@ -0,0 +1,8 @@ +package com.stslex.aproselection.feature.auth.data.repository + +import kotlinx.coroutines.flow.Flow + +interface AuthRepository { + + fun getHello(username: String): Flow +} \ No newline at end of file diff --git a/feature/auth/src/main/java/com/stslex/aproselection/feature/auth/data/repository/AuthRepositoryImpl.kt b/feature/auth/src/main/java/com/stslex/aproselection/feature/auth/data/repository/AuthRepositoryImpl.kt new file mode 100644 index 0000000..8760b92 --- /dev/null +++ b/feature/auth/src/main/java/com/stslex/aproselection/feature/auth/data/repository/AuthRepositoryImpl.kt @@ -0,0 +1,17 @@ +package com.stslex.aproselection.feature.auth.data.repository + +import com.stslex.aproselection.core.network.clients.auth.AuthNetworkClient +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow + +class AuthRepositoryImpl( + private val networkClient: AuthNetworkClient +) : AuthRepository { + + override fun getHello(username: String): Flow = flow { + val helloResponse = networkClient + .getHello(username = username) + .text + emit(helloResponse) + } +} \ No newline at end of file diff --git a/feature/auth/src/main/java/com/stslex/aproselection/feature/auth/di/ModuleFeatureAuth.kt b/feature/auth/src/main/java/com/stslex/aproselection/feature/auth/di/ModuleFeatureAuth.kt index 5ccadc9..3b69a03 100644 --- a/feature/auth/src/main/java/com/stslex/aproselection/feature/auth/di/ModuleFeatureAuth.kt +++ b/feature/auth/src/main/java/com/stslex/aproselection/feature/auth/di/ModuleFeatureAuth.kt @@ -1,12 +1,20 @@ package com.stslex.aproselection.feature.auth.di +import com.stslex.aproselection.feature.auth.data.repository.AuthRepository +import com.stslex.aproselection.feature.auth.data.repository.AuthRepositoryImpl +import com.stslex.aproselection.feature.auth.domain.interactor.AuthInteractor +import com.stslex.aproselection.feature.auth.domain.interactor.AuthInteractorImpl import com.stslex.aproselection.feature.auth.ui.AuthViewModel import org.koin.androidx.viewmodel.dsl.viewModelOf +import org.koin.core.module.dsl.bind +import org.koin.core.module.dsl.factoryOf import org.koin.dsl.module object ModuleFeatureAuth { val moduleFeatureAuth = module { viewModelOf(::AuthViewModel) + factoryOf(::AuthInteractorImpl) { bind() } + factoryOf(::AuthRepositoryImpl) { bind() } } } \ No newline at end of file diff --git a/feature/auth/src/main/java/com/stslex/aproselection/feature/auth/domain/interactor/AuthInteractor.kt b/feature/auth/src/main/java/com/stslex/aproselection/feature/auth/domain/interactor/AuthInteractor.kt new file mode 100644 index 0000000..0629d66 --- /dev/null +++ b/feature/auth/src/main/java/com/stslex/aproselection/feature/auth/domain/interactor/AuthInteractor.kt @@ -0,0 +1,8 @@ +package com.stslex.aproselection.feature.auth.domain.interactor + +import kotlinx.coroutines.flow.Flow + +interface AuthInteractor { + + fun getHello(username: String): Flow +} \ No newline at end of file diff --git a/feature/auth/src/main/java/com/stslex/aproselection/feature/auth/domain/interactor/AuthInteractorImpl.kt b/feature/auth/src/main/java/com/stslex/aproselection/feature/auth/domain/interactor/AuthInteractorImpl.kt new file mode 100644 index 0000000..d696509 --- /dev/null +++ b/feature/auth/src/main/java/com/stslex/aproselection/feature/auth/domain/interactor/AuthInteractorImpl.kt @@ -0,0 +1,15 @@ +package com.stslex.aproselection.feature.auth.domain.interactor + +import com.stslex.aproselection.feature.auth.data.repository.AuthRepository +import kotlinx.coroutines.flow.Flow + +class AuthInteractorImpl( + private val repository: AuthRepository +) : AuthInteractor { + + override fun getHello( + username: String + ): Flow = repository.getHello( + username = username + ) +} \ No newline at end of file diff --git a/feature/auth/src/main/java/com/stslex/aproselection/feature/auth/ui/AuthScreen.kt b/feature/auth/src/main/java/com/stslex/aproselection/feature/auth/ui/AuthScreen.kt index 3f4aba2..3c29bac 100644 --- a/feature/auth/src/main/java/com/stslex/aproselection/feature/auth/ui/AuthScreen.kt +++ b/feature/auth/src/main/java/com/stslex/aproselection/feature/auth/ui/AuthScreen.kt @@ -1,37 +1,84 @@ package com.stslex.aproselection.feature.auth.ui +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Divider +import androidx.compose.material3.ElevatedButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text +import androidx.compose.material3.TextField import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp import com.stslex.aproselection.core.ui.navigation.NavigationScreen @Composable fun AuthScreen( text: String, navigate: (NavigationScreen) -> Unit, + setUsername: (String) -> Unit, modifier: Modifier = Modifier, ) { + var inputUsername by remember { + mutableStateOf("") + } + Box( - modifier = modifier.fillMaxSize() + modifier = modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.background), + contentAlignment = Alignment.Center, ) { - Text( - modifier = Modifier.align(Alignment.Center), - text = text, - style = MaterialTheme.typography.headlineMedium - ) + + Column( + horizontalAlignment = Alignment.CenterHorizontally + ) { + TextField( + value = inputUsername, + onValueChange = { value -> + if (inputUsername != value) { + inputUsername = value + } + }, + maxLines = 1, + ) + Divider(Modifier.padding(16.dp)) + ElevatedButton( + onClick = { + setUsername(inputUsername) + inputUsername = "" + } + ) { + Text( + text = "submit", + style = MaterialTheme.typography.headlineMedium + ) + } + Divider(Modifier.padding(16.dp)) + Text( + text = text, + style = MaterialTheme.typography.headlineMedium, + color = MaterialTheme.colorScheme.onBackground + ) + } } } -@Preview +@Preview(device = "id:pixel_6", showSystemUi = true, showBackground = true) @Composable fun AuthScreenPreview() { AuthScreen( text = "text", - navigate = {} + navigate = {}, + setUsername = {} ) } \ No newline at end of file diff --git a/feature/auth/src/main/java/com/stslex/aproselection/feature/auth/ui/AuthViewModel.kt b/feature/auth/src/main/java/com/stslex/aproselection/feature/auth/ui/AuthViewModel.kt index 99f7c1c..4fc3873 100644 --- a/feature/auth/src/main/java/com/stslex/aproselection/feature/auth/ui/AuthViewModel.kt +++ b/feature/auth/src/main/java/com/stslex/aproselection/feature/auth/ui/AuthViewModel.kt @@ -1,41 +1,33 @@ package com.stslex.aproselection.feature.auth.ui -import android.util.Log -import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.stslex.aproselection.core.network.client.NetworkClient -import io.ktor.client.call.body -import io.ktor.client.request.get -import io.ktor.http.appendPathSegments -import kotlinx.coroutines.flow.SharingStarted +import com.stslex.aproselection.core.ui.base.BaseViewModel +import com.stslex.aproselection.feature.auth.domain.interactor.AuthInteractor +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.catch -import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.flow.stateIn -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach class AuthViewModel( - private val networkClient: NetworkClient -) : ViewModel() { + private val interactor: AuthInteractor +) : BaseViewModel() { + + private val _text: MutableStateFlow = MutableStateFlow("...") val text: StateFlow - get() = flow { - val result = networkClient.apiClient.get { - url.appendPathSegments("hello") + get() = _text.asStateFlow() + + fun setUsername(username: String) { + _text.value = "..." + interactor.getHello(username) + .catch { throwable -> + handleError(throwable) } - .body() - .hello - emit(result) - } - .catch { - Log.e(javaClass.simpleName, it.message, it) + .onEach { receivedText -> + _text.emit(receivedText) } - .stateIn(viewModelScope, SharingStarted.Lazily, "") + .launchIn(viewModelScope) + } } - -@Serializable -data class HelloRequest( - @SerialName("text") - val hello: String -) \ No newline at end of file diff --git a/feature/auth/src/main/java/com/stslex/aproselection/feature/auth/ui/navigation/AuthRouter.kt b/feature/auth/src/main/java/com/stslex/aproselection/feature/auth/ui/navigation/AuthRouter.kt index e11ebeb..557bbd3 100644 --- a/feature/auth/src/main/java/com/stslex/aproselection/feature/auth/ui/navigation/AuthRouter.kt +++ b/feature/auth/src/main/java/com/stslex/aproselection/feature/auth/ui/navigation/AuthRouter.kt @@ -28,6 +28,7 @@ fun NavGraphBuilder.authRouter( AuthScreen( text = text, navigate = navigate, + setUsername = viewModel::setUsername, modifier = modifier ) } diff --git a/feature/auth/src/test/java/com/stslex/aproselection/feature/auth/ModuleFeatureAuthTest.kt b/feature/auth/src/test/java/com/stslex/aproselection/feature/auth/ModuleFeatureAuthTest.kt index 81c2ea6..7d10fe9 100644 --- a/feature/auth/src/test/java/com/stslex/aproselection/feature/auth/ModuleFeatureAuthTest.kt +++ b/feature/auth/src/test/java/com/stslex/aproselection/feature/auth/ModuleFeatureAuthTest.kt @@ -1,7 +1,8 @@ package com.stslex.aproselection.feature.auth import android.content.Context -import com.stslex.aproselection.feature.auth.di.ModuleFeatureAuth +import com.stslex.aproselection.core.network.di.ModuleCoreNetwork.moduleCoreNetwork +import com.stslex.aproselection.feature.auth.di.ModuleFeatureAuth.moduleFeatureAuth import org.junit.Test import org.koin.android.ext.koin.androidContext import org.koin.dsl.koinApplication @@ -15,7 +16,10 @@ class ModuleFeatureAuthTest : KoinTest { fun checkKoinModules() { koinApplication { androidContext(Mockito.mock(Context::class.java)) - modules(ModuleFeatureAuth.moduleFeatureAuth) + modules( + moduleFeatureAuth, + moduleCoreNetwork + ) checkModules() } }