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!