Skip to content

Commit 1b44963

Browse files
authored
MBL-2793: Search integrated within HomeActivity as navigation destination (#2466)
1 parent 92e2056 commit 1b44963

File tree

8 files changed

+417
-262
lines changed

8 files changed

+417
-262
lines changed

app/src/main/java/com/kickstarter/features/home/ui/HomeActivity.kt

Lines changed: 113 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,11 @@ import androidx.activity.viewModels
1010
import androidx.compose.foundation.background
1111
import androidx.compose.foundation.layout.Arrangement
1212
import androidx.compose.foundation.layout.Box
13-
import androidx.compose.foundation.layout.WindowInsets
1413
import androidx.compose.foundation.layout.fillMaxSize
1514
import androidx.compose.foundation.layout.fillMaxWidth
1615
import androidx.compose.foundation.layout.height
1716
import androidx.compose.foundation.layout.padding
17+
import androidx.compose.foundation.layout.systemBarsPadding
1818
import androidx.compose.foundation.lazy.LazyColumn
1919
import androidx.compose.foundation.lazy.rememberLazyListState
2020
import androidx.compose.material3.Card
@@ -30,18 +30,27 @@ import androidx.compose.ui.graphics.Color
3030
import androidx.compose.ui.tooling.preview.Preview
3131
import androidx.compose.ui.unit.dp
3232
import androidx.lifecycle.compose.collectAsStateWithLifecycle
33+
import androidx.navigation.NavGraph.Companion.findStartDestination
34+
import androidx.navigation.NavHostController
3335
import androidx.navigation.compose.NavHost
3436
import androidx.navigation.compose.composable
37+
import androidx.navigation.compose.currentBackStackEntryAsState
3538
import androidx.navigation.compose.rememberNavController
3639
import com.kickstarter.features.home.data.Tab
3740
import com.kickstarter.features.home.ui.components.FloatingBottomNav
3841
import com.kickstarter.features.home.viewmodel.HomeScreenViewModel
42+
import com.kickstarter.features.search.viewmodel.FilterMenuViewModel
43+
import com.kickstarter.features.search.viewmodel.SearchAndFilterViewModel
3944
import com.kickstarter.libs.Environment
45+
import com.kickstarter.libs.utils.ThirdPartyEventValues
4046
import com.kickstarter.libs.utils.TransitionUtils
4147
import com.kickstarter.libs.utils.extensions.getEnvironment
4248
import com.kickstarter.libs.utils.extensions.isDarkModeEnabled
49+
import com.kickstarter.ui.activities.compose.search.SearchAndFilterScreen
4350
import com.kickstarter.ui.compose.designsystem.KickstarterApp
4451
import com.kickstarter.ui.extensions.setUpConnectivityStatusCheck
52+
import com.kickstarter.ui.extensions.startPreLaunchProjectActivity
53+
import com.kickstarter.ui.extensions.startProjectActivity
4554
import com.kickstarter.ui.extensions.transition
4655
import kotlin.getValue
4756
import kotlin.random.Random
@@ -52,6 +61,12 @@ class HomeActivity : ComponentActivity() {
5261
private lateinit var viewModelFactory: HomeScreenViewModel.Factory
5362
private val viewModel: HomeScreenViewModel by viewModels { viewModelFactory }
5463

64+
// Search related VM's
65+
private lateinit var searchVMFactory: SearchAndFilterViewModel.Factory
66+
private lateinit var filterMenuViewModelFactory: FilterMenuViewModel.Factory
67+
private val searchVM: SearchAndFilterViewModel by viewModels { searchVMFactory }
68+
private val filterMenuVM: FilterMenuViewModel by viewModels { filterMenuViewModelFactory }
69+
5570
override fun onCreate(savedInstanceState: Bundle?) {
5671
super.onCreate(savedInstanceState)
5772
setUpConnectivityStatusCheck(lifecycle)
@@ -60,6 +75,9 @@ class HomeActivity : ComponentActivity() {
6075
this.getEnvironment()?.let { env ->
6176
environment = env
6277
viewModelFactory = HomeScreenViewModel.Factory(env)
78+
searchVMFactory = SearchAndFilterViewModel.Factory(environment)
79+
filterMenuViewModelFactory = FilterMenuViewModel.Factory(environment)
80+
filterMenuVM.getRootCategories()
6381
}
6482

6583
setContent {
@@ -70,12 +88,12 @@ class HomeActivity : ComponentActivity() {
7088
listOf(
7189
Tab.Home,
7290
Tab.Search,
73-
if (homeUIState.userAvatarUrl.isNotEmpty()) Tab.Profile(homeUIState.userAvatarUrl) else Tab.LogIn
91+
if (homeUIState.isLoggedInUser) Tab.Profile(homeUIState.userAvatarUrl) else Tab.LogIn
7492
)
7593
}
7694

7795
KickstarterApp(useDarkTheme = darModeEnabled) {
78-
App(tabs = tabs)
96+
App(tabs)
7997
}
8098
}
8199

@@ -86,52 +104,105 @@ class HomeActivity : ComponentActivity() {
86104
}
87105
})
88106
}
89-
}
90107

