Implement showing authentication progress

This commit is contained in:
Kirill Kamakin
2022-04-09 18:56:25 +05:00
parent 4944cd3cf3
commit 50c8e14593
8 changed files with 89 additions and 21 deletions

View File

@@ -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<T> {
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<T> : OperationUiState<T>()
class Progress<T> : OperationUiState<T>()
data class Failure<T>(val exception: Throwable) : OperationUiState<T>()
data class Success<T>(val value: T) : OperationUiState<T>()
companion object {
fun <T> fromResult(result: Result<T>) = result.fold({ Success(it) }, { Failure(it) })
}
}

View File

@@ -12,6 +12,7 @@ import gq.kirmanak.mealient.R
import gq.kirmanak.mealient.data.network.NetworkError import gq.kirmanak.mealient.data.network.NetworkError
import gq.kirmanak.mealient.databinding.FragmentAuthenticationBinding import gq.kirmanak.mealient.databinding.FragmentAuthenticationBinding
import gq.kirmanak.mealient.extensions.checkIfInputIsEmpty import gq.kirmanak.mealient.extensions.checkIfInputIsEmpty
import gq.kirmanak.mealient.ui.OperationUiState
import gq.kirmanak.mealient.ui.activity.MainActivityViewModel import gq.kirmanak.mealient.ui.activity.MainActivityViewModel
import timber.log.Timber import timber.log.Timber
@@ -26,7 +27,7 @@ class AuthenticationFragment : Fragment(R.layout.fragment_authentication) {
Timber.v("onViewCreated() called with: view = $view, savedInstanceState = $savedInstanceState") Timber.v("onViewCreated() called with: view = $view, savedInstanceState = $savedInstanceState")
binding.button.setOnClickListener { onLoginClicked() } binding.button.setOnClickListener { onLoginClicked() }
activityViewModel.updateUiState { it.copy(loginButtonVisible = false, titleVisible = true) } activityViewModel.updateUiState { it.copy(loginButtonVisible = false, titleVisible = true) }
viewModel.authenticationResult.observe(viewLifecycleOwner, ::onAuthenticationResult) viewModel.uiState.observe(viewLifecycleOwner, ::onUiStateChange)
} }
private fun onLoginClicked(): Unit = with(binding) { private fun onLoginClicked(): Unit = with(binding) {
@@ -45,22 +46,22 @@ class AuthenticationFragment : Fragment(R.layout.fragment_authentication) {
trim = false, trim = false,
) ?: return ) ?: return
button.isClickable = false
viewModel.authenticate(email, pass) viewModel.authenticate(email, pass)
} }
private fun onAuthenticationResult(result: Result<Unit>) { private fun onUiStateChange(uiState: OperationUiState<Unit>) = with(binding) {
Timber.v("onAuthenticationResult() called with: result = $result") Timber.v("onUiStateChange() called with: authUiState = $uiState")
if (result.isSuccess) { if (uiState.isSuccess) {
findNavController().popBackStack() findNavController().popBackStack()
return return
} }
binding.passwordInputLayout.error = when (result.exceptionOrNull()) { passwordInputLayout.error = when (uiState.exceptionOrNull) {
is NetworkError.Unauthorized -> getString(R.string.fragment_authentication_credentials_incorrect) is NetworkError.Unauthorized -> getString(R.string.fragment_authentication_credentials_incorrect)
else -> null else -> null
} }
binding.button.isClickable = true uiState.updateButtonState(button)
uiState.updateProgressState(progress)
} }
} }

View File

