Fix handling result in BaseURLFragment and AuthenticationFragment

This commit is contained in:
Kirill Kamakin
2022-04-08 21:19:05 +05:00
parent 5b56ff9932
commit d2029438d7
10 changed files with 77 additions and 45 deletions

View File

@@ -17,7 +17,7 @@
tools:ignore="UnusedAttribute" tools:ignore="UnusedAttribute"
android:theme="@style/Theme.Mealient"> android:theme="@style/Theme.Mealient">
<activity <activity
android:name=".MainActivity" android:name=".ui.activity.MainActivity"
android:exported="true"> android:exported="true">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />

View File

@@ -1,4 +1,4 @@
package gq.kirmanak.mealient package gq.kirmanak.mealient.ui.activity
import android.os.Bundle import android.os.Bundle
import android.view.Menu import android.view.Menu
@@ -10,17 +10,17 @@ import androidx.navigation.findNavController
import com.google.android.material.shape.CornerFamily import com.google.android.material.shape.CornerFamily
import com.google.android.material.shape.MaterialShapeDrawable import com.google.android.material.shape.MaterialShapeDrawable
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import gq.kirmanak.mealient.R
import gq.kirmanak.mealient.databinding.MainActivityBinding import gq.kirmanak.mealient.databinding.MainActivityBinding
import gq.kirmanak.mealient.ui.auth.AuthenticationState import gq.kirmanak.mealient.ui.auth.AuthenticationState
import gq.kirmanak.mealient.ui.auth.AuthenticationState.AUTHORIZED import gq.kirmanak.mealient.ui.auth.AuthenticationState.AUTHORIZED
import gq.kirmanak.mealient.ui.auth.AuthenticationState.UNAUTHORIZED import gq.kirmanak.mealient.ui.auth.AuthenticationState.UNAUTHORIZED
import gq.kirmanak.mealient.ui.auth.AuthenticationViewModel
import timber.log.Timber import timber.log.Timber
@AndroidEntryPoint @AndroidEntryPoint
class MainActivity : AppCompatActivity() { class MainActivity : AppCompatActivity() {
private lateinit var binding: MainActivityBinding private lateinit var binding: MainActivityBinding
private val authViewModel by viewModels<AuthenticationViewModel>() private val viewModel by viewModels<MainActivityViewModel>()
private var lastAuthenticationState: AuthenticationState? = null private var lastAuthenticationState: AuthenticationState? = null
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
@@ -53,7 +53,7 @@ class MainActivity : AppCompatActivity() {
private fun listenToAuthStatuses() { private fun listenToAuthStatuses() {
Timber.v("listenToAuthStatuses() called") Timber.v("listenToAuthStatuses() called")
authViewModel.authenticationStateLive.observe(this, ::onAuthStateUpdate) viewModel.authenticationStateLive.observe(this, ::onAuthStateUpdate)
} }
private fun onAuthStateUpdate(authState: AuthenticationState) { private fun onAuthStateUpdate(authState: AuthenticationState) {
@@ -78,7 +78,7 @@ class MainActivity : AppCompatActivity() {
true true
} }
R.id.logout -> { R.id.logout -> {
authViewModel.logout() viewModel.logout()
true true
} }
else -> super.onOptionsItemSelected(item) else -> super.onOptionsItemSelected(item)

View File

@@ -0,0 +1,34 @@
package gq.kirmanak.mealient.ui.activity
import androidx.lifecycle.ViewModel
import androidx.lifecycle.asLiveData
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import gq.kirmanak.mealient.data.auth.AuthRepo
import gq.kirmanak.mealient.ui.auth.AuthenticationState
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
@HiltViewModel
class MainActivityViewModel @Inject constructor(
private val authRepo: AuthRepo,
) : ViewModel() {
private val showLoginButtonFlow = MutableStateFlow(false)
var showLoginButton: Boolean by showLoginButtonFlow::value
private val authenticationStateFlow = combine(
showLoginButtonFlow,
authRepo.isAuthorizedFlow,
AuthenticationState::determineState
)
val authenticationStateLive = authenticationStateFlow.asLiveData()
fun logout() {
Timber.v("logout() called")
viewModelScope.launch { authRepo.logout() }
}
}

View File

@@ -4,7 +4,7 @@ import android.os.Bundle
import android.view.View import android.view.View
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import by.kirich1409.viewbindingdelegate.viewBinding import by.kirich1409.viewbindingdelegate.viewBinding
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
@@ -12,13 +12,12 @@ 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.extensions.launchWithViewLifecycle
import timber.log.Timber import timber.log.Timber
@AndroidEntryPoint @AndroidEntryPoint
class AuthenticationFragment : Fragment(R.layout.fragment_authentication) { class AuthenticationFragment : Fragment(R.layout.fragment_authentication) {
private val binding by viewBinding(FragmentAuthenticationBinding::bind) private val binding by viewBinding(FragmentAuthenticationBinding::bind)
private val viewModel by activityViewModels<AuthenticationViewModel>() private val viewModel by viewModels<AuthenticationViewModel>()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
@@ -26,6 +25,7 @@ class AuthenticationFragment : Fragment(R.layout.fragment_authentication) {
binding.button.setOnClickListener { onLoginClicked() } binding.button.setOnClickListener { onLoginClicked() }
(requireActivity() as? AppCompatActivity)?.supportActionBar?.title = (requireActivity() as? AppCompatActivity)?.supportActionBar?.title =
getString(R.string.app_name) getString(R.string.app_name)
viewModel.authenticationResult.observe(viewLifecycleOwner, ::onAuthenticationResult)
} }
private fun onLoginClicked(): Unit = with(binding) { private fun onLoginClicked(): Unit = with(binding) {
@@ -45,7 +45,7 @@ class AuthenticationFragment : Fragment(R.layout.fragment_authentication) {
) ?: return ) ?: return
button.isClickable = false button.isClickable = false
launchWithViewLifecycle { onAuthenticationResult(viewModel.authenticate(email, pass)) } viewModel.authenticate(email, pass)
} }
private fun onAuthenticationResult(result: Result<Unit>) { private fun onAuthenticationResult(result: Result<Unit>) {

View File

@@ -5,7 +5,7 @@ import timber.log.Timber
enum class AuthenticationState { enum class AuthenticationState {
AUTHORIZED, AUTHORIZED,
UNAUTHORIZED, UNAUTHORIZED,
UNKNOWN; HIDDEN;
companion object { companion object {
@@ -15,7 +15,7 @@ enum class AuthenticationState {
): AuthenticationState { ): AuthenticationState {
Timber.v("determineState() called with: showLoginButton = $showLoginButton, isAuthorized = $isAuthorized") Timber.v("determineState() called with: showLoginButton = $showLoginButton, isAuthorized = $isAuthorized")
val result = when { val result = when {
!showLoginButton -> UNKNOWN !showLoginButton -> HIDDEN
isAuthorized -> AUTHORIZED isAuthorized -> AUTHORIZED
else -> UNAUTHORIZED else -> UNAUTHORIZED
} }

View File

@@ -1,14 +1,12 @@
package gq.kirmanak.mealient.ui.auth package gq.kirmanak.mealient.ui.auth
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.asLiveData
import androidx.lifecycle.viewModelScope 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 kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.combine
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,24 +16,16 @@ class AuthenticationViewModel @Inject constructor(
private val authRepo: AuthRepo, private val authRepo: AuthRepo,
) : ViewModel() { ) : ViewModel() {
private val showLoginButtonFlow = MutableStateFlow(false) private val _authenticationResult = MutableLiveData<Result<Unit>>()
private val authenticationStateFlow = combine( val authenticationResult: LiveData<Result<Unit>>
showLoginButtonFlow, get() = _authenticationResult
authRepo.isAuthorizedFlow,
AuthenticationState::determineState
)
val authenticationStateLive: LiveData<AuthenticationState>
get() = authenticationStateFlow.asLiveData()
var showLoginButton: Boolean by showLoginButtonFlow::value
suspend fun authenticate(email: String, password: String) = runCatchingExceptCancel { fun authenticate(email: String, password: String) {
authRepo.authenticate(email, password) Timber.v("authenticate() called with: email = $email, password = $password")
}.onFailure { viewModelScope.launch {
Timber.e(it, "authenticate: can't authenticate") _authenticationResult.value = runCatchingExceptCancel {
} authRepo.authenticate(email, password)
}
fun logout() { }
Timber.v("logout() called")
viewModelScope.launch { authRepo.logout() }
} }
} }

View File

@@ -11,7 +11,6 @@ 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.extensions.launchWithViewLifecycle
import timber.log.Timber import timber.log.Timber
@AndroidEntryPoint @AndroidEntryPoint
@@ -24,6 +23,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)
} }
private fun onProceedClick(view: View) { private fun onProceedClick(view: View) {
@@ -33,7 +33,7 @@ class BaseURLFragment : Fragment(R.layout.fragment_base_url) {
lifecycleOwner = viewLifecycleOwner, lifecycleOwner = viewLifecycleOwner,
stringId = R.string.fragment_baseurl_url_input_empty, stringId = R.string.fragment_baseurl_url_input_empty,
) ?: return ) ?: return
launchWithViewLifecycle { onCheckURLResult(viewModel.saveBaseUrl(url)) } viewModel.saveBaseUrl(url)
} }
private fun onCheckURLResult(result: Result<Unit>) { private fun onCheckURLResult(result: Result<Unit>) {

View File

@@ -1,10 +1,14 @@
package gq.kirmanak.mealient.ui.baseurl package gq.kirmanak.mealient.ui.baseurl
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel 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 kotlinx.coroutines.launch
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@@ -14,21 +18,25 @@ class BaseURLViewModel @Inject constructor(
private val versionDataSource: VersionDataSource, private val versionDataSource: VersionDataSource,
) : ViewModel() { ) : ViewModel() {
suspend fun saveBaseUrl(baseURL: String): Result<Unit> { private val _checkURLResult = MutableLiveData<Result<Unit>>()
val checkURLResult: LiveData<Result<Unit>> get() = _checkURLResult
fun saveBaseUrl(baseURL: String) {
Timber.v("saveBaseUrl() called with: baseURL = $baseURL") Timber.v("saveBaseUrl() called with: baseURL = $baseURL")
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)
return checkBaseURL(url) viewModelScope.launch { checkBaseURL(url) }
} }
private suspend fun checkBaseURL(baseURL: String): Result<Unit> { private suspend fun checkBaseURL(baseURL: String) {
Timber.v("checkBaseURL() called with: baseURL = $baseURL") Timber.v("checkBaseURL() called with: baseURL = $baseURL")
val result = runCatchingExceptCancel { val result = runCatchingExceptCancel {
// If it returns proper version info then it must be a Mealie // If it returns proper version info then it must be a Mealie
versionDataSource.getVersionInfo(baseURL) versionDataSource.getVersionInfo(baseURL)
baseURLStorage.storeBaseURL(baseURL)
} }
baseURLStorage.storeBaseURL(baseURL) Timber.i("checkBaseURL: result is $result")
return result.map { } _checkURLResult.value = result
} }
companion object { companion object {

View File

@@ -14,19 +14,19 @@ import gq.kirmanak.mealient.data.recipes.db.entity.RecipeSummaryEntity
import gq.kirmanak.mealient.databinding.FragmentRecipesBinding import gq.kirmanak.mealient.databinding.FragmentRecipesBinding
import gq.kirmanak.mealient.extensions.collectWithViewLifecycle import gq.kirmanak.mealient.extensions.collectWithViewLifecycle
import gq.kirmanak.mealient.extensions.refreshRequestFlow import gq.kirmanak.mealient.extensions.refreshRequestFlow
import gq.kirmanak.mealient.ui.auth.AuthenticationViewModel import gq.kirmanak.mealient.ui.activity.MainActivityViewModel
import timber.log.Timber import timber.log.Timber
@AndroidEntryPoint @AndroidEntryPoint
class RecipesFragment : Fragment(R.layout.fragment_recipes) { class RecipesFragment : Fragment(R.layout.fragment_recipes) {
private val binding by viewBinding(FragmentRecipesBinding::bind) private val binding by viewBinding(FragmentRecipesBinding::bind)
private val viewModel by viewModels<RecipeViewModel>() private val viewModel by viewModels<RecipeViewModel>()
private val authViewModel by activityViewModels<AuthenticationViewModel>() private val activityViewModel by activityViewModels<MainActivityViewModel>()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
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")
authViewModel.showLoginButton = true activityViewModel.showLoginButton = true
setupRecipeAdapter() setupRecipeAdapter()
(requireActivity() as? AppCompatActivity)?.supportActionBar?.title = null (requireActivity() as? AppCompatActivity)?.supportActionBar?.title = null
} }
@@ -64,6 +64,6 @@ class RecipesFragment : Fragment(R.layout.fragment_recipes) {
Timber.v("onDestroyView() called") Timber.v("onDestroyView() called")
// Prevent RV leaking through mObservers list in adapter // Prevent RV leaking through mObservers list in adapter
binding.recipes.adapter = null binding.recipes.adapter = null
authViewModel.showLoginButton = false activityViewModel.showLoginButton = false
} }
} }

View File

@@ -4,7 +4,7 @@
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
tools:context=".MainActivity"> tools:context=".ui.activity.MainActivity">
<com.google.android.material.appbar.AppBarLayout <com.google.android.material.appbar.AppBarLayout
android:id="@+id/toolbar_holder" android:id="@+id/toolbar_holder"