DEV Community

Faizullah
Faizullah

Posted on

Clean Architecture on Android: What I Learned Building a Wallpaper App From Scratch

By Faiz Ullah — Android Developer & Founder of DG Technology


A wallpaper app sounds simple: fetch images, show a grid, let the user tap "set wallpaper." I built exactly that — WallReddit, a native Android app pulling images from Reddit — and used it as an excuse to do something I'd wanted to get right for a while: a genuinely clean, layered architecture instead of the "everything in one Activity" approach most small apps fall into.

Here's what building it taught me.


Why Bother With Clean Architecture for "Just a Wallpaper App"

It's tempting to skip architecture for a small app. But the moment you add offline favorites, a download manager, NSFW filtering, and a background auto-rotation feature, an unstructured app turns into a maze of interdependent code that's terrifying to touch.

So I split the app into three layers with one hard rule: dependencies only point inward.

ui/        → talks to → domain/        → talks to → data/
(Compose)                (use cases,                (Retrofit, Room,
                          pure Kotlin)                DataStore)
Enter fullscreen mode Exit fullscreen mode

The payoff: the domain layer has zero Android imports. No Context, no Activity, nothing Android-specific — just plain Kotlin business logic. That means it's trivially testable and would survive a total UI rewrite untouched.


Reactive Settings Without the Boilerplate

Every setting in the app — dark mode, AMOLED mode, grid density, NSFW visibility — needed to update the UI instantly when changed, from anywhere in the app. Instead of manually plumbing callbacks everywhere, I exposed every preference as a Kotlin Flow backed by Jetpack DataStore:

val isDarkMode: Flow<Boolean> = dataStore.data.map { it[DARK_MODE_KEY] ?: false }
Enter fullscreen mode Exit fullscreen mode

Any screen can collectAsState() on this and the UI updates the instant the value changes anywhere else in the app — no event bus, no manual notification, no stale state.


Fighting Reddit's Anti-Scraping Defenses

Reddit's public JSON endpoints are great until you hit their CDN's bot protection — a default Retrofit client gets 403 Forbidden almost immediately. The fix is straightforward but easy to miss: send a real browser User-Agent and proper timeout/retry configuration:

OkHttpClient.Builder()
    .addInterceptor { chain ->
        chain.proceed(chain.request().newBuilder()
            .header("User-Agent", "WallReddit/1.0 (Android)")
            .build())
    }
Enter fullscreen mode Exit fullscreen mode

It's a one-line fix, but without it the entire app silently fails to load any content — the kind of bug that's invisible until you actually test against the real network.


Wallpapers Look Wrong Without Real Math

The naive way to "set a wallpaper" is to just hand the bitmap to Android's WallpaperManager and hope for the best. The result: stretched, cropped, or letterboxed images depending on the device's aspect ratio.

The actual fix is computing the transform yourself before handing off the bitmap — comparing the image's aspect ratio to the screen's, then applying a Matrix for center-crop or fit-to-screen:

val scale = max(screenWidth / bitmapWidth, screenHeight / bitmapHeight)
matrix.setScale(scale, scale)
Enter fullscreen mode Exit fullscreen mode

This is the difference between an app that looks like an amateur project and one that looks production-grade — and it's invisible work nobody notices unless you get it wrong.


Background Work That Doesn't Drain the Battery

Auto-rotating wallpapers on a schedule means running work when the app isn't even open. Android's background execution limits make naive approaches (a Service that just runs forever) both unreliable and battery-hostile.

WorkManager is the right tool here specifically because it respects Doze mode and battery optimization automatically — your job runs reliably without you having to reinvent Android's power management:

val request = PeriodicWorkRequestBuilder<AutoWallpaperWorker>(interval, TimeUnit.HOURS)
    .setConstraints(Constraints.Builder().setRequiresBatteryNotLow(true).build())
    .build()
Enter fullscreen mode Exit fullscreen mode

What I'd Tell Someone Building Their Second Android App

  • Keep your domain layer Android-free. It's the single highest-leverage architectural decision you can make.
  • Expose settings as reactive streams, not getter functions. Future-you will thank present-you.
  • Test your networking against the real API, not a mock — anti-bot measures only show up in production traffic.
  • Use WorkManager for anything recurring. Don't fight the OS's power management; work with it.

The Stack

Layer Technology
Language Kotlin
UI Jetpack Compose, Material 3
Architecture MVVM + Clean Architecture
Networking Retrofit + OkHttp
Local storage Room, DataStore
Pagination Paging 3
Background work WorkManager
Images/Video Coil, Media3

Faiz Ullah
Android Developer · Full-Stack Engineer · Founder of DG Technology
🌐 faizullah.pk · 💻 github.com/faizullahpk/wallpaper-app-android


Building something on Android and hitting architecture questions? Follow along — I write about real-world mobile and full-stack engineering.

Top comments (0)