https://superohinsung.tistory.com/252
지난 번에 로그인 화면에 디자인에 대해서 글을 작성하였고, 이번에는 본격적으로 기능에 대해서 작성을 할 생각입니다.
LoginActivity
액티비티에서는 우선 Ui 작동에 필요한 기능들을 포함하고 있습니다.
companion object {
fun getIntent(context: Context): Intent {
return Intent(context, LoginActivity::class.java)
}
}
private val viewModel: LoginViewModel by viewModels()
private lateinit var binding: ActivityLoginBinding
companion object 블록
- companion object는 Kotlin에서 사용되는 특수한 객체로, 클래스 내에서 정적 메서드나 상수를 선언하는데 사용됩니다.
- getIntet 함수는 Context를 매개변수로 받고 Intent를 객체로 반환하는 정적 메서드입니다.
- 이 함수는 LoginActivity의 인스턴스를 생성하지 않고도 다른 곳에서 LoginActivity 클래스의 Intent를 생성할 수 있게 해줍니다.
private val viewModel: LoginViewModel by viewModels()
- 이 코드는 viewModels() 확장 함수를 사용하여 LoginView 인스턴스를 생성하고 초기화합니다.
- viewModel은 LoginActivity클래스 내에서 사용할 수 있는 LoginViewModel의 인스터스를 가리킵니다.
private lateinit var binding: ActivityLoginBinding
- binding 변수는 ActivityLoginBinding 유형의 지연 초기화 프로퍼티로 선언됩니다.
- ActivityLoginBinding은 데이터 바인딩을 통해 레아이웃의 뷰 컴포넌트와 상호작용하기 위해 사용되는 클래스입니다.
- lateinit 키워드는 나중에 binding 변수로 초기화될 것임을 나타냅니다. 초기화하기 전에 사용하면 예외가 발생될 수 있습니다.
WindowCompat.setDecorFitsSystemWindows(window, true)
if (viewModel.signedIn) {
navigateToHomeView()
}
initEventListeners()
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.uiState.collect(::updateUi)
}
}
WindowCompat.setDecorFitsSystemWindows(window, true)
- 이 코드는 시스템 창을 액티비티 레이아웃에 맞추도록 설정하는 데 사용됩니다.
- WindowCompat.setDecorFitsSystemWindows() 함수는 창(window)에 대한 시스템 창 옵션을 설정하는 데 사용됩니다. 레이아웃이 상태 표시줄이나 내비게이션 바와 겹치지 않도록 설정하는 데 유용합니다.
if (viewModel.signedIn) { navigateToHomeView() }
- viewModel.signedIn이 true일 때, 홈 화면으로 이동하는 로직이 실행됩니다.
- 이 부분은 viewModel 객체의 signedIn 속성을 확인하고, 사용자가 로그인했을 경우 navigateToHomeView() 함수를 호출하는 조건문입니다.
initEventListeners()
- 이 함수는 화면의 이벤트 리스너를 설정하거나 초기화 작업을 수행하는 함수입니다. 아래에 따로 설명하겠습니다.
lifecycleScope.launch { ... }
- 이 부분은 Kotlin의 코루틴을 사용하여 비동기 작업을 수행하는 부분입니다.
- lifecycleScope는 Android의 수명주기를 고려한 코루틴 범위를 제공합니다.
- repeatOnLifecycle(Lifecycle.State.STARTED)은 액티비티가 시작된 상태인 동안에만 코드 블록을 반복 실행합니다.
- viewModel.uiState.collect(::updateUi)는 viewModel에서 제공되는 uiState Flow를 수신하고, updateUi 함수를 호출하여 UI 업데이트를 처리하는 코드입니다.
private fun initEventListeners() = with(binding) {
email.addTextChangedListener {
if (it != null) {
viewModel.updateEmail(it.toString())
}
}
password.addTextChangedListener {
if (it != null) {
viewModel.updatePassword(it.toString())
}
}
signInButton.setOnClickListener {
viewModel.signIn()
}
signUpText.setOnClickListener {
navigateToSignUpView()
}
}
with(binding) { ... }
- with 함수는 특정 객체(binding 객체)에 대해 수행할 일련의 작업을 그룹화합니다. 이렇게 하면 코드 중복을 줄이고 가독성을 향상시킵니다. binding 객체의 메서드를 호출하는 모든 코드는 중괄호 내에 있습니다.
email.addTextChangedListener { ... }
- email은 레이아웃에서 사용자의 이메일을 입력하는 EditText 뷰를 나타냅니다.
- addTextChangedListener 함수는 EditText의 텍스트가 변경될 때 호출되는 리스너를 추가합니다.
- 리스너는 텍스트가 변경될 때 viewModel.updateEmail(it.toString())를 호출하여 viewModel의 이메일 업데이트 메서드를 호출합니다. it은 변경된 텍스트를 나타냅니다.
password.addTextChangedListener { ... }
- password는 비밀번호를 입력하는 EditText 뷰를 나타냅니다.
- addTextChangedListener 함수를 사용하여 텍스트 변경 리스너를 추가하고, 텍스트가 변경될 때 viewModel.updatePassword(it.toString())를 호출하여 viewModel의 비밀번호 업데이트 메서드를 호출합니다.
signInButton.setOnClickListener { ... }
- signInButton은 로그인 버튼을 나타냅니다.
- setOnClickListener 함수를 사용하여 버튼을 클릭할 때 호출될 리스너를 설정하고, 클릭 시 viewModel.signIn()을 호출하여 로그인을 수행합니다.
signUpText.setOnClickListener { ... }
- signUpText는 "회원가입" 텍스트를 나타냅니다.
- setOnClickListener 함수를 사용하여 "회원가입" 텍스트를 클릭할 때 호출될 리스너를 설정하고, 클릭 시 navigateToSignUpView() 함수를 호출하여 회원가입 화면으로 이동합니다.
private fun updateUi(uiState: LoginUiState) {
binding.emailInputLayout.apply {
isErrorEnabled = uiState.showEmailError
error = if (uiState.showEmailError) {
context.getString(R.string.email_is_not_valid)
} else null
}
binding.passwordInputLayout.apply {
isErrorEnabled = uiState.showPasswordError
error = if (uiState.showPasswordError) {
context.getString(R.string.password_is_not_valid)
} else null
}
if (uiState.successToSignIn) {
onSuccessToLogin()
}
if (uiState.userMessage != null) {
showSnackBar(uiState.userMessage)
viewModel.userMessageShown()
}
binding.signInButton.apply {
isEnabled = uiState.isInputValid && !uiState.isLoading
setText(if (uiState.isLoading) R.string.loading else R.string.login)
}
}
이 함수는 LoginUiState 객체를 기반으로 UI 요소를 동적으로 업데이트합니다.
binding.emailInputLayout 및 binding.passwordInputLayout
- 이 두 줄은 binding 객체를 통해 화면에 표시되는 이메일 및 비밀번호 입력란의 상태를 처리합니다.
- apply 함수를 사용하여 InputLayout 뷰의 속성을 변경합니다.
- isErrorEnabled은 오류 메시지를 표시할지 여부를 결정하며, uiState.showEmailError 및 uiState.showPasswordError 값에 따라 설정됩니다.
- error 속성은 오류 메시지를 설정합니다. showEmailError 또는 showPasswordError가 true일 경우, 오류 메시지가 표시되며, 그렇지 않으면 null로 설정됩니다.
if (uiState.successToSignIn) { onSuccessToLogin() }
- successToSignIn이 true인 경우, onSuccessToLogin() 함수가 호출되어 로그인 성공 시 필요한 동작을 수행합니다.
if (uiState.userMessage != null)
- userMessage가 null이 아닌 경우, 사용자에게 메시지를 보여주는 스낵바 또는 다른 형태의 UI 메시지를 표시합니다.
- 메시지를 표시한 후, viewModel.userMessageShown() 함수가 호출되어 이 메시지가 표시되었음을 알립니다.
binding.signInButton
- 이 부분은 로그인 버튼의 상태를 관리합니다.
- isEnabled 속성은 uiState.isInputValid가 true이고 uiState.isLoading이 false일 때만 버튼을 활성화합니다.
- setText 함수는 버튼의 텍스트를 설정하며, uiState.isLoading이 true이면 "로딩 중"을, 그렇지 않으면 "로그인"을 표시합니다.
private fun onSuccessToLogin() {
viewModel.checkUserInfoExists { exists ->
if (exists) {
val sharedPreferences = getSharedPreferences(
getString(R.string.preference_file_key),
Context.MODE_PRIVATE
)
sharedPreferences.edit()
.putBoolean(getString(R.string.prefs_has_user_info), true)
.apply()
navigateToHomeView()
}
}
}
private fun showSnackBar(message: String) {
Snackbar.make(binding.root, message, Snackbar.LENGTH_LONG).show()
}
private fun navigateToSignUpView() {
val intent = SignUpActivity.getIntent(this)
startActivity(intent)
}
private fun navigateToHomeView() {
val intent = HomeActivity.getIntent(this).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_TASK_ON_HOME
}
startActivity(intent)
finish()
}
private fun onSuccessToLogin()
- 이 함수는 로그인 성공 시 호출됩니다.
- viewModel.checkUserInfoExists { exists -> ... }를 사용하여 사용자 정보의 존재 여부를 확인합니다.
- 사용자 정보가 존재하면 로컬 저장소(SharedPreferences)에 해당 정보를 저장하고(putBoolean) 저장소를 업데이트합니다.
- 그런 다음 navigateToHomeView() 함수를 호출하여 홈 화면으로 이동합니다.
private fun showSnackBar(message: String)
- 이 함수는 화면에 스낵바 메시지를 표시하는 데 사용됩니다.
- Snackbar.make(binding.root, message, Snackbar.LENGTH_LONG).show()를 호출하여 binding.root (레이아웃의 루트 뷰)에 지정된 메시지(message)를 포함한 스낵바를 생성하고 표시합니다.
private fun navigateToSignUpView()
- 이 함수는 회원가입 화면으로 이동하는 데 사용됩니다.
- SignUpActivity.getIntent(this)를 호출하여 SignUpActivity의 Intent를 생성하고, 이 Intent를 사용하여 새로운 액티비티를 시작합니다.
private fun navigateToHomeView()
- 이 함수는 홈 화면으로 이동하는 데 사용됩니다.
- HomeActivity.getIntent(this)를 호출하여 HomeActivity의 Intent를 생성하고, 이 Intent에 특정 플래그(flags)를 설정하여 새로운 액티비티를 시작합니다.
- 또한 현재 액티비티를 finish()를 통해 종료하여 뒤로 가기 버튼을 사용하여 로그인 화면으로 되돌아갈 수 없도록 합니다.
LoginActivity 전체코드
class LoginActivity : AppCompatActivity() {
companion object {
fun getIntent(context: Context): Intent {
return Intent(context, LoginActivity::class.java)
}
}
private val viewModel: LoginViewModel by viewModels()
private lateinit var binding: ActivityLoginBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityLoginBinding.inflate(layoutInflater)
setContentView(binding.root)
WindowCompat.setDecorFitsSystemWindows(window, true)
if (viewModel.signedIn) {
navigateToHomeView()
}
initEventListeners()
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.uiState.collect(::updateUi)
}
}
}
private fun initEventListeners() = with(binding) {
email.addTextChangedListener {
if (it != null) {
viewModel.updateEmail(it.toString())
}
}
password.addTextChangedListener {
if (it != null) {
viewModel.updatePassword(it.toString())
}
}
signInButton.setOnClickListener {
viewModel.signIn()
}
signUpText.setOnClickListener {
navigateToSignUpView()
}
}
private fun updateUi(uiState: LoginUiState) {
binding.emailInputLayout.apply {
isErrorEnabled = uiState.showEmailError
error = if (uiState.showEmailError) {
context.getString(R.string.email_is_not_valid)
} else null
}
binding.passwordInputLayout.apply {
isErrorEnabled = uiState.showPasswordError
error = if (uiState.showPasswordError) {
context.getString(R.string.password_is_not_valid)
} else null
}
if (uiState.successToSignIn) {
onSuccessToLogin()
}
if (uiState.userMessage != null) {
showSnackBar(uiState.userMessage)
viewModel.userMessageShown()
}
binding.signInButton.apply {
isEnabled = uiState.isInputValid && !uiState.isLoading
setText(if (uiState.isLoading) R.string.loading else R.string.login)
}
}
private fun onSuccessToLogin() {
viewModel.checkUserInfoExists { exists ->
if (exists) {
val sharedPreferences = getSharedPreferences(
getString(R.string.preference_file_key),
Context.MODE_PRIVATE
)
sharedPreferences.edit()
.putBoolean(getString(R.string.prefs_has_user_info), true)
.apply()
navigateToHomeView()
}
}
}
private fun showSnackBar(message: String) {
Snackbar.make(binding.root, message, Snackbar.LENGTH_LONG).show()
}
private fun navigateToSignUpView() {
val intent = SignUpActivity.getIntent(this)
startActivity(intent)
}
private fun navigateToHomeView() {
val intent = HomeActivity.getIntent(this).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_TASK_ON_HOME
}
startActivity(intent)
finish()
}
}
LoginUiState와 LoginViewModel은 다음에 작성하도록 하겠습니다.
'Android > Study' 카테고리의 다른 글
[Android] RelativeLayout(상대적 레이아웃) 정리 (0) | 2023.12.23 |
---|---|
[Android] LinearLayout(선형 레이아웃) 정리 (0) | 2023.12.23 |
[Android] 로그인 화면 구성 및 디자인 (0) | 2023.10.25 |
[Android] Mock을 사용해 Android test하기 (0) | 2023.10.04 |
[Android] ViewBinding(뷰 바인딩) 정리 (0) | 2023.09.23 |