Transform collection of items with another collection inside in kotlin - android

I have a collection of items. And each item has another collection inside.
To transform the first collection I'm using mapNotNull.
I'm trying to achive something like this:
data class QuestionData(
val items: List<Question>
)
val questions = listOf(
QuestionData(0, emptyList()),
QuestionData(1, listOf(Question(2, emptyList()))),
)
fun convertItem(item: QuestionData): QuestionEntity {
return QuestionEntity(item)
}
val result: List<QuestionEntity> = questions.mapNotNull {
convertItem(it)
it.items.forEach {it2-> convertItem(it2) }
}
but this is not working.

The body of lambda that you pass to mapNotNull function doesn't return the initial item, since forEach returns Unit as the last statement in lambda body, so the end result is List<Unit>.
You should return it from mapNotNull in order for this to work
For example:
data class Question(
var someCounter: Int,
val items: List<Question>
)
val questions = listOf(
Question(0, emptyList()),
Question(1, listOf(Question(2, emptyList()))),
)
fun convertItem(item: Question) {
item.someCounter++
}
val result = questions.mapNotNull {
convertItem(it)
it.items.forEach { it2 -> convertItem(it2) }
it
}
Even though it's not very idiomatic code, but something to start with

Related

Android Jetpack Compose: VM not updating data structure when modified

I’ve got a problem with a LazyColumn of elements that have a favourite button: basically when I tap the favourite button, the item that is being favourited (a document in my case) is changed in the underlying data structure in the VM, but the view isn’t updated, so I never see any change in the button state.
class MainViewModel(private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO) : ViewModel() {
var documentList = emptyList<PDFDocument>().toMutableStateList()
....
fun toggleFavoriteDocument(pdfDocument: PDFDocument) {
documentList.find {
it == pdfDocument
}?.let {
it.favorite = !it.favorite
}
}
}
The composables are:
#Composable
fun DocumentRow(
document: PDFDocument,
onDocumentClicked: (String, Boolean) -> Unit,
onFavoriteValueChange: (Uri) -> Unit
) {
HeartIcon(
isFavorite = document.favorite,
onValueChanged = { onFavoriteValueChange(document.uri) }
)
}
#Composable
fun HeartIcon(
isFavorite: Boolean,
color: Color = Color(0xffE91E63),
onValueChanged: (Boolean) -> Unit
) {
IconToggleButton(
checked = isFavorite,
onCheckedChange = {
onValueChanged()
}
) {
Icon(
tint = color,
imageVector = if (isFavorite) {
Icons.Filled.Favorite
} else {
Icons.Default.FavoriteBorder
},
contentDescription = null
)
}
}
Am I doing something wrong? because when I call the toggleFavouriteDocument in the ViewModel, I see it’s marked or unmarked as favorite but there is no recomposition at all anywhere.
I might be missing it because you didn't post the rest of your code, but your documentList in the VM isn't observable, so how would the Composable know that it got changed? It needs to be something like Flow or LiveData, and it needs to be observed in the Composable. Something like this:
in ViewModel:
val documentList = MutableLiveData<List<PDFDocument>>()
in Composable:
val documentList by viewModel.documentList.observeAsState(List<PDFDocument>())
And you'll probably have to change the way you modify items in documentList. LiveData is weird about mutable collections inside MutableLiveData, and modifying individual items doesn't trigger a state change. You have to create a copy of the list with the modified items, and then re-port the whole list to the LiveData variable:
fun toggleFavoriteDocument(pdfDocument: PDFDocument) {
documentList.value?.let { oldList ->
// create a copy of existing list
val newList = mutableListOf<PDFDocument>()
newList.addAll(oldList)
// modify the item in the new list
newList.find {
it == pdfDocument
}?.let {
it.favorite = !it.favorite
}
// update the observable
documentList.postValue(newList)
}
}
Edit: There's also a potential problem with the way that you're trying to update the favorite value in the existing list. Without knowing how PDFDocument is implemented, I don't know if you can use the = operator. You should test that to make sure that newList.find { it == pdfDocument } actually finds the document

