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
10 changes: 10 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -247,3 +247,13 @@ extensions.getByType(com.android.build.api.variant.AndroidComponentsExtension::c
GenerateDeepLinkManifestTask::manifest
)
}

kotlin {
sourceSets {
all {
languageSettings {
optIn("androidx.compose.material3.ExperimentalMaterial3ExpressiveApi")
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import androidx.compose.animation.animateContentSize
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.Interaction
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
Expand Down Expand Up @@ -37,12 +36,13 @@ import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.FloatingToolbarDefaults.floatingToolbarVerticalNestedScroll
import androidx.compose.material3.Icon
import androidx.compose.material3.ListItem
import androidx.compose.material3.ListItemDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalWideNavigationRail
import androidx.compose.material3.NavigationBarItem
import androidx.compose.material3.NavigationDrawerItem
import androidx.compose.material3.NavigationRailItem
import androidx.compose.material3.SegmentedListItem
import androidx.compose.material3.ShortNavigationBarItem
import androidx.compose.material3.Surface
import androidx.compose.material3.WideNavigationRailState
Expand Down Expand Up @@ -83,6 +83,7 @@ import dev.dimension.flare.R
import dev.dimension.flare.data.model.BottomBarBehavior
import dev.dimension.flare.data.model.BottomBarStyle
import dev.dimension.flare.data.model.LocalAppearanceSettings
import dev.dimension.flare.ui.theme.segmentedShapes2
import soup.compose.material.motion.animation.materialElevationScaleIn
import soup.compose.material.motion.animation.materialElevationScaleOut

Expand Down Expand Up @@ -244,26 +245,26 @@ fun NavigationSuiteScaffold2(
}
if (layoutType == NavigationSuiteType.NavigationBar) {
Column(
verticalArrangement = Arrangement.spacedBy(2.dp),
verticalArrangement = Arrangement.spacedBy(ListItemDefaults.SegmentedGap),
) {
secondaryScope.itemList.forEachIndexed { index, it ->
ListItem(
headlineContent = {
SegmentedListItem(
onClick = {
it.onClick()
},
shapes =
ListItemDefaults.segmentedShapes2(
index,
secondaryScope.itemsCount,
),
content = {
it.label?.invoke()
},
leadingContent = it.icon,
trailingContent = it.badge,
modifier =
it.modifier
.padding(horizontal = 16.dp)
.listCard(
index = index,
totalCount = secondaryScope.itemsCount,
).clickable(
enabled = it.enabled,
onClick = it.onClick,
interactionSource = it.interactionSource,
),
.padding(horizontal = 16.dp),
)
}
}
Expand All @@ -286,26 +287,26 @@ fun NavigationSuiteScaffold2(
Spacer(modifier = Modifier.weight(1f))
if (layoutType == NavigationSuiteType.NavigationBar) {
Column(
verticalArrangement = Arrangement.spacedBy(2.dp),
verticalArrangement = Arrangement.spacedBy(ListItemDefaults.SegmentedGap),
) {
footerScope.itemList.forEachIndexed { index, it ->
ListItem(
headlineContent = {
SegmentedListItem(
content = {
it.label?.invoke()
},
onClick = {
it.onClick()
},
shapes =
ListItemDefaults.segmentedShapes2(
index,
footerScope.itemsCount,
),
leadingContent = it.icon,
trailingContent = it.badge,
modifier =
it.modifier
.padding(horizontal = 16.dp)
.listCard(
index = index,
totalCount = footerScope.itemsCount,
).clickable(
enabled = it.enabled,
onClick = it.onClick,
interactionSource = it.interactionSource,
),
.padding(horizontal = 16.dp),
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ListItemDefaults
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
Expand Down Expand Up @@ -97,7 +98,7 @@ internal fun DMListScreen(
modifier =
Modifier
.padding(horizontal = screenHorizontalPadding),
verticalArrangement = Arrangement.spacedBy(2.dp),
verticalArrangement = Arrangement.spacedBy(ListItemDefaults.SegmentedGap),
) {
dmList(
data = state.items,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.ListItemDefaults
import androidx.compose.material3.RadioButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
Expand All @@ -14,13 +15,13 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import dev.dimension.flare.R
import dev.dimension.flare.ui.component.listCard
import dev.dimension.flare.ui.model.onSuccess
import dev.dimension.flare.ui.presenter.invoke
import dev.dimension.flare.ui.presenter.settings.AccountsPresenter
import dev.dimension.flare.ui.route.Route
import dev.dimension.flare.ui.screen.settings.AccountItem
import dev.dimension.flare.ui.theme.screenHorizontalPadding
import dev.dimension.flare.ui.theme.segmentedShapes2
import moe.tlaster.precompose.molecule.producePresenter

@Composable
Expand All @@ -37,18 +38,13 @@ internal fun AccountSelectionModal(
val state by producePresenter { presenter() }
state.accounts.onSuccess {
Column(
verticalArrangement = Arrangement.spacedBy(2.dp),
verticalArrangement = Arrangement.spacedBy(ListItemDefaults.SegmentedGap),
) {
for (index in 0 until it.size) {
val (accountKey, data) = it[index]
AccountItem(
modifier =
Modifier
.listCard(
index = index,
totalCount = it.size,
),
userState = data,
shapes = ListItemDefaults.segmentedShapes2(index, it.size),
onClick = {
state.setActiveAccount(it)
onBack.invoke()
Expand Down
52 changes: 25 additions & 27 deletions app/src/main/java/dev/dimension/flare/ui/screen/home/HomeScreen.kt
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,12 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.retain.retain
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
Expand Down Expand Up @@ -114,32 +114,30 @@ internal fun HomeScreen(afterInit: () -> Unit) {
val wideNavigationRailState = rememberWideNavigationRailState()
state.tabs
.onSuccess { tabs ->
val topLevelBackStack by producePresenter(
key = "home_top_level_back_stack_${tabs.all.first().key}",
useImmediateClock = true,
) {
TopLevelBackStack<Route>(
getDirection(tabs.all.first()),
)
}

fun navigate(route: Route) {
topLevelBackStack.addTopLevel(route)
scope.launch {
wideNavigationRailState.collapse()
val topLevelBackStack =
retain(
"home_top_level_back_stack_${tabs.all.first().key}",
) {
TopLevelBackStack(
getDirection(tabs.all.first()),
)
}
Comment on lines +117 to 124
Copy link

Copilot AI Jan 5, 2026

Choose a reason for hiding this comment

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

Using "retain" here instead of "producePresenter" changes the lifecycle management of the TopLevelBackStack. The "retain" function appears to bypass the presenter's clock and composition lifecycle, which could lead to stale state not being properly cleaned up when the composition is disposed. This needs verification that the retained object is properly released when no longer needed.

Copilot uses AI. Check for mistakes.
}

val currentRoute by remember {
derivedStateOf {
topLevelBackStack.topLevelKey
}
}
val accountType by remember {
derivedStateOf {
currentRoute.accountTypeOr(AccountType.Active)
val topLevelBackStackState by rememberUpdatedState(topLevelBackStack)

val navigate =
remember(topLevelBackStack, wideNavigationRailState) {
{ route: Route ->
topLevelBackStack.addTopLevel(route)
scope.launch {
wideNavigationRailState.collapse()
}
Unit
Comment on lines +126 to +135
Copy link

Copilot AI Jan 5, 2026

Choose a reason for hiding this comment

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

The remember block has been removed around the navigate lambda. This lambda now gets recreated on every recomposition when topLevelBackStack or wideNavigationRailState changes, which could cause unnecessary recompositions in child components that receive this lambda. The lambda should be wrapped with remember and use rememberUpdatedState if needed to capture the latest values while maintaining referential stability.

Suggested change
val topLevelBackStackState by rememberUpdatedState(topLevelBackStack)
val navigate =
remember(topLevelBackStack, wideNavigationRailState) {
{ route: Route ->
topLevelBackStack.addTopLevel(route)
scope.launch {
wideNavigationRailState.collapse()
}
Unit
val topLevelBackStackState = rememberUpdatedState(topLevelBackStack)
val wideNavigationRailStateState = rememberUpdatedState(wideNavigationRailState)
val navigate: (Route) -> Unit =
remember {
{ route: Route ->
topLevelBackStackState.value.addTopLevel(route)
scope.launch {
wideNavigationRailStateState.value.collapse()
}

Copilot uses AI. Check for mistakes.
}
}
}

val currentRoute = topLevelBackStack.topLevelKey
val accountType = currentRoute.accountTypeOr(AccountType.Active)
Comment on lines +139 to +140
Copy link

Copilot AI Jan 5, 2026

Choose a reason for hiding this comment

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

The "by remember { derivedStateOf { ... } }" pattern has been replaced with direct property access, removing the derived state optimization. This means currentRoute and accountType will now be recalculated on every recomposition instead of only when topLevelBackStack.topLevelKey changes. This could lead to unnecessary recalculations and recompositions of dependent composables.

Copilot uses AI. Check for mistakes.
val userState by producePresenter(key = "home_account_type_$accountType") {
userPresenter(accountType)
}
Expand Down Expand Up @@ -177,7 +175,7 @@ internal fun HomeScreen(afterInit: () -> Unit) {
userState,
layoutType,
currentRoute,
::navigate,
navigate,
)
},
navigationSuiteItems = {
Expand Down Expand Up @@ -300,10 +298,10 @@ internal fun HomeScreen(afterInit: () -> Unit) {
}
},
navigate = {
topLevelBackStack.add(it)
topLevelBackStackState.add(it)
},
onBack = {
topLevelBackStack.removeLast()
topLevelBackStackState.removeLast()
},
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.IconButton
import androidx.compose.material3.ListItem
import androidx.compose.material3.ListItemDefaults
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
Expand Down Expand Up @@ -51,6 +52,7 @@ import dev.dimension.flare.ui.screen.settings.EditTabDialog
import dev.dimension.flare.ui.screen.settings.TabAddBottomSheet
import dev.dimension.flare.ui.screen.settings.TabCustomItem
import dev.dimension.flare.ui.theme.screenHorizontalPadding
import dev.dimension.flare.ui.theme.segmentedShapes2
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.map
Expand Down Expand Up @@ -129,7 +131,7 @@ internal fun TabSettingScreen(
modifier =
Modifier
.padding(horizontal = screenHorizontalPadding),
verticalArrangement = Arrangement.spacedBy(2.dp),
verticalArrangement = Arrangement.spacedBy(ListItemDefaults.SegmentedGap),
) {
state.enableMixedTimeline.onSuccess { enabled ->
if (state.currentTabs.size > 1) {
Expand Down Expand Up @@ -166,6 +168,7 @@ internal fun TabSettingScreen(
itemsIndexed(state.currentTabs, key = { _, item -> item.key }) { index, item ->
TabCustomItem(
item = item,
shapes = ListItemDefaults.segmentedShapes2(index, state.currentTabs.size),
deleteTab = {
if (it is TimelineTabItem) {
state.deleteTab(item)
Expand All @@ -178,12 +181,6 @@ internal fun TabSettingScreen(
},
reorderableLazyColumnState = reorderableLazyColumnState,
canSwipeToDelete = state.canSwipeToDelete,
modifier =
Modifier
.listCard(
index = index,
totalCount = state.currentTabs.size,
),
)
}
}
Expand Down Expand Up @@ -243,7 +240,7 @@ private fun presenter(
object {
val currentTabs = cacheTabs
val allTabsState = allTabsState
val canSwipeToDelete = cacheTabs.size > 1
val canSwipeToDelete = true
Copy link

Copilot AI Jan 5, 2026

Choose a reason for hiding this comment

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

The deletion constraint logic has been removed but the variable name "canSwipeToDelete" is now misleading since it's always true. Consider either renaming this to something like "enableSwipeToDelete" or removing the variable entirely and passing true directly to the component.

Suggested change
val canSwipeToDelete = true
val enableSwipeToDelete = true

Copilot uses AI. Check for mistakes.
val showAddTab = showAddTab
val selectedEditTab = selectedEditTab
val enableMixedTimeline = enableMixedTimeline
Expand Down Expand Up @@ -285,16 +282,10 @@ private fun presenter(
}

fun deleteTab(tab: TimelineTabItem) {
if (cacheTabs.size <= 1) {
return
}
cacheTabs.removeIf { it.key == tab.key }
}

fun deleteTab(key: String) {
if (cacheTabs.size <= 1) {
return
}
cacheTabs.removeIf { it.key == key }
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.IconButton
import androidx.compose.material3.ListItemDefaults
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
Expand Down Expand Up @@ -109,7 +110,7 @@ internal fun ListScreen(
modifier =
Modifier
.padding(horizontal = screenHorizontalPadding),
verticalArrangement = Arrangement.spacedBy(2.dp),
verticalArrangement = Arrangement.spacedBy(ListItemDefaults.SegmentedGap),
) {
uiListWithTabs(
state = state,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.IconButton
import androidx.compose.material3.ListItemDefaults
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
Expand All @@ -21,7 +22,6 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import compose.icons.FontAwesomeIcons
import compose.icons.fontawesomeicons.Solid
import compose.icons.fontawesomeicons.solid.FileExport
Expand Down Expand Up @@ -105,7 +105,7 @@ internal fun RssSourcesScreen(
modifier =
Modifier
.padding(horizontal = screenHorizontalPadding),
verticalArrangement = Arrangement.spacedBy(2.dp),
verticalArrangement = Arrangement.spacedBy(ListItemDefaults.SegmentedGap),
contentPadding = contentPadding,
) {
rssListWithTabs(
Expand Down
Loading
Loading