-
Notifications
You must be signed in to change notification settings - Fork 0
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
base: develop
Are you sure you want to change the base?
[Feat] 7주차 과제 완료 #14
Changes from all commits
2b4fdc0
1c51977
637a829
0f3f9bb
2260732
f72b089
9b408af
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -56,10 +56,6 @@ android { | |
} | ||
} | ||
|
||
hilt { | ||
enableAggregatingTask = false | ||
} | ||
|
||
|
||
dependencies { | ||
|
||
|
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) | ||
} | ||
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 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
package org.sopt.and.presentation.core | ||
|
||
interface UiState |
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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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() | ||
} |
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 | ||
|
@@ -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, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
|
@@ -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가지 형태 | ||
|
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) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
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() | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
BaseViewModel을 두어서 하는 방식 좋네요!!