How to refresh UI in Kotlin with Compose desktop when runBlocking? - kotlin

I'm learning Kotlin and Compose Desktop and I'm trying refresh the UI before fetch data from an API.
But the request is running inside a runBlocking, thus the UI freezes until request is completed.
This is my code, everything works.
val client = HttpClient(CIO)
#OptIn(ExperimentalComposeUiApi::class)
#Composable
#Preview
fun App() {
var text by remember { mutableStateOf("Test button") }
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Column(
modifier = Modifier.padding(50.dp)
) {
Button(
onClick = {
text = "Wait..."//How to refresh UI to display this text?
runBlocking {
delay(5000)//blocking test
val response: HttpResponse = client.request("https://myapi.com/") {
// Configure request parameters exposed by HttpRequestBuilder
}
if (response.status == HttpStatusCode.OK) {
val body = response.body<String>()
println(body)
} else {
println("Error has occurred")
}
}
text = "Test button"
},
modifier = Modifier.fillMaxWidth()
) {
Text(text)
}
OutlinedTextField(
value = "",
singleLine = true,
onValueChange = { text = it }
)
}
}
}
fun main() = application {
Window(
onCloseRequest = ::exitApplication,
state = WindowState(size = DpSize(350.dp, 500.dp)),
title = "Compose test"
) {
App()
}
}
How to achieve that?

The problem here is that you are using runBlocking at all.
The most straightforward solution for your case would be to replace your runBlocking {} with a coroutine scope. At the top of your App() function, create your scope: val scope = rememberCoroutineScope(), then instead of runBlocking you can say scope.launch {}.
New code would be:
#OptIn(ExperimentalComposeUiApi::class)
#Composable
#Preview
fun App() {
var text by remember { mutableStateOf("Test button") }
val scope = rememberCoroutineScope()
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Column(
modifier = Modifier.padding(50.dp)
) {
Button(
onClick = {
text = "Wait..."//How to refresh UI to display this text?
scope.launch {
delay(5000)//blocking test
val response: HttpResponse = client.request("https://myapi.com/") {
// Configure request parameters exposed by HttpRequestBuilder
}
if (response.status == HttpStatusCode.OK) {
val body = response.body<String>()
println(body)
} else {
println("Error has occurred")
}
}
text = "Test button"
},
modifier = Modifier.fillMaxWidth()
) {
Text(text)
}
OutlinedTextField(
value = "",
singleLine = true,
onValueChange = { text = it }
)
}
}
}
I saw one comment say to use LaunchedEffect() but this won't work in your case since you can't use that in an onClick since it's not a Composable.

Related

Problem with custom dialog show in jetpack compose. Not correct background

I'm created custom dialog from common class in ini
init {
activity.setContent {
CustomDialog(viewModel)
}
}
#Composable
fun CustomDialog(viewModel: ViewModel){
Dialog(
onDismissRequest = { },
properties = DialogProperties(dismissOnBackPress = true, dismissOnClickOutside = true)
) {
}
}
But under dialog background is an empty activity, but must be a preference activity.
Not correct composable:
correct dialog via XML:
I tried, but didn't help
Surface(modifier = Modifier.background(Color.Transparent)) {
CustomDialog(viewModel)
}
```
Exampe:
Dialog(onDismissRequest = { onDismissRequest() }, properties = DialogProperties()) {
Column(
modifier = Modifier
.wrapContentSize()
.background(
color = MaterialTheme.colors.surface,
shape = RoundedCornerShape(size = 16.dp)
).clip(shape = RoundedCornerShape(size = 16.dp))
) {
//.....
}
}
Found solution:
dialog?.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
dialog?.window?.setDimAmount(0.0f)

How to test onDismissRequest attribute of AlertDialog?

In its simplest form I have this dialog:
#Composable
fun MyDialog(
showDialogState: MutableState<Boolean>
) {
if (showDialogState.value) {
AlertDialog(onDismissRequest = { showDialogState.value = false },
// Other irrelevant attributes have been omitted
)
}
}
How can I trigger "onDismissRequest" on this composable in Robolectric?
This is usually how I build my composable tests by the way:
#Config(sdk = [Build.VERSION_CODES.O_MR1])
#RunWith(AndroidJUnit4::class)
#LooperMode(LooperMode.Mode.PAUSED)
class MyDialogTest {
#get:Rule
val composeTestRule = createComposeRule()
#Test
fun `MyDialog - when showing state and dismissed - changes showing state`() {
val state = mutableStateOf(true)
composeTestRule.setContent {
MyDialog(
showDialogState = state
)
}
// TODO: How do I trigger dismiss!?
assertFalse(state.value)
}
}
Compose version: 1.1.0-rc01
Android Gradle Plugin version: 7.0.4
Robolectric version: 4.7.3
I don't think this is possible at the moment. I have written this test to confirm:
val onButtonPressed = mock<() -> Unit>()
composeTestRule.setContent {
Scaffold(topBar = {
TopAppBar {
Text(text = "This test does not work")
}
}) {
AlertDialog(
onDismissRequest = {},
properties = DialogProperties(
dismissOnBackPress = true,
dismissOnClickOutside = true
),
title = { Text(text = "This is a dialog")},
confirmButton = { Button(onClick = {}) {
Text(text = "Confirm")
}}
)
Column(modifier = Modifier.fillMaxSize()) {
Spacer(modifier = Modifier.weight(1f))
Button(onClick = onButtonPressed) {
Text(text = "test")
}
}
}
}
composeTestRule.onNode(isDialog()).assertExists()
composeTestRule.onNodeWithText("test", ignoreCase = true).performClick()
verify(onButtonPressed).invoke()
composeTestRule.onNode(isDialog()).assertDoesNotExist()
Even though the button is "behind" the dialog, it receives click events without dismissing the dialog.
Manual testing has confirmed that the implementation works, so perhaps a UIAutomator test could automate this, but that seems like an overly complicated way of solving this issue.
I quote the official documentation:
Dismiss the dialog when the user clicks outside the dialog or on the
back button. If you want to disable that functionality, simply use an
empty onCloseRequest.
https://foso.github.io/Jetpack-Compose-Playground/material/alertdialog/

Navigate with Jetpack Compose

I was testing Compose and navigation and noticed strange behavior (nav ver: androidx.navigation:navigation-compose:2.4.0-alpha10)
In this example, is a screen to check if the app is up to date or not (boolean var in the code for simple test), and if yes, navigate to other screen.
The big issue I found here was that when automating the navigation, the second screen switches to the first one very quickly. But if the navigation is done with the click of a button, for example, nothing strange happens...
MainActivity file code:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
AppTheme {
Surface(color = MaterialTheme.colors.background) {
val navController = rememberNavController()
NavHost(
navController = navController,
startDestination = "first_screen"
) {
composable(route = "first_screen") {
FirstScreen(navController)
}
composable(route = "second_screen") {
SecondScreen(navController)
}
}
}
}
}
}
}
FirstScreen file code:
#Composable
fun FirstScreen(navController: NavController) {
val isAppUpdated = true // switch case
Scaffold(
topBar = {
TopAppBar {
Text(text = "First Screen Bar")
}
}
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
modifier = Modifier.fillMaxSize()
) {
Text(
text = "First Screen",
textAlign = TextAlign.Center,
style = MaterialTheme.typography.h4
)
if (!isAppUpdated) OutdatedApp()
else {
// Two options to navigate: with compose button or automatically
// UpdatedApp {
// navigateToSecondScreen(navController)
// }
navigateToSecondScreen(navController)
}
}
}
}
#Composable
fun OutdatedApp() {
Text(
text = "Your app is out of date, consider updating it!",
textAlign = TextAlign.Center,
style = MaterialTheme.typography.body1
)
}
#Composable
fun UpdatedApp(onClick: () -> Unit) {
Button(
onClick = { onClick() }
) {
Text(
text = "Navigate to Second Screen",
style = MaterialTheme.typography.button
)
}
}
private fun navigateToSecondScreen(navController: NavController) {
navController.navigate(route = "second_screen") {
launchSingleTop
popUpTo(route = "first_screen") { inclusive = true }
}
}
SecondScreen file code:
#Composable
fun SecondScreen(navController: NavController) {
Scaffold(
topBar = {
TopAppBar {
Text(text = "Second Screen Bar")
}
}
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
modifier = Modifier.fillMaxSize()
) {
Text(
text = "Second Screen",
textAlign = TextAlign.Center,
style = MaterialTheme.typography.h4
)
}
}
}
Can anyone explain what happens in these cases?
And any solution to implement this logic?
EDIT
I did another test without Scaffold on FirstScreen and it looks like this problem didn't happen...
SOLUTION
Philip's answer
LaunchedEffect's documentation

Jetpack Compose - Disable TextField long press handler

I have an IconButton in the trailingIcon of OutlinedTextField like:
OutlinedTextField(
modifier = Modifier.weight(1f),
label = { Text(text = "Label") },
value = text,
onValueChange = { text = it },
trailingIcon = {
IconButton2(onClick = {
println("onClick")
}, onLongClick = {
println("onLongClick shows TextToolbar")
}) {
Icon(
imageVector = Icons.Filled.Menu,
contentDescription = null
)
}
}
)
IconButton2 is just a copy of IconButton but with combinedClickable to include onLongClick instead of clickable.
The problem is that when I long click IconButton2, it shows the TextToolbar for the TextField. Doesn't matter what I do, the text field will handle long click, show the TextToolbar and provide haptic feedback.
Even if I use pointerInput with awaitPointerEvent and consumeAllChanges (like here) it still triggers it. The TextField doesn't answer to any tap or anything but if I long click it, it answers!
The workaround I'm doing for now is wrapping the text field in a Row and add the IconButton beside it instead of "inside" but I needed to have the icon button as the trailingIcon.
Is there any way to properly do it?
Compose 1.0.3 and 1.1.0-alpha05 both behaves the same.
I ended up making a small hack that seems to be working fine, so basically I add a dummy Box as the trailingIcon to get the position of it, then I add an IconButton outside of it (both wrapped in a Box) and I also get the position of it + I offset it using the position of the dummy box. Not the ideal solution but works fine.
Here's the full source if anyone needs it:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MyApplicationTheme {
Surface(
modifier = Modifier.padding(16.dp),
color = MaterialTheme.colors.background
) {
var text by remember { mutableStateOf("") }
var trailingIconOffset by remember { mutableStateOf(Offset.Zero) }
var iconButtonOffset by remember { mutableStateOf(Offset.Zero) }
val colors = TextFieldDefaults.outlinedTextFieldColors()
Column {
//With hack
Box {
OutlinedTextField(
modifier = Modifier.fillMaxWidth(),
label = { Text(text = "With hack") },
value = text,
onValueChange = { text = it },
trailingIcon = {
Box(modifier = IconButtonSizeModifier
.onGloballyPositioned {
trailingIconOffset = it.positionInRoot()
}
)
},
colors = colors
)
val contentColor by colors.trailingIconColor(
enabled = true,
isError = false
)
CompositionLocalProvider(
LocalContentColor provides contentColor,
LocalContentAlpha provides contentColor.alpha
) {
IconButton2(
modifier = Modifier
.onGloballyPositioned {
iconButtonOffset = it.positionInRoot()
}
.absoluteOffset {
IntOffset(
(trailingIconOffset.x - iconButtonOffset.x).toInt(),
(trailingIconOffset.y - iconButtonOffset.y).toInt()
)
},
onClick = {
text = "onClick"
},
onLongClick = {
text = "onLongClick"
}
) {
Icon(imageVector = Icons.Filled.Menu, contentDescription = null)
}
}
}
//Without hack
Box {
OutlinedTextField(
modifier = Modifier.fillMaxWidth(),
label = { Text(text = "Without hack") },
value = text,
onValueChange = { text = it },
trailingIcon = {
IconButton2(
onClick = {
text = "onClick"
},
onLongClick = {
text = "onLongClick"
}
) {
Icon(
imageVector = Icons.Filled.Menu,
contentDescription = null
)
}
},
colors = colors
)
}
}
}
}
}
}
}
private val RippleRadius = 24.dp
private val IconButtonSizeModifier = Modifier.size(48.dp)
#OptIn(ExperimentalFoundationApi::class)
#Composable
fun IconButton2(
modifier: Modifier = Modifier,
onClick: () -> Unit,
onLongClick: (() -> Unit)? = null,
enabled: Boolean = true,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
content: #Composable () -> Unit
) {
Box(
modifier = modifier
.combinedClickable(
onClick = onClick,
onLongClick = onLongClick,
enabled = enabled,
role = Role.Button,
interactionSource = interactionSource,
indication = rememberRipple(bounded = false, radius = RippleRadius)
)
.then(IconButtonSizeModifier),
contentAlignment = Alignment.Center
) {
val contentAlpha = if (enabled) LocalContentAlpha.current else ContentAlpha.disabled
CompositionLocalProvider(LocalContentAlpha provides contentAlpha, content = content)
}
}

How to move DropdownMenu to preferable location in Jepack Compose

Is it possible to center DropdownMenu in my example? or show it wherever I click or tap?
I tried alignment and arrangements and none of them work. I prefer showing the DropdownMenu wherever I tab but I couldn't find a way to do it.
fun main() = Window {
var helloText by remember { mutableStateOf("") }
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Item("Darian", "Russ") {
helloText = it
}
Item("Maynerd", "Andre") {
helloText = it
}
Item("Sandra", "Victoria") {
helloText = it
}
Spacer(modifier = Modifier.height(2.dp))
Text(text = helloText)
}
}
#Composable
fun Item(text: String, text2: String, onMenuTab: (String) -> Unit) {
var expanded by remember { mutableStateOf(false) }
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
val modifier = Modifier.clickable {
expanded = true
}
DropdownMenu(
expanded = expanded,
onDismissRequest = {
expanded = false
}
) {
DropdownMenuItem(onClick = {
onMenuTab("hello $text $text2")
expanded = false
}, modifier = Modifier.align(Alignment.CenterHorizontally)) {
Text("Hello")
}
DropdownMenuItem(onClick = { /* Handle settings! */ }) {
Text("Settings")
}
Divider()
DropdownMenuItem(onClick = { /* Handle send feedback! */ }) {
Text("Send Feedback")
}
}
Text(text = text, modifier = modifier)
Text(text = text2, modifier = modifier)
Divider(modifier = Modifier.height(2.dp))
Spacer(modifier = Modifier.height(2.dp))
}
}
For me fixing the alignment of the popup did the trick-
Popup(alignment = Alignment.TopStart)
Earlier it was Popup(alignment = Alignment.CenterStart)
and that was taking my popup view to the top of the screen. But now comes below the clicked item.