Offline Caching in Android Studio

Overview

Hello World


Code

 plugins {
id 'com.android.application'
id 'kotlin-android'
id 'kotlin-kapt'
id 'dagger.hilt.android.plugin'
}
 // Dagger Hilt
def hilt_version = "2.38.1"
implementation "com.google.dagger:hilt-android:$hilt_version"
kapt "com.google.dagger:hilt-compiler:$hilt_version"

// Retrofit
def retrofit_version = "2.9.0"
implementation "com.squareup.retrofit2:retrofit:$retrofit_version"
implementation "com.squareup.retrofit2:converter-gson:$retrofit_version"
implementation "com.squareup.okhttp3:logging-interceptor:4.9.1"

// Coroutines
def coroutines_version = "1.6.0"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"

// ViewModel and LiveData
def lifecycle_version = "2.4.0"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"

// AndroidX for Activity used for Dependency Injection
def activity_version = "1.4.0"
implementation "androidx.activity:activity-ktx:$activity_version"

// AndroidX for Fragment used for Dependency Injection
def fragment_version = "1.4.1"
implementation "androidx.fragment:fragment-ktx:$fragment_version"
debugImplementation "androidx.fragment:fragment-testing:$fragment_version"

// Room Database
def room_version = "2.4.0"
implementation "androidx.room:room-runtime:$room_version"
kapt "androidx.room:room-compiler:$room_version"
implementation "androidx.room:room-ktx:$room_version"
annotationProcessor "androidx.room:room-compiler:$room_version"

// Glide
def glide_version = "4.11.0"
implementation "com.github.bumptech.glide:glide:$glide_version"
annotationProcessor "com.github.bumptech.glide:compiler:$glide_version"
 /*
* Raj Kanchan
* * Data Resource: https://random-data-api.com/documentation
*
* */

@Entity(tableName = "restaurants")
data class Restaurant(
@PrimaryKey(autoGenerate = false) val uid: String,
val name: String,
val description: String,
val logo: String,
val address: String
)
 interface RestaurantApi {
companion object {
const val BASE_URL = "https://random-data-api.com/api/"
}

@GET("restaurant/random_restaurant?size=20")
suspend fun getRestaurants(): List<Restaurant>
}
 @Dao
interface RestaurantDao {

@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertRestaurant(restaurants: List<Restaurant>)

@Query("DELETE FROM restaurants")
suspend fun deleteAllRestaurants()

@Query("SELECT * FROM restaurants")
fun getRestaurants(): Flow<List<Restaurant>>
}
 @Database(entities = [Restaurant::class], version = 1)
abstract class RestaurantDatabase : RoomDatabase() {
abstract fun getRestaurantDao(): RestaurantDao
}
 class RestaurantRepository @Inject constructor(
private val restaurantApi: RestaurantApi,
private val restaurantDatabase: RestaurantDatabase
) {
private val restaurantDao = restaurantDatabase.getRestaurantDao()
fun getRestaurants() =
networkBoundResource(query = { restaurantDao.getRestaurants() }, fetch = {
delay(2000)
restaurantApi.getRestaurants()
}, saveFetchResult = { restaurants ->
restaurantDatabase.withTransaction {
restaurantDao.deleteAllRestaurants()
restaurantDao.insertRestaurant(restaurants)
}
})
}
 @Module
@InstallIn(SingletonComponent::class)
object AppModule {
@Provides
@Singleton
fun provideRetrofit(): Retrofit = Retrofit.Builder().baseUrl(RestaurantApi.BASE_URL)
.addConverterFactory(GsonConverterFactory.create()).build()

@Provides
@Singleton
fun provideRestaurants(retrofit: Retrofit): RestaurantApi =
retrofit.create(RestaurantApi::class.java)

@Provides
@Singleton
fun provideDatabase(app: Application): RestaurantDatabase =
Room.databaseBuilder(app, RestaurantDatabase::class.java, "restaurantDB").build()
}
 sealed class Resource<T>(
val data: T? = null,
val error: Throwable? = null
) {
class Success<T>(data: T) : Resource<T>()
class Loading<T>(data: T? = null) : Resource<T>()
class Error<T>(throwable: Throwable, data: T? = null) : Resource<T>(data, throwable)
}
 inline fun <ResultType, RequestType> networkBoundResource(
crossinline query: () -> Flow<ResultType>,
crossinline fetch: suspend () -> RequestType,
crossinline saveFetchResult: suspend (RequestType) -> Unit,
crossinline shouldFetch: (ResultType) -> Boolean = { true }
) = flow {
val data = query().first()
val flow = if (shouldFetch(data)) {
emit(Resource.Loading(data))
try {
saveFetchResult(fetch())
query().map { Resource.Success(it) }
} catch (throwable: Throwable) {
query().map { Resource.Error(throwable, it) }
}
} else {
query().map { Resource.Success(it) }
}
emitAll(flow)
}
 @HiltViewModel
