Catching Up on CatchUp: 2023

CatchUp turned 7 recently. I started the project in the spring of 2016 and open-sourced it a little over a year later. Its development has ebbed and flowed over the years, but it still serves its primary purpose well: a playground.

In its lifetime, I've used it to learn or tinker with different tools, architectures, languages, and more. In this post, I want to give a little update on what I've been doing with it recently, and ideas of where I want to go with it.

State of the Project

CatchUp in 2023 is a Kotlin Android app written fully in Circuit and Compose. It uses a number of tools and libraries under the hood including Coroutines, Datastore, Material3 (M3), Apollo, EitherNet/Retrofit/OkHttp, SqlDelight, Dagger+Anvil, and AndroidX Paging.

Some of these have been in CatchUp for years, but many are new! I've been working on rewriting most of CatchUp over the past six months or so to modernize it.

Code Diffs

After this rewrite, CatchUp's codebase saw a 20% reduction in Kotlin LoC and a 50% reduction in XML 😮.

Before

--------------------------------------------------------------
Language    files          blank        comment           code
--------------------------------------------------------------
Kotlin        267           2338           5540          16647
XML           129            503           1892           3714
Java            3            122            167            831

After

--------------------------------------------------------------
Language    files          blank        comment           code
--------------------------------------------------------------
Kotlin        231           1654           3683          14318
XML            80            215           1156           1623
Java            1             31             26            252

Circuit

Circuit is a library I co-authored working at Slack and maintain. It's a multiplatform, compose-based architecture for Kotlin applications. These days at Slack I work on developer experience, so I don't get as much internal exposure to developing with Circuit as I'd like. CatchUp gives me a good outlet for that, and rewriting CatchUp's architecture from standard Android activities/fragments was a good exercise in both proving out its production-readiness as well as finding areas we could improve.

CatchUp is now single-activity, fragment-less, and MainActivity only has around 40 meaningful lines of code before jumping off into Circuit for the rest of the app.

Some of my favorite parts of this have been how much simpler every screen of CatchUp is now, with clear separations of concerns and easy extension. Writing new screens is dead-simple to both write and wire up. Writing presentation logic in Compose is :chefs-kiss:. The biggest win with going all in on Compose though is the ability to take (edit: mostly*) full control of config changes, allowing CatchUp to actually avoid activity recreation entirely and just reacting to live Configuration sources instead. This is a massive win for productivity, as it means I don't have to spend any time thinking about how to handle rotation.

One big thing Circuit still needs is improved animation APIs, which we purposefully punted to wait for LookaheadLayout to come along more.

Compose

CatchUp's UI was mostly standard Android views/xml, with a tiny bit of Compose that I'd previously dabbled with in service ordering left over from a couple years ago. Rewriting all of this UI in Compose was daunting at first, often frustrating during, and ultimately incredibly rewarding after. Compose UI is a substantial QoL improvement over the olden days of Android UI work.

Compose also makes supporting different screen sizes pretty simple, and some of the new APIs Google's been writing for tablets and foldables are great. CatchUp has extremely basic support for them, but only really as far as trying to better fit content on wide screens for now.

I also implemented support for M3's dynamic theme support more or less seamlessly.

That's not to say it's perfect though. There are some things about Compose UI that really bug me after this experience.

  • IDE tooling is just not good. Things like Live Edit (or before it - live literals) virtually never work in my experience. Compose previews can be extremely hit or miss. This is a long-running theme of IDE design tools seemingly only working on trivial projects, and it's disappointing to see this still happening in Compose.
  • M3's defaults are often just ugly. This is obviously beholden personal taste, but it doesn't feel like the average app using M3 defaults would look good. M3's theme builder also isn't particularly useful as it doesn't think black and white are valid colors, and will usually just spit out things that are vaguely magenta hues.
  • Compose UI performance in debug is terrible. In release (i.e. minified + debuggable=false), it's just fine in my experience. Why does it matter then? Because developers fix what's in front of them, which is usually a debug build. If you normalize janky apps in debug, developers won't notice "real" jank nearly as often and fix it. It's also annoying to use a janky app in any context.
  • Compose and M3 animations still feel primitive and often difficult to do. There is still no shared element transitions.

