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 all commits
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
@@ -0,0 +1,15 @@
/*
* 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.core.common.utils

class DatabaseFetchException(
message: String,
cause: Throwable? = null,
) : Exception(message, cause)
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*
* 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.core.common.utils

import kotlinx.coroutines.flow.Flow
import retrofit2.CallAdapter
import retrofit2.Response
import retrofit2.Retrofit
import java.lang.reflect.ParameterizedType
import java.lang.reflect.Type

class FlowCallAdapterFactory private constructor() : CallAdapter.Factory() {
override fun get(
returnType: Type,
annotations: Array<Annotation>,
retrofit: Retrofit,
): CallAdapter<*, *>? {
if (getRawType(returnType) != Flow::class.java) {
return null
}
check(returnType is ParameterizedType) { "Flow return type must be parameterized as Flow<Foo> or Flow<out Foo>" }
val responseType = getParameterUpperBound(0, returnType)
val rawFlowType = getRawType(responseType)
return if (rawFlowType == Response::class.java) {
check(responseType is ParameterizedType) { "Response must be parameterized as Response<Foo> or Response<out Foo>" }
ResponseCallAdapter<Any>(
getParameterUpperBound(
0,
responseType,
),
)
} else {
BodyCallAdapter<Any>(responseType)
}
}

companion object {
@JvmStatic
fun create() = FlowCallAdapterFactory()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/*
* 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.core.common.utils // FlowCallAdapter.kt
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.suspendCancellableCoroutine
import retrofit2.Call
import retrofit2.CallAdapter
import retrofit2.Callback
import retrofit2.Response
import java.lang.reflect.Type
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException

class ResponseCallAdapter<T>(
private val responseType: Type,
) : CallAdapter<T, Flow<Response<T>>> {
override fun adapt(call: Call<T>): Flow<Response<T>> {
return flow {
emit(
suspendCancellableCoroutine { continuation ->
call.enqueue(object : Callback<T> {
override fun onFailure(call: Call<T>, t: Throwable) {
continuation.resumeWithException(t)
}

override fun onResponse(call: Call<T>, response: Response<T>) {
continuation.resume(response)
}
})
continuation.invokeOnCancellation { call.cancel() }
},
)
}
}

override fun responseType() = responseType
}

class BodyCallAdapter<T>(private val responseType: Type) : CallAdapter<T, Flow<T>> {
override fun adapt(call: Call<T>): Flow<T> {
return flow {
emit(
suspendCancellableCoroutine { continuation ->
call.enqueue(object : Callback<T> {
override fun onFailure(call: Call<T>, t: Throwable) {
continuation.resumeWithException(t)
}

override fun onResponse(call: Call<T>, response: Response<T>) {
try {
continuation.resume(response.body()!!)
} catch (e: Exception) {
continuation.resumeWithException(e)
}
}
})
continuation.invokeOnCancellation { call.cancel() }
},
)
}
}

override fun responseType() = responseType
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,12 @@ 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.common.utils.DatabaseFetchException
import com.mifos.core.model.objects.clients.Page
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.catch
import kotlinx.coroutines.flow.first

class ClientChargesPagingSource(
private val clientId: Int,
Expand Down Expand Up @@ -48,29 +45,29 @@ class ClientChargesPagingSource(
)
} catch (exception: Exception) {
LoadResult.Error(exception)
} catch (exception: DatabaseFetchException) {
LoadResult.Error(exception)
}
}

private suspend fun getClientChargeList(
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)
}
var page: Page<Charges>? = null

override fun onNext(page: Page<Charges>) {
continuation.resume(Pair(page.pageItems, page.totalFilteredRecords))
}
})
dataManagerCharge.getClientCharges(
clientId = clientId,
offset = position,
limit = 10,
).catch {
throw DatabaseFetchException("Failed to fetch client charges")
}.collect {
page = it
}

return page?.let {
Pair(it.pageItems, it.totalFilteredRecords)
} ?: throw DatabaseFetchException("Failed to fetch client charges")
}
}
29 changes: 29 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,29 @@
/*
* 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.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 {

@Query("SELECT * FROM Charges where clientId = :clientId")
fun getClientCharges(clientId: Int): Flow<List<Charges>>

@Insert
suspend fun insertAllCharges(vararg charges: List<Charges>)
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,11 @@ 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.GroupsDao
import com.mifos.room.dao.LoanDao
import com.mifos.room.dao.OfficeDao
import com.mifos.room.dao.StaffDao
import com.mifos.room.dao.SurveyDao
import com.mifos.room.entities.PaymentTypeOption
Expand Down Expand Up @@ -46,7 +48,6 @@ import com.mifos.room.utils.typeconverters.SurveyTypeConverters
// [TODO -> add other entities ]
entities = [
ColumnValue::class,
// loan
LoanWithAssociations::class,
LoanRepaymentRequest::class,
LoanRepaymentTemplate::class,
Expand All @@ -55,7 +56,6 @@ import com.mifos.room.utils.typeconverters.SurveyTypeConverters
Timeline::class,
Status::class,
Summary::class,
// survey
Survey::class,
QuestionDatas::class,
ResponseDatas::class,
Expand Down Expand Up @@ -85,7 +85,9 @@ abstract class MifosDatabase : RoomDatabase() {
abstract fun loanDao(): LoanDao
abstract fun surveyDao(): SurveyDao
abstract fun staffDao(): StaffDao
abstract fun officeDao(): OfficeDao
abstract fun groupsDao(): GroupsDao
abstract fun chargeDao(): ChargeDao

companion object {
const val VERSION = 1
Expand Down
14 changes: 14 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.GroupsDao
import com.mifos.room.dao.LoanDao
Expand All @@ -20,37 +21,50 @@ import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton

@Module
@InstallIn(SingletonComponent::class)
object DaoModule {
@Provides
@Singleton
fun providesColumnValueDao(database: MifosDatabase): ColumnValueDao {
return database.columnValueDao()
}

@Provides
@Singleton
fun providesLoanDao(database: MifosDatabase): LoanDao {
return database.loanDao()
}

@Provides
@Singleton
fun providesSurveyDao(database: MifosDatabase): SurveyDao {
return database.surveyDao()
}

@Provides
@Singleton
fun providesStaffDao(database: MifosDatabase): StaffDao {
return database.staffDao()
}

@Provides
@Singleton
fun providesGroupDao(database: MifosDatabase): GroupsDao {
return database.groupsDao()
}

@Provides
@Singleton
fun providesOfficeDao(database: MifosDatabase): OfficeDao {
return database.officeDao()
}

@Provides
@Singleton
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()
}
}
Loading
Loading