The CommonsBlog


Compose for Wear: CurvedRow() and CurvedText()

Compose UI is not just for phones, tablets, foldables, notebooks, and desktops. Compose UI is for watches as well, via the Compose for Wear set of libraries.

(Google calls it “Wear Compose” on that page, but that just makes me think “Wear Compose? There! There Compose!”).

(and, yes, I’m old)

Compose for Wear has a bunch of composables designed for the watch experience. In particular, Compose for Wear has support for having content curve to match the edges of a round Wear OS device.

The Compose for Wear edition of Scaffold() has a timeText parameter. This is a slot API, taking a composable as a value, where typically you will see that composable delegate purely to TimeText(). That gives you the current time across the top of the watch screen, including curving that time on round screens:

TimeText() on a Round Watch

The implementation of TimeText() uses CurvedRow() and CurvedText() to accomplish this, if the code is running on a round device. Otherwise, it uses the normal Row() and Text() composables,

TimeText() is a bit overblown, particularly for a blog post, so this sample project has a SimpleTimeText() composable with a subset of the functionality:

@ExperimentalWearMaterialApi
@Composable
fun SimpleTimeText(
    modifier: Modifier = Modifier,
    timeSource: TimeSource = TimeTextDefaults.timeSource(TimeTextDefaults.timeFormat()),
    timeTextStyle: TextStyle = TimeTextDefaults.timeTextStyle(),
    contentPadding: PaddingValues = PaddingValues(4.dp)
) {
    val timeText = timeSource.currentTime

    if (LocalConfiguration.current.isScreenRound) {
        CurvedRow(modifier.padding(contentPadding)) {
            CurvedText(
                text = timeText,
                style = CurvedTextStyle(timeTextStyle)
            )
        }
    } else {
        Row(
            modifier = modifier
                .fillMaxSize()
                .padding(contentPadding),
            verticalAlignment = Alignment.Top,
            horizontalArrangement = Arrangement.Center
        ) {
            Text(
                text = timeText,
                style = timeTextStyle,
            )
        }
    }
}

We can determine whether or not the screen is round from the isScreenRound property on the Configuration, which we get via LocalConfiguration.current. If the screen is round, we display the current time in a CurvedText() and wrap that in a CurvedRow(). CurvedText() knows how to have the letters of the text follow the curve of the screen, and CurvedRow() knows how to have child composables follow the curve of the screen.

The timeText slot parameter in Scaffold() puts the time at the top of the screen by default. That position is controlled by the anchor parameter to CurvedRow(), where the default anchor is 270f. anchor is measured in degrees, and 270f is the value for the top of the screen (probably for historical reasons).

SampleRow() in that sample project lets us display multiple separate strings via individual CurvedText() composables, in a CurvedRow() with a custom anchor value:

@Composable
private fun SampleRow(anchor: Float, modifier: Modifier, vararg textBits: String) {
    CurvedRow(
        modifier = modifier.padding(4.dp),
        anchor = anchor
    ) {
        textBits.forEach { CurvedText(it, modifier = Modifier.padding(end = 8.dp)) }
    }
}

SampleRow() accepts a Modifier and tailors it to add a bit of padding to the CurvedRow().

We can then use SampleRow() to display text in other positions on the screen:

@ExperimentalWearMaterialApi
@Composable
fun MainScreen() {
    Scaffold(
        timeText = {
            SimpleTimeText()
        },
        content = {
            Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
                Text(text = "Hello, world!")

                SampleRow(anchor = 180f, modifier = Modifier.align(Alignment.CenterStart), "one", "two", "three")
                SampleRow(anchor = 0f, modifier = Modifier.align(Alignment.CenterEnd), "uno", "dos", "tres")
                SampleRow(anchor = 90f, modifier = Modifier.align(Alignment.BottomCenter), "eins", "zwei", "drei")
            }
        }
    )
}

An anchor of 0f is the end edge of the screen, 90f is the bottom, and 180f is the start edge. Note that we also use align() to control the positioning within the Box(), with values that line up with our chosen anchor values.

The result is that we have text on all four edges, plus a centered “Hello, world!”:

Sample Compose UI on a Round Watch

CurvedRow() does not handle consecutive bits of CurvedText() all that well — ideally, use a single CurvedText() with the combined text. However, CurvedRow() is not limited to CurvedText(), and I hope to explore that more in a future blog post.

Jan 17, 2022


Tiramusu Thoughts

Leaks about what will be coming up Android 13 / Android T / Tiramusu are making the rounds, in places like XDA Developers and Android Police. Some of what is discussed will have little impact on developers. Other things will be your typical “double-edged sword” of opportunity and pain.

So, let’s slice some tiramisu with a sword.

Notification Permission

It appears as though being able to raise notifications will require a runtime permission. XDA has screenshots showing “Notifications” as a permission alongside other standard runtime permissions like “Camera” and “Contacts”. This suggests that there will be a new dangerous permission for notifications.

However, “notifications” is a rather broad bucket. App developers are going to have to do a fair bit of work to educate users about how the app uses notifications before presenting the permission. Perhaps that education process alone will help to get firms to cut back on the number of superfluous notifications that are presented.

My biggest concern here, though, is what happens when the permission is declined by the user. Typically, with these permissions, that triggers a SecurityException when you attempt to use an API tied to the permission. So, in this case, perhaps notify() on NotificationManager would throw a SecurityException.

My sincere hope is that either this does not happen or it is something we can opt out of (e.g., via StrictMode). Ideally, this is a quiet failure, logging messages to Logcat but not crashing the app.

