Search icon CANCEL
Subscription
0
Cart icon
Your Cart (0 item)
Close icon
You have no products in your basket yet
Save more on your purchases! discount-offer-chevron-icon
Savings automatically calculated. No voucher code required.
Arrow left icon
Explore Products
Best Sellers
New Releases
Books
Videos
Audiobooks
Learning Hub
Newsletter Hub
Free Learning
Arrow right icon
timer SALE ENDS IN
0 Days
:
00 Hours
:
00 Minutes
:
00 Seconds
Arrow up icon
GO TO TOP
Mastering Kotlin for Android 14

You're reading from   Mastering Kotlin for Android 14 Build powerful Android apps from scratch using Jetpack libraries and Jetpack Compose

Arrow left icon
Product type Paperback
Published in Apr 2024
Publisher Packt
ISBN-13 9781837631711
Length 370 pages
Edition 1st Edition
Languages
Tools
Arrow right icon
Author (1):
Arrow left icon
Harun Wangereka Harun Wangereka
Author Profile Icon Harun Wangereka
Harun Wangereka
Arrow right icon
View More author details
Toc

Table of Contents (22) Chapters Close

Preface 1. Part 1: Building Your App FREE CHAPTER
2. Chapter 1: Get Started with Kotlin Android Development 3. Chapter 2: Creating Your First Android App 4. Chapter 3: Jetpack Compose Layout Basics 5. Chapter 4: Design with Material Design 3 6. Part 2: Using Advanced Features
7. Chapter 5: Architect Your App 8. Chapter 6: Network Calls with Kotlin Coroutines 9. Chapter 7: Navigating within Your App 10. Chapter 8: Persisting Data Locally and Doing Background Work 11. Chapter 9: Runtime Permissions 12. Part 3: Code Analysis and Tests
13. Chapter 10: Debugging Your App 14. Chapter 11: Enhancing Code Quality 15. Chapter 12: Testing Your App 16. Part 4: Publishing Your App
17. Chapter 13: Publishing Your App 18. Chapter 14: Continuous Integration and Continuous Deployment 19. Chapter 15: Improving Your App 20. Index 21. Other Books You May Enjoy

Saving and reading data from a local database

We are going to build up on the Pets app, which displays a list of cute cats. We will save our cute cats in a local database, Room, which is a part of the Android Jetpack libraries and provides a wrapper and abstraction layer over SQLite. We will also use the repository pattern to abstract away the data source from ViewModel. The Room database provides an abstraction layer over SQLite to allow fluent database access while harnessing the full power of SQLite. It also has inbuilt support for Kotlin coroutines and flows to allow for asynchronous database access. Room is also compile-time safe and hence any errors in SQL queries are caught at compile time. It allows us to do all this with concise code.

To use Room in our project, we need to add its dependency to our libs.versions.toml file. Let us start by defining the Room version in the versions section as the following:

room = "2.5.2"

Next, let us add the dependencies in our libraries section:

room-runtime = { module = "androidx.room:room-runtime" , version.ref = "room" }
room-compiler = { module = "androidx.room:room-compiler", version.ref = "room" }
room-ktx = { module = "androidx.room:room-ktx", version.ref = "room" }

Sync the project for the changes to be added. Before we add these dependencies to the app level build.gradle.kts file, we need to set up an annotation processor for the room compiler. Room uses the annotation processor to generate the code that will be used to read, write, update, and delete data from the database. To do this, we need to add the following to the plugins section of the project level build.gradle.kts file:

id("com.google.devtools.ksp") version "1.9.0-1.0.13" apply false

We have added the Kotlin Symbol Processing (KSP) plugin to our project. This is a new annotation processing tool that is faster than the Kotlin Annotation Processing Tool (KAPT). KSP analyses the Kotlin code directly and has a better understanding of the Kotlin language constructs. KSP is now the recommended annotation processing tool for Kotlin. Next, we need to add KSP to our app level build.gradle.kts file:

id("com.google.devtools.ksp")

This allows us to use KSP in our app module. To finalize setting up Room, now let us add the dependencies we declared earlier to the app level build.gradle.kts file:

implementation(libs.room.runtime)
implementation(libs.room.ktx)
ksp(libs.room.compiler)

We have added our Room dependencies and the Room KTX library with the implementation configuration and the Room compiler with the ksp configuration. We are now ready to start using Room in our project. Let us start by creating an entity class for our Cat object. This will be the data class that will be used to store our pets in the database. Inside the data package, create a new file called CatEntity.kt and add the following code:

