BottomSheetScaffold auto-expand sheetContent in jetpack compose - kotlin

In Jetpack Compose, how to automatically expand the sheetContent of BottomSheetScaffold?
BottomSheetScaffold(
topBar = {},
sheetContent = { CommentView(navController, wind) }) {
}

Found a solution, so simple
val scaffoldState = rememberBottomSheetScaffoldState()
val scope = rememberCoroutineScope()
scope.launch {
scaffoldState.bottomSheetState.expand()
}

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 refresh UI in Kotlin with Compose desktop when runBlocking?

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.

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

Compose navigation back button is not showing

I am trying to set up the navigation back button following this SO thread, but can't make it work.
#ExperimentalFoundationApi
#Composable
fun LazyVerticalGridActivityScreen() {
val navController = rememberNavController()
val navigationIcon: (#Composable () -> Unit)? =
if (navController.previousBackStackEntry != null) {
{
IconButton(onClick = { navController.popBackStack() }) {
Icon(imageVector = Icons.Filled.ArrowBack, contentDescription = null)
}
}
} else {
null
}
Scaffold(
topBar = {
TopAppBar(title = { Text("Lazy Vertical Grid") }, navigationIcon = navigationIcon)
},
content = {
NavHost(navController = navController, startDestination = "home") {
composable("home") { HomeScreen(navController) }
composable("details/{listId}") { backStackEntry ->
backStackEntry.arguments?.getString("listId")
?.let { DetailsScreen(it, navController) }
}
}
}
)
}
Can anyone please help to fix this? Thanks!
Changing navController state doesn't make it container recompose, that's why this navigationIcon stays null.
To make it recompose you need to use addOnDestinationChangedListener:
var canPop by remember { mutableStateOf(false) }
DisposableEffect(navController) {
val listener = NavController.OnDestinationChangedListener { controller, _, _ ->
canPop = controller.previousBackStackEntry != null
}
navController.addOnDestinationChangedListener(listener)
onDispose {
navController.removeOnDestinationChangedListener(listener)
}
}
val navigationIcon: (#Composable () -> Unit)? =
if (canPop) {
{
IconButton(onClick = { navController.popBackStack() }) {
Icon(imageVector = Icons.Filled.ArrowBack, contentDescription = null)
}
}
} else {
null
}
...