What happens if #WorkerThread annonated function calls another function in android? - android

Which thread the subsequently called functions are executed?
You may consider a function like the getNewsFeed() function inside the repository class given below -
#Singleton
class NewsFeedRepository #Inject constructor(
private val networkDataSource: NetworkDataSource,
private val diskDataSource: DiskDataSource
) {
#WorkerThread
suspend fun getNewsFeed(): NewsResponse {
return try {
val news = networkDataSource.getNewsFeed()
diskDataSource.updateCache(news)
NewsResponse(news = news)
} catch (ex: Exception) {
NewsResponse(
news = diskDataSource.getNews(),
errorMessage = ex.message
)
}
}
}
I called the getNewsFeed() function from ViewModel using kotlin coroutine as given below -
#HiltViewModel
class MainViewModel #Inject constructor(
private val repository: NewsFeedRepository
) : ViewModel() {
private val _newsResponse = MutableLiveData<NewsResponse>()
val newsResponse: LiveData<NewsResponse>
get() = _newsResponse
init {
viewModelScope.launch {
_newsResponse.value = repository.getNewsFeed()
}
}
}
I tried to use Dispatchers.IO as well but I couldn't update livedata value hence I had to use #WorkerThread.

Related

Passing errors coming from the API call

I am using 2 separate liveData exposed to show the error coming from the API. I am basically checking if there is an exception with the API call, pass a failure status and serverErrorLiveData will be observed.
So I have serverErrorLiveData for error and creditReportLiveData for result without an error.
I think I am not doing this the right way. Could you please guide me on what is the right way of catching error from the API call. Also, any concerns/recommendation on passing data from repository on to view model.
What is the right way of handing loading state?
CreditScoreFragment
private fun initViewModel() {
viewModel.getCreditReportObserver().observe(viewLifecycleOwner, Observer<CreditReport> {
showScoreUI(true)
binding.score.text = it.creditReportInfo.score.toString()
binding.maxScoreValue.text = "out of ${it.creditReportInfo.maxScoreValue}"
initDonutView(
it.creditReportInfo.score.toFloat(),
it.creditReportInfo.maxScoreValue.toFloat()
)
})
viewModel.getServerErrorLiveDataObserver().observe(viewLifecycleOwner, Observer<Boolean> {
if (it) {
showScoreUI(false)
showToastMessage()
}
})
viewModel.getCreditReport()
}
MainActivityViewModel
class MainActivityViewModel #Inject constructor(
private val dataRepository: DataRepository
) : ViewModel() {
var creditReportLiveData: MutableLiveData<CreditReport>
var serverErrorLiveData: MutableLiveData<Boolean>
init {
creditReportLiveData = MutableLiveData()
serverErrorLiveData = MutableLiveData()
}
fun getCreditReportObserver(): MutableLiveData<CreditReport> {
return creditReportLiveData
}
fun getServerErrorLiveDataObserver(): MutableLiveData<Boolean> {
return serverErrorLiveData
}
fun getCreditReport() {
viewModelScope.launch(Dispatchers.IO) {
val response = dataRepository.getCreditReport()
when(response.status) {
CreditReportResponse.Status.SUCCESS -> creditReportLiveData.postValue(response.creditReport)
CreditReportResponse.Status.FAILURE -> serverErrorLiveData.postValue(true)
}
}
}
}
DataRepository
class DataRepository #Inject constructor(
private val apiServiceInterface: ApiServiceInterface
) {
suspend fun getCreditReport(): CreditReportResponse {
return try {
val creditReport = apiServiceInterface.getDataFromApi()
CreditReportResponse(creditReport, CreditReportResponse.Status.SUCCESS)
} catch (e: Exception) {
CreditReportResponse(null, CreditReportResponse.Status.FAILURE)
}
}
}
ApiServiceInterface
interface ApiServiceInterface {
#GET("endpoint.json")
suspend fun getDataFromApi(): CreditReport
}
CreditScoreResponse
data class CreditReportResponse constructor(val creditReport: CreditReport?, val status: Status) {
enum class Status {
SUCCESS, FAILURE
}
}
It's creates complexity and increased chances for a coding error to have two LiveData channels for success and failure. You should have a single LiveData that can offer up the data or an error so you know it's coming in orderly and you can observe it in one place. Then if you add a retry policy, for example, you won't risk somehow showing an error after a valid value comes in. Kotlin can facilitate this in a type-safe way using a sealed class. But you're already using a wrapper class for success and failure. I think you can go to the source and simplify it. You can even just use Kotlin's own Result class.
(Side note, your getCreditReportObserver() and getServerErrorLiveDataObserver() functions are entirely redundant because they simply return the same thing as a property. You don't need getter functions in Kotlin because properties basically are getter functions, with the exception of suspend getter functions because Kotlin doesn't support suspend properties.)
So, to do this, eliminate your CreditReportResponse class. Change your repo function to:
suspend fun getCreditReport(): Result<CreditReport> = runCatching {
apiServiceInterface.getDataFromApi()
}
If you must use LiveData (I think it's simpler not to for a single retrieved value, see below), your ViewModel can look like:
class MainActivityViewModel #Inject constructor(
private val dataRepository: DataRepository
) : ViewModel() {
val _creditReportLiveData = MutableLiveData<Result<CreditReport>>()
val creditReportLiveData: LiveData<Result<CreditReport>> = _creditReportLiveData
fun fetchCreditReport() { // I changed the name because "get" implies a return value
// but personally I would change this to an init block so it just starts automatically
// without the Fragment having to manually call it.
viewModelScope.launch { // no need to specify dispatcher to call suspend function
_creditReportLiveData.value = dataRepository.getCreditReport()
}
}
}
Then in your fragment:
private fun initViewModel() {
viewModel.creditReportLiveData.observe(viewLifecycleOwner) { result ->
result.onSuccess {
showScoreUI(true)
binding.score.text = it.creditReportInfo.score.toString()
binding.maxScoreValue.text = "out of ${it.creditReportInfo.maxScoreValue}"
initDonutView(
it.creditReportInfo.score.toFloat(),
it.creditReportInfo.maxScoreValue.toFloat()
)
}.onFailure {
showScoreUI(false)
showToastMessage()
}
viewModel.fetchCreditReport()
}
Edit: the below would simplify your current code, but closes you off from being able to easily add a retry policy on failure. It might make better sense to keep the LiveData.
Since you are only retrieving a single value, it would be more concise to expose a suspend function instead of LiveData. You can privately use a Deferred so the fetch doesn't have to be repeated if the screen rotates (the result will still arrive and be cached in the ViewModel). So I would do:
class MainActivityViewModel #Inject constructor(
private val dataRepository: DataRepository
) : ViewModel() {
private creditReportDeferred = viewModelScope.async { dataRepository.getCreditReport() }
suspend fun getCreditReport() = creditReportDeferred.await()
}
// In fragment:
private fun initViewModel() = lifecycleScope.launch {
viewModel.getCreditReport()
.onSuccess {
showScoreUI(true)
binding.score.text = it.creditReportInfo.score.toString()
binding.maxScoreValue.text = "out of ${it.creditReportInfo.maxScoreValue}"
initDonutView(
it.creditReportInfo.score.toFloat(),
it.creditReportInfo.maxScoreValue.toFloat()
)
}.onFailure {
showScoreUI(false)
showToastMessage()
}
}

