Strange behaviour with horizontalArrangement using JetPack Compose - android-jetpack-compose

I don't understand why my switches are not aligned, why it is expanding outside the screenview ? of course I didn't succeed so far to find a proper solution to align my switches with jetpack compose.
the kotlin code :
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.*
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Switch
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
class ToolsActivityK2 : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MaterialTheme {
ToolContent_()
}
}
}
}
#Preview
#Composable
fun Preview_ToolContent_() {
ToolContent_()
}
#Composable
fun SettingsSwitch_(
subtitle: String
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(start = 16.dp, end = 16.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
subtitle,
Modifier.padding(end = 16.dp)
)
Switch(checked = false, onCheckedChange = null)
}
}
#Composable
fun ToolContent_() {
Column {
SettingsSwitch_("launch something 1")
SettingsSwitch_("launch launch launch launch again and again and again something 2")
SettingsSwitch_("launch something 3")
}
}
if I replace the switches by images, it's worse the image for line 2 does not show on screen ! I for sure miss something but I cannot figure what
Configuration infos:
Android Studio Bumblebee | 2021.1.1
classpath 'com.android.tools.build:gradle:7.1.0'
ext.kotlin_version = '1.6.10'
kotlinCompilerExtensionVersion '1.1.0-rc02'
androidx.compose.ui:ui:1.0.5

