Most apps display content created and stored outside the app binary: articles, images, videos, translations, configuration. Delivering this content efficiently — fast first load, offline resilience, minimal data usage — requires a deliberate delivery architecture.
Content Types and Their Delivery Needs
| Content type | Size | Change frequency | Cache strategy |
|---|---|---|---|
| App strings/translations | Small | Rare | Bundle in APK; update via remote config |
| Configuration/feature flags | Tiny | Moderate | Remote config; short TTL |
| Images | Medium | Rarely changes per URL | Long TTL (1 year) + CDN |
| Articles/structured content | Medium | Moderate | Medium TTL (1 hour) + Room cache |
| Videos | Large | Rarely | CDN + progressive streaming |
CDN Strategy for Images
All user-uploaded images should be served through a CDN with image processing capabilities (Cloudflare, Imgix, Thumbor):
// Server stores: gs://bucket/uploads/abc123.jpg
// CDN serves: https://cdn.example.com/abc123.jpg?w=200&h=200&fit=crop&fmt=webp
fun buildThumbnailUrl(originalUrl: String, widthDp: Int, context: Context): String {
val density = context.resources.displayMetrics.density
val widthPx = (widthDp * density).toInt()
return "$CDN_BASE${originalUrl.substringAfter("/uploads/")}?w=$widthPx&fmt=webp&q=80"
}
With a CDN, you can set Cache-Control: max-age=31536000, immutable on image responses — once loaded, the client never re-requests the image.
Remote-Controlled UI Configuration
Send layout configuration from the server to avoid shipping UI changes as app updates:
@Serializable
data class FeedConfig(
val cardStyle: String = "standard", // "standard", "compact", "hero"
val showImages: Boolean = true,
val maxItemsPerPage: Int = 20,
val refreshIntervalMinutes: Int = 30
)
class ConfigRepository(
private val api: ConfigApi,
private val dataStore: DataStore<Preferences>
) {
val feedConfig: Flow<FeedConfig> = flow {
// 1. Emit cached config immediately
val cached = dataStore.data.first()[FEED_CONFIG_KEY]
?.let { Json.decodeFromString<FeedConfig>(it) }
if (cached != null) emit(cached)
// 2. Fetch fresh config
try {
val fresh = api.getFeedConfig()
dataStore.edit { it[FEED_CONFIG_KEY] = Json.encodeToString(fresh) }
emit(fresh)
} catch (e: IOException) {
if (cached == null) emit(FeedConfig()) // emit defaults if no cache
}
}
}
Adaptive Content Delivery
Adjust content quality based on network conditions:
class AdaptiveContentLoader(private val connectivityManager: ConnectivityManager) {
val isOnUnmetered: Boolean
get() {
val network = connectivityManager.activeNetwork ?: return false
val caps = connectivityManager.getNetworkCapabilities(network) ?: return false
return caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED)
}
fun buildImageUrl(imageId: String): String {
val quality = if (isOnUnmetered) 90 else 60
val format = "webp"
return "$CDN_BASE/$imageId?q=$quality&fmt=$format"
}
fun shouldPreloadVideos(): Boolean = isOnUnmetered
}
In-App Update Delivery
Deliver major content updates while the user is active:
class ContentUpdateManager(private val appUpdateManager: AppUpdateManager) {
fun checkForFlexibleUpdate(activity: Activity) {
appUpdateManager.appUpdateInfo
.addOnSuccessListener { info ->
if (info.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE &&
info.isUpdateTypeAllowed(AppUpdateType.FLEXIBLE)) {
appUpdateManager.startUpdateFlowForResult(
info,
AppUpdateType.FLEXIBLE,
activity,
UPDATE_REQUEST_CODE
)
}
}
}
fun completeFlexibleUpdate() {
appUpdateManager.completeUpdate() // install downloaded update
}
}
Prefetching Critical Content
Prefetch content the user is likely to need next:
class FeedPrefetcher(
private val repository: ArticleRepository,
private val imageLoader: ImageLoader
) {
suspend fun prefetchNextPage(currentPage: Int) {
val nextArticles = repository.getArticles(page = currentPage + 1)
// Prefetch images into Coil's disk cache
nextArticles.forEach { article ->
val request = ImageRequest.Builder(context)
.data(article.thumbnailUrl)
.memoryCachePolicy(CachePolicy.DISABLED) // disk only
.diskCachePolicy(CachePolicy.ENABLED)
.build()
imageLoader.enqueue(request)
}
}
}
Key Takeaways
| Concept | Rule |
|---|---|
| CDN for media | All images through CDN; use URL params for resize/format |
| Long TTL for immutable assets | max-age=31536000 for images with content-addressable URLs |
| Remote config for UI | Layout changes without app update via server config |
| Adaptive quality | Lower quality images on metered networks |
| Prefetch | Load next page content while user reads current page |