Skip to content

Commit

Permalink
Add: Fix BackHandler
Browse files Browse the repository at this point in the history
  • Loading branch information
Velord committed Mar 24, 2023
1 parent baeaf98 commit 2d2566d
Show file tree
Hide file tree
Showing 3 changed files with 129 additions and 137 deletions.
Original file line number Diff line number Diff line change
@@ -1,38 +1,37 @@
package com.velord.composemultiplebackstackdemo.ui.main.bottomNavigation

import android.content.Context
import android.os.Bundle
import android.util.Log
import android.view.View
import androidx.activity.OnBackPressedCallback
import androidx.activity.compose.BackHandler
import androidx.activity.compose.LocalOnBackPressedDispatcherOwner
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.scale
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.core.view.forEach
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.navigation.NavController
import androidx.navigation.NavDestination
import androidx.navigation.fragment.findNavController
import androidx.navigation.ui.NavigationUI
import com.velord.composemultiplebackstackdemo.R
import com.velord.composemultiplebackstackdemo.databinding.FragmentBottomNavBinding
import com.velord.composemultiplebackstackdemo.ui.compose.theme.MainTheme
import com.velord.composemultiplebackstackdemo.ui.compose.theme.setContentWithTheme
import com.velord.composemultiplebackstackdemo.ui.navigation.BottomNavigationItem
import com.velord.composemultiplebackstackdemo.ui.utils.viewLifecycleScope
import com.velord.multiplebackstackapplier.MultipleBackstackApplier
import com.velord.multiplebackstackapplier.MultipleBackstackApplier.matchDestination
import com.velord.multiplebackstackapplier.MultipleBackstack
import com.velord.multiplebackstackapplier.utils.compose.SnackBarOnBackPressHandler
import kotlinx.coroutines.launch

Expand All @@ -44,31 +43,36 @@ class BottomNavFragment : Fragment(R.layout.fragment_bottom_nav) {
private val viewModel by viewModels<BottomNavViewModel>()
private var binding: FragmentBottomNavBinding? = null

private var listener: NavController.OnDestinationChangedListener =
NavController.OnDestinationChangedListener { controller, destination, _ ->
MultipleBackstackApplier.createNavigationBarMenu(
requireContext(),
viewModel.getNavigationItems()
).forEach { item ->
if (destination.matchDestination(item.itemId)) {
item.isChecked = true
val current = controller.currentDestination
viewModel.updateBackHandling(current)
}
private val multipleBackStack by lazy {
MultipleBackstack(
navController = lazy { navController },
lifecycleOwner = this,
context = requireContext(),
items = viewModel.getNavigationItems(),
flowOnSelect = viewModel.currentTabFlow,
onMenuChange = {
val current = navController.currentDestination
viewModel.updateBackHandling(current)
}
}
)
}

override fun onDestroy() {
binding = null
lifecycle.removeObserver(multipleBackStack)
super.onDestroy()
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycle.addObserver(multipleBackStack)
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)

binding = FragmentBottomNavBinding.bind(view).apply {
initView()
initMultipleBackStack()
}
initObserving()
}
Expand All @@ -80,62 +84,11 @@ class BottomNavFragment : Fragment(R.layout.fragment_bottom_nav) {
}
}

override fun onResume() {
super.onResume()
listener?.let {
Log.d("@@@", "onResume")
navController.addOnDestinationChangedListener(it)
}
}

override fun onPause() {
listener?.let {
Log.d("@@@", "onPause")
navController.removeOnDestinationChangedListener(it)
}
super.onPause()
}

context(FragmentBottomNavBinding)
private fun initMultipleBackStack() {
// Log.d("@@@", "initMultipleBackStack")
// if (listener == null) {
// listener = MultipleBackstackApplier.setupWithNavController(
// items = viewModel.getNavigationItems(),
// navigationView = bottomNavBarView,
// lifecycleOwner = this@BottomNavFragment,
// navController = navController,
// flowOnSelect = viewModel.currentTabFlow,
// ) {
// val destination = navController.currentDestination
// viewModel.updateBackHandling(destination)
// }
// }
}

private fun initObserving() {
viewLifecycleScope.launch {
launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.finishAppEvent.collect {
requireActivity().finish()
}
}
}
launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.currentTabFlow.collect {
Log.d("@@@", "currentTabFlow.collect: $it")
val menu = MultipleBackstackApplier.createNavigationBarMenu(
requireContext(),
viewModel.getNavigationItems()
)
val menuItem = menu.findItem(it.navigationGraphId)
NavigationUI.onNavDestinationSelected(
item = menuItem,
navController = navController
)
}
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.finishAppEvent.collect {
requireActivity().finish()
}
}
}
Expand All @@ -145,27 +98,25 @@ class BottomNavFragment : Fragment(R.layout.fragment_bottom_nav) {
@Composable
private fun BottomNavScreen(viewModel: BottomNavViewModel) {
val tabFlow = viewModel.currentTabFlow.collectAsStateWithLifecycle()
val isBackHandlingEnabledState = viewModel.isBackHandlingEnabledFlow.collectAsStateWithLifecycle()
val isBackHandlingEnabledState =
viewModel.isBackHandlingEnabledFlow.collectAsStateWithLifecycle()

Content(
selectedItem = tabFlow.value,
onClick = viewModel::onTabClick,
)

val str = stringResource(id = R.string.press_again_to_exit)
BackHandler(isBackHandlingEnabledState.value) {
Log.d("@@@", "BackHandler")
SnackBarOnBackPressHandler(
message = str,
modifier = Modifier.padding(horizontal = 8.dp),
enabled = isBackHandlingEnabledState.value,
onBackClickLessThanDuration = viewModel::onBackDoubleClick,
) {
Snackbar {
Text(text = it.visuals.message)
}
}
// SnackBarOnBackPressHandler(
// message = str,
// modifier = Modifier.padding(horizontal = 8.dp),
// enabled = isBackHandlingEnabledState.value,
// onBackClickLessThanDuration = viewModel::onBackDoubleClick,
// ) {
// Snackbar {
// Text(text = it.visuals.message)
// }
// }
}

@Composable
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,13 @@ import android.view.MenuItem
import android.view.View
import androidx.annotation.IdRes
import androidx.core.view.forEach
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.findViewTreeLifecycleOwner
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.lifecycle.*
import androidx.navigation.NavController
import androidx.navigation.NavDestination
import androidx.navigation.NavDestination.Companion.hierarchy
import androidx.navigation.ui.NavigationUI
import com.google.android.material.navigation.NavigationBarMenu
import com.velord.multiplebackstackapplier.MultipleBackstackApplier.matchDestination
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.collectLatest
Expand All @@ -43,54 +40,64 @@ object MultipleBackstackApplier {
}
}

@SuppressLint("RestrictedApi")
fun setupWithNavController(
fun createListener(
context: Context,
items: List<MultipleBackstackGraphItem>,
navigationView: View,
navController: NavController,
flowOnSelect: Flow<MultipleBackstackGraphItem>,
onMenuChange: (MenuItem) -> Unit,
) {
val menu = createNavigationBarMenu(navigationView.context, items)

navigationView.findViewTreeLifecycleOwner()?.let {
it.lifecycleScope.launch {
it.repeatOnLifecycle(Lifecycle.State.STARTED) {
flowOnSelect.collectLatest { navItem ->
val menuItem = menu.findItem(navItem.navigationGraphId)
NavigationUI.onNavDestinationSelected(
item = menuItem,
navController = navController
)
}
}
onChangeDestination: (MenuItem) -> Unit
): NavController.OnDestinationChangedListener = NavController.OnDestinationChangedListener {
_, destination, _ ->
createNavigationBarMenu(context, items).forEach { item ->
if (destination.matchDestination(item.itemId)) {
item.isChecked = true
onChangeDestination(item)
}
}
val weakReference = WeakReference(navigationView)
navController.addOnDestinationChangedListener(
object : NavController.OnDestinationChangedListener {
override fun onDestinationChanged(
controller: NavController,
destination: NavDestination,
arguments: Bundle?
) {
val view = weakReference.get()
if (view == null) {
navController.removeOnDestinationChangedListener(this)
return
}
menu.forEach { item ->
if (destination.matchDestination(item.itemId)) {
item.isChecked = true
onMenuChange(item)
}
}
}
}
)
}

// Copy from androidx.navigation.ui.NavigationUI. Cause it's internal.
fun NavDestination.matchDestination(@IdRes destId: Int): Boolean =
internal fun NavDestination.matchDestination(@IdRes destId: Int): Boolean =
hierarchy.any { it.id == destId }
}

class MultipleBackstack(
private val navController: Lazy<NavController>,
private val lifecycleOwner: LifecycleOwner,
private val context: Context,
private val items: List<MultipleBackstackGraphItem>,
private val flowOnSelect: Flow<MultipleBackstackGraphItem>,
onMenuChange: (MenuItem) -> Unit,
) : DefaultLifecycleObserver {

private var listener: NavController.OnDestinationChangedListener =
MultipleBackstackApplier.createListener(context, items, onMenuChange)

init {
observe()
}

override fun onPause(owner: LifecycleOwner) {
super.onPause(owner)
navController.value.removeOnDestinationChangedListener(listener)
}

override fun onResume(owner: LifecycleOwner) {
navController.value.addOnDestinationChangedListener(listener)
super.onResume(owner)
}

private fun observe() {
lifecycleOwner.lifecycleScope.launch {
lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
flowOnSelect.collectLatest { navItem ->
Log.d("@@@", "onSelect: ${navItem.navigationGraphId}")
val menu = MultipleBackstackApplier.createNavigationBarMenu(context, items)
val menuItem = menu.findItem(navItem.navigationGraphId)
NavigationUI.onNavDestinationSelected(
item = menuItem,
navController = navController.value
)
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,19 +1,54 @@
package com.velord.multiplebackstackapplier.utils.compose

import android.util.Log
import androidx.activity.compose.BackHandler
import androidx.activity.OnBackPressedCallback
import androidx.activity.compose.LocalOnBackPressedDispatcherOwner
import androidx.compose.material3.SnackbarData
import androidx.compose.material3.SnackbarDuration
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalLifecycleOwner
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch

private const val DELAY_TO_EXIT_APP = 3000L
private const val DEFAULT_TIME = 0L

@Composable
fun BackHandler(enabled: Boolean = true, onBack: () -> Unit) {
// Safely update the current `onBack` lambda when a new one is provided
val currentOnBack by rememberUpdatedState(onBack)
// Remember in Composition a back callback that calls the `onBack` lambda
val backCallback = remember {
object : OnBackPressedCallback(enabled) {
override fun handleOnBackPressed() {
currentOnBack()
}
}
}
// On every successful composition, update the callback with the `enabled` value
SideEffect {
backCallback.isEnabled = enabled
}
val backDispatcher = checkNotNull(LocalOnBackPressedDispatcherOwner.current) {
"No OnBackPressedDispatcherOwner was provided via LocalOnBackPressedDispatcherOwner"
}.onBackPressedDispatcher
val lifecycleOwner = LocalLifecycleOwner.current
// "enabled" is a key which guruarantees
// addCallback will be triggered when user leaves composition and return back a.k.a resubscribing
DisposableEffect(lifecycleOwner, backDispatcher, enabled) {
// Add callback to the backDispatcher
Log.d("@@@", "addCallback")
backDispatcher.addCallback(lifecycleOwner, backCallback)
// When the effect leaves the Composition, remove the callback
onDispose {
backCallback.remove()
}
}
}

@Composable
fun OnBackPressHandler(
enabled: Boolean = true,
Expand All @@ -33,7 +68,6 @@ fun OnBackPressHandler(
mutableStateOf(DEFAULT_TIME)
}

//Log.d("@@@", "OnBackPressHandler: $enabled")
BackHandler(enabled) {
triggerState.value = System.currentTimeMillis()
}
Expand Down

0 comments on commit 2d2566d

Please sign in to comment.