Can I use object properties as a mutableState in kotlin compose? - kotlin

The code has been changed and reduced for this question!
I have a class called "Game" and I want to show in compose the dices value. And I want to update the composable with the new value every time a new turn is started and when that turn ends.
class Game (val board: Board, var player1: Player, var player2: Player) {
var dices = Dices()
}
class Dices (var first: Int = 0, var second: Int = 0) {
fun rollDices() {
first = rollDice() //Returns a value between 1 and 6
second = rollDice()
}
fun resetDices() {
first = 0
second = 0
}
fun areEqual() = first == second
}
I have the composable function "gameView" which calls "diceView", that is a composable function, which makes 2 boxes and puts the dices value on them.
Because "dices" is an object it doesn't get a new reference, neither does it update itself and neither can I change the value because it is inside the function.
#Composable
fun gameView(game: Game) {
paintBoard(game.board)
val dices by remember {mutableStateOf(game.dices)}
diceView(dices = dices, onClick = { game.newTurn()})
}
Any ideas on how to solve this?

If you update class with MutableState your Composables that listen these values will trigger recomposition any of these values change
class Dices(first: Int = 0, second: Int = 0) {
var firstDice by mutableStateOf(first)
var secondDice by mutableStateOf(first)
fun rollDices() {
firstDice = rollDice() //Returns a value between 1 and 6
secondDice = rollDice()
}
fun resetDices() {
firstDice = 0
secondDice = 0
}
fun areEqual() = firstDice == secondDice
}
Example
Column(modifier = Modifier.fillMaxSize()) {
val dices = remember { Dices(1, 1) }
Text("Dice1: ${dices.firstDice}")
Text("Dice2: ${dices.secondDice}")
Button(onClick = { dices.rollDices() }) {
Text("Roll")
}
Button(onClick = { dices.resetDices() }) {
Text("Reset")
}
}

Related

Why my return value is coming as NaN or default in kotlin?