91-
@Composable
92-
@Preview(name = "Light", uiMode = Configuration.UI_MODE_NIGHT_NO)
93-
@Preview(name = "Dark", uiMode = Configuration.UI_MODE_NIGHT_YES)
94-
fun HomeActivityPreview() {
95-
val tabs = listOf(
96-
Tab.Home,
97-
Tab.Search,
98-
Tab.LogIn
99-
)
100-
KickstarterApp {
101-
App(tabs = tabs)
108+
@Composable
109+
@Preview(name = "Light", uiMode = Configuration.UI_MODE_NIGHT_NO)
110+
@Preview(name = "Dark", uiMode = Configuration.UI_MODE_NIGHT_YES)
111+
fun HomeActivityPreview() {
112+
val tabs = listOf(
113+
Tab.Home,
114+
Tab.Search,
115+
Tab.LogIn
116+
)
117+
KickstarterApp {
118+
App(tabs = tabs)
119+
}
120+
}
121+
122+
@Composable
123+
private fun App(tabs: List<Tab>) {
124+
val navController = rememberNavController()
125+
val shouldShowBottomNav = remember { mutableStateOf(true) }
126+
val backStack by navController.currentBackStackEntryAsState()
127+
val currentRoute = backStack?.destination?.route
128+
129+
val activeTab = tabs.find { it.route == currentRoute } ?: tabs.first()
130+
Scaffold(
131+
modifier = Modifier.systemBarsPadding(),
132+
bottomBar = {
133+
if (shouldShowBottomNav.value) {
134+
FloatingBottomNav(
135+
tabs = tabs,
136+
activeTab = activeTab,
137+
onTabClicked = { tab ->
138+
navController.navWithDefaults(tab.route)
139+
}
140+
)
141+
}
142+
}
143+
) { inner ->
144+
NavHost(
145+
navController = navController,
146+
startDestination = tabs.first().route,
147+
modifier = Modifier
148+
.fillMaxSize()
149+
.padding(top = inner.calculateTopPadding())
150+
) {
151+
tabs.map { tab ->
152+
when (tab) {
153+
is Tab.Search -> {
154+
composable(tab.route) {
155+
SearchAndFilterScreen(
156+
env = environment,
157+
searchViewModel = searchVM,
158+
filterMenuVM = filterMenuVM,
159+
onBackClicked = { },
160+
preLaunchedCallback = { project, tag ->
161+
startPreLaunchProjectActivity(
162+
project = project,
163+
previousScreen = ThirdPartyEventValues.ScreenName.SEARCH.value,
164+
refTag = tag
165+
)
166+
},
167+
projectCallback = { projectAndRef ->
168+
startProjectActivity(
169+
project = projectAndRef.first,
170+
refTag = projectAndRef.second,
171+
previousScreen = ThirdPartyEventValues.ScreenName.SEARCH.value
172+
)
173+
},
174+
)
175+
}
176+
}
177+
178+
else -> {
179+
composable(tab.route) {
180+
ScreenStub(tab.route)
181+
}
182+
}
183+
}
184+
}
185+
}
186+
}
102187
}
103188
}
104189

105190
/**
106-
* Home Screen composable parent UI
191+
* Navigates to a specified route using the standard default configuration for bottom navigation.
107192
*
108-
* @param tabs: Contains the list of tabs represented on the floating bottomNav
193+
* This helper ensures that:
194+
* 1. The back stack is popped up to the start destination to avoid a large stack of screens.
195+
* 2. State is saved and restored when switching between tabs.
196+
* 3. Only a single instance of a destination is launched (launchSingleTop) to prevent multiple
197+
* copies of the same screen when re-selecting a tab.
198+
*
199+
* @param route The destination route to navigate to.
109200
*/
110-
@Composable
111-
fun App(
112-
tabs: List<Tab> = listOf(Tab.Home, Tab.Search, Tab.LogIn)
113-
) {
114-
val nav = rememberNavController()
115-
val shouldShowBottomNav = remember { mutableStateOf(true) }
116-
Scaffold(
117-
contentWindowInsets = WindowInsets(0, 0, 0, 0),
118-
bottomBar = {
119-
if (shouldShowBottomNav.value) {
120-
FloatingBottomNav(nav, tabs = tabs)
121-
}
122-
}
123-
) { inner ->
124-
NavHost(
125-
navController = nav,
126-
startDestination = Tab.Home.route,
127-
modifier = Modifier
128-
.fillMaxSize()
129-
.padding(top = inner.calculateTopPadding())
130-
) {
131-
tabs.map { tab ->
132-
composable(tab.route) { ScreenStub(tab.route) }
133-
}
134-
}
201+
private fun NavHostController.navWithDefaults(route: String) {
202+
this.navigate(route) {
203+
popUpTo(this@navWithDefaults.graph.findStartDestination().id) { saveState = true }
204+
launchSingleTop = true
205+
restoreState = true
135206
}
136207
}
137208

app/src/main/java/com/kickstarter/features/home/ui/components/FloatingBottomNav.kt

Lines changed: 11 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -42,10 +42,6 @@ import androidx.compose.ui.res.painterResource
4242
import androidx.compose.ui.tooling.preview.Preview
4343
import androidx.compose.ui.unit.DpOffset
4444
import androidx.compose.ui.unit.dp
45-
import androidx.navigation.NavGraph.Companion.findStartDestination
46-
import androidx.navigation.NavHostController
47-
import androidx.navigation.compose.currentBackStackEntryAsState
48-
import androidx.navigation.compose.rememberNavController
4945
import com.kickstarter.features.home.data.Tab
5046
import com.kickstarter.features.home.data.TabIcon
5147
import com.kickstarter.features.home.ui.components.FloatingBottomNavTestTags.SLIDING_INDICATOR
@@ -61,9 +57,8 @@ fun FloatingBottomNavLoggedOutPreview() {
6157
Box(
6258
modifier = Modifier.background(Color.LightGray)
6359
) {
64-
val nav = rememberNavController()
6560
val tabs = listOf<Tab>(Tab.Home, Tab.Search, Tab.LogIn)
66-
FloatingBottomNav(nav, tabs)
61+
FloatingBottomNav(tabs)
6762
}
6863
}
6964
}
@@ -76,9 +71,8 @@ fun FloatingBottomNavLoggedInPreview() {
7671
Box(
7772
modifier = Modifier.background(Color.LightGray)
7873
) {
79-
val nav = rememberNavController()
8074
val tabs = listOf<Tab>(Tab.Home, Tab.Search, Tab.Profile(""))
81-
FloatingBottomNav(nav, tabs)
75+
FloatingBottomNav(tabs)
8276
}
8377
}
8478
}
@@ -167,21 +161,19 @@ private fun FloatingCenterNavItem(
167161

168162
@Composable
169163
fun FloatingBottomNav(
170-
nav: NavHostController,
171-
tabs: List<Tab> = listOf(Tab.Home, Tab.Search, Tab.LogIn)
164+
tabs: List<Tab> = listOf(Tab.Home, Tab.Search, Tab.LogIn),
165+
activeTab: Tab = Tab.Home, // - initial active tab will be home
166+
onTabClicked: (Tab) -> Unit = { a -> }
172167
) {
173-
// TODO: hoisting the navigation logic, FloatingCenterBottomNav should only have a callback for ie onTabSelected: (Tab) -> Unit
174-
val backStack by nav.currentBackStackEntryAsState()
175-
val current = backStack?.destination?.route
176-
val activeIndex = remember(current, tabs) {
177-
tabs.indexOfFirst { it.route == current }.coerceAtLeast(0)
178-
}
179-
180168
// - animation offSet X for sliding container
181169
val indicatorOffset = remember { Animatable(0f) }
182170
// - coordinates directory data sample: Index 0 (Home) is at 40.0f || Index 1 (Search) is at 120.0f ...
183171
val tabsXCoordinate = remember { mutableStateMapOf<Int, Float>() }
184172

173+
val activeIndex = remember(activeTab, tabs) {
174+
tabs.indexOf(activeTab).coerceAtLeast(0)
175+
}
176+
185177
LaunchedEffect(activeIndex, tabsXCoordinate.size) {
186178
tabsXCoordinate[activeIndex]?.let { targetX ->
187179
indicatorOffset.animateTo(
@@ -240,7 +232,7 @@ fun FloatingBottomNav(
240232
verticalAlignment = Alignment.CenterVertically
241233
) {
242234
tabs.forEachIndexed { index, tab ->
243-
val selected = current == tab.route
235+
val selected = activeTab.route == tab.route
244236
Box(
245237
modifier = Modifier
246238
.weight(1f)
@@ -259,14 +251,7 @@ fun FloatingBottomNav(
259251
tab = tab,
260252
selected = selected,
261253
onClick = {
262-
// TODO: should be a callback floating Nav should have no knowledge of navigation graph.
263-
nav.navigate(tab.route) {
264-
popUpTo(nav.graph.findStartDestination().id) {
265-
saveState = true
266-
}
267-
launchSingleTop = true
268-
restoreState = true
269-
}
254+
onTabClicked(tab)
270255
}
271256
)
272257
}

0 commit comments

Comments
 (0)