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
:
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