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

[Feat] 7주차 과제 완료 #14

Open
wants to merge 7 commits into
base: develop
Choose a base branch
from
Open
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
4 changes: 0 additions & 4 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,6 @@ android {
}
}

hilt {
enableAggregatingTask = false
}


dependencies {

Expand Down
7 changes: 2 additions & 5 deletions app/src/main/java/org/sopt/and/presentation/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Scaffold
import androidx.compose.ui.Modifier
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
Expand All @@ -17,7 +16,6 @@ import org.sopt.and.presentation.login.LogInScreen
import org.sopt.and.presentation.main.MainScreen
import org.sopt.and.presentation.navigation.Screen
import org.sopt.and.presentation.signup.SignUpScreen
import org.sopt.and.presentation.signup.UserViewModel
import org.sopt.and.ui.theme.ANDANDROIDTheme

@AndroidEntryPoint
Expand All @@ -29,17 +27,16 @@ class MainActivity : ComponentActivity() {
ANDANDROIDTheme {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
val navController = rememberNavController()
val userViewModel: UserViewModel = viewModel()
NavHost(
navController = navController,
startDestination = Screen.LogIn.route,
modifier = Modifier.padding(innerPadding)
) {
composable(Screen.LogIn.route) {
LogInScreen(navController, userViewModel)
LogInScreen(navController)
}
composable(Screen.SignUp.route) {
SignUpScreen(navController, userViewModel)
SignUpScreen(navController)
}
composable(Screen.MainScreen.route) {
MainScreen(navController)
Expand Down
38 changes: 38 additions & 0 deletions app/src/main/java/org/sopt/and/presentation/core/BaseViewModel.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package org.sopt.and.presentation.core

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch

abstract class BaseViewModel<State : UiState, SideEffect : UiSideEffect, Event : UiEvent> :
ViewModel() {

private val initialState: State by lazy { createInitialState() }
abstract fun createInitialState(): State

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

private val _sideEffect = MutableSharedFlow<SideEffect>()
val sideEffect: SharedFlow<SideEffect> = _sideEffect.asSharedFlow()

fun setState(reduce: State.() -> State) {
_uiState.value = _uiState.value.reduce()
}

fun setSideEffect(builder: () -> SideEffect) {
viewModelScope.launch { _sideEffect.emit(builder()) }
}

fun setEvent(event: Event) {
viewModelScope.launch { handleEvent(event) }
}

protected abstract suspend fun handleEvent(event: Event)
}
Comment on lines +13 to +38
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BaseViewModel을 두어서 하는 방식 좋네요!!

3 changes: 3 additions & 0 deletions app/src/main/java/org/sopt/and/presentation/core/UiEvent.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package org.sopt.and.presentation.core

interface UiEvent
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package org.sopt.and.presentation.core

interface UiSideEffect
3 changes: 3 additions & 0 deletions app/src/main/java/org/sopt/and/presentation/core/UiState.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package org.sopt.and.presentation.core

interface UiState
25 changes: 25 additions & 0 deletions app/src/main/java/org/sopt/and/presentation/login/LogInContract.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package org.sopt.and.presentation.login

import org.sopt.and.presentation.core.UiEvent
import org.sopt.and.presentation.core.UiSideEffect
import org.sopt.and.presentation.core.UiState

// State 정의
data class LogInState(
val username: String = "",
val password: String = "",
val isLoading: Boolean = false,
Comment on lines +9 to +11

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

요기서는 왜 모든 값에 기본값을 제공해주신걸꺼용?

) : UiState

// Event 정의
sealed class LogInEvent : UiEvent {
data class UsernameChanged(val username: String) : LogInEvent()
data class PasswordChanged(val password: String) : LogInEvent()
object LogInClicked : LogInEvent()
}

// SideEffect 정의
sealed class LogInEffect : UiSideEffect {
data class ShowSnackBar(val message: Int) : LogInEffect()
object NavigateToMain : LogInEffect()
}
73 changes: 34 additions & 39 deletions app/src/main/java/org/sopt/and/presentation/login/LogInScreen.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package org.sopt.and.presentation.login

import android.app.Activity
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
Expand All @@ -20,66 +19,63 @@ import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavController
import org.sopt.and.R
import org.sopt.and.data.local.SharedPreferenceManager.saveToken
import org.sopt.and.presentation.login.components.AuthManagement
import org.sopt.and.presentation.signup.SignUpEvent
import org.sopt.and.presentation.signup.components.PasswordField
import org.sopt.and.presentation.signup.UserViewModel
import org.sopt.and.presentation.signup.components.IdHobbyTextField
import org.sopt.and.presentation.signup.components.SocialServiceLogIn
import org.sopt.and.util.showSnackBar

@Composable
fun LogInScreen(
navController: NavController,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

책임 분리를 위해서 navController 자체를 인자로 받기 보다는 이를 이용한 함수를 인자로 받는 게 좋을 것 같아요!
관련한 내용이 합동 세미나 피드백에 있으니 읽어보시면 좋을 것 같습니다 ~

viewModel: UserViewModel = hiltViewModel()
viewModel: LogInViewModel = hiltViewModel()
) {
// textStyle 변경을 위한 textFieldValue 추적
val idState = remember { mutableStateOf(TextFieldValue()) }
val passwordState = remember { mutableStateOf(TextFieldValue()) }
val loginState = viewModel.userLoginState.collectAsState().value
val context = LocalContext.current as Activity
val snackbarHostState = remember { SnackbarHostState() }
val state by viewModel.uiState.collectAsStateWithLifecycle()
val context = LocalContext.current
val snackBarHostState = remember { SnackbarHostState() }
val coroutineScope = rememberCoroutineScope()

LaunchedEffect(loginState) {
when (loginState) {
is LoginState.Loading -> {}
is LoginState.Success -> {
saveToken(loginState.data)
navController.popBackStack("login", inclusive = true)
navController.navigate("mainScreen")
LaunchedEffect(Unit) {
viewModel.sideEffect.collect { effect ->
when (effect) {
is LogInEffect.ShowSnackBar -> {
snackBarHostState.showSnackBar(
context = context,
scope = coroutineScope,
message = effect.message
)
}
is LogInEffect.NavigateToMain -> {
navController.popBackStack("login", inclusive = true)
navController.navigate("mainScreen")
}
}
is LoginState.Failure -> {
snackbarHostState.showSnackbar(
message = context.getString(R.string.log_in_method),
actionLabel = context.getString(R.string.log_in_ok)
)
}
else -> {}
}

}

// SnackBar 구현을 위해 Scaffold 안에 정의
Scaffold(
// 스낵바의 표시 상태 관리
snackbarHost = { SnackbarHost(hostState = snackbarHostState) }) { padding ->
snackbarHost = { SnackbarHost(hostState = snackBarHostState) }) { padding ->

Column(
modifier = Modifier
Expand All @@ -103,32 +99,31 @@ fun LogInScreen(
modifier = Modifier.size(28.dp)
)
}

// id text remeber를 통한 변수 변경
var textId by remember { mutableStateOf("") }

Spacer(modifier = Modifier.weight(2f))
// 윤곽선의 색상 및 두께를 커스텀 가능한 OutLinedTextField
IdHobbyTextField(
valueState = idState,
valueState = state.username,
onValueChange = {
viewModel.setEvent(LogInEvent.UsernameChanged(it))
},
holderText = R.string.log_in_id,
modifier = Modifier.padding(bottom = 8.dp)
)


Spacer(modifier = Modifier.height(10.dp))
// password text remeber를 통한 변수 변경
var textPasswd by remember { mutableStateOf("") }
PasswordField(
passwordState = passwordState, modifier = Modifier.padding(top = 8.dp)
passwordState = state.password,
onValueChange = {
viewModel.setEvent(LogInEvent.PasswordChanged(it))
},
modifier = Modifier.padding(top = 8.dp)
)

Spacer(modifier = Modifier.weight(1f))
Button(
onClick = {
textId = idState.value.text
textPasswd = passwordState.value.text
viewModel.logIn(textId, textPasswd)
viewModel.setEvent(LogInEvent.LogInClicked)
},
modifier = Modifier.fillMaxWidth(), contentPadding = PaddingValues(16.dp),
// ButtonDefaults 의 4가지 형태
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package org.sopt.and.presentation.login

import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
import org.sopt.and.domain.model.UserLoginRequest
import org.sopt.and.domain.repository.UserRepository
import org.sopt.and.presentation.core.BaseViewModel
import javax.inject.Inject
import org.sopt.and.R
import retrofit2.HttpException

@HiltViewModel
class LogInViewModel @Inject constructor(
private val userRepository: UserRepository
) : BaseViewModel<LogInState, LogInEffect, LogInEvent>() {

override fun createInitialState(): LogInState = LogInState()

override suspend fun handleEvent(event: LogInEvent) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

event와 sideEffect의 차이는 뭘까요?

when (event) {
is LogInEvent.UsernameChanged -> {
setState { copy(username = event.username) }
}

is LogInEvent.PasswordChanged -> {
setState { copy(password = event.password) }
}

is LogInEvent.LogInClicked -> {
logIn()
}
}
}


// 로그인 로직
fun logIn() {
setState { copy(isLoading = true) }
viewModelScope.launch {
val state = uiState.value
val result = userRepository.postUserLogin(
Comment on lines +38 to +42

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

여기에 상태 업데이트와 비지니스 로직이 섞여있는 것 같아용 약간 분리해줘도 좋을 것 같은데요??

UserLoginRequest(
username = state.username,
password = state.password
)
)
result.fold(
onSuccess = {
setState { copy(isLoading = false) }
setSideEffect { LogInEffect.NavigateToMain }
},
onFailure = { error ->
val message = if (error is HttpException) {
when (error.code()) {
400 -> R.string.log_in_method
403 -> R.string.sign_up_paswd
else -> R.string.unknown_error
}
} else {
R.string.unknown_error
}
setState { copy(isLoading = false) }
setSideEffect { LogInEffect.ShowSnackBar(message) }
}
)
}
}

}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.navigation.NavController
import org.sopt.and.R
import org.sopt.and.presentation.signup.UserViewModel

@Composable
fun AuthManagement(
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package org.sopt.and.presentation.signup

import org.sopt.and.presentation.core.UiEvent
import org.sopt.and.presentation.core.UiSideEffect
import org.sopt.and.presentation.core.UiState

// State 정의
data class SignUpState(
val username: String = "",
val password: String = "",
val hobby: String = "",
val isLoading: Boolean = false
) : UiState

// Event 정의
sealed class SignUpEvent : UiEvent {
data class UsernameChanged(val username: String) : SignUpEvent()
data class PasswordChanged(val password: String) : SignUpEvent()
data class HobbyChanged(val hobby: String) : SignUpEvent()
object SignUpClicked : SignUpEvent()
}

// SideEffect 정의
sealed class SignUpSideEffect : UiSideEffect {
data class ShowToast(val message: String) : SignUpSideEffect()
object NavigateToLogin : SignUpSideEffect()
}
Loading