Mockito cannot mock/spy because : - final class

I am a simple function getCreditReport in viewmodel in which I am trying to make an API call repository layer
I want to test getCreditReport in viewmodel to check that thge repository is called but I get the following error
org.mockito.exceptions.base.MockitoException:
Cannot mock/spy class com.example.clearscore.data.DataRepository
Mockito cannot mock/spy because :
- final class
What I have tried
MainActivityViewModel
class MainActivityViewModel #Inject constructor(
//private val retroServiceInterface: RetroServiceInterface,
private val dataRepository: DataRepository
) : ViewModel() {
var creditReportLiveData: MutableLiveData<CreditReport>
init {
creditReportLiveData = MutableLiveData()
}
fun getCreditReportObserver(): MutableLiveData<CreditReport> {
return creditReportLiveData
}
fun getCreditReport() {
viewModelScope.launch(Dispatchers.IO) {
try {
val response = dataRepository.getCreditReport()
creditReportLiveData.postValue(response)
Log.d("data", response.toString())
} catch (e: IOException) {
Log.d("data", e.toString())
}
}
}
}
DataRepository
class DataRepository #Inject constructor(
private val retroServiceInterface: RetroServiceInterface
) {
suspend fun getCreditReport(): CreditReport {
return retroServiceInterface.getDataFromApi()
}
}
Unit test
#RunWith(MockitoJUnitRunner::class)
class MainActivityViewModelTest {
#Mock
private lateinit var dataRepository: DataRepository
#Mock
private lateinit var mainActivityViewModel: MainActivityViewModel
#Test
fun getCreditReport() {
runBlocking {
mainActivityViewModel.getCreditReport()
verify(dataRepository).getCreditReport()
}
}
}
Dependency Injection component - in case this is helpful
#Singleton
#Component(modules = [RetroModule::class])
interface RetroComponent {
fun inject(mainActivityViewModel: MainActivityViewModel)
fun getMainactivityViewModel(): MainActivityViewModel
}
Please suggest what I am doing wrong
Thanks
R
Please try adding this dependency
testImplementation "org.mockito:mockito-inline:3.11.2"
[Adding to help future folks who stumble upon this question]
In my case, the class didn't need to be final so I just removed final from the class declaration.

