AndroidStudio에서 프로젝트 생성할 때 Login Activity 로 생성했더니 꽤나 좋은 샘플이 생성되어 정리해보았다.
- 기본적을 MVVM, ViewModel, dataBinding viewBinding, Repository까지 모두 생성된다!!
- 예전에 이걸로 공부할껄..
플젝 생성 시 제공하는 다른 컨셉??들 프로젝트도 좀 생성해보고, 정기적으로 안드로이드 스튜디오 제공 플젝 코드 좀 봐봐야겠다.
배운점
1. 사용자 이벤트를 VM에서 받아 유효성 체크하여 에러메시지 resID값 등 각 데이터를 설정해 LoginFormState생성하여 LiveData 갱신 -> UI 갱신하는 것이 인상적
2. 결과 클래스를
>> 실제 원격 데이터 요청 및 응답에 사용하는 (Result, LoggedInUser)
>> VM의 응답 수신 및 UI갱신 등의 후처리(LoginResult, LoggedInUserView),
로 나뉘어 만든 것이 인상적이며 (특히 Result가 제네릭으로 값을 보유하는 게▼)
sealed class Result<out T : Any> {
data class Success<out T : Any>(val data: T) : Result<T>()
data class Error(val exception: Exception) : Result<Nothing>()
override fun toString(): String {
return when (this) {
is Success<*> -> "Success[data=$data]"
is Error -> "Error[exception=$exception]"
}
}
}
3. classLoginViewModelFactory:ViewModelProvider.Factory 를 따로 두어
LoginRepository를 생성자에서 파라미터로 가지는 'LoginViewModel을 생성'했다. (파라미터 VM 생성 간편화 언제쯤...ㅠㅠ)
ㄴ> LoginRepository는 생성자에서 LoginDataSource를 파라미터로 받는다.
loginViewModel = ViewModelProvider(this, LoginViewModelFactory())
.get(LoginViewModel::class.java)
class LoginViewModelFactory : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(LoginViewModel::class.java)) {
return LoginViewModel(
loginRepository = LoginRepository(
dataSource = LoginDataSource()
)
) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
}
- EditText의 리스너 등록을 확장함수를 사용했다.
fun EditText.afterTextChanged(afterTextChanged: (String) -> Unit) {
this.addTextChangedListener(object : TextWatcher {
override fun afterTextChanged(editable: Editable?) {
afterTextChanged.invoke(editable.toString())
}
override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {}
})
}
- TextView에 setError()가 있었구나!
- 이메일 체크법
if (username.contains('@')) {
Patterns.EMAIL_ADDRESS.matcher(username).matches()
}
동작정리
[입력값의 유효성 검사 관련 클래스 및 동작]
- 로그인폼 상태 클래스를 아래와 같이 따로 만들고, afterTextChanged 이벤트 올 때마다
data class LoginFormState(
val usernameError: Int? = null,
val passwordError: Int? = null,
val isDataValid: Boolean = false
)
아래 메소드로 유효성 체크 및 에러메시지 resID값 등 _loginForm.value(LoginFormState)에 각 데이터를 설정해 생성한다.
fun loginDataChanged(username: String, password: String) {
if (!isUserNameValid(username)) {
_loginForm.value = LoginFormState(usernameError = R.string.invalid_username)
} else if (!isPasswordValid(password)) {
_loginForm.value = LoginFormState(passwordError = R.string.invalid_password)
} else {
_loginForm.value = LoginFormState(isDataValid = true)
}
}
[데이터 클래스]
LoginResult - 로그인 결과별로 후처리에 필요한 객체만 받도록 LoginResult 데이터클래스로 관리
data class LoginResult(
val success: LoggedInUserView? = null,
val error: Int? = null
)
vs
Result - 성공/실패에 따른 하위 클래스를 가지며 성공 시 응답 데이터를 보유하기 위한 <T>를 보유
sealed class Result<out T : Any> {
data class Success<out T : Any>(val data: T) : Result<T>()
data class Error(val exception: Exception) : Result<Nothing>()
override fun toString(): String {
return when (this) {
is Success<*> -> "Success[data=$data]"
is Error -> "Error[exception=$exception]"
}
}
}
LoggedInUserView - 로그인 성공 시 후처리인 UI 갱신에 필요한 정보 들고 있는 클래스!! / 응답받은 VM에서 생성 / LoginResult에 포함
/**
* User details post authentication that is exposed to the UI
*/
data class LoggedInUserView(
val displayName: String
//... other data fields that may be accessible to the UI
)
vs
LoggedInUser - Result.Success에서 <T> / 응답 성공 시 성공한 사용자정보 대한 LoggedInUser 클래스가 존재한다.
data class LoggedInUser(
val userId: String,
val displayName: String
)
LoginRepository클래스 : 원격 데이터 소스에서 인증 및 사용자 정보를 요청하는 클래스와 로그인 상태 및 사용자 자격 증명 정보의 메모리 내 캐시를 유지 관리합니다.
ㄴ> LoginDataSource를 생성자로 주입받음
ㄴ> 리턴 타입이 명확하다! Result<LoggedInUser>
ㄴ> 성공 시 LoggedInUser클래스 전달해서 LoginRepository의 성공 데이터 저장(캐싱용)
LoginDataSource클래스 : '원격 데이터 소스에서 인증 및 사용자 정보를 요청'를 실제로 처리하고 사용자 정보를 검색하는 클래스입니다.
ㄴ> 사용자 인증 조회 결과별로 Result 클래스 전달(여기에 인증정보 LoggedInUser를 포함)
ㄴ> 리턴 타입이 명확하다! Result<LoggedInUser>
[로그인 처리 과정]
1.요청
(유효성 검사 통과하는 값을 입력하여) 로그인 선택
-> VM의 로그인 처리 메소드 호출하면 > LoginRepository의 원격 로그인 처리 메소드 호출하면 > LoginDataSource 로그인 자격 증명으로 인증 처리한다.
2.응답
-> 이후 LoginDataSource에서는 인증 결과에 따라 Result의 하위 클래스 객체를 생성하고 여기에 인증정보 LoggedInUser를 포함하여 리턴해준다.
class LoginDataSource {
fun login(username: String, password: String): Result<LoggedInUser> {
try {
// TODO: handle loggedInUser authentication
val fakeUser = LoggedInUser(java.util.UUID.randomUUID().toString(), "Jane Doe")
return Result.Success(fakeUser)
} catch (e: Throwable) {
return Result.Error(IOException("Error logging in", e))
}
}
-> LoginRepository는 결과가 성공객체일 때, 내부 LoggedInUser타입 프로퍼티에 캐싱용으로 이 객체를 저장 후 리턴
-> 성공, 실패에 따른 Result 클래스 객체를 리턴받은 VM은 결과별 LoginResult 객체를 생성 후 내부 프로퍼티 MutableLiveData<LoginResult>()를 갱신시킨다.
-> 이 Livedata를 observe한 액티비티에서는 UI를 갱신한다.
[의문]
이 샘플에서 로그인 성공 후 아래 코드는 왜 호출하는지 ??
setResult(Activity.RESULT_OK)
[startActivityForResult(), setResult() 설명]
startActivityForResult()를 통해 액티비티를 생성하면 액티비티가 종료될 때 지정한 requestCode와 함께 onActivityResult() 메소드가 호출됩니다.
이때 종료한 액티비티를 호출한 액티비티로 결과를 반환하기 위해서 setResult()를 호출하여 resultCode와 result Intent를 지정할 수 있습니다.