I'm optimistic that some of the above can be improved with time, and definitely don't consider them to be blockers of any sort.

SqlDelight

I migrated from ROOM to SqlDelight for a couple of reasons.

  1. Multiplatform support (more on this later)
  2. I like SqlDelight's design better

SqlDelight is wonderful. I wish it supported some of ROOM's features more easily (namely @Embedded and easier reading of bundled dbs), and I find its migration/versioning handling confusing at times, but overall I vastly prefer it. ROOM works just fine, and worked just fine for CatchUp for years.

Coroutines

This will be my spiciest take: I struggled a lot with coroutines adoption. I would consider myself fairly good at reactive frameworks and at least not terrible at concurrency, and even then it was roughy at times. Debugging them felt significantly harder than RxJava, and I often found myself feeling like it was just too fucking magic. Incredibly powerful, yes. But at times to the point of being opaque. I prefer them to RxJava (what CatchUp used before) now I think, but mostly because I like structured concurrency a lot. As the author of AutoDispose, I wish it didn't need to exist and structured concurrency is the right solution to this problem space.

RxJava works really well. I actually left much of it in place initially while moving the major API surfaces to coroutines and just interop-ing as needed. The interop APIs are very good.

Build Tools

CatchUp now sits on top of SGP, which is the open source Gradle plugin I wrote and maintain for Slack's internal android repo. Aside from being partial to it and the features it offers, it's been super helpful in allowing us to test out or repro things in other projects than the main android repo first.

Anvil is something we adopted heavily in Slack a couple years ago and it's awesome, no notes. Here's a blog I wrote for Slack about it.

One thing I built in SGP is a tool called Dependency Rake, which sits on top of Tony Robalik's excellent Dependency Analysis Gradle Plugin (DAGP) plugin. It post-processes the computed advice and then applies it to the project's build file. I've gotten this working more or less perfectly in CatchUp, so its build files are nice and exact.

CatchUp chases the latest Kotlin versions pretty aggressively (I'm a member of Kotlin's EAP-champions program and use it as one of my test repos). It's at the point now where it almost compiles successfully with the upcoming K2 compiler, pending (at the time of writing) a new Dagger release.

Misc

Datastore is fine as a replacement for preferences. I like that it's now multiplatform too. The API is a bit awkward and tedious at times like old preferences were, but I think that's more a nature of preferences at this point. The coroutines-first API and semantics is also a nice QoL improvement for reactive prefs.

AndroidX Paging is also fine, it solves a problem we all have to write customs solutions for and solves most of them well enough. There are still oddities in CatchUp where it either sticks with stale data, loads mid-page, or shows two loading indicators. I suspect I'm the bug for those, rather than the library.

Some services CatchUp loads have changed

  • Product Hunt deprecated their v1 API and replaced it with a GraphQL v2 API. While it works, they removed a bunch of data from it and don't appear to actively be maintaining it.
  • Reddit's informal API of just appending .json to a URL still seems to work for now, but the way they handled 3rd party apps is such an unprofessional and unethical horror show.
  • Medium's unofficial API became even more unofficial and disappeared entirely ¯\_(ツ)_/¯.
  • Unsplash's API is nice to work with, and a fun test bed for doing things with image loading.
  • I wrote a little summarization utility that uses ChatGPT to summarize links on long press. Not always useful, but a fun little demo.
(Spoiler: they should be free)

Finally, I rewrote the image viewer using Telephoto. It's a great.

Future

I have some general areas I want to tinker with more in CatchUp.

  • I'd like to start working toward multiplatform support, or at least desktop. As I use an iOS device as my primary these days, I don't use CatchUp the app much anymore otherwise. I'd like to also continue experimenting with Kotlin multiplatform, as I think it's a really promising space. Many of the tools CatchUp sits on top of now (Circuit, Compose, SqlDelight, Datastore, etc) are all multiplatform, and most of the rest are at least JVM-compatible.
  • Improving large/foldable screen support. I bought a Pixel Fold as my test device for the next few years.
  • Continue not publishing it on the play store 🫣.

*It's been pointed out that you can't escape config changes if your wallpaper changes. Good news is I'm CatchUp's only user and don't particularly care about that.