Can't see DI outside of init block of ViewModel

I'm trying to control ViewModel from a fragment by sending category_id and pass it to the repository which is injected by Hilt.
But ViewModel cant see repository outside of init block. What did I miss?
#HiltViewModel
class ProjectViewModel #Inject constructor(
repository: ScienceTrackerRepository
) : ViewModel() {
private val _flow = MutableStateFlow(LoadUiState.Success(emptyList()))
val flow: StateFlow<LoadUiState> = _flow.asStateFlow()
fun loadProjects(categoryId: Int) {
viewModelScope.launch {
repository.getProjects(categoryId) // unresolved reference "repository"
repository.flowProjects.collect { feed ->
_flow.value = LoadUiState.Success(feed)
}
}
}
init {
viewModelScope.launch {
repository.getProjects(0)
repository.flowProjects.collect { feed ->
_flow.value = LoadUiState.Success(feed)
}
}
}
}
you need to add var or val keyword like this:
#HiltViewModel
class ProjectViewModel #Inject constructor(
val repository: ScienceTrackerRepository
) : ViewModel() {
}
to access the constructor params outside the init block.

What is the best way to test a function in my viewModel?

I started to implement automated tests recently and I have a little doubt of how to test my ViewModel functions. My project follows some clean architecture concepts, I'm using lib kotlin for mockito (https://github.com/nhaarman/mockito-kotlin) and I would like to do the following test: When call makeLoginUser get success then set loginUserLiveData .
I have studied some concepts and I know the tests I should do, but I am still in doubt as to how to do them, which objects should I mock for a given test and which should be instantiated. I needed a small example to address me.
Here is my test class and some classes of the structure of my project.
TestClass
#RunWith(JUnit4::class)
class MainViewModelTest {
#get:Rule
var instantTaskExecutorRule = InstantTaskExecutorRule()
#Mock
private lateinit var myRepository: MyRepository
private lateinit var loginUserUseCase: LoginUserUseCase
private lateinit var checkUserAuthenticatedUsecase: CheckUserAuthenticatedUsecase
private lateinit var logoutUserUseCase: LogoutUserUseCase
private lateinit var mainViewModel: MainViewModel
#Before
fun setUp() {
MockitoAnnotations.initMocks(this)
}
#Test
fun `When call makeLoginUser get success then set loginUserLiveData`() {
// prepare
myRepository = mock { // should I mock or instantiate?
on { execLoginUser(ArgumentMatchers.anyString(), ArgumentMatchers.anyString()) } doReturn Flowable.just(true)
}
loginUserUseCase = mock { // should I mock or instantiate?
on { execute(ArgumentMatchers.anyMap()) } doReturn Flowable.just(true)
}
mainViewModel = MainViewModel(loginUserUseCase, checkUserAuthenticatedUsecase, logoutUserUseCase)
val observer = mock<Observer<ResultState<Boolean>>> { mock }
// Execute
mainViewModel.makeLoginUser("test#gmail.com", "123456")
// Check
// ?
}
}
ViewModel
// components 'LoginUserUseCase', 'CheckUserAuthenticatedUsecase' and 'LogoutUserUseCase' injected via koin
class MainViewModel(
private val loginUserUseCase: LoginUserUseCase,
private val checkUserAuthenticatedUsecase: CheckUserAuthenticatedUsecase,
private val logoutUserUsecase: LogoutUserUseCase
): BaseViewModel() {
val loginUserLiveData = MutableLiveData<ResultState<Boolean>>()
val userAuthenticatedLiveData = MutableLiveData<ResultState<Boolean>>()
val logoutUserLiveData = MutableLiveData<ResultState<Boolean>>()
fun makeLoginUser(email: String, password: String) {
loginUserLiveData.postValue(ResultState.Loading())
loginUserUseCase.execute(mapOf(EMAIL to email, PASSWORD to password))
.subscribe({
loginUserLiveData.postValue(ResultState.Success(it))
}, {
loginUserLiveData.postValue(ResultState.Error(it))
}).addTo(disposables)
}
...
}
UseCase Domain Class
// components 'ThreadExecutor' and 'PostExecutionThread' injected via koin
abstract class FlowableUseCase<T, in Params> constructor(
private val threadExecutor: ThreadExecutor,
private val postExecutionThread: PostExecutionThread) {
protected abstract fun buildUseCaseObservable(params: Params? = null): Flowable<T>
open fun execute(params: Params? = null): Flowable<T> {
return this.buildUseCaseObservable(params)
.subscribeOn(Schedulers.from(threadExecutor))
.observeOn(postExecutionThread.scheduler)
}
}
Abstraction UseCases
// components 'MyRepository', 'ThreadExecutor' and 'PostExecutionThread' injected via koin
// 'MyRepository' abstracts remote and local data repository
class LoginUserUseCase(
private val myRepository: MyRepository,
threadExecutor: ThreadExecutor,
postExecutionThread: PostExecutionThread
): FlowableUseCase<Boolean, Map<String, Any>?>(threadExecutor, postExecutionThread) {
override fun buildUseCaseObservable(params: Map<String, Any>?) = myRepository
.execLoginUser(
params?.get(EMAIL) as String,
params[PASSWORD] as String
)
}

