I have written a unit test, based on Spek Framework, for one of my use cases. Following is the use case code:
class InsertRemoteWidgetUseCase(
private val remoteConfigProviderImplement: RemoteConfigProviderImplement,
private val feedDataStore: FeedDataStoreImplement
) {
fun invoke(): ArrayList<FeedData> {
if (remoteConfigProviderImplement.getHomeWidgetVisible()) {
val cardData = CardData()
cardData.url = remoteConfigProviderImplement.getHomeWidgetURL()
val feedData = FeedData()
feedData.type = CardType.REMOTE_CONFIG_WIDGET
feedData.listData = arrayListOf(cardData)
feedDataStore.insertRemoteWidgetCard(feedData)
} else {
feedDataStore.removeRemoteWidgetCard()
}
return feedDataStore.getData()
}
}
Here is the test case implementation:
class InsertRemoteWidgetUseCaseTest : Spek({
lateinit var mockRemoteConfigProviderImplement: RemoteConfigProviderImplement
lateinit var mockFeedDataStoreImplement: FeedDataStoreImplement
lateinit var instance: InsertRemoteWidgetUseCase
val feedComponentData1: FeedComponentData = mockk(relaxed = true)
val feedComponentData2: FeedComponentData = mockk(relaxed = true)
var remoteWidgetUrl = ""
var remoteWidgetVisible = false
val feedDataList1 = arrayListOf(
FeedData(listItems = arrayListOf(
CardItem(articleData = ArticleData(title = "A0"))
)
),
FeedData(listItems = arrayListOf(
CardItem(articleData = ArticleData(title = "B0")),
CardItem(articleData = ArticleData(title = "B1")),
CardItem(articleData = ArticleData(title = "B2"))
))
)
val feedDataList2 = arrayListOf(FeedData())
val pagination1 = Pagination(current = 0, next = 1)
val pagination2 = Pagination(current = 1, next = 2)
beforeEachTest {
every{feedComponentData1.pagination} returns pagination1
every{feedComponentData1.feedList} returns feedDataList1
every{feedComponentData2.pagination} returns pagination2
every{feedComponentData2.feedList} returns feedDataList2
mockRemoteConfigProviderImplement = mockk(relaxed = true)
mockFeedDataStoreImplement = spyk(FeedDataStoreImplement())
instance = spyk(InsertRemoteWidgetUseCase(mockRemoteConfigProviderImplement, mockFeedDataStoreImplement))
}
describe("invoke()") {
context("getHomeWidgetVisible is true") {
it("FeedData with CardType.REMOTE_CONFIG_WIDGET is inserted") {
remoteWidgetUrl = "www.google.com"
remoteWidgetVisible = true
every { mockRemoteConfigProviderImplement.getHomeWidgetURL() } returns remoteWidgetUrl
every { mockRemoteConfigProviderImplement.getHomeWidgetVisible() } returns remoteWidgetVisible
mockFeedDataStoreImplement.update(feedComponentData1)
mockFeedDataStoreImplement.update(feedComponentData2)
val feedDataListBefore = mockFeedDataStoreImplement.getData()
val feedDataListAfter = instance.invoke()
assert(feedDataListAfter[0].type == CardType.REMOTE_CONFIG_WIDGET)
assert(feedDataListBefore.size == feedDataListAfter.size + 1)
}
}
context("getHomeWidgetVisible is false") {
it("FeedData with CardType.REMOTE_CONFIG_WIDGET is removed") {
remoteWidgetUrl = "www.google.com"
remoteWidgetVisible = false
every { mockRemoteConfigProviderImplement.getHomeWidgetURL() } returns remoteWidgetUrl
every { mockRemoteConfigProviderImplement.getHomeWidgetVisible() } returns remoteWidgetVisible
mockFeedDataStoreImplement.update(feedComponentData1)
mockFeedDataStoreImplement.update(feedComponentData2)
val feedData = FeedData()
feedData.type = CardType.REMOTE_CONFIG_WIDGET
mockFeedDataStoreImplement.insertRemoteWidgetCard(feedData)
val feedDataListBefore = mockFeedDataStoreImplement.getData()
val feedDataListAfter = instance.invoke()
assert(feedDataListAfter[0].type != CardType.REMOTE_CONFIG_WIDGET)
assert(feedDataListBefore[0].type == CardType.REMOTE_CONFIG_WIDGET)
assert(feedDataListBefore.size == feedDataListAfter.size - 1)
}
}
}
})
The test cases in question runs successfully in local system. But with Bitrise, both of them fails with following:
java.lang.AssertionError: Assertion failed
at sg.com.sph.appbt.views.bnavigation.home.InsertRemoteWidgetUseCaseTest$1$2$1$1.invoke(InsertRemoteWidgetUseCaseTest.kt:74)
at sg.com.sph.appbt.views.bnavigation.home.InsertRemoteWidgetUseCaseTest$1$2$1$1.invoke(InsertRemoteWidgetUseCaseTest.kt:17)
at org.spekframework.spek2.runtime.scope.TestScopeImpl.execute(Scopes.kt:94)
at org.spekframework.spek2.runtime.Executor$execute$$inlined$executeSafely$lambda$1$1.invokeSuspend(Executor.kt:52)
at ???(Coroutine boundary.?(?)
at kotlinx.coroutines.DeferredCoroutine.await$suspendImpl(Builders.common.kt:99)
at org.spekframework.spek2.runtime.ExecutorsKt$doRunBlocking$1.invokeSuspend(executors.kt:8)
Caused by: java.lang.AssertionError: Assertion failed
at sg.com.sph.appbt.views.bnavigation.home.InsertRemoteWidgetUseCaseTest$1$2$1$1.invoke(InsertRemoteWidgetUseCaseTest.kt:74)
at sg.com.sph.appbt.views.bnavigation.home.InsertRemoteWidgetUseCaseTest$1$2$1$1.invoke(InsertRemoteWidgetUseCaseTest.kt:17)
at org.spekframework.spek2.runtime.scope.TestScopeImpl.execute(Scopes.kt:94)
at org.spekframework.spek2.runtime.Executor$execute$$inlined$executeSafely$lambda$1$1.invokeSuspend(Executor.kt:52)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at kotlinx.coroutines.DispatchedTask.run(Dispatched.kt:241)
at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:594)
at kotlinx.coroutines.scheduling.CoroutineScheduler.access$runSafely(CoroutineScheduler.kt:60)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:740)
Problem Statement
This executes well at local system without errors but fails with Bitrise on cloud.
Is there any silver bullet solution for this local vs remote issue.
Could this be due to different environment, if yes then which all components's versions should be checked.
Related
I need to view NFT-image with all metadata. I decide to call tokenURI() function like it, but it's ain't working
private fun getNFTMetadata() = viewModelScope.launch(Dispatchers.IO){
//tokenURI -- by token ID
val web3j: Web3j = createWeb3j() ?: return#launch
var ids = listOf<Uint256>(Uint256.DEFAULT)
val function: org.web3j.abi.datatypes.Function = org.web3j.abi.datatypes.Function(
"tokenURI",
ids,
listOf()
)
val encodedFunction = FunctionEncoder.encode(function)
val response: EthCall = web3j.ethCall(
Transaction.createEthCallTransaction(WALLET_ADDRESS, CONTRACT_ADDRESS, encodedFunction),
LATEST
).sendAsync().get()
if (response.value != null){
state.value = response.value
} else {
state.value = "NAN"
}
}
private fun createWeb3j(): Web3j? {
val webSocketService = WebSocketService(WEB_SOCKET_URL, true)
try {
webSocketService.connect()
} catch (e: ConnectException) {
e.printStackTrace()
}
return Web3j.build(webSocketService)
}
I really don't know how to call that function rightly. Help me please!)
I found my mistake. I had change received parameters.
private fun getNFTMetadata() = viewModelScope.launch(Dispatchers.IO){
//tokenURI -- by token ID
val web3j: Web3j = createWeb3j() ?: return#launch
val big: Uint256 = Uint256(1)
val function: org.web3j.abi.datatypes.Function = org.web3j.abi.datatypes.Function(
"tokenURI",
listOf(big),
listOf(object : TypeReference<Utf8String>() {})
)
val encodedFunction = FunctionEncoder.encode(function)
val response: EthCall = web3j.ethCall(
Transaction.createEthCallTransaction(WALLET_ADDRESS, CONTRACT_ADDRESS, encodedFunction),
LATEST
).sendAsync().get()
if (response.value != null){
state.value = response.value
} else {
state.value = "NAN"
}
}
So I have been tackling this issue for a couple of days now. I have a Data class that is used to send information back into the API. In this instance, I have this x amount of fields. In these fields, there are three List fields with different types, as such.
The Data Classes
data class ApiSurveySiteUpdateBody(
#SerializedName("UserId") val userId: Int,
#SerializedName("SStatusId") val sStatusId: Int,
#SerializedName("SSId") val sSId: Int,
#SerializedName("SPONum") val sPoNum: Int,
#SerializedName("WorkPerformanceTypeId") val workPerformTypeId: Int,
#SerializedName("SSAddressId") val sSAddressId: Int,
#SerializedName("WorktoPerformDate") val workToBePerformedDate: String,
#SerializedName("CableRun") val cableRun: String,
#SerializedName("Entrance") val entranceInfo: String,
#SerializedName("DoorLockHardware") val doorLockHardware: String,
#SerializedName("HandicapOperator") val handicapOperator: String,
#SerializedName("DeviceComplete") val completedPrimaryDeviceList: List<Int>,
#SerializedName("TemplateId") val templateId: Int,
#SerializedName("NewDeviceList") val newDeviceList: List<ApiNewDeviceList> = emptyList(),
#SerializedName("UpdateDeviceList") val updateDeviceList: List<ApiUpdateDeviceList> = emptyList(),
#SerializedName("RemoveDeviceList") val removedDeviceList: List<ApiRemovedDeviceList> = emptyList()
)
The Converter function
private fun getSomeRequestBody(
dbInfo: DbFormWithEList,
apiSurveySiteMedias: List<ApiSSMediaInfo>
)
: ApiSSUpdateBody {
val updateRequestApi = ApiSSUpdateBody(
userId = dbInfo.sSDbInfo.userId,
sSId = dbInfo.sSDbInfo.sSId,
sStatusId = dbInfo.sStatusDbInfo.sSId,
sPoNum = dbInfo.sSDbInfo.sPoNumber,
workPerformTypeId = dbInfo.sSDbInfo.workPerformTypeId,
sSAddressId = dbInfo.sSDbInfo.sSAddressId,
workToBePerformedDate = dbInfo.sSDbInfo.workToBePerformedDate,
cableRun = dbInfo.sSDbInfo.cableRun,
entranceInfo = dbInfo.sSDbInfo.entranceInfo,
doorLockHardware = dbInfo.sSDbInfo.doorLockHardware,
handicapOperator = dbInfo.sSDbInfo.handicapOperator,
completedPrimaryDeviceList = dbInfo.sSDbInfo.completedPrimaryDeviceList.toIntList(),
templateId = dbInfo.sSDbInfo.templateId,
newDeviceList = List(dbInfo.equipmentList.size) { i -> // “NewDeviceList”
val dbEquipmentInfo = dbInfo.equipmentList[i].sSEquipmentDbInfo
Log.d(TAG, "NewDeviceListDB $dbEquipmentInfo")
val secondaryDeviceCheckedStatus = dbEquipmentInfo.secondaryDeviceCheckedStatus
val isDuplicateDeviceInUpdatePhase = dbEquipmentInfo.isDeviceUpdateMode
if (sDeviceCheckedS == CHECKED_YES && !isDuplicateDUP){
val newDeviceListRequestBody = ApiNewDeviceList(
secondaryDeviceId = dbEquipmentInfo.secondaryDeviceId,
deviceInstanceId = dbEquipmentInfo.deviceInstanceId.toString(),
mediaNameList = dbEquipmentInfo.mediaNames,
deviceSerialNumber = dbEquipmentInfo.deviceSerialNumber,
devicePartNumber = dbEquipmentInfo.devicePartNumber,
deviceManufacturerName = dbEquipmentInfo.deviceManufacturer,
deviceInstallationDate = DateUtil.dateToStringUTCSS(dbEquipmentInfo.deviceInstallationDate),
deviceLocation = dbEquipmentInfo.locationInfo,
deviceTechnicianNotes = dbEquipmentInfo.deviceTechnicianNotes
)
Log.d(TAG, "newDeviceListRequestBodyAPI $newDeviceListRequestBody")
newDeviceListRequestBody
}
else if (sDeviceCheckedS == CHECKED_NO){
apiDeviceListMapperUpdateSS.sendDeviceNotExistsInNewDeviceList(dbEquipmentInfo)
}
else {
apiDeviceListMapperUpdateSS.sendEmptyNewDeviceList()
}
},
updateDeviceList = (List(dbInfo.equipmentList.size) { i ->
val dbEquipmentInfo = dbInfo.equipmentList[i].sSEquipmentDbInfo
Log.d("UpdatingSiteSurvey", "UpdateDeviceListDB $dbEquipmentInfo")
val secondaryDeviceCheckedStatus = dbEquipmentInfo.secondaryDeviceCheckedStatus
val isDuplicateDeviceInUpdatePhase = dbEquipmentInfo.isDeviceUpdateMode
if (secondaryDeviceCheckedStatus == CHECKED_YES && isDuplicateDeviceInUpdatePhase){
val updateDeviceListRequestBody = ApiUpdateDeviceList(
deviceEquipmentId = dbEquipmentInfo.deviceEquipmentId,
secondaryDeviceId = dbEquipmentInfo.secondaryDeviceId,
deviceInstanceId = dbEquipmentInfo.deviceInstanceId.toString(),
deviceSerialNumber = dbEquipmentInfo.deviceSerialNumber,
devicePartNumber = dbEquipmentInfo.devicePartNumber,
deviceManufacturerName = dbEquipmentInfo.deviceManufacturer,
deviceInstallationDate = DateUtil.dateToStringUTCSiteSurvey(dbEquipmentInfo.deviceInstallationDate),
deviceLocation = dbEquipmentInfo.locationInfo,
deviceTechnicianNotes = dbEquipmentInfo.deviceTechnicianNotes
)
Log.d(TAG, "updateDeviceListRequestBodyAPI $updateDeviceListRequestBody")
updateDeviceListRequestBody
} else Unit.apply { } //<- the issue is here
}) as List<ApiUpdateDeviceList>,
removedDeviceList = List(dbInfo.sSDbInfo.removedDeviceList.size) { i ->
val dbRemovedMediaItem = dbInfo.sSDbInfo.removedDeviceList[i]
Log.d(TAG, "RemovedListDB $dbRemovedMediaItem")
if (dbRemovedMediaItem.removedDeviceEquipmentId == null && dbRemovedMediaItem.removedMediaName.isNullOrEmpty()){
val removeDevice = apiDeviceListMapperUpdateSiteSurvey.removeDevice(dbRemovedMediaItem)
Log.d(TAG, "removeDevice $removeDevice")
removeDevice
}else{
val removeMediaForExistingDevice = apiDeviceListMapperUpdateSS.removeMediaForExistingDevice(dbRemovedMediaItem)
Log.d(TAG, "removeMediaForExistingDevice $removeMediaForExistingDevice")
removeMediaForExistingDevice
}
}
)
Log.d(TAG, "MainUpdateRequestAPI $updateRequestApi")
return updateRequestApi
}
The goal is to have the else statement that is highlighted to return an emptyList "[]" to that updateDeviceList field. I have tried a few ways but never was able to return that exact empty list "[]". Any help will be appreciated. Thank you.
I can't tell if you want (1) the whole list to be invalidated and become empty if any item in the iteration fails the if check, or if you just want (2) to filter out items that fail the if check. But here's how I would approach each of those tasks.
I am breaking out the conversion between DbEquipmentInfo and ApiUpdateDeviceList into a separate extension function (fun DbEquipmentInfo.toApiUpdateDeviceList(): ApiUpdateDeviceList). Not just to avoid code repetition, but also to keep the logic code easy to read, and make the project's code more maintainable in general.
1.
val isValid = dbInfo.equipmentList.all { dbEquipmentInfo ->
val secondaryDeviceCheckedStatus = dbEquipmentInfo.secondaryDeviceCheckedStatus
val isDuplicateDeviceInUpdatePhase = dbEquipmentInfo.isDeviceUpdateMode
secondaryDeviceCheckedStatus == CHECKED_YES && isDuplicateDeviceInUpdatePhase
}
updateDeviceList =
if (isValid) dbInfo.equipmentList.map { it.toApiUpdateDeviceList() }
else emptyList()
updateDeviceList = dbInfo.equipmentList.filter { dbEquipmentInfo ->
val secondaryDeviceCheckedStatus = dbEquipmentInfo.secondaryDeviceCheckedStatus
val isDuplicateDeviceInUpdatePhase = dbEquipmentInfo.isDeviceUpdateMode
secondaryDeviceCheckedStatus == CHECKED_YES && isDuplicateDeviceInUpdatePhase
}.map { it.toApiUpdateDeviceList() }
I'm doing a small project to learn flow and the latest Android features, and I'm currently facing the viewModel's testing, which I don't know if I'm performing correctly. can you help me with it?
Currently, I am using a use case to call the repository which calls a remote data source that gets from an API service a list of strings.
I have created a State to control the values in the view model:
data class StringItemsState(
val isLoading: Boolean = false,
val items: List<String> = emptyList(),
val error: String = ""
)
and the flow:
private val stringItemsState = StringtemsState()
private val _stateFlow = MutableStateFlow(stringItemsState)
val stateFlow = _stateFlow.asStateFlow()
and finally the method that performs all the logic in the viewModel:
fun fetchStringItems() {
try {
_stateFlow.value = stringItemsState.copy(isLoading = true)
viewModelScope.launch(Dispatchers.IO) {
val result = getStringItemsUseCase.execute()
if (result.isEmpty()) {
_stateFlow.value = stringItemsState
} else {
_stateFlow.value = stringItemsState.copy(items = result)
}
}
} catch (e: Exception) {
e.localizedMessage?.let {
_stateFlow.value = stringItemsState.copy(error = it)
}
}
}
I am trying to perform the test following the What / Where / Then pattern, but the result is always an empty list and the assert verification always fails:
private val stringItems = listOf<String>("A", "B", "C")
#Test
fun `get string items - not empty`() = runBlocking {
// What
coEvery {
useCase.execute()
} returns stringItems
// Where
viewModel.fetchStringItems()
// Then
assert(viewModel.stateFlow.value.items == stringItems)
coVerify(exactly = 1) { viewModel.fetchStringItems() }
}
Can someone help me and tell me if I am doing it correctly? Thanks.
Currently, I am writing some test for my proto datastore. The only problem I have here is that I can't call a specific function because then my test fails / crashes. I find this very confusing, because all my other functions seem to work, except resetDatastore
Here is my code:
Repository
private companion object {
private const val SHOP_FILTER_PRODUCT_DATASTORE: String = "shop_filter_product_datastore_test"
private const val SHOP_FILTER_LIST_DATASTORE: String = "shop_filter_list_datastore_test"
private const val SHOP_FILTER_BTN_DATASTORE: String = "shop_filter_btn_datastore_test"
}
private val testNonVolatileProductDataStore = context.createDataStore(
fileName = SHOP_FILTER_PRODUCT_DATASTORE,
serializer = ShopFilterProductSerializer
)
private val testNonVolatileListDataStore = context.createDataStore(
fileName = SHOP_FILTER_LIST_DATASTORE,
serializer = ShopFilterListSerializer
)
private val testNonVolatileBtnDataStore = context.createDataStore(
fileName = SHOP_FILTER_BTN_DATASTORE,
serializer = ShopFilterBtnSerializer
)
override suspend fun setValueProduct(newProduct: ShopFilterTempHolder) {
if (newProduct.id == null || newProduct.mQuery == null) return
testNonVolatileProductDataStore.updateData { preferences ->
preferences.toBuilder().apply {
positionId = newProduct.id!!
query = newProduct.mQuery
}.build()
}
}
override suspend fun setValueList(newList: ShopFilterTempHolder) {
if (newList.id == null || newList.mQuery == null) return
testNonVolatileListDataStore.updateData { preferences ->
preferences.toBuilder().apply {
positionId = newList.id!!
query = newList.mQuery
mQueryDirection = newList.mQueryDirection
}.build()
}
}
override suspend fun setShopFilterBtn(value: Boolean) {
testNonVolatileBtnDataStore.updateData { preferences ->
preferences.toBuilder().apply {
isChecked = value
}.build()
}
}
override suspend fun peekProductValue(): ShopFilterTempHolder {
val temp = shopFilterProduct.first()
return ShopFilterTempHolder(temp.positionId, temp.query)
}
override suspend fun peekListValue(): ShopFilterTempHolder {
val temp = shopFilterList.first()
return ShopFilterTempHolder(temp.positionId, temp.query, temp.mQueryDirection)
}
override suspend fun peekBtnValue(): Boolean = mappedShopFilterBtn.first()
override suspend fun resetDatastore() {
testNonVolatileProductDataStore.updateData { preferences ->
preferences.toBuilder().apply {
positionId = Constants.SHOP_FILTER_DEFAULT_PRODUCT_ID
query = Constants.SHOP_FILTER_DEFAULT_PRODUCT_QUERY
}.build()
}
testNonVolatileListDataStore.updateData { preferences ->
preferences.toBuilder().apply {
positionId = Constants.SHOP_FILTER_DEFAULT_LIST_ID
query = Constants.SHOP_FILTER_DEFAULT_LIST_QUERY
mQueryDirection = Constants.SHOP_FILTER_DEFAULT_LIST_QUERY_DIRECTION
}.build()
}
testNonVolatileBtnDataStore.updateData { preferences ->
preferences.toBuilder().apply {
isChecked = true
}.build()
}
}
Test
#Test
fun `values should be set to default`() = runBlocking {
val newBtn = false
val newList = ShopFilterTempHolder(0, "testString", 0)
val newProduct = ShopFilterTempHolder(0, "testString", 0)
shopFilterValidator.tempBtnFilterValue = newBtn
shopFilterValidator.tempListFilter = newList
shopFilterValidator.tempProductFilter = newProduct
shopFilterValidator.setNewBtnFilter()
shopFilterValidator.setNewListFilter()
shopFilterValidator.setNewProductFilter()
assertEquals(newProduct, shopFilterDataStoreRepository.peekProductValue())
assertEquals(newList, shopFilterDataStoreRepository.peekListValue())
assertEquals(newBtn, shopFilterDataStoreRepository.peekBtnValue())
shopFilterValidator.deleteAllValues()
assertEquals(defautTempProductFilter, shopFilterDataStoreRepository.peekProductValue())
assertEquals(defaultTempListFilter, shopFilterDataStoreRepository.peekListValue())
assertEquals(defaultTempBtnFilterValue, shopFilterDataStoreRepository.peekBtnValue())
}
Stacktrace
Exception in thread "DefaultDispatcher-worker-2 #coroutine#5" java.io.IOException: Unable to rename C:\Users\Censored\AppData\Local\Temp\robolectric-Method_values_should_be_set_to_default1366629743868428403\com.example.app-dataDir\files\datastore\shop_filter_product_datastore_test.tmp.This likely means that there are multiple instances of DataStore for this file. Ensure that you are only creating a single instance of datastore for this file.
at androidx.datastore.core.SingleProcessDataStore.writeData$datastore_core(SingleProcessDataStore.kt:303)
at androidx.datastore.core.SingleProcessDataStore.transformAndWrite(SingleProcessDataStore.kt:280)
at androidx.datastore.core.SingleProcessDataStore$actor$1.invokeSuspend(SingleProcessDataStore.kt:165)
(Coroutine boundary)
at kotlinx.coroutines.CompletableDeferredImpl.await(CompletableDeferred.kt:86)
at androidx.datastore.core.SingleProcessDataStore$updateData$2.invokeSuspend(SingleProcessDataStore.kt:96)
at androidx.datastore.core.SingleProcessDataStore.updateData(SingleProcessDataStore.kt:96)
at com.example.app.repository.FakeDataStoreRepositoryImpl.deleteDataStore(FakeDataStoreRepositoryImpl.kt:86)
at com.example.app.data.models.validator.ShopFilterValidator$deleteAllValues$1.invokeSuspend(ShopFilterValidator.kt:80)
not sure if that could help you, but in my case the problem occurred when running tests on Windows machine and wasn't there when switching to Linux or executing the test on the emulator instead
I am not able to retrieve data.
Is there any way by which we can access our database and we can check what we have inserted so far.
In this code I am trying to insert the latest calculation I did in my calculator with the number of my transaction. Using Coroutines, Room and View Model.
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.navigation.NavController
import com.kotlin_developer.calculator.database.CalculationDatabaseDao
import com.kotlin_developer.calculator.database.CalculatorHistory
import kotlinx.coroutines.*
import timber.log.Timber
class CalculatorViewModel(
val
database: CalculationDatabaseDao,
application: Application
) : AndroidViewModel(application) {
var operatorEnabled: Boolean = false
var firstResult: Double = 0.0
var operator: Char = ' '
var ifNumeric: Boolean = true
var secondResultLenght = 0
// First step of coroutines is to create job, this can cancel all the coroutines started by this view model
private var viewModelJob = Job()
// Second step is to create the scope where we want to run our code
// Scope determines what thread the coroutines will run on, it also needs to know about the job
private val uiScope = CoroutineScope(Dispatchers.Main + viewModelJob)
// private val history = database.getAllCalculation()
private var _totalTransaction = MutableLiveData<Int>()
val totalTransaction: LiveData<Int>
get() = _totalTransaction
//Getting currentCalculation
private var _currentCalculation = MutableLiveData<String>()
val currentCalculation: LiveData<String>
get() = _currentCalculation
//Getting current result
private var _currentResult = MutableLiveData<String>()
val currentResult: LiveData<String>
get() = _currentResult
val navControler = MutableLiveData<NavController>()
private val _secondResult = MutableLiveData<Double>()
val secondResult: LiveData<Double>
get() = _secondResult
private var _resultTextValue = MutableLiveData<Double>()
val resultTextValue: LiveData<Double>
get() = _resultTextValue
private var _lastHistory = MutableLiveData<CalculatorHistory>()
val lastHistory: LiveData<CalculatorHistory>
get() = _lastHistory
val totalCalculation = mutableListOf<String>()
init {
Timber.i("Calculator View Model created")
_resultTextValue.value = 0.0
_secondResult.value = 0.0
_totalTransaction.value = 0
}
fun insertData() {
uiScope.launch {
val newHistory = CalculatorHistory(
totalTransaction.value?.toLong()!!,
totalCalculation[totalTransaction.value!! - 1]
)
insert(newHistory)
}
}
private suspend fun insert(newHistory: CalculatorHistory) {
withContext(Dispatchers.IO) {
database.insert(newHistory)
Timber.i("Data Inserted")
}
}
internal fun initializeHistory() {
uiScope.launch {
_lastHistory.value = getHistoryFromDatabase()
}
Timber.i("${lastHistory.value?.transactionNumber} and ${lastHistory.value?.calculation}")
}
private suspend fun getHistoryFromDatabase(): CalculatorHistory? {
return withContext(Dispatchers.IO) {
var lastCalculation = database.get(1)
lastCalculation
}
Timber.i("${lastHistory.value?.transactionNumber} and ${lastHistory.value?.calculation}")
}
fun calculator(operator: Char): Double {
return when (operator) {
'+' -> firstResult.plus(secondResult.value ?: 0.0)
'-' -> firstResult.minus(secondResult.value ?: 0.0)
'*' -> firstResult.times(secondResult.value ?: 1.0)
'/' -> firstResult.div(secondResult.value ?: 1.0)
else -> firstResult.rem(secondResult.value ?: 1.0)
}
}
fun createCalculation() {
ifNumeric = false
operatorEnabled = false
_resultTextValue.value = calculator(operator)
//This we can use in future to create a list of calculation
totalCalculation.add(
totalTransaction.value!!,
"${currentCalculation.value}=${_resultTextValue.value}"
)
_currentResult.value = totalCalculation[totalTransaction.value!!]
insertData()
firstResult = _resultTextValue.value ?: 0.0
_totalTransaction.value = _totalTransaction.value?.plus(1)
_secondResult.value = 0.0
secondResultLenght = 0
}
fun seprateNumber(number: Double) {
if (operatorEnabled) {
if (ifNumeric) {
_secondResult.value = number + (secondResult.value?.times(10.0))!!
} else {
_secondResult.value = number
}
} else {
firstResult = number + (firstResult * 10)
}
}
fun clearText() {
_resultTextValue.value = 0.0
_currentResult.value = ""
firstResult = 0.0
_secondResult.value = 0.0
secondResultLenght = 0
operator = ' '
operatorEnabled = false
ifNumeric = false
_currentCalculation.value = ""
}
fun ifSeprateNumber(number: Double) {
seprateNumber(number)
if (operatorEnabled) {
secondCalculationText()
} else {
_currentCalculation.value = firstResult.toString()
}
ifNumeric = true
}
fun secondCalculationText() {
_currentCalculation.value = _currentCalculation.value
?.removeRange(
_currentCalculation.value!!.length -
secondResultLenght, _currentCalculation.value!!.length
)
_currentCalculation.value =
"${_currentCalculation.value}${secondResult.value.toString()}"
secondResultLenght = secondResult.value.toString().length
ifNumeric = true
}
fun addTextToField() {
ifNumeric = false
operatorEnabled = true
_currentCalculation.value = "${_currentCalculation.value}${operator}"
}
override fun onCleared() {
super.onCleared()
//This cancels all the coroutines when the view model is getting closed
viewModelJob.cancel()
Timber.i("Calculator ViewModel destroyed")
}
}
I think you are doing too many things in your ViewModel. If you want to see what the data in your database is, you should have an activity (the View, the activity which creates, updates and handles UI events) that observes your livedata in the viewmodel. Everytime you insert values into your database, regardless of the time it takes, once it's done it will trigger a callback to the observer and you will get the new values. I would start with this, so you can keep track of what you are inserting in your database, and it's the starting point to then using those values.
Would be something like this, from your View/Activity:
yourViewModel.variableYouWant.observe(this, Observer { yourValue ->
//do something with yourValue
})