For a while, I want to convert the entered currencies to each other and show them on the other screen, but when I check the view model with println, the result I can see is NaN when I make viewmodel.result in the ui. What is the reason for this and how can I solve it?
my ui
If the user presses oncofirm on the button, the operations in the view model are performed.
if (viewModel.isDialogShown) {
AlertDialog(
onDismiss = {
viewModel.onDismissClick()
},
onConfirm = {
viewModel.getConversionRateByCurrency()
viewModel.onDismissClick()
//viewModel.calculate()
println(viewModel.resultState)
With println(viewModel.resultState) comes 0.0
but when I press the button for the second time and say confirm, then the correct result comes.
my view model
#HiltViewModel
class ExchangeMainViewModel #Inject constructor(
private val exchangeInsertUseCase: InsertExchangeUseCase,
private val exchangeGetAllUseCase: GetAllExchangeUseCase,
private val getConversionRateByCurrencyUseCase: GetConversionRateByCurrencyUseCase
) : ViewModel() {
var isDialogShown by mutableStateOf(false)
private set
var dropDownMenuItem1 by mutableStateOf("")
var dropDownMenuItem2 by mutableStateOf("")
var outLineTxtFieldValue by mutableStateOf(TextFieldValue(""))
var firstConversionRate by mutableStateOf(0.0)
var secondConversionRate by mutableStateOf(0.0)
var resultState by mutableStateOf(0.0)
fun onConfirmClick() {
isDialogShown = true
}
fun onDismissClick() {
isDialogShown = false
}
fun check(context: Context): Boolean {
if (outLineTxtFieldValue.text.isNullOrEmpty() || dropDownMenuItem1 == "" || dropDownMenuItem2 == "") {
Toast.makeText(context, "please select a value and currency ", Toast.LENGTH_LONG).show()
return false
}
return true
}
fun getConversionRateByCurrency() {
viewModelScope.launch {
val firstRate = async {
getConversionRateByCurrencyUseCase.getConversionRateByCurrency(dropDownMenuItem1)
}
val secondRate = async {
getConversionRateByCurrencyUseCase.getConversionRateByCurrency(dropDownMenuItem2)
}
firstConversionRate = firstRate.await()
secondConversionRate = secondRate.await()
delay(200L)
val result = async {
calculate()
}
resultState = result.await()
}
}
private fun calculate(): Double {
if (!firstConversionRate.equals(0.0) && !secondConversionRate.equals(0.0)) {
val amount = outLineTxtFieldValue.text.toInt()
val resultOfCalculate = (amount / firstConversionRate) * secondConversionRate
return resultOfCalculate
}
return 1.1
}
}
I can see the value in the view model but not the ui. Also, I do a lot of checking with if and 0.0 because I couldn't get out of it, so I followed a method like this because I couldn't solve the problem. Anyone have a better idea?

My response data return " kotlinx.coroutines.flow.SafeFlow#1493a74"

I am trying to do dictionary app using kotlin language. I built the project with mvvm and clean architecture. I have been trying to pull vocabulary information from the internet using jsoap. I am using flow for data. I couldnt find where the issiue is. Normally, the words should appear on the screen or I should be able to see the data when I println on the console.But I can't see it on the screen or on the console, probably because the data coming from the internet is as follows.
kotlinx.coroutines.flow.SafeFlow#1493a74
I am sharing my codes below
ExtractedData
data class ExtractedData(
var id :Int = 0,
var word:String = "",
var meaning :String = ""
)
I created ExtractedData class to represent vocabulary or word data from internet
WordInfoRepositoryImpl
class WordInfoRepositoryImpl #Inject constructor(
private val api:DictionaryApi
) : WordInfoRepository {
//get words with meanings on the internet using jsoap
override fun getEventsList(): Flow<Resource<MutableList<ExtractedData>>> = flow {
emit(Resource.Loading())
val listData = mutableListOf<ExtractedData>()
try {
val url = "https://ielts.com.au/australia/prepare/article-100-new-english-words-and-phrases-updated-2020"
val doc = withContext(Dispatchers.IO){
Jsoup.connect(url).get()//-->Here it gives the following warning even though I have it in withContext `Inappropriate blocking method call`
}
val table = doc.select("table")
val rows = table.select("tr")
val eventsSize = rows.size
for (i in 1 until eventsSize) {
val row = rows[i]
val cols = row.select("td")
val word = cols[0].text()
val meaning = cols[1].text()
listData.add(ExtractedData(i,word,meaning))
}
}
catch (e: IOException) {
emit(Resource.Error("IO Exception"))
}
catch (e : HttpException) {
emit(Resource.Error("HTTP EXCEPTION"))
}
emit(Resource.Success(listData))
}
}
getEventsList is in my WordInfoRepositoryImpl class in my data layer here I am pulling data from internet using jsoap
WordInfoRepository
interface WordInfoRepository {
fun getEventsList(): Flow<Resource<MutableList<ExtractedData>>>
}
this is the interface that I reference wordInforepositoryImpl in the data layer in my interface domain layer
GetWordsAndMeaningsOnTheInternetUseCase
class GetWordsAndMeaningsOnTheInternetUseCase#Inject constructor(
private val repository: WordInfoRepository
){
operator fun invoke() : Flow<Resource<MutableList<ExtractedData>>> {
return repository.getEventsList()
}
}
GetWordsAndMeaningsOnTheInternetUseCase is my usecase in my domain layer
ViewModel
#HiltViewModel
class MostUsedWordScreenViewModel #Inject constructor(
private val getWordsAndMeaningsOnTheInternetUseCase: GetWordsAndMeaningsOnTheInternetUseCase
) : ViewModel() {
private var searchJob: Job? = null
private val _state = mutableStateOf(MostUsedWordState())
val state: State<MostUsedWordState> = _state
init {
fetchData()
}
private fun fetchData() {
searchJob?.cancel()
searchJob = viewModelScope.launch(IO) {
getWordsAndMeaningsOnTheInternetUseCase().onEach { result ->
when (result) {
is Resource.Success -> {
_state.value = state.value.copy(
mostWordUsedItems = result.data ?: mutableListOf(),
isLoading = false
)
}
is Resource.Error -> {
_state.value = state.value.copy(
mostWordUsedItems = result.data ?: mutableListOf(),
isLoading = false
)
}
is Resource.Loading -> {
_state.value = state.value.copy(
mostWordUsedItems = result.data ?: mutableListOf(),
isLoading = true
)
}
}
}
}
}
}
MostUsedWordScreen
#Composable
fun MostUsedWordScreen(viewModel: MostUsedWordScreenViewModel = hiltViewModel()) {
val state = viewModel.state.value
println("state --- >>> "+state.mostWordUsedItems)
LazyColumn(
modifier = Modifier.fillMaxSize()
) {
items(state.mostWordUsedItems.size) { i ->
val wordInfo = state.mostWordUsedItems[i]
if(i > 0) {
Spacer(modifier = Modifier.height(8.dp))
}
MostUsedWordItem(word = wordInfo)
if(i < state.mostWordUsedItems.size - 1) {
Divider()
}
}
}
}
#Composable
fun MostUsedWordItem(word : ExtractedData ) {
// println("this is MostUsedWordItem")
Column(modifier = Modifier
.padding(5.dp)
.fillMaxWidth()) {
Text(text = word.word,
modifier = Modifier.padding(3.dp),
textAlign = TextAlign.Center,
fontSize = 18.sp,
)
}
}
It is included in the MostUsedWordScreenViewModel and MostUsedWordScreen presententation layer
Where I println("state --- >>> "+state.mostWordUsedItems) in MostUsedWordScreen, the state console shows as empty like this System.out: state --- >>> []
I tried to explain as detailed as I can, I hope you can understand.
A Flow doesn't do anything until you call a terminal operator on it. You called onEach, which is not a terminal operator. You should use collect. Or you can avoid the nesting inside a launch block by using onEach and launchIn, which does the same thing as launching a coroutine and calling collect() on the flow. You don't need to specify Dispatchers.IO here because nothing in your Flow is blocking. You correctly wrapped the blocking call in withContext(Dispatchers.IO), and the warning is a false positive. That's a well-known bug in their compiler inspection.
searchJob = getWordsAndMeaningsOnTheInternetUseCase().onEach { result ->
when (result) {
is Resource.Success -> {
_state.value = state.value.copy(
mostWordUsedItems = result.data ?: mutableListOf(),
isLoading = false
)
}
is Resource.Error -> {
_state.value = state.value.copy(
mostWordUsedItems = result.data ?: mutableListOf(),
isLoading = false
)
}
is Resource.Loading -> {
_state.value = state.value.copy(
mostWordUsedItems = result.data ?: mutableListOf(),
isLoading = true
)
}
}
}.launchIn(viewModelScope)
By the way, you need to move your emit(Success...) inside your try block. The way it is now, when there is an error, the error will immediately get replaced by a Success with empty list.
Side note, I recommend avoiding passing MutableLists around between classes. You have no need for them and it's a code smell. Sharing mutable state between classes is error-prone. I don't think there is any justification for using a Flow<MutableList> instead of a Flow<List>.
You rarely even need a MutableList locally in a function. For example, you could have done in your try block:
val listData = List(eventsSize - 1) {
val row = rows[it + 1]
val cols = row.select("td")
val word = cols[0].text()
val meaning = cols[1].text()
ExtractedData(i,word,meaning)
}
emit(Resource.Success(listData))