Android LiveData/StateFlow List Item Property Update Issue

So I'm updating my RecylerView with StateFlow<List> like following:
My data class:
data class Student(val name: String, var isSelected: Boolean)
My ViewModel logic:
fun updateStudentsOnSelectionChanged(targetStudent: Student) {
val targetIndex = _students.value.indexOf(targetStudent)
val isSelected = !targetStudent.isSelected
_students.value[targetIndex].isSelected = isSelected //<- doesn't work
}
Problem: The UI is not changed, but the isSelected inside _student is changed, what's going on? (same to LiveData)
I assume _students is a StateFlow<List>. Changing the isSelected property of the Student model doesn't trigger the StateFlow. The workaround would be to make the isSelected property of the Student data class immutable to get it compared when new state is set, create a MutableList out of the current list and copy the existing Student object with the new value for isSelected property:
data class Student(val name: String, val isSelected: Boolean)
val students = _students.value.toMutableList()
students[targetIndex] = students[targetIndex].copy(isSelected = isSelected)
_students.value = students
Ok, thanks to #Tenfour04 and #Sergey, I finally found out that StateFlow/LiveData cannot detect the internal changes, that's because they are both actually comparing the Reference of the .value.
That means, If I want to force the StateFow<List> to update, the only way is to assign a new List to it, therefore I created the following helper extension function:
fun <T> List<T>.mapButReplace(targetItem: T, newItem: T) = map {
if (it == targetItem) {
newItem
} else {
it
}
}
//this function returns a new List with the content I wanted
In Fragment:
val itemCheckAction: (Student) -> Unit = { student ->
val newStudent = student.copy(isSelected = !student.isSelected) //prepare a new Student
viewModel.updateStudentsOnSelectionChanged(student, newStudent) //update the StateFlow
}
In ViewModel:
fun updateStudentsOnSelectionChanged(currentStudent: Student, newStudent: Student) {
val newList = _students.value.mapButReplace(currentStudent, newStudent)
_students.value = newList //assign a new List with different reference
}

Access Room DB inside Transformation.map android

