diff --git a/README.md b/README.md
index 5146b39..63fcc2e 100644
--- a/README.md
+++ b/README.md
@@ -149,9 +149,10 @@ sense.
|[How can I use Jetpack Compose components inside existing screens?](https://github.com/vinaygaba/Learn-Jetpack-Compose-By-Example/blob/master/app/src/main/java/com/example/jetpackcompose/interop/ComposeInClassicAndroidActivity.kt) |
|
### Navigation
-|Example|Preview|
-|-------|-------|
-|[How can I navigate to different screen in Jetpack Compose?](https://github.com/vinaygaba/Learn-Jetpack-Compose-By-Example/blob/master/app/src/main/java/com/example/jetpackcompose/navigation/ComposeNavigationActivity.kt) |
|
+| Example |Preview|
+|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------|
+| [How can I navigate to different screen and send argument in Jetpack Compose?](https://github.com/vinaygaba/Learn-Jetpack-Compose-By-Example/blob/master/app/src/main/java/com/example/jetpackcompose/navigation/compose/ComposeNavigationWithArgActivity.kt) |
|
+| [How can I navigate to different screen in Jetpack Compose?](https://github.com/vinaygaba/Learn-Jetpack-Compose-By-Example/blob/master/app/src/main/java/com/example/jetpackcompose/navigation/ComposeNavigationActivity.kt) |
|
### Testing
|Example|Preview|
diff --git a/app/build.gradle b/app/build.gradle
index ae84e66..d99a156 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -26,6 +26,11 @@ android {
}
kotlinOptions {
jvmTarget = '11'
+
+ freeCompilerArgs += [
+ "-Xopt-in=androidx.compose.foundation.ExperimentalFoundationApi",
+ "-Xopt-in=androidx.compose.material.ExperimentalMaterialApi",
+ ]
}
buildFeatures {
compose true
diff --git a/app/src/androidTest/java/com/example/jetpackcompose/navigation/compose/NavigationComposeWithArgumentNavigationTest.kt b/app/src/androidTest/java/com/example/jetpackcompose/navigation/compose/NavigationComposeWithArgumentNavigationTest.kt
new file mode 100644
index 0000000..f79bf36
--- /dev/null
+++ b/app/src/androidTest/java/com/example/jetpackcompose/navigation/compose/NavigationComposeWithArgumentNavigationTest.kt
@@ -0,0 +1,82 @@
+package com.example.jetpackcompose.navigation.compose
+
+import androidx.activity.ComponentActivity
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.test.junit4.createAndroidComposeRule
+import androidx.compose.ui.test.onNodeWithContentDescription
+import androidx.compose.ui.test.onNodeWithText
+import androidx.compose.ui.test.performClick
+import androidx.navigation.compose.ComposeNavigator
+import androidx.navigation.testing.TestNavHostController
+import com.example.jetpackcompose.navigation.assertCurrentRouteName
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+
+class NavigationComposeWithArgumentNavigationTest {
+
+ @get:Rule
+ val composeTestRule = createAndroidComposeRule()
+
+ private lateinit var navController: TestNavHostController
+
+ @Before
+ fun setupAppNavHost() {
+ composeTestRule.setContent {
+ navController = TestNavHostController(LocalContext.current)
+ navController.navigatorProvider.addNavigator(ComposeNavigator())
+
+ ComposeNavigationWithArgApp(navController = navController)
+ }
+ }
+
+ @Test
+ fun verifyStartDestination() {
+ navController
+ .assertCurrentRouteName("tasks")
+ }
+
+ @Test
+ fun verifyBackNavigationNotShownOnStartDestination() {
+ val backText = "Back"
+
+ composeTestRule
+ .onNodeWithContentDescription(backText)
+ .assertDoesNotExist()
+ }
+
+ @Test
+ fun clickTask_navigatesToTaskDetailsScreen() {
+ navController.assertCurrentRouteName("tasks")
+
+ composeTestRule
+ .onNodeWithText("Contribute to Learn-Jetpack-Compose-By-Example")
+ .performClick()
+
+ navController.assertCurrentRouteName("tasks/{taskId}")
+ }
+
+ @Test
+ fun clickNavigateUpButton_navigatesToTaskListScreen() {
+ navController.assertCurrentRouteName("tasks")
+
+ navigateToTaskDetails()
+
+ navController.assertCurrentRouteName("tasks/{taskId}")
+
+ val backText = "Back"
+
+ composeTestRule
+ .onNodeWithContentDescription(backText)
+ .performClick()
+
+ navController.assertCurrentRouteName("tasks")
+ }
+
+ private fun navigateToTaskDetails() {
+ composeTestRule
+ .onNodeWithText("Contribute to Learn-Jetpack-Compose-By-Example")
+ .performClick()
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 6dc0e12..2c62a84 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -193,6 +193,11 @@
android:exported="true"
android:label="@string/title_compose_navigation_example"
android:theme="@style/AppTheme.NoActionBar" />
+
diff --git a/app/src/main/java/com/example/jetpackcompose/core/MainActivity.kt b/app/src/main/java/com/example/jetpackcompose/core/MainActivity.kt
index 1c7ff84..805dd5f 100644
--- a/app/src/main/java/com/example/jetpackcompose/core/MainActivity.kt
+++ b/app/src/main/java/com/example/jetpackcompose/core/MainActivity.kt
@@ -29,6 +29,7 @@ import com.example.jetpackcompose.material.FlowRowActivity
import com.example.jetpackcompose.material.MaterialActivity
import com.example.jetpackcompose.material.ShadowActivity
import com.example.jetpackcompose.navigation.ComposeNavigationActivity
+import com.example.jetpackcompose.navigation.compose.ComposeNavigationWithArgActivity
import com.example.jetpackcompose.scrollers.HorizontalScrollableActivity
import com.example.jetpackcompose.scrollers.VerticalScrollableActivity
import com.example.jetpackcompose.stack.StackActivity
@@ -190,4 +191,8 @@ class MainActivity : AppCompatActivity() {
fun startComposeNavigationExample(view: View) {
startActivity(Intent(this, ComposeNavigationActivity::class.java))
}
+
+ fun startDetailViewComposeNavigationExample(view: View) {
+ startActivity(Intent(this, ComposeNavigationWithArgActivity::class.java))
+ }
}
diff --git a/app/src/main/java/com/example/jetpackcompose/navigation/compose/ComposeNavigationWithArgActivity.kt b/app/src/main/java/com/example/jetpackcompose/navigation/compose/ComposeNavigationWithArgActivity.kt
new file mode 100644
index 0000000..e3b022b
--- /dev/null
+++ b/app/src/main/java/com/example/jetpackcompose/navigation/compose/ComposeNavigationWithArgActivity.kt
@@ -0,0 +1,323 @@
+package com.example.jetpackcompose.navigation.compose
+
+import android.content.res.Configuration
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.IntrinsicSize
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.material.Card
+import androidx.compose.material.Icon
+import androidx.compose.material.IconButton
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.Scaffold
+import androidx.compose.material.Surface
+import androidx.compose.material.Text
+import androidx.compose.material.TopAppBar
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.ArrowBack
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.navigation.NavHostController
+import androidx.navigation.NavType
+import androidx.navigation.compose.NavHost
+import androidx.navigation.compose.composable
+import androidx.navigation.compose.currentBackStackEntryAsState
+import androidx.navigation.compose.rememberNavController
+import androidx.navigation.navArgument
+import java.text.SimpleDateFormat
+import java.util.Date
+import java.util.Locale
+
+class ComposeNavigationWithArgActivity : ComponentActivity() {
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ setContent {
+ ComposeNavigationWithArgApp()
+ }
+ }
+}
+
+@Composable
+fun ComposeNavigationWithArgApp(
+ navController: NavHostController = rememberNavController()
+) {
+
+ val backStackEntry by navController.currentBackStackEntryAsState()
+ val canNavigateBack by remember(backStackEntry) {
+ mutableStateOf(navController.previousBackStackEntry != null )
+ }
+
+ Scaffold(
+ topBar = {
+ MyTopAppBar(
+ title = "Tasks",
+ canNavigateBack = canNavigateBack,
+ navigateUp = { navController.navigateUp() }
+ )
+ }
+ ) { innerPadding ->
+ Column(
+ modifier = Modifier.padding(innerPadding) // #1
+ ) {
+ // or you can directly pass the modifier(#1) to AppNavHost(..)
+ AppNavHost(navController)
+ }
+ }
+}
+
+@Composable
+private fun MyTopAppBar(
+ title: String,
+ canNavigateBack: Boolean,
+ navigateUp: () -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ TopAppBar(
+ title = {
+ Text(text = title)
+ },
+ modifier = modifier,
+ navigationIcon = if (canNavigateBack) {
+ {
+ IconButton(onClick = navigateUp) {
+ Icon(
+ imageVector = Icons.Default.ArrowBack,
+ contentDescription = "Back"
+ )
+ }
+ }
+ } else null,
+ )
+}
+
+@Composable
+private fun AppNavHost(
+ navController: NavHostController,
+) {
+ NavHost(
+ navController = navController,
+ startDestination = "tasks",
+ ) {
+ composable(route = "tasks") {
+ TaskListScreen(
+ tasks = DataSource.getAllTasks(),
+ onTaskClick = { task -> navController.navigate("tasks/${task.id}") }
+ )
+ }
+
+ composable(
+ route = "tasks/{taskId}",
+ arguments = listOf(navArgument("taskId") { type = NavType.StringType }),
+ ) { backStackEntry ->
+ TaskDetails(
+ taskId = backStackEntry.arguments?.getString("taskId"),
+ )
+ }
+ }
+}
+
+@Composable
+private fun TaskListScreen(
+ tasks: List,
+ onTaskClick: (Task) -> Unit,
+) {
+
+ LazyColumn(
+ contentPadding = PaddingValues(16.dp),
+ verticalArrangement = Arrangement.spacedBy(8.dp)
+ ) {
+ stickyHeader {
+ Text(
+ text = "Tasks",
+ style = MaterialTheme.typography.h6
+ )
+ }
+
+ items(tasks) { task ->
+ TaskItem(task = task, onItemClick = onTaskClick)
+ }
+ }
+}
+
+@Composable
+private fun TaskItem(
+ task: Task,
+ onItemClick: (Task) -> Unit,
+) {
+ Card(
+ modifier = Modifier
+ .fillMaxWidth(),
+ onClick = {
+ onItemClick(task)
+ }
+ ) {
+ Row(
+ modifier = Modifier
+ .height(IntrinsicSize.Max)
+ ) {
+ Spacer(
+ modifier = Modifier
+ .fillMaxHeight()
+ .width(4.dp)
+ .background(Color(0xFFFFAA00))
+ )
+
+ Row(
+ modifier = Modifier.padding(16.dp)
+ ) {
+ Text(text = task.title, modifier = Modifier.weight(1f))
+ }
+ }
+ }
+}
+
+@Composable
+private fun TaskDetails(
+ taskId: String?,
+) {
+
+ val task by remember { mutableStateOf(DataSource.findTaskById(taskId)) }
+
+ if (task == null) {
+ Column(
+ verticalArrangement = Arrangement.Center,
+ horizontalAlignment = Alignment.CenterHorizontally,
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(16.dp)
+ ) {
+ Text(
+ text = "Task with id $taskId not found!",
+ style = MaterialTheme.typography.h6
+ )
+ }
+ } else {
+ // you should avoid using `!!`,
+ // the data should be wrapped in the ViewState class for type safety.
+ TaskDetails(task!!)
+ }
+}
+
+@Composable
+private fun TaskDetails(task: Task) {
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(16.dp)
+ ) {
+ Card(
+ backgroundColor = Color(0xFFF3F2F2)
+ ) {
+ Column(
+ verticalArrangement = Arrangement.spacedBy(8.dp),
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(16.dp)
+ ) {
+ Text(
+ text = "#${task.id}",
+ style = MaterialTheme.typography.caption,
+ color = MaterialTheme.colors.onSurface.copy(alpha = 0.6f)
+ )
+ TitleAndLabel(title = "Title", label = task.title)
+ TitleAndLabel(title = "Description", label = task.description)
+ TitleAndLabel(title = "Created On", label = task.timestamp)
+ }
+ }
+ }
+}
+
+@Composable
+private fun TitleAndLabel(
+ title: String,
+ label: String?,
+) {
+ Column {
+ Text(
+ text = title,
+ style = MaterialTheme.typography.caption,
+ color = MaterialTheme.colors.onSurface.copy(alpha = 0.6f)
+ )
+ Text(
+ text = label ?: "-",
+ style = MaterialTheme.typography.body1
+ )
+ }
+}
+
+@Composable
+private fun TitleAndLabel(
+ title: String,
+ label: Date?,
+) {
+ val simpleDateTimeFormatter = SimpleDateFormat("EEE, dd MMMM yyyy HH:mm", Locale.getDefault())
+ val formattedTimestamp =
+ if (label != null) simpleDateTimeFormatter.format(label)
+ else "-"
+
+ TitleAndLabel(
+ title = title,
+ label = formattedTimestamp
+ )
+}
+
+@Preview(
+ name = "Night Mode",
+ uiMode = Configuration.UI_MODE_NIGHT_YES,
+)
+@Preview(
+ name = "Day Mode",
+ uiMode = Configuration.UI_MODE_NIGHT_NO,
+)
+@Composable
+@Suppress("UnusedPrivateMember", "MagicNumber")
+private fun DetailViewComposeNavigationActivityPreview() {
+ MaterialTheme {
+ Surface {
+ ComposeNavigationWithArgApp()
+ }
+ }
+}
+
+@Preview(
+ name = "Night Mode",
+ uiMode = Configuration.UI_MODE_NIGHT_YES,
+)
+@Preview(
+ name = "Day Mode",
+ uiMode = Configuration.UI_MODE_NIGHT_NO,
+)
+@Composable
+@Suppress("UnusedPrivateMember", "MagicNumber")
+private fun TaskDetailsPreview() {
+ MaterialTheme {
+ Surface {
+ TaskDetails(
+ taskId = "1"
+ )
+ }
+ }
+}
diff --git a/app/src/main/java/com/example/jetpackcompose/navigation/compose/DataSource.kt b/app/src/main/java/com/example/jetpackcompose/navigation/compose/DataSource.kt
new file mode 100644
index 0000000..1276008
--- /dev/null
+++ b/app/src/main/java/com/example/jetpackcompose/navigation/compose/DataSource.kt
@@ -0,0 +1,45 @@
+package com.example.jetpackcompose.navigation.compose
+
+import java.util.Calendar
+import java.util.Date
+
+data class Task(
+ val id: Int,
+ val title: String,
+ val description: String? = null,
+ // I haven't used LocalDateTime as de-sugaring is another concept altogether
+ val timestamp: Date? = Calendar.getInstance().time,
+)
+
+// You shouldn't use static data source as it makes testing difficult.
+object DataSource {
+
+ private val tasks = listOf(
+ Task(
+ id = 1,
+ title = "Contribute to Learn-Jetpack-Compose-By-Example",
+ description = "Pick a simple issue and try to solve it.",
+ ),
+ Task(
+ id = 2,
+ title = "Binge Batman Trilogy",
+ ),
+ Task(
+ id = 3,
+ title = "Buy groceries.",
+ description = "Don't forget to buy bread and butter!",
+ timestamp = Calendar.getInstance().time
+ ),
+ Task(
+ id = 4,
+ title = "Clear out weeds from the backyard",
+ ),
+ )
+
+ fun getAllTasks() = tasks
+
+ fun findTaskById(id: String?): Task? {
+ val taskId = id?.toIntOrNull() ?: return null
+ return tasks.firstOrNull { it.id == taskId }
+ }
+}
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
index c7ff7c7..2995cf8 100644
--- a/app/src/main/res/layout/activity_main.xml
+++ b/app/src/main/res/layout/activity_main.xml
@@ -677,5 +677,24 @@
android:layout_gravity="center"
android:fontFamily="monospace" />
+
+
+
+
+
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index b8f3e3e..d2f1ad5 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -34,6 +34,7 @@
Back Press Example
Zoomable Example
Compose Navigation Example
+ Compose Navigation With Arg Example
Display Text
Display Styled Text
@@ -70,4 +71,5 @@
Back Press Component
Zoomable Component
Compose Navigation
+ Compose Navigation With Arg
diff --git a/app/src/test/java/com.example.jetpackcompose/navigation_compose/DataSourceTest.kt b/app/src/test/java/com.example.jetpackcompose/navigation_compose/DataSourceTest.kt
new file mode 100644
index 0000000..d5dfe7b
--- /dev/null
+++ b/app/src/test/java/com.example.jetpackcompose/navigation_compose/DataSourceTest.kt
@@ -0,0 +1,32 @@
+package com.example.jetpackcompose.navigation_compose
+
+import com.example.jetpackcompose.navigation.compose.DataSource
+import org.junit.Assert.assertEquals
+import org.junit.Test
+
+class DataSourceTest {
+
+ @Test
+ fun verifyTaskListSize() {
+ val tasks = DataSource.getAllTasks()
+ val tasksSize = tasks.size
+
+ assertEquals(4, tasksSize)
+ }
+
+ @Test
+ fun givenValidTaskIdThenFindTaskByIdShouldReturnTask() {
+ val taskId = "2"
+
+ val task = DataSource.findTaskById(taskId)
+ assertEquals(taskId, task?.id.toString())
+ }
+
+ @Test
+ fun givenInvalidTaskIdThenFindTaskByIdShouldReturnNull() {
+ val taskId = "10"
+
+ val task = DataSource.findTaskById(taskId)
+ assertEquals(null, task?.id?.toString())
+ }
+}
diff --git a/screenshots/compose_navigation_w_arg_example.gif b/screenshots/compose_navigation_w_arg_example.gif
new file mode 100644
index 0000000..e59d8bc
Binary files /dev/null and b/screenshots/compose_navigation_w_arg_example.gif differ