Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix auth and ui #16

Merged
merged 3 commits into from
Aug 30, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 10 additions & 5 deletions app/src/main/java/com/stslex/aproselection/ui/InitialApp.kt
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.unit.dp
import androidx.navigation.NavHostController
import com.stslex.aproselection.core.navigation.destination.AppDestination
import com.stslex.aproselection.core.navigation.ext.NavExt.isAuth
import com.stslex.aproselection.core.ui.ext.noRippleClick
import com.stslex.aproselection.core.ui.theme.AppDimens
import com.stslex.aproselection.navigation.NavigationHost
Expand All @@ -48,17 +47,22 @@ fun InitialApp(
val isInitialAuth by remember {
viewModel.isInitialAuth
}.collectAsState()
var isAuth by remember {
mutableStateOf(false)
}

LaunchedEffect(Unit) {
viewModel.init()
}

navController.addOnDestinationChangedListener { _, destination, _ ->
isAuth = destination.route != AppDestination.AUTH.navigationRoute
}

AppDestination
.getStartDestination(isInitialAuth)
?.let { destination ->
AppContainer(
isAuth = navController.isAuth
) {
AppContainer(isAuth = isAuth) {
NavigationHost(
navController = navController,
startDestination = destination
Expand All @@ -85,7 +89,8 @@ fun AppContainer(
AppDrawerState.OPEN -> 0.dp
AppDrawerState.CLOSE -> -screenWidth * 0.3f
},
animationSpec = tween(500)
animationSpec = tween(500),
label = "drawer animation"
)

BackHandler(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
package com.stslex.aproselection.ui

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.stslex.aproselection.controller.AuthController
import com.stslex.aproselection.core.navigation.destination.NavigationScreen
import com.stslex.aproselection.core.navigation.navigator.Navigator
import com.stslex.aproselection.core.ui.base.BaseViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
Expand All @@ -14,7 +14,7 @@ import kotlinx.coroutines.flow.onEach
class InitialAppViewModel(
private val navigator: Navigator,
private val controller: AuthController
) : BaseViewModel() {
) : ViewModel() {

private val _isInitialAuth = MutableStateFlow<Boolean?>(null)
val isInitialAuth: StateFlow<Boolean?>
Expand Down
41 changes: 37 additions & 4 deletions app/src/test/java/com/stslex/aproselection/DiKoinModuleTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,22 +10,38 @@ import com.stslex.aproselection.di.appModule
import com.stslex.aproselection.di.initialAppModule
import com.stslex.aproselection.feature.auth.di.ModuleFeatureAuth.moduleFeatureAuth
import com.stslex.aproselection.feature.home.di.moduleFeatureHome
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestDispatcher
import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.setMain
import org.junit.Rule
import org.junit.Test
import org.koin.android.ext.koin.androidContext
import org.junit.rules.TestWatcher
import org.junit.runner.Description
import org.koin.dsl.koinApplication
import org.koin.test.KoinTest
import org.koin.test.check.checkModules
import org.koin.test.mock.MockProviderRule
import org.mockito.Mockito

class DiKoinModuleTest : KoinTest {

@ExperimentalCoroutinesApi
@get:Rule
val mainCoroutineRule = MainCoroutineRule()

@get:Rule
val mockProvider = MockProviderRule.create { clazz ->
Mockito.mock(clazz.java)
}

@Test
fun checkKoinModules() {
val navController = Mockito.mock(NavHostController::class.java)

koinApplication {

androidContext(Mockito.mock(Context::class.java))
modules(
moduleCoreNavigation(navController),
initialAppModule,
Expand All @@ -36,7 +52,24 @@ class DiKoinModuleTest : KoinTest {
moduleFeatureAuth,
moduleFeatureHome,
)
checkModules()
checkModules {
withInstance<Context>()
}
}
}
}

@ExperimentalCoroutinesApi
class MainCoroutineRule(private val dispatcher: TestDispatcher = StandardTestDispatcher()) :
TestWatcher() {

override fun starting(description: Description?) {
super.starting(description)
Dispatchers.setMain(dispatcher)
}

override fun finished(description: Description?) {
super.finished(description)
Dispatchers.resetMain()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,5 @@ object NavExt {
}

val NavController.isAuth: Boolean
get() = currentDestination?.route != AppDestination.AUTH.route
get() = currentDestination?.route != AppDestination.AUTH.navigationRoute
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ package com.stslex.aproselection.core.network.client

import com.stslex.aproselection.core.datastore.AppDataStore
import com.stslex.aproselection.core.network.BuildConfig
import com.stslex.aproselection.core.network.clients.auth.model.UserAuthResponseModel
import com.stslex.aproselection.core.network.clients.auth.model.UserAuthRequestModel
import com.stslex.aproselection.core.network.clients.auth.model.UserAuthResponseModel
import com.stslex.aproselection.core.network.model.ApiError
import com.stslex.aproselection.core.network.model.ApiErrorRespond
import com.stslex.aproselection.core.network.model.ApiErrorType
Expand All @@ -27,9 +27,9 @@ import io.ktor.client.request.post
import io.ktor.client.request.setBody
import io.ktor.http.ContentType
import io.ktor.http.URLProtocol
import io.ktor.http.appendPathSegments
import io.ktor.http.contentType
import io.ktor.http.encodedPath
import io.ktor.http.path
import io.ktor.serialization.kotlinx.json.json
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
Expand Down Expand Up @@ -76,7 +76,7 @@ class NetworkClientImpl(

override val apiClient: HttpClient = client.config {
defaultRequest {
url {
url("${BuildConfig.API_SERVER_HOST}") {
host = BuildConfig.API_SERVER_HOST
encodedPath = BuildConfig.API_VERSION
protocol = URLProtocol.HTTP
Expand All @@ -100,9 +100,7 @@ class NetworkClientImpl(
)

when (apiError.type) {
is ApiErrorType.Unauthorized.Token -> {
dataStore.setToken(auth().token)
}
is ApiErrorType.Unauthorized.Token -> auth()

else -> throw apiError
}
Expand All @@ -122,10 +120,15 @@ class NetworkClientImpl(
username = dataStore.credential.value.username,
password = dataStore.credential.value.password
)
apiClient.post {
url.appendPathSegments("passport/login")
setBody<UserAuthRequestModel>(user)
}.body<UserAuthResponseModel>()
apiClient
.post {
url.path("passport/login")
setBody<UserAuthRequestModel>(user)
}
.body<UserAuthResponseModel>()
.also { authModel ->
dataStore.setToken(token = authModel.token)
}
}

companion object {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,12 @@ package com.stslex.aproselection.core.network.clients.auth

import com.stslex.aproselection.core.datastore.AppDataStore
import com.stslex.aproselection.core.network.client.NetworkClient
import com.stslex.aproselection.core.network.clients.auth.model.HelloRequestModel
import com.stslex.aproselection.core.network.clients.auth.model.UserAuthResponseModel
import com.stslex.aproselection.core.network.clients.auth.model.UserAuthRequestModel
import com.stslex.aproselection.core.network.clients.auth.model.UserAuthResponseModel
import com.stslex.aproselection.core.network.clients.auth.model.toStorage
import io.ktor.client.call.body
import io.ktor.client.request.get
import io.ktor.client.request.post
import io.ktor.client.request.setBody
import io.ktor.http.appendPathSegments
import io.ktor.http.path
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
Expand All @@ -36,7 +33,7 @@ class AuthNetworkClientImpl(
networkClient
.apiClient
.post {
url.appendPathSegments("passport/registration")
url.path("passport/registration")
setBody<UserAuthRequestModel>(user)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,5 @@ data class UserAuthRequestModel(
@SerialName("username")
val username: String,
@SerialName("password")
val password: String
val password: String,
)
Original file line number Diff line number Diff line change
Expand Up @@ -2,51 +2,25 @@ package com.stslex.aproselection.core.ui.base

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.paging.Pager
import androidx.paging.PagingData
import androidx.paging.cachedIn
import androidx.paging.map
import com.stslex.aproselection.core.core.Logger
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
import com.stslex.aproselection.core.ui.base.store.Store
import com.stslex.aproselection.core.ui.base.store.Store.Action
import com.stslex.aproselection.core.ui.base.store.Store.Event
import com.stslex.aproselection.core.ui.base.store.Store.State
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn

open class BaseViewModel : ViewModel() {
open class BaseViewModel<out S : State, out E : Event, in A : Action>(
private val store: Store<S, E, A>
) : ViewModel() {

inline fun <T : Any, R : Any> Pager<Int, T>.mapState(
crossinline transform: suspend (T) -> R
): StateFlow<PagingData<R>> = this
.flow
.map { pagingData ->
pagingData.map { item ->
transform(item)
}
}
.primaryPagingFlow
val state: StateFlow<S> = store.state
val event: SharedFlow<E> = store.event

fun <T> Flow<T>.stateIn(
initialValue: T
): StateFlow<T> = this.stateIn(
scope = viewModelScope,
started = SharingStarted.Lazily,
initialValue = initialValue
)

val <T : Any> Flow<PagingData<T>>.primaryPagingFlow: StateFlow<PagingData<T>>
get() = cachedIn(viewModelScope)
.makeStateFlow(PagingData.empty())

private fun <T : Any> Flow<T>.makeStateFlow(initialValue: T): StateFlow<T> =
flowOn(Dispatchers.IO)
.stateIn(
initialValue = initialValue
)
init {
store.init(viewModelScope)
}

fun handleError(throwable: Throwable) {
Logger.exception(throwable)
fun sendAction(action: A) {
store.processAction(action)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package com.stslex.aproselection.core.ui.base.store

import com.stslex.aproselection.core.core.Logger
import com.stslex.aproselection.core.ui.base.store.Store.Action
import com.stslex.aproselection.core.ui.base.store.Store.Event
import com.stslex.aproselection.core.ui.base.store.Store.State
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch

abstract class BaseStoreImpl<S : State, E : Event, A : Action> :
Store<S, E, A>,
StoreImpl<S, E, A> {

private var _scope: CoroutineScope? = null
val scope: CoroutineScope
get() = requireNotNull(_scope)

@Suppress("LeakingThis")
override val state: MutableStateFlow<S> = MutableStateFlow(initialState)
override val event: MutableSharedFlow<E> = MutableSharedFlow()

override fun updateState(update: (S) -> S) {
state.update(update)
}

override fun sendEvent(event: E) {
scope.launch {
this@BaseStoreImpl.event.emit(event)
}
}

override fun init(scope: CoroutineScope) {
_scope = scope
}

fun logError(error: Throwable) {
Logger.exception(error)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.stslex.aproselection.core.ui.base.store

import com.stslex.aproselection.core.ui.base.store.Store.Action
import com.stslex.aproselection.core.ui.base.store.Store.Event
import com.stslex.aproselection.core.ui.base.store.Store.State
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow

interface Store<out S : State, out E : Event, in A : Action> {

val state: StateFlow<S>
val event: SharedFlow<E>

fun processAction(action: A)

fun init(scope: CoroutineScope)

interface State
interface Event
interface Action
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.stslex.aproselection.core.ui.base.store

import com.stslex.aproselection.core.ui.base.store.Store.Action
import com.stslex.aproselection.core.ui.base.store.Store.Event
import com.stslex.aproselection.core.ui.base.store.Store.State

internal interface StoreImpl<S : State, in E : Event, A : Action> {

val initialState: S

fun sendEvent(event: E)

fun updateState(update: (S) -> S)
}
Loading
Loading