Use single UI state for activity

This commit is contained in:
Kirill Kamakin
2022-04-09 00:47:50 +05:00
parent 536c9765cb
commit e7620400b8
8 changed files with 56 additions and 45 deletions

View File

@@ -12,16 +12,14 @@ 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.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.AUTHORIZED
import gq.kirmanak.mealient.ui.auth.AuthenticationState.UNAUTHORIZED
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 viewModel by viewModels<MainActivityViewModel>() private val viewModel by viewModels<MainActivityViewModel>()
private var lastAuthenticationState: AuthenticationState? = null private val title: String by lazy { getString(R.string.app_name) }
private val uiState: MainActivityUiState get() = viewModel.uiState
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@@ -31,7 +29,13 @@ class MainActivity : AppCompatActivity() {
setSupportActionBar(binding.toolbar) setSupportActionBar(binding.toolbar)
supportActionBar?.setIcon(R.drawable.ic_toolbar) supportActionBar?.setIcon(R.drawable.ic_toolbar)
setToolbarRoundCorner() setToolbarRoundCorner()
listenToAuthStatuses() viewModel.uiStateLive.observe(this, ::onUiStateChange)
}
private fun onUiStateChange(uiState: MainActivityUiState) {
Timber.v("onUiStateChange() called with: uiState = $uiState")
supportActionBar?.title = if (uiState.titleVisible) title else null
invalidateOptionsMenu()
} }
private fun setToolbarRoundCorner() { private fun setToolbarRoundCorner() {
@@ -51,22 +55,11 @@ class MainActivity : AppCompatActivity() {
} }
} }
private fun listenToAuthStatuses() {
Timber.v("listenToAuthStatuses() called")
viewModel.authenticationStateLive.observe(this, ::onAuthStateUpdate)
}
private fun onAuthStateUpdate(authState: AuthenticationState) {
Timber.v("onAuthStateUpdate() called with: it = $authState")
lastAuthenticationState = authState
invalidateOptionsMenu()
}
override fun onCreateOptionsMenu(menu: Menu): Boolean { override fun onCreateOptionsMenu(menu: Menu): Boolean {
Timber.v("onCreateOptionsMenu() called with: menu = $menu") Timber.v("onCreateOptionsMenu() called with: menu = $menu")
menuInflater.inflate(R.menu.main_toolbar, menu) menuInflater.inflate(R.menu.main_toolbar, menu)
menu.findItem(R.id.logout).isVisible = lastAuthenticationState == AUTHORIZED menu.findItem(R.id.logout).isVisible = uiState.canShowLogout
menu.findItem(R.id.login).isVisible = lastAuthenticationState == UNAUTHORIZED menu.findItem(R.id.login).isVisible = uiState.canShowLogin
return true return true
} }

View File

@@ -0,0 +1,13 @@
package gq.kirmanak.mealient.ui.activity
data class MainActivityUiState(
val loginButtonVisible: Boolean = false,
val titleVisible: Boolean = true,
val isAuthorized: Boolean = false,
) {
val canShowLogin: Boolean
get() = !isAuthorized && loginButtonVisible
val canShowLogout: Boolean
get() = isAuthorized && loginButtonVisible
}

View File

@@ -1,14 +1,10 @@
package gq.kirmanak.mealient.ui.activity package gq.kirmanak.mealient.ui.activity
import androidx.lifecycle.LiveData import androidx.lifecycle.*
import androidx.lifecycle.ViewModel
import androidx.lifecycle.asLiveData
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.ui.auth.AuthenticationState import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.onEach
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,16 +14,22 @@ class MainActivityViewModel @Inject constructor(
private val authRepo: AuthRepo, private val authRepo: AuthRepo,
) : ViewModel() { ) : ViewModel() {
private val showLoginButtonFlow = MutableStateFlow(false) private val _uiState = MutableLiveData(MainActivityUiState())
var showLoginButton: Boolean by showLoginButtonFlow::value val uiStateLive: LiveData<MainActivityUiState>
get() = _uiState.distinctUntilChanged()
var uiState: MainActivityUiState
get() = checkNotNull(_uiState.value) { "UiState must not be null" }
private set(value) = _uiState.postValue(value)
private val authenticationStateFlow = combine( init {
showLoginButtonFlow, authRepo.isAuthorizedFlow
authRepo.isAuthorizedFlow, .onEach { isAuthorized -> updateUiState { it.copy(isAuthorized = isAuthorized) } }
AuthenticationState::determineState .launchIn(viewModelScope)
) }
val authenticationStateLive: LiveData<AuthenticationState>
get() = authenticationStateFlow.asLiveData() fun updateUiState(updater: (MainActivityUiState) -> MainActivityUiState) {
uiState = updater(uiState)
}
fun logout() { fun logout() {
Timber.v("logout() called") Timber.v("logout() called")

View File

@@ -2,8 +2,8 @@ package gq.kirmanak.mealient.ui.auth
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
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.fragment.app.viewModels
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import by.kirich1409.viewbindingdelegate.viewBinding import by.kirich1409.viewbindingdelegate.viewBinding
@@ -12,19 +12,20 @@ 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.activity.MainActivityViewModel
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 viewModels<AuthenticationViewModel>() private val viewModel by viewModels<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")
binding.button.setOnClickListener { onLoginClicked() } binding.button.setOnClickListener { onLoginClicked() }
(requireActivity() as? AppCompatActivity)?.supportActionBar?.title = activityViewModel.updateUiState { it.copy(loginButtonVisible = false, titleVisible = true) }
getString(R.string.app_name)
viewModel.authenticationResult.observe(viewLifecycleOwner, ::onAuthenticationResult) viewModel.authenticationResult.observe(viewLifecycleOwner, ::onAuthenticationResult)
} }

View File

@@ -3,6 +3,7 @@ package gq.kirmanak.mealient.ui.baseurl
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.fragment.app.viewModels 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
@@ -11,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.activity.MainActivityViewModel
import timber.log.Timber import timber.log.Timber
@AndroidEntryPoint @AndroidEntryPoint
@@ -18,12 +20,14 @@ class BaseURLFragment : Fragment(R.layout.fragment_base_url) {
private val binding by viewBinding(FragmentBaseUrlBinding::bind) private val binding by viewBinding(FragmentBaseUrlBinding::bind)
private val viewModel by viewModels<BaseURLViewModel>() private val viewModel by viewModels<BaseURLViewModel>()
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")
binding.button.setOnClickListener(::onProceedClick) binding.button.setOnClickListener(::onProceedClick)
viewModel.checkURLResult.observe(viewLifecycleOwner, ::onCheckURLResult) viewModel.checkURLResult.observe(viewLifecycleOwner, ::onCheckURLResult)
activityViewModel.updateUiState { it.copy(loginButtonVisible = false, titleVisible = true) }
} }
private fun onProceedClick(view: View) { private fun onProceedClick(view: View) {

View File

@@ -2,20 +2,22 @@ package gq.kirmanak.mealient.ui.disclaimer
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
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.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
import gq.kirmanak.mealient.R import gq.kirmanak.mealient.R
import gq.kirmanak.mealient.databinding.FragmentDisclaimerBinding import gq.kirmanak.mealient.databinding.FragmentDisclaimerBinding
import gq.kirmanak.mealient.ui.activity.MainActivityViewModel
import timber.log.Timber import timber.log.Timber
@AndroidEntryPoint @AndroidEntryPoint
class DisclaimerFragment : Fragment(R.layout.fragment_disclaimer) { class DisclaimerFragment : Fragment(R.layout.fragment_disclaimer) {
private val binding by viewBinding(FragmentDisclaimerBinding::bind) private val binding by viewBinding(FragmentDisclaimerBinding::bind)
private val viewModel by viewModels<DisclaimerViewModel>() private val viewModel by viewModels<DisclaimerViewModel>()
private val activityViewModel by activityViewModels<MainActivityViewModel>()
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@@ -48,7 +50,6 @@ class DisclaimerFragment : Fragment(R.layout.fragment_disclaimer) {
binding.okay.isClickable = it == 0 binding.okay.isClickable = it == 0
} }
viewModel.startCountDown() viewModel.startCountDown()
(requireActivity() as? AppCompatActivity)?.supportActionBar?.title = activityViewModel.updateUiState { it.copy(loginButtonVisible = false, titleVisible = true) }
getString(R.string.app_name)
} }
} }

View File

@@ -2,7 +2,6 @@ package gq.kirmanak.mealient.ui.recipes
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
@@ -26,9 +25,8 @@ class RecipesFragment : Fragment(R.layout.fragment_recipes) {
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")
activityViewModel.showLoginButton = true activityViewModel.updateUiState { it.copy(loginButtonVisible = true, titleVisible = false) }
setupRecipeAdapter() setupRecipeAdapter()
(requireActivity() as? AppCompatActivity)?.supportActionBar?.title = null
} }
private fun navigateToRecipeInfo(recipeSummaryEntity: RecipeSummaryEntity) { private fun navigateToRecipeInfo(recipeSummaryEntity: RecipeSummaryEntity) {
@@ -64,6 +62,5 @@ 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
activityViewModel.showLoginButton = false
} }
} }

View File

@@ -6,7 +6,7 @@
android:id="@+id/login" android:id="@+id/login"
android:contentDescription="@string/menu_main_toolbar_content_description_login" android:contentDescription="@string/menu_main_toolbar_content_description_login"
android:title="@string/menu_main_toolbar_login" android:title="@string/menu_main_toolbar_login"
app:showAsAction="never" /> app:showAsAction="ifRoom" />
<item <item
android:id="@+id/logout" android:id="@+id/logout"