I am working on a Jetpack Compose Messaging Screen, and I have a LazyColumn displaying messages. When the keyboard opens, I want the LazyColumn to resize (shrink) instead of shifting the entire layout upward, including the TopAppBar.
I have already tried the following:
Used android:windowSoftInputMode=”adjustResize” in the manifest for the activity.
Modified the LazyColumn’s weight dynamically based on keyboard visibility (WindowInsets.isImeVisible).
Ensured my Scaffold and TopAppBar are not scrollable by setting scrollBehavior = null.
However, the TopAppBar and the background layout still shift upward when the keyboard opens, which is not the desired behavior.
Here is the relevant part of my code:
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MessagingScreen(
id: String,
navController: NavController,
chatListViewModel: ChatListViewModel = hiltViewModel(),
messageViewModel: MessageViewModel = hiltViewModel()
) {
val windowSize = rememberWindowSizeClass()
// Dinamik boyutlandırmalar
val profileImageSize = when (windowSize.width) {
WindowType.Compact -> 50.dp
WindowType.Medium -> 60.dp
WindowType.Expanded -> 70.dp
}
val fontSize = when (windowSize.width) {
WindowType.Compact -> 14.sp
WindowType.Medium -> 16.sp
WindowType.Expanded -> 18.sp
}
val messagePadding = when (windowSize.width) {
WindowType.Compact -> 8.dp
WindowType.Medium -> 10.dp
WindowType.Expanded -> 12.dp
}
val textFieldHeight = when (windowSize.width) {
WindowType.Compact -> 50.dp
WindowType.Medium -> 60.dp
WindowType.Expanded -> 70.dp
}
val textFieldCornerRadius = when (windowSize.width) {
WindowType.Compact -> 12.dp
WindowType.Medium -> 16.dp
WindowType.Expanded -> 20.dp
}
// UIState ve ChatRoom
val chatRoom by chatListViewModel.getChatRoomById(id).collectAsState(initial = null)
val userCoins by chatListViewModel.userCoin.collectAsState(initial = 0)
val chatMessages by messageViewModel.uiState.collectAsState(initial = emptyList())
val isTyping by messageViewModel.isTyping.collectAsState()
var tfMessage by remember { mutableStateOf("") }
val listState = rememberLazyListState()
val snackbarHostState = remember { SnackbarHostState() }
val coroutineScope = rememberCoroutineScope()
// Mesajları yükleme
LaunchedEffect(id) {
messageViewModel.loadMessages(id)
}
// Scroll pozisyonunu son mesaja taşıma
LaunchedEffect(chatMessages.size) {
if (chatMessages.isNotEmpty()) {
listState.animateScrollToItem(chatMessages.size - 1)
}
}
// Hata mesajlarını gösterme
LaunchedEffect(Unit) {
messageViewModel.error.collect { errorMessage ->
coroutineScope.launch {
snackbarHostState.showSnackbar(message = errorMessage, actionLabel = "OK")
}
}
}
Scaffold(
snackbarHost = { SnackbarHost(snackbarHostState) },
topBar = {
TopAppBar(
title = {
Row(verticalAlignment = Alignment.CenterVertically) {
chatRoom?.let {
GlideImage(
url = it.userPicture ?: "",
modifier = Modifier
.size(profileImageSize)
.clip(CircleShape)
)
Spacer(modifier = Modifier.width(8.dp))
Column {
Text(
text = it.userName ?: "Unknown User",
color = Color.White,
fontWeight = FontWeight.Bold,
fontSize = fontSize
)
if (isTyping) {
Text(
text = "Typing...",
color = Color.LightGray,
fontSize = fontSize
)
}
}
} ?: Text(
text = "Loading...",
color = Color.Gray,
fontWeight = FontWeight.Normal,
fontSize = fontSize
)
}
},
navigationIcon = {
IconButton(onClick = { navController.navigateUp() }) {
Icon(
imageVector = Icons.Default.ArrowBack,
contentDescription = "Back",
tint = Color.White
)
}
},
actions = {
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(
painter = painterResource(id = R.drawable.ic_gold),
contentDescription = "Coin",
tint = Color.Yellow,
modifier = Modifier.size(profileImageSize - 20.dp)
)
Spacer(modifier = Modifier.width(4.dp))
Text(text = "$userCoins", color = Color.White, fontSize = fontSize)
}
},
colors = TopAppBarDefaults.topAppBarColors(containerColor = Color.Transparent)
)
}
) { paddingValues ->
Box(modifier = Modifier.fillMaxSize()) {
chatRoom?.userPicture?.let { userPictureUrl ->
GlideImage(
url = userPictureUrl,
modifier = Modifier.fillMaxSize(),
)
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.Black.copy(alpha = 0.3f))
)
}
Column(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
) {
LazyColumn(
state = listState,
modifier = Modifier
.weight(1f)
.padding(horizontal = messagePadding, vertical = messagePadding),
verticalArrangement = Arrangement.spacedBy(messagePadding)
) {
items(chatMessages, key = { it.Id }) { message ->
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = if (message.isSentByUser) Arrangement.End else Arrangement.Start
) {
Box(
modifier = Modifier
.widthIn(max = 300.dp)
.background(
color = if (message.isSentByUser) Color(0xFF8C6A5D).copy(alpha = 0.7f) else Color(0xFF5F374B).copy(alpha = 0.7f),
shape = RoundedCornerShape(18.dp)
)
.padding(messagePadding)
) {
Text(
text = message.messageText,
color = Color.White,
fontSize = fontSize
)
}
}
}
}
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 8.dp, vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically
) {
TextField(
value = tfMessage,
onValueChange = { tfMessage = it },
placeholder = {
Text(
text = "Type your message",
color = Color.White,
fontSize = fontSize
)
},
modifier = Modifier
.weight(1f)
.height(textFieldHeight)
.background(
Color(0xFF24253E),
shape = RoundedCornerShape(textFieldCornerRadius)
),
colors = TextFieldDefaults.colors(
focusedContainerColor = Color(0xFF4B4376),
unfocusedContainerColor = Color(0xFF4B4376),
focusedIndicatorColor = Color.Transparent,
unfocusedIndicatorColor = Color.Transparent,
focusedTextColor = Color.White,
unfocusedTextColor = Color.White
),
shape = RoundedCornerShape(textFieldCornerRadius),
singleLine = true
)
Spacer(modifier = Modifier.width(8.dp))
IconButton(
onClick = {
val message = tfMessage.trim()
if (message.isNotEmpty()) {
tfMessage = ""
messageViewModel.sendMessageWithCoins(
chatRoomId = id,
userMessage = message
) { result ->
result.fold(
onSuccess = {},
onFailure = { error ->
coroutineScope.launch {
snackbarHostState.showSnackbar(
message = error.message ?: "Failed to send message.",
actionLabel = "OK"
)
}
}
)
}
} else {
coroutineScope.launch {
snackbarHostState.showSnackbar(
message = "Message cannot be empty.",
actionLabel = "OK"
)
}
}
}
) {
Icon(
imageVector = Icons.Default.Send,
contentDescription = "Send Message",
tint = Color.White
)
}
}
}
}
}
}
What I Tried:
I set android:windowSoftInputMode=”adjustResize” in my activity’s manifest to handle keyboard resizing.
I used WindowInsets.isImeVisible to detect keyboard visibility and dynamically adjust the LazyColumn height with .weight(if (isKeyboardOpen) 0.7f else 1f).
I made sure my Scaffold and TopAppBar have scrollBehavior = null to avoid unintended scrolling behavior.
What I Expected:
When the keyboard opens, only the LazyColumn (message list) should shrink, leaving the TopAppBar and background layout stationary.
The rest of the layout (e.g., TopAppBar) should not shift upward.
What Actually Happened:
The TopAppBar and the entire background layout still move upward when the keyboard opens.
Dynamically resizing the LazyColumn worked partially, but it caused layout inconsistencies and didn’t completely solve the issue.
How can I achieve this behavior reliably in Jetpack Compose?