@Entity(tableName = "Cat")
data class CatEntity(
    @PrimaryKey
    val id: String,
    val owner: String,
    val tags: List<String>,
    val createdAt: String,
    val updatedAt: String
)

This data class represents the Room table for our cats. The @Entity annotation is used to define the table for our cats. We have passed the tableName value to specify the name of our table. The @PrimaryKey annotation is used to define the primary key for our table. The other properties are the columns in our table. One thing to keep in mind is that Room needs type converters to save fields such as tags, which is a list of strings. Room provides functionality to save non-primitive types using the @TypeConverter annotation. Let us create a new file named PetsTypeConverters.kt and add the following code:

class PetsTypeConverters {
    @TypeConverter
    fun convertTagsToString(tags: List<String>): String {
        return Json.encodeToString(tags)
    }
    @TypeConverter
    fun convertStringToTags(tags: String): List<String> {
        return Json.decodeFromString(tags)
    }
}

This class has two functions annotated with the @TypeConverter annotation. The first function converts a list of strings to a string. The second function converts a string to a list of strings. We have used the Kotlinx serialization library to convert the list of strings to a string and vice versa. This class will be referenced in our database class that we will create shortly.

We are now ready to create our database. We need to create a Data Access Object (DAO) to access our database. A DAO is an interface that defines the methods to create, read, update, and delete values from our database. Inside the data package, create a new file called CatDao.kt and add the following code:

@Dao
interface CatDao {
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insert(catEntity: CatEntity)
    @Query("SELECT * FROM Cat")
    fun getCats(): Flow<List<CatEntity>>
}

The interface is annotated with the @Dao annotation to tell Room that we will use this class as our DAO. We have defined two functions in our DAO. The insert function is used to insert a cat into our database. Notice that this is a suspend function. This is because we will be using coroutines to insert the cats into our database. Inserting items into the database needs to happen on a background thread since it is a resource-intensive operation. We also use the @Insert annotation with the onConflict parameter set to OnConflictStrategy.REPLACE. This tells Room to replace the cat if it already exists in the database. The getCats function is used to get all the cats from our database. It has the @Query annotation, which is used to define a query to get the cats from our database. We are using Flow to return the cats from our database. Flow is a stream of data that can be observed. This means that every time we update the database, the changes will be emitted to the view layers immediately without us doing any extra work. Cool, right?

We now need to create our database class. Inside the data package, create a new file called CatDatabase.kt and add the following code:

@Database(
    entities = [CatEntity::class],
    version = 1
)
@TypeConverters(PetsTypeConverters::class)
abstract class CatDatabase: RoomDatabase() {
    abstract fun catDao(): CatDao
}

We have defined an abstract class that extends the RoomDatabase class. We passed the entities parameter to specify the entities or tables stored in our database. We have also passed the version parameter to specify the version of our database. We have used the @TypeConverters annotation to specify the type converters that we will be using in our database. We have also defined an abstract method that returns our CatDao. We need to provide an instance of the database to classes that need it. We will do this by using the dependency injection pattern we have been using in our project. Let us head over to the di package and in the Module.kt file, add the Room dependency just below the Retrofit dependency:

single {
    Room.databaseBuilder(
        androidContext(),
        CatDatabase::class.java,
        "cat-database"
    ).build()
}
single { get<CatDatabase>().carDao() }

First, we have created a single instance of our database. We have used the databaseBuilder method to create our database. We have passed the androidContext() method from Koin to get the context of our application. We have also passed CatDatabase::class.java to specify the class of our database. We have also passed the name of our database. We have then created a single instance of our CatDao. We are using the get method to get the instance of our database and then calling the catDao function to get our CatDao.

Our database is now ready to be used in our repository. We are going to modify PetRepository and its implementation to be able to do the following:

  • Save items to our database
  • Read items from our database
  • Change our getPets() function to return a Flow of pets

The modified PetRepository.kt file should look like the following:

interface PetsRepository {
    suspend fun getPets(): Flow<List<Cat>>
    suspend fun fetchRemotePets()
}

We have modified the getPets function to return a Flow of pets. Room does not allow database access on the main thread, therefore, our queries have to be asynchronous. Room provides support for observable queries that read data from our database every time data in our database changes and emits new values to reflect the changes. This is the reason we return a Flow instance type from the getPets function. We have also added the fetchRemotePets function to fetch the pets from the remote data source. Let us now modify PetRepositoryImpl.kt with a few changes:

