Android & Kotlin Technical Articles

Detailed articles on Android development, Jetpack Compose internals, Kotlin coroutines, and open source library design by skydoves, Google Developer Expert and maintainer of Android libraries with 40M+ annual downloads. Read practical guides on Retrofit, Compose Preview, BottomSheet UI, coroutine compilation, and more.

Exclusive Articles
RSS

This is a collection of private or subscriber-first articles written by the Dove Letter, skydoves (Jaewoong). These articles can be released somewhere like Medium in the future, but always they will be revealed for Dove Letter members first.

Scalable API Response Handling Across Multi Layered Architectures with Sandwich

Modern Android applications commonly adopt multi layered architectures such as MVVM or MVI, where data flows through distinct layers: a data source, a repository, and a ViewModel or presentation layer. Each layer has a specific responsibility, and network responses must propagate through all of them before reaching the UI. While this separation produces clean, testable code, it introduces a real challenge: how do you handle API responses, including errors and exceptions, as they cross each layer boundary? Most developers solve this by wrapping API calls in try-catch blocks and returning fallback values. This works for small projects, but as the number of API calls grows, the approach creates ambiguous results, scattered boilerplate, and lost context that downstream layers need. You end up with ViewModels that cannot tell whether an empty list means "no data" or "network failure," repositories that swallow important error details, and data sources that repeat the same error handling pattern dozens of times. In this article, you'll explore the problems that emerge when handling Retrofit API calls across layered architectures, why conventional approaches break down at scale, and how Sandwichhttps://github.com/skydoves/sandwich provides a type safe, composable solution that simplifies response handling from the network layer all the way to the UI. You'll also walk through the full set of Sandwich APIs, from basic response handling to advanced patterns like sequential composition, response merging, global error mapping, and Flow integration, each with real world use cases that show when and why you would reach for them. Retrofit API calls with coroutines Most Android projects use Retrofithttps://github.com/square/retrofit with Kotlin coroutineshttps://github.com/Kotlin/kotlinx.coroutines for network communication. A typical service interface looks like this: kotlin interface PosterService { @GET"DisneyPosters.json" suspend fun fetchPosterList: List<Poster } The service returns a List<Poster directly. Retrofit deserializes the JSON response body and gives you the data. This works perfectly when the request succeeds, but it gives you no structured way to handle failures. Retrofit throws an HttpException for non 2xx status codes and various IO exceptions for network problems. The responsibility of catching these falls entirely on the caller. When you consume this service in a data source, the conventional approach looks like this: kotlin class PosterRemoteDataSource private val posterService: PosterService, { suspend fun fetchPosterList: List<Poster { return try { posterService.fetchPosterList } catch e: HttpException { emptyList } catch e: Throwable { emptyList } } } The data source catches every possible exception and returns emptyList as a fallback. From the caller's perspective, this function always succeeds, it always returns a List<Poster. If we create a flow from the code above, it will be like so: !https://velog.velcdn.com/images/skydoves/post/cc3deaea-7244-4091-88d3-744d297112cc/image.png But that apparent simplicity hides a serious problem. This compiles and runs. But once you trace the data flow through a full architecture, where the data source feeds a repository that feeds a ViewModel that drives the UI, the problems become clear. The problems with conventional response handling The code above has three major issues that compound as your project grows and the number of API endpoints increases. Ambiguous results The data source returns emptyList for both HTTP errors and network exceptions. Downstream layers the repository, the ViewModel receive a List<Poster with no way to distinguish between three completely different scenarios: 1. The request succeeded and the server returned an empty list. 2. The request failed with a 401 Unauthorized error. 3. The device had no network connectivity. All three produce the same result: an empty list. The repository cannot decide whether to show an error message, redirect to a login screen, or display "no data" content. The ViewModel might show an empty state when it should be showing a "please log in" dialog. The response has lost its context, and once that context is gone, no amount of downstream logic can recover it. You might try to work around this by returning null for failures instead of emptyList. But that introduces its own ambiguity: does null mean "error" or "no data"? You end up needing a wrapper type anyway, which leads to the next problem. That's just adding one more implicit convention on your head. Boilerplate error handling Every API call requires its own try-catch block. If you have 20 service methods, you write 20 nearly identical try-catch blocks. Each one catches HttpException, catches Throwable, and returns some fallback value. This repetition creates maintenance overhead and increases the surface area for mistakes, like forgetting to handle a specific exception type in one of the 20 call sites. Consider a data source with multiple methods: kotlin class UserRemoteDataSourceprivate val userService: UserService { suspend fun fetchUserid: String: User? { return try { userService.fetchUserid } catch e: HttpException { null } catch e: Throwable { null } } suspend fun fetchFollowersid: String: List<User { return try { userService.fetchFollowersid } catch e: HttpException { emptyList } catch e: Throwable { emptyList } } suspend fun updateProfileprofile: Profile: Boolean { return try { userService.updateProfileprofile true } catch e: HttpException { false } catch e: Throwable { false } } } The pattern is identical every time: try the call, catch HttpException, catch Throwable, return a fallback. The only thing that changes is the fallback value null, emptyList, false. This is textbook boilerplate that should not exist in every data source class. One dimensional response processing

CoroutinesNetworkArchitecture
Sunday, February 8, 2026
Explore internal mechanisms of Retrofit, and how it works

Making REST API calls has been a fundamental requirement in Android development, yet the complexity of managing HTTP requests, serialization, error handling, and thread management has long been a persistent challenge. Retrofit emerged as Square's solution to this problem, transforming a verbose, error-prone process into an elegant, annotation-driven API. But the real power of Retrofit isn't just its simplified interface, it's the sophisticated machinery working behind the scenes to turn interface methods into HTTP calls. In this article, you'll dive deep into the internal mechanisms of Retrofit, exploring how Java's dynamic proxies create implementation classes at runtime, how annotations are parsed and cached using sophisticated locking strategies, how the framework transforms method calls into OkHttp requests through a layered architecture, and the subtle optimizations that make it production-ready. This isn't a beginner's guide to using Retrofit, it's a deep dive into how Retrofit actually works under the hood. Understanding the core abstraction: What makes Retrofit special At its heart, Retrofit is a type-safe HTTP client that uses dynamic proxies and annotation processing to convert interface method declarations into HTTP requests. What distinguishes Retrofit from manual HTTP clients is its adherence to two fundamental principles: declarative API definition and pluggable architecture. The declarative API definition means you don't manually construct HTTP requests for every endpoint. Instead, Retrofit provides annotations that describe the request: kotlin interface GitHubApi { @GET"users/{user}/repos" fun listRepos@Path"user" user: String: Call<List<Repo } // Implementation generated automatically: val api = retrofit.create<GitHubApi val call = api.listRepos"octocat" The pluggable architecture means Retrofit separates concerns through factory patterns. Every aspect of request/response handling is customizable: - CallAdapter: Transforms Call<T into other types RxJava Observable, Kotlin suspend fun, Java 8 CompletableFuture - Converter: Serializes/deserializes request/response bodies Gson, Jackson, Moshi, Protobuf - Call.Factory: Creates HTTP calls typically OkHttp, but swappable These properties aren't just conveniences, they're architectural constraints that enable compile-time type safety and runtime flexibility. The dynamic proxy mechanism allows Retrofit to parse annotations once per method and cache the parsing logic, making subsequent calls extremely fast. The factory chains allow you to add Gson JSON parsing or RxJava integration without modifying any core Retrofit code. The dynamic proxy pattern: How Retrofit creates implementations When you call retrofit.createMyApi.class, you're not getting a manually written implementation. You're getting a JDK dynamic proxy that intercepts every method call at runtime. This is the foundation of Retrofit's "magic." Proxy creation in Retrofit.create Let's examine the actual proxy creation code in the Retrofit class: java @SuppressWarnings"unchecked" public <T T createfinal Class<T service { validateServiceInterfaceservice; return T Proxy.newProxyInstance service.getClassLoader, new Class<? {service}, new InvocationHandler { private final Object emptyArgs = new Object0; @Override public @Nullable Object invokeObject proxy, Method method, @Nullable Object args throws Throwable { // If the method is a method from Object then defer to normal invocation. if method.getDeclaringClass == Object.class { return method.invokethis, args; } args = args != null ? args : emptyArgs; Reflection reflection = Platform.reflection; return reflection.isDefaultMethodmethod ? reflection.invokeDefaultMethodmethod, service, proxy, args : loadServiceMethodservice, method.invokeproxy, args; } }; } This code uses Java's Proxy.newProxyInstance to generate a class at runtime that implements your interface. Every method call goes through the InvocationHandler.invoke method, which has three dispatch paths: 1. Object methods: Methods like equals, hashCode, and toString are delegated to the handler itself: java if method.getDeclaringClass == Object.class { return method.invokethis, args; } This ensures that basic Java object operations work correctly on the proxy instance. 2. Default methods Java 8+ - Interface default methods are invoked using platform-specific reflection: java return reflection.invokeDefaultMethodmethod, service, proxy, args; On Java 8+, Retrofit uses MethodHandle to invoke default methods. This allows you to add helper methods to your API interfaces without Retrofit trying to parse them as HTTP endpoints. 3. Retrofit methods: Everything else is treated as an HTTP endpoint: java return loadServiceMethodservice, method.invokeproxy, args; This is where the real work happens. The loadServiceMethod call parses annotations and caches the result, then invoke executes the HTTP request. Interface validation Before creating the proxy, Retrofit validates the interface with some strict rules in the Retrofit class: java private void validateServiceInterfaceClass<? service { if !service.isInterface { throw new IllegalArgumentException"API declarations must be interfaces."; } Deque<Class<? check = new ArrayDeque<1; check.addservice; while !check.isEmpty { Class<? candidate = check.removeFirst; if candidate.getTypeParameters.length != 0 { StringBuilder message = new StringBuilder"Type parameters are unsupported on ".appendcandidate.getName; if candidate != service { message.append" which is an interface of ".appendservice.getName; } throw new IllegalArgumentExceptionmessage.toString; } Collections.addAllcheck, candidate.getInterfaces; } // ... } This validation enforces two critical constraints: 1. Must be an interface: Classes can't be proxied by JDK proxies they'd need CGLIB or ByteBuddy 2. No generic type parameters: interface Api<T is forbidden because generics are erased at runtime The breadth-first search through the interface hierarchy ensures that even inherited interfaces don't violate these rules. The performance benefit of proxies Why use dynamic proxies instead of annotation processing to generate implementation classes at compile time? The answer is flexibility. Proxies allow Retrofit to: - Parse annotations lazily only when methods are first called - Support different return types through the CallAdapter mechanism - Avoid compile-time code generation complexity The trade-off is a slight runtime overhead for the first method call annotation parsing, but this is amortized through aggressive caching. The service method cache: lazy initialization

NetworkArchitecture
Monday, November 24, 2025
Retrofit's Proxy pattern: how can the instance actually be created from an interface? The magic is revealed

REST APIs form the backbone of modern Android applications, yet the question of how Retrofit creates concrete implementations from plain interface definitions has puzzled many developers. When you call retrofit.createGitHubApi.class and receive a working API client, something remarkable happens under the hood, an entire implementation class is generated at runtime, complete with HTTP logic, parameter handling, and response parsing. This seemingly magical transformation is powered by Java's dynamic proxy mechanism combined with Retrofit's sophisticated annotation parsing and caching strategies. In this article, you'll dive deep into the internal mechanisms of Retrofit's proxy system, exploring how Java's Proxy.newProxyInstance creates implementation classes at runtime, how the InvocationHandler intercepts method calls and routes them to the correct execution path, how Retrofit's three-state cache ensures thread-safe lazy initialization with lock-free fast paths, and how annotation metadata is transformed into executable HTTP requests. This isn't a guide on using Retrofit, it's a deep dive into how the instance creation magic actually works. Understanding the fundamental challenge: From interfaces to instances At its core, Retrofit faces a seemingly impossible task: creating instances from interfaces. In standard Java, you cannot instantiate an interface: kotlin interface GitHubApi { @GET"users/{user}/repos" fun listRepos@Path"user" user: String: Call<List<Repo } // This won't compile val api = GitHubApi // ERROR: Cannot create an instance of an interface Interfaces define contracts, not implementations. Yet Retrofit allows you to write: kotlin val retrofit = Retrofit.Builder .baseUrl"https://api.github.com/" .build val api = retrofit.createGitHubApi::class.java val repos = api.listRepos"octocat".execute The api object behaves as if someone manually wrote an implementation class that handles HTTP requests, parameter encoding, and response parsing. How does Retrofit generate this implementation without any explicit code? The answer lies in Java's dynamic proxy pattern, a runtime code generation mechanism that allows creating interface implementations on the fly. Understanding this pattern is key to understanding Retrofit's architecture. The dynamic proxy pattern: Runtime class generation Java's java.lang.reflect.Proxy class provides a mechanism to create proxy instances that implement specified interfaces at runtime. Let's examine how this works before diving into Retrofit's specific implementation. The basic proxy mechanism The Proxy.newProxyInstance method takes three parameters and returns an object that implements your interface: java public static Object newProxyInstance ClassLoader loader, // Which ClassLoader to define the proxy class in Class<? interfaces, // The list of interfaces to implement InvocationHandler h // The handler that processes method calls When you call any method on the proxy instance, Java automatically routes the call to the InvocationHandler.invoke method, which receives: - The proxy instance itself - The Method object representing the called method - The array of arguments passed to the method This is powerful because you can implement one handler that processes all methods on the interface. The handler can inspect the method's annotations, parameters, and return type to determine what to do. Here's a minimal example: java interface HelloService { String sayHelloString name; String sayGoodbyeString name; } HelloService service = HelloService Proxy.newProxyInstance HelloService.class.getClassLoader, new Class<? { HelloService.class }, new InvocationHandler { @Override public Object invokeObject proxy, Method method, Object args { String methodName = method.getName; String name = String args0; return methodName + " called with: " + name; } } ; service.sayHello"Alice"; // Returns: "sayHello called with: Alice" service.sayGoodbye"Bob"; // Returns: "sayGoodbye called with: Bob" The same invoke method handles both sayHello and sayGoodbye. It inspects the method name and arguments to determine the behavior. This is exactly how Retrofit works: it creates a proxy instance where every method call is routed to an InvocationHandler that parses annotations and constructs HTTP requests. Retrofit's proxy creation: The create method Let's examine Retrofit's actual implementation of create in the Retrofit class: java @SuppressWarnings"unchecked" public <T T createfinal Class<T service { validateServiceInterfaceservice; return T Proxy.newProxyInstance service.getClassLoader, new Class<? {service}, new InvocationHandler { private final Object emptyArgs = new Object0;

Network
Monday, November 24, 2025
A Study: How Retrofit, written in Java, interpolates Kotlin's Coroutines to enable `suspend` functions

In the modern Android development ecosystem, the synergy between Kotlin and Java is quite still important since many of very traditional projects are written in Java. A prime example of this great interoperability is Square's Retrofit library. Despite being written entirely in Java, Retrofit seamlessly supports Kotlin's suspend functions, allowing developers to write clean, idiomatic asynchronous code for network requests. This capability is not magic, it is a sophisticated illusion built upon a cooperative understanding between the Kotlin compiler and Retrofit's dynamic, reflection-based architecture. This study examines the internal mechanisms that make this "interpolation" possible, revealing how a Java library can interact with a language feature it has no native concept of. The Foundation: Continuation-Passing Style CPS Transformation

CoroutinesNetworkKotlin
Sunday, August 24, 2025

Like what you see?

Subscribe to Dove Letter to get weekly insights about Android and Kotlin development, plus access to exclusive content and discussions.