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
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ package io.github.xxfast.decompose.router.app
import androidx.compose.ui.test.hasTestTag
import io.github.xxfast.decompose.router.screens.BUTTON_BACK
import io.github.xxfast.decompose.router.screens.BOTTOM_NAV_BAR
import io.github.xxfast.decompose.router.screens.BOTTOM_NAV_ITEMS
import io.github.xxfast.decompose.router.screens.BOTTOM_NAV_PAGES
import io.github.xxfast.decompose.router.screens.BOTTOM_NAV_SLOT
import io.github.xxfast.decompose.router.screens.BOTTOM_NAV_STACK
import io.github.xxfast.decompose.router.screens.REMOVE_ITEM
import io.github.xxfast.decompose.router.screens.BOTTOM_SHEET
import io.github.xxfast.decompose.router.screens.BUTTON_BOTTOM_SHEET
import io.github.xxfast.decompose.router.screens.BUTTON_DIALOG
Expand All @@ -23,6 +25,7 @@ internal val bottomNav = hasTestTag(BOTTOM_NAV_BAR)
internal val bottomNavPagesItem = hasTestTag(BOTTOM_NAV_PAGES)
internal val bottomNavSlotItem = hasTestTag(BOTTOM_NAV_SLOT)
internal val bottomNavStackItem = hasTestTag(BOTTOM_NAV_STACK)
internal val bottomNavItemsItem = hasTestTag(BOTTOM_NAV_ITEMS)
internal val bottomSheet = hasTestTag(BOTTOM_SHEET)
internal val buttonBottomSheet = hasTestTag(BUTTON_BOTTOM_SHEET)
internal val buttonDialog = hasTestTag(BUTTON_DIALOG)
Expand All @@ -32,3 +35,5 @@ internal val fabAdd = hasTestTag(FAB_ADD)
internal val lazyColumn = hasTestTag(LIST_TAG)
internal val pager = hasTestTag(PAGER)
internal val titleBar = hasTestTag(TITLE_BAR)

internal fun removeItem(index: Int) = hasTestTag("$REMOVE_ITEM$index")
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package io.github.xxfast.decompose.router.app

import android.content.pm.ActivityInfo
import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.assertTextEquals
import androidx.compose.ui.test.hasText
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performScrollToNode
import androidx.compose.ui.test.waitUntilDoesNotExist
import androidx.compose.ui.test.waitUntilExactlyOneExists
import org.junit.Rule
import org.junit.Test

