ios widget showing what is available at chucks
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: android disk cache

+107 -11
+65
android/app/src/main/java/com/wasupchucks/data/repository/MenuCache.kt
··· 1 + package com.wasupchucks.data.repository 2 + 3 + import android.content.Context 4 + import androidx.datastore.preferences.core.edit 5 + import androidx.datastore.preferences.core.longPreferencesKey 6 + import androidx.datastore.preferences.core.stringPreferencesKey 7 + import androidx.datastore.preferences.preferencesDataStore 8 + import com.squareup.moshi.Moshi 9 + import com.squareup.moshi.Types 10 + import com.wasupchucks.data.model.VenueMenu 11 + import dagger.hilt.android.qualifiers.ApplicationContext 12 + import kotlinx.coroutines.flow.first 13 + import kotlinx.coroutines.flow.map 14 + import javax.inject.Inject 15 + import javax.inject.Singleton 16 + 17 + private val Context.menuDataStore by preferencesDataStore(name = "menu_cache") 18 + 19 + data class CachedMenu( 20 + val menu: Map<String, List<VenueMenu>>, 21 + val cacheTime: Long 22 + ) 23 + 24 + @Singleton 25 + class MenuCache @Inject constructor( 26 + @ApplicationContext private val context: Context, 27 + moshi: Moshi 28 + ) { 29 + private val menuKey = stringPreferencesKey("menu_json") 30 + private val cacheTimeKey = longPreferencesKey("cache_time") 31 + 32 + private val menuMapType = Types.newParameterizedType( 33 + Map::class.java, 34 + String::class.java, 35 + Types.newParameterizedType(List::class.java, VenueMenu::class.java) 36 + ) 37 + private val menuAdapter = moshi.adapter<Map<String, List<VenueMenu>>>(menuMapType) 38 + 39 + suspend fun save(menu: Map<String, List<VenueMenu>>) { 40 + context.menuDataStore.edit { prefs -> 41 + prefs[menuKey] = menuAdapter.toJson(menu) 42 + prefs[cacheTimeKey] = System.currentTimeMillis() 43 + } 44 + } 45 + 46 + suspend fun load(): CachedMenu? { 47 + return context.menuDataStore.data.map { prefs -> 48 + val json = prefs[menuKey] ?: return@map null 49 + val cacheTime = prefs[cacheTimeKey] ?: return@map null 50 + val menu = try { 51 + menuAdapter.fromJson(json) ?: return@map null 52 + } catch (e: Exception) { 53 + return@map null 54 + } 55 + CachedMenu(menu, cacheTime) 56 + }.first() 57 + } 58 + 59 + suspend fun clear() { 60 + context.menuDataStore.edit { prefs -> 61 + prefs.remove(menuKey) 62 + prefs.remove(cacheTimeKey) 63 + } 64 + } 65 + }
+1 -1
android/app/src/main/java/com/wasupchucks/data/repository/MenuRepository.kt
··· 10 10 suspend fun getMenuForDate(date: LocalDate): Result<List<VenueMenu>> 11 11 suspend fun getSpecials(date: LocalDate, phase: MealPhase): Result<List<MenuItem>> 12 12 suspend fun getSpecialsWithVenue(date: LocalDate, phase: MealPhase): Result<Pair<List<MenuItem>, String>> 13 - fun invalidateCache() 13 + suspend fun invalidateCache() 14 14 } 15 15 16 16 sealed class ChucksError : Exception() {
+26 -9
android/app/src/main/java/com/wasupchucks/data/repository/MenuRepositoryImpl.kt
··· 14 14 15 15 @Singleton 16 16 class MenuRepositoryImpl @Inject constructor( 17 - private val apiService: ChucksApiService 17 + private val apiService: ChucksApiService, 18 + private val menuCache: MenuCache 18 19 ) : MenuRepository { 19 20 20 21 private val mutex = Mutex() 21 22 private var cachedMenu: Map<String, List<VenueMenu>>? = null 22 23 private var cacheTime: Long = 0 23 - private val cacheExpiration = 60 * 60 * 1000L // 1 hour in milliseconds 24 + private val cacheExpiration = 12 * 60 * 60 * 1000L // 12 hours in milliseconds 24 25 25 26 private val dateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd") 26 27 .withZone(ZoneId.of("America/New_York")) 27 28 28 29 override suspend fun fetchMenu(): Result<Map<String, List<VenueMenu>>> { 29 30 return mutex.withLock { 30 - // Check cache 31 31 val currentTime = System.currentTimeMillis() 32 - val cached = cachedMenu 33 - if (cached != null && currentTime - cacheTime < cacheExpiration) { 34 - return@withLock Result.success(cached) 32 + 33 + // Check in-memory cache first 34 + val memCached = cachedMenu 35 + if (memCached != null && currentTime - cacheTime < cacheExpiration) { 36 + return@withLock Result.success(memCached) 37 + } 38 + 39 + // Check persistent cache 40 + val diskCached = menuCache.load() 41 + if (diskCached != null && currentTime - diskCached.cacheTime < cacheExpiration) { 42 + cachedMenu = diskCached.menu 43 + cacheTime = diskCached.cacheTime 44 + return@withLock Result.success(diskCached.menu) 35 45 } 36 46 37 47 // Fetch from API ··· 39 49 val menu = apiService.fetchMenu() 40 50 cachedMenu = menu 41 51 cacheTime = currentTime 52 + menuCache.save(menu) 42 53 Result.success(menu) 43 54 } catch (e: retrofit2.HttpException) { 55 + // Return stale cache if available on network error 56 + diskCached?.menu?.let { return@withLock Result.success(it) } 44 57 Result.failure(ChucksError.NetworkError) 45 58 } catch (e: java.io.IOException) { 59 + diskCached?.menu?.let { return@withLock Result.success(it) } 46 60 Result.failure(ChucksError.NetworkError) 47 61 } catch (e: com.squareup.moshi.JsonDataException) { 48 62 Result.failure(ChucksError.DecodingError(e)) ··· 78 92 } 79 93 } 80 94 81 - override fun invalidateCache() { 82 - cachedMenu = null 83 - cacheTime = 0 95 + override suspend fun invalidateCache() { 96 + mutex.withLock { 97 + cachedMenu = null 98 + cacheTime = 0 99 + menuCache.clear() 100 + } 84 101 } 85 102 }
+15 -1
android/app/src/main/java/com/wasupchucks/di/NetworkModule.kt
··· 1 1 package com.wasupchucks.di 2 2 3 + import android.content.Context 3 4 import com.squareup.moshi.Moshi 4 5 import com.wasupchucks.data.api.ChucksApiInterceptor 5 6 import com.wasupchucks.data.api.ChucksApiService 6 7 import dagger.Module 7 8 import dagger.Provides 8 9 import dagger.hilt.InstallIn 10 + import dagger.hilt.android.qualifiers.ApplicationContext 9 11 import dagger.hilt.components.SingletonComponent 12 + import okhttp3.Cache 10 13 import okhttp3.OkHttpClient 11 14 import okhttp3.logging.HttpLoggingInterceptor 12 15 import retrofit2.Retrofit 13 16 import retrofit2.converter.moshi.MoshiConverterFactory 17 + import java.io.File 14 18 import java.util.concurrent.TimeUnit 15 19 import javax.inject.Singleton 16 20 ··· 20 24 21 25 private const val BASE_URL = "https://diningdata.cedarville.edu/api/" 22 26 private const val TIMEOUT_SECONDS = 30L 27 + private const val CACHE_SIZE_BYTES = 10L * 1024 * 1024 // 10 MB 23 28 24 29 @Provides 25 30 @Singleton ··· 30 35 31 36 @Provides 32 37 @Singleton 38 + fun provideHttpCache(@ApplicationContext context: Context): Cache { 39 + val cacheDir = File(context.cacheDir, "http_cache") 40 + return Cache(cacheDir, CACHE_SIZE_BYTES) 41 + } 42 + 43 + @Provides 44 + @Singleton 33 45 fun provideOkHttpClient( 34 - chucksApiInterceptor: ChucksApiInterceptor 46 + chucksApiInterceptor: ChucksApiInterceptor, 47 + cache: Cache 35 48 ): OkHttpClient { 36 49 return OkHttpClient.Builder() 50 + .cache(cache) 37 51 .addInterceptor(chucksApiInterceptor) 38 52 .addInterceptor(HttpLoggingInterceptor().apply { 39 53 level = HttpLoggingInterceptor.Level.BASIC