The CommonsBlog


Hey, Where Did My Artifacts Go?

We are getting a lot of questions like this one, of the form:

My builds are now failing because some of my dependencies cannot be found. Where did they go?

While there can be a few causes for this, the one that is tripping up a lot of developers now stems from the fact that JCenter’s Maven repository, used by many library publishers, no longer exists.

For actively-published libraries, the library developers would have moved elsewhere by now. For many developers, the best answer would be Maven Central, though some have other options. Google, for example, has the latest version of Volley available through their own Maven repository.

However, there are a lot of not-actively-published libraries. Those are probably gone for good.

The reason the JCenter change is not affecting everyone at the same time comes down to Gradle caches. Gradle caches artifacts and only tries downloading them when needed, such as:

  • You add a new library dependency
  • You change the version of a dependency that you want to use
  • Your Gradle artifact cache gets cleared
  • You try building on a new machine than where you had been before

Those latter two probably are the ones that cause the most JCenter-related grief. Some build that worked before and perhaps works elsewhere (e.g., on a coworker’s machine) does not work for you, because “elsewhere” has the artifact cached and you do not. Teams using CI servers that retrieve artifacts on every build might have found out within a day of the JCenter shutdown. Those using just local build machines might not find out for years, depending on how long they keep their Gradle artifact cache around.


So, what do you do if you get caught by this?

First, search “teh interwebs” for the library to see if there is a site regarding it. Many of these libraries were developed in the open in places like GitHub. See if that site has instructions for some newer maintained version, and migrate to it. Conversely, if the library has not been update in years, please consider moving to something that is actively maintained.

You could also search MvnRepository. This is an index of several different artifact repositories, including Maven Central and Google’s Maven repository. It used to index JCenter, and it also has some other less-common Maven repositories. Perhaps you will find another source for the particular artifact that you need. However, be careful when depending on a semi-random library from an even more random repository — you are asking to become the victim of a supply-chain attack.

If the source is available on GitHub or elsewhere, you could fork the source and maintain your own copy. Whether you make that available to the public (e.g., on Maven Central) or just share it with your team is up to you. Maven Local is a quick-and-dirty way to have a Maven repository on your machine for artifacts. Setting up a private shared repository, such as via Amazon S3, is eminently doable, if slightly arcane.

Going forward, aim to do a clean build on a clean environment periodically. Even if you do not elect to go for a nightly CI server job, do a build once a month on an environment that lacks a Gradle cache. Part of the value in those builds is to more rapidly identify problems like this, so you can take steps before the problem starts affecting the productivity of individual developers.

Oct 16, 2024


android.tech Shutdown

Five years ago, I rolled out AndroidX Tech (androidx.tech). This site contained an index of all the androidx artifacts from maven.google.com, including copies of the source code for every version of every artifact.

I have shut it down.

I created it back when CommonsWare was still an operating business, rather than “a hobby with a logo”. I created it back when Google had very little to offer about the available artifacts and what they contained. And I created it back when all the AndroidX artifacts were written in Java and were targeting Android.

All those things have changed. Google still could do a better job of making the source code for a specific artifact version available — you have to download and unpack a JAR. But the code that I wrote to generate that site was failing more and more, not handling Kotlin source correctly and struggling with Kotlin Multiplatform libraries. If CommonsWare were still in business, perhaps I would invest in trying to address those problems, but there are other things that I would like to do with my time — you’ll see some of that shortly.

For those of you who used AndroidX Tech, I apologize for any problems this shutdown causes you.

Oct 12, 2024


How to Ripple Outside of Compose Material

There is more to life than Material Design.

After all, it is a common complaint, at least here in the US, that designers design around iOS. Last I checked, iOS designs are not very Material. So, it stands to reason that Android developers using Compose will need to deviate from Material Design in many cases.

In theory, developers would create custom design systems from the ground up. In practice, I suspect that most developers use Compose Material or Material3 and try to implement the designs that way. This can work, but sometimes developers wind up having to resort to some fairly significant hacks in order to work around cases where Material’s opinions deviate from the designers’ opinions.

I am working on a project where I am creating a from-scratch custom design system. Along the way, I will try to point out how I am filling in various Compose gaps, where Material provides a solution but “you’re on your own” for a custom design system.


This time, let’s look at indications. Indications are how a clickable composable lets the user know that, indeed, they clicked the composable. Material Design calls for a ripple effect. Compose Material offers the ripple.

Your graphic designer probably is not designing indications for you, but you are going to need some sort of touch feedback. The ripple is a perfectly cromulent option, but the ripple is a Compose Material thing and will not be supplied “out of the box” for non-Material design systems.

The good news is that a lot of the ripple logic resides in a standalone library, one without other Material dependencies. This makes it reasonable for use in a non-Material Compose app. The bad news is that the documentation on how to actually apply that library is limited.