@@ -7,6 +7,7 @@ import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import gq.kirmanak.mealient.data.auth.AuthRepo import gq.kirmanak.mealient.data.auth.AuthRepo
import gq.kirmanak.mealient.extensions.runCatchingExceptCancel import gq.kirmanak.mealient.extensions.runCatchingExceptCancel
import gq.kirmanak.mealient.ui.OperationUiState
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@@ -16,16 +17,15 @@ class AuthenticationViewModel @Inject constructor(
private val authRepo: AuthRepo, private val authRepo: AuthRepo,
) : ViewModel() { ) : ViewModel() {
private val _authenticationResult = MutableLiveData<Result<Unit>>() private val _uiState = MutableLiveData<OperationUiState<Unit>>(OperationUiState.Initial())
val authenticationResult: LiveData<Result<Unit>> val uiState: LiveData<OperationUiState<Unit>> get() = _uiState
get() = _authenticationResult
fun authenticate(email: String, password: String) { fun authenticate(email: String, password: String) {
Timber.v("authenticate() called with: email = $email, password = $password") Timber.v("authenticate() called with: email = $email, password = $password")
_uiState.value = OperationUiState.Progress()
viewModelScope.launch { viewModelScope.launch {
_authenticationResult.value = runCatchingExceptCancel { val result = runCatchingExceptCancel { authRepo.authenticate(email, password) }
authRepo.authenticate(email, password) _uiState.value = OperationUiState.fromResult(result)
}
} }
} }
} }

View File

