How can I open Keyboard on Button click in Jetpack Compose? I have a button and I want the keyboard to open when I click this button. How can I do this? This is my screen and the keyboard does not open when I click:
#ExperimentalComposeUiApi
#Composable
fun MainScreen(
viewModel: MainScreenViewModel = hiltViewModel(),
) {
val number = viewModel.score.value
val kc = LocalSoftwareKeyboardController.current
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Column(modifier = Modifier.align(Alignment.Center)) {
ScoreBoard(number)
Spacer(modifier = Modifier.height(12.dp))
Button(onClick = { viewModel.increaseScore(1) }) { Text(text = "Increase Number") }
Spacer(modifier = Modifier.height(12.dp))
Button(onClick = {
kc?.show()
viewModel.startGame()
}) { Text(text = "Start the game") }
}
}
}
You can use the LocalSoftwareKeyboardController.current to get the software keyboard controller inside a composable.
Then you can use its show() and hide() functions.
Keep in mind that the soft keyboard can usually only be opened if an input (or something that accepts input) has focus.
Since this is an experimental Compose API, you will have to annotate the parent function with #OptIn(ExperimentalComposeUiApi::class) (or #ExperimentalComposeUiApi)
//#ExperimentalComposeUiApi
#OptIn(ExperimentalComposeUiApi::class)
#Composable
fun OpenKeyboardExample() {
val kc = LocalSoftwareKeyboardController.current
Button(
onClick = { kc?.show() }
) {
// ...
}
}
If you get warnings related to opt-in you can add the compiler argument to your app.gradle
android {
...
kotlinOptions {
freeCompilerArgs += "-opt-in=kotlin.RequiresOptIn"
}
}
From LocalSoftwareKeyboardController.current docs:
Return a SoftwareKeyboardController that can control the current software keyboard.
If it is not provided, the default implementation will delegate to LocalTextInputService.
Returns null if the software keyboard cannot be controlled.
Source: https://developer.android.com/reference/kotlin/androidx/compose/ui/platform/LocalSoftwareKeyboardController
Related
I want to implement a simple user flow, where the user sees multiple screens to input data. The flow should share a common navbar where each screen can contribute its menu items to when it is active (e.g. add a "search" or a "next" button). The navbar also has buttons belonging conceptually to the user flow and not to individual screens (like the back button and a close button). Screens should be reusable in other contexts, so screens should not know about the flow they operate in.
Technically the user flow is implemented as a compose function defining the navbar and using compose navigation. Each screen is implemented as a separate compose function.
In fragment/view based Android this scenario was supported out of box with onCreateOptionsMenu and related functions. But how would I do this in compose? I could not find any guidance on that topic.
To illustrate the problem in code:
#Composable
fun PaymentCoordinator(
navController: NavHostController = rememberNavController()
) {
AppTheme {
Scaffold(
bottomBar = {
BottomAppBar(backgroundColor = Color.Red) {
IconButton(onClick = navController::popBackStack) {
Icon(Icons.Filled.ArrowBack, "Back")
}
Spacer(modifier = Modifier.weight(1f))
// 0..n IconButtons provided by the active Screen
// should be inserted here
// How can we do that, because state should never
// go up from child to parent
// this button (or at least its text and onClick action) should
// be defined by the currently visible Screen as well
Button(
onClick = { /* How to call function of screen? */ }
) {
Text("Next"))
}
}
}
) { padding ->
Box(modifier = Modifier.padding(padding)) {
NavHost(
navController = navController,
startDestination = "selectAccount"
) {
// screens that can contribute items to the menu
composable("selectAccount") {
AccountSelectionRoute(
onAccountSelected = {
navController.navigate("nextScreen")
}
)
}
composable("...") {
// ...
}
}
}
}
}
}
I came up with an approach leveraging side effects and lifecycle listener to achieve my goal. Basically whenever a screen becomes active (ON_START) it informs the parent (coordinator) about its menu configuration. The coordinator evaluates the configuration and updates the navbar accordingly.
The approach is based on Googles documentation on side effects (https://developer.android.com/jetpack/compose/side-effects#disposableeffect)
The approach feels complicated and awkward and I think the compose framework is missing some functionality to achieve this here. However, my implementation seems to be working fine in my test use case.
Helper classes
// currently I only need to configure a single button, however the approach
// can be easily extended now (you can put anything inside rightButton)
data class MenuConfiguration(
val rightButton: #Composable () -> Unit
)
#Composable
fun SimpleMenuConfiguration(
lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current,
onRegisterMenuConfiguration: (MenuConfiguration?) -> Unit,
onUnregisterMenuConfiguration: () -> Unit,
rightButton: #Composable () -> Unit
) {
val currentOnRegisterMenuConfiguration by rememberUpdatedState(onRegisterMenuConfiguration)
val currentOnUnregisterMenuConfiguration by rememberUpdatedState(onUnregisterMenuConfiguration)
DisposableEffect(lifecycleOwner) {
val observer = LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_START) {
currentOnRegisterMenuConfiguration(
MenuConfiguration(
rightButton = rightButton
)
)
} else if (event == Lifecycle.Event.ON_STOP) {
currentOnUnregisterMenuConfiguration()
}
}
lifecycleOwner.lifecycle.addObserver(observer)
onDispose {
lifecycleOwner.lifecycle.removeObserver(observer)
}
}
}
Coordinator level
#Composable
fun PaymentCoordinator(
navController: NavHostController = rememberNavController()
) {
var menuConfiguration by remember { mutableStateOf<MenuConfiguration?>(null) }
AppTheme {
Scaffold(
bottomBar = {
BottomAppBar(backgroundColor = Color.Red) {
IconButton(onClick = navController::popBackStack) {
Icon(Icons.Filled.ArrowBack, "Back")
}
Spacer(modifier = Modifier.weight(1f))
menuConfiguration?.rightButton?.invoke()
}
}
) { padding ->
Box(modifier = Modifier.padding(padding)) {
PaymentNavHost(
navController = navController,
finishedHandler = finishedHandler,
onRegisterMenuConfiguration = { menuConfiguration = it },
onUnregisterMenuConfiguration = { menuConfiguration = null }
)
}
}
}
}
#Composable
fun PaymentNavHost(
navController: NavHostController = rememberNavController(),
onRegisterMenuConfiguration: (MenuConfiguration?) -> Unit,
onUnregisterMenuConfiguration:() -> Unit
) {
NavHost(
navController = navController,
startDestination = "selectAccount"
) {
composable("selectAccount") {
DemoAccountSelectionRoute(
onAccountSelected = {
navController.navigate("amountInput")
},
onRegisterMenuConfiguration = onRegisterMenuConfiguration,
onUnregisterMenuConfiguration = onUnregisterMenuConfiguration
)
}
composable("amountInput") {
AmountInputRoute(
onRegisterMenuConfiguration = onRegisterMenuConfiguration,
onUnregisterMenuConfiguration = onUnregisterMenuConfiguration,
onFinished = {
...
}
)
}
}
}
Screen level
#Composable
internal fun AmountInputRoute(
onRegisterMenuConfiguration: (MenuConfiguration?) -> Unit,
onUnregisterMenuConfiguration:() -> Unit,
onFinished: (Amount?) -> Unit
) {
SimpleMenuConfiguration(
onRegisterMenuConfiguration = onRegisterMenuConfiguration,
onUnregisterMenuConfiguration = onUnregisterMenuConfiguration,
rightButton = {
Button(
onClick = {
...
}
) {
Text(text = stringResource(id = R.string.next))
}
}
)
When I am using these 2 composables, if I click on the back button, the app is closing, which is expected.
#Composable
fun Greeting(modifier: Modifier = Modifier, name: String) {
val focusRequester by remember { mutableStateOf(FocusRequester()) }
Container(modifier = modifier) {
Column(Modifier.padding(start = 24.dp)) {
Button(onClick = { /*TODO*/ }) {
Text("1")
}
Button(modifier = modifier.focusRequester(focusRequester), onClick = { /*TODO*/ }) {
Text("2")
}
Button(onClick = { /*TODO*/ }) {
Text("3")
}
Button(onClick = { /*TODO*/ }) {
Text("4")
}
}
}
LaunchedEffect(Unit) {
focusRequester.requestFocus()
}
}
#Composable
fun Container(modifier: Modifier = Modifier, content: #Composable () -> Unit) {
val focusManager = LocalFocusManager.current
Box {
Box {
content()
}
}
}
But, if I make a change on the Container composable (I set it as focusable) :
#Composable
fun Container(modifier:Modifier = Modifier, content: #Composable () -> Unit) {
val focusManager = LocalFocusManager.current
Box {
Box(modifier = Modifier.focusable()){
content()
}
}
}
I have to press the back button twice to have the application to exit (on the first click, it removes the focus, and on the second click, it exit the app).
It seems strange to me that the back button is interfering with focus management, but I suppose that once you set your composable focusable, you have to handle the back button manually ?
All I can think of is that when you do not set any Modifier, the default behavior is focusable(false) , that would explain the different behavior in both cases, and make sense in mobile perspective.
In Jetpack Compose, where is ScrollToTopButton coming from? It is mentioned in Google's documentation. Annoyingly, they neglect to mention the package. I have imports of foundation version 1.2.0-alpha08; also tried with 1.2.0-beta02 as well as ui and material (1.1.1). Not found. (yes did do an internet search on the term, came back empty handed).
implementation "androidx.compose.foundation:foundation:${version}"
implementation "androidx.compose.foundation:foundation-layout:${version}"
implementation "androidx.compose.ui:ui:$compose_version"
implementation "androidx.compose.material:material:$compose_version"
#Composable
fun MessageList(messages: List<Message>) {
val listState = rememberLazyListState()
// Remember a CoroutineScope to be able to launch
val coroutineScope = rememberCoroutineScope()
LazyColumn(state = listState) {
// ...
}
ScrollToTopButton(
onClick = {
coroutineScope.launch {
// Animate scroll to the first item
listState.animateScrollToItem(index = 0)
}
}
)
}
Google documentation
Edit: If this is NOT a function they offer, but rather a suggestion to create your own, shame on whoever wrote the documentation, it literally suggests being a function offered by Compose.
Edit 2: Turns out it is a custom function (see the answer). What moved the author of the documentation to write it like this? Why not just put Button? Sigh.
It's not clear from the documentation but you actually have to make your own. For example you can use this:
#Composable
fun ScrollToTopButton(onClick: () -> Unit) {
Box(
Modifier
.fillMaxSize()
.padding(bottom = 50.dp), Alignment.BottomCenter
) {
Button(
onClick = { onClick() }, modifier = Modifier
.shadow(10.dp, shape = CircleShape)
.clip(shape = CircleShape)
.size(65.dp),
colors = ButtonDefaults.buttonColors(
backgroundColor = Color.White,
contentColor = Color.Green
)
) {
Icon(Icons.Filled.KeyboardArrowUp, "arrow up")
}
}
}
And then:
val showButton by remember{
derivedStateOf {
listState.firstVisibleItemIndex > 0
}
}
AnimatedVisibility(
visible = showButton,
enter = fadeIn(),
exit = fadeOut(),
) {
ScrollToTopButton(onClick = {
scope.launch {
listState.animateScrollToItem(0)
}
})
}
I have an issue with screen reader and jetpack compose
Imagine following scenario:
I have 2 Composables, Home and Detail:
#Composable
fun Home(navController: NavController) {
Column(
modifier = Modifier.testTag("hometag")
) {
Text("home1")
Text("home2")
Button(onClick = {
navController.navigate("detail",
)
}) {
Text("NEXT")
}
}
}
#Composable
fun Detail(navController: NavController) {
Column(
modifier = Modifier
.testTag("detailTag")
) {
Text("detail1")
Text("detail2")
Text("detail3")
}
}
The button in Home() just navigates to the Detail composable via compose navigation.
When I use the app via Talkback and will click on the next button - the Detail screen gets opened. But instead of the text "detail1" gets focussed, "detail3" is in the next focus for talkback.
When I remove both Modifier.testTag modifier, the talkback order is correct.
Anything I'm missing here?
UPDATED:
When switiching compose to 1.2.0_beta02 the issue with this example is gone. But when wrapping Text("detail3") also in an Button like this:
#Composable
fun Home(navController: NavController) {
Column(
modifier = Modifier
.testTag("hometag")
) {
Text("home1")
Text("home2")
Button(onClick = {
navController.navigate(
"detail"
)
}) {
Text("NEXT")
}
}
}
#Composable
fun Detail() {
Column(
modifier = Modifier
.testTag("detailTag")
) {
Text("detail1")
Text("detail2")
Button(onClick = { /*TODO*/ }) {
Text("detail3")
}
}
}
then the issue is still present.
Already filed an issue in google issue tracker:
https://issuetracker.google.com/issues/233251832
The Code A is from the offical sample project here.
The Code B is from Android Studio source code.
I have searched the article about the function key by Google, but I can't find more details about it.
How can Android Studio launch the inline fun <T> key()? Why can't the author use Code C to launch directly?
Code A
key(detailPost.id) {
LazyColumn(
state = detailLazyListState,
contentPadding = contentPadding,
modifier = Modifier
.padding(horizontal = 16.dp)
.fillMaxSize()
.notifyInput {
onInteractWithDetail(detailPost.id)
}
) {
stickyHeader {
val context = LocalContext.current
PostTopBar(
isFavorite = hasPostsUiState.favorites.contains(detailPost.id),
onToggleFavorite = { onToggleFavorite(detailPost.id) },
onSharePost = { sharePost(detailPost, context) },
modifier = Modifier.fillMaxWidth().wrapContentWidth(Alignment.End)
)
}
postContentItems(detailPost)
}
}
Code B
#Composable
inline fun <T> key(
#Suppress("UNUSED_PARAMETER")
vararg keys: Any?,
block: #Composable () -> T
) = block()
Code C
LazyColumn(
state = detailLazyListState,
contentPadding = contentPadding,
modifier = Modifier
.padding(horizontal = 16.dp)
.fillMaxSize()
.notifyInput {
onInteractWithDetail(detailPost.id)
}
) {
stickyHeader {
val context = LocalContext.current
PostTopBar(
isFavorite = hasPostsUiState.favorites.contains(detailPost.id),
onToggleFavorite = { onToggleFavorite(detailPost.id) },
onSharePost = { sharePost(detailPost, context) },
modifier = Modifier.fillMaxWidth().wrapContentWidth(Alignment.End)
)
}
postContentItems(detailPost)
}
From key documentation:
key is a utility composable that is used to "group" or "key" a block of execution inside of a composition. This is sometimes needed for correctness inside of control-flow that may cause a given composable invocation to execute more than once during composition.
It also contains several examples, so check it out.
Here is a basic example of the usefulness of it. Suppose you have the following Composable. I added DisposableEffect to track its lifecycle.
#Composable
fun SomeComposable(text: String) {
DisposableEffect(text) {
println("appear $text")
onDispose {
println("onDispose $text")
}
}
Text(text)
}
And here's usage:
val items = remember { List(10) { it } }
var offset by remember {
mutableStateOf(0)
}
Button(onClick = {
println("click")
offset += 1
}) {
}
Column {
items.subList(offset, offset + 3).forEach { item ->
key(item) {
SomeComposable(item.toString())
}
}
}
I only display two list items, and move the window each time the button is clicked.
Without key, each click will remove all previous views and create new ones.
But with key(item), only the disappeared item disappears, and the items that are still on the screen are reused without recomposition.
Here are the logs:
appear 0
appear 1
appear 2
click
onDispose 0
appear 3
click
onDispose 1
appear 4
click
onDispose 2
appear 5