refactor(android): revamp the user interactions of tablet UI and cleanup code

This commit is contained in:
Harsh Shandilya 2024-10-28 01:10:18 +05:30
parent b51eb5415a
commit 1fc8f9a2be
8 changed files with 74 additions and 192 deletions

View file

@ -94,7 +94,7 @@ fun TwoPaneLayoutPostActions(
override fun viewComments(postId: String) {
viewModel.markPostAsRead(postId)
setSelectedPost(postId) // Update selectedPostId
setSelectedPost(postId)
}
override fun viewCommentsPage(post: UIPost) {

View file

@ -7,7 +7,6 @@
package dev.msfjarvis.claw.android.ui.lists
import androidx.activity.compose.ReportDrawnWhen
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
@ -51,7 +50,6 @@ fun NetworkPosts(
postActions: PostActions,
contentPadding: PaddingValues,
modifier: Modifier = Modifier,
onPostClick: (String) -> Unit,
) {
ReportDrawnWhen { lazyPagingItems.itemCount > 0 }
val refreshLoadState by rememberUpdatedState(lazyPagingItems.loadState.refresh)
@ -84,14 +82,7 @@ fun NetworkPosts(
) { index ->
val item = lazyPagingItems[index]
if (item != null) {
LobstersListItem(
item = item,
postActions = postActions,
modifier =
Modifier.clickable {
onPostClick(item.shortId) // Trigger the click listener
},
)
LobstersListItem(item = item, postActions = postActions)
HorizontalDivider()
}
}
@ -121,7 +112,6 @@ private fun ListPreview() {
listState = rememberLazyListState(),
postActions = TEST_POST_ACTIONS,
contentPadding = PaddingValues(),
onPostClick = {},
)
}
}

View file

@ -1,127 +0,0 @@
/*
* Copyright © 2021-2024 Harsh Shandilya.
* Use of this source code is governed by an MIT-style
* license that can be found in the LICENSE file or at
* https://opensource.org/licenses/MIT.
*/
package dev.msfjarvis.claw.android.ui.lists
import androidx.activity.compose.ReportDrawnWhen
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
import androidx.compose.material3.pulltorefresh.PullToRefreshDefaults
import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.paging.LoadState
import androidx.paging.PagingData
import androidx.paging.compose.LazyPagingItems
import androidx.paging.compose.collectAsLazyPagingItems
import androidx.paging.compose.itemContentType
import androidx.paging.compose.itemKey
import dev.msfjarvis.claw.common.posts.PostActions
import dev.msfjarvis.claw.common.posts.TEST_POST
import dev.msfjarvis.claw.common.posts.TEST_POST_ACTIONS
import dev.msfjarvis.claw.common.theme.LobstersTheme
import dev.msfjarvis.claw.common.ui.NetworkError
import dev.msfjarvis.claw.common.ui.ProgressBar
import dev.msfjarvis.claw.common.ui.preview.DevicePreviews
import dev.msfjarvis.claw.model.UIPost
import kotlinx.coroutines.flow.MutableStateFlow
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun NetworkPostsForTwoPaneLayout(
lazyPagingItems: LazyPagingItems<UIPost>,
listState: LazyListState,
postActions: PostActions,
contentPadding: PaddingValues,
modifier: Modifier = Modifier,
onPostClick: (String) -> Unit,
) {
ReportDrawnWhen { lazyPagingItems.itemCount > 0 }
val refreshLoadState by rememberUpdatedState(lazyPagingItems.loadState.refresh)
val state = rememberPullToRefreshState()
PullToRefreshBox(
modifier = modifier.fillMaxSize(),
isRefreshing = refreshLoadState == LoadState.Loading,
state = state,
onRefresh = { lazyPagingItems.refresh() },
indicator = {
PullToRefreshDefaults.Indicator(
state = state,
isRefreshing = refreshLoadState == LoadState.Loading,
modifier = Modifier.align(Alignment.TopCenter).padding(contentPadding),
)
},
) {
if (lazyPagingItems.itemCount == 0 && refreshLoadState is LoadState.Error) {
NetworkError(
label = "Failed to load posts",
error = (refreshLoadState as LoadState.Error).error,
modifier = Modifier.align(Alignment.Center),
)
} else {
LazyColumn(contentPadding = contentPadding, state = listState) {
items(
count = lazyPagingItems.itemCount,
key = lazyPagingItems.itemKey { it.shortId },
contentType = lazyPagingItems.itemContentType { "LobstersItem" },
) { index ->
val item = lazyPagingItems[index]
if (item != null) {
LobstersListItem(
item = item,
postActions = postActions,
modifier =
Modifier.clickable {
onPostClick(item.shortId) // Trigger the click listener)
},
)
HorizontalDivider()
}
}
if (lazyPagingItems.loadState.append == LoadState.Loading) {
item(key = "progressbar") {
ProgressBar(
modifier =
Modifier.fillMaxWidth()
.wrapContentWidth(Alignment.CenterHorizontally)
.padding(vertical = 16.dp)
)
}
}
}
}
}
}
@DevicePreviews
@Composable
private fun ListPreview() {
val items = List(20) { TEST_POST.copy(shortId = "${TEST_POST.shortId}${it}") }
val flow = MutableStateFlow(PagingData.from(items))
LobstersTheme {
NetworkPostsForTwoPaneLayout(
lazyPagingItems = flow.collectAsLazyPagingItems(),
listState = rememberLazyListState(),
postActions = TEST_POST_ACTIONS,
contentPadding = PaddingValues(),
onPostClick = {},
)
}
}

View file

@ -33,7 +33,6 @@ fun SearchList(
setSearchQuery: (String) -> Unit,
contentPadding: PaddingValues,
modifier: Modifier = Modifier,
onPostClick: (String) -> Unit,
) {
val triggerSearch = { query: String ->
setSearchQuery(query)
@ -54,7 +53,6 @@ fun SearchList(
listState = listState,
postActions = postActions,
contentPadding = contentPadding,
onPostClick = onPostClick,
)
}
}

View file

@ -39,7 +39,6 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.platform.LocalContext
@ -90,7 +89,7 @@ import kotlinx.collections.immutable.persistentMapOf
import kotlinx.collections.immutable.toPersistentList
import kotlinx.coroutines.launch
@OptIn(ExperimentalComposeUiApi::class, ExperimentalMaterial3Api::class)
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun LobstersPostsScreen(
urlLauncher: UrlLauncher,
@ -238,7 +237,6 @@ fun LobstersPostsScreen(
listState = hottestListState,
postActions = postActions,
contentPadding = contentPadding,
onPostClick = { postId -> navController.navigate(Comments(postId)) },
)
}
composable<Newest> {
@ -248,7 +246,6 @@ fun LobstersPostsScreen(
listState = newestListState,
postActions = postActions,
contentPadding = contentPadding,
onPostClick = { postId -> navController.navigate(Comments(postId)) },
)
}
composable<Saved> {

View file

@ -43,7 +43,6 @@ fun SearchScreen(
val listState = rememberLazyListState()
val searchResults = viewModel.searchResults.collectAsLazyPagingItems()
val onPostClick: (String) -> Unit = { postId -> navController.navigate("comments/$postId") }
Scaffold(modifier = modifier) { contentPadding ->
NavHost(navController = navController, startDestination = Search) {
composable<Search> {
@ -55,7 +54,6 @@ fun SearchScreen(
searchQuery = viewModel.searchQuery,
contentPadding = contentPadding,
setSearchQuery = { query -> viewModel.searchQuery = query },
onPostClick = onPostClick,
)
}
composable<Comments> { backStackEntry ->

View file

@ -4,8 +4,11 @@
* license that can be found in the LICENSE file or at
* https://opensource.org/licenses/MIT.
*/
@file:OptIn(ExperimentalMaterial3AdaptiveApi::class)
package dev.msfjarvis.claw.android.ui.screens
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
@ -18,13 +21,16 @@ import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
import androidx.compose.material3.adaptive.navigation.NavigableListDetailPaneScaffold
import androidx.compose.material3.adaptive.layout.AnimatedPane
import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffold
import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffoldRole
import androidx.compose.material3.adaptive.layout.PaneAdaptedValue
import androidx.compose.material3.adaptive.navigation.BackNavigationBehavior
import androidx.compose.material3.adaptive.navigation.ThreePaneScaffoldNavigator
import androidx.compose.material3.adaptive.navigation.rememberListDetailPaneScaffoldNavigator
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
@ -37,14 +43,22 @@ import androidx.paging.compose.collectAsLazyPagingItems
import com.deliveryhero.whetstone.compose.injectedViewModel
import dev.msfjarvis.claw.android.R
import dev.msfjarvis.claw.android.ui.TwoPaneLayoutPostActions
import dev.msfjarvis.claw.android.ui.lists.NetworkPostsForTwoPaneLayout
import dev.msfjarvis.claw.android.ui.lists.NetworkPosts
import dev.msfjarvis.claw.android.ui.navigation.Comments
import dev.msfjarvis.claw.android.ui.navigation.User
import dev.msfjarvis.claw.android.viewmodel.ClawViewModel
import dev.msfjarvis.claw.common.comments.CommentsPage
import dev.msfjarvis.claw.common.comments.HTMLConverter
import dev.msfjarvis.claw.common.urllauncher.UrlLauncher
import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterial3AdaptiveApi::class, ExperimentalMaterial3Api::class)
private fun ThreePaneScaffoldNavigator<*>.isListExpanded() =
scaffoldValue[ListDetailPaneScaffoldRole.List] == PaneAdaptedValue.Expanded
private fun ThreePaneScaffoldNavigator<*>.isDetailExpanded() =
scaffoldValue[ListDetailPaneScaffoldRole.Detail] == PaneAdaptedValue.Expanded
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun TwoPaneLayout(
urlLauncher: UrlLauncher,
@ -52,23 +66,30 @@ fun TwoPaneLayout(
modifier: Modifier = Modifier,
viewModel: ClawViewModel = injectedViewModel(),
) {
val context = LocalContext.current
val hottestListState = rememberLazyListState()
val navController = rememberNavController()
// Track the selected postId as a mutable state
var selectedPostId by remember { mutableStateOf<String?>(null) }
// Initialize postActions with selectedPostId as a mutable state
val postActions = remember {
TwoPaneLayoutPostActions(context, urlLauncher, viewModel, { selectedPostId = it })
}
val coroutineScope = rememberCoroutineScope()
val hottestPosts = viewModel.hottestPosts.collectAsLazyPagingItems()
val navigator = rememberListDetailPaneScaffoldNavigator<Comments>()
val backBehavior =
if (navigator.isListExpanded() && navigator.isDetailExpanded()) {
BackNavigationBehavior.PopUntilContentChange
} else {
BackNavigationBehavior.PopUntilScaffoldValueChange
}
// Navigator state
val navigator = rememberListDetailPaneScaffoldNavigator<Any>()
val postActions = remember {
TwoPaneLayoutPostActions(context, urlLauncher, viewModel) {
coroutineScope.launch {
navigator.navigateTo(pane = ListDetailPaneScaffoldRole.Detail, contentKey = Comments(it))
}
}
}
BackHandler(navigator.canNavigateBack(backBehavior)) {
coroutineScope.launch { navigator.navigateBack(backBehavior) }
}
Scaffold(
topBar = {
@ -84,40 +105,46 @@ fun TwoPaneLayout(
)
},
content = { paddingValues ->
NavigableListDetailPaneScaffold(
ListDetailPaneScaffold(
modifier = modifier.padding(paddingValues),
navigator = navigator,
directive = navigator.scaffoldDirective,
value = navigator.scaffoldValue,
listPane = {
NetworkPostsForTwoPaneLayout(
lazyPagingItems = hottestPosts,
listState = hottestListState,
postActions = postActions,
contentPadding = PaddingValues(),
modifier = Modifier.fillMaxSize(),
) { postId ->
selectedPostId = postId // Update selectedPostId on click
}
},
detailPane = {
selectedPostId?.let { postId ->
CommentsPage(
postId = postId,
AnimatedPane {
NetworkPosts(
lazyPagingItems = hottestPosts,
listState = hottestListState,
postActions = postActions,
htmlConverter = htmlConverter,
getSeenComments = viewModel::getSeenComments,
markSeenComments = viewModel::markSeenComments,
openUserProfile = { navController.navigate(User(it)) },
contentPadding = PaddingValues(),
modifier = Modifier.fillMaxSize(),
)
}
?: Box(Modifier.fillMaxSize()) {
// Placeholder when no post is selected
Text(
text = "Select a post to view comments",
modifier = Modifier.align(Alignment.Center),
)
},
detailPane = {
AnimatedPane {
when (val contentKey = navigator.currentDestination?.contentKey) {
null -> {
Box(Modifier.fillMaxSize()) {
Text(
text = "Select a post to view comments",
modifier = Modifier.align(Alignment.Center),
)
}
}
else -> {
CommentsPage(
postId = contentKey.postId,
postActions = postActions,
htmlConverter = htmlConverter,
getSeenComments = viewModel::getSeenComments,
markSeenComments = viewModel::markSeenComments,
openUserProfile = { navController.navigate(User(it)) },
contentPadding = PaddingValues(),
modifier = Modifier.fillMaxSize(),
)
}
}
}
},
)
},

View file

@ -31,7 +31,6 @@ class NetworkPostsTest {
listState = rememberLazyListState(),
postActions = TEST_POST_ACTIONS,
contentPadding = PaddingValues(),
onPostClick = {},
)
}
}