¿Cómo implementar el almacenamiento en caché sin conexión usando NetworkBoundResource en Android?

Casi todas las aplicaciones de Android que requieren obtener datos a través de una red necesitan almacenamiento en caché . Primero, comprendamos ¿Qué significa almacenamiento en caché? La mayoría de nosotros hemos utilizado aplicaciones que requieren que los datos se obtengan de la web. Una aplicación de este tipo con una arquitectura sin conexión primero siempre intentará obtener los datos del almacenamiento local. Por otro lado, si hay alguna falla, solicita que los datos se obtengan de una red, y luego los almacena localmente, para una futura recuperación. Los datos se almacenarán en una base de datos SQLite . La ventaja de una arquitectura de este tipo es que podremos usar la aplicación incluso si está fuera de línea. Además, dado que los datos se almacenan en caché, la aplicación responderá más rápido. Para manejar el almacenamiento en caché, usaremosRecurso vinculado a la red. Es una clase auxiliar que decide cuándo usar los datos de caché y cuándo obtener datos de la web y actualizar la vista. Se coordina entre los dos.

El árbol de decisión anterior muestra el algoritmo para el algoritmo NetworkBound Resource. 

el algoritmo

Veamos el flujo de este algoritmo: 

  • Cada vez que el usuario accede a la aplicación en modo fuera de línea, los datos se envían a la vista, ya sea un fragmento o una actividad.
  • Si no hay datos o los datos son insuficientes en el disco como caché, entonces debería obtener los datos a través de la red.
  • Comprueba si es necesario iniciar sesión (si el usuario cierra la sesión, será necesario volver a iniciar sesión). Se vuelve a autenticar, si tiene éxito, obtiene los datos, pero falla, luego le solicita al usuario que se vuelva a autenticar.
  • Una vez que las credenciales coinciden, obtiene los datos a través de la red.
  • Si la fase de obtención falla, se le pregunta al usuario.
  • De lo contrario, si tiene éxito, los datos se almacenan automáticamente en el almacenamiento local. Luego refresca la vista.

El requisito aquí es que debe haber cambios mínimos en la experiencia del usuario cuando el usuario accede al modo en línea. Por lo tanto, el proceso como la reautenticación, la obtención de datos a través de la red y la actualización de las vistas deben realizarse en segundo plano. Una cosa a tener en cuenta aquí es que el usuario solo necesita volver a iniciar sesión, si hay algunos cambios en las credenciales del usuario, como la contraseña o el nombre de usuario.

Implementación

Para entender más sobre esto, construyamos una aplicación. Esta es una aplicación de noticias simple, que utiliza una API falsa para obtener datos de la web. Veamos el diseño de alto nivel de nuestra aplicación:

  1. Utilizará la arquitectura MVVM .
  2. Base de datos SQLite para almacenar datos en caché.
  3. Usa Kotlin FLow. ( Kotlin Coroutine )
  4. Dagger Hilt para inyección de dependencia.

El diagrama anterior es la descripción general de la arquitectura que se implementará en nuestra aplicación. Android recomienda esta arquitectura para desarrollar una aplicación moderna de Android con una buena arquitectura. Comencemos a construir el proyecto.

Implementación paso a paso

Paso 1: crear un nuevo proyecto

Para crear un nuevo proyecto en Android Studio, consulte Cómo crear/iniciar un nuevo proyecto en Android Studio . Tenga en cuenta que seleccione Kotlin como lenguaje de programación.

Paso 2: Configuración del diseño

Siempre se recomienda configurar primero el diseño y luego implementar la lógica. Así que primero crearemos el diseño. Como se mencionó, vamos a obtener datos de un servicio web. Dado que este es un proyecto de muestra, solo obtendríamos datos de un generador de datos aleatorios. Ahora los datos son una lista de autos, que incluiría las siguientes propiedades:

  1. Marca y modelo de coche
  2. transmisión del coche
  3. color del coche
  4. Tipo de conducción del coche.
  5. Tipo de combustible del coche.
  6. Tipo de coche del coche.

Usaremos RecyclerView para mostrar la lista. Por lo tanto, primero se requiere diseñar cómo se vería cada elemento de la lista. Seguido haciendo la lista.