How do I notify in Android the ViewModel when a background job has finished creating a new record using LiveData?

I have an application which from one activity triggers a background job using JobScheduler. This job inserts in a database using Room once it's done. To notify the ViewModel I'm using LiveData. I have set up a main activity which uses ViewModel and everything is injected with Dagger 2.
The problem is as follows: when I launch the main activity the list is populated as expected. However when from other activity I schedule a job and said job is finished and a new record has been inserted in the database the main activity does not update.
Moreover, if I update a record from the main activity (without using a job) the view refreshes as expected, which makes me believe that the problem has to do with the ViewModel not detecting when a record is inserted using a job.
If I'm using LiveData, isn't it supposed to catch all the new insertions? How could I manually notify the ViewModel of the new record?
Edit:
Repository:
#Singleton
class DebtsRepository #Inject constructor(
var debtDao: DebtDao
) {
fun insertDebt(debt: Debt) {
debtDao.insert(debt)
Log.e("DebtsRepository", "debt inserted")
}
fun getAllDebts(): LiveData<List<Debt>> {
return debtDao.findAll()
}
fun getAllDebtsWithInformation(): LiveData<List<DebtWithDebtorCreditorAndProduct>> {
return debtDao.findAllWithProductDebtorAndCreditor()
}
fun payDebt(debtId: Long) {
val paidDebt = debtDao.findOne(debtId)
debtDao.update(paidDebt.copy(paid = true))
}
fun getUnpaidDebts(debts: List<DebtWithDebtorCreditorAndProduct>): List<DebtWithDebtorCreditorAndProduct> = debts.filter { !it.debt.paid }
}
Here is the ViewModel:
#Singleton
class DebtsListViewModel #Inject constructor(var debtsRepository: DebtsRepository) : ViewModel() {
private var debts: LiveData<List<Debt>> = MutableLiveData()
private var formattedDebts: LiveData<List<DebtWithDebtorCreditorAndProduct>> = MutableLiveData()
init {
debts = debtsRepository.getAllDebts()
formattedDebts = Transformations.switchMap(debts) {
Log.e("DebtsListViewModel", "change triggered")
debtsRepository.getAllDebtsWithInformation()
}
}
fun getFormattedDebts() = formattedDebts
fun payDebt(debtId: Long) {
debtsRepository.payDebt(debtId)
}
fun getUnpaidDebts(): List<DebtWithDebtorCreditorAndProduct> =
if (formattedDebts.value != null)
debtsRepository.getUnpaidDebts(formattedDebts.value!!)
else
emptyList()
}
This is the job:
class GenerateDebtJobService : JobService() {
#Inject
lateinit var debtsRepository: DebtsRepository
#Inject
lateinit var productDao: ProductDao
#Inject
lateinit var productsDebtorsDao: ProductDebtorDao
#Inject
lateinit var productCreditorsDao: ProductCreditorDao
override fun onStartJob(params: JobParameters): Boolean {
DaggerGraphBuilder.build(applicationContext as FineApplication).inject(this)
val productId = params.extras.getLong("id")
val product = productDao.findOne(productId)
val productCreditor = productCreditorsDao.findOneByProduct(productId)
val debtors = productsDebtorsDao.findAllByProduct(productId)
// When the bill day is reached for a given product the debtors list associated with that product is looped through and a new debt is created
debtors.forEach {
debtsRepository.insertDebt(Debt(productId = it.productProductId, debtorId = it.productDebtorId, quantity = product.recurringCost, date = DateTime().toString(), creditorId = productCreditor.productCreditorId))
}
return false
}
override fun onStopJob(params: JobParameters): Boolean {
Log.e("GenerateDebtJob", "job finished")
return false
}
companion object {
fun build(application: Application, productId: Long, periodicity: Days, startDay: Days) {
val serviceComponent = ComponentName(application, GenerateDebtJobService::class.java)
val bundle = PersistableBundle()
bundle.putLong("id", productId)
val builder = JobInfo.Builder(productId.toInt(), serviceComponent)
.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)
// .setPeriodic(TimeUnit.DAYS.toMillis(periodicity.days.toLong()))
.setOverrideDeadline(1_000)
.setExtras(bundle)
(application.getSystemService(Context.JOB_SCHEDULER_SERVICE) as JobScheduler).schedule(builder.build())
}
}
}
More edits:
I found out that the instance of the repository on the App is different that the instance of the repository on the background job. This makes the LiveData unable to trigger new changes when there are new records inserted in the DB.
So my guess is that somehow I have to check if the app is on the foreground and if so inject the current instance of the repository into the job.
If not, then the dependency graph must be created and as the job executes without the app running.
Here are the relevant parts of DI configuration:
AppComponent:
#Singleton
#Component(modules = [
AppModule::class,
AndroidInjectionModule::class,
DebtsListViewModelModule::class,
DebtsListModule::class,
AddProductModule::class
])
interface AppComponent {
fun inject(application: FineApplication)
fun inject(generateDebtJobService: GenerateDebtJobService)
}
AppModule:
#Singleton
#Module
class AppModule(private val application: Application) {
#Provides
fun provideApplicationContext(): Context = application
#Provides
fun provideAppDatabase(context: Context): AppDatabase =
Room
.databaseBuilder<AppDatabase>(context, AppDatabase::class.java, AppDatabase.DB_NAME)
.allowMainThreadQueries()
.build()
#Provides
fun provideDebtDao(database: AppDatabase): DebtDao = database.debtDao()
#Provides
fun provideDebtorDao(database: AppDatabase): DebtorDao = database.debtorDao()
#Provides
fun provideCreditorDao(database: AppDatabase): CreditorDao = database.creditorDao()
#Provides
fun provideProductDao(database: AppDatabase): ProductDao = database.productDao()
#Provides
fun provideProductDebtorDao(database: AppDatabase): ProductDebtorDao = database.productDebtorDao()
#Provides
fun provideProductCreditorDao(database: AppDatabase): ProductCreditorDao = database.productCreditorDao()
}
Application:
class FineApplication : Application(), HasActivityInjector {
#Inject
lateinit var dispatchingActivityInjector: DispatchingAndroidInjector<Activity>
private val component by lazy {
DaggerAppComponent
.builder()
.appModule(AppModule(this))
.build()
}
override fun onCreate() {
super.onCreate()
component.inject(this)
Stetho.initializeWithDefaults(this)
JodaTimeAndroid.init(this)
}
override fun activityInjector(): AndroidInjector<Activity> = dispatchingActivityInjector
}
If there is anything else needed just let me know.

Categories

Resources