Skip to content
Open
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 @@ -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,7 @@ 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 = "bottonNavItems"
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,143 @@
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.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)
) {
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)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package io.github.xxfast.decompose.router.items

import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.grid.LazyGridState
import androidx.compose.runtime.Composable
import com.arkivanov.decompose.ExperimentalDecomposeApi
import com.arkivanov.decompose.extensions.compose.lazyitems.ChildItemsLifecycleController

@OptIn(ExperimentalDecomposeApi::class)
@Composable
fun <C : Any> ItemsRouterLifecycleController(
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure about the naming. Open to suggestions.

router: Router<C>,
lazyListState: LazyListState,
itemIndexConverter: (Int) -> Int,
forwardPreloadCount: Int = 0,
backwardPreloadCount: Int = 0,
) {
ChildItemsLifecycleController(
items = router.lazyItems,
lazyListState = lazyListState,
itemIndexConverter = itemIndexConverter,
forwardPreloadCount = forwardPreloadCount,
backwardPreloadCount = backwardPreloadCount
)
}

@OptIn(ExperimentalDecomposeApi::class)
@Composable
fun <C : Any> ItemsRouterLifecycleController(
router: Router<C>,
lazyGridState: LazyGridState,
itemIndexConverter: (Int) -> Int,
forwardPreloadCount: Int = 0,
backwardPreloadCount: Int = 0,
) {
ChildItemsLifecycleController(
items = router.lazyItems,
lazyGridState = lazyGridState,
itemIndexConverter = itemIndexConverter,
forwardPreloadCount = forwardPreloadCount,
backwardPreloadCount = backwardPreloadCount
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package io.github.xxfast.decompose.router.items

import androidx.compose.foundation.lazy.LazyItemScope
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.grid.GridItemSpan
import androidx.compose.foundation.lazy.grid.LazyGridItemScope
import androidx.compose.foundation.lazy.grid.LazyGridItemSpanScope
import androidx.compose.foundation.lazy.grid.LazyGridScope
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
import com.arkivanov.decompose.ExperimentalDecomposeApi
import com.arkivanov.decompose.router.items.ChildItems
import io.github.xxfast.decompose.router.LocalRouterContext
import io.github.xxfast.decompose.router.RouterContext

@OptIn(ExperimentalDecomposeApi::class)
inline fun <C : Any> LazyListScope.items(
router: Router<C>,
noinline key: ((item: C) -> Any)? = null,
noinline contentType: (item: C) -> Any? = { null },
crossinline itemContent: @Composable LazyItemScope.(item: C) -> Unit
) {
val childItems: ChildItems<C, RouterContext> by router.items

items(
count = childItems.items.size,
key = if (key != null) { index: Int -> key(childItems.items[index]) } else null,
contentType = { index: Int -> contentType(childItems.items[index]) }
) {
val item: C = childItems.items[it]
val context: RouterContext = router.getContext(item)

CompositionLocalProvider(LocalRouterContext provides context) {
itemContent(item)
}
}
}

@OptIn(ExperimentalDecomposeApi::class)
inline fun <C : Any> LazyGridScope.items(
router: Router<C>,
noinline key: ((item: C) -> Any)? = null,
noinline span: (LazyGridItemSpanScope.(item: C) -> GridItemSpan)? = null,
noinline contentType: (item: C) -> Any? = { null },
crossinline itemContent: @Composable LazyGridItemScope.(item: C) -> Unit
) {
val childItems: ChildItems<C, RouterContext> by router.items

items(
count = childItems.items.size,
key = if (key != null) { index: Int -> key(childItems.items[index]) } else null,
span = if (span != null) { index: Int -> span(childItems.items[index]) } else null,
contentType = { index: Int -> contentType(childItems.items[index]) }
) {
val item: C = childItems.items[it]
val context: RouterContext = router.getContext(item)

CompositionLocalProvider(LocalRouterContext provides context) {
itemContent(item)
}
}
}
Loading