XML

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_margin="4dp">
      
      <!-- This will display the make and model of the car-->
    <TextView
        android:id="@+id/car_name"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="8dp"
        android:textColor="@color/black"
        android:textSize="15sp"
        tools:text="Car Name" />
    
    <!-- This will display the transmission type of the car-->
    <TextView
        android:id="@+id/car_transmission"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentEnd="true"
        android:layout_marginStart="16dp"
        android:layout_marginEnd="16dp"
        android:layout_toEndOf="@id/car_name"
        tools:text="Transmission type" />
      
      <!-- This will display the colour of the car-->
    <TextView
        android:id="@+id/car_color"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@id/car_name"
        android:layout_marginStart="8dp"
        android:layout_marginEnd="8dp"
        tools:text="Car colour" />
      
   <!-- This will display the drive type of the car-->
    <TextView
        android:id="@+id/car_drive_type"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@id/car_name"
        android:layout_marginStart="8dp"
        android:layout_marginEnd="8dp"
        android:layout_toEndOf="@id/car_color"
        tools:text="Car Drive Type" />
      
    <!-- This will display the fuel type of the car-->
    <TextView
        android:id="@+id/car_fuel_type"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@id/car_transmission"
        android:layout_marginStart="8dp"
        android:layout_marginEnd="8dp"
        android:layout_toEndOf="@id/car_drive_type"
        tools:text="Car fuel_type" />
      
      <!-- This will display the car type of the car-->
    <TextView
        android:id="@+id/car_car_type"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@id/car_transmission"
        android:layout_marginStart="8dp"
        android:layout_marginEnd="8dp"
        android:layout_toEndOf="@id/car_fuel_type"
        tools:text="Car Type" />
  
</RelativeLayout>

Ahora, codifiquemos el diseño de la lista:

XML

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout 
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".CarActivity">
      
      <!-- The recycler view-->
    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recycler_viewer"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:clipToPadding="false"
        android:padding="4dp"
        tools:listitem="@layout/carlist_item" />
      
      <!--Initially the app will fetch data from the
         web, hence a progress bar for that-->
    <ProgressBar
        android:id="@+id/progress_bar"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        android:visibility="invisible"
        tools:visibility="visible" />
    
    <!--If the application is not able to 
        fetch/ expose the data to the view-->
    <TextView
        android:id="@+id/text_view_error"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        android:layout_margin="8dp"
        android:gravity="center_horizontal"
        android:visibility="invisible"
        tools:text="Error Message"
        tools:visibility="visible" />
    
</RelativeLayout>

Paso 3: ahora vamos a crear el paquete API

CarListAPI.kt

Kotlin

package com.gfg.carlist.api
  
import com.gfg.carlist.data.CarList
import retrofit2.http.GET
  
interface CarListAPI {
    // Companion object to hold the base URL
    companion object{
        const val BASE_URL = "https://random-data-api.com/api/"
    }
    // The number of cars can be varied using the size.
    // By default it is kept at 20, but can be tweaked.
    // @GET annotation to make a GET request.
    @GET("vehicle/random_vehicle?size=20")
    // Store the data in a list.
    suspend fun getCarList() : List<CarList>
}

Paso 4: Implementación del módulo de la aplicación

Un módulo no es más que una clase de objeto, que proporciona un contenedor al código fuente de la aplicación. Encapsula modelos de datos asociados con una tarea. La arquitectura de Android sugiere hacer un uso mínimo de la lógica comercial en el modelo de vista, por lo tanto, la tarea de la aplicación comercial se representa en el módulo de la aplicación. Incluirá tres métodos:

  • Un método para llamar a la API a través de Retrofit
  • Un método para proporcionar la lista.
  • Un método para proporcionar la base de datos o más bien construir una base de datos.

AppModule.kt

Kotlin

package com.gfg.carlist.di
  
import android.app.Application
import androidx.room.Room
import com.gfg.carlist.api.CarListAPI
import com.gfg.carlist.data.CarListDatabase
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import javax.inject.Singleton
  
@Module
@InstallIn(SingletonComponent::class)
object AppModule {
  
    @Provides
    @Singleton
    fun provideRetrofit(): Retrofit =
        Retrofit.Builder()
            .baseUrl(CarListAPI.BASE_URL)
            .addConverterFactory(GsonConverterFactory.create())
            .build()
  