In my Database i have a table called Account which looks kinda like this
#Entity(tableName = "accounts", primaryKeys = ["server_id", "account_id"])
data class Account(
#ColumnInfo(name = "server_id")
val serverId: Long,
#ColumnInfo(name = "account_id")
val accountId: Int,
#ColumnInfo(name = "first_name", defaultValue = "")
var firstname: String
)
So lets say that we have the following Database snapshot
server_id account_id first_name
1 10 Zak
1 11 Tom
1 12 Bob
1 13 Jim
1 14 Mike
Now i also have the following POJO which represents an available video room inside a chatRoom
data class RoomInfo(
#SerializedName("m")
val participantIntList: List<Int>,
#SerializedName("o")
val roomId: String,
#SerializedName("s")
val status: Int
)
So i get an incoming response from my Socket which is like the following
[
{"m": [10, 11, 12], "o": "room_technical", "s": 1},
{"m": [13, 14], "o": "room_operation", "s": 1}
]
which i map it in a List so i have
val roomInfo: LiveData<List<RoomInfo>> = socketManager.roomInfo
// So the value is basically the json converted to a list of RoomInfos using Gson
In order to display this available list of Rooms to the User i need to convert the m (which is the members that are inside the room right now) from accountIds to account.firstnames.
So what i want to have finally is a List of a new object called RoomInfoItem which will hold the list of the rooms with the accountIds converted to firstNames from the Account table of the Database.
data class RoomInfoItem(
val roomInfo: RoomInfo,
val participantNames: List<String>
)
So if we make the transformation we need to have the following result
RoomInfo (
// RoomInfo
{"m": [10, 11, 12], "o": "room_technical", "s": 1},
// Participant names
["Zak", "Tom", "Bob"]
)
RoomInfo (
// RoomInfo
{"m": [13, 14], "o": "room_operation", "s": 1},
// Participant names
["Jim", "Mike"]
)
My Activity needs to observe a LiveData with the RoomInfoItems so what i want is given the LiveData<List> to transform it to LiveData<List>. How can i do that?
Well, finally i could not find a solution but i think that what i am trying to achieve, cannot be done using the Transformation.switchMap or Transformation.map
As I understand you want get LiveData<List<RoomInfoItem>> by analogy LiveData<List<ResultData>> in my sample. And you have next condition: you want to observe list of RoomInfo and for each RoomInfo in this list you want to observe participantNames. (Each pair of RoomInfo and participantNames you map to RoomInfoItem). I think you can achive this behaviour by using MediatorLiveData. I show sample how you can do this bellow:
// For example we have method which returns liveData of List<String> - analogy to your List<RoomInfo>
fun firstLifeData(): LiveData<List<String>> {
//TODO
}
// and we have method which returns liveData of List<Int> - analogy to your participantNames(List<String>)
fun secondLifeData(param: String): LiveData<List<Int>> {
//TODO
}
//and analogy of your RoomInfoItem
data class ResultData(
val param: String,
val additionalData: List<Int>
)
Then I will show my idea of implementation of combined liveDatas:
#MainThread
fun <T> combinedLiveData(liveDatas: List<LiveData<T>>): LiveData<List<T>> {
val mediatorLiveData = MediatorLiveData<List<T>>()
// cache for values which emit each liveData, where key is an index of liveData from input [liveDatas] list
val liveDataIndexToValue: MutableMap<Int, T> = HashMap()
// when [countOfNotEmittedLifeDatas] is 0 then each liveData from [liveDatas] emited value
var countOfNotEmittedLifeDatas = liveDatas.size
liveDatas.forEachIndexed { index, liveData ->
mediatorLiveData.addSource(liveData) { value ->
// when liveData emits first value then mack it by decrementing of countOfNotEmittedLifeDatas
if (!liveDataIndexToValue.containsKey(index)) {
countOfNotEmittedLifeDatas--
}
liveDataIndexToValue[index] = value
// when countOfNotEmittedLifeDatas is 0 then all liveDatas emits at least one value
if (countOfNotEmittedLifeDatas == 0) {
// then we can push list of values next to client-side observer
mediatorLiveData.value = liveDataIndexToValue.toListWithoutSavingOrder()
}
}
}
return mediatorLiveData
}
fun <V> Map<Int, V>.toListWithoutSavingOrder(): List<V> = this.values.toList()
/**
* Key should be an order
*/
fun <V> Map<Int, V>.toListWithSavingOrder(): List<V> = this.entries.sortedBy { it.key }.map { it.value }
/*
or you can run [for] cycle by liveDataIndexToValue in [combinedLiveData] method or apply [mapIndexed] like:
liveDatas.mapIndexed{ index, _ ->
liveDataIndexToValue[index]
}
to receive ordered list.
*/
And how to use all of that together:
fun resultSample(): LiveData<List<ResultData>> {
return firstLifeData().switchMap { listOfParams ->
val liveDatas = listOfParams.map { param -> secondLifeData(param).map { ResultData(param, it) } }
combinedLiveData(liveDatas)
}
}
// u can add extension function like:
fun <T> List<LiveData<T>>.combined(): LiveData<List<T>> = combinedLiveData(this)
// and then use it in this way
fun resultSample_2(): LiveData<List<ResultData>> = firstLifeData().switchMap { listOfParams ->
listOfParams.map { param -> secondLifeData(param).map { ResultData(param, it) } }.combined()
}
I suggest you to consider using room's Relations. I think by room's Relations you can get LiveData<RoomInfoItem> . I cant get you more details about this approach because I don't know details about your data scheme and domain, at the moment.

Jetpack Compose: Modify Room data class using TextField

