From e6dbff4a6707e3a4b616294bce8e05e10744371d Mon Sep 17 00:00:00 2001 From: Kirill Kamakin Date: Mon, 4 Apr 2022 18:52:19 +0500 Subject: [PATCH 1/4] Fix IllegalStateException when inflating menu --- app/src/main/java/gq/kirmanak/mealient/MainActivity.kt | 10 +++++----- .../mealient/ui/auth/AuthenticationViewModel.kt | 2 -- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/gq/kirmanak/mealient/MainActivity.kt b/app/src/main/java/gq/kirmanak/mealient/MainActivity.kt index 99a1b54..4d79231 100644 --- a/app/src/main/java/gq/kirmanak/mealient/MainActivity.kt +++ b/app/src/main/java/gq/kirmanak/mealient/MainActivity.kt @@ -19,8 +19,7 @@ import timber.log.Timber class MainActivity : AppCompatActivity() { private lateinit var binding: MainActivityBinding private val authViewModel by viewModels() - private val authenticationState: AuthenticationState - get() = authViewModel.currentAuthenticationState + private var lastAuthenticationState: AuthenticationState? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -57,14 +56,15 @@ class MainActivity : AppCompatActivity() { private fun onAuthStateUpdate(authState: AuthenticationState) { Timber.v("onAuthStateUpdate() called with: it = $authState") + lastAuthenticationState = authState invalidateOptionsMenu() } override fun onCreateOptionsMenu(menu: Menu): Boolean { Timber.v("onCreateOptionsMenu() called with: menu = $menu") menuInflater.inflate(R.menu.main_toolbar, menu) - menu.findItem(R.id.logout).isVisible = authenticationState == AUTHORIZED - menu.findItem(R.id.login).isVisible = authenticationState == UNAUTHORIZED + menu.findItem(R.id.logout).isVisible = lastAuthenticationState == AUTHORIZED + menu.findItem(R.id.login).isVisible = lastAuthenticationState == UNAUTHORIZED return true } @@ -76,7 +76,7 @@ class MainActivity : AppCompatActivity() { true } R.id.login -> { - authViewModel.login() + authViewModel.enableLoginRequest() true } else -> super.onOptionsItemSelected(item) 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 a5e0592..cfc3cf8 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 @@ -19,8 +19,6 @@ class AuthenticationViewModel @Inject constructor( flow = authRepo.isAuthorizedFlow, transform = AuthenticationState::determineState ).asLiveData() - val currentAuthenticationState: AuthenticationState - get() = checkNotNull(authenticationState.value) { "Auth state flow mustn't be null" } fun authenticate(username: String, password: String): LiveData> { Timber.v("authenticate() called with: username = $username, password = $password") From c98feceab476ef0ba014747ffa6cf86a8c7b626b Mon Sep 17 00:00:00 2001 From: Kirill Kamakin Date: Mon, 4 Apr 2022 18:54:48 +0500 Subject: [PATCH 2/4] Fix pressing back on AuthenticationFragment If login request isn't disabled when user presses back then they might get navigated back to authentication again. --- .../mealient/extensions/FragmentExtensions.kt | 30 +++++++++++++++++++ .../{ui => extensions}/ViewExtensions.kt | 2 +- .../ui/auth/AuthenticationFragment.kt | 4 ++- .../ui/auth/AuthenticationViewModel.kt | 15 ++++++++-- .../mealient/ui/baseurl/BaseURLFragment.kt | 2 +- .../mealient/ui/recipes/RecipesFragment.kt | 2 +- .../mealient/ui/splash/SplashFragment.kt | 4 +-- 7 files changed, 50 insertions(+), 9 deletions(-) create mode 100644 app/src/main/java/gq/kirmanak/mealient/extensions/FragmentExtensions.kt rename app/src/main/java/gq/kirmanak/mealient/{ui => extensions}/ViewExtensions.kt (99%) diff --git a/app/src/main/java/gq/kirmanak/mealient/extensions/FragmentExtensions.kt b/app/src/main/java/gq/kirmanak/mealient/extensions/FragmentExtensions.kt new file mode 100644 index 0000000..511b2f3 --- /dev/null +++ b/app/src/main/java/gq/kirmanak/mealient/extensions/FragmentExtensions.kt @@ -0,0 +1,30 @@ +package gq.kirmanak.mealient.extensions + +import androidx.activity.OnBackPressedDispatcher +import androidx.activity.addCallback +import androidx.fragment.app.Fragment +import androidx.lifecycle.lifecycleScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch + +fun Fragment.executeOnceOnBackPressed(action: () -> Unit) { + val onBackPressedDispatcher = requireActivity().onBackPressedDispatcher + lifecycleScope.launch { + onBackPressedDispatcher.backPressedFlow().first() + action() + onBackPressedDispatcher.onBackPressed() // Execute other callbacks now + } +} + +@OptIn(ExperimentalCoroutinesApi::class) +fun OnBackPressedDispatcher.backPressedFlow(): Flow = callbackFlow { + val callback = addCallback { trySend(Unit) } + awaitClose { + callback.isEnabled = false + callback.remove() + } +} \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/ui/ViewExtensions.kt b/app/src/main/java/gq/kirmanak/mealient/extensions/ViewExtensions.kt similarity index 99% rename from app/src/main/java/gq/kirmanak/mealient/ui/ViewExtensions.kt rename to app/src/main/java/gq/kirmanak/mealient/extensions/ViewExtensions.kt index 1b608d0..c123800 100644 --- a/app/src/main/java/gq/kirmanak/mealient/ui/ViewExtensions.kt +++ b/app/src/main/java/gq/kirmanak/mealient/extensions/ViewExtensions.kt @@ -1,4 +1,4 @@ -package gq.kirmanak.mealient.ui +package gq.kirmanak.mealient.extensions import android.app.Activity import android.os.Build 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 0f68663..48d61f0 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 @@ -13,7 +13,8 @@ import dagger.hilt.android.AndroidEntryPoint import gq.kirmanak.mealient.R import gq.kirmanak.mealient.data.network.NetworkError.Unauthorized import gq.kirmanak.mealient.databinding.FragmentAuthenticationBinding -import gq.kirmanak.mealient.ui.checkIfInputIsEmpty +import gq.kirmanak.mealient.extensions.checkIfInputIsEmpty +import gq.kirmanak.mealient.extensions.executeOnceOnBackPressed import timber.log.Timber @AndroidEntryPoint @@ -28,6 +29,7 @@ class AuthenticationFragment : Fragment(R.layout.fragment_authentication) { super.onCreate(savedInstanceState) Timber.v("onCreate() called with: savedInstanceState = $savedInstanceState") authStatuses.observe(this, ::onAuthStatusChange) + executeOnceOnBackPressed { viewModel.disableLoginRequest() } } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 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 cfc3cf8..472078d 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 @@ -45,8 +45,17 @@ class AuthenticationViewModel @Inject constructor( } } - fun login() { - Timber.v("login() called") - viewModelScope.launch { loginRequestsFlow.emit(true) } + fun enableLoginRequest() { + Timber.v("enableLoginRequest() called") + updateIsLoginRequested(true) + } + + fun disableLoginRequest() { + Timber.v("disableLoginRequest() called") + updateIsLoginRequested(false) + } + + private fun updateIsLoginRequested(isRequested: Boolean) { + viewModelScope.launch { loginRequestsFlow.emit(isRequested) } } } \ 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 1a76b25..dc6ef5a 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 @@ -11,7 +11,7 @@ import dagger.hilt.android.AndroidEntryPoint import gq.kirmanak.mealient.R import gq.kirmanak.mealient.data.network.NetworkError import gq.kirmanak.mealient.databinding.FragmentBaseUrlBinding -import gq.kirmanak.mealient.ui.checkIfInputIsEmpty +import gq.kirmanak.mealient.extensions.checkIfInputIsEmpty import timber.log.Timber @AndroidEntryPoint diff --git a/app/src/main/java/gq/kirmanak/mealient/ui/recipes/RecipesFragment.kt b/app/src/main/java/gq/kirmanak/mealient/ui/recipes/RecipesFragment.kt index cf4b556..4e68417 100644 --- a/app/src/main/java/gq/kirmanak/mealient/ui/recipes/RecipesFragment.kt +++ b/app/src/main/java/gq/kirmanak/mealient/ui/recipes/RecipesFragment.kt @@ -13,9 +13,9 @@ import dagger.hilt.android.AndroidEntryPoint import gq.kirmanak.mealient.R import gq.kirmanak.mealient.data.recipes.db.entity.RecipeSummaryEntity import gq.kirmanak.mealient.databinding.FragmentRecipesBinding +import gq.kirmanak.mealient.extensions.refreshesLiveData import gq.kirmanak.mealient.ui.auth.AuthenticationState import gq.kirmanak.mealient.ui.auth.AuthenticationViewModel -import gq.kirmanak.mealient.ui.refreshesLiveData import kotlinx.coroutines.flow.collect import timber.log.Timber diff --git a/app/src/main/java/gq/kirmanak/mealient/ui/splash/SplashFragment.kt b/app/src/main/java/gq/kirmanak/mealient/ui/splash/SplashFragment.kt index 32af21d..e52975f 100644 --- a/app/src/main/java/gq/kirmanak/mealient/ui/splash/SplashFragment.kt +++ b/app/src/main/java/gq/kirmanak/mealient/ui/splash/SplashFragment.kt @@ -9,8 +9,8 @@ import androidx.navigation.NavDirections import androidx.navigation.fragment.findNavController import dagger.hilt.android.AndroidEntryPoint import gq.kirmanak.mealient.R -import gq.kirmanak.mealient.ui.setActionBarVisibility -import gq.kirmanak.mealient.ui.setSystemUiVisibility +import gq.kirmanak.mealient.extensions.setActionBarVisibility +import gq.kirmanak.mealient.extensions.setSystemUiVisibility import timber.log.Timber @AndroidEntryPoint From fb10333c2c8d3975cee68f6c32d0321ec2f11e00 Mon Sep 17 00:00:00 2001 From: Kirill Kamakin Date: Mon, 4 Apr 2022 19:34:21 +0500 Subject: [PATCH 3/4] Fix showing login/logout button on initial screens --- .../java/gq/kirmanak/mealient/MainActivity.kt | 11 ++-- .../ui/auth/AuthenticationFragment.kt | 38 ++++++------ .../mealient/ui/auth/AuthenticationState.kt | 7 ++- .../ui/auth/AuthenticationViewModel.kt | 61 +++++++++---------- .../mealient/ui/recipes/RecipesFragment.kt | 5 +- 5 files changed, 59 insertions(+), 63 deletions(-) diff --git a/app/src/main/java/gq/kirmanak/mealient/MainActivity.kt b/app/src/main/java/gq/kirmanak/mealient/MainActivity.kt index 4d79231..1446e96 100644 --- a/app/src/main/java/gq/kirmanak/mealient/MainActivity.kt +++ b/app/src/main/java/gq/kirmanak/mealient/MainActivity.kt @@ -51,7 +51,7 @@ class MainActivity : AppCompatActivity() { private fun listenToAuthStatuses() { Timber.v("listenToAuthStatuses() called") - authViewModel.authenticationState.observe(this, ::onAuthStateUpdate) + authViewModel.authenticationStateLive.observe(this, ::onAuthStateUpdate) } private fun onAuthStateUpdate(authState: AuthenticationState) { @@ -71,12 +71,9 @@ class MainActivity : AppCompatActivity() { override fun onOptionsItemSelected(item: MenuItem): Boolean { Timber.v("onOptionsItemSelected() called with: item = $item") val result = when (item.itemId) { - R.id.logout -> { - authViewModel.logout() - true - } - R.id.login -> { - authViewModel.enableLoginRequest() + R.id.logout, R.id.login -> { + // When user clicks logout they don't want to be authorized + authViewModel.authRequested = item.itemId == R.id.login true } else -> super.onOptionsItemSelected(item) 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 48d61f0..98e694f 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 @@ -5,13 +5,12 @@ import android.view.View import androidx.appcompat.app.AppCompatActivity import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels -import androidx.lifecycle.LiveData import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.findNavController import by.kirich1409.viewbindingdelegate.viewBinding import dagger.hilt.android.AndroidEntryPoint import gq.kirmanak.mealient.R -import gq.kirmanak.mealient.data.network.NetworkError.Unauthorized +import gq.kirmanak.mealient.data.network.NetworkError import gq.kirmanak.mealient.databinding.FragmentAuthenticationBinding import gq.kirmanak.mealient.extensions.checkIfInputIsEmpty import gq.kirmanak.mealient.extensions.executeOnceOnBackPressed @@ -22,14 +21,10 @@ class AuthenticationFragment : Fragment(R.layout.fragment_authentication) { private val binding by viewBinding(FragmentAuthenticationBinding::bind) private val viewModel by activityViewModels() - private val authStatuses: LiveData - get() = viewModel.authenticationState - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) Timber.v("onCreate() called with: savedInstanceState = $savedInstanceState") - authStatuses.observe(this, ::onAuthStatusChange) - executeOnceOnBackPressed { viewModel.disableLoginRequest() } + executeOnceOnBackPressed { viewModel.authRequested = false } } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { @@ -38,13 +33,7 @@ class AuthenticationFragment : Fragment(R.layout.fragment_authentication) { binding.button.setOnClickListener { onLoginClicked() } (requireActivity() as? AppCompatActivity)?.supportActionBar?.title = getString(R.string.app_name) - } - - private fun onAuthStatusChange(isAuthenticated: AuthenticationState) { - Timber.v("onAuthStatusChange() called with: isAuthenticated = $isAuthenticated") - if (isAuthenticated == AuthenticationState.AUTHORIZED) { - findNavController().popBackStack() - } + viewModel.authenticationResult.observe(viewLifecycleOwner, ::onAuthenticationResult) } private fun onLoginClicked(): Unit = with(binding) { @@ -59,14 +48,21 @@ class AuthenticationFragment : Fragment(R.layout.fragment_authentication) { } ?: return button.isClickable = false - viewModel.authenticate(email, pass).observe(viewLifecycleOwner) { - Timber.d("onLoginClicked: result $it") - passwordInputLayout.error = when (it.exceptionOrNull()) { - is Unauthorized -> getString(R.string.fragment_authentication_credentials_incorrect) - else -> null - } + viewModel.authenticate(email, pass) + } - button.isClickable = true + private fun onAuthenticationResult(result: Result) { + Timber.v("onAuthenticationResult() called with: result = $result") + if (result.isSuccess) { + findNavController().popBackStack() + return } + + binding.passwordInputLayout.error = when (result.exceptionOrNull()) { + is NetworkError.Unauthorized -> getString(R.string.fragment_authentication_credentials_incorrect) + else -> null + } + + binding.button.isClickable = true } } \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/ui/auth/AuthenticationState.kt b/app/src/main/java/gq/kirmanak/mealient/ui/auth/AuthenticationState.kt index b9b4a6d..70ec901 100644 --- a/app/src/main/java/gq/kirmanak/mealient/ui/auth/AuthenticationState.kt +++ b/app/src/main/java/gq/kirmanak/mealient/ui/auth/AuthenticationState.kt @@ -5,16 +5,19 @@ import timber.log.Timber enum class AuthenticationState { AUTHORIZED, AUTH_REQUESTED, - UNAUTHORIZED; + UNAUTHORIZED, + UNKNOWN; companion object { fun determineState( isLoginRequested: Boolean, + showLoginButton: Boolean, isAuthorized: Boolean, ): AuthenticationState { - Timber.v("determineState() called with: isLoginRequested = $isLoginRequested, isAuthorized = $isAuthorized") + Timber.v("determineState() called with: isLoginRequested = $isLoginRequested, showLoginButton = $showLoginButton, isAuthorized = $isAuthorized") val result = when { + !showLoginButton -> UNKNOWN isAuthorized -> AUTHORIZED isLoginRequested -> AUTH_REQUESTED else -> UNAUTHORIZED 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 472078d..51ee7ee 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 @@ -4,6 +4,7 @@ import androidx.lifecycle.* import dagger.hilt.android.lifecycle.HiltViewModel import gq.kirmanak.mealient.data.auth.AuthRepo import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.combine import kotlinx.coroutines.launch import timber.log.Timber @@ -14,48 +15,44 @@ class AuthenticationViewModel @Inject constructor( private val authRepo: AuthRepo, ) : ViewModel() { - private val loginRequestsFlow = MutableStateFlow(false) - val authenticationState: LiveData = loginRequestsFlow.combine( - flow = authRepo.isAuthorizedFlow, - transform = AuthenticationState::determineState - ).asLiveData() + private val authRequestsFlow = MutableStateFlow(false) + private val showLoginButtonFlow = MutableStateFlow(false) + private val authenticationStateFlow = combine( + authRequestsFlow, + showLoginButtonFlow, + authRepo.isAuthorizedFlow, + AuthenticationState::determineState + ) + val authenticationStateLive: LiveData + get() = authenticationStateFlow.asLiveData() + var authRequested: Boolean by authRequestsFlow::value + var showLoginButton: Boolean by showLoginButtonFlow::value - fun authenticate(username: String, password: String): LiveData> { + private val _authenticationResult = MutableLiveData>() + val authenticationResult: LiveData> + get() = _authenticationResult + + init { + viewModelScope.launch { + authRequestsFlow.collect { isRequested -> + // Clear auth token on logout request + if (!isRequested) authRepo.logout() + } + } + } + + fun authenticate(username: String, password: String) { Timber.v("authenticate() called with: username = $username, password = $password") - val result = MutableLiveData>() viewModelScope.launch { runCatching { authRepo.authenticate(username, password) }.onFailure { Timber.e(it, "authenticate: can't authenticate") - result.value = Result.failure(it) + _authenticationResult.value = Result.failure(it) }.onSuccess { Timber.d("authenticate: authenticated") - result.value = Result.success(Unit) + _authenticationResult.value = Result.success(Unit) } } - return result - } - - fun logout() { - Timber.v("logout() called") - viewModelScope.launch { - loginRequestsFlow.emit(false) - authRepo.logout() - } - } - - fun enableLoginRequest() { - Timber.v("enableLoginRequest() called") - updateIsLoginRequested(true) - } - - fun disableLoginRequest() { - Timber.v("disableLoginRequest() called") - updateIsLoginRequested(false) - } - - private fun updateIsLoginRequested(isRequested: Boolean) { - viewModelScope.launch { loginRequestsFlow.emit(isRequested) } } } \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/ui/recipes/RecipesFragment.kt b/app/src/main/java/gq/kirmanak/mealient/ui/recipes/RecipesFragment.kt index 4e68417..01e8cfb 100644 --- a/app/src/main/java/gq/kirmanak/mealient/ui/recipes/RecipesFragment.kt +++ b/app/src/main/java/gq/kirmanak/mealient/ui/recipes/RecipesFragment.kt @@ -27,7 +27,8 @@ class RecipesFragment : Fragment(R.layout.fragment_recipes) { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - authViewModel.authenticationState.observe(this, ::onAuthStateChange) + Timber.v("onCreate() called with: savedInstanceState = $savedInstanceState") + authViewModel.authenticationStateLive.observe(this, ::onAuthStateChange) } private fun onAuthStateChange(authenticationState: AuthenticationState) { @@ -40,6 +41,7 @@ class RecipesFragment : Fragment(R.layout.fragment_recipes) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) Timber.v("onViewCreated() called with: view = $view, savedInstanceState = $savedInstanceState") + authViewModel.showLoginButton = true setupRecipeAdapter() (requireActivity() as? AppCompatActivity)?.supportActionBar?.title = null } @@ -78,5 +80,6 @@ class RecipesFragment : Fragment(R.layout.fragment_recipes) { Timber.v("onDestroyView() called") // Prevent RV leaking through mObservers list in adapter binding.recipes.adapter = null + authViewModel.showLoginButton = false } } \ No newline at end of file From f14afd2ebe3551c53a2d3b98bd828a7b49522b47 Mon Sep 17 00:00:00 2001 From: Kirill Kamakin Date: Mon, 4 Apr 2022 20:52:14 +0500 Subject: [PATCH 4/4] Fix IllegalStateException when clicking login after logout The previous login result was stored as live data and prevented AuthenticationFragment from being shown properly. However, an attempt to destroy RecipesFragment was made. This attempt caused IllegalStateException when accessing view in onDestroyView. --- .../mealient/extensions/FragmentExtensions.kt | 8 +++- .../mealient/extensions/ViewExtensions.kt | 25 +++++------- .../ui/auth/AuthenticationFragment.kt | 6 ++- .../ui/auth/AuthenticationViewModel.kt | 26 ++++-------- .../mealient/ui/recipes/RecipeViewHolder.kt | 2 +- .../mealient/ui/recipes/RecipeViewModel.kt | 40 +------------------ .../mealient/ui/recipes/RecipesFragment.kt | 28 ++++++------- 7 files changed, 45 insertions(+), 90 deletions(-) diff --git a/app/src/main/java/gq/kirmanak/mealient/extensions/FragmentExtensions.kt b/app/src/main/java/gq/kirmanak/mealient/extensions/FragmentExtensions.kt index 511b2f3..17186e9 100644 --- a/app/src/main/java/gq/kirmanak/mealient/extensions/FragmentExtensions.kt +++ b/app/src/main/java/gq/kirmanak/mealient/extensions/FragmentExtensions.kt @@ -8,6 +8,7 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch @@ -27,4 +28,9 @@ fun OnBackPressedDispatcher.backPressedFlow(): Flow = callbackFlow { callback.isEnabled = false callback.remove() } -} \ No newline at end of file +} + +inline fun Fragment.collectWithViewLifecycle( + flow: Flow, + crossinline collector: suspend (T) -> Unit, +) = viewLifecycleOwner.lifecycleScope.launch { flow.collect(collector) } diff --git a/app/src/main/java/gq/kirmanak/mealient/extensions/ViewExtensions.kt b/app/src/main/java/gq/kirmanak/mealient/extensions/ViewExtensions.kt index c123800..afb1fad 100644 --- a/app/src/main/java/gq/kirmanak/mealient/extensions/ViewExtensions.kt +++ b/app/src/main/java/gq/kirmanak/mealient/extensions/ViewExtensions.kt @@ -10,8 +10,6 @@ import androidx.annotation.RequiresApi import androidx.appcompat.app.AppCompatActivity import androidx.core.widget.doAfterTextChanged import androidx.lifecycle.LifecycleCoroutineScope -import androidx.lifecycle.LiveData -import androidx.lifecycle.asLiveData import androidx.swiperefreshlayout.widget.SwipeRefreshLayout import com.google.android.material.textfield.TextInputLayout import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -26,20 +24,17 @@ import kotlinx.coroutines.flow.first import timber.log.Timber @OptIn(ExperimentalCoroutinesApi::class) -fun SwipeRefreshLayout.refreshesLiveData(): LiveData { - val callbackFlow: Flow = callbackFlow { - val listener = SwipeRefreshLayout.OnRefreshListener { - Timber.v("Refresh requested") - trySend(Unit).logErrors("refreshesFlow") - } - Timber.v("Adding refresh request listener") - setOnRefreshListener(listener) - awaitClose { - Timber.v("Removing refresh request listener") - setOnRefreshListener(null) - } +fun SwipeRefreshLayout.refreshRequestFlow(): Flow = callbackFlow { + Timber.v("refreshRequestFlow() called") + val listener = SwipeRefreshLayout.OnRefreshListener { + Timber.v("refreshRequestFlow: listener called") + trySend(Unit).logErrors("refreshesFlow") + } + setOnRefreshListener(listener) + awaitClose { + Timber.v("Removing refresh request listener") + setOnRefreshListener(null) } - return callbackFlow.asLiveData() } fun Activity.setSystemUiVisibility(isVisible: Boolean) { 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 98e694f..ece0518 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 @@ -14,6 +14,7 @@ import gq.kirmanak.mealient.data.network.NetworkError import gq.kirmanak.mealient.databinding.FragmentAuthenticationBinding import gq.kirmanak.mealient.extensions.checkIfInputIsEmpty import gq.kirmanak.mealient.extensions.executeOnceOnBackPressed +import kotlinx.coroutines.launch import timber.log.Timber @AndroidEntryPoint @@ -33,7 +34,6 @@ class AuthenticationFragment : Fragment(R.layout.fragment_authentication) { binding.button.setOnClickListener { onLoginClicked() } (requireActivity() as? AppCompatActivity)?.supportActionBar?.title = getString(R.string.app_name) - viewModel.authenticationResult.observe(viewLifecycleOwner, ::onAuthenticationResult) } private fun onLoginClicked(): Unit = with(binding) { @@ -48,7 +48,9 @@ class AuthenticationFragment : Fragment(R.layout.fragment_authentication) { } ?: return button.isClickable = false - viewModel.authenticate(email, pass) + viewLifecycleOwner.lifecycleScope.launch { + onAuthenticationResult(viewModel.authenticate(email, pass)) + } } private fun onAuthenticationResult(result: Result) { 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 51ee7ee..d17b360 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 @@ -1,6 +1,9 @@ package gq.kirmanak.mealient.ui.auth -import androidx.lifecycle.* +import androidx.lifecycle.LiveData +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 kotlinx.coroutines.flow.MutableStateFlow @@ -28,10 +31,6 @@ class AuthenticationViewModel @Inject constructor( var authRequested: Boolean by authRequestsFlow::value var showLoginButton: Boolean by showLoginButtonFlow::value - private val _authenticationResult = MutableLiveData>() - val authenticationResult: LiveData> - get() = _authenticationResult - init { viewModelScope.launch { authRequestsFlow.collect { isRequested -> @@ -41,18 +40,9 @@ class AuthenticationViewModel @Inject constructor( } } - fun authenticate(username: String, password: String) { - Timber.v("authenticate() called with: username = $username, password = $password") - viewModelScope.launch { - runCatching { - authRepo.authenticate(username, password) - }.onFailure { - Timber.e(it, "authenticate: can't authenticate") - _authenticationResult.value = Result.failure(it) - }.onSuccess { - Timber.d("authenticate: authenticated") - _authenticationResult.value = Result.success(Unit) - } - } + suspend fun authenticate(username: String, password: String): Result = runCatching { + authRepo.authenticate(username, password) + }.onFailure { + Timber.e(it, "authenticate: can't authenticate") } } \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/ui/recipes/RecipeViewHolder.kt b/app/src/main/java/gq/kirmanak/mealient/ui/recipes/RecipeViewHolder.kt index a7b7fb0..64fd1f3 100644 --- a/app/src/main/java/gq/kirmanak/mealient/ui/recipes/RecipeViewHolder.kt +++ b/app/src/main/java/gq/kirmanak/mealient/ui/recipes/RecipeViewHolder.kt @@ -9,7 +9,7 @@ import timber.log.Timber class RecipeViewHolder( private val binding: ViewHolderRecipeBinding, private val recipeViewModel: RecipeViewModel, - private val clickListener: (RecipeSummaryEntity) -> Unit + private val clickListener: (RecipeSummaryEntity) -> Unit, ) : RecyclerView.ViewHolder(binding.root) { private val loadingPlaceholder by lazy { binding.root.resources.getString(R.string.view_holder_recipe_text_placeholder) diff --git a/app/src/main/java/gq/kirmanak/mealient/ui/recipes/RecipeViewModel.kt b/app/src/main/java/gq/kirmanak/mealient/ui/recipes/RecipeViewModel.kt index f24beec..6ff9334 100644 --- a/app/src/main/java/gq/kirmanak/mealient/ui/recipes/RecipeViewModel.kt +++ b/app/src/main/java/gq/kirmanak/mealient/ui/recipes/RecipeViewModel.kt @@ -1,8 +1,6 @@ package gq.kirmanak.mealient.ui.recipes import android.widget.ImageView -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.paging.cachedIn @@ -10,34 +8,17 @@ import dagger.hilt.android.lifecycle.HiltViewModel import gq.kirmanak.mealient.data.recipes.RecipeImageLoader import gq.kirmanak.mealient.data.recipes.RecipeRepo import gq.kirmanak.mealient.data.recipes.db.entity.RecipeSummaryEntity -import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.collect -import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.launch import timber.log.Timber import javax.inject.Inject @HiltViewModel class RecipeViewModel @Inject constructor( - private val recipeRepo: RecipeRepo, + recipeRepo: RecipeRepo, private val recipeImageLoader: RecipeImageLoader ) : ViewModel() { - private var _isRefreshing = MutableLiveData() - val isRefreshing: LiveData get() = _isRefreshing - private val _nextRecipeInfoChannel = Channel() - val nextRecipeInfo: Flow = - _nextRecipeInfoChannel.receiveAsFlow() - - val adapter = RecipesPagingAdapter(this) { - Timber.d("onClick: recipe clicked $it") - viewModelScope.launch { _nextRecipeInfoChannel.send(it) } - } - - init { - setupAdapter() - } + val pagingData = recipeRepo.createPager().flow.cachedIn(viewModelScope) fun loadRecipeImage(view: ImageView, recipeSummary: RecipeSummaryEntity?) { Timber.v("loadRecipeImage() called with: view = $view, recipeSummary = $recipeSummary") @@ -45,21 +26,4 @@ class RecipeViewModel @Inject constructor( recipeImageLoader.loadRecipeImage(view, recipeSummary?.slug) } } - - private fun setupAdapter() { - with(viewModelScope) { - launch { - recipeRepo.createPager().flow.cachedIn(this).collect { - Timber.d("setupAdapter: received data update") - adapter.submitData(it) - } - } - launch { - adapter.onPagesUpdatedFlow.collect { - Timber.d("setupAdapter: pages have been updated") - _isRefreshing.value = false - } - } - } - } } \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/ui/recipes/RecipesFragment.kt b/app/src/main/java/gq/kirmanak/mealient/ui/recipes/RecipesFragment.kt index 01e8cfb..dab5abd 100644 --- a/app/src/main/java/gq/kirmanak/mealient/ui/recipes/RecipesFragment.kt +++ b/app/src/main/java/gq/kirmanak/mealient/ui/recipes/RecipesFragment.kt @@ -6,17 +6,16 @@ import androidx.appcompat.app.AppCompatActivity import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import androidx.fragment.app.viewModels -import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.findNavController import by.kirich1409.viewbindingdelegate.viewBinding import dagger.hilt.android.AndroidEntryPoint import gq.kirmanak.mealient.R import gq.kirmanak.mealient.data.recipes.db.entity.RecipeSummaryEntity import gq.kirmanak.mealient.databinding.FragmentRecipesBinding -import gq.kirmanak.mealient.extensions.refreshesLiveData +import gq.kirmanak.mealient.extensions.collectWithViewLifecycle +import gq.kirmanak.mealient.extensions.refreshRequestFlow import gq.kirmanak.mealient.ui.auth.AuthenticationState import gq.kirmanak.mealient.ui.auth.AuthenticationViewModel -import kotlinx.coroutines.flow.collect import timber.log.Timber @AndroidEntryPoint @@ -58,20 +57,19 @@ class RecipesFragment : Fragment(R.layout.fragment_recipes) { private fun setupRecipeAdapter() { Timber.v("setupRecipeAdapter() called") - binding.recipes.adapter = viewModel.adapter - viewModel.isRefreshing.observe(viewLifecycleOwner) { - Timber.d("setupRecipeAdapter: isRefreshing = $it") - binding.refresher.isRefreshing = it + val adapter = RecipesPagingAdapter(viewModel, ::navigateToRecipeInfo) + binding.recipes.adapter = adapter + collectWithViewLifecycle(viewModel.pagingData) { + Timber.v("setupRecipeAdapter: received data update") + adapter.submitData(lifecycle, it) } - binding.refresher.refreshesLiveData().observe(viewLifecycleOwner) { - Timber.d("setupRecipeAdapter: received refresh request") - viewModel.adapter.refresh() + collectWithViewLifecycle(adapter.onPagesUpdatedFlow) { + Timber.v("setupRecipeAdapter: pages updated") + binding.refresher.isRefreshing = false } - viewLifecycleOwner.lifecycleScope.launchWhenResumed { - viewModel.nextRecipeInfo.collect { - Timber.d("setupRecipeAdapter: navigating to recipe $it") - navigateToRecipeInfo(it) - } + collectWithViewLifecycle(binding.refresher.refreshRequestFlow()) { + Timber.v("setupRecipeAdapter: received refresh request") + adapter.refresh() } }