    @Provides
    @Singleton
    fun provideCarListAPI(retrofit: Retrofit): CarListAPI =
        retrofit.create(CarListAPI::class.java)
  
    @Provides
    @Singleton
    fun provideDatabase(app: Application): CarListDatabase =
        Room.databaseBuilder(app, CarListDatabase::class.java, "carlist_database")
            .build()
}

Paso 5: Creación de clase de datos

Hemos terminado con el manejo de la API, obteniendo los datos del servicio web, pero ¿dónde almacenar los datos? Vamos a crear una clase para almacenar los datos. Tenemos que crear una clase de datos. Si la aplicación solo buscara y expusiera datos, tendría un solo archivo de clase de datos. Pero aquí, tenemos que buscar, exponer y almacenar en caché los datos. Por lo tanto, ROOM entra en juego aquí. Entonces, en la clase de datos, tenemos que crear una entidad.

CarList.kt

Kotlin

package com.gfg.carlist.data
  
import androidx.room.Entity
import androidx.room.PrimaryKey
  
// Data Class to store the data
// Here the name of the table is "cars"
@Entity(tableName = "cars")
data class CarList(
    @PrimaryKey val make_and_model: String,
    val color: String,
    val transmission: String,
    val drive_type: String,
    val fuel_type: String,
    val car_type: String
)

Dado que estaríamos almacenando en caché los datos localmente, es necesario crear una base de datos.

CarListDatabase.kt

Kotlin

package com.gfg.carlist.data
  
import androidx.room.Database
import androidx.room.RoomDatabase
  
@Database(entities = [CarList::class], version = 1)
abstract class CarListDatabase : RoomDatabase() {
    abstract fun carsDao(): CarsDao
}

Como hemos creado una tabla, necesitamos algunas consultas para recuperar datos de la tabla. Esto se logra utilizando DAO o D ata A ccess O bject .

CochesDao.kt

Kotlin

package com.gfg.carlist.data
  
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import kotlinx.coroutines.flow.Flow
  
@Dao
interface CarsDao {
  
    // Query to fetch all the data from the
    // SQLite database
    // No need of suspend method here
    @Query("SELECT * FROM cars")
      
    // Kotlin flow is an asynchronous stream of values
    fun getAllCars(): Flow<List<CarList>>
  
    // If a new data is inserted with same primary key
    // It will get replaced by the previous one
    // This ensures that there is always a latest
    // data in the database
    @Insert(onConflict = OnConflictStrategy.REPLACE)
      
    // The fetching of data should NOT be done on the
    // Main thread. Hence coroutine is used
    // If it is executing on one one thread, it may suspend
    // its execution there, and resume in another one
    suspend fun insertCars(cars: List<CarList>)
  
    // Once the device comes online, the cached data
    // need to be replaced, i.e. delete it
    // Again it will use coroutine to achieve this task
    @Query("DELETE FROM cars")
    suspend fun deleteAllCars()
}

Una clase de repositorio para manejar datos del servicio web y los datos localmente.

CarListRepository.kt

Kotlin

package com.gfg.carlist.data
  
import androidx.room.withTransaction
import com.gfg.carlist.api.CarListAPI
import com.gfg.carlist.util.networkBoundResource
import kotlinx.coroutines.delay
import javax.inject.Inject
  
class CarListRepository @Inject constructor(
    private val api: CarListAPI,
    private val db: CarListDatabase
) {
    private val carsDao = db.carsDao()
  
    fun getCars() = networkBoundResource(
        
        // Query to return the list of all cars
        query = {
            carsDao.getAllCars()
        },
        
        // Just for testing purpose, 
          // a delay of 2 second is set.
        fetch = {
            delay(2000)
            api.getCarList()
        },
        
        // Save the results in the table.
        // If data exists, then delete it 
        // and then store.
        saveFetchResult = { CarList ->
            db.withTransaction {
                carsDao.deleteAllCars()
                carsDao.insertCars(CarList)
            }
        }
    )
}

Paso 6: trabajar en la interfaz de usuario

Recuerde en el Paso 1, creamos un RecyclerView para exponer la lista de autos. Pero el trabajo no está terminado hasta ahora. Necesitamos hacer un adaptador así como un ViewModel. Estas dos clases trabajan juntas para definir cómo se muestran nuestros datos.