Modifying simple values and data classes using EditText is fairly straight forward, and generally looks like this:
data class Person(var firstName: String, var lastName: Int)
// ...
val (person, setPerson) = remember { mutableStateOf(Person()) }
// common `onChange` function handles both class properties, ensuring maximum code re-use
fun <T> onChange(field: KMutableProperty1<Person, T>, value: T) {
val nextPerson = person.copy()
field.set(nextPerson, value)
setPerson(nextPerson)
}
// text field for first name
TextField(
value = person.firstName,
onChange = { it -> onChange(Person::firstName, it) })
// text field for last name name
TextField(
value = person.lastName,
onChange = { it -> onChange(Person::lastName, it) })
As you can see, the code in this example is highly reusable: thanks to Kotlin's reflection features, we can use a single onChange function to modify every property in this class.
However, a problem arises when the Person class is not instantiated from scratch, but rather pulled from disk via Room. For example, a PersonDao might contain a `findOne() function like so:
#Query("SELECT * FROM peopleTable WHERE id=:personId LIMIT 1")
fun findOne(personId: String): LiveData<Person>
However, you cannot really use this LiveData in a remember {} for many reasons:
While LiveData has a function called observeAsState(), it returns State<T> and not MutableState<T>, meaning that you cannot modify it with the TextFields. As such this does not work:
remember { personFromDb.observeAsState()}
You cannot .copy() the Person that you get from your database because your component will render before the Room query is returned, meaning that you cannot do this, because the Person class instance will be remembered as null:
remember { mutableStateOf(findPersonQueryResult.value) }
Given that, what is the proper way to handle this? Should the component that contains the TextFields be wrapped in another component that handles the Room query, and only displays the form when the query is returned? What would that look like with this case of LiveData<Person>?
I would do it with a copy and an immutable data class
typealias PersonID = Long?
#Entity
data class Person(val firstName: String, val lastName: String) {
#PrimaryKey(autoGenerate = true)
val personID: PersonID = null
}
//VM or sth
object VM {
val liveData: LiveData<Person> = MutableLiveData() // your db call
val personDao: PersonDao? = null // Pretending it exists
}
#Dao
abstract class PersonDao {
abstract fun upsert(person: Person)
}
#Composable
fun test() {
val personState = VM.liveData.observeAsState(Person("", ""))
TextField(
value = personState.value.firstName,
onValueChange = { fName -> VM.personDao?.upsert(personState.value.copy(firstName = fName))}
)
}

Filtering list inside Observable collection

I want to filter List <Notification> which is inside Observable collection by the specified event.
Here is a Retrofit call:
#GET("/notifications")
Observable<NotificationCollection> getNotifications(#Query("page") Integer page);
NotificationCollection model:
class NotificationCollection {
var items: List<Notification>? = null
var pagination: Pagination? = null
}
Notification model:
class Notification {
var event: String? = null
var id: Long? = null
var data: NotificationData? = null
}
In my helper class, I return Observable to interactor:
override fun getNotifications(page: Int): Observable<NotificationCollection> {
return service.getNotifications(page)
}
I tried a couple of approaches:
override fun getNotifications(page: Int): Observable<NotificationCollection> {
return service.getNotificationsTimeline(page)
.filter { it.items?.get(0)?.event == "STORY"}
}
Here I want to apply a predicate to all list items and not only to the ones I define by an index. Is there a way to make something like .filter { it.items.event = "STORY"} ?
Another approach I tried using flatMap here which makes more sense to me but I don't see the way how to map my filtered result to the original response type of Observable<NotificationCollection> and not to the Observable<Notification> like here :
return service.getNotifications(page)
.flatMap { Observable.fromIterable(it.items) }
.filter {it.event == "STORY"}
The easiest way would be to apply filter function in my presenter class with flatMap but I want to generalize my solution because the method in the helper class is called in different places. So I want to make the list filtered here.
So, is there a way to filter the list inside Observable collection and return the original response type of Observable<NotificationCollection>?
You should use 'filter' and 'any' together.
class Test {
init {
val testList = listOf(
TestData("1","1", listOf("1a")),
TestData("2","2", listOf("2b")),
TestData("3","3", listOf("3c")))
val result = testList.filter { item ->
item.c.any { it == "STORY" }
}
}
}
data class TestData(val a: String, val b: String, val c: List<String>)

Categories

Resources