Part 7: Implementing RoomDB for the saving Bookmark in local storage

Mohit Damke
4 min readJul 4, 2024

--

Here we have implemented RoomDB for storing news locally in app in the bookmark section

  • We have to create a NewsDao
package com.example.newsapp.data.local

import androidx.lifecycle.LiveData
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import com.example.newsapp.domain.model.Article
import kotlinx.coroutines.flow.Flow


@Dao
interface NewsDao {

@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun upsert(article: Article)

@Delete
suspend fun delete(article: Article)

@Query("SELECT * FROM Article")
fun getArticles(): Flow<List<Article>>

@Query("SELECT * FROM Article WHERE url=:url")
suspend fun getArticle(url: String): Article?


}
  • First we have to convert
package com.example.newsapp.data.local

import androidx.room.ProvidedTypeConverter
import androidx.room.TypeConverter
import com.example.newsapp.domain.model.Source


@ProvidedTypeConverter
class NewsTypeConverter {


@TypeConverter
fun sourceToString(source: Source): String{
return "${source.id},${source.name}"
}

@TypeConverter
fun stringToSource(source: String): Source{
return source.split(',').let { sourceArray ->
Source(id = sourceArray[0], name = sourceArray[1])
}
}
}
  • We have to create News Database file for the implementation of the room database
package com.example.newsapp.data.local

import androidx.room.Database
import androidx.room.RoomDatabase
import androidx.room.TypeConverters
import com.example.newsapp.domain.model.Article

@Database(entities = [Article::class],version = 1,)
@TypeConverters(NewsTypeConverter::class)
abstract class NewsDatabase : RoomDatabase() {

abstract val newsDao: NewsDao

}
  • We have to provide it in the App Module
@Provides
@Singleton
fun provideNewsDatabase(
application: Application
): NewsDatabase {
return Room.databaseBuilder(
context = application,
klass = NewsDatabase::class.java,
name = NEW_DATABASE
).addTypeConverter(NewsTypeConverter())
.fallbackToDestructiveMigration()
.build()

}

@Provides
@Singleton
fun provideNewsDao(
newsDatabase: NewsDatabase
): NewsDao {
return newsDatabase.newsDao
}


}

Make Sure to app the Primary key

@Parcelize
@Entity
data class Article(
val author: String,
val content: String,
val description: String,
val publishedAt: String,
val source: Source,
val title: String,
@PrimaryKey val url: String,
val urlToImage: String
): Parcelable
package com.example.newsapp.domain.model

import android.os.Parcelable
import kotlinx.parcelize.Parcelize


@Parcelize
data class Source(
val id: String,
val name: String
): Parcelable

This is how you can make the Detail Screen

Lets Build Bookmark Screen

  • We have to make the use case for Upsert, Delete, Get Article, Get Articles
package com.example.newsapp.domain.usecases.news

import com.example.newsapp.data.local.NewsDao
import com.example.newsapp.domain.model.Article
import com.example.newsapp.domain.repository.NewsRepository

class UpsertArticle(
private val newsRepository: NewsRepository
) {

suspend operator fun invoke(article: Article){
newsRepository.upsertArticle(article = article)
}

}
package com.example.newsapp.domain.usecases.news

import com.example.newsapp.data.local.NewsDao
import com.example.newsapp.domain.model.Article
import com.example.newsapp.domain.repository.NewsRepository

class DeleteArticle (
private val newsRepository: NewsRepository
) {

suspend operator fun invoke(article: Article){
newsRepository.deleteArticle(article = article)
}

}
package com.example.newsapp.domain.usecases.news

import com.example.newsapp.data.local.NewsDao
import com.example.newsapp.domain.model.Article
import com.example.newsapp.domain.repository.NewsRepository

class SelectArticle(
private val newsRepository: NewsRepository
) {
suspend operator fun invoke(url: String): Article? {
return newsRepository.getArticle(url)
}
}
package com.example.newsapp.domain.usecases.news

import com.example.newsapp.data.local.NewsDao
import com.example.newsapp.domain.model.Article
import com.example.newsapp.domain.repository.NewsRepository
import kotlinx.coroutines.flow.Flow

class SelectArticles(
private val newsRepository: NewsRepository
) {

operator fun invoke(): Flow<List<Article>> {
return newsRepository.getArticles()
}

}
  • Make sure to add this in the data class
data class NewsUseCases(
val getNews: GetNews,
val searchNews: SearchNews,
val upsertArticle: UpsertArticle,
val deleteArticle: DeleteArticle,
val selectArticles: SelectArticles,
val selectArticle: SelectArticle,
)

Provide it in a App Module