@@ -12,6 +12,7 @@ import gq.kirmanak.mealient.R
import gq.kirmanak.mealient.data.network.NetworkError import gq.kirmanak.mealient.data.network.NetworkError
import gq.kirmanak.mealient.databinding.FragmentBaseUrlBinding import gq.kirmanak.mealient.databinding.FragmentBaseUrlBinding
import gq.kirmanak.mealient.extensions.checkIfInputIsEmpty import gq.kirmanak.mealient.extensions.checkIfInputIsEmpty
import gq.kirmanak.mealient.ui.OperationUiState
import gq.kirmanak.mealient.ui.activity.MainActivityViewModel import gq.kirmanak.mealient.ui.activity.MainActivityViewModel
import timber.log.Timber import timber.log.Timber
@@ -26,7 +27,7 @@ class BaseURLFragment : Fragment(R.layout.fragment_base_url) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
Timber.v("onViewCreated() called with: view = $view, savedInstanceState = $savedInstanceState") Timber.v("onViewCreated() called with: view = $view, savedInstanceState = $savedInstanceState")
binding.button.setOnClickListener(::onProceedClick) binding.button.setOnClickListener(::onProceedClick)
viewModel.checkURLResult.observe(viewLifecycleOwner, ::onCheckURLResult) viewModel.uiState.observe(viewLifecycleOwner, ::onUiStateChange)
activityViewModel.updateUiState { it.copy(loginButtonVisible = false, titleVisible = true) } activityViewModel.updateUiState { it.copy(loginButtonVisible = false, titleVisible = true) }
} }
@@ -40,13 +41,13 @@ class BaseURLFragment : Fragment(R.layout.fragment_base_url) {
viewModel.saveBaseUrl(url) viewModel.saveBaseUrl(url)
} }
private fun onCheckURLResult(result: Result<Unit>) { private fun onUiStateChange(uiState: OperationUiState<Unit>) = with(binding) {
Timber.v("onCheckURLResult() called with: result = $result") Timber.v("onUiStateChange() called with: uiState = $uiState")
if (result.isSuccess) { if (uiState.isSuccess) {
findNavController().navigate(BaseURLFragmentDirections.actionBaseURLFragmentToRecipesFragment()) findNavController().navigate(BaseURLFragmentDirections.actionBaseURLFragmentToRecipesFragment())
return 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.NoServerConnection -> getString(R.string.fragment_base_url_no_connection)
is NetworkError.NotMealie -> getString(R.string.fragment_base_url_unexpected_response) is NetworkError.NotMealie -> getString(R.string.fragment_base_url_unexpected_response)
is NetworkError.MalformedUrl -> { is NetworkError.MalformedUrl -> {
@@ -56,5 +57,8 @@ class BaseURLFragment : Fragment(R.layout.fragment_base_url) {
null -> null null -> null
else -> getString(R.string.fragment_base_url_unknown_error) else -> getString(R.string.fragment_base_url_unknown_error)
} }
uiState.updateButtonState(button)
uiState.updateProgressState(progress)
} }
} }

View File

@@ -8,6 +8,7 @@ import dagger.hilt.android.lifecycle.HiltViewModel
import gq.kirmanak.mealient.data.baseurl.BaseURLStorage import gq.kirmanak.mealient.data.baseurl.BaseURLStorage
import gq.kirmanak.mealient.data.baseurl.VersionDataSource import gq.kirmanak.mealient.data.baseurl.VersionDataSource
import gq.kirmanak.mealient.extensions.runCatchingExceptCancel import gq.kirmanak.mealient.extensions.runCatchingExceptCancel
import gq.kirmanak.mealient.ui.OperationUiState
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@@ -18,11 +19,12 @@ class BaseURLViewModel @Inject constructor(
private val versionDataSource: VersionDataSource, private val versionDataSource: VersionDataSource,
) : ViewModel() { ) : ViewModel() {
private val _checkURLResult = MutableLiveData<Result<Unit>>() private val _uiState = MutableLiveData<OperationUiState<Unit>>(OperationUiState.Initial())
val checkURLResult: LiveData<Result<Unit>> get() = _checkURLResult val uiState: LiveData<OperationUiState<Unit>> get() = _uiState
fun saveBaseUrl(baseURL: String) { fun saveBaseUrl(baseURL: String) {
Timber.v("saveBaseUrl() called with: baseURL = $baseURL") Timber.v("saveBaseUrl() called with: baseURL = $baseURL")
_uiState.value = OperationUiState.Progress()
val hasPrefix = ALLOWED_PREFIXES.any { baseURL.startsWith(it) } val hasPrefix = ALLOWED_PREFIXES.any { baseURL.startsWith(it) }
val url = baseURL.takeIf { hasPrefix } ?: WITH_PREFIX_FORMAT.format(baseURL) val url = baseURL.takeIf { hasPrefix } ?: WITH_PREFIX_FORMAT.format(baseURL)
viewModelScope.launch { checkBaseURL(url) } viewModelScope.launch { checkBaseURL(url) }
@@ -36,7 +38,7 @@ class BaseURLViewModel @Inject constructor(
baseURLStorage.storeBaseURL(baseURL) baseURLStorage.storeBaseURL(baseURL)
} }
Timber.i("checkBaseURL: result is $result") Timber.i("checkBaseURL: result is $result")
_checkURLResult.value = result _uiState.value = OperationUiState.fromResult(result)
} }
companion object { companion object {

View File

@@ -6,6 +6,14 @@
android:layout_height="match_parent" android:layout_height="match_parent"
tools:context=".ui.auth.AuthenticationFragment"> tools:context=".ui.auth.AuthenticationFragment">
<com.google.android.material.progressindicator.LinearProgressIndicator
android:id="@+id/progress"
style="@style/IndeterminateProgress"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:visibility="visible" />
<com.google.android.material.textfield.TextInputLayout <com.google.android.material.textfield.TextInputLayout
android:id="@+id/email_input_layout" android:id="@+id/email_input_layout"
style="@style/SmallMarginTextInputLayoutStyle" style="@style/SmallMarginTextInputLayoutStyle"

View File

@@ -6,6 +6,14 @@
android:layout_height="match_parent" android:layout_height="match_parent"
tools:context=".ui.baseurl.BaseURLFragment"> tools:context=".ui.baseurl.BaseURLFragment">
<com.google.android.material.progressindicator.LinearProgressIndicator
android:id="@+id/progress"
style="@style/IndeterminateProgress"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:visibility="visible" />
<com.google.android.material.textfield.TextInputLayout <com.google.android.material.textfield.TextInputLayout
android:id="@+id/url_input_layout" android:id="@+id/url_input_layout"
style="@style/SmallMarginTextInputLayoutStyle" style="@style/SmallMarginTextInputLayoutStyle"

View File

@@ -28,4 +28,11 @@
<item name="shapeAppearanceOverlay">@null</item> <item name="shapeAppearanceOverlay">@null</item>
<item name="android:background">@drawable/recipe_info_background</item> <item name="android:background">@drawable/recipe_info_background</item>
</style> </style>
<style name="IndeterminateProgress">
<item name="android:layout_width">0dp</item>
<item name="android:layout_height">wrap_content</item>
<item name="android:indeterminate">true</item>
<item name="android:visibility">gone</item>
</style>
</resources> </resources>