From 8e52c19237842561a2408fde69129571dc6dd7d3 Mon Sep 17 00:00:00 2001 From: Brayan Bedritchuk Date: Sat, 6 Dec 2025 19:31:04 -0300 Subject: [PATCH] Add bottom navigation bar --- app/build.gradle.kts | 3 + app/src/main/AndroidManifest.xml | 4 + .../main/java/br/com/sailboat/todozy/App.kt | 1 + .../br/com/sailboat/todozy/di/DiProvider.kt | 2 + .../com/sailboat/todozy/home/HomeActivity.kt | 110 ++++++++++++++++++ .../todozy/navigation/AppNavigationModule.kt | 57 +++++++++ .../main/res/color/selector_bottom_nav.xml | 5 + app/src/main/res/layout/activity_home.xml | 32 +++++ .../main/res/menu/menu_home_bottom_nav.xml | 15 +++ app/src/main/res/navigation/nav_home.xml | 23 ++++ buildSrc/src/main/java/Dependency.kt | 9 ++ .../navigation/android/HomeDestination.kt | 15 +++ .../impl/presentation/TaskListFragment.kt | 20 +++- .../list/impl/presentation/TaskListScreen.kt | 49 ++++---- 14 files changed, 310 insertions(+), 35 deletions(-) create mode 100644 app/src/main/java/br/com/sailboat/todozy/home/HomeActivity.kt create mode 100644 app/src/main/java/br/com/sailboat/todozy/navigation/AppNavigationModule.kt create mode 100644 app/src/main/res/color/selector_bottom_nav.xml create mode 100644 app/src/main/res/layout/activity_home.xml create mode 100644 app/src/main/res/menu/menu_home_bottom_nav.xml create mode 100644 app/src/main/res/navigation/nav_home.xml create mode 100644 feature/navigation/public-android/src/main/java/br/com/sailboat/todozy/feature/navigation/android/HomeDestination.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 861146b4..efbf93e6 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -113,7 +113,10 @@ dependencies { implementation(project(Module.splashImpl)) implementation(project(Module.platformImpl)) implementation(project(Module.uiComponentImpl)) + implementation(project(Module.navigationPublicAndroid)) + implementation(Navigation.fragmentKtx) + implementation(Navigation.uiKtx) implementation(Koin.android) implementation(Timber.timber) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 26601a97..3488b133 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -16,6 +16,10 @@ android:theme="@style/AppTheme" tools:replace="android:icon"> + diff --git a/app/src/main/java/br/com/sailboat/todozy/App.kt b/app/src/main/java/br/com/sailboat/todozy/App.kt index e79e1851..1f67439f 100644 --- a/app/src/main/java/br/com/sailboat/todozy/App.kt +++ b/app/src/main/java/br/com/sailboat/todozy/App.kt @@ -14,6 +14,7 @@ internal class App : Application() { override fun onCreate() { super.onCreate() startKoin { + allowOverride(true) androidLogger(Level.ERROR) androidContext(this@App) modules(DiProvider.modules) diff --git a/app/src/main/java/br/com/sailboat/todozy/di/DiProvider.kt b/app/src/main/java/br/com/sailboat/todozy/di/DiProvider.kt index 5d35b8e4..b7b7777b 100644 --- a/app/src/main/java/br/com/sailboat/todozy/di/DiProvider.kt +++ b/app/src/main/java/br/com/sailboat/todozy/di/DiProvider.kt @@ -8,6 +8,7 @@ import br.com.sailboat.todozy.feature.task.details.impl.di.taskDetailsModule import br.com.sailboat.todozy.feature.task.form.impl.di.taskFormModule import br.com.sailboat.todozy.feature.task.history.impl.di.taskHistoryModule import br.com.sailboat.todozy.feature.task.list.impl.di.taskListModule +import br.com.sailboat.todozy.navigation.appNavigationModule import br.com.sailboat.todozy.platform.impl.di.platformModule import br.com.sailboat.uicomponent.impl.di.uiComponentModule import org.koin.core.module.Module @@ -25,5 +26,6 @@ internal object DiProvider { taskListModule, taskDetailsModule, uiComponentModule, + appNavigationModule, ).flatten() } diff --git a/app/src/main/java/br/com/sailboat/todozy/home/HomeActivity.kt b/app/src/main/java/br/com/sailboat/todozy/home/HomeActivity.kt new file mode 100644 index 00000000..3b74e452 --- /dev/null +++ b/app/src/main/java/br/com/sailboat/todozy/home/HomeActivity.kt @@ -0,0 +1,110 @@ +package br.com.sailboat.todozy.home + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity +import androidx.core.view.isVisible +import androidx.navigation.NavController +import androidx.navigation.fragment.NavHostFragment +import androidx.navigation.navOptions +import br.com.sailboat.todozy.R +import br.com.sailboat.todozy.databinding.ActivityHomeBinding +import br.com.sailboat.todozy.feature.navigation.android.HomeDestination +import br.com.sailboat.todozy.feature.navigation.android.HomeNavigationExtras.EXTRA_HOME_DESTINATION +import br.com.sailboat.todozy.feature.navigation.android.HomeTabNavigator + +class HomeActivity : AppCompatActivity(), HomeTabNavigator { + private lateinit var binding: ActivityHomeBinding + + private val rootDestinations = + setOf( + R.id.nav_tasks, + R.id.nav_history, + R.id.nav_settings, + ) + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = ActivityHomeBinding.inflate(layoutInflater) + setContentView(binding.root) + + val navController = resolveNavController() + configureBottomNav(navController) + applyStartDestination(navController) + } + + private fun configureBottomNav(navController: NavController) { + binding.homeBottomNav.setOnItemSelectedListener { item -> + navigateToRoot(item.itemId, navController) + true + } + + binding.homeBottomNav.setOnItemReselectedListener { item -> + if (navController.currentDestination?.id == item.itemId) { + navController.popBackStack(item.itemId, inclusive = false) + } + } + + navController.addOnDestinationChangedListener { _, destination, _ -> + binding.homeBottomNav.isVisible = destination.id in rootDestinations + } + } + + private fun applyStartDestination(navController: NavController) { + val targetDestination = + when (intent.getSerializableExtra(EXTRA_HOME_DESTINATION) as? HomeDestination) { + HomeDestination.HISTORY -> R.id.nav_history + HomeDestination.SETTINGS -> R.id.nav_settings + else -> R.id.nav_tasks + } + if (binding.homeBottomNav.selectedItemId != targetDestination) { + binding.homeBottomNav.selectedItemId = targetDestination + navigateToRoot(targetDestination, navController) + } + } + + private fun navigateToRoot( + itemId: Int, + navController: NavController, + ) { + val options = + navOptions { + launchSingleTop = true + restoreState = true + popUpTo(navController.graph.startDestinationId) { + saveState = true + } + } + navController.navigate(itemId, null, options) + } + + private fun resolveNavController(): NavController { + val navHost = + supportFragmentManager.findFragmentById(R.id.home_nav_host) as NavHostFragment + return navHost.navController + } + + override fun switchTo(destination: HomeDestination) { + val navController = resolveNavController() + val targetId = + when (destination) { + HomeDestination.TASKS -> R.id.nav_tasks + HomeDestination.HISTORY -> R.id.nav_history + HomeDestination.SETTINGS -> R.id.nav_settings + } + if (binding.homeBottomNav.selectedItemId != targetId) { + binding.homeBottomNav.selectedItemId = targetId + } + navigateToRoot(targetId, navController) + } + + companion object { + fun createIntent( + context: Context, + destination: HomeDestination = HomeDestination.TASKS, + ): Intent = Intent(context, HomeActivity::class.java).apply { + putExtra(EXTRA_HOME_DESTINATION, destination) + } + } +} diff --git a/app/src/main/java/br/com/sailboat/todozy/navigation/AppNavigationModule.kt b/app/src/main/java/br/com/sailboat/todozy/navigation/AppNavigationModule.kt new file mode 100644 index 00000000..5412e315 --- /dev/null +++ b/app/src/main/java/br/com/sailboat/todozy/navigation/AppNavigationModule.kt @@ -0,0 +1,57 @@ +package br.com.sailboat.todozy.navigation + +import android.content.Context +import android.content.Intent +import androidx.activity.result.ActivityResultLauncher +import br.com.sailboat.todozy.feature.navigation.android.HomeDestination +import br.com.sailboat.todozy.feature.navigation.android.SettingsNavigator +import br.com.sailboat.todozy.feature.navigation.android.TaskHistoryNavigator +import br.com.sailboat.todozy.feature.navigation.android.TaskListNavigator +import br.com.sailboat.todozy.home.HomeActivity +import org.koin.core.module.Module +import org.koin.dsl.module + +private fun Context.startHome(destination: HomeDestination) { + val intent = HomeActivity.createIntent(this, destination) + startActivity(intent) +} + +private fun ActivityResultLauncher.launchHome( + context: Context, + destination: HomeDestination, +) { + val intent = HomeActivity.createIntent(context, destination) + launch(intent) +} + +internal val appNavigationModule: List = + listOf( + module(override = true) { + factory { + object : TaskListNavigator { + override fun navigateToTaskList(context: Context) { + context.startHome(HomeDestination.TASKS) + } + } + } + + factory { + object : TaskHistoryNavigator { + override fun navigateToTaskHistory(context: Context) { + context.startHome(HomeDestination.HISTORY) + } + } + } + + factory { + object : SettingsNavigator { + override fun navigateToSettings( + context: Context, + launcher: ActivityResultLauncher, + ) { + launcher.launchHome(context, HomeDestination.SETTINGS) + } + } + } + }, + ) diff --git a/app/src/main/res/color/selector_bottom_nav.xml b/app/src/main/res/color/selector_bottom_nav.xml new file mode 100644 index 00000000..8f023062 --- /dev/null +++ b/app/src/main/res/color/selector_bottom_nav.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/layout/activity_home.xml b/app/src/main/res/layout/activity_home.xml new file mode 100644 index 00000000..d86b697f --- /dev/null +++ b/app/src/main/res/layout/activity_home.xml @@ -0,0 +1,32 @@ + + + + + + + + diff --git a/app/src/main/res/menu/menu_home_bottom_nav.xml b/app/src/main/res/menu/menu_home_bottom_nav.xml new file mode 100644 index 00000000..1d930825 --- /dev/null +++ b/app/src/main/res/menu/menu_home_bottom_nav.xml @@ -0,0 +1,15 @@ + + + + + + diff --git a/app/src/main/res/navigation/nav_home.xml b/app/src/main/res/navigation/nav_home.xml new file mode 100644 index 00000000..a415d3ef --- /dev/null +++ b/app/src/main/res/navigation/nav_home.xml @@ -0,0 +1,23 @@ + + + + + + + + + + diff --git a/buildSrc/src/main/java/Dependency.kt b/buildSrc/src/main/java/Dependency.kt index 91989db8..4b188697 100644 --- a/buildSrc/src/main/java/Dependency.kt +++ b/buildSrc/src/main/java/Dependency.kt @@ -81,6 +81,15 @@ object AndroidX { const val activity = "androidx.activity:activity:${Versions.activity}" } +object Navigation { + object Version { + const val navigation = "2.7.7" + } + + const val fragmentKtx = "androidx.navigation:navigation-fragment-ktx:${Version.navigation}" + const val uiKtx = "androidx.navigation:navigation-ui-ktx:${Version.navigation}" +} + object AndroidXTest { object Version { const val test = "1.5.0" diff --git a/feature/navigation/public-android/src/main/java/br/com/sailboat/todozy/feature/navigation/android/HomeDestination.kt b/feature/navigation/public-android/src/main/java/br/com/sailboat/todozy/feature/navigation/android/HomeDestination.kt new file mode 100644 index 00000000..95094dcd --- /dev/null +++ b/feature/navigation/public-android/src/main/java/br/com/sailboat/todozy/feature/navigation/android/HomeDestination.kt @@ -0,0 +1,15 @@ +package br.com.sailboat.todozy.feature.navigation.android + +enum class HomeDestination { + TASKS, + HISTORY, + SETTINGS, +} + +interface HomeTabNavigator { + fun switchTo(destination: HomeDestination) +} + +object HomeNavigationExtras { + const val EXTRA_HOME_DESTINATION = "extra_home_destination" +} diff --git a/feature/task-list/impl/src/main/java/br/com/sailboat/todozy/feature/task/list/impl/presentation/TaskListFragment.kt b/feature/task-list/impl/src/main/java/br/com/sailboat/todozy/feature/task/list/impl/presentation/TaskListFragment.kt index 7feda67c..1ffda087 100644 --- a/feature/task-list/impl/src/main/java/br/com/sailboat/todozy/feature/task/list/impl/presentation/TaskListFragment.kt +++ b/feature/task-list/impl/src/main/java/br/com/sailboat/todozy/feature/task/list/impl/presentation/TaskListFragment.kt @@ -14,6 +14,8 @@ import androidx.compose.ui.platform.ViewCompositionStrategy import androidx.fragment.app.Fragment import br.com.sailboat.todozy.domain.model.TaskProgressRange import br.com.sailboat.todozy.feature.navigation.android.AboutNavigator +import br.com.sailboat.todozy.feature.navigation.android.HomeDestination +import br.com.sailboat.todozy.feature.navigation.android.HomeTabNavigator import br.com.sailboat.todozy.feature.navigation.android.SettingsNavigator import br.com.sailboat.todozy.feature.navigation.android.TaskDetailsNavigator import br.com.sailboat.todozy.feature.navigation.android.TaskFormNavigator @@ -113,10 +115,6 @@ internal class TaskListFragment : Fragment() { ) { super.onViewCreated(view, savedInstanceState) observeActions() - } - - override fun onResume() { - super.onResume() viewModel.dispatchViewIntent(TaskListViewIntent.OnStart) } @@ -144,7 +142,12 @@ internal class TaskListFragment : Fragment() { } private fun navigateToSettings() { - settingsNavigator.navigateToSettings(requireContext(), launcher) + val homeNavigator = activity as? HomeTabNavigator + if (homeNavigator != null) { + homeNavigator.switchTo(HomeDestination.SETTINGS) + } else { + settingsNavigator.navigateToSettings(requireContext(), launcher) + } } private fun navigateToTaskDetails(taskId: Long) { @@ -152,7 +155,12 @@ internal class TaskListFragment : Fragment() { } private fun navigateToHistory() { - taskHistoryNavigator.navigateToTaskHistory(requireContext()) + val homeNavigator = activity as? HomeTabNavigator + if (homeNavigator != null) { + homeNavigator.switchTo(HomeDestination.HISTORY) + } else { + taskHistoryNavigator.navigateToTaskHistory(requireContext()) + } } private fun showErrorLoadingTasks() { diff --git a/feature/task-list/impl/src/main/java/br/com/sailboat/todozy/feature/task/list/impl/presentation/TaskListScreen.kt b/feature/task-list/impl/src/main/java/br/com/sailboat/todozy/feature/task/list/impl/presentation/TaskListScreen.kt index 4e3b0e0f..3271418c 100644 --- a/feature/task-list/impl/src/main/java/br/com/sailboat/todozy/feature/task/list/impl/presentation/TaskListScreen.kt +++ b/feature/task-list/impl/src/main/java/br/com/sailboat/todozy/feature/task/list/impl/presentation/TaskListScreen.kt @@ -34,9 +34,7 @@ import androidx.compose.material.TextFieldDefaults import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Close -import androidx.compose.material.icons.filled.History import androidx.compose.material.icons.filled.Search -import androidx.compose.material.icons.filled.Settings import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -241,20 +239,6 @@ private fun TaskListTopBar( tint = Color.White, ) } - IconButton(onClick = onOpenHistory) { - Icon( - Icons.Default.History, - contentDescription = stringResource(id = UiR.string.history), - tint = Color.White, - ) - } - IconButton(onClick = onOpenSettings) { - Icon( - Icons.Default.Settings, - contentDescription = stringResource(id = UiR.string.settings), - tint = Color.White, - ) - } } if (isSearchExpanded) { @@ -316,19 +300,26 @@ private fun SwipeableTaskItem( onSwipe: (TaskStatus) -> Unit, modifier: Modifier = Modifier, ) { - val semanticColors = LocalTodozySemanticColors.current val spacing = LocalTodozySpacing.current - val dismissState = - androidx.compose.material.rememberDismissState( - confirmStateChange = { newValue -> - when (newValue) { - DismissValue.DismissedToEnd -> onSwipe(TaskStatus.DONE) - DismissValue.DismissedToStart -> onSwipe(TaskStatus.NOT_DONE) - else -> Unit - } - false - }, - ) + val dismissState = androidx.compose.material.rememberDismissState() + + LaunchedEffect(task.taskId, task.inlineStatus, task.showInlineMetrics) { + dismissState.snapTo(DismissValue.Default) + } + + LaunchedEffect(dismissState.currentValue) { + when (dismissState.currentValue) { + DismissValue.DismissedToEnd -> { + onSwipe(TaskStatus.DONE) + dismissState.reset() + } + DismissValue.DismissedToStart -> { + onSwipe(TaskStatus.NOT_DONE) + dismissState.reset() + } + else -> Unit + } + } if (task.showInlineMetrics) { TaskItem( @@ -341,7 +332,7 @@ private fun SwipeableTaskItem( SwipeToDismiss( state = dismissState, directions = setOf(DismissDirection.StartToEnd, DismissDirection.EndToStart), - dismissThresholds = { FractionalThreshold(0.6f) }, + dismissThresholds = { FractionalThreshold(0.35f) }, background = { val direction = dismissState.dismissDirection val (icon, backgroundColor) =