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.
Related
I'm playing with Paging3 with Room. I wrote a little test app that populates a Room DB with a single table with 5 string fields (plus its id). I populate the table with 100,000 items.
The code for test project is here: https://github.com/johngray1965/wordlePeople
The list is a recyclerview with a PagingDataAdapter.
There are options in the UI to select filters for the color and gender.
The initial loading works fine. Filtering down the data works fine. Clearing the filter (going back to full list) sends the app into a dizzy for a number of seconds. I've looked at the profiling data in Android Studio (which I'm finding to be a source of frustration). I can see where I did the touch for the clear, I can see that resulted in garbage collection. Then the UI seems to be moderately busy for a long time (the UI is unresponsive for 5-7 seconds when not running the profiler).
It doesn't look like the query is what's slow. But I don't see the problem if there are only 2,000 items in the table.
I've turned the debug logging in Room, and I can see the queries always get all the items in a subquery, and then apply the limit and offset:
SELECT * FROM ( SELECT * FROM wordle_people ) LIMIT 90 OFFSET 0
BTW, I see the problem without debug logging in Room, on an emulator, or on a real device.
Here's the entity:
#Entity(
tableName = "wordle_people",
indices = [
Index(value = ["gender"], unique = false),
Index(value = ["color"], unique = false),
]
)
data class WordlePerson(
#PrimaryKey(autoGenerate = true)
override var id: Long? = null,
var firstName: String = "",
var middleName: String = "",
var lastName: String = "",
#TypeConverters(Gender::class)
var gender: Gender,
#TypeConverters(Color::class)
var color: Color
): BaseEntity()
The Dao:
#Dao
abstract class WordlePersonDao : BaseDao<WordlePerson>("wordle_people") {
#Query("SELECT * FROM wordle_people")
abstract fun pagingSource(): PagingSource<Int, WordlePerson>
#Query("SELECT * FROM wordle_people where gender IN (:genderFilter)")
abstract fun pagingSourceFilterGender(genderFilter: List<Gender>): PagingSource<Int, WordlePerson>
#Query("SELECT * FROM wordle_people where color IN (:colorFilter)")
abstract fun pagingSourceFilterColor(colorFilter: List<Color>): PagingSource<Int, WordlePerson>
#Query("SELECT * FROM wordle_people where color IN (:colorFilter) AND gender IN (:genderFilter)")
abstract fun pagingSourceFilterGenderAndColor(genderFilter: List<Gender>, colorFilter: List<Color>): PagingSource<Int, WordlePerson>
}
Relevant parts of the ViewModel:
private val stateFlow = MutableStateFlow(FilterState(mutableSetOf(), mutableSetOf()))
#OptIn(ExperimentalCoroutinesApi::class)
val wordlePeopleFlow = stateFlow.flatMapLatest {
Pager(
config = PagingConfig(
pageSize = 30,
),
pagingSourceFactory = {
when {
it.colorSet.isEmpty() && it.genderSet.isEmpty() ->
wordlePeopleDao.pagingSource()
it.colorSet.isNotEmpty() && it.genderSet.isNotEmpty() ->
wordlePeopleDao.pagingSourceFilterGenderAndColor(it.genderSet.toList(), it.colorSet.toList())
it.colorSet.isNotEmpty() ->
wordlePeopleDao.pagingSourceFilterColor(it.colorSet.toList())
else ->
wordlePeopleDao.pagingSourceFilterGender(it.genderSet.toList())
}
}
).flow
}
And finally in the fragment, we get the data and pass to the adapter:
lifecycleScope.launchWhenCreated {
viewModel.wordlePeopleFlow.collectLatest { pagingData ->
adapter.submitData(pagingData)
}
}
The smaller red circle is the db lookup, the larger red circle is all the DiffUtil. Its selected, you can see FlameChart for it. I might be wrong, but this looks like a bug in the PagingDataAdapter
I am trying to figure out how to loop a data class. I have a function called getAges() which contains a listof Ages from 1 - 10. Each age are called from a data class called Age, which should be an Int. How can I successfully loop through Age with different numbers, for ex 1-10? Appreciate the feedback!
My Data class:
#Entity(tableName = "dropdown_age")
data class Age(
#PrimaryKey(autoGenerate = true)
#ColumnInfo(name = "age")
val age: Int?
)
My Function called getAges:
class ProfileViewModel: Viewmodel() {
fun getAges() = listOf(
Age(1), Age(2), Age(3), Age(4), Age(5),
Age(6), Age(7), Age(8), Age(9), Age(10),
)
}
There is a List "constructor" function that can be used to create a List using a lambda where the lambda parameter is an index, starting at 0. (I use quotation marks for constructor because interfaces don't have true constructors. This is just a function that looks like a constructor because of how it is capitalized.)
fun getAges() = List(10) { Age(it + 1) }
Or you can use the map function with a range. map modifies each item out of any Iterable to produce a new List.
fun getAges() = (1..10).map { Age(it) }
// or
fun getAges() = (1..10).map(::Age)
Using collections api forEach
getAges().forEach{
println(it.age)
}
Or using normal For loop
for (age: Age in getAges()) {
println(age.age)
}
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
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))}
)
}
So, I'm using Room database to store courses and I'm stuck on the method that returns the course with the name(course) that I want because it's always returning null. I have diminished my database to have 2 courses with the course variable as:
As you can see in the picture above, when I try to get the CourseEnt in the Repository with course = fun, which I can see below that it exists, it returns a LiveData with a null value instead of the CourseEnt that I wanted.
Any idea on what I'm doing wrong or on what should I look into with debugger?
Here's the code:
Entity:
#Entity(tableName = "courses_table")
data class CoursesEnt (#PrimaryKey val course: String,
val location: String,
val description: String,
val difficulty: Double,
val distance: Double,
val photos: ListInt,
val category: String,
val activities: ListString)//ListString is a type converter that converts a String into a List<String> and vice-versa
DAO:
#Dao
interface CoursesDao {
#Query("SELECT * from courses_table ORDER BY course ASC")
fun getAllCourses(): LiveData<List<CoursesEnt>>
#Query("SELECT * FROM courses_table WHERE course LIKE :str")
fun getCourse(str: String):LiveData<CoursesEnt>
...
}
Repository:
class CoursesRepository(private val coursesDao: CoursesDao){
val allCourses: LiveData<List<CoursesEnt>> = coursesDao.getAllCourses()
var singleCourse: LiveData<CoursesEnt> = coursesDao.getCourse("")
#WorkerThread
fun getCourse(str: String) {
singleCourse = coursesDao.getCourse(str)
}
...
}
Read documentation, liveData will always return null for straight call, you have to observe LiveData, the result value from Room will be in block. This is some example of usage
mViewModel.getAllUsers().observe( this#YourActivity, Observer {
// it - is all users from DB
})
but if you will call
val users = mViewModel.getAllUsers()
the result will be null