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) =