Skip to content

Instantly share code, notes, and snippets.

@ashley-figueira
Created August 12, 2020 13:58
Show Gist options
  • Save ashley-figueira/342cddf1fda8215bb8860d3245844d41 to your computer and use it in GitHub Desktop.
Save ashley-figueira/342cddf1fda8215bb8860d3245844d41 to your computer and use it in GitHub Desktop.
A BaseViewModel includes a channel which receives intents and a stateFlow which handles the screen states.
abstract class BaseViewModel<screenState: BaseScreenState, action: BaseAction>(initialState: screenState) : ViewModel() {
val intent = Channel<action>(Channel.UNLIMITED)
protected val _state = MutableStateFlow(initialState)
val state: StateFlow<screenState> get() = _state
init {
viewModelScope.launch {
handleIntents()
}
}
protected abstract suspend fun handleIntents()
}
@ExperimentalCoroutinesApi
@AndroidEntryPoint
class LoginFragment : BaseFragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.loginButton.clicks()
.onEach { vm.intent.send(LoginActions.LoginButtonClicked(binding.emailInput.text?.toString(), binding.passwordInput.text?.toString())) }
.launchIn(lifecycleScope)
vm.state
.onEach { state -> handleState(state) }
.launchIn(lifecycleScope)
}
private fun handleState(state: LoginScreenState) {
when (state) {
LoginScreenState.Idle -> { /* do nothing */ }
LoginScreenState.Success -> {
binding.emailInputLayout.error = null
binding.passwordInputLayout.error = null
(requireActivity() as MainActivity).showBottomNav()
navigateTo(LoginFragmentDirections.actionLoginFragmentToMapsFragment())
}
LoginScreenState.EmailNeedsToBeVerified -> Toast.makeText(requireContext(), getString(R.string.login_screen_email_verification_needed), Toast.LENGTH_LONG).show()
is LoginScreenState.Loading -> {
binding.loginButton.setInvisible(state.isLoading)
binding.loginProgress.setVisible(state.isLoading)
}
is LoginScreenState.Failure -> {
state.errors.forEach {
when (it) {
LoginError.EmailEmpty -> binding.emailInputLayout.error = getString(R.string.login_screen_empty_email_error)
LoginError.EmailInvalid -> binding.emailInputLayout.error = getString(R.string.login_screen_invalid_email_error, binding.emailInput.text.toString())
LoginError.PasswordEmpty -> binding.passwordInputLayout.error = getString(R.string.login_screen_password_error)
LoginError.PasswordInvalid -> binding.passwordInputLayout.error = getString(R.string.login_screen_password_error)
}
}
}
}
}
}
@ExperimentalCoroutinesApi
class LoginViewModel @ViewModelInject constructor(
private val loginWithEmailAndPasswordUseCase: LoginWithEmailAndPasswordUseCase,
@Assisted private val savedStateHandle: SavedStateHandle
) : BaseViewModel<LoginScreenState, LoginActions>(LoginScreenState.Idle) {
override suspend fun handleIntents() {
intent.consumeAsFlow().collect {
when (it) {
is LoginActions.LoginButtonClicked -> {
_state.value = LoginScreenState.Loading(true)
val result = loginWithEmailAndPasswordUseCase(it.email, it.password)
_state.value = LoginScreenState.Loading(false)
_state.value = when (result) {
is LoginResult.Success -> {
when {
result.isEmailVerified -> LoginScreenState.Success
result.isEmailVerified.not() -> LoginScreenState.EmailNeedsToBeVerified
else -> LoginScreenState.Failure(listOf(LoginError.Unknown))
}
}
is LoginResult.Failure -> LoginScreenState.Failure(result.errors)
}
}
}
}
}
}
sealed class LoginActions : BaseAction {
data class LoginButtonClicked(val email: String?, val password: String?) : LoginActions()
}
sealed class LoginScreenState : BaseScreenState {
data class Loading(val isLoading: Boolean) : LoginScreenState()
data class Failure(val errors: List<LoginError>) : LoginScreenState()
object EmailNeedsToBeVerified : LoginScreenState()
object Success : LoginScreenState()
object Idle : LoginScreenState()
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment