Building a Wallpaper App That Fetches From an API
Building an Android wallpaper app with Kotlin that fetches high-resolution images from a REST API, implements local caching with Room, and sets wallpapers programmatically.
title: "Building a Wallpaper App That Fetches From an API" date: "2024-07-20" description: "Building an Android wallpaper app with Kotlin that fetches high-resolution images from a REST API, implements local caching with Room, and sets wallpapers programmatically." tags: ["kotlin", "android", "api"] published: true image: ""
Wallpaper apps are a surprisingly good learning project. They seem simple — fetch images, display them, set them — but each step has genuine complexity: API rate limits, large file downloads, offline behaviour, and a system API (WallpaperManager) that behaves differently depending on the Android version and launcher. This was my first Android project that involved all three of networking, local persistence, and a system service at the same time.
Project Overview
The app lets users browse a grid of wallpapers fetched from a free API, preview them full-screen, mark favourites that persist offline, and apply any wallpaper to the home screen or lock screen (or both). The tech stack is deliberately different from my Compose gallery app — this uses Views and RecyclerView rather than Compose, which meant working with adapters and DiffUtil instead of lazy lists.
The main components:
| Component | Technology |
|---|---|
| Networking | Retrofit 2 + OkHttp |
| Image loading | Coil |
| Local database | Room |
| Async work | Kotlin Coroutines + Flow |
| DI | Manual (no Hilt) |
| Layout | RecyclerView + StaggeredGridLayoutManager |
API Integration With Retrofit
The wallpaper API follows a structure similar to Unsplash — paginated endpoints returning JSON arrays of photo objects with URLs for different resolutions:
The Retrofit instance is built with an OkHttpClient that handles auth headers and sets sensible timeouts:
The API key lives in BuildConfig sourced from local.properties, not hardcoded in source. This is table stakes — never commit API keys.
Image Loading With Coil
For the grid, I load thumbnails at reduced quality. For the preview screen, I load the regular size (~1080px wide), and only switch to full when the user taps "apply":
Coil's disk cache is enabled by default, which means scrolling back through previously loaded wallpapers doesn't re-fetch from the network. For a wallpaper app where images are large and bandwidth-expensive, this is not optional.
One thing I got wrong early on: I was using binding.ivPreview.load(urls.full) on the preview screen. The full URL returns the original resolution — sometimes 5000×7000px. Loading that into a preview screen for display purposes is wasteful and slow. Switching to regular made the preview load in 1–2 seconds instead of 10+.
Room for Favourites
The Favourite entity is minimal — I store only what's needed to display the wallpaper offline, not the full DTO:
Returning Flow<List<FavouriteEntity>> from the DAO rather than a plain List means the UI automatically updates when a favourite is added or removed — no manual refresh logic needed. The ViewModel collects this flow:
Setting Wallpapers Programmatically
WallpaperManager is the system API for setting wallpapers. The tricky part is that setting a wallpaper requires decoding a potentially large bitmap, which must happen off the main thread, and you have to handle the case where the download itself is still in progress:
The allowHardware(false) flag is necessary because Coil by default may decode into a HardwareBitmap, which lives in GPU memory and cannot be read by WallpaperManager. Without this, you get a java.lang.IllegalArgumentException: Software rendering doesn't support hardware bitmaps at runtime.
The FLAG_SYSTEM and FLAG_LOCK constants for targeting home vs lock screen were added in API 24. On older devices, setBitmap() without flags sets both simultaneously — something to document in your manifest's <uses-sdk>.
Pagination
For the main grid, I implemented simple cursor-based pagination with a manual "load more" trigger. Jetpack Paging 3 exists for this, but I wanted to understand the mechanics before using an abstraction:
The RecyclerView triggers loadNextPage() via a scroll listener when the user reaches the last 5 items. It works, but the Paging 3 library handles edge cases (network errors, retry logic, placeholder items) much more robustly. I'd use it for anything production-facing.
What I Learned About API-Driven Apps
Throttle your API calls. Free API tiers have rate limits. I hit the Unsplash limit during testing by rapid-scrolling through the grid and triggering a new page load every second. Adding a debounce on the scroll listener fixed it.
Use the right image size for the right context. Thumb for grid, regular for preview, full only for the actual wallpaper set operation. This seems obvious in retrospect but the temptation to "just use the full URL everywhere" is real when you first get it working.
Flow from Room makes favourite-state synchronisation trivial. The heart icon on each grid item reflects the correct state automatically because it's driven by a database query that emits on every change. Before I switched to Flow, I was manually refreshing the adapter after every insert/delete, which was fragile.
Writing the pagination manually before using Paging 3 was the right call. I now understand what the library is doing underneath, which makes debugging much easier when things go wrong.
The app ended up being one of the more complete projects I built during my academic years — it has networking, caching, a local database, system API integration, and pagination all working together. Each of those pieces is straightforward in isolation; getting them to work cleanly together requires thinking carefully about data flow and threading.