Use weight to achieve something like this,
Complete code
#Composable
fun SettingsSwitch_(
subtitle: String
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(start = 16.dp, end = 16.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
Text(
subtitle,
Modifier.padding(end = 16.dp).weight(1f)
)
Switch(checked = false, onCheckedChange = null)
}
}
Reason
I have applied background color to all the components in your code to show how they are placed.
#Composable
fun SettingsSwitch_(
subtitle: String
) {
Row(
modifier = Modifier
.fillMaxWidth()
.background(Red)
.padding(start = 16.dp, end = 16.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
) {
Text(
subtitle,
Modifier.background(Cyan).padding(end = 16.dp)// .weight(1f)
)
Switch(checked = false, onCheckedChange = null, modifier = Modifier.background(Green))
}
}
And this code gives the following layout.
So, you can see that the Text has occupied the full space available in the Row. Hence the Switch appears in the wrong position.
More info
If we look into the docs for Arrangement.SpaceBetween we can see,
Place children such that they are spaced evenly across the main axis,
without free space before the first child or after the last child.
Visually: 1##2##3 for LTR or 3##2##1 for RTL.
All the Arrangement options only affect the extra free space between the children. Since the is no extra free space in the second Row the Arrangement does not have any effect there.

Related

NullPointerException at androidx.compose.ui.platform.RenderNodeLayer.updateDisplayList

I have a Drawer on my MainScreen which navigates me through drawer screens, but for some reason when I try to navigate to two specific screens (this only happens at the first time when I open the app), my app crashes with this error:
FATAL EXCEPTION: main
Process: com.tags.taglife, PID: 21478
java.lang.NullPointerException at androidx.compose.ui.platform.RenderNodeLayer.updateDisplayList(RenderNodeLayer.android.kt:245) at androidx.compose.ui.platform.AndroidComposeView.dispatchDraw(AndroidComposeView.android.kt:760) at android.view.View.draw(View.java:24398) at android.view.View.updateDisplayListIfDirty(View.java:23256) at android.view.ViewGroup.recreateChildDisplayList(ViewGroup.java:4732) at android.view.ViewGroup.dispatchGetDisplayList(ViewGroup.java:4704) at android.view.View.updateDisplayListIfDirty(View.java:23203) at android.view.ViewGroup.recreateChildDisplayList(ViewGroup.java:4732) at android.view.ViewGroup.dispatchGetDisplayList(ViewGroup.java:4704) at android.view.View.updateDisplayListIfDirty(View.java:23203)
at android.view.ViewGroup.recreateChildDisplayList(ViewGroup.java:4732)
at android.view.ViewGroup.dispatchGetDisplayList(ViewGroup.java:4704)
at android.view.View.updateDisplayListIfDirty(View.java:23203)
at android.view.ViewGroup.recreateChildDisplayList(ViewGroup.java:4732)
at android.view.ViewGroup.dispatchGetDisplayList(ViewGroup.java:4704)
at android.view.View.updateDisplayListIfDirty(View.java:23203)
at android.view.ThreadedRenderer.updateViewTreeDisplayList(ThreadedRenderer.java:753)
at android.view.ThreadedRenderer.updateRootDisplayList(ThreadedRenderer.java:759)
at android.view.ThreadedRenderer.draw(ThreadedRenderer.java:857)
at android.view.ViewRootImpl.draw(ViewRootImpl.java:5501)
at android.view.ViewRootImpl.performDraw(ViewRootImpl.java:5194)
at android.view.ViewRootImpl.performTraversals(ViewRootImpl.java:4356)
at android.view.ViewRootImpl.doTraversal(ViewRootImpl.java:2991)
at android.view.ViewRootImpl$TraversalRunnable.run(ViewRootImpl.java:10665)
at android.view.Choreographer$CallbackRecord.run(Choreographer.java:1301)
at android.view.Choreographer$CallbackRecord.run(Choreographer.java:1309)
at android.view.Choreographer.doCallbacks(Choreographer.java:923)
at android.view.Choreographer.doFrame(Choreographer.java:852)
at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:1283)
at android.os.Handler.handleCallback(Handler.java:942)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loopOnce(Looper.java:226)
at android.os.Looper.loop(Looper.java:313)
at android.app.ActivityThread.main(ActivityThread.java:8741)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:571)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1067)
This is the code for one of those screens:
package com.tags.taglife.presentation.MainScreen.sidebar
import android.annotation.SuppressLint
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.InlineTextContent
import androidx.compose.foundation.text.appendInlineContent
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.Placeholder
import androidx.compose.ui.text.PlaceholderVerticalAlign
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.hilt.navigation.compose.hiltViewModel
import com.tags.taglife.presentation.MainScreen.MainViewModel
import com.tags.taglife.presentation.MainScreen.NavScreens
import com.tags.taglife.presentation.ProgressScreen
import com.tags.taglife.presentation.tutorial.ItemModel
import com.tags.taglife.presentation.tutorial.TutorialViewModel
import com.tags.taglife.ui.theme.bottomLine
import com.tags.taglife.ui.theme.white
import taglife.R
#SuppressLint("CoroutineCreationDuringComposition")
#Composable
fun Tutorial(
modifier: Modifier = Modifier,
viewModel: MainViewModel,
tutorialViewModel: TutorialViewModel = hiltViewModel()
) {
viewModel.setCurrentScreen(NavScreens.DrawerScreens.Tutorial)
val isDark = tutorialViewModel.isDarkTheme
val loading = tutorialViewModel.isLoaded
if (!loading.value)
ProgressScreen()
else {
Column(
modifier = modifier
.fillMaxWidth()
.verticalScroll(rememberScrollState())
.padding(20.dp),
) {
//INSTRUCTIONS FOR USE
Title(R.string.instructions_for_use)
tutorialViewModel.instructions.mapIndexed { index, item ->
Item(isDark = isDark.value, item = item, index = index + 1)
}
Spacer(modifier = Modifier.height(15.dp))
//FUNCTIONS
Title(text = R.string.functions_of_homepage_buttons)
tutorialViewModel.functions.mapIndexed { index, item ->
Item(isDark = isDark.value, item = item, index = index + 1)
}
Spacer(modifier = Modifier.height(15.dp))
//QR CODE
Title(text = R.string.qr_code_functions)
tutorialViewModel.qrCodeFunctions.mapIndexed { index, item ->
Item(isDark = isDark.value, item = item, index = index + 1)
}
Spacer(modifier = Modifier.height(15.dp))
//NON NFC SCAN TAG
Title(text = R.string.non_nfc_scan_tag)
tutorialViewModel.nonNFCScanTag.mapIndexed { index, item ->
Item(isDark = isDark.value, item = item, index = index + 1)
}
}
}
}
#Composable
fun Title(text: Int) {
Text(
text = stringResource(id = text),
fontWeight = FontWeight.Bold,
fontSize = 20.sp,
color = MaterialTheme.colors.primary,
)
}
#Composable
fun Item(
isDark : Boolean,
item: List<ItemModel>,
index: Int
) {
val myId = index.toString()
var image: Int = R.drawable.vector
var mainText = buildAnnotatedString {
append("$index. ")
item.forEach {
if (it.text != null)
append(stringResource(id = it.text!!))
if (it.icon != null) {
image = it.icon!!
appendInlineContent(myId, "[icon]")
}
}
}
val inlineContent = mapOf(
Pair(
myId,
InlineTextContent(
Placeholder(
width = 30.sp,
height = 30.sp,
placeholderVerticalAlign = PlaceholderVerticalAlign.TextCenter
)
) {
Image(
painter = painterResource(image),
modifier = Modifier.padding(5.dp),
contentDescription = "",
colorFilter = if (isDark) ColorFilter.tint(white) else null
)
}
)
)
Column(
modifier = Modifier
.fillMaxWidth(), verticalArrangement = Arrangement.Center
) {
Text(
text = mainText,
inlineContent = inlineContent,
textAlign = TextAlign.Start,
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 12.dp)
)
Spacer(
modifier = Modifier
.height(1.dp)
.fillMaxWidth()
.background(bottomLine)
)
}
}
I tried simply to navigate to those screens via drawer.
I figured it out.
I needed to update my whole project to a new jetpack compose version, and compileSdk to 33.

