I'm new to jetpack compose and trying to implement a scrollable data table using jetpack LazyColumn/LazyRow. The first row and first column of the entire table shall be sticky.
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
BaseTheme {
ExceptionsSpreadSheet(exceptions = generateData())
}
}
}
}
This is my activity, the BaseTheme is just the default theme android studio generated for me.
@OptIn(ExperimentalFoundationApi::class)
@Preview(showBackground = true, widthDp = 960, heightDp = 540)
@Composable
fun ExceptionSpreadSheetHeader(
exception: DropBoxException = DropBoxException(),
state: LazyListState = rememberLazyListState()
) {
LazyRow(
modifier = Modifier.background(color = Color.DarkGray),
state = state
) {
stickyHeader {
ExceptionSpreadSheetCell(text = "id")
}
items(1) {
ExceptionSpreadSheetCell(text = "packageName")
ExceptionSpreadSheetCell(text = "processName")
ExceptionSpreadSheetCell(text = "packageVersionCode")
ExceptionSpreadSheetCell(text = "packageVersionName")
ExceptionSpreadSheetCell(text = "activity")
ExceptionSpreadSheetCell(text = "romVersion")
ExceptionSpreadSheetCell(text = "platformId")
ExceptionSpreadSheetCell(text = "productName")
ExceptionSpreadSheetCell(text = "modelName")
ExceptionSpreadSheetCell(text = "exceptionSummary")
ExceptionSpreadSheetCell(text = "exceptionType")
ExceptionSpreadSheetCell(text = "exceptionDigest")
ExceptionSpreadSheetCell(text = "androidId")
ExceptionSpreadSheetCell(text = "occurTime")
ExceptionSpreadSheetCell(text = "callStack")
}
}
}
@OptIn(ExperimentalFoundationApi::class)
@Preview(showBackground = true, widthDp = 960, heightDp = 540)
@Composable
fun ExceptionSpreadSheetRow(
exception: DropBoxException = DropBoxException(),
state: LazyListState = rememberLazyListState()
) {
LazyRow(state = state) {
stickyHeader {
ExceptionSpreadSheetCell(
text = exception.id.toString(),
modifier = Modifier.background(color = Color.DarkGray)
)
}
items(1) {
ExceptionSpreadSheetCell(text = exception.packageName)
ExceptionSpreadSheetCell(text = exception.processName)
ExceptionSpreadSheetCell(text = exception.packageVersionCode)
ExceptionSpreadSheetCell(text = exception.packageVersionName)
ExceptionSpreadSheetCell(text = exception.activity)
ExceptionSpreadSheetCell(text = exception.romVersion)
ExceptionSpreadSheetCell(text = exception.platformId.toString())
ExceptionSpreadSheetCell(text = exception.productName)
ExceptionSpreadSheetCell(text = exception.modelName)
ExceptionSpreadSheetCell(text = exception.exceptionSummary)
ExceptionSpreadSheetCell(text = exception.exceptionType)
ExceptionSpreadSheetCell(text = exception.exceptionDigest)
ExceptionSpreadSheetCell(text = exception.androidId)
ExceptionSpreadSheetCell(text = exception.occurTime.toString())
ExceptionSpreadSheetCell(text = exception.callStack)
}
}
}
I created a ExceptionSpreadSheetHeader for the sticky title, which will display some static names for columns, and a ExceptionSpreadSheetRow for rows below the header, each one has a sticky id column.
Then I combined these two components together as ``
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun ExceptionsSpreadSheet(exceptions: List<DropBoxException>) {
val sharedScrollState = rememberLazyListState()
if (sharedScrollState.isScrollInProgress) {
sharedScrollState.firstVisibleItemIndex
sharedScrollState.firstVisibleItemScrollOffset
}
LazyColumn {
stickyHeader {
ExceptionSpreadSheetHeader(exception = exceptions.first())
ExceptionSpreadSheetHeader(exception = exceptions.first(), state = sharedScrollState)
ExceptionSpreadSheetHeader(state = sharedScrollState)
ExceptionSpreadSheetRow(exception = exceptions.first(), state = sharedScrollState)
ExceptionSpreadSheetRow(state = sharedScrollState)
ExceptionSpreadSheetHeaderTest(state = sharedScrollState)
ExceptionSpreadSheetHeaderTest(exception = exceptions.first(), state = sharedScrollState)
}
items(exceptions) {
ExceptionSpreadSheetRow(exception = it, state = sharedScrollState)
}
}
}
I used sharedScrollState to sync horizontal scroll state across columns, then I found 2 strange behavior, any help or explanation would be great for me, thanks in advance.
in the items section, the shared scroll state does not work at first, only when i added the if (sharedScrollState.isScrollInProgress) part, the rows in items would sync, otherwise it looks like items_not_sync. I would love to know why it works after I added the if part, or why it does not work before
in the stickyHeaders section of ExceptionsSpreadSheet, I added a lot of rows for testing, and the result looks like header_not_sync. The first 6 lines are headers I added, including the 2 red one as ExceptionSpreadSheetHeaderTest:
@OptIn(ExperimentalFoundationApi::class)
@Preview(showBackground = true, widthDp = 960, heightDp = 540)
@Composable
fun ExceptionSpreadSheetHeaderTest(
exception: DropBoxException = DropBoxException(),
state: LazyListState = rememberLazyListState()
) {
LazyRow(
modifier = Modifier.background(color = Color.Red),
state = state
) {
stickyHeader {
ExceptionSpreadSheetCell(text = "id")
}
items(1) {
ExceptionSpreadSheetCell(text = "packageName")
ExceptionSpreadSheetCell(text = "processName")
ExceptionSpreadSheetCell(text = "packageVersionCode")
ExceptionSpreadSheetCell(text = "packageVersionName")
ExceptionSpreadSheetCell(text = "activity")
ExceptionSpreadSheetCell(text = "romVersion")
ExceptionSpreadSheetCell(text = "platformId")
ExceptionSpreadSheetCell(text = "productName")
ExceptionSpreadSheetCell(text = "modelName")
ExceptionSpreadSheetCell(text = "exceptionSummary")
ExceptionSpreadSheetCell(text = "exceptionType")
ExceptionSpreadSheetCell(text = "exceptionDigest")
ExceptionSpreadSheetCell(text = "androidId")
ExceptionSpreadSheetCell(text = exception.occurTime.toString())
ExceptionSpreadSheetCell(text = "callStack")
}
}
}
In the gif, only the 4th and 7th header is synced with contents below, so here are the strange behavior(s):
ExceptionSpreadSheetHeader(exception = exceptions.first(), state = sharedScrollState)) and the 7th header (ExceptionSpreadSheetHeaderTest(exception = exceptions.first(), state = sharedScrollState)) has nothing different but between the second last ExceptionSpreadSheetCell in there items section, it seems that I have to somehow use the exception passed in then the sync would work, that looks really strange to me.The DropBoxException class is a simple data class
data class DropBoxException(
var id: Int = 0,
var packageName: String = "",
var processName: String = "",
var packageVersionCode: String = "",
var packageVersionName: String = "",
var activity: String = "",
var romVersion: String = "",
var platformId: Int = -1,
var productName: String = "",
var modelName: String = "",
var exceptionSummary: String = "",
var exceptionType: String = "",
var exceptionDigest: String = "",
var androidId: String = "",
var occurTime: Int = 0,
var callStack: String = "",
)
and generateData() create some dummy data for me
fun generateData(): List<DropBoxException> = (1..300).map {
DropBoxException(
id = it,
packageName = "$it packageName",
processName = "$it processName"
)
}
If you beleived I mis-used some of the interfaces or the question is asked somewhere else before, feel free to post links and I'll look into them, thanks again!
After I checked bunch of examples, it's now clear to me that each row/column shall has its own state. So here is my current solution, by making each row's lazyListState listen to a mutable value, and change that value when any row scrolls
class MainActivity : ComponentActivity() {
// ...
}
val horizontalFirstVisibleItemIndex = mutableStateOf(0)
val horizontalFirstVisibleItemScrollOffset = mutableStateOf(0)
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun ExceptionsSpreadSheet(exceptions: List<DropBoxException>) {
// ...
}
@OptIn(ExperimentalFoundationApi::class)
@Preview(showBackground = true, widthDp = 960, heightDp = 540)
@Composable
fun ExceptionSpreadSheetHeader() {
val scrollState = rememberLazyListState()
if (scrollState.isScrollInProgress) {
horizontalFirstVisibleItemIndex.value = scrollState.firstVisibleItemIndex
horizontalFirstVisibleItemScrollOffset.value = scrollState.firstVisibleItemScrollOffset
}
LazyRow(modifier = Modifier.background(color = Color.DarkGray), state = scrollState) {
// ...
}
LaunchedEffect(horizontalFirstVisibleItemIndex.value, horizontalFirstVisibleItemScrollOffset.value) {
Log.v("test", "launch title $horizontalFirstVisibleItemScrollOffset")
scrollState.scrollToItem(horizontalFirstVisibleItemIndex.value, horizontalFirstVisibleItemScrollOffset.value)
}
}
@OptIn(ExperimentalFoundationApi::class)
@Preview(showBackground = true, widthDp = 960, heightDp = 540)
@Composable
fun ExceptionSpreadSheetRow(
exception: DropBoxException = DropBoxException(),
) {
val scrollState = rememberLazyListState()
if (scrollState.isScrollInProgress) {
horizontalFirstVisibleItemIndex.value = scrollState.firstVisibleItemIndex
horizontalFirstVisibleItemScrollOffset.value = scrollState.firstVisibleItemScrollOffset
}
LazyRow(state = scrollState) {
// ...
}
LaunchedEffect(horizontalFirstVisibleItemIndex.value, horizontalFirstVisibleItemScrollOffset.value) {
scrollState.scrollToItem(horizontalFirstVisibleItemIndex.value, horizontalFirstVisibleItemScrollOffset.value)
}
}
hope it could help anyone looking for solutions. It occurs to me with some animation lagging but good enough for my case, so I'm not digging more into this problem, but further optimization is welcomed.