Android Domain layer with Room Dao's and Repositories - android

My project has grown to point where it makes sense to implement a domain layer.
Reading the doc's I would implement a use case like so.
class FormatDateUseCase(userRepository: UserRepository) {
private val formatter = SimpleDateFormat(
userRepository.getPreferredDateFormat(),
userRepository.getPreferredLocale()
)
operator fun invoke(date: Date): String {
return formatter.format(date)
}
}
And invoke it like this.
class MyViewModel(formatDateUseCase: FormatDateUseCase) : ViewModel() {
init {
val today = Calendar.getInstance()
val todaysDate = formatDateUseCase(today)
/* ... */
}
}
Yet the Room examples i've come across led me to define my user repository like this.
class UserRepository(private val userDao: UserDao) {
fun insertUser(user: User) {
userDao.insert(user)
}
}
So now my FormatDateUseCase has a dependancy on UserDao that gets instantiated by my Room database.
So my question is there a cleaner way to do this than my approach here?
val viewModel: MyViewModel = MyViewModel(
FormatDateUseCase(
UserRepository(
myRoomDatabase.userDao
)
)
)

Related

Proper way to insert Retrofit data to Room and display it using ViewModel

What would be the proper (best practice) way to get data from a Retrofit api, insert it into a Room database, and display the data from Room in the Viewmodel? Should the operation of inserting the data from Retrofit to Room happen in the repository or another class? How should the Room data be returned to the viewmodel?
As of right now my code fetches data using Retrofit <- repository <- ViewModel <- Fragment
-- Using Hilt for di. Also using the same data class for Retrofit and Room entity
Any advice or implementation suggestions are appreciated
Repository:
class ItemRepository #Inject constructor(
private val api: ShopraApi
) {
suspend fun getItems() = api.getUserItemFeed()
}
Api:
interface ShopraApi {
companion object {
const val BASE_URL = "-"
}
#GET("getUserItemFeed.php?user_id=1")
suspend fun getUserItemFeed() : List<Item>
}
ViewModel:
#HiltViewModel
class ItemViewModel #Inject constructor(
itemRepository: ItemRepository
) : ViewModel() {
private val itemsLiveData = MutableLiveData<List<Item>>()
val items: LiveData<List<Item>> = itemsLiveData
init {
viewModelScope.launch {
itemsLiveData.value = itemRepository.getItems()
}
}
}
Entity:
#Entity(tableName = "item_table")
data class Item(
#PrimaryKey(autoGenerate = false)
#NonNull
val listing_id: Long,
val title: String,
val description: String
)
Dao:
#Dao
interface ItemDao {
#Query("SELECT * FROM item_table")
fun getItemsFromRoom(): LiveData<List<Item>>
#Insert(onConflict = IGNORE)
suspend fun insert(item: Item)
#Insert(onConflict = IGNORE)
suspend fun insertAllItems(itemRoomList: List<Item>)
}
ItemDatabase:
#Database(entities = [Item::class],version = 1)
abstract class ItemDatabase : RoomDatabase() {
abstract fun itemDao() : ItemDao
}
AppModule:
#Module
#InstallIn(SingletonComponent::class)
object AppModule {
#Provides
#Singleton
fun provideRetrofit(): Retrofit {
return Retrofit.Builder()
.baseUrl(ShopraApi.BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
}
#Provides
#Singleton
fun provideShopraApi(retrofit: Retrofit): ShopraApi {
return retrofit.create(ShopraApi::class.java)
}
#Provides
#Singleton
fun provideDatabase(app: Application) =
Room.databaseBuilder(app, ItemDatabase::class.java, "item_database")
.fallbackToDestructiveMigration()
.build()
#Provides
fun provideTaskDao(db: ItemDatabase) = db.itemDao()
}
EDIT Updated Repository:
class ItemRepository #Inject constructor(
private val api: ShopraApi,
private val itemDao: ItemDao
) {
fun loadItems(): LiveData<List<Item>> {
return liveData{
val getItems = api.getUserItemFeed()
Log.e("ItemRepository","The size of the item list is
${getItems.size}")
getItems.forEach {
item -> itemDao.insert(item)
}
val loadFromRoom = itemDao.getItemsFromRoom()
emitSource(loadFromRoom)
}
}
}
Updated ItemViewModel:
class ItemViewModel #Inject constructor(
itemRepository: ItemRepository
) : ViewModel() {
val items: LiveData<List<Item>> = itemRepository.loadItems()
}
I then call this code in my fragment
viewModel.items.observe(viewLifecycleOwner) { items ->
itemAdapter.submitList(items)
You have to do this in Repository and Repository is the "single source of truth" this is where you make the decision, when you get the data from network and save into localDB and load/expose from the localDB which is going to be the room persistence
You can use the utility class known as Network Bound Resource and there are alot implementation of network bound resource, this one is from official sample google's sample of network bound resource,
which is generic type abstract class use for this specific purpose but if you still wanna do it in the custom way, here is what you need to do.
1-> Make the network call and get the data from network and this data you are not going to expose to viewmodel
2-> once you receive the data insert into the localDB.
3-> Load or Expose your data from localDB to the viewmodel
Code Sample:
class ItemRepo(
val dao: Dao,
val api: Api
) {
fun loadItems(): LiveData<List<Item>> {
return liveData {
// get from network
val getFromNetwork = api.getList()
// save into local
getFromNetwork.forEach{
item ->
dao.insertItem(item)
}
//load from local
val loadFromLocal = dao.getAllItem()
emitSource(loadFromLocal)
}
}
}
You are gonna Inject Repository in the viewmodel and from there data will observed by view, with that you are only gonna get the data from localDB.
Note: This is just a sample you can further reuse it according to your use case for instance error handling, network handling like when network is not available or when you encounter the error what you should do.. things like that.

Compose and Room: Issue with initializing ViewModel with a repository

I am currently following the Android Room with a View codelab and trying to adopt it with Jetpack Compose. I am stuck in initializing the viewModel in a compose function.
The error I am getting:
None of the following functions can be called with the arguments supplied:
public inline fun <reified VM : ViewModel> viewModel(viewModelStoreOwner: ViewModelStoreOwner = ..., key: String? = ..., factory: ViewModelProvider.Factory? = ...): TypeVariable(VM) defined in androidx.lifecycle.viewmodel.compose
public fun <VM : ViewModel> viewModel(modelClass: Class<TypeVariable(VM)>, viewModelStoreOwner: ViewModelStoreOwner = ..., key: String? = ..., factory: ViewModelProvider.Factory? = ...): TypeVariable(VM) defined in androidx.lifecycle.viewmodel.compose
#Composable
fun WordBookApp() {
val context = LocalContext.current
val wordViewModel: WordViewModel by viewModel( // error here - viewModel
WordViewModelFactory((context.applicationContext as WordsApplication).repository)
)
val words: List<Word> by wordViewModel.allWords.observeAsState(listOf())
...
The View Model and the View Model Factory:
class WordViewModel(private val repository: WordRepository) : ViewModel() {
val allWords: LiveData<List<Word>> = repository.allWords.asLiveData()
fun insert(word: Word) = viewModelScope.launch {
repository.insert(word)
}
}
class WordViewModelFactory(private val repository: WordRepository) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(WordViewModel::class.java)) {
#Suppress("UNCHECKED_CAST")
return WordViewModel(repository) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
}
The other parts of the code:
class WordRepository(private val wordDao: WordDao) {
val allWords: Flow<List<Word>> = wordDao.getAlphabetizedWords()
#Suppress("RedundantSuspendModifier")
#WorkerThread
suspend fun insert(word: Word) {
wordDao.insert(word)
}
}
class WordsApplication : Application() {
private val database by lazy { WordRoomDatabase.getDatabase(this) }
val repository by lazy { WordRepository(database.wordDao()) }
}
#Database(entities = [Word::class], version = 1, exportSchema = false)
public abstract class WordRoomDatabase : RoomDatabase() {
abstract fun wordDao(): WordDao
companion object {
// Singleton prevents multiple instances of database opening at the
// same time.
#Volatile
private var INSTANCE: WordRoomDatabase? = null
fun getDatabase(context: Context): WordRoomDatabase {
// if the INSTANCE is not null, then return it,
// if it is, then create the database
return INSTANCE ?: synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
WordRoomDatabase::class.java,
"word_database"
).build()
INSTANCE = instance
// return instance
instance
}
}
}
}
AndroidMenifest.xml
<application
android:name=".WordsApplication"
....
Can anyone please help? Thanks!
Since you did not mention the question, I already started typing this. This may not pertain to your question, but perhaps you still should read this.
If the problem is that values are not being updated inside the viewmodel, - Initialising a viewmodel inside a composable, very bad idea.
You see the composables often recompose, where every line of code inside it is re-executed. Hence, if you initialize the viewmodel inside this composable like that, it will be re-initialised at every recomposition. Recompositions can take place theoretically even at the frame rate (even do in many cases). Hence, this is not how to declare variables inside the composable.
Ok so there's the remember composable to help you out with that. If you wrap the initializing statement with remember, it will not be re-initialised upon recompositions. However, it has its limitations. For example, if the composable gets destroyed, for example, if you swipe it off the screen, the remembered value is lost. remember is destroyed with the destruction of the composable enclosing it.
Hence, for small stuff like animations and all, it is ok to store variables inside the composables, but for important things, you should not trust this framework.
Hence, the best way would be to initialise the viewmodel in your main activity, then pass its methods and varibales around to composables. You can even pass the viewmodel itself around, but its not required most of the time.
Code:
#Composable
fun WordBookApp() {
val context = LocalContext.current
val wordViewModel: WordViewModel by remember {
viewModel( // error here - viewModel
WordViewModelFactory((context.applicationContext as WordsApplication).repository)
)
}
val words: List<Word> by wordViewModel.allWords.observeAsState(listOf())
...
Got a clue from the answer of #MARSK and fixed it. Moved the initialization of the view model to the onCreate() of the MainActivity, and passed it to the composable function. Working everything perfectly now!
Here is the code if anyone needs it in the future:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
...
val wordViewModel by viewModels<WordViewModel> {
WordViewModelFactory((this.applicationContext as WordsApplication).repository)
}
setContent {
WordBookApp(wordViewModel)
}
}
}
#Composable
fun WordBookApp(wordViewModel: WordViewModel) {
val words: List<Word> by wordViewModel.allWords.observeAsState(listOf())
...

Kotlin Room withTransaction in Repository or #Transaction in Dao

I have a question regarding Room and it’s withTransaction { } code block in combination with Koin.
I have a repository where I need to access a couple of DAOs at the same time. I wanted to work with a withTransaction { } so I wouldn’t clutter 1 DAO with references to other DAOs.
I’m not sure which object to inject in the constructor of my repository. The withTransaction{ } can only be accessed by getting the RoomDatabase. But having the RoomDatabase in my Repository means that I have access to all the DAOs connected to that RoomDatabase. I’m not sure what the best practice around this use-case would be.
Should I use the withTransaction { } and risk that all DAOs are accessible be that Repository or should I have the DAOs in my Repository's constructor and hand them to the `ReviewDao' to handle every insert?
An example would be something like this with withTransaction { }
class ReviewRepository(
private val roomDatabase: RoomDatabase
) {
private val reviewDao = roomDatabase.reviewDao()
private val userDao = roomDatabase.userDao()
suspend fun saveReview(reviewResponse: ReviewResponse) {
roomDatabase.withTransaction {
reviewDao.insert(reviewResponse.getAsEntity())
userDao.insert(reviewResponse.user.getAsEntity())
}
}
}
And an example without withTransaction { } would be this
class ReviewRepository(
private val reviewDao : ReviewDao,
private val userDao : UserDao
) {
suspend fun saveReview(reviewResponse: ReviewResponse) {
reviewDao.insertWithUser(reviewResponse.getAsEntity(), reviewResponse.user.getAsEntity(), userDao)
}
}
#Dao
interface ReviewDao {
#Transaction
suspend fun insertWithUser(review: Review, user: User, userDao: UserDao) {
insert(review)
userDao.insert(user)
}
}
The solution that worked for me was to use a separate class that takes in the database in the constructor and provides an extension to the withTransaction.
class TransactionProvider(
private val db: AppDatabase
) {
suspend fun <R> runAsTransaction(block: suspend () -> R): R {
return db.withTransaction(block)
}
}
I can then inject this class into the Repository I need without providing the complete database object to that Repository.
I would go with a variant of your Repository solution. In a project, we had UseCase and Handler structures. To simply put;
class SaveReviewHandler(private val db: RoomDatabase) {
private val reviewDao = db.reviewDao()
private val userDao = db.userDao()
suspend fun execute(useCase: SaveReview) {
db.withTransaction {
reviewDao.insert(useCase.review)
userDao.insert(useCase.user)
}
}
}
data class SaveReview(review: ReviewEntitry, user: UserEntity)
and we can call it as follows:
saveReviewHandler.execute(SaveReview(reviewResponse.getAsEntity(),reviewResponse.user.getAsEntity()))
One good side of this architecture is; your repository may have other complex transactions, but it does not have to have direct access to DAOs.

Android Room Database: Threadsafe usage of database properties

In order to support synchronization of time using NTP for time sensitive data, I've added a Volatile property to our RoomDatabase from where all update operations can request a "timestamp". But I was wondering if this is threadsafe in regards to using Room with suspending functions and transactions? Also not quite sure it actually has to be marked as Volatile as every access should produce a new java.time.Clock.
Example:
abstract class AppDatabase : RoomDatabase() {
abstract val chatRepository: ChatRepository
abstract val taskRepository: TaskRepository
abstract val userSampleRepository: UserRepository
companion object {
private const val DB_NAME = "example_database"
#Volatile
internal var clock: Clock = Clock.systemDefaultZone()
private set
...
#JvmStatic
fun withClock(clock: Clock) {
synchronized(this) {
this.clock = clock
}
}
}
}
When used:
abstract class TaskRepository
...
private suspend fun updateAll(tasks: List<TaskEntity>) = updateAllInternal(
tasks.map {
TaskEntity.Builder()
.copyOf(it)
.withUpdatedAt(OffsetDateTime.now(AppDatabase.clock))
.create()
}
)
...
}
Related passing tests:
#Test
fun whenRequestingDefaultClock_shouldCreateUpdatedTimestamp() {
val firstTimestamp = Instant.now(AppDatabase.clock)
sleep(1) // Adding 1ms delay to ensure Clock is not fixed and returns a different timestamp
val secondTimestamp = Instant.now(AppDatabase.clock)
assertThat(firstTimestamp).isLessThan(secondTimestamp)
}
#Test
fun whenSwitchingToFixedClock_shouldUseSameTimestamp() {
val instant = Instant.now()
val clock = Clock.fixed(instant, Clock.systemDefaultZone().zone)
AppDatabase.withClock(clock)
val firstTimestamp = Instant.now(AppDatabase.clock)
sleep(1) // Adding 1ms delay to ensure Clock is fixed and returns a fixed timestamp
val secondTimestamp = Instant.now(AppDatabase.clock)
assertThat(firstTimestamp).isEqualTo(secondTimestamp)
}

Correct flow between Kotlin, Realm and ViewModels using Coroutines

Disclaimer: This is not actual code from any app, but an example of the flow and my current understanding on how best to do this. I am looking for help improving upon or what I am doing wrong.
I am trying to figure out the best way to structure an android application using the new jetpack viewModels, realm, and coroutines. I put together a gist of the flow that I have so far, and would love some feedback on how I can improve, what I could change, or what I am doing wrong. Ideally with examples or direct changes to my code.
It works as is, I am just not sure if I am using coroutines correctly or efficiently, and if there is a better way to structure the DAO's so that Realm can be injected for better testability. Someone has already mentioned changing the DAO to extend the LiveData<>, and using onActive() and onInactive() for posting the object. Is that a good idea?
// About Model is the model used by Realm. These models contains realm specific types, like RealmList
open class AboutModel(
var name: String = "",
#PrimaryKey
var version: String = ""
): RealmObject() {
/**
* Conversion function, to convert the view model layer object to the data layer object
*/
companion object {
fun from(about: About): AboutModel = AboutModel(about.name, about.version)
}
fun toObject(): About =
About(
this.name,
this.version
)
}
// About class used everywhere outside of the data/realm layer.
// Lines up with the AboutModel class, but free of realm or any other database specific types.
// This way, realm objects are not being referenced anywhere else. In case I ever need to
// replace realm for something else.
class About (val name: String = "Test", val version: String = "1.0.0") {
override fun toString(): String {
return "author is : $name, version is: $version"
}
}
// Couldn't inject the realm instance because its thread would not match with a suspend function.
// Even if both where background threads. Would be better if I could inject it, but couldn't get
// that to work.
class AboutDao() {
private val _about = MutableLiveData<About>()
init {
val realm = Realm.getDefaultInstance()
val aboutModel = realm.where(AboutModel::class.java).findFirst()
_about.postValue(aboutModel?.toObject() ?: About())
realm.close()
}
suspend fun setAbout(about: About) = withContext(Dispatchers.IO) {
val realm: Realm = Realm.getDefaultInstance()
realm.executeTransaction {
realm.copyToRealmOrUpdate(AboutModel.from(about))
_about.postValue(about)
}
realm.close()
}
fun getAbout() = _about as LiveData<About>
}
// Database is a singleton instance, so there is only ever one instance of the DAO classes
class Database private constructor() {
var aboutDao = AboutDao()
private set
companion object {
// #Volatile - Writes to this property are immediately visible to other threads
#Volatile private var instance: Database? = null
suspend fun getInstance() = withContext(Dispatchers.IO) {
return#withContext instance ?: synchronized(this) {
instance ?: Database().also { instance = it }
}
}
}
}
// Repo maintains the dao access. Is also setup to run as a singleton
class AboutRepo private constructor(private val aboutDao: AboutDao){
// This may seem redundant.
// Imagine a code which also updates and checks the backend.
suspend fun set(about: About) {
aboutDao.setAbout(about)
}
suspend fun getAbout() = aboutDao.getAbout()
companion object {
// Singleton instantiation you already know and love
#Volatile private var instance: AboutRepo? = null
fun getInstance(aboutDao: AboutDao) =
instance ?: synchronized(this) {
instance ?: AboutRepo(aboutDao).also { instance = it }
}
}
}
// Injector is used to help keep the injection in a single place for the fragments and activities.
object Injector {
// This will be called from About Fragment
suspend fun provideAboutViewModelFactory(): AboutViewModelFactory = withContext(Dispatchers.Default) {
AboutViewModelFactory(getAboutRepo())
}
private suspend fun getAboutRepo() = withContext(Dispatchers.IO) {
AboutRepo.getInstance(Database.getInstance().aboutDao)
}
}
// AboutViewModel's Factory. I found this code online, as a helper for injecting into the viewModel's factory.
class AboutViewModelFactory (private val aboutRepo: AboutRepo)
: ViewModelProvider.NewInstanceFactory() {
#Suppress("UNCHECKED_CAST")
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
return AboutViewModel(aboutRepo) as T
}
}
// About Fragments ViewModel
class AboutViewModel(private val aboutRepo: AboutRepo) : ViewModel() {
suspend fun getAbout() = aboutRepo.getAbout()
suspend fun setAbout(about: About) = aboutRepo.set(about)
}
// Fragment's onActivityCreated, I set the viewModel and observe the model from the view model for changes
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
lifecycleScope.launch {
viewModel = ViewModelProviders.of(
this#AboutFragment,
Injector.provideAboutViewModelFactory()
).get(AboutViewModel::class.java)
withContext(Dispatchers.Main) {
viewModel.getAbout().observe(viewLifecycleOwner, Observer { about ->
version_number.text = about?.version
})
}
}
}

Categories

Resources