class TestItemsRouters {
@get:Rule
val composeRule: TestActivityRule = createAndroidComposeRule()

// Each item renders as "#<index> - <tick>", where <tick> increments every second.
// Matching on the "#<index> - " prefix keeps assertions stable against the ticking
// counter and avoids "#1" also matching "#10"/"#11".
private fun itemAt(index: Int) = hasText("#$index - ", substring = true)

@Test
fun testInitialState(): Unit = with(composeRule) {
// Go to the items screen
onNode(bottomNavItemsItem).assertExists()
onNode(bottomNavItemsItem).performClick()
onNode(titleBar).assertExists().assertTextEquals("Items")
onNode(lazyColumn).assertExists()

// The initial 10 items are present
onNode(itemAt(1)).assertExists()
onNode(lazyColumn).performScrollToNode(itemAt(10))
onNode(itemAt(10)).assertExists()
}

@OptIn(ExperimentalTestApi::class)
@Test
fun testAddAndRemoveItems(): Unit = with(composeRule) {
onNode(bottomNavItemsItem).performClick()

// Add a new item via the FAB and verify it shows up
onNode(fabAdd).performClick()
onNode(lazyColumn).performScrollToNode(itemAt(11))
waitUntilExactlyOneExists(itemAt(11))

// Remove an item and verify it's gone. Scroll to the top first so the removed
// item's button isn't under the floating action button, and wait past the item
// removal animation (each row runs a ticking coroutine, so the clock never idles).
onNode(lazyColumn).performScrollToNode(itemAt(1))
onNode(removeItem(1)).performClick()
waitUntilDoesNotExist(itemAt(1), timeoutMillis = 5_000)

// Removing an item leaves the rest of the list intact
onNode(itemAt(2)).assertExists()
}

@OptIn(ExperimentalTestApi::class)
@Test
fun testRestoredAcrossConfigurationChanges(): Unit = with(composeRule) {
onNode(bottomNavItemsItem).performClick()
onNode(lazyColumn).performScrollToNode(itemAt(3))
onNode(itemAt(3)).assertExists()

// Rotate to landscape and verify the items list is retained. The activity is
// recreated asynchronously, so wait for the rebuilt list before asserting.
activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
waitUntilExactlyOneExists(lazyColumn, timeoutMillis = 5_000)
onNode(lazyColumn).performScrollToNode(itemAt(3))
waitUntilExactlyOneExists(itemAt(3), timeoutMillis = 5_000)

// Rotate back and verify it's still there
activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
waitUntilExactlyOneExists(lazyColumn, timeoutMillis = 5_000)
onNode(lazyColumn).performScrollToNode(itemAt(3))
waitUntilExactlyOneExists(itemAt(3), timeoutMillis = 5_000)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.rounded.List
import androidx.compose.material.icons.rounded.CropSquare
import androidx.compose.material.icons.rounded.ImportContacts
import androidx.compose.material.icons.rounded.Reorder
Expand All @@ -25,13 +26,15 @@ import io.github.xxfast.decompose.router.pages.rememberRouter
import io.github.xxfast.decompose.router.screens.HomeScreens.Pages
import io.github.xxfast.decompose.router.screens.HomeScreens.Slot
import io.github.xxfast.decompose.router.screens.HomeScreens.Stack
import io.github.xxfast.decompose.router.screens.HomeScreens.Items
import io.github.xxfast.decompose.router.screens.items.ItemsScreen
import io.github.xxfast.decompose.router.screens.pages.PagesScreen
import io.github.xxfast.decompose.router.screens.slot.SlotScreen
import io.github.xxfast.decompose.router.screens.stack.StackScreen

@Composable
fun HomeScreen() {
val pager: Router<HomeScreens> = rememberRouter { pagesOf(Stack, Pages, Slot) }
val pager: Router<HomeScreens> = rememberRouter { pagesOf(Stack, Pages, Slot, Items) }

Scaffold(
bottomBar = {
Expand All @@ -47,6 +50,7 @@ fun HomeScreen() {
Stack -> Icons.Rounded.Reorder
Pages -> Icons.Rounded.ImportContacts
Slot -> Icons.Rounded.CropSquare
Items -> Icons.AutoMirrored.Rounded.List
},
contentDescription = null,
)
Expand All @@ -58,6 +62,7 @@ fun HomeScreen() {
Stack -> BOTTOM_NAV_STACK
Pages -> BOTTOM_NAV_PAGES
Slot -> BOTTOM_NAV_SLOT
Items -> BOTTOM_NAV_ITEMS
}
)
)
Expand All @@ -84,6 +89,7 @@ fun HomeScreen() {
Stack -> StackScreen()
Pages -> PagesScreen()
Slot -> SlotScreen()
Items -> ItemsScreen()
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ enum class HomeScreens {
Stack,
Pages,
Slot,
Items,
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ const val BOTTOM_NAV_BAR = "bottomNav"
const val BOTTOM_NAV_PAGES = "bottomNavPages"
const val BOTTOM_NAV_SLOT = "bottomNavSlot"
const val BOTTOM_NAV_STACK = "bottomNavStack"
const val BOTTOM_NAV_ITEMS = "bottomNavItems"
const val REMOVE_ITEM = "removeItem"
const val BOTTOM_SHEET = "bottomSheet"
const val BUTTON_BOTTOM_SHEET = "btnBottomSheet"
const val BUTTON_DIALOG = "btnDialog"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
package io.github.xxfast.decompose.router.screens.items

import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.WindowInsets
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.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Add
import androidx.compose.material.icons.rounded.Remove
import androidx.compose.material3.Card
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.unit.dp
import com.arkivanov.decompose.ExperimentalDecomposeApi
import com.arkivanov.decompose.router.items.setItems
import io.github.xxfast.decompose.router.items.ItemsRouterLifecycleController
import io.github.xxfast.decompose.router.items.Router
import io.github.xxfast.decompose.router.items.items
import io.github.xxfast.decompose.router.items.rememberRouter
import io.github.xxfast.decompose.router.rememberOnRoute
import io.github.xxfast.decompose.router.screens.FAB_ADD
import io.github.xxfast.decompose.router.screens.LIST_TAG
import io.github.xxfast.decompose.router.screens.REMOVE_ITEM
import io.github.xxfast.decompose.router.screens.TITLE_BAR
import io.github.xxfast.decompose.router.screens.TOOLBAR
import io.github.xxfast.decompose.router.screens.items.item.ItemViewModel
import io.github.xxfast.decompose.router.screens.items.item.ItemState

@OptIn(ExperimentalMaterial3Api::class, ExperimentalDecomposeApi::class)
@Composable
fun ItemsScreen() {
var lastIndex: Int by remember { mutableStateOf(10) }
val router: Router<Int> = rememberRouter { (1..lastIndex).toList() }
val listState: LazyListState = rememberLazyListState()

Scaffold(
topBar = {
TopAppBar(
modifier = Modifier.testTag(TOOLBAR),
title = {
Text(
text = "Items",
modifier = Modifier.testTag(TITLE_BAR)
)
},
)
},
floatingActionButton = {
FloatingActionButton(
onClick = {
router.setItems { it + (++lastIndex) }
},
content = { Icon(Icons.Rounded.Add, null) },
modifier = Modifier.testTag(FAB_ADD)
)
},
contentWindowInsets = WindowInsets(0, 0, 0, 0)
) { paddingValues ->
LazyColumn(
state = listState,
modifier = Modifier
.padding(paddingValues)
.fillMaxSize()
.testTag(LIST_TAG),
verticalArrangement = Arrangement.spacedBy(8.dp),
contentPadding = PaddingValues(16.dp),
) {
items(
router = router,
key = { it }
) { item ->
val itemComponent: ItemViewModel = rememberOnRoute(key = "item_$item") {
ItemViewModel(this)
}

val state: ItemState by itemComponent.states.collectAsState()

Card(
modifier = Modifier
.height(128.dp)
.fillMaxWidth()
.clip(MaterialTheme.shapes.medium)
.animateItem(),
) {
Box(
modifier = Modifier.fillMaxSize()
) {
IconButton(
onClick = {
router.setItems { it - item }
},
modifier = Modifier
.align(Alignment.TopEnd)
.testTag("$REMOVE_ITEM$item")
) {
Icon(
imageVector = Icons.Rounded.Remove,
contentDescription = null,
tint = MaterialTheme.colorScheme.error
)
}

Text(
text = "#$item - ${state.tick}",
style = MaterialTheme.typography.displayMedium,
modifier = Modifier
.align(Alignment.Center)
.padding(8.dp)
)
}
}
}
}
}

ItemsRouterLifecycleController(
router = router,
lazyListState = listState,
itemIndexConverter = { it },
forwardPreloadCount = 3,
backwardPreloadCount = 3
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package io.github.xxfast.decompose.router.screens.items.item

import kotlinx.serialization.Serializable

@Serializable data class ItemState(val tick: Int)
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package io.github.xxfast.decompose.router.screens.items.item

import io.github.xxfast.decompose.router.RouterContext
import io.github.xxfast.decompose.router.state
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlin.coroutines.CoroutineContext
import kotlin.time.Duration.Companion.seconds

class ItemViewModel(context: RouterContext): CoroutineScope {
private val initialState: ItemState = context.state(ItemState(0)) { states.value }
private val _state: MutableStateFlow<ItemState> = MutableStateFlow(initialState)
val states: StateFlow<ItemState> = _state

override val coroutineContext: CoroutineContext = Dispatchers.Main

init {
launch {
while (isActive) {
delay(1.seconds)
val previous: ItemState = _state.value
val updated: ItemState = previous.copy(tick = previous.tick + 1)
_state.emit(updated)
}
}
}
}
22 changes: 22 additions & 0 deletions decompose-router/api/desktop/decompose-router.api
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,28 @@ public final class io/github/xxfast/decompose/router/RouterContextKt {
public static final fun getLocalRouterContext ()Landroidx/compose/runtime/ProvidableCompositionLocal;
}

public final class io/github/xxfast/decompose/router/items/ItemsRouterLifecycleControllerKt {
public static final fun ItemsRouterLifecycleController (Lio/github/xxfast/decompose/router/items/Router;Landroidx/compose/foundation/lazy/LazyListState;Lkotlin/jvm/functions/Function1;IILandroidx/compose/runtime/Composer;II)V
public static final fun ItemsRouterLifecycleController (Lio/github/xxfast/decompose/router/items/Router;Landroidx/compose/foundation/lazy/grid/LazyGridState;Lkotlin/jvm/functions/Function1;IILandroidx/compose/runtime/Composer;II)V
}

public final class io/github/xxfast/decompose/router/items/RoutedItemsKt {
public static final fun items (Landroidx/compose/foundation/lazy/LazyListScope;Lio/github/xxfast/decompose/router/items/Router;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function4;)V
public static final fun items (Landroidx/compose/foundation/lazy/grid/LazyGridScope;Lio/github/xxfast/decompose/router/items/Router;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function4;)V
public static synthetic fun items$default (Landroidx/compose/foundation/lazy/LazyListScope;Lio/github/xxfast/decompose/router/items/Router;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function4;ILjava/lang/Object;)V
public static synthetic fun items$default (Landroidx/compose/foundation/lazy/grid/LazyGridScope;Lio/github/xxfast/decompose/router/items/Router;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function4;ILjava/lang/Object;)V
}

public final class io/github/xxfast/decompose/router/items/Router : com/arkivanov/decompose/router/items/ItemsNavigation {
public static final field $stable I
public fun <init> (Lcom/arkivanov/decompose/router/items/ItemsNavigation;Lcom/arkivanov/decompose/router/items/LazyChildItems;Lcom/arkivanov/essenty/lifecycle/Lifecycle;)V
public final fun getContext (Ljava/lang/Object;)Lio/github/xxfast/decompose/router/RouterContext;
public final fun getItems ()Landroidx/compose/runtime/State;
public final fun getLazyItems ()Lcom/arkivanov/decompose/router/items/LazyChildItems;
public fun navigate (Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;)V
public fun subscribe (Lkotlin/jvm/functions/Function1;)Lcom/arkivanov/decompose/Cancellation;
}

public final class io/github/xxfast/decompose/router/pages/RoutedContentKt {
public static final fun RoutedContent (Lio/github/xxfast/decompose/router/pages/Router;Landroidx/compose/ui/Modifier;Lcom/arkivanov/decompose/extensions/compose/pages/PagesScrollAnimation;Lkotlin/jvm/functions/Function6;Lkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;II)V
}
Expand Down
Loading
Loading