@@ -19,8 +19,7 @@ import timber.log.Timber
|
|||||||
class MainActivity : AppCompatActivity() {
|
class MainActivity : AppCompatActivity() {
|
||||||
private lateinit var binding: MainActivityBinding
|
private lateinit var binding: MainActivityBinding
|
||||||
private val authViewModel by viewModels<AuthenticationViewModel>()
|
private val authViewModel by viewModels<AuthenticationViewModel>()
|
||||||
private val authenticationState: AuthenticationState
|
private var lastAuthenticationState: AuthenticationState? = null
|
||||||
get() = authViewModel.currentAuthenticationState
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
@@ -52,31 +51,29 @@ class MainActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
private fun listenToAuthStatuses() {
|
private fun listenToAuthStatuses() {
|
||||||
Timber.v("listenToAuthStatuses() called")
|
Timber.v("listenToAuthStatuses() called")
|
||||||
authViewModel.authenticationState.observe(this, ::onAuthStateUpdate)
|
authViewModel.authenticationStateLive.observe(this, ::onAuthStateUpdate)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onAuthStateUpdate(authState: AuthenticationState) {
|
private fun onAuthStateUpdate(authState: AuthenticationState) {
|
||||||
Timber.v("onAuthStateUpdate() called with: it = $authState")
|
Timber.v("onAuthStateUpdate() called with: it = $authState")
|
||||||
|
lastAuthenticationState = authState
|
||||||
invalidateOptionsMenu()
|
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 = authenticationState == AUTHORIZED
|
menu.findItem(R.id.logout).isVisible = lastAuthenticationState == AUTHORIZED
|
||||||
menu.findItem(R.id.login).isVisible = authenticationState == UNAUTHORIZED
|
menu.findItem(R.id.login).isVisible = lastAuthenticationState == UNAUTHORIZED
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
Timber.v("onOptionsItemSelected() called with: item = $item")
|
Timber.v("onOptionsItemSelected() called with: item = $item")
|
||||||
val result = when (item.itemId) {
|
val result = when (item.itemId) {
|
||||||
R.id.logout -> {
|
R.id.logout, R.id.login -> {
|
||||||
authViewModel.logout()
|
// When user clicks logout they don't want to be authorized
|
||||||
true
|
authViewModel.authRequested = item.itemId == R.id.login
|
||||||
}
|
|
||||||
R.id.login -> {
|
|
||||||
authViewModel.login()
|
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
else -> super.onOptionsItemSelected(item)
|
else -> super.onOptionsItemSelected(item)
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
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.collect
|
||||||
|
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<Unit> = callbackFlow {
|
||||||
|
val callback = addCallback { trySend(Unit) }
|
||||||
|
awaitClose {
|
||||||
|
callback.isEnabled = false
|
||||||
|
callback.remove()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fun <T> Fragment.collectWithViewLifecycle(
|
||||||
|
flow: Flow<T>,
|
||||||
|
crossinline collector: suspend (T) -> Unit,
|
||||||
|
) = viewLifecycleOwner.lifecycleScope.launch { flow.collect(collector) }
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package gq.kirmanak.mealient.ui
|
package gq.kirmanak.mealient.extensions
|
||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
@@ -10,8 +10,6 @@ import androidx.annotation.RequiresApi
|
|||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.core.widget.doAfterTextChanged
|
import androidx.core.widget.doAfterTextChanged
|
||||||
import androidx.lifecycle.LifecycleCoroutineScope
|
import androidx.lifecycle.LifecycleCoroutineScope
|
||||||
import androidx.lifecycle.LiveData
|
|
||||||
import androidx.lifecycle.asLiveData
|
|
||||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||||
import com.google.android.material.textfield.TextInputLayout
|
import com.google.android.material.textfield.TextInputLayout
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
@@ -26,21 +24,18 @@ import kotlinx.coroutines.flow.first
|
|||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
fun SwipeRefreshLayout.refreshesLiveData(): LiveData<Unit> {
|
fun SwipeRefreshLayout.refreshRequestFlow(): Flow<Unit> = callbackFlow {
|
||||||
val callbackFlow: Flow<Unit> = callbackFlow {
|
Timber.v("refreshRequestFlow() called")
|
||||||
val listener = SwipeRefreshLayout.OnRefreshListener {
|
val listener = SwipeRefreshLayout.OnRefreshListener {
|
||||||
Timber.v("Refresh requested")
|
Timber.v("refreshRequestFlow: listener called")
|
||||||
trySend(Unit).logErrors("refreshesFlow")
|
trySend(Unit).logErrors("refreshesFlow")
|
||||||
}
|
}
|
||||||
Timber.v("Adding refresh request listener")
|
|
||||||
setOnRefreshListener(listener)
|
setOnRefreshListener(listener)
|
||||||
awaitClose {
|
awaitClose {
|
||||||
Timber.v("Removing refresh request listener")
|
Timber.v("Removing refresh request listener")
|
||||||
setOnRefreshListener(null)
|
setOnRefreshListener(null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return callbackFlow.asLiveData()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Activity.setSystemUiVisibility(isVisible: Boolean) {
|
fun Activity.setSystemUiVisibility(isVisible: Boolean) {
|
||||||
Timber.v("setSystemUiVisibility() called with: isVisible = $isVisible")
|
Timber.v("setSystemUiVisibility() called with: isVisible = $isVisible")
|
||||||
@@ -5,15 +5,16 @@ 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.activityViewModels
|
||||||
import androidx.lifecycle.LiveData
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
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.data.network.NetworkError.Unauthorized
|
import gq.kirmanak.mealient.data.network.NetworkError
|
||||||
import gq.kirmanak.mealient.databinding.FragmentAuthenticationBinding
|
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 kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
@@ -21,13 +22,10 @@ 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 activityViewModels<AuthenticationViewModel>()
|
||||||
|
|
||||||
private val authStatuses: LiveData<AuthenticationState>
|
|
||||||
get() = viewModel.authenticationState
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
Timber.v("onCreate() called with: savedInstanceState = $savedInstanceState")
|
Timber.v("onCreate() called with: savedInstanceState = $savedInstanceState")
|
||||||
authStatuses.observe(this, ::onAuthStatusChange)
|
executeOnceOnBackPressed { viewModel.authRequested = false }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
@@ -38,13 +36,6 @@ class AuthenticationFragment : Fragment(R.layout.fragment_authentication) {
|
|||||||
getString(R.string.app_name)
|
getString(R.string.app_name)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onAuthStatusChange(isAuthenticated: AuthenticationState) {
|
|
||||||
Timber.v("onAuthStatusChange() called with: isAuthenticated = $isAuthenticated")
|
|
||||||
if (isAuthenticated == AuthenticationState.AUTHORIZED) {
|
|
||||||
findNavController().popBackStack()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onLoginClicked(): Unit = with(binding) {
|
private fun onLoginClicked(): Unit = with(binding) {
|
||||||
Timber.v("onLoginClicked() called")
|
Timber.v("onLoginClicked() called")
|
||||||
|
|
||||||
@@ -57,14 +48,23 @@ class AuthenticationFragment : Fragment(R.layout.fragment_authentication) {
|
|||||||
} ?: return
|
} ?: return
|
||||||
|
|
||||||
button.isClickable = false
|
button.isClickable = false
|
||||||
viewModel.authenticate(email, pass).observe(viewLifecycleOwner) {
|
viewLifecycleOwner.lifecycleScope.launch {
|
||||||
Timber.d("onLoginClicked: result $it")
|
onAuthenticationResult(viewModel.authenticate(email, pass))
|
||||||
passwordInputLayout.error = when (it.exceptionOrNull()) {
|
}
|
||||||
is Unauthorized -> getString(R.string.fragment_authentication_credentials_incorrect)
|
}
|
||||||
|
|
||||||
|
private fun onAuthenticationResult(result: Result<Unit>) {
|
||||||
|
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
|
else -> null
|
||||||
}
|
}
|
||||||
|
|
||||||
button.isClickable = true
|
binding.button.isClickable = true
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -5,16 +5,19 @@ import timber.log.Timber
|
|||||||
enum class AuthenticationState {
|
enum class AuthenticationState {
|
||||||
AUTHORIZED,
|
AUTHORIZED,
|
||||||
AUTH_REQUESTED,
|
AUTH_REQUESTED,
|
||||||
UNAUTHORIZED;
|
UNAUTHORIZED,
|
||||||
|
UNKNOWN;
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
fun determineState(
|
fun determineState(
|
||||||
isLoginRequested: Boolean,
|
isLoginRequested: Boolean,
|
||||||
|
showLoginButton: Boolean,
|
||||||
isAuthorized: Boolean,
|
isAuthorized: Boolean,
|
||||||
): AuthenticationState {
|
): AuthenticationState {
|
||||||
Timber.v("determineState() called with: isLoginRequested = $isLoginRequested, isAuthorized = $isAuthorized")
|
Timber.v("determineState() called with: isLoginRequested = $isLoginRequested, showLoginButton = $showLoginButton, isAuthorized = $isAuthorized")
|
||||||
val result = when {
|
val result = when {
|
||||||
|
!showLoginButton -> UNKNOWN
|
||||||
isAuthorized -> AUTHORIZED
|
isAuthorized -> AUTHORIZED
|
||||||
isLoginRequested -> AUTH_REQUESTED
|
isLoginRequested -> AUTH_REQUESTED
|
||||||
else -> UNAUTHORIZED
|
else -> UNAUTHORIZED
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
package gq.kirmanak.mealient.ui.auth
|
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 dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import gq.kirmanak.mealient.data.auth.AuthRepo
|
import gq.kirmanak.mealient.data.auth.AuthRepo
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.collect
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
@@ -14,41 +18,31 @@ class AuthenticationViewModel @Inject constructor(
|
|||||||
private val authRepo: AuthRepo,
|
private val authRepo: AuthRepo,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
private val loginRequestsFlow = MutableStateFlow(false)
|
private val authRequestsFlow = MutableStateFlow(false)
|
||||||
val authenticationState: LiveData<AuthenticationState> = loginRequestsFlow.combine(
|
private val showLoginButtonFlow = MutableStateFlow(false)
|
||||||
flow = authRepo.isAuthorizedFlow,
|
private val authenticationStateFlow = combine(
|
||||||
transform = AuthenticationState::determineState
|
authRequestsFlow,
|
||||||
).asLiveData()
|
showLoginButtonFlow,
|
||||||
val currentAuthenticationState: AuthenticationState
|
authRepo.isAuthorizedFlow,
|
||||||
get() = checkNotNull(authenticationState.value) { "Auth state flow mustn't be null" }
|
AuthenticationState::determineState
|
||||||
|
)
|
||||||
|
val authenticationStateLive: LiveData<AuthenticationState>
|
||||||
|
get() = authenticationStateFlow.asLiveData()
|
||||||
|
var authRequested: Boolean by authRequestsFlow::value
|
||||||
|
var showLoginButton: Boolean by showLoginButtonFlow::value
|
||||||
|
|
||||||
fun authenticate(username: String, password: String): LiveData<Result<Unit>> {
|
init {
|
||||||
Timber.v("authenticate() called with: username = $username, password = $password")
|
|
||||||
val result = MutableLiveData<Result<Unit>>()
|
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
runCatching {
|
authRequestsFlow.collect { isRequested ->
|
||||||
|
// Clear auth token on logout request
|
||||||
|
if (!isRequested) authRepo.logout()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun authenticate(username: String, password: String): Result<Unit> = runCatching {
|
||||||
authRepo.authenticate(username, password)
|
authRepo.authenticate(username, password)
|
||||||
}.onFailure {
|
}.onFailure {
|
||||||
Timber.e(it, "authenticate: can't authenticate")
|
Timber.e(it, "authenticate: can't authenticate")
|
||||||
result.value = Result.failure(it)
|
|
||||||
}.onSuccess {
|
|
||||||
Timber.d("authenticate: authenticated")
|
|
||||||
result.value = Result.success(Unit)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
fun logout() {
|
|
||||||
Timber.v("logout() called")
|
|
||||||
viewModelScope.launch {
|
|
||||||
loginRequestsFlow.emit(false)
|
|
||||||
authRepo.logout()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun login() {
|
|
||||||
Timber.v("login() called")
|
|
||||||
viewModelScope.launch { loginRequestsFlow.emit(true) }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -11,7 +11,7 @@ import dagger.hilt.android.AndroidEntryPoint
|
|||||||
import gq.kirmanak.mealient.R
|
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.ui.checkIfInputIsEmpty
|
import gq.kirmanak.mealient.extensions.checkIfInputIsEmpty
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import timber.log.Timber
|
|||||||
class RecipeViewHolder(
|
class RecipeViewHolder(
|
||||||
private val binding: ViewHolderRecipeBinding,
|
private val binding: ViewHolderRecipeBinding,
|
||||||
private val recipeViewModel: RecipeViewModel,
|
private val recipeViewModel: RecipeViewModel,
|
||||||
private val clickListener: (RecipeSummaryEntity) -> Unit
|
private val clickListener: (RecipeSummaryEntity) -> Unit,
|
||||||
) : RecyclerView.ViewHolder(binding.root) {
|
) : RecyclerView.ViewHolder(binding.root) {
|
||||||
private val loadingPlaceholder by lazy {
|
private val loadingPlaceholder by lazy {
|
||||||
binding.root.resources.getString(R.string.view_holder_recipe_text_placeholder)
|
binding.root.resources.getString(R.string.view_holder_recipe_text_placeholder)
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
package gq.kirmanak.mealient.ui.recipes
|
package gq.kirmanak.mealient.ui.recipes
|
||||||
|
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import androidx.lifecycle.LiveData
|
|
||||||
import androidx.lifecycle.MutableLiveData
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import androidx.paging.cachedIn
|
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.RecipeImageLoader
|
||||||
import gq.kirmanak.mealient.data.recipes.RecipeRepo
|
import gq.kirmanak.mealient.data.recipes.RecipeRepo
|
||||||
import gq.kirmanak.mealient.data.recipes.db.entity.RecipeSummaryEntity
|
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 kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class RecipeViewModel @Inject constructor(
|
class RecipeViewModel @Inject constructor(
|
||||||
private val recipeRepo: RecipeRepo,
|
recipeRepo: RecipeRepo,
|
||||||
private val recipeImageLoader: RecipeImageLoader
|
private val recipeImageLoader: RecipeImageLoader
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
private var _isRefreshing = MutableLiveData<Boolean>()
|
|
||||||
val isRefreshing: LiveData<Boolean> get() = _isRefreshing
|
|
||||||
|
|
||||||
private val _nextRecipeInfoChannel = Channel<RecipeSummaryEntity>()
|
val pagingData = recipeRepo.createPager().flow.cachedIn(viewModelScope)
|
||||||
val nextRecipeInfo: Flow<RecipeSummaryEntity> =
|
|
||||||
_nextRecipeInfoChannel.receiveAsFlow()
|
|
||||||
|
|
||||||
val adapter = RecipesPagingAdapter(this) {
|
|
||||||
Timber.d("onClick: recipe clicked $it")
|
|
||||||
viewModelScope.launch { _nextRecipeInfoChannel.send(it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
init {
|
|
||||||
setupAdapter()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun loadRecipeImage(view: ImageView, recipeSummary: RecipeSummaryEntity?) {
|
fun loadRecipeImage(view: ImageView, recipeSummary: RecipeSummaryEntity?) {
|
||||||
Timber.v("loadRecipeImage() called with: view = $view, recipeSummary = $recipeSummary")
|
Timber.v("loadRecipeImage() called with: view = $view, recipeSummary = $recipeSummary")
|
||||||
@@ -45,21 +26,4 @@ class RecipeViewModel @Inject constructor(
|
|||||||
recipeImageLoader.loadRecipeImage(view, recipeSummary?.slug)
|
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -6,17 +6,16 @@ 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
|
||||||
import androidx.lifecycle.lifecycleScope
|
|
||||||
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.data.recipes.db.entity.RecipeSummaryEntity
|
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.refreshRequestFlow
|
||||||
import gq.kirmanak.mealient.ui.auth.AuthenticationState
|
import gq.kirmanak.mealient.ui.auth.AuthenticationState
|
||||||
import gq.kirmanak.mealient.ui.auth.AuthenticationViewModel
|
import gq.kirmanak.mealient.ui.auth.AuthenticationViewModel
|
||||||
import gq.kirmanak.mealient.ui.refreshesLiveData
|
|
||||||
import kotlinx.coroutines.flow.collect
|
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
@@ -27,7 +26,8 @@ class RecipesFragment : Fragment(R.layout.fragment_recipes) {
|
|||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
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) {
|
private fun onAuthStateChange(authenticationState: AuthenticationState) {
|
||||||
@@ -40,6 +40,7 @@ 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")
|
||||||
|
authViewModel.showLoginButton = true
|
||||||
setupRecipeAdapter()
|
setupRecipeAdapter()
|
||||||
(requireActivity() as? AppCompatActivity)?.supportActionBar?.title = null
|
(requireActivity() as? AppCompatActivity)?.supportActionBar?.title = null
|
||||||
}
|
}
|
||||||
@@ -56,20 +57,19 @@ class RecipesFragment : Fragment(R.layout.fragment_recipes) {
|
|||||||
|
|
||||||
private fun setupRecipeAdapter() {
|
private fun setupRecipeAdapter() {
|
||||||
Timber.v("setupRecipeAdapter() called")
|
Timber.v("setupRecipeAdapter() called")
|
||||||
binding.recipes.adapter = viewModel.adapter
|
val adapter = RecipesPagingAdapter(viewModel, ::navigateToRecipeInfo)
|
||||||
viewModel.isRefreshing.observe(viewLifecycleOwner) {
|
binding.recipes.adapter = adapter
|
||||||
Timber.d("setupRecipeAdapter: isRefreshing = $it")
|
collectWithViewLifecycle(viewModel.pagingData) {
|
||||||
binding.refresher.isRefreshing = it
|
Timber.v("setupRecipeAdapter: received data update")
|
||||||
|
adapter.submitData(lifecycle, it)
|
||||||
}
|
}
|
||||||
binding.refresher.refreshesLiveData().observe(viewLifecycleOwner) {
|
collectWithViewLifecycle(adapter.onPagesUpdatedFlow) {
|
||||||
Timber.d("setupRecipeAdapter: received refresh request")
|
Timber.v("setupRecipeAdapter: pages updated")
|
||||||
viewModel.adapter.refresh()
|
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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,5 +78,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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -9,8 +9,8 @@ import androidx.navigation.NavDirections
|
|||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import gq.kirmanak.mealient.R
|
import gq.kirmanak.mealient.R
|
||||||
import gq.kirmanak.mealient.ui.setActionBarVisibility
|
import gq.kirmanak.mealient.extensions.setActionBarVisibility
|
||||||
import gq.kirmanak.mealient.ui.setSystemUiVisibility
|
import gq.kirmanak.mealient.extensions.setSystemUiVisibility
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
|
|||||||
Reference in New Issue
Block a user