Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,10 @@ android {
buildFeatures {
buildConfig = true
viewBinding = true
compose = true
}
composeOptions {
kotlinCompilerExtensionVersion = Compose.Version.compiler
}
}

Expand All @@ -119,6 +123,9 @@ dependencies {
implementation(Navigation.uiKtx)
implementation(Koin.android)
implementation(Timber.timber)
implementation(Compose.ui)
implementation(Compose.material)
implementation(Compose.materialIconsExtended)

testImplementation(Junit.junit)
testImplementation(MockK.core)
Expand Down
208 changes: 154 additions & 54 deletions app/src/main/java/br/com/sailboat/todozy/home/HomeActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,99 +4,185 @@ import android.content.Context
import android.content.Intent
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.material.BottomNavigation
import androidx.compose.material.BottomNavigationItem
import androidx.compose.material.ContentAlpha
import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ListAlt
import androidx.compose.material.icons.filled.History
import androidx.compose.material.icons.filled.Settings
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.compose.ui.res.stringResource
import androidx.core.view.isVisible
import androidx.navigation.NavController
import androidx.fragment.app.commit
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
import br.com.sailboat.uicomponent.impl.theme.TodozyTheme
import br.com.sailboat.todozy.R as AppR
import br.com.sailboat.uicomponent.impl.R as UiR

class HomeActivity : AppCompatActivity(), HomeTabNavigator {
private lateinit var binding: ActivityHomeBinding
private var selectedTabId by mutableStateOf(AppR.id.nav_tasks)

private val rootDestinations =
setOf(
R.id.nav_tasks,
R.id.nav_history,
R.id.nav_settings,
private val navHostIds =
mapOf(
AppR.id.nav_tasks to AppR.id.tasks_nav_host,
AppR.id.nav_history to AppR.id.history_nav_host,
AppR.id.nav_settings to AppR.id.settings_nav_host,
)
private val navGraphIds =
mapOf(
AppR.id.nav_tasks to AppR.navigation.nav_tasks,
AppR.id.nav_history to AppR.navigation.nav_history,
AppR.id.nav_settings to AppR.navigation.nav_settings,
)
private val bottomBarItems =
listOf(
BottomBarItem(
id = AppR.id.nav_tasks,
icon = Icons.AutoMirrored.Filled.ListAlt,
title = UiR.string.label_tasks,
),
BottomBarItem(
id = AppR.id.nav_history,
icon = Icons.Filled.History,
title = UiR.string.history,
),
BottomBarItem(
id = AppR.id.nav_settings,
icon = Icons.Filled.Settings,
title = UiR.string.settings,
),
)

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityHomeBinding.inflate(layoutInflater)
setContentView(binding.root)

val navController = resolveNavController()
configureBottomNav(navController)
applyStartDestination(navController)
ensureNavHostsCreated()
configureBottomBar()
applyStartDestination()
}

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)
private fun configureBottomBar() {
binding.homeBottomNav.setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
binding.homeBottomNav.setContent {
TodozyTheme {
BottomNavigation(
backgroundColor = MaterialTheme.colors.surface,
contentColor = MaterialTheme.colors.onSurface,
) {
bottomBarItems.forEach { item ->
val selected = selectedTabId == item.id
BottomNavigationItem(
selected = selected,
onClick = { onBottomTabSelected(item.id) },
icon = {
Icon(imageVector = item.icon, contentDescription = stringResource(id = item.title))
},
label = { Text(text = stringResource(id = item.title)) },
selectedContentColor = MaterialTheme.colors.primary,
unselectedContentColor = MaterialTheme.colors.onSurface.copy(alpha = ContentAlpha.medium),
alwaysShowLabel = true,
)
}
}
}
}

navController.addOnDestinationChangedListener { _, destination, _ ->
binding.homeBottomNav.isVisible = destination.id in rootDestinations
}
}

private fun applyStartDestination(navController: NavController) {
private fun applyStartDestination() {
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
HomeDestination.HISTORY -> AppR.id.nav_history
HomeDestination.SETTINGS -> AppR.id.nav_settings
else -> AppR.id.nav_tasks
}
if (binding.homeBottomNav.selectedItemId != targetDestination) {
binding.homeBottomNav.selectedItemId = targetDestination
navigateToRoot(targetDestination, navController)
}
selectTab(targetDestination, allowReselectPop = false)
}

private fun navigateToRoot(
private fun onBottomTabSelected(itemId: Int) {
selectTab(itemId, allowReselectPop = true)
}

private fun selectTab(
itemId: Int,
navController: NavController,
allowReselectPop: Boolean,
) {
val options =
navOptions {
launchSingleTop = true
restoreState = true
popUpTo(navController.graph.startDestinationId) {
saveState = true
}
if (itemId == AppR.id.nav_history) {
notifyHistoryTabSelected()
}

if (allowReselectPop && itemId == selectedTabId) {
popToRoot(itemId)
return
}

selectedTabId = itemId
showNavHost(itemId)
}

private fun showNavHost(itemId: Int) {
val transaction = supportFragmentManager.beginTransaction()

navHostIds.forEach { (destinationId, containerId) ->
val fragment = supportFragmentManager.findFragmentById(containerId) ?: return@forEach
val containerView = binding.root.findViewById<androidx.fragment.app.FragmentContainerView>(containerId)
if (destinationId == itemId) {
containerView?.isVisible = true
transaction.show(fragment)
transaction.setPrimaryNavigationFragment(fragment)
} else {
containerView?.isVisible = false
transaction.hide(fragment)
}
navController.navigate(itemId, null, options)
}

transaction.commit()
}

private fun resolveNavController(): NavController {
val navHost =
supportFragmentManager.findFragmentById(R.id.home_nav_host) as NavHostFragment
return navHost.navController
private fun popToRoot(itemId: Int) {
val controller = navHostFragmentFor(itemId).navController
controller.popBackStack(controller.graph.startDestinationId, inclusive = false)
}

private fun ensureNavHostsCreated() {
navHostIds.keys.forEach { navHostFragmentFor(it) }
}

fun navHostFragmentFor(itemId: Int): NavHostFragment {
val containerId = navHostIds.getValue(itemId)
val existing = supportFragmentManager.findFragmentById(containerId) as? NavHostFragment
if (existing != null) return existing

val navHostFragment = NavHostFragment.create(navGraphIds.getValue(itemId))
supportFragmentManager.commit {
setReorderingAllowed(true)
replace(containerId, navHostFragment)
}
supportFragmentManager.executePendingTransactions()
return navHostFragment
}

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
HomeDestination.TASKS -> AppR.id.nav_tasks
HomeDestination.HISTORY -> AppR.id.nav_history
HomeDestination.SETTINGS -> AppR.id.nav_settings
}
if (binding.homeBottomNav.selectedItemId != targetId) {
binding.homeBottomNav.selectedItemId = targetId
}
navigateToRoot(targetId, navController)
selectTab(targetId, allowReselectPop = false)
}

companion object {
Expand All @@ -108,3 +194,17 @@ class HomeActivity : AppCompatActivity(), HomeTabNavigator {
}
}
}

private const val HISTORY_REFRESH_KEY = "history-refresh-request"

private data class BottomBarItem(
val id: Int,
val icon: androidx.compose.ui.graphics.vector.ImageVector,
val title: Int,
)

private fun HomeActivity.notifyHistoryTabSelected() {
val navController = this.navHostFragmentFor(AppR.id.nav_history).navController
val historyBackStackEntry = navController.getBackStackEntry(AppR.id.nav_history)
historyBackStackEntry.savedStateHandle[HISTORY_REFRESH_KEY] = System.currentTimeMillis()
}
37 changes: 29 additions & 8 deletions app/src/main/res/layout/activity_home.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,28 +5,49 @@
android:layout_height="match_parent">

<androidx.fragment.app.FragmentContainerView
android:id="@+id/home_nav_host"
android:id="@+id/tasks_nav_host"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="0dp"
android:layout_height="0dp"
android:visibility="visible"
app:defaultNavHost="true"
app:layout_constraintBottom_toTopOf="@id/home_bottom_nav"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:navGraph="@navigation/nav_home" />
app:navGraph="@navigation/nav_tasks" />

<com.google.android.material.bottomnavigation.BottomNavigationView
<androidx.fragment.app.FragmentContainerView
android:id="@+id/history_nav_host"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="0dp"
android:layout_height="0dp"
android:visibility="gone"
app:layout_constraintBottom_toTopOf="@id/home_bottom_nav"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:navGraph="@navigation/nav_history" />

<androidx.fragment.app.FragmentContainerView
android:id="@+id/settings_nav_host"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="0dp"
android:layout_height="0dp"
android:visibility="gone"
app:layout_constraintBottom_toTopOf="@id/home_bottom_nav"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:navGraph="@navigation/nav_settings" />

<androidx.compose.ui.platform.ComposeView
android:id="@+id/home_bottom_nav"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:background="?attr/colorSurface"
app:itemIconTint="@color/selector_bottom_nav"
app:itemTextColor="@color/selector_bottom_nav"
app:labelVisibilityMode="labeled"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:menu="@menu/menu_home_bottom_nav" />
app:layout_constraintStart_toStartOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>
10 changes: 10 additions & 0 deletions app/src/main/res/navigation/nav_history.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/nav_history_graph"
app:startDestination="@id/nav_history">

<fragment
android:id="@+id/nav_history"
android:name="br.com.sailboat.todozy.feature.task.history.impl.presentation.TaskHistoryFragment" />
</navigation>
23 changes: 0 additions & 23 deletions app/src/main/res/navigation/nav_home.xml

This file was deleted.

10 changes: 10 additions & 0 deletions app/src/main/res/navigation/nav_settings.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/nav_settings_graph"
app:startDestination="@id/nav_settings">

<fragment
android:id="@+id/nav_settings"
android:name="br.com.sailboat.todozy.feature.settings.impl.presentation.SettingsFragment" />
</navigation>
10 changes: 10 additions & 0 deletions app/src/main/res/navigation/nav_tasks.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/nav_tasks_graph"
app:startDestination="@id/nav_tasks">

<fragment
android:id="@+id/nav_tasks"
android:name="br.com.sailboat.todozy.feature.task.list.impl.presentation.TaskListFragment" />
</navigation>
Loading