So, here is how you can do it.

First, you will need to add that library. That is androidx.compose.material:material-ripple, and the version I wrote this blog post around is 1.7.2:

composeRipple = "1.7.2"
compose-ripple = { group = "androidx.compose.material", name = "material-ripple", version.ref = "composeRipple" }
dependencies {
    implementation(libs.compose.ripple)
    // TODO other cool libraries go here
}

The documentation tells you “oh, do what Material3 does to provide the ripple”. That is contained mostly in Ripple.kt. So, copy its contents into your own project (be sure to abide by the license!). This will require you to also grab these StateTokens defined elsewhere:

internal object StateTokens {
    const val DraggedStateLayerOpacity = 0.16f
    const val FocusStateLayerOpacity = 0.1f
    const val HoverStateLayerOpacity = 0.08f
    const val PressedStateLayerOpacity = 0.1f
}

For the file version I used, IIRC there is only one real connection to the rest of Material3 in the file: a reference to currentValueOf(LocalContentColor). This is only needed if you do not supply a color directly when creating the ripple or via a LocalRippleConfiguration composition local. In my case, I was perfectly happy with two options for providing a color and did not need a third, so I swapped currentValueOf(LocalContentColor) with a RuntimeException:

    private fun attachNewRipple() {
        val calculateColor = ColorProducer {
            val userDefinedColor = color()
            if (userDefinedColor.isSpecified) {
                userDefinedColor
            } else {
                // If this is null, the ripple will be removed, so this should always be non-null in
                // normal use
                val rippleConfiguration = currentValueOf(LocalRippleConfiguration)
                if (rippleConfiguration?.color?.isSpecified == true) {
                    rippleConfiguration.color
                } else {
                    // currentValueOf(LocalContentColor)
                    throw RuntimeException("missing color for ripple")
                }
            }
        }

That should be all the changes that are needed… at least for the version of Ripple.kt that I used. It is possible that I forgot something, in which case I apologize.

Then, to actually apply the ripple, set the LocalIndication composition local to a ripple() that you create from the Ripple.kt code that you copied and revised:

CompositionLocalProvider(LocalIndication provides ripple(color = Color.White)) {
	// TODO cool stuff goes here
}

If you want to be able to change the ripple color without replacing the entire ripple, rather than provide the color to ripple(), you could leave that parameter out and also define a LocalRippleConfiguration:

CompositionLocalProvider(
	LocalIndication provides ripple(),
	LocalRippleConfiguration provides RippleConfiguration(color = Color.White)
) {
	// TODO cool stuff goes here
}

RippleConfiguration also lets you control the alpha values used in the ripple effect, overriding the defaults coming from StateTokens.

No matter how you provide the color, you will need to do that separately if you need different colors for different themes, such as light and dark.

If you are using ComposeTheme for your custom design system, there is an indication property that you can set in your buildComposeTheme() builder:

private val AwesomeDarkTheme = buildComposeTheme {
    name = "AwesomeDarkTheme"
    indication = ripple(color = Color.White)

    // TODO define the rest of the theme, which hopefully also is cool
}

ComposeTheme will take care of setting LocalIndication for you.

Once LocalIndication is set, clickable() and other modifiers and code will apply it automatically, so your ripple should show up. Alternatively, you can provide a ripple() directly as the indication to clickable(), perhaps for cases where you want a different implementation than the one from LocalIndication.

If you want an indication, but you want to do something other than a ripple… look at the implementation of ripple(), especially the RippleNodeFactory and DelegatingThemeAwareRippleNode, which eventually link into code from that material-ripple library mentioned earlier.

Sep 25, 2024


When remember() Does Not Remember, Consider if()

One of my concerns when Jetpack Compose was released is its reliance on magic coming from things like the Compose Compiler. Magic is wonderful for newcomers, as it reduces cognitive load. Magic is fine for serious experts (magicians), as for them it is not magic, but rather is sufficiently advanced technology. Magic can be a problem for those of us in between those extremes, to the extent it makes it difficult for us to understand subtle behavior differences coming from small code changes.

For example, I have been using Alex Styl’s ComposeTheme recently, to help organize a non-Material design system in Compose UI. The way that you build a theme with ComposeTheme is via a buildComposeTheme() top-level function:

val MyTheme = buildComposeTheme { 
  // TODO wonderful theme bits go here
}

This returns a composable function, which you can apply akin to MaterialTheme():

@Composable
fun MainScreen() {
    MyTheme {
        BasicText("Um, hi!")
    }
}

This works well.

I then added support for light and dark themes. Alex’s documentation shows doing that outside of the constructed theme function:


@Composable
fun MainScreen() {
    val MyTheme = if (isSystemInDarkTheme()) MyDarkTheme else MyLightTheme

    MyTheme {
        // use the theme, where color references get mapped to light or dark
    }
}

Here, MyDarkTheme() and MyLightTheme() are created using buildComposeTheme(), just with different colors. We choose which one to use, then apply it to our content.

I wanted to hide the decision-making, so I didn’t need it sprinkled throughout the code (e.g., @Preview functions). So, I wrote my own wrapper:

@Composable
fun MyTheme(content: @Composable () -> Unit) {
    if (isSystemInDarkTheme()) MyDarkTheme(content) else MyLightTheme(content)
}

This could be called like MyTheme() was before, routing to MyDarkTheme() or MyLightTheme() as needed.

And it worked… or so I thought.

The app opts out of all automatic configuration change “destroy the activity” behavior via android:configChanges. What happens is that Compose UI recomposes, and we update the UI based on the new Configuration, not significantly different than updating the UI based on the result of some other sort of data change.

What I noticed was that while the app worked, if I changed the theme while the app was running, everything would reset to the beginning. So, if I did some stuff in the app (e.g., navigated in bottom nav), then used the notification shade tile to turn on/off dark mode, the app would draw the right theme, but my changes would be undone (e.g., I would be back at the default bottom nav location).

Eventually, after some debugging, I discovered that remember() seemed to stop working. 😮

@Composable
fun MainScreen() {
    MyTheme {
        val uuid = remember { UUID.randomUuid() }

        BasicText("Um, hi! My name is: $uuid")
    }
}

Here, I remember a generated UUID. That should survive recomposition. For most things, it would – I could rotate the screen without issue. But if I changed theme, I would get a fresh UUID.

🧐

Much debugging later, I realized the problem.

Let’s go back to the MyTheme() implementation:

@Composable
fun MyTheme(content: @Composable () -> Unit) {
    if (isSystemInDarkTheme()) MyDarkTheme(content) else MyLightTheme(content)
}

When I toggle dark mode, my use of isSystemInDarkTheme() triggers a recomposition. Let’s suppose that isSystemInDarkTheme() originally returned false, then later returns true on the recomposition. The false meant that my original composition of MyTheme() went down the MyLightTheme() branch. The later recomposition takes me down the MyDarkTheme() branch. Compose treats those as separate compositions. MyTheme() is recomposing, but it is doing so by discarding the MyLightTheme() composition and creating a new MyDarkTheme() composition. It does not matter whether content would generate the same composition nodes or not — the change in the root from MyLightTheme() to MyDarkTheme() causes the swap in compositions.

My uuid is in the content lambda expression. When we dispose of the MyLightTheme() composition and switch to the MyDarkTheme() composition, we start over with respect to the remember() call, and I wind up with a fresh random UUID.

One workaround is to “lift the if”, blending Alex’s original approach with mine:

@Composable
fun MyTheme(content: @Composable () -> Unit) {
    val theme = if (isSystemInDarkTheme()) MyDarkTheme else MyLightTheme

    theme(content)
}

This does the same thing, but Compose treats this as a single changed composition, and the remember() is retained. To be honest, I am not completely clear why this workaround works. This is still magic to me, though I am certain that there are others for whom the reasoning is clear.

This is the sort of thing that we have to watch out for when working in Compose. Compose is a principled framework, but the Principle of Least Surprise is not always followed… at least for those among us who are not magicians.

Sep 13, 2024


Requiem for a Ranch

While elements will still remain, to an extent it appears that Big Nerd Ranch is locking up the corral for good.

A long long time ago, I was their original Android app development trainer. Working on a contract basis, I delivered training in Historic Banning Mills (“come for the training, stay for the zipline!”) on many occasions. That included leading some nature walks during breaks, helping folks figure out how to get cell service (tip: walk up a steep hill to get out of the valley), and wrangling the occasional piece of luggage. Banning Mills was an unexpectedly delightful spot to deliver training, one of the most entertaining locations that I ever used.

Eventually, Big Nerd Ranch concluded that this Android thing might pan out, so they hired dedicated staff and I moved on to deliver training through a series of other firms. Over time, they wrote their own set of books, and you’ll probably recognize a few of the authors for their contributions to the Android development community.

I remain grateful to Aaron Hillegass for taking a chance on me and giving me the opportunity to deliver Big Nerd Ranch training. That played a significant role in my overall success with CommonsWare. I even wound up doing some more contract work for them in 2018-19, albeit not in the form of delivering training.

As their announcement post puts it, “The landscape of tech education has evolved significantly since our inception”. That’s putting it mildly. Discontinuing their public bootcamps and their series of books is not surprising — after all, I did much the same thing. Still, it’s tough to see and makes for a somber end to the day.

That said… maybe I’ll pick up a Stetson and put a propeller on the top, for old times’ sake. 🤠

Jun 11, 2024


Older Posts