@Provides
@Singleton
fun provideNewsUseCases(
newsRepository: NewsRepository,
): NewsUseCases {
return NewsUseCases(
getNews = GetNews(newsRepository),
searchNews = SearchNews(newsRepository),
upsertArticle = UpsertArticle(newsRepository),
deleteArticle = DeleteArticle(newsRepository),
selectArticles = SelectArticles(newsRepository),
selectArticle = SelectArticle(newsRepository),
)
}
  • Create a Bookmark State
package com.example.newsapp.presentation.bookmark

import com.example.newsapp.domain.model.Article

data class BookmarkState(
val articles: List<Article> = emptyList(),

)
  • After that we have to create a viewmodel for bookmark
package com.example.newsapp.presentation.bookmark

import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.example.newsapp.domain.usecases.news.NewsUseCases
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import javax.inject.Inject

@HiltViewModel
class BookmarkViewModel @Inject constructor(
private val newsUseCases: NewsUseCases
) : ViewModel()
{

private val _state = mutableStateOf(BookmarkState())
val state: State<BookmarkState> = _state

init {
getArticles()
}

private fun getArticles() {
newsUseCases.selectArticles().onEach {
_state.value = _state.value.copy(articles = it.reversed())
}.launchIn(viewModelScope)
}
}
  • We have to create new function as
@Composable
fun ArticlesList(
modifier: Modifier = Modifier,
articles: List<Article>,
onClick: (Article) -> Unit
)
package com.example.newsapp.presentation.common

import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.paging.LoadState
import androidx.paging.compose.LazyPagingItems
import com.example.newsapp.domain.model.Article
import com.example.newsapp.presentation.Dimens.ExtraSmallPadding2
import com.example.newsapp.presentation.Dimens.MediumPadding1
import com.example.newsapp.presentation.Dimens.MediumPadding2


@Composable
fun ArticlesList(
modifier: Modifier = Modifier,
articles: List<Article>,
onClick: (Article) -> Unit
) {
if (articles.isEmpty()){
EmptyScreen()
}
LazyColumn(
modifier = modifier.fillMaxSize(),
verticalArrangement = Arrangement.spacedBy(MediumPadding1),
contentPadding = PaddingValues(all = ExtraSmallPadding2)
) {
items(
count = articles.size,
) {
articles[it].let { article ->
ArticleCard(article = article, onClick = { onClick(article) })
}
}
}

}



@Composable
fun ArticlesList(
modifier: Modifier = Modifier,
articles: LazyPagingItems<Article>,
onClick: (Article) -> Unit
) {

val handlePagingResult = handlePagingResult(articles = articles)


if (handlePagingResult) {
LazyColumn(
modifier = modifier.fillMaxSize(),
verticalArrangement = Arrangement.spacedBy(MediumPadding1),
contentPadding = PaddingValues(all = ExtraSmallPadding2)
) {
items(
count = articles.itemCount,
) {
articles[it]?.let { article ->
ArticleCard(article = article, onClick = { onClick(article) })
}
}
}
}
}

@Composable
fun handlePagingResult(
modifier: Modifier = Modifier,
articles: LazyPagingItems<Article>
): Boolean {

val loadState = articles.loadState
val error = when {
loadState.refresh is LoadState.Error -> loadState.refresh as LoadState.Error
loadState.prepend is LoadState.Error -> loadState.prepend as LoadState.Error
loadState.append is LoadState.Error -> loadState.append as LoadState.Error
else -> null
}

return when {
loadState.refresh is LoadState.Loading -> {
ShimmerEffect()
false
}

else -> {
true
}
}


}

@Composable
private fun ShimmerEffect(modifier: Modifier = Modifier) {
Column(verticalArrangement = Arrangement.spacedBy(MediumPadding2)) {
repeat(10) {
ArticleCardShimmerEffect(
modifier = Modifier.padding(horizontal = MediumPadding2)
)
}
}
}
  • Hence we have successfully implemented the detail screen.

If you have not read Part 6 of the series the here is the link below
https://medium.com/@mohitrdamke/part-6-implementing-news-detail-screen-in-app-c522856aa74b

You can visit the introduction article for the news app with the link below
https://medium.com/@mohitrdamke/news-app-clean-code-architecture-paging-room-db-in-android-studio-24919ba7d16a


You can add 50 clap by just pressing on clap icon

Visit my GitHub Repository : https://github.com/mohitdamke/NewsApp
Make sure to follow me on
Link-tree :
https://linktr.ee/MohitDamke01
LinkedIn :
https://www.linkedin.com/in/mohitdamke01

aa

--

--

Mohit Damke

Junior Android Developer | Kotlin | Jetpack | Firebase | Android Studio | MVVM & Clean Code Architecture