LazyColumn does not show me the list of items

I'm just learning how to use LayzyColumn. The LazyColumn does not show me the list of items. When I click the button, the items that are loaded in the list are not displayed.
Here my code:
#Composable
fun MainContent() {
val list = remember { mutableListOf<String>() }
LazyColumn(
contentPadding = PaddingValues(12.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
itemsIndexed(list) { index, item ->
Card(
backgroundColor = Color(0xFFF7C2D4),
elevation = 4.dp
) {
Row(
Modifier
.fillMaxWidth()
.padding(12.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(text = item.takeLast(5), Modifier.weight(1F))
}
}
}
}
Box(
modifier = Modifier
.fillMaxWidth()
.padding(12.dp),
Alignment.BottomEnd
) {
FloatingActionButton(onClick = { list.add(UUID.randomUUID().toString()) }) {
Icon(imageVector = Icons.Default.Add, contentDescription = "")
}
}
}
Compose cannot track changes of plain kotlin types. You need to use Compose mutable states.
In this case mutableStateListOf should be used instead of mutableListOf.
I suggest you start with this youtube video which explains the basic principles of why do you need to use state in Compose. You can continue deepening your knowledge with state in Compose documentation.

Modifier.scrollable doesn't scroll the content

I am trying to have a layout where the content should be scrollable. But it doesn't work. Am I doing anything wrong here?
#Composable
fun Screen() {
Box(
modifier = Modifier
.padding(16.dp)
.fillMaxSize()
) {
val offset = remember { mutableStateOf(0f) }
Column( // This should be scrollable
modifier = Modifier
.fillMaxSize()
.scrollable(
orientation = Orientation.Vertical,
state = rememberScrollableState { delta ->
offset.value = offset.value + delta
delta
}
)
) {
Text(...)
Text(...)
Spacer(modifier = Modifier.padding(8.dp))
LazyColumn(...)
Spacer(modifier = Modifier.padding(8.dp))
DropdwonMenu(...)
Text(...)
Spacer(modifier = Modifier.padding(8.dp))
Text(...)
Spacer(modifier = Modifier.padding(8.dp))
Column(...)
Spacer(modifier = Modifier.padding(8.dp))
Button(...)
}
Column(
Modifier
.fillMaxWidth()
.align(Alignment.BottomCenter)
) {
Error(snackbarHostState)
}
}
LaunchedEffect(state.error) {
if (state.error.isNotEmpty()) {
snackbarHostState.showSnackbar(state.error)
}
}
Replace scrollable with .verticalScroll(rememberScrollState()). Your code is updating offset but this value is not actually being used anywhere to offset content. scrollable is useful if you need fine control over scrolling. But it appears in your case that you don't need it and verticalScroll will do the job.

How can I judge whether the code will run inside of the Composition or outside of the Composition in Android Studio Compose?

The following content is from the article.
Using LaunchedEffect in this case wasn't possible because we needed to trigger the call to create a coroutine in a regular callback that was outside of the Composition.
I was told that I should use LaunchedEffect when the code run inside of the Composition, and I should use rememberCoroutineScope when the code run outside of the Composition.
The Code A and Code B are samples from the article above.
But I don't understand why scaffoldState.drawerState.open() will run outside of the Compositionn in Code A? You know that fun CraneHome() is #Composable.
Code A
#Composable
fun CraneHome(
onExploreItemClicked: OnExploreItemClicked,
modifier: Modifier = Modifier,
) {
val scaffoldState = rememberScaffoldState()
Scaffold(
scaffoldState = scaffoldState,
modifier = Modifier.statusBarsPadding(),
drawerContent = {
CraneDrawer()
}
) {
val scope = rememberCoroutineScope()
CraneHomeContent(
modifier = modifier,
onExploreItemClicked = onExploreItemClicked,
openDrawer = {
scope.launch {
scaffoldState.drawerState.open()
}
}
)
}
}
Code B
#Composable
fun LandingScreen(modifier: Modifier = Modifier, onTimeout: () -> Unit) {
Box(modifier = modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
val currentOnTimeout by rememberUpdatedState(onTimeout)
LaunchedEffect(true) {
delay(SplashWaitTime)
currentOnTimeout()
}
Image(painterResource(id = R.drawable.ic_crane_drawer), contentDescription = null)
}
}

Use Dialog as navigation destination with jetpack compose

A dialog can have a rather complex ui, acting more like a floating screen rather than a typical AlertDialog. Therefore it can be desired to let the dialog have its own ViewModel and being able to navigate to it. When using the jetpack compose navigation artifact the code indicates that only one composable is shown at any time inside the NavHost.
Is there a way to navigate to a dialog that is overlaid onto the current ui? This would be in line with how we can navigate to fragment dialogs. Thanks.
Aha. This is now a feature in compose navigation version 2.4.0-alpha04
From the release notes
The NavHost of the navigation-compose artifact now supports dialog
destinations in addition to composable destinations. These dialog
destinations will each be shown within a Composable Dialog, floating
above the current composable destination.
val navController = rememberNavController()
Scaffold { innerPadding ->
NavHost(navController, "home", Modifier.padding(innerPadding)) {
composable("home") {
// This content fills the area provided to the NavHost
HomeScreen()
}
dialog("detail_dialog") {
// This content will be automatically added to a Dialog() composable
// and appear above the HomeScreen or other composable destinations
DetailDialogContent()
}
}
}
Example:
val navController = rememberNavController()
NavHost(
navController = navController,
startDestination = Screen.Menu.route
) {
// some other screens here: composable(...) { ... }
dialog(
route = "exit_dialog",
dialogProperties = DialogProperties(
dismissOnBackPress = true,
dismissOnClickOutside = true,
)
) {
Box(modifier = Modifier.width(280.dp)) {
Box(
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(10.dp))
.background(DialogBorder)
.padding(bottom = 3.dp)
.clip(RoundedCornerShape(10.dp))
.background(DialogBackground),
) {
Column {
Column(
modifier = Modifier.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = stringResource(id = R.string.text_dialog_exit_title),
fontSize = 16.sp,
fontWeight = FontWeight.Bold,
textAlign = TextAlign.Center,
color = Color.Black
)
Text(
text = stringResource(id = R.string.text_dialog_exit_description),
fontSize = 16.sp,
fontWeight = FontWeight.Normal,
textAlign = TextAlign.Center,
color = Color.Black
)
}
Divider(color = DialogBorder)
Row(modifier = Modifier.height(IntrinsicSize.Min)) {
Box(
modifier = Modifier
.weight(1f)
.clickable {
// dismiss dialog
navController.popBackStack()
}
.padding(horizontal = 16.dp, vertical = 8.dp),
contentAlignment = Alignment.Center
) {
Text(
text = stringResource(id = R.string.button_cancel),
fontSize = 16.sp,
fontWeight = FontWeight.Bold,
color = Color.Black
)
}
Box(
modifier = Modifier
.fillMaxHeight()
.width(1.dp)
.background(DialogBorder),
)
Box(
modifier = Modifier
.weight(1f)
.clickable {
// go back to home other screen
navController.popBackStack(
route = "home_screen",
inclusive = false
)
}
.padding(horizontal = 16.dp, vertical = 8.dp),
contentAlignment = Alignment.Center
) {
Text(
text = stringResource(id = R.string.button_ok),
fontSize = 16.sp,
fontWeight = FontWeight.Normal,
color = Color.Black
)
}
}
}
}
}
}
}
It's a feature request: https://issuetracker.google.com/issues/179608120
You can star it so perhaps we'll increase it's priority

Resources