class RestaurantViewModel @Inject constructor(restaurantRepository: RestaurantRepository) :
ViewModel() {
private val restaurantMutableLiveData = MutableLiveData<List<Restaurant>>()
val restaurants = restaurantRepository.getRestaurants().asLiveData()
}
 class RestaurantAdapter :
ListAdapter<Restaurant, RestaurantAdapter.RestaurantViewHolder>(RestaurantComparator()) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RestaurantViewHolder {
val binding =
RestaurantItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return RestaurantViewHolder(binding)
}

override fun onBindViewHolder(holder: RestaurantViewHolder, position: Int) {
val currentItem = getItem(position)
if (currentItem != null) {
holder.bind(currentItem)
}
}

class RestaurantViewHolder(private val binding: RestaurantItemBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(restaurant: Restaurant) {
binding.apply {
Glide.with(itemView).load(restaurant.logo).into(imgRestaurantLogo)
tvRestaurantTitle.text = restaurant.name
tvRestaurantDescription.text = restaurant.description
tvRestaurantAddress.text = restaurant.address
}
}
}

class RestaurantComparator : DiffUtil.ItemCallback<Restaurant>() {
override fun areItemsTheSame(oldItem: Restaurant, newItem: Restaurant) =
oldItem.uid == newItem.uid

override fun areContentsTheSame(oldItem: Restaurant, newItem: Restaurant) =
oldItem == newItem
}
}
 @AndroidEntryPoint
class RestaurantActivity : AppCompatActivity() {
private val restaurantViewModel: RestaurantViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding = ActivityRestaurantBinding.inflate(layoutInflater)
setContentView(binding.root)
val restaurantAdapter = RestaurantAdapter()
binding.apply {
recyclerViewRestaurant.apply {
adapter = restaurantAdapter
layoutManager = LinearLayoutManager(this@RestaurantActivity)
}

restaurantViewModel.restaurants.observe(this@RestaurantActivity) { result ->
restaurantAdapter.submitList(result.data)
progressBar.isVisible = result is Resource.Loading && result.data.isNullOrEmpty()
tvError.isVisible = result is Resource.Error && result.data.isNullOrEmpty()
tvError.text = result.error?.localizedMessage
}
}
}
}
 @HiltAndroidApp
class MainApplication : Application() {
}
 <?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="120dp"
android:padding="20dp">

<ImageView
android:id="@+id/imgRestaurantLogo"
android:layout_width="80dp"
android:layout_height="80dp"
android:src="@drawable/ic_launcher_background"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />

<TextView
android:id="@+id/tvRestaurantTitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="20dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/imgRestaurantLogo"
app:layout_constraintTop_toTopOf="@id/imgRestaurantLogo" />

<TextView
android:id="@+id/tvRestaurantDescription"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
app:layout_constraintEnd_toEndOf="@+id/tvRestaurantTitle"
app:layout_constraintStart_toStartOf="@+id/tvRestaurantTitle"
app:layout_constraintTop_toBottomOf="@+id/tvRestaurantTitle" />

<TextView
android:id="@+id/tvRestaurantAddress"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
app:layout_constraintEnd_toEndOf="@+id/tvRestaurantTitle"
app:layout_constraintStart_toStartOf="@+id/tvRestaurantTitle"
app:layout_constraintTop_toBottomOf="@+id/tvRestaurantDescription" />

</androidx.constraintlayout.widget.ConstraintLayout>
 <?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".features.restaurants.RestaurantActivity">

<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerViewRestaurant"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:padding="5dp"
tools:listitem="@layout/restaurant_item" />

<ProgressBar
android:id="@+id/progressBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:visibility="visible" />

<TextView
android:id="@+id/tvError"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:text="Error Message"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:visibility="visible" />

</androidx.constraintlayout.widget.ConstraintLayout>

Reference



Comments

Popular posts from this blog

Architecture Components in Android

DataBinding in Android

SSLSocketFactory in Android