PUBLIC OBJECT

Building on the Wrong Abstraction

Writing sturdy software is hard work. Sometimes it’s very hard work, but it doesn’t need to be.

Very Hard Problem: ORM

I have a confession: I like Hibernate. The @Version feature is my favorite; it makes optimistic locking easy and safe. I also like how simple it is to do CRUD operations.

But I also hate Hibernate. I hate the limits of what I can express with it and I hate throwing out working code to drop down to SQL. I struggle to debug Hibernate: the implementation is full of abstractions and indirection. It’s well documented but the users guide is 500 pages!

SQLDelight addresses the same problem as Hibernate but with a different abstraction. Instead of generating SQL from Java at runtime, SQLDelight generates Kotlin from SQL at compile-time.

Hibernate’s ORM solves a more difficult problem than SQLDelight’s SQL binding. It’s runtime .jar is 164x larger! But even though ORM is much bigger problem, it isn’t really a problem I need solved! All I want is a concise and typesafe way to query & update my database.

Hibernate is an extremely good implementation of an ORM, but an ORM isn’t something that should be implemented.

Very Hard Problem: Reactive Engine

RxJava changed Android development dramatically and permanently. Screens used to be updated clumsily from manual observers. With RxJava, screens are computed on-demand from a live dataset. It’s fucking great.

The implementation of RxJava is heroic. It solves several difficult problems that are tangled up: thread-jumping, lifecycle, declarative APIs, and error handling. RxJava’s flatMap() shows just how subtle this is:

  @Override public void onNext(@NonNull T t) {
    if (done) return;
    try {
      try (Stream<? extends R> stream = mapper.apply(t)) {
        Iterator<? extends R> it = stream.iterator();
        while (it.hasNext()) {
          if (disposed) {
            done = true;
            break;
          }
          R value = it.next();
          if (disposed) {
            done = true;
            break;
          }
          downstream.onNext(value);
          if (disposed) {
            done = true;
            break;
          }
        }
      }
    } catch (Throwable ex) {
      Exceptions.throwIfFatal(ex);
      upstream.dispose();
      onError(ex);
    }
  }

Kotlin’s Flow address the same problem, but it’s built on a richer abstraction: coroutines! This pushes thread-jumping, lifecycle, and exceptions into the infrastructure. Here’s the whole thing:

public fun <T> Flow<Flow<T>>.flattenConcat(): Flow<T> = flow {
    collect { value -> emitAll(value) }
}

Flow is simple because its platform supports it so well. RxJava does more work to achieve a similar result because it doesn’t use coroutines or structured concurrency.

Very Hard Problem: Declarative and Interactive Layout

ConstraintLayout is built with a deep appreciation for the difficulty of the problem it addresses: dynamic UI layout. It introduces clever ideas and features to solve this problem well: guidelines, chains, and even an interactive editor.

But despite that, ConstraintLayout is difficult to use because it has a cohesion problem: my layout is in XML but I need to operate it from Kotlin.

Contour just shipped 1.0 and it’s big insight is that Layout XML is a bug, not a feature. Putting everything into Kotlin dramatically reduces the size of the problem I’m solving!  No more findViewById(), no more mismatch between views in the XML and views on the screen, no more limitations on what I can express in my layout.

Contour is stable and widely-used within the app my team works on. It’s an incremental migration from XML layouts. They’ll be going away permanently with Compose; Contour is a useful step in that direction.

Avoid Very Hard Problems

When a problem is very difficult sometimes the best solution is to reconsider the premise. Adding an abstraction (yay coroutines) or taking one away (boo XML) may shrink the problem!