Merge pull request #42 from kirmanak/auth-progress
Implement showing authentication progress
This commit is contained in:
@@ -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) })
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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>
|
||||||
Reference in New Issue
Block a user