From 50c8e14593543243734f1a1a41f71a4b04a90f6d Mon Sep 17 00:00:00 2001 From: Kirill Kamakin Date: Sat, 9 Apr 2022 18:56:25 +0500 Subject: [PATCH] Implement showing authentication progress --- .../kirmanak/mealient/ui/OperationUiState.kt | 38 +++++++++++++++++++ .../ui/auth/AuthenticationFragment.kt | 15 ++++---- .../ui/auth/AuthenticationViewModel.kt | 12 +++--- .../mealient/ui/baseurl/BaseURLFragment.kt | 14 ++++--- .../mealient/ui/baseurl/BaseURLViewModel.kt | 8 ++-- .../res/layout/fragment_authentication.xml | 8 ++++ app/src/main/res/layout/fragment_base_url.xml | 8 ++++ app/src/main/res/values/styles.xml | 7 ++++ 8 files changed, 89 insertions(+), 21 deletions(-) create mode 100644 app/src/main/java/gq/kirmanak/mealient/ui/OperationUiState.kt diff --git a/app/src/main/java/gq/kirmanak/mealient/ui/OperationUiState.kt b/app/src/main/java/gq/kirmanak/mealient/ui/OperationUiState.kt new file mode 100644 index 0000000..faf6f21 --- /dev/null +++ b/app/src/main/java/gq/kirmanak/mealient/ui/OperationUiState.kt @@ -0,0 +1,38 @@ +package gq.kirmanak.mealient.ui + +import android.widget.Button +import android.widget.ProgressBar +import androidx.core.view.isVisible + +sealed class OperationUiState { + + val exceptionOrNull: Throwable? + get() = (this as? Failure)?.exception + + val isSuccess: Boolean + get() = this is Success + + val isProgress: Boolean + get() = this is Progress + + fun updateButtonState(button: Button) { + button.isEnabled = !isProgress + button.isClickable = !isProgress + } + + fun updateProgressState(progressBar: ProgressBar) { + progressBar.isVisible = isProgress + } + + class Initial : OperationUiState() + + class Progress : OperationUiState() + + data class Failure(val exception: Throwable) : OperationUiState() + + data class Success(val value: T) : OperationUiState() + + companion object { + fun fromResult(result: Result) = result.fold({ Success(it) }, { Failure(it) }) + } +} diff --git a/app/src/main/java/gq/kirmanak/mealient/ui/auth/AuthenticationFragment.kt b/app/src/main/java/gq/kirmanak/mealient/ui/auth/AuthenticationFragment.kt index e0d5743..f698e90 100644 --- a/app/src/main/java/gq/kirmanak/mealient/ui/auth/AuthenticationFragment.kt +++ b/app/src/main/java/gq/kirmanak/mealient/ui/auth/AuthenticationFragment.kt @@ -12,6 +12,7 @@ import gq.kirmanak.mealient.R import gq.kirmanak.mealient.data.network.NetworkError import gq.kirmanak.mealient.databinding.FragmentAuthenticationBinding import gq.kirmanak.mealient.extensions.checkIfInputIsEmpty +import gq.kirmanak.mealient.ui.OperationUiState import gq.kirmanak.mealient.ui.activity.MainActivityViewModel import timber.log.Timber @@ -26,7 +27,7 @@ class AuthenticationFragment : Fragment(R.layout.fragment_authentication) { Timber.v("onViewCreated() called with: view = $view, savedInstanceState = $savedInstanceState") binding.button.setOnClickListener { onLoginClicked() } activityViewModel.updateUiState { it.copy(loginButtonVisible = false, titleVisible = true) } - viewModel.authenticationResult.observe(viewLifecycleOwner, ::onAuthenticationResult) + viewModel.uiState.observe(viewLifecycleOwner, ::onUiStateChange) } private fun onLoginClicked(): Unit = with(binding) { @@ -45,22 +46,22 @@ class AuthenticationFragment : Fragment(R.layout.fragment_authentication) { trim = false, ) ?: return - button.isClickable = false viewModel.authenticate(email, pass) } - private fun onAuthenticationResult(result: Result) { - Timber.v("onAuthenticationResult() called with: result = $result") - if (result.isSuccess) { + private fun onUiStateChange(uiState: OperationUiState) = with(binding) { + Timber.v("onUiStateChange() called with: authUiState = $uiState") + if (uiState.isSuccess) { findNavController().popBackStack() return } - binding.passwordInputLayout.error = when (result.exceptionOrNull()) { + passwordInputLayout.error = when (uiState.exceptionOrNull) { is NetworkError.Unauthorized -> getString(R.string.fragment_authentication_credentials_incorrect) else -> null } - binding.button.isClickable = true + uiState.updateButtonState(button) + uiState.updateProgressState(progress) } } \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/ui/auth/AuthenticationViewModel.kt b/app/src/main/java/gq/kirmanak/mealient/ui/auth/AuthenticationViewModel.kt index 3b975f5..70b3738 100644 --- a/app/src/main/java/gq/kirmanak/mealient/ui/auth/AuthenticationViewModel.kt +++ b/app/src/main/java/gq/kirmanak/mealient/ui/auth/AuthenticationViewModel.kt @@ -7,6 +7,7 @@ import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import gq.kirmanak.mealient.data.auth.AuthRepo import gq.kirmanak.mealient.extensions.runCatchingExceptCancel +import gq.kirmanak.mealient.ui.OperationUiState import kotlinx.coroutines.launch import timber.log.Timber import javax.inject.Inject @@ -16,16 +17,15 @@ class AuthenticationViewModel @Inject constructor( private val authRepo: AuthRepo, ) : ViewModel() { - private val _authenticationResult = MutableLiveData>() - val authenticationResult: LiveData> - get() = _authenticationResult + private val _uiState = MutableLiveData>(OperationUiState.Initial()) + val uiState: LiveData> get() = _uiState fun authenticate(email: String, password: String) { Timber.v("authenticate() called with: email = $email, password = $password") + _uiState.value = OperationUiState.Progress() viewModelScope.launch { - _authenticationResult.value = runCatchingExceptCancel { - authRepo.authenticate(email, password) - } + val result = runCatchingExceptCancel { authRepo.authenticate(email, password) } + _uiState.value = OperationUiState.fromResult(result) } } } \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/ui/baseurl/BaseURLFragment.kt b/app/src/main/java/gq/kirmanak/mealient/ui/baseurl/BaseURLFragment.kt index a3f5513..afee238 100644 --- a/app/src/main/java/gq/kirmanak/mealient/ui/baseurl/BaseURLFragment.kt +++ b/app/src/main/java/gq/kirmanak/mealient/ui/baseurl/BaseURLFragment.kt @@ -12,6 +12,7 @@ import gq.kirmanak.mealient.R import gq.kirmanak.mealient.data.network.NetworkError import gq.kirmanak.mealient.databinding.FragmentBaseUrlBinding import gq.kirmanak.mealient.extensions.checkIfInputIsEmpty +import gq.kirmanak.mealient.ui.OperationUiState import gq.kirmanak.mealient.ui.activity.MainActivityViewModel import timber.log.Timber @@ -26,7 +27,7 @@ class BaseURLFragment : Fragment(R.layout.fragment_base_url) { super.onViewCreated(view, savedInstanceState) Timber.v("onViewCreated() called with: view = $view, savedInstanceState = $savedInstanceState") binding.button.setOnClickListener(::onProceedClick) - viewModel.checkURLResult.observe(viewLifecycleOwner, ::onCheckURLResult) + viewModel.uiState.observe(viewLifecycleOwner, ::onUiStateChange) activityViewModel.updateUiState { it.copy(loginButtonVisible = false, titleVisible = true) } } @@ -40,13 +41,13 @@ class BaseURLFragment : Fragment(R.layout.fragment_base_url) { viewModel.saveBaseUrl(url) } - private fun onCheckURLResult(result: Result) { - Timber.v("onCheckURLResult() called with: result = $result") - if (result.isSuccess) { + private fun onUiStateChange(uiState: OperationUiState) = with(binding) { + Timber.v("onUiStateChange() called with: uiState = $uiState") + if (uiState.isSuccess) { findNavController().navigate(BaseURLFragmentDirections.actionBaseURLFragmentToRecipesFragment()) return } - binding.urlInputLayout.error = when (val exception = result.exceptionOrNull()) { + urlInputLayout.error = when (val exception = uiState.exceptionOrNull) { is NetworkError.NoServerConnection -> getString(R.string.fragment_base_url_no_connection) is NetworkError.NotMealie -> getString(R.string.fragment_base_url_unexpected_response) is NetworkError.MalformedUrl -> { @@ -56,5 +57,8 @@ class BaseURLFragment : Fragment(R.layout.fragment_base_url) { null -> null else -> getString(R.string.fragment_base_url_unknown_error) } + + uiState.updateButtonState(button) + uiState.updateProgressState(progress) } } \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/ui/baseurl/BaseURLViewModel.kt b/app/src/main/java/gq/kirmanak/mealient/ui/baseurl/BaseURLViewModel.kt index 38a8257..2c717b8 100644 --- a/app/src/main/java/gq/kirmanak/mealient/ui/baseurl/BaseURLViewModel.kt +++ b/app/src/main/java/gq/kirmanak/mealient/ui/baseurl/BaseURLViewModel.kt @@ -8,6 +8,7 @@ import dagger.hilt.android.lifecycle.HiltViewModel import gq.kirmanak.mealient.data.baseurl.BaseURLStorage import gq.kirmanak.mealient.data.baseurl.VersionDataSource import gq.kirmanak.mealient.extensions.runCatchingExceptCancel +import gq.kirmanak.mealient.ui.OperationUiState import kotlinx.coroutines.launch import timber.log.Timber import javax.inject.Inject @@ -18,11 +19,12 @@ class BaseURLViewModel @Inject constructor( private val versionDataSource: VersionDataSource, ) : ViewModel() { - private val _checkURLResult = MutableLiveData>() - val checkURLResult: LiveData> get() = _checkURLResult + private val _uiState = MutableLiveData>(OperationUiState.Initial()) + val uiState: LiveData> get() = _uiState fun saveBaseUrl(baseURL: String) { Timber.v("saveBaseUrl() called with: baseURL = $baseURL") + _uiState.value = OperationUiState.Progress() val hasPrefix = ALLOWED_PREFIXES.any { baseURL.startsWith(it) } val url = baseURL.takeIf { hasPrefix } ?: WITH_PREFIX_FORMAT.format(baseURL) viewModelScope.launch { checkBaseURL(url) } @@ -36,7 +38,7 @@ class BaseURLViewModel @Inject constructor( baseURLStorage.storeBaseURL(baseURL) } Timber.i("checkBaseURL: result is $result") - _checkURLResult.value = result + _uiState.value = OperationUiState.fromResult(result) } companion object { diff --git a/app/src/main/res/layout/fragment_authentication.xml b/app/src/main/res/layout/fragment_authentication.xml index 098f119..66cd7e7 100644 --- a/app/src/main/res/layout/fragment_authentication.xml +++ b/app/src/main/res/layout/fragment_authentication.xml @@ -6,6 +6,14 @@ android:layout_height="match_parent" tools:context=".ui.auth.AuthenticationFragment"> + + + + @null @drawable/recipe_info_background + + \ No newline at end of file