CarAdapter.kt

Kotlin

package com.gfg.carlist.features.carlist
  
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.gfg.carlist.data.CarList
import com.gfg.carlist.databinding.CarlistItemBinding
  
class CarAdapter : ListAdapter<CarList, CarAdapter.CarViewHolder>(CarListComparator()) {
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CarViewHolder {
        val binding =
            CarlistItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
        return CarViewHolder(binding)
    }
  
    override fun onBindViewHolder(holder: CarViewHolder, position: Int) {
        val currentItem = getItem(position)
        if (currentItem != null) {
            holder.bind(currentItem)
        }
    }
  
    // View Holder class to hold the view
    class CarViewHolder(private val binding: CarlistItemBinding) :
        RecyclerView.ViewHolder(binding.root) {
        fun bind(carlist: CarList) {
            binding.apply {
                carName.text = carlist.make_and_model
                carTransmission.text = carlist.transmission
                carColor.text = carlist.color
                carDriveType.text = carlist.drive_type
                carFuelType.text = carlist.fuel_type
                carCarType.text = carlist.car_type
            }
        }
    }
  
    // Comparator class to check for the changes made.
    // If there are no changes then no need to do anything.
    class CarListComparator : DiffUtil.ItemCallback<CarList>() {
        override fun areItemsTheSame(oldItem: CarList, newItem: CarList) =
            oldItem.make_and_model == newItem.make_and_model
  
        override fun areContentsTheSame(oldItem: CarList, newItem: CarList) =
            oldItem == newItem
    }
}

CarListViewModel.kt

Kotlin

package com.gfg.carlist.features.carlist
  
import androidx.lifecycle.ViewModel
import androidx.lifecycle.asLiveData
import com.gfg.carlist.data.CarListRepository
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
  
// Using Dagger Hilt library to 
// inject the data into the view model
@HiltViewModel
class CarListViewModel @Inject constructor(
    repository: CarListRepository
) : ViewModel() {
    val cars = repository.getCars().asLiveData()
}

Finalmente, tenemos que crear una actividad para mostrar los datos del ViewModel. Recuerde, toda la lógica comercial debe estar presente en ViewModel y no en la actividad. La actividad tampoco debe contener los datos, porque cuando se inclina la pantalla, los datos se destruyen, por lo que aumenta el tiempo de carga. Por lo tanto, el propósito de la actividad es solo mostrar los datos.

CarActivity.kt

Kotlin

package com.gfg.carlist.features.carlist
  
import android.os.Bundle
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.isVisible
import androidx.recyclerview.widget.LinearLayoutManager
import com.gfg.carlist.databinding.ActivityCarBinding
import com.gfg.carlist.util.Resource
import dagger.hilt.android.AndroidEntryPoint
  
@AndroidEntryPoint
class CarActivity : AppCompatActivity() {
    
    // Helps to preserve the view
    // If the app is closed, then after 
      // reopening it the app will open
    // in a state in which it was closed
  
    // DaggerHilt will inject the view-model for us
    private val viewModel: CarListViewModel by viewModels()
  
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
  
        // The bellow segment would 
          // instantiate the activity_car layout
        // and will create a property for different
        // views inside it!
        val binding = ActivityCarBinding.inflate(layoutInflater)
        setContentView(binding.root)
  
        val carAdapter = CarAdapter()
  
        binding.apply {
            recyclerViewer.apply {
                adapter = carAdapter
                layoutManager = LinearLayoutManager(this@CarActivity)
            }
  
            viewModel.cars.observe(this@CarActivity) { result ->
                carAdapter.submitList(result.data)
  
                progressBar.isVisible = result is Resource.Loading<*> && result.data.isNullOrEmpty()
                textViewError.isVisible = result is Resource.Error<*> && result.data.isNullOrEmpty()
                textViewError.text = result.error?.localizedMessage
            }
        }
    }
}

Finalmente, hemos terminado con la parte de codificación. Después de construir con éxito el proyecto, la aplicación se vería así:

Producción: 

El siguiente video demuestra la aplicación.

Salida Explicación:

Enlace del proyecto : Haga clic aquí

Publicación traducida automáticamente

Artículo escrito por duttabhishek0 y traducido por Barcelona Geeks. The original can be accessed here. Licence: CCBY-SA

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *