Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: [database/charge] dbflow to room #2312

Merged
merged 5 commits into from
Feb 19, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,10 @@ package com.mifos.core.data.pagingSource

import androidx.paging.PagingSource
import androidx.paging.PagingState
import com.mifos.core.entity.client.Charges
import com.mifos.core.network.datamanager.DataManagerCharge
import com.mifos.core.objects.clients.Page
import kotlinx.coroutines.suspendCancellableCoroutine
import rx.Subscriber
import rx.android.schedulers.AndroidSchedulers
import rx.schedulers.Schedulers
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
import com.mifos.room.entities.client.Charges
import kotlinx.coroutines.flow.first

class ClientChargesPagingSource(
private val clientId: Int,
Expand Down Expand Up @@ -55,22 +50,16 @@ class ClientChargesPagingSource(
clientId: Int,
position: Int,
): Pair<List<Charges>, Int> {
return suspendCancellableCoroutine { continuation ->
dataManagerCharge.getClientCharges(clientId = clientId, offset = position, 10)
.observeOn(AndroidSchedulers.mainThread())
.subscribeOn(Schedulers.io())
.subscribe(object : Subscriber<Page<Charges>>() {
override fun onCompleted() {
}

override fun onError(exception: Throwable) {
continuation.resumeWithException(exception)
}
return try {
val page = dataManagerCharge.getClientCharges(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See documentation, How to use coroutines in PagingSource

clientId = clientId,
offset = position,
limit = 10,
).first()

override fun onNext(page: Page<Charges>) {
continuation.resume(Pair(page.pageItems, page.totalFilteredRecords))
}
})
Pair(page.pageItems, page.totalFilteredRecords)
} catch (exception: Throwable) {
throw exception

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest wrapping the throw statement in a custom exception. It will help us to easily trace errors during debugging.
somehting like:
throw DatabaseFetchException("Failed to fetch client charges", exception)

}
}
}
30 changes: 30 additions & 0 deletions core/database/src/main/java/com/mifos/room/dao/ChargeDao.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
* Copyright 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
* See https://github.com/openMF/android-client/blob/master/LICENSE.md
*/
package com.mifos.room.dao

import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import com.mifos.room.entities.client.Charges
import kotlinx.coroutines.flow.Flow

/**
* Created by Pronay Sarker on 14/02/2025 (3:32 PM)
*/
@Dao
interface ChargeDao {

@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertCharges(charge: List<Charges>)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

instead of charge: List<Charges> vararg charges: Charges


@Query("SELECT * FROM Charges where clientId = :clientId")
fun getClientCharges(clientId: Int): Flow<List<Charges>>
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ package com.mifos.room.db
import androidx.room.Database
import androidx.room.RoomDatabase
import androidx.room.TypeConverters
import com.mifos.room.dao.ChargeDao
import com.mifos.room.dao.ColumnValueDao
import com.mifos.room.dao.LoanDao
import com.mifos.room.dao.StaffDao
Expand Down Expand Up @@ -72,6 +73,7 @@ abstract class MifosDatabase : RoomDatabase() {
abstract fun loanDao(): LoanDao
abstract fun surveyDao(): SurveyDao
abstract fun staffDao(): StaffDao
abstract fun chargeDao(): ChargeDao

companion object {
const val VERSION = 1
Expand Down
6 changes: 6 additions & 0 deletions core/database/src/main/java/com/mifos/room/di/DaoModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
*/
package com.mifos.room.di

import com.mifos.room.dao.ChargeDao
import com.mifos.room.dao.ColumnValueDao
import com.mifos.room.dao.LoanDao
import com.mifos.room.dao.StaffDao
Expand Down Expand Up @@ -41,4 +42,9 @@ object DaoModule {
fun providesStaffDao(database: MifosDatabase): StaffDao {
return database.staffDao()
}

@Provides
fun providesClientDao(database: MifosDatabase): ChargeDao {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@itsPronay I think it would be a good idea to add @singleton to the providesClientDao() method. Since Room is designed to work with a single shared database connection, using @singleton ensures that all components use the same ChargeDao instance.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@HekmatullahAmin thanks for the suggestion brother.
but we already used @InstallIn(SingletonComponent::class) on the DaoModule, it already ensures that all the provided components in this module are scoped as singletons

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @itsPronay !

Just to clarify the difference between @Installin(SingletonComponent::class) and @singleton in Hilt:

@Installin(SingletonComponent::class) means that the dependency will live as long as the application is alive, but it doesn't guarantee a single instance by itself.
@singleton tells Hilt to reuse the same instance of the dependency within the same scope.

Example:

@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {

    @Provides
    fun provideApiService(): ApiService {
        return ApiServiceImpl()
    }
}

In this case, every class that requests ApiService gets a new instance, because @singleton is missing.

Singleton Example:

@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {

@Singleton
@Provides
    fun provideApiService(): ApiService {
        return ApiServiceImpl()
    }
}

By adding @singleton, only one instance of SomeService is created and shared across the entire app (within the SingletonComponent scope).
Without @singleton, multiple instances are created even if SingletonComponent is used.

If you’d like more details, you can check out the official documentation here:
Hilt - Android Developer Guide

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@HekmatullahAmin Thanks for explaining.
I have a doubt, since MifosDatabase is already a singleton, wouldn't the DAO instances be shared across the app by default? meaning singleton

return database.chargeDao()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
package com.mifos.room.entities.client

import android.os.Parcelable
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.PrimaryKey
Expand Down Expand Up @@ -56,62 +55,43 @@ import kotlinx.serialization.json.Json
)
data class Charges(
@PrimaryKey
@ColumnInfo(name = "id")
var id: Int? = null,
val id: Int? = null,

@ColumnInfo(name = "clientId")
var clientId: Int? = null,
val clientId: Int? = null,

@ColumnInfo(name = "loanId")
var loanId: Int? = null,
val loanId: Int? = null,

@ColumnInfo(name = "chargeId")
var chargeId: Int? = null,
val chargeId: Int? = null,

@ColumnInfo(name = "name")
var name: String? = null,
val name: String? = null,

@ColumnInfo(name = "chargeTimeType")
var chargeTimeType: ChargeTimeType? = null,
val chargeTimeType: ChargeTimeType? = null,

@ColumnInfo(name = "chargeDueDateId")
var chargeDueDate: ClientDate? = null,
val chargeDueDate: ClientDate? = null,

@ColumnInfo(name = "dueDate")
var dueDate: String? = null,
val dueDate: String? = null,

@ColumnInfo(name = "chargeCalculationType")
var chargeCalculationType: ChargeCalculationType? = null,
val chargeCalculationType: ChargeCalculationType? = null,

@ColumnInfo(name = "currency")
var currency: Currency? = null,
val currency: Currency? = null,

@ColumnInfo(name = "amount")
var amount: Double? = null,
val amount: Double? = null,

@ColumnInfo(name = "amountPaid")
var amountPaid: Double? = null,
val amountPaid: Double? = null,

@ColumnInfo(name = "amountWaived")
var amountWaived: Double? = null,
val amountWaived: Double? = null,

@ColumnInfo(name = "amountWrittenOff")
var amountWrittenOff: Double? = null,
val amountWrittenOff: Double? = null,

@ColumnInfo(name = "amountOutstanding")
var amountOutstanding: Double? = null,
val amountOutstanding: Double? = null,

@ColumnInfo(name = "penalty")
var penalty: Boolean? = null,
val penalty: Boolean? = null,

@ColumnInfo(name = "active")
var active: Boolean? = null,
val active: Boolean? = null,

@ColumnInfo(name = "paid")
var paid: Boolean? = null,
val paid: Boolean? = null,

@ColumnInfo(name = "waived")
var waived: Boolean? = null,
val waived: Boolean? = null,
) : Parcelable {

val formattedDueDate: String
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/*
* Copyright 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
* See https://github.com/openMF/android-client/blob/master/LICENSE.md
*/
package com.mifos.room.helper

import com.mifos.core.model.objects.clients.Page
import com.mifos.room.dao.ChargeDao
import com.mifos.room.entities.client.Charges
import com.mifos.room.entities.client.ClientDate
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import javax.inject.Inject

/**
* Created by Pronay Sarker on 14/02/2025 (3:36 PM)
*/
class ChargeDaoHelper @Inject constructor(
private val chargeDao: ChargeDao,
) {
/**
* This Method save the All Client Charges in Database and save the Charge Due date in the
* ClientDate as reference with Charge Id.
*
* @param chargesPage
* @param clientId
* @return null
*/
suspend fun saveClientCharges(
chargesPage: Page<Charges>,
clientId: Int,
) {
val updatedCharges = chargesPage.pageItems.map { charges ->
val dateParts = charges.dueDate.orEmpty().split("-").mapNotNull { it.toIntOrNull() }

val clientDate = if (dateParts.size == 3) {
charges.id?.toLong()?.let {
ClientDate(
0,
it,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Always try to pass parameters names for better Readability

dateParts[2],
dateParts[1],
dateParts[0],
)
}
} else {
null
}

charges.copy(clientId = clientId, chargeDueDate = clientDate)
}

chargeDao.insertCharges(updatedCharges)
}

/**
* This method Retrieve the Charges from Charges_Table and set the Charges Due date after
* loading the Charge due date from the ChargeDate_table as reference with charge Id.
*
* @param clientId Client ID
* @return Page of Charges
*/
fun readClientCharges(clientId: Int): Flow<Page<Charges>> {
return chargeDao.getClientCharges(clientId).map { chargesList ->
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inject ioDispatcher in this class and use flowOn(ioDispatcher) oparator, for all methods which returns as Flow, do this in other classes too

And this function implementation looks messy to me, copy this code and chat on Deepseek, Chatgpt etc and ask them to optimize/improve this codebase, nowadays we've many free AI tools consider to use those for better usability.

val updatedChargesList = chargesList.map { charge ->
charge.copy(
dueDate = charge.chargeDueDate?.let {
"${it.year}-${it.month}-${it.day}"
},
)
}

Page<Charges>().apply { pageItems = updatedChargesList }
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
* Copyright 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
* See https://github.com/openMF/android-client/blob/master/LICENSE.md
*/
package com.mifos.room.utils.typeconverters

import androidx.room.TypeConverter
import com.mifos.room.entities.client.ChargeCalculationType
import com.mifos.room.entities.client.ChargeTimeType
import com.mifos.room.entities.client.ClientDate
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json

/**
* Created by Pronay Sarker on 14/02/2025 (9:18 PM)
*/

class ChargeTypeConverter {

@TypeConverter
fun fromChargeTimeType(type: ChargeTimeType?): String? {
return type?.let { Json.encodeToString(it) }
}

@TypeConverter
fun toChargeTimeType(json: String?): ChargeTimeType? {
return json?.let { Json.decodeFromString(it) }
}

@TypeConverter
fun fromClientDate(date: ClientDate?): String? {
return date?.let { Json.encodeToString(it) }
}

@TypeConverter
fun toClientDate(json: String?): ClientDate? {
return json?.let { Json.decodeFromString(it) }
}

@TypeConverter
fun fromChargeCalculationType(type: ChargeCalculationType?): String? {
return type?.let { Json.encodeToString(it) }
}

@TypeConverter
fun toChargeCalculationType(json: String?): ChargeCalculationType? {
return json?.let { Json.decodeFromString(it) }
}
}
Loading
Loading