I am trying to display data on two different fragments from viewmodels: Recent Races and Upcoming Races. i wrote a filter function to filter the races that are yet to start and the ones that finished. Upcoming races works perfectly, when there is a change in api endpoint it removes the race from upcoming races list. but the problem is that it wont add it to recent races.
here is my code in RecentRacesViewModel
private fun getDetails() {
getRaceDetailsUseCase().onEach { result ->
when (result) {
is Resource.Success -> {
val filteredList = result.data.filter {
val time = Calendar.getInstance().time
val formatterCurrentTime = SimpleDateFormat("yyyy-MM-dd")
val formatterNow = DateTimeFormatter.ofPattern("yyyy-MM-dd")
val currentTime = formatterCurrentTime.format(time)
val dateNow = LocalDate.parse(currentTime, formatterNow)
val dateFromModel = it.date
val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd")
val date = LocalDate.parse(dateFromModel, formatter)
dateNow >= date
}
_state1.value = Resource.Success(filteredList)
}
is Resource.Error -> {
_state1.value = Resource.Error("woops!")
}
is Resource.Loading -> {
_state1.value = Resource.Loading(true)
}
}
}.launchIn(viewModelScope)
}
thanks for help
Edit: adding the UseCase:
class RaceDetailsUseCase #Inject constructor(
private val repository: RaceResultsRepository
) {
operator fun invoke(): Flow<Resource<List<RaceDomain>>> = flow {
try {
emit(Resource.Loading(true))
val raceData = repository.GetRaceResultsRepository()
emit(Resource.Success(raceData))
} catch (e: HttpException) {
Log.d("tag", "error")
} catch (e: IOException) {
Log.d("tag", "io error")
}
}
}
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"
}
}
I've build clean architectured app (with mvvm, use cases, compose). I've a CoinListViewModel for list all crypto coins by using CoinPaprika API. It is like;
#HiltViewModel
class CoinListViewModel #Inject constructor(
private val getCoinsUseCase: GetCoinsUseCase
) : ViewModel() {
private val _state = mutableStateOf(CoinListState())
val state: State<CoinListState> = _state
init {
getCoins()
}
private fun getCoins() {
getCoinsUseCase().onEach { result ->
when (result) {
is Resource.Success -> {
_state.value = CoinListState(coins = result.data ?: emptyList())
}
is Resource.Error -> {
_state.value = CoinListState(
error = result.message ?: "An unexpected error occured"
)
}
is Resource.Loading -> {
_state.value = CoinListState(isLoading = true)
}
}
}.launchIn(viewModelScope)
}
}
And this viewmodel is used in my CoinListScreen like;
#Composable
fun CoinListScreen(
navController: NavController,
viewModel: CoinListViewModel = hiltViewModel()
) {
val state = viewModel.state.value
Box(modifier = Modifier.fillMaxSize()) {
LazyColumn(modifier = Modifier.fillMaxSize()) {
items(state.coins) { coin ->
CoinListItem(
coin = coin,
onItemClick = {
navController.navigate(Screen.CoinDetailScreen.route + "/${coin.id}")
}
)
}
}
if(state.error.isNotBlank()) {
Text(
text = state.error,
color = MaterialTheme.colors.error,
textAlign = TextAlign.Center,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 20.dp)
.align(Alignment.Center)
)
}
if(state.isLoading) {
CircularProgressIndicator(modifier = Modifier.align(Alignment.Center))
}
}
}
And that is my GetCoinsUseCase:
class GetCoinsUseCase #Inject constructor(
private val repository: CoinRepository
) {
operator fun invoke(): Flow<Resource<List<Coin>>> = flow {
try {
emit(Resource.Loading<List<Coin>>())
val coins = repository.getCoins().map { it.toCoin() }
emit(Resource.Success<List<Coin>>(coins))
} catch(e: HttpException) {
emit(Resource.Error<List<Coin>>(e.localizedMessage ?: "An unexpected error occured"))
} catch(e: IOException){
emit(Resource.Error<List<Coin>>("Couldn't reach to server. Check your internet connection."))
}
}
}
I have 2 questions;
How can I make this API call every 3 seconds ?
How can I continue to do API call on background in phone ?
You could add a loop with a delay in your use case:
class GetCoinsUseCase #Inject constructor(
private val repository: CoinRepository
) {
operator fun invoke(): Flow<Resource<List<Coin>>> = flow {
while (currentCoroutineContext().isActive) {
try {
emit(Resource.Loading<List<Coin>>())
val coins = repository.getCoins().map { it.toCoin() }
emit(Resource.Success<List<Coin>>(coins))
} catch(e: HttpException) {
emit(Resource.Error<List<Coin>>(e.localizedMessage ?: "An unexpected error occured"))
} catch(e: IOException){
emit(Resource.Error<List<Coin>>("Couldn't reach to server. Check your internet connection."))
}
// Wait until next request
delay(3000)
}
}
}
You could also move the loop and delay call around depending on the precise functionality you are looking for. For example, to stop once there is an error you could do
class GetCoinsUseCase #Inject constructor(
private val repository: CoinRepository
) {
operator fun invoke(): Flow<Resource<List<Coin>>> = flow {
emit(Resource.Loading<List<Coin>>())
try {
while (currentCoroutineContext().isActive) {
val coins = repository.getCoins().map { it.toCoin() }
emit(Resource.Success<List<Coin>>(coins))
// Wait until next request
delay(3000)
}
} catch(e: HttpException) {
emit(Resource.Error<List<Coin>>(e.localizedMessage ?: "An unexpected error occured"))
} catch(e: IOException){
emit(Resource.Error<List<Coin>>("Couldn't reach to server. Check your internet connection."))
}
}
}
This should already naturally "run in the background" as long as your app is running and you are not cancelling the coroutine manually when going in the background.
The way to repeat a task in Jetpack Compose is
LaunchedEffect(Unit) {
while(true) {
vm.someMethod()
delay(3000)
}
}
The someMethod is the method that fetches the coin data.
I want to display a loading indicator when I download data from the API. However, when this happens, the indicator often stops. How can I change this or what could be wrong? Basically, I fetch departure times and process them (E.g. I convert hex colors to Jetpack Compose color, or unix dates to Date type, etc.) and then load them into a list and display them.
#Composable
fun StopScreen(
unixDate: Long? = null,
stopId: String,
viewModel: MainViewModel = hiltViewModel()
) {
LaunchedEffect(Unit) {
viewModel.getArrivalsAndDeparturesForStop(
unixDate,
stopId,
false
)
}
val isLoading by remember { viewModel.isLoading }
if (!isLoading) {
//showData
} else {
LoadingView()
}
}
#Composable
fun LoadingView() {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier.fillMaxSize()
) {
CircularProgressIndicator(color = MaterialTheme.colors.primary)
}
}
And the viewmodel where I process the data:
#HiltViewModel
class MainViewModel #Inject constructor(
private val mainRepository: MainRepository
) : ViewModel() {
var stopTimesList = mutableStateOf<MutableList<StopTime>>(arrayListOf())
var alertsList = mutableStateOf<MutableList<Alert>>(arrayListOf())
var loadError = mutableStateOf("")
var isLoading = mutableStateOf(false)
var isRefreshing = mutableStateOf(false)
fun getArrivalsAndDeparturesForStop(unixDate: Long? = null, stopId: String, refresh: Boolean) {
viewModelScope.launch {
if (refresh) {
isRefreshing.value = true
} else {
isLoading.value = true
}
val result = mainRepository.getArrivalsAndDeparturesForStop(stopId = stopId, time = unixDate)
when (result) {
is Resource.Success -> {
//I temporarily store the data here, so that the screen is only refreshed on reload when all the new data has arrived and loaded
var preStopTimes: MutableList<StopTime> = arrayListOf()
var preAlertsList: MutableList<Alert> = arrayListOf()
if (result.data!!.stopTimes != null && result.data!!.alerts != null) {
var count = 0
val countAll =
result.data!!.stopTimes!!.count() + result.data!!.alertIds!!.count()
if (countAll == 0) {
loadError.value = ""
isLoading.value = false
isRefreshing.value = false
}
//ALERTS
for (alert in result.data!!.data.alerts) {
preAlertsList.add(alert)
count += 1
if (count == countAll) {
stopTimesList.value = preStopTimes
alertsList.value = preAlertsList
loadError.value = ""
isLoading.value = false
isRefreshing.value = false
}
}
for (stopTime in result.data!!.stopTimes!!) {
preStopTimes.add(stopTime)
count += 1
if (count == countAll) {
stopTimesList.value = preStopTimes
alertsList.value = preAlertsList
loadError.value = ""
isLoading.value = false
isRefreshing.value = false
}
}
} else {
loadError.value = "Error"
isLoading.value = false
isRefreshing.value = false
}
}
is Resource.Error -> {
loadError.value = result.message!!
isLoading.value = false
isRefreshing.value = false
}
}
}
}
}
Repository:
#ActivityScoped
class MainRepository #Inject constructor(
private val api: MainApi
) {
suspend fun getArrivalsAndDeparturesForStop(stopId: String,time: Long? = null): Resource<ArrivalsAndDeparturesForStop> {
val response = try {
api.getArrivalsAndDeparturesForStop(
stopId,
time
)
} catch (e: Exception) { return Resource.Error(e.message!!)}
return Resource.Success(response)
}
}
My take is that your Composable recomposes way too often. Since you're updating your state within your for loops. Otherwise, it might be because your suspend method in your MainRepository is not dispatched in the right thread.
I feel you didn't yet grasp how Compose works internally (and that's fine, it's a new topic anyway). I'd recommend hoisting a unique state instead of having several mutable states for all your properties. Then build it internally in your VM to then notify the view when the state changes.
Something like this:
data class YourViewState(
val stopTimesList: List<StopTime> = emptyList(),
val alertsList: List<Alert> = emptyList(),
val isLoading: Boolean = false,
val isRefreshing: Boolean = false,
val loadError: String? = null,
)
#HiltViewModel
class MainViewModel #Inject constructor(
private val mainRepository: MainRepository
) : ViewModel() {
var viewState by mutableStateOf<YourViewState>(YourViewState())
fun getArrivalsAndDeparturesForStop(unixDate: Long? = null, stopId: String, refresh: Boolean) {
viewModelScope.launch {
viewState = if (refresh) {
viewState.copy(isRefreshing = true)
} else {
viewState.copy(isLoading = true)
}
when (val result = mainRepository.getArrivalsAndDeparturesForStop(stopId = stopId, time = unixDate)) {
is Resource.Success -> {
//I temporarily store the data here, so that the screen is only refreshed on reload when all the new data has arrived and loaded
val preStopTimes: MutableList<StopTime> = arrayListOf()
val preAlertsList: MutableList<Alert> = arrayListOf()
if (result.data!!.stopTimes != null && result.data!!.alerts != null) {
var count = 0
val countAll = result.data!!.stopTimes!!.count() + result.data!!.alertIds!!.count()
if (countAll == 0) {
viewState = viewState.copy(isLoading = false, isRefreshing = false)
}
//ALERTS
for (alert in result.data!!.data.alerts) {
preAlertsList.add(alert)
count += 1
if (count == countAll) {
break
}
}
for (stopTime in result.data!!.stopTimes!!) {
preStopTimes.add(stopTime)
count += 1
if (count == countAll) {
break
}
}
viewState = viewState.copy(isLoading = false, isRefreshing = false, stopTimesList = preStopTimes, alertsList = preAlertsList)
} else {
viewState = viewState.copy(isLoading = false, isRefreshing = false, loadError = "Error")
}
}
is Resource.Error -> {
viewState = viewState.copy(isLoading = false, isRefreshing = false, loadError = result.message!!)
}
}
}
}
}
#Composable
fun StopScreen(
unixDate: Long? = null,
stopId: String,
viewModel: MainViewModel = hiltViewModel()
) {
LaunchedEffect(Unit) {
viewModel.getArrivalsAndDeparturesForStop(
unixDate,
stopId,
false
)
}
if (viewModel.viewState.isLoading) {
LoadingView()
} else {
//showData
}
}
Note that I've made a few improvements while keeping the original structure.
EDIT:
You need to make your suspend method from your MainRepository main-safe. It's likely it runs on the main thread (caller thread) because you don't specify on which dispatcher the coroutine runs.
suspend fun getArrivalsAndDeparturesForStop(stopId: String,time: Long? = null): Resource<ArrivalsAndDeparturesForStop> = withContext(Dispatchers.IO) {
try {
api.getArrivalsAndDeparturesForStop(
stopId,
time
)
Resource.Success(response)
} catch (e: Exception) {
Resource.Error(e.message!!)
}
After 6 months I figured out the exact solution. Everything was running on the main thread when I processed the data in the ViewModel. Looking into things further, I should have used Dispatchers.Default and / or Dispatchers.IO within the functions for CPU intensive / list soring / JSON parsing tasks.
https://developer.android.com/kotlin/coroutines/coroutines-adv
suspend fun doSmg() {
withContext(Dispatchers.IO) {
//This dispatcher is optimized to perform disk or network I/O outside of the main thread. Examples include using the Room component, reading from or writing to files, and running any network operations.
}
withContext(Dispatchers.Default) {
//This dispatcher is optimized to perform CPU-intensive work outside of the main thread. Example use cases include sorting a list and parsing JSON.
}
}
I have a problem for now in JetpackCompose.
The problem is, when I'm collecting the Data from a flow, the value is getting fetched from firebase like there is a listener and the data's changing everytime. But tthat's not that.
I don't know what is the real problem!
FirebaseSrcNav
suspend fun getName(uid: String): Flow<Resource.Success<Any?>> = flow {
val query = userCollection.document(uid)
val snapshot = query.get().await().get("username")
emit(Resource.success(snapshot))
}
NavRepository
suspend fun getName(uid: String) = firebase.getName(uid)
HomeViewModel
fun getName(uid: String): MutableStateFlow<Any?> {
val name = MutableStateFlow<Any?>(null)
viewModelScope.launch {
navRepository.getName(uid).collect { nameState ->
when (nameState) {
is Resource.Success -> {
name.value = nameState.data
//_posts.value = state.data
loading.value = false
}
is Resource.Failure<*> -> {
Log.e(nameState.throwable, nameState.throwable)
}
}
}
}
return name
}
The probleme is in HomeScreen I think, when I'm calling the collectasState().value.
HomeScreen
val state = rememberLazyListState()
LazyColumn(
state = state,
verticalArrangement = Arrangement.spacedBy(10.dp)
) {
items(post) { post ->
//val difference = homeViewModel.getDateTime(homeViewModel.getTimestamp())
val date = homeViewModel.getDateTime(post.timeStamp!!)
val name = homeViewModel.getName(post.postAuthor_id.toString()).collectAsState().value
QuestionCard(
name = name.toString(),
date = date!!,
image = "",
text = post.postText!!,
like = 0,
response = 0,
topic = post.topic!!
)
}
}
I can't post video but if you need an image, imagine a textField where the test is alternating between "null" and "MyName" every 0.005 second.
Check official documentation.
https://developer.android.com/kotlin/flow
Flow is asynchronous
On viewModel
private val _name = MutableStateFlow<String>("")
val name: StateFlow<String>
get() = _name
fun getName(uid: String) {
viewModelScope.launch {
//asyn call
navRepository.getName(uid).collect { nameState ->
when (nameState) {
is Resource.Success -> {
name.value = nameState.data
}
is Resource.Failure<*> -> {
//manager error
Log.e(nameState.throwable, nameState.throwable)
}
}
}
}
}
on your view
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
...
lifecycleScope.launch {
viewModel.name.collect { name -> handlename
}
}
}
I know that there are a lot of posts "How to cancel Coroutines Scope" but I couldn't find the answer for my case.
I have an Array of objects that I want to send each of them to Server using Coroutines.
What I need is, if one of my requests returns error, canceling others.
Here is my code:
private fun sendDataToServer(function: () -> Unit) {
LiabilitiesWizardSessionManager.getLiabilityAddedDocuments().let { documents ->
if (documents.isEmpty().not()) {
CoroutineScope(Dispatchers.IO).launch {
documents.mapIndexed { index, docDetail ->
async {
val result = uploadFiles(docDetail)
}
}.map {
var result = it.await()
}
}
} else function.invoke()
}
}
Below is my uploadFiles() function:
private suspend fun uploadFiles(docDetail: DocDetail): ArchiveFileResponse? {
LiabilitiesWizardSessionManager.mCreateLiabilityModel.let { model ->
val file = File(docDetail.fullFilePath)
val crmCode = docDetail.docTypeCode
val desc = docDetail.docTypeDesc
val id = model.commitmentMember?.id
val idType = 1
val createArchiveFileModel = CreateArchiveFileModel(108, desc, id, idType).apply {
this.isLiability = true
this.adaSystem = 3
}
val result = mRepositoryControllerKotlin.uploadFile(file, createArchiveFileModel)
return when (result) {
is ResultWrapper.Success -> {
result.value
}
is ResultWrapper.GenericError -> {
null
}
is ResultWrapper.NetworkError -> {
null
}
}
}
}
I know, I'm missing something.