Wait for result from Coroutine and then use it in Composable function

I am creating a video scraper, and it has the following function which scrapes the video source from a URL that has been given as the parameter:
fun scrapeVideoSrcFromUrl(url: String): String? {
val document = Jsoup.connect(url).get()
for (element in document.getElementsByTag("script")) {
if (element.attr("type") == "application/ld+json") {
val content = element.data()
val array = JsonParser.parseString(content).asJsonArray
val embedUrl = Gson().fromJson(array.get(0).asJsonObject.get("embedUrl"), String::class.java)
var embedId = ""
for (char in embedUrl.dropLast(1).reversed()) {
if (char != '/') {
embedId += char
} else {
break
}
}
val doc = Jsoup.connect("$RUMBLE_API_URL${embedId.reversed()}").ignoreContentType(true).get()
val jsonData = doc.getElementsByTag("body").first()?.text()
val mp4 = JsonParser.parseString(jsonData).asJsonObject.get("u").asJsonObject.get("mp4").asJsonObject.get("url").toString()
return mp4.replace("\"", "")
}
}
return null
}
I want to show this in a dialog for a certain link using ExoPlayer, so I did the following:
#Composable
fun VideoPlayer(videoSrc: String) {
val context = LocalContext.current
val exoPlayer = remember {
ExoPlayer.Builder(context).build().apply {
setMediaItem(
MediaItem.fromUri(
videoSrc
)
)
prepare()
playWhenReady = true
}
}
Box(modifier = Modifier.fillMaxSize()) {
DisposableEffect(key1 = Unit) {
onDispose {
exoPlayer.release()
}
}
AndroidView(
factory = {
StyledPlayerView(context).apply {
player = exoPlayer
layoutParams =
FrameLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
}
}
)
}
}
Then, in the main Composable:
if (openDialog) {
AlertDialog(
onDismissRequest = {
openDialog = false
},
title = {
Column {
Text(
text = viewModel.currentRumbleSearchResult?.title ?: ""
)
Spacer(
Modifier.height(8.dp)
)
Text(
text = "By ${viewModel.currentRumbleSearchResult?.channel?.name ?: ""}",
style = MaterialTheme.typography.titleSmall
)
}
},
text = {
VideoPlayer(RumbleScraper.create().scrapeVideoSrcFromUrl("https://rumble.com/v1m9oki-our-first-automatic-afk-farms-locals-minecraft-server-smp-ep3-live-stream.html")!!)
},
confirmButton = {
TextButton(
onClick = {
openDialog = false
}
) {
Text("Exit")
}
}
)
}
After running that code I keep getting NetworkOnMainThread exceptions, and I tried many things to fix it but nothing worked.
So I am unsure what to do as to how I can go around fixing this. I was wondering how I would go around waiting in the background for a result and then show it in the Compose function when it returns the value?
You can do something like this:
var videoSrc by remember { mutableStateOf<String?>(null) }
LaunchedEffect(Unit) {
withContext(Dispatchers.IO) {
videoSrc = RumbleScraper.create().scrapeVideoSrcFromUrl("")
}
}
text = { VideoPlayer(videoSrc) }
You can also call the scrapeVideoSrcFromUrl inside your viewModel and update some state that you will use in UI.
If you want to run it in response to some event like item click, you will be better of with something like this:
val scope = rememberCoroutineScope()
Button(
onClick = {
scope.launch {
withContext(Dispatchers.IO) { ... }
}
}
)

Jetpack Compose AndroidView call Update

Changing the state in the following way can work just fine
#Composable
fun CustomView() {
val selectedItem = remember { mutableStateOf(0) }
AndroidView(
modifier = Modifier.fillMaxSize(),
factory = { context ->
CustomView(context).apply {
myView.setOnClickListener {
selectedItem.value = 1
}
}
},
update = { view ->
view.coordinator.selectedItem = selectedItem.value
}
)
}
What if I want to make a call?
Update the value by calling, not by changing the state
#Composable
fun CustomView() {
val selectedItem = remember { mutableStateOf(0) }
// I need to call some methods like below, but I can't get cropLayoutView
// cropLayoutView.flipImageHorizontally()
// cropLayoutView.flipImageVertically()
// cropLayoutView.rotateClockwise()
// val bitmap = cropLayoutView.croppedImage
AndroidView(
modifier = Modifier.fillMaxSize(),
factory = { context ->
CustomView(context).apply {
myView.setOnClickListener {
selectedItem.value = 1
}
}
},
update = { view ->
// How to make a call here?
view.setCoordinator()
}
)
}
What if I want to make a call?
Update the value by calling, not by changing the state

How to simply add another source to MediatorLiveData in kotlin?

I want to combine multiple data sources in a MediatorLiveData. Unfortunately, there are not many examples yet. So in my ViewModel I have
//all lists have been declared before
val playerList = MediatorLiveData<List<Player>>()
init {
playerList.addSource(footballPlayerList) { value ->
playerList.value = value
}
playerList.addSource(basketballPlayerList) { value ->
playerList.value = value
}
}
But apparently this will always override the current value of playerList. I mean I could build some hacky workarounds with helper variables like _playerList but maybe there is an easier solution?
Having done quite some research.. I found it out. Here is an example
fun blogpostBoilerplateExample(newUser: String): LiveData<UserDataResult> {
val liveData1 = userOnlineDataSource.getOnlineTime(newUser)
val liveData2 = userCheckinsDataSource.getCheckins(newUser)
val result = MediatorLiveData<UserDataResult>()
result.addSource(liveData1) { value ->
result.value = combineLatestData(liveData1, liveData2)
}
result.addSource(liveData2) { value ->
result.value = combineLatestData(liveData1, liveData2)
}
return result
}
The actual combination of data is done in a separate combineLatestData method like so
private fun combineLatestData(
onlineTimeResult: LiveData<Long>,
checkinsResult: LiveData<CheckinsResult>
): UserDataResult {
val onlineTime = onlineTimeResult.value
val checkins = checkinsResult.value
// Don't send a success until we have both results
if (onlineTime == null || checkins == null) {
return UserDataLoading()
}
// TODO: Check for errors and return UserDataError if any.
return UserDataSuccess(timeOnline = onlineTime, checkins = checkins)
}
Here is a simple example
class MergeMultipleLiveData : ViewModel() {
private val fictionMenu: MutableLiveData<Resource<RssMenu>> = MutableLiveData()
private val nonFictionMenu: MutableLiveData<Resource<RssMenu>> = MutableLiveData()
val allCategoryMenus: MediatorLiveData<Resource<RssMenu>> = MediatorLiveData()
init {
getFictionMenus()
getNonFictionMenus()
getAllCategoryMenus()
}
private fun getAllCategoryMenus() = viewModelScope.launch(Dispatchers.IO) {
allCategoryMenus.addSource(fictionMenu) { value ->
allCategoryMenus.value = value
}
allCategoryMenus.addSource(nonFictionMenu) { value ->
allCategoryMenus.value = value
}
}
private fun getFictionMenus() = viewModelScope.launch(Dispatchers.IO) {
fictionMenu.postValue( // todo )
}
private fun getNonFictionMenus() = viewModelScope.launch(Dispatchers.IO) {
nonFictionMenu.postValue( // todo )
}
}
And in your fragment you can observer as;
viewModel.allCategoryMenus.observe(viewLifecycleOwner) {
// todo
}