Advanced Features
Caching
Implement intelligent caching strategies to improve performance and offline support.
Setup Cache Manager
// Create Room database
val database = Room.databaseBuilder(
context,
AppDatabase::class.java,
"cache-db"
).build()
// Initialize cache manager
val cacheManager = CacheManager(database, Gson())
Use Caching
suspend fun getUsersWithCache(): NetworkResponse<List<User>, ErrorResponse> {
return executeWithCache(
cacheManager = cacheManager,
cacheKey = "users_list",
type = object : TypeToken<List<User>>() {}.type,
config = CacheConfig(
strategy = CacheStrategy.CACHE_FIRST,
maxAgeSeconds = 300 // 5 minutes
)
) {
apiService.getUsers()
}
}
Cache Strategies
NETWORK_FIRST
Try network first, fallback to cache on error. Best for data that changes frequently.
CacheConfig(strategy = CacheStrategy.NETWORK_FIRST)
CACHE_FIRST
Use cache if available, otherwise fetch from network. Best for relatively static data.
CacheConfig(strategy = CacheStrategy.CACHE_FIRST)
NETWORK_ONLY
Always fetch from network, update cache. Best for real-time data.
CacheConfig(strategy = CacheStrategy.NETWORK_ONLY)
CACHE_ONLY
Only use cache, never make network requests. Best for offline mode.
CacheConfig(strategy = CacheStrategy.CACHE_ONLY)
CACHE_WITH_EXPIRY
Use cache if not expired, otherwise fetch from network. Best for time-sensitive data.
CacheConfig(
strategy = CacheStrategy.CACHE_WITH_EXPIRY,
maxAgeSeconds = 300
)
Retry Mechanism
Automatically retry failed requests with exponential backoff.
Basic Retry
suspend fun getUsersWithRetry(): NetworkResponse<List<User>, ErrorResponse> {
return executeWithRetry(
times = 3, // Retry up to 3 times
initialDelay = 100, // Start with 100ms delay
maxDelay = 1000, // Max 1 second delay
factor = 2.0 // Double the delay each time
) {
apiService.getUsers()
}
}
Retry with Custom Logic
suspend fun getUsersWithConditionalRetry(): NetworkResponse<List<User>, ErrorResponse> {
return executeWithRetry(
times = 3,
shouldRetry = { response ->
// Only retry on network errors or 5xx server errors
when (response) {
is NetworkResponse.NetworkError -> true
is NetworkResponse.ServerError -> response.code >= 500
else -> false
}
}
) {
apiService.getUsers()
}
}
Combining Retry and Cache
Get the best of both worlds - resilient network calls with caching:
suspend fun getUsersResilient(): NetworkResponse<List<User>, ErrorResponse> {
return executeWithRetryAndCache(
cacheManager = cacheManager,
cacheKey = "users_list",
type = object : TypeToken<List<User>>() {}.type,
config = CacheConfig(strategy = CacheStrategy.CACHE_FIRST),
times = 3,
initialDelay = 100
) {
apiService.getUsers()
}
}
OkHttp Interceptors
Add caching at the HTTP level for more control.
Cache Interceptor
val okHttpClient = OkHttpClient.Builder()
.addInterceptor(CacheInterceptor(maxAgeSeconds = 300))
.cache(Cache(context.cacheDir, 10 * 1024 * 1024)) // 10 MB
.build()
Offline Cache Interceptor
val okHttpClient = OkHttpClient.Builder()
.addInterceptor(OfflineCacheInterceptor {
isNetworkAvailable()
})
.cache(Cache(context.cacheDir, 10 * 1024 * 1024))
.build()
Complete Setup
val okHttpClient = OkHttpClient.Builder()
.addInterceptor(CacheInterceptor(maxAgeSeconds = 300))
.addInterceptor(OfflineCacheInterceptor { isNetworkAvailable() })
.cache(Cache(context.cacheDir, 10 * 1024 * 1024))
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.build()
val retrofit = Retrofit.Builder()
.client(okHttpClient)
.baseUrl("https://api.example.com/")
.addCallAdapterFactory(NetworkResponseAdapterFactory())
.addConverterFactory(GsonConverterFactory.create())
.build()
Best Practices
1. Repository Pattern
Encapsulate network logic in repositories:
class UserRepository(
private val apiService: ApiService,
private val cacheManager: CacheManager
) {
suspend fun getUsers(forceRefresh: Boolean = false): Result<List<User>> {
val config = CacheConfig(
strategy = if (forceRefresh) {
CacheStrategy.NETWORK_ONLY
} else {
CacheStrategy.CACHE_FIRST
},
maxAgeSeconds = 300
)
return when (val response = executeWithCache(
cacheManager,
"users",
object : TypeToken<List<User>>() {}.type,
config
) {
apiService.getUsers()
}) {
is NetworkResponse.Success -> Result.success(response.body)
is NetworkResponse.Error -> Result.failure(response.error)
}
}
}
2. UI State Management
Use sealed classes for UI states:
sealed class UiState<out T> {
object Loading : UiState<Nothing>()
data class Success<T>(val data: T) : UiState<T>()
data class Error(val message: String) : UiState<Nothing>()
}
class UserViewModel(private val repository: UserRepository) : ViewModel() {
private val _uiState = MutableStateFlow<UiState<List<User>>>(UiState.Loading)
val uiState: StateFlow<UiState<List<User>>> = _uiState
fun loadUsers(forceRefresh: Boolean = false) {
viewModelScope.launch {
_uiState.value = UiState.Loading
_uiState.value = when (val response = repository.getUsers(forceRefresh)) {
is NetworkResponse.Success -> UiState.Success(response.body)
is NetworkResponse.Error -> UiState.Error(
response.error.message ?: "Unknown error"
)
}
}
}
}
3. Custom Error Types
Define domain-specific error types:
data class ApiError(
val message: String,
val errorCode: String,
val timestamp: Long,
val details: Map<String, Any>? = null
)
// Use in API interface
interface ApiService {
@GET("users")
suspend fun getUsers(): NetworkResponse<List<User>, ApiError>
}
4. Testing
Mock responses for testing:
class FakeApiService : ApiService {
override suspend fun getUsers(): NetworkResponse<List<User>, ErrorResponse> {
return NetworkResponse.Success(
body = listOf(User(1, "Test User", "test@example.com")),
code = 200,
headers = null
)
}
}
Performance Tips
- Use appropriate cache strategies based on data volatility
- Set reasonable cache expiry times
- Implement retry only for idempotent operations
- Use OkHttp’s connection pooling for better performance
- Consider using
CACHE_FIRSTfor static data - Monitor cache size and implement cleanup strategies
Next Steps
- API Reference - Complete API documentation
- GitHub Repository - Source code and examples