Google went out of their way, for a better part of a decade, to shove notifications down the throats of developers. With pretty much all the other dangerous permissions, Google simply made APIs available, then restricted them later. In the case of notifications, though, Google proactively took steps to try to convince developers to rely upon notifications. Saying that “OK, now what we told you to do is at risk of crashing your app” is just plain rude.

We also have to deal with specific notification scenarios. For example, does this permission imply that foreground services are impossible if users decline the permission? What happens if libraries raise notifications? And so on.

Of all the proposed changes, this one scares me the most, just in terms of how Google might go about implementing it and the impacts it can have on developers.

TARE: The Android Resource Economy

XDA also talks about TARE: The Android Resource Economy.

The idea that users might have some way of offering fine-grained advice on what they want to allow apps to do in the background is interesting. The UI shown in that XDA article is dreadful (WTAF is a “satiated balance”?), but the concept has some merit.

However, each year Google goes in and changes the rules as part of The War on Background Processing that has been going on for 6+ years. Combine that with manufacturer-specific changes and developers are completely lost as to what we can and cannot do on any given device. That in turn leads users to assume that apps or devices are broken, just because developers cannot keep apace with documented and undocumented rules.

IOW, it would be really rather nice if Google stuck with a plan for more than a year and took steps (e.g., CDD rules) to get manufacturers to stick with that same plan.

Per-App Locale Settings

For a long time, developers have resorted to hacks to allow a single app to support multiple languages, by messing with Locale. While this appears to have held up better than I would have expected over the years, there are still serious gaps. The biggest is any UI that comes from other processes, such as notification dialogs — those processes will not have the customized Locale and will display their contents in the default language specified for the device as a whole.

Through a “panlingual” feature, Android 13 might allow users to choose a locale per app via Settings.

On the one hand, this seems wonderful.

On the other hand…

  • Where does the language change end? It will be interesting to see how they distinguish “showing the system file UI via ACTION_OPEN_DOCUMENT” and “launching Snapchat”. The former, in theory, ought to follow the language chosen for the app that makes the request; the latter ought to follow the language of the app that is started. Yet, in both cases, the requesting app is just calling startActivity() or startActivityForResult().

  • Will Google provide Compat code that will combine the new Android 13 capability with Google-supported forms of Locale switching for older devices? If yes, how will they handle manufacturers that fail to support the Intent for allowing users to switch a language? If no, how will Google advise developers on supporting both the new approach and the old hacks in the same app?


These are early leaks. The things shown in these leaks may not ship, or they may ship in substantially different form. And Android 13 is likely to have many more new features than these, including some that impact developers. With luck, all my worries will vanish in the breeze and it will turn out that everything is awesome.

I’m a Murphy, though, so I’m not counting on that.

Jan 15, 2022


Final Books, Free for Everyone

As I announced three months ago, the Warescription program has ended. The CommonsWare site now has the full catalog of books available for download. The books are published under the the Creative Commons Attribution-ShareAlike 4.0 International license. PDFs, EPUBs, and MOBI/Kindle editions are available for all of the books, and many of the newer ones are also available for direct reading on the site. If you see any problem with the content, let me know!

I will be adding full-text search in the coming weeks — it was simpler to get the content up first and add search in a second pass. Also, I will be looking to release the second-generation books via Amazon and other distribution channels in the near future, and I will add links to those when they become available.

The Warescription site will remain up for another week, mostly for the final office hours chats. I will then have it simply redirect to the main CommonsWare site.

To everyone who subscribed, I am grateful for your support, and I can only hope that I was able to help you in some small way.

As for what comes next… I own the codetransparency.org domain, and I probably should start working on getting some content up for that. 😅 Reach out if you have been working in this space and have some stuff that I should link to!

Dec 24, 2021


"Elements of Android Room" Version 0.9 Released

Subscribers now have access to Version 0.9 of Elements of Android Room, in PDF, EPUB, and MOBI/Kindle formats. Just log into your Warescription page to download it, or set up an account and subscribe!


This update adds two more chapters, covering:

In addition:

  • A bunch of dependencies were updated, notably Room itself

  • Material tied to Android Studio is updated for Arctic Fox

  • Uses of startActivityForResult() were replaced with ActivityResultContracts

  • Various bugs were fixed

Nov 07, 2021


About the Environment Undeprecations

A week and a half ago, in my random musings on Android 12L, I wrote:

getExternalStorageDirectory() and getExternalStoragePublicDirectory() on Environment are now undeprecated. DATA in MediaStore.MediaColumns is also undeprecated.

I don’t know what to make of this.

A Google engineer reached out to clarify what is going on.

Good news! These changes were not an accident! getExternalStorageDirectory() and getExternalStoragePublicDirectory() on Environment are safe for use on all supported API levels. 🎉

The fact that they are undeprecated means that you can rely on them to return the same sorts of directories that they always have. However, this does not change your access rights. Scoped storage is still a thing, and what you can read from and write to is still governed by the scoped storage rules.

(someday, I will sit down and try to write the definitive guide for what those rules are)

So, for example, just because you can now access DIRECTORY_DOWNLOADS via getExternalStoragePublicDirectory() does not change the rules for that directory:

  • You can write content there

  • You can read back the content that you wrote there

  • You cannot read or write content created by other apps there, which includes prior installations of your own app (after the user uninstalled and reinstalled your app)

So, this undeprecation does not have a security impact. It just means that you will stop getting compiler Lint complaints about using a deprecated API, as soon as you start using compileSdkVersion 32, perhaps in 2022.

I am grateful for this change. Once Android 11 restored read access to much of external storage, the deprecations were an annoyance. This will help to simplify the discussion around how to work with external storage on modern versions of Android, because that is a major pain point for newer Android app developers.

To the Google engineers who decided to make this change: thanks!

Nov 06, 2021


Older Posts