class PetsRepositoryImpl(
    private  val catsAPI: CatsAPI,
    private val dispatcher: CoroutineDispatcher,
    private val catDao: CatDao
): PetsRepository {
    override suspend fun getPets(): Flow<List<Cat>> {
        return withContext(dispatcher) {
           catDao.getCats()
               .map { petsCached ->
                   petsCached.map { catEntity ->
                       Cat(
                           id = catEntity.id,
                           owner = catEntity.owner,
                           tags = catEntity.tags,
                           createdAt = catEntity.createdAt,
                           updatedAt = catEntity.updatedAt
                       ) }
               }
               .onEach {
                     if (it.isEmpty()) {
                          fetchRemotePets()
                     }
               }
        }
    }
    override suspend fun fetchRemotePets() {
        withContext(dispatcher) {
            val response = catsAPI.fetchCats("cute")
            if (response.isSuccessful) {
                response.body()!!.map {
                    catDao.insert(CatEntity(
                        id = it.id,
                        owner = it.owner,
                        tags = it.tags,
                        createdAt = it.createdAt,
                        updatedAt = it.updatedAt
                    ))
                }
            }
        }
    }
}

We have made the following changes:

  • We have added the catDao property to the constructor of the class.
  • We have modified the getPets function to return a Flow of pets. Additionally, we have added a map operator to map CatEntity to a Cat object. We have also added an onEach operator to check if the list of pets is empty. If it is empty, we call the fetchRemotePets function to fetch the pets from the remote data source. This provides an offline first experience to our users; that is, we first check if we have the data in our database and if we don’t, we fetch it from the remote data source.
  • Lastly, we have modified the fetchRemotePets function that fetches the pets from the remote data source. When the response is successful, we map the response to a CatEntity instance type and insert it into our database.

We need to update the PetsRepository dependency in our Module.kt file to add the CatDao dependency:

single<PetsRepository> { PetsRepositoryImpl(get(), get(), get()) }

In our PetsRepositoryImpl class, we have been able to read and fetch data from the Room database. Next, we are going to modify the getPets() function in PetsViewModel to accommodate these new changes. Head over to the PetsViewModel.kt file and modify the getPets() function to look like the following:

private fun getPets() {
    petsUIState.value = PetsUIState(isLoading = true)
    viewModelScope.launch {
        petsRepository.getPets().asResult().collect { result ->
            when (result ) {
                is NetworkResult.Success -> {
                    petsUIState.update {
                        it.copy(isLoading = false, pets = result.data)
                    }
                }
                is NetworkResult.Error -> {
                    petsUIState.update {
                        it.copy(isLoading = false, error = result.error)
                    }
                }
            }
        }
    }
}

We have made a few minor changes. We have used the asResult() extension function to convert the Flow of pets to a Flow of NetworkResult. This is because we are now returning a Flow of pets from our repository. The rest of the code remains the same as before. We will get an error since we have not created the asResult() extension function. Let us create it in our NetworkResult.kt file:

fun <T> Flow<T>.asResult(): Flow<NetworkResult<T>> {
    return this
        .map<T, NetworkResult<T>> {
            NetworkResult.Success(it)
        }
        .catch { emit(NetworkResult.Error(it.message.toString())) }
}

This is an extension function on the Flow class. It maps a Flow of items to the NetworkResult class. We can now head back to our PetsViewModel class and add the extension function imports to resolve the error.

The last change we need to make is to provide the application context to our Koin instance in the Application class. Head over to the ChapterEightApplication.kt file and modify the startKoin block to the following:

startKoin {
    androidContext(applicationContext)
    modules(appModules)
}

We have provided the application context to our Koin instance. Now, we can run the app. You should see the list of cute cats.

Figure 8.1 – Cute cats

Figure 8.1 – Cute cats

The app still works as before, but now we are reading items from the Room database. If you turn off your data and Wi-Fi, the app still shows the list of cute cats! Amazing, isn’t it? We have been able to make the app work offline. One of the benefits of having an architecture in place for our app is that we can change the different layers without necessarily affecting the other layers. We have been able to change the data source from the remote data source to the local data source without affecting the view layer. This is the power of having a good architecture in place.

We know how to insert and read data from our Room database, but what about updating it? In the next section, we will learn how to update the data that is in our Room database. In the process, we will also learn how to migrate from one database version to the other using the Room automated migration feature.

lock icon The rest of the chapter is locked
Register for a free Packt account to unlock a world of extra content!
A free Packt account unlocks extra newsletters, articles, discounted offers, and much more. Start advancing your knowledge today.
Unlock this book and the full library FREE for 7 days
Get unlimited access to 7000+ expert-authored eBooks and videos courses covering every tech area you can think of
Renews at $19.99/month. Cancel anytime
Banner background image