Blog Infos
Author
Published
Topics
Published

When you are creating an app for Wear OS it is a good idea to make sure all user interaction is easy and makes sense for a small screen. There are unavoidable situations of course where you do need to get more information from the user than a simple button press, slider number or toggle set.

If you are asking for input from the user on the Wear OS device you can direct them to use a companion phone app but if this is not possible you can ask for voice, text or emoji input directly from the watch using the inbuilt IME tools.

Display of three input methods for Wear OS, voice, keyboard and emoji

Wear input methods, voice to text, keyboard & emoji. (Source: https://developer.android.google)

The official documentation is mainly focussed on how to create custom IME methods so I thought I would show an example on how the standard input methods can be implemented using the RemoteInput API

You may initially think that you can just use an EditText, but unfortunately if you are using Jetpack Compose on Wear OS (as is the official recommendation) EditText is not (yet?) available for Jetpack Compose Wear OS. Instead, we need to launch the input methods via a button. Using a button to launch a full screen input interface has the added benefit of giving the user much more room to type or make their selections and providing a more consistent user experience on such a tiny screen.

Requesting User Input for Wear OS

In order to launch the RemoteInput behaviour we first need to include the Wear Input dependency:

implementation("androidx.wear:wear-input:1.2.0-alpha02")

Then, we need to create a few variables to store our default text, to store received text and a key to identify the input request when it is returned from the remote input method:

// Default text to be displayed before text is entered
val defaultText = stringResource(R.string.edit_user_input)
// A variable to store the received text in
var userInput by remember { mutableStateOf(defaultText) }
// A key to identify the text we are receiving when coming back from the remote input
val inputTextKey = "input_text"
view raw UserInput.kt hosted with ❤ by GitHub

The next step is to set up what input methods are allowed using the RemoteInput.Builder:

val remoteInputs: List<RemoteInput> = listOf(
RemoteInput.Builder(inputTextKey)
.setLabel(stringResource(id = R.string.edit_user_input))
.wearableExtender {
setEmojisAllowed(true)
setInputActionType(EditorInfo.IME_ACTION_DONE)
}.build()
)
view raw UserInput.kt hosted with ❤ by GitHub

Here, we first pass in the resultKey (named inputTextKey in my example above) to identify the result of the input. Then, we can set the label that will be displayed when the input is requested using setLabel (“Enter your input” in my screenshot below).

Then the wearableExtender extension method can be used to indicate whether the emoji input method is allowed (voice and keyboard are always allowed) and setInputActionType to set the IME action label (this could also be IME_ACTION_SEARCH , IME_ACTION_SEND etc.)

Finally we call build() to build the result.

 

Displaying the user choice of which input type to use

Providing input type choice to the user

 

Here the user can select how they want to enter their input, the next step for us is to capture it.

To do this, we need to handle the result using a remembered ManagedActivityResultLauncher and StartActivityForResult.

val launcher = rememberLauncherForActivityResult(
ActivityResultContracts.StartActivityForResult()
) {
it.data?.let { data ->
// Get all the results
val results: Bundle = RemoteInput.getResultsFromIntent(data)
// Use the inputTextKey to select the input we are interested in
val newInputText: CharSequence? = results.getCharSequence(inputTextKey)
// Save the text to our variable as a string. Ensure to handle the null case
userInput = newInputText?.toString() ?: ""
}
}
view raw UserInput.kt hosted with ❤ by GitHub

In the onResult lambda for rememberLauncherForActivityResult we get the input results and fetch the specific one we are interested in using the result key we specified in the RemoteInput.Builder. Be careful to handle the null case as there is chance that the result could be null if the request is interrupted. You could also trim or do other processing on the text here before saving it to the previously defined variable.

Now we have set up the interface and handled the result we need to be able to use this launcher to start the input request!

To do this we create an Intent using RemoteInputIntentHelper and launch this intent on click of a button:

// Create the intent to launch the remote input
val intent: Intent = RemoteInputIntentHelper.createActionRemoteInputIntent()
RemoteInputIntentHelper.putRemoteInputsExtra(intent, remoteInputs)
CompactButton(
onClick = {
// Use the launcher to launch the intent on click of a button
launcher.launch(intent)
},
) {
...
}
view raw UserInput.kt hosted with ❤ by GitHub

To the intent we add our remote input configuration defined earlier and use the ManagedActivityResultLauncher to launch this intent.

This is now done! We can request the input from the user and save it to a variable for use elsewhere in our Wear OS application.

Gif of the user entering a single line of input using both keyboard and emojis

Hello user input!

Job Offers

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

, ,

Migrating to Jetpack Compose – an interop love story

Most of you are familiar with Jetpack Compose and its benefits. If you’re able to start anew and create a Compose-only app, you’re on the right track. But this talk might not be for you…
Watch Video

Migrating to Jetpack Compose - an interop love story

Simona Milanovic
Android DevRel Engineer for Jetpack Compose
Google

Migrating to Jetpack Compose - an interop love story

Simona Milanovic
Android DevRel Engin ...
Google

Migrating to Jetpack Compose - an interop love story

Simona Milanovic
Android DevRel Engineer f ...
Google

Jobs

The full code is as follows:

@Composable
fun UserInputScreen(
modifier: Modifier = Modifier
) {
val defaultText = stringResource(id = R.string.edit_user_input)
var userInput by remember { mutableStateOf(defaultText) }
val inputTextKey = "input_text"
val remoteInputs: List<RemoteInput> = listOf(
RemoteInput.Builder(inputTextKey)
.setLabel(stringResource(id = R.string.edit_user_input))
.wearableExtender {
setEmojisAllowed(true)
setInputActionType(EditorInfo.IME_ACTION_DONE)
}.build(),
)
val launcher = rememberLauncherForActivityResult(
ActivityResultContracts.StartActivityForResult()
) {
it.data?.let { data ->
val results: Bundle = RemoteInput.getResultsFromIntent(data)
val newInputText: CharSequence? = results.getCharSequence(inputTextKey)
userInput = newInputText?.toString() ?: ""
}
}
val intent: Intent = RemoteInputIntentHelper.createActionRemoteInputIntent()
RemoteInputIntentHelper.putRemoteInputsExtra(intent, remoteInputs)
Box(
contentAlignment = Alignment.Center,
modifier = modifier.fillMaxSize()
) {
Row(modifier = Modifier.fillMaxWidth(0.75f)) {
Text(text = userInput, Modifier.weight(1f))
CompactButton(
onClick = { launcher.launch(intent) },
) {
Icon(
imageVector = Icons.Filled.Edit,
contentDescription = stringResource(id = R.string.edit_user_input)
)
}
}
}
}
view raw UserInput.kt hosted with ❤ by GitHub
Chaining User Input

The eagle eyed of you may have noticed when we pass the RemoteInput into the RemoteInputIntentHelper it is done using a list. This may initially seem strange but instead it allows a better user experience when multiple input items are requested from the user. If you do require the user to fill out a form (not really recommended to be done on a watch!!) rather than the user having to click a new button for each form field you can request them one after another.

To set this up, provide each RemoteInput in the list with different resultKey values and labels:

val remoteInputs: List<RemoteInput> = listOf(
RemoteInput.Builder(inputTextKey1)
.setLabel(stringResource(R.string.edit_user_input_1))
.wearableExtender {
setEmojisAllowed(true)
setInputActionType(EditorInfo.IME_ACTION_NEXT)
}.build(),
RemoteInput.Builder(inputTextKey2)
.setLabel(stringResource(R.string.edit_user_input_2))
.wearableExtender {
setEmojisAllowed(true)
setInputActionType(EditorInfo.IME_ACTION_NEXT)
}.build(),
RemoteInput.Builder(inputTextKey3)
.setLabel(stringResource(R.string.edit_user_input_2))
.wearableExtender {
setEmojisAllowed(true)
setInputActionType(EditorInfo.IME_ACTION_DONE)
}.build(),
)

And then when processing the result you can fetch each input item separately using the associated key and join them together or construct object as needed:

val launcher = rememberLauncherForActivityResult(
ActivityResultContracts.StartActivityForResult()
) {
it.data?.let { data ->
val results: Bundle = RemoteInput.getResultsFromIntent(data)
val newInputText1: CharSequence = results.getCharSequence(inputTextKey1) ?: ""
val newInputText2: CharSequence = results.getCharSequence(inputTextKey2) ?: ""
val newInputText3: CharSequence = results.getCharSequence(inputTextKey3) ?: ""
userInput = "$newInputText1 $newInputText2 and $newInputText3"
}
}

Here I am just joining my text inputs to a string and saving it. To launch, it is just the same as for a single input.

 

Gif of the user entering multiple items of text input using the keyboard

A much better user experience than requesting with button presses one by one

 

You can find a full working example of requesting user input on Wear OS on github here:

This article was previously published on proandroiddev.com

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
It’s one of the common UX across apps to provide swipe to dismiss so…
READ MORE
blog
In this part of our series on introducing Jetpack Compose into an existing project,…
READ MORE
blog
This is the second article in an article series that will discuss the dependency…
READ MORE
blog
Let’s suppose that for some reason we are interested in doing some tests with…
READ MORE

Leave a Reply

Your email address will not be published. Required fields are marked *

Fill out this field
Fill out this field
Please enter a valid email address.

Menu