I have a ViewModel that has a MutableLiveData of an arraylist of class Course
private var coursesList: MutableLiveData<ArrayList<Course>> = MutableLiveData()
This coursesList is filled with data got from an API (by Retrofit): coursesList.postValue(response.body())
Now, a user can search for a course by its name. The function that I have for searching is that I iterate through the elements of the coursesList and check if its name is equal to what a user typed. It returns an arrayList with the courses that start with the name typed (this list is later sent to a fragment which passes it to an adapter to be shown in a recyclerview):
fun getCoursesList(): MutableLiveData<ArrayList<Course>> {
return coursesList
}
fun searchCourses(searchString: String): ArrayList<Course> {
val resultsList: ArrayList<Course> = ArrayList()
if (getCoursesList().value == null) return resultsList
if (getCoursesList().value!!.size > 0) {
for (course in getCoursesList().value!!.iterator()) {
if (course.name.toLowerCase(Locale.ROOT).startsWith(searchString)) {
resultsList.add(course)
}
}
}
resultsList.sortBy { it.price }
return resultsList
}
This function works and all but my instructor asked me to use LiveData for searching without giving any additional hints on how to do that.
So my question is how to use LiveData for searching? I tried to search for answers, I saw that some used LiveDataTransformations.switchMap but they were all using RoomDAOs and I couldn't adapt it to the code that I have.
Any help would be appreciated very much. Thanks in advance.
Maybe that can help you a little bit,
class YourViewModel(
private val courcesRepository: CourcesRepository
) : ViewModel() {
// Private access - mutableLiveData!
private val _coursesList = MutableLiveData<ArrayList<Course>>()
// Public access - immutableLiveData
val coursesList: LiveData<ArrayList<Course>>
get() = _coursesList
init {
// mutableLiveData initialize, automatic is immutable also initialize
_coursesList.postValue(getCourses())
}
// Here you get your data from repository
private fun getCourses(): ArrayList<Course> {
return courcesRepository.getCources()
}
// Search function
fun searchCourses(searchString: String) {
// you hold your data into this methode
val list: ArrayList<Course> = getCources()
if (searchString.isEmpty()) {
// here you reset the data if search string is empty
_coursesList.postValue(list)
} else {
// here you can search the list and post the new one to your LiveData
val filterList = list.filter {
it.name.toLowerCase(Locale.ROOT).startsWith(searchString)
}
filterList.sortedBy { it.price }
_coursesList.postValue(filterList)
}
}
}
The first tip is you should use LiveData like below, that is also recommended from google's jet pack team. The reason is so you can encapsulate the LivaData.
The second tip is you should use kotlin's idiomatic way to filter a list. Your code is readable and faster.
At least is a good idea to make a repository class to separate the concerns in your app.
And some useful links for you:
https://developer.android.com/jetpack/guide
https://developer.android.com/topic/libraries/architecture/livedata
I hope that's helpful for you
Ii is hard to guess the desired outcome, but a possible solution is to use live data for searched string also. And then combine them with coursesList live data into live data for searched courses, like this for example.
val searchStringLiveData: MutableLiveData<String> = MutableLiveData()
val coursesListLiveData: MutableLiveData<ArrayList<Course>> = MutableLiveData()
val searchedCourses: MediatorLiveData<ArrayList<Course>> = MediatorLiveData()
init {
searchedCourses.addSource(searchStringLiveData) {
searchedCourses.value = combineLiveData(searchStringLiveData, coursesListLiveData)
}
searchedCourses.addSource(coursesListLiveData) {
searchedCourses.value = combineLiveData(searchStringLiveData, coursesListLiveData)
}
}
fun combineLiveData(searchStringLiveData: LiveData<String>, coursesListLiveData: LiveData<ArrayList<Course>> ): ArrayList<Course> {
// your logic here to filter courses
return ArrayList()
}
I haven't run the code so I am not 100% sure that it works, but the idea is that every time either of the two live data changes value, searched string or courses, the combine function is executed and the result is set as value of the searchedCourses mediator live data. Also I omitted the logic of the filtering for simplicity.
Related
My task is to get whole Article with provided title from RecyclerView.
When I click on specific Article i get title from it.
Room database:
#Query("SELECT * FROM article_table WHERE title = :title")
fun getArticleDetails(title: String): Flow<ArticleLocal>
Repository:
fun getArticleDetails(title: String): Flow<ArticleLocal> {
return articleDao.getArticleDetails(title)
}
ViewModel:
val articleDetail = MutableStateFlow<ArticleLocal>(ArticleLocal("","","","",""))
fun getArticle(title: String) {
viewModelScope.launch {
articleRepository.getArticleDetails(title).collect {
articleDetail.emit(it)
}
}
}
MainActivity:
lifecycleScope.launch {
viewModel.getArticle(title)
viewModel.articleDetail.collect {
Log.d(TAG, "onCreate: $it")
}
}
Problem with this code is that articleDetail on first touch gives me empty ArticleLocal e.g. title = "" I defined in ViewModel, later I get good result.
EDIT: With MyActivity .collet I get whole object but cannot access propert like it.title
Use a SharedFlow so it doesn't have to publish a default result. The flow won't emit anything until it receives its first value. Use replay = 1 to get similar behavior as StateFlow as far as new subscribers getting the most recent value immediately.
You also need to consider that if the title changes, it should not keep publishing values with the old title. Currently, you have it collecting from more and more flows each time the title changes.
If you use another MutableSharedFlow just for the title, you can get it to automatically cancel unnecessary collection of those old title flows. It also allows you to get the benefit of SharingStarted.WhileSubscribed to avoid unnecessary collection from the repository when there are no subscribers.
In ViewModel:
private val articleTitle = MutableSharedFlow<String>(bufferOverflow = BufferOverflow.DROP_OLDEST)
val articleDetail = articleTitle.flatMapLatest { articleRepository.getArticleDetails(it) }
.shareIn(viewModelScope, SharingStarted.WhileSubscribed(5000), replay = 1)
fun getArticle(title: String) {
articleTitle.tryEmit(title)
}
You can get rid of additional flow to emit data and use the flow returned from the repository directly.
ViewModel:
fun getArticle(title: String): Flow<ArticleLocal> {
return articleRepository.getArticleDetails(title)
}
MainActivity:
lifecycleScope.launch {
viewModel.getArticle(title).collect {
Log.d(TAG, "onCreate: $it")
}
}
Am learning android kotlin follow this:
https://developer.android.com/topic/libraries/architecture/viewmodel#kotlin
class MyViewModel : ViewModel() {
private val users: MutableLiveData<List<User>> by lazy {
MutableLiveData<List<User>>().also {
loadUsers(it)
}
}
fun getUsers(): LiveData<List<User>> {
return users
}
private fun loadUsers() {
// Do an asynchronous operation to fetch users.
}
}
Dont know how to write the fun loadUsers()
Here is my User:
class User {
constructor(name: String?) {
this.name = name
}
var name:String? = null
}
If dont use the keyword 'also' , i know how to do it.
But if use 'also' , it seems not work.
Here is how i try to write the fun loadUsers:
private fun loadUsers( it: MutableLiveData<List<User>>){
val users: MutableList<User> = ArrayList()
for (i in 0..9) {
users.add(User("name$i"))
}
it = MutableLiveData<List<User>>(users)
}
Error tips near it : Val cant be ressigned
Part 1: According to the Kotlin documentation, also provides the object in question to the function block as a this parameter. So, every function call and property object you access is implied to refer to your MutableLiveData<List<User>>() object. also returns this from the function block when you are done.
Thus, another way of writing your MutableLiveData<> would be like this:
val users = MutableLiveData<List<User>>()
users.loadUsers()
Part 2: As far as how to implement loadUsers(), that is a separate issue (your question is not clear). You can use Retrofit + RxJava to load the data asynchronously, and that operation is totally outside of the realm of ViewModel or also.
Part 3: With your approach, you have conflicting things going on. Instead of doing a loadUsers() from your lazy {} operation, I would remove your lazy {} operation and create a MutableLiveData<> directly. Then, you can load users later on and update the users property any time new data is loaded. Here is a similar example I worked on a while ago. It uses state flows, but the idea is similar. Also use a data class to model the User instead of a regular class. Another example.
It is solved change to code:
private fun loadUsers( it: MutableLiveData<List<User>>){
val users: MutableList<User> = ArrayList()
for (i in 0..9) {
users.add(User("name$i"))
}
it.value = users
}
it can't be reassigned , but it.value could .
I have a Composable, a ViewModel and an object of a User class with a List variable in it. Inside the ViewModel I define a LiveData object to hold the User object and in the Composable I want to observe changes to the List inside the User object but it doesn't seem to work very well.
I understand when you change the contents of a List its reference is the same so the List object doesn't change itself, but I've tried copying the list, and it doesn't work; copying the whole User object doesn't work either; and the only way it seems to work is if I create a copy of both. This seems too far-fetched and too costly for larger lists and objects. Is there any simpler way to do this?
The code I have is something like this:
Composable
#Composable
fun Greeting(viewModel: ViewModel) {
val user = viewModel.user.observeAsState()
Column {
// TextField and Button that calls viewModel.addPet(petName)
LazyColumn {
items(user.value!!.pets) { pet ->
Text(text = pet)
}
}
}
}
ViewModel
class ViewModel {
val user: MutableLiveData<User> = MutableLiveData(User())
fun addPet(petName: String){
val sameList = user.value!!.pets
val newList = user.value!!.pets.toMutableList()
newList.add(petName)
sameList.add(petName) // This doesn't work
user.value = user.value!!.copy() // This doesn't work
user.value!!.pets = newList // This doesn't work
user.value = user.value!!.copy(pets = newList) // This works BUT...
}
}
User
data class User(
// Other variables
val pets: MutableList<String> = mutableListOf()
)
MutableLiveData will only notify view when it value changes, e.g. when you place other value which is different from an old one. That's why user.value = user.value!!.copy(pets = newList) works.
MutableLiveData cannot know when one of the fields was changed, when they're simple basic types/classes.
But you can make pets a mutable state, in this case live data will be able to notify about changes. Define it like val pets = mutableStateListOf<String>().
I personally not a big fan of live data, and code with value!! looks not what I'd like to see in my project. So I'll tell you about compose way of doing it, in case your project will allow you to use it. You need to define both pets as a mutable state list of strings, and user as a mutable state of user.
I suggest you read about compose states in the documentation carefully.
Also note that in my code I'm defining user with delegation, and pets without delegation. You can use delegation only in view model, and inside state holders you cannot, othervise it'll become plain objects at the end.
#Composable
fun TestView() {
val viewModel = viewModel<TestViewModel>()
Column {
// TextField and Button that calls viewModel.addPet(petName)
var i by remember { mutableStateOf(0) }
Button(onClick = { viewModel.addPet("pet ${i++}") }) {
Text("add new pet")
}
LazyColumn {
items(viewModel.user.pets) { pet ->
Text(text = pet)
}
}
}
}
class User {
val pets = mutableStateListOf<String>()
}
class TestViewModel: ViewModel() {
val user by mutableStateOf(User())
fun addPet(petName: String) {
user.pets.add(petName)
}
}
Jetpack Compose works best with immutable objects, making a copy with modern Android and ART is not the issue that it was in the past.
However, if you do not want to make a whole copy of your object, you could add a dummy int to it and then mutate that int when you also mutate the list, but I strongly urge you to consider immutability and instantiate a new User object instead.
I have a huge understanding problem here, I have a ecommerce app and I cannot properly calculate value of users cart.
The problem is, my solution works well to the point but I have an issue when there are no products in the cart. Obviously LiveData observer or switchMap will not get executed when it's value is empty.
It seems like something trivial, only thing I want to do here is handle the situation when user have no products in the cart. Is the livedata and switchMap a wrong approach here?
I get userCart from the repo -> I calculate its value in the viewModel and expose it to the view with dataBinding.
#HiltViewModel
class CartFragmentViewModel
#Inject
constructor(
private val repository: ProductRepository,
private val userRepository: UserRepository,
private val priceFormatter: PriceFormatter
) : ViewModel() {
private val user = userRepository.currentUser
val userCart = user.switchMap {
repository.getProductsFromCart(it.cart)
}
val cartValue = userCart.switchMap {
calculateCartValue(it)
}
private fun calculateCartValue(list: List<Product>?): LiveData<String> {
val cartVal = MutableLiveData<String>()
var cartValue = 0L
list?.let { prods ->
prods.forEach {
cartValue += it.price
}
cartVal.postValue(priceFormatter.formatPrice(cartValue))
} ?: cartVal.postValue(priceFormatter.formatPrice(0))
return cartVal
}
fun removeFromCart(product: Product) {
userRepository.removeFromCart(product)
getUserData()
}
private fun getUserData() {
userRepository.getUserData()
}
init {
getUserData()
}
}
Default value is to solve the "initial" empty cart.
Now if you need to trigger it when there's no data... (aka: after you remove items and the list is now empty), I'd use a sealed class to wrap the actual value.
(names and code are pseudo-code, so please don't copy-paste)
Something like this:
Your Repository should expose the cart, user, etc. wrapped in a sealed class:
sealed class UserCartState {
object Empty : UserCartState()
data class HasItems(items: List<things>)
object Error(t: Throwable) :UserCartState() //hypotetical state to signal problems
}
In your CartFragmentViewModel, you observe and use when (for example), to determine what did the repo responded with.
repo.cartState.observe(...) {
when (state) {
is Empty -> //deal with it
is HasItems -> // do what it takes to convert it, calculate it, etc.
is Error -> // handle it
}
}
When the user removes the last item in the cart, your repo should emit Empty.
The VM doesn't care how that happened, it simply reacts to the new state.
The UI cares even less. :)
You get the idea (I hope).
That's how I would look into it.
You can even use a flow of cart items, or the new "FlowState" thingy (see the latest Google I/O 21) to conserve resources when the lifecycle owner is not ready.
I suppose that this part of code creates the problem
list?.let { prods ->
prods.forEach {
cartValue += it.price
}
cartVal.postValue(priceFormatter.formatPrice(cartValue))
} ?: cartVal.postValue(priceFormatter.formatPrice(0))
Probably, list is not null but is empty. Please try this:
if (list.isNullOrEmpty) {
list.forEach {
cartValue += it.price
}
cartVal.postValue(priceFormatter.formatPrice(cartValue))
} else {
cartVal.postValue(priceFormatter.formatPrice(0))
}
I'm trying to implement paging I'm using Room and it took me ages to realize that its all done for me 😆 but what I need to do is be able to filter search and sort my data. I want to keep it as LiveData for now I can swap to flow later.
I had this method to filter search and sort and it worked perfectly,
#SuppressLint("DefaultLocale")
private fun searchAndFilterPokemon(): LiveData<List<PokemonWithTypesAndSpecies>> {
return Transformations.switchMap(search) { search ->
val allPokemon = repository.searchPokemonWithTypesAndSpecies(search)
Transformations.switchMap(filters) { filters ->
val pokemon = when {
filters.isNullOrEmpty() -> allPokemon
else -> {
Transformations.switchMap(allPokemon) { pokemonList ->
val filteredList = pokemonList.filter { pokemon ->
pokemon.matches = 0
val filter = filterTypes(pokemon, filters)
filter
}
maybeSortList(filters, filteredList)
}
}
}
pokemon
}
}
}
It have a few switchmaps here, the first is responding to search updating
var search: MutableLiveData<String> = getSearchState()
the second is responding to filters updating
val filters: MutableLiveData<MutableSet<String>> = getCurrentFiltersState()
and the third is watching the searched list updating, it then calls filterTypes and maybeSortList which are small methods for filtering and sorting
#SuppressLint("DefaultLocale")
private fun filterTypes(
pokemon: PokemonWithTypesAndSpecies,
filters: MutableSet<String>
): Boolean {
var match = false
for (filter in filters) {
for (type in pokemon.types) {
if (type.name.toLowerCase() == filter.toLowerCase()) {
val matches = pokemon.matches.plus(1)
pokemon.apply {
this.matches = matches
}
match = true
}
}
}
return match
}
private fun maybeSortList(
filters: MutableSet<String>,
filteredList: List<PokemonWithTypesAndSpecies>
): MutableLiveData<List<PokemonWithTypesAndSpecies>> {
return if (filters.size > 1)
MutableLiveData(filteredList.sortedByDescending {
Log.d("VM", "SORTING ${it.pokemon.name} ${it.matches}")
it.matches
})
else MutableLiveData(filteredList)
}
as mentioned I want to migrate these to paging 3 and am having difficulty doing it Ive changed my repository and dao to return a PagingSource and I just want to change my view model to return the PagingData as a live data, so far I have this
#SuppressLint("DefaultLocale")
private fun searchAndFilterPokemonPager(): LiveData<PagingData<PokemonWithTypesAndSpecies>> {
val pager = Pager(
config = PagingConfig(
pageSize = 50,
enablePlaceholders = false,
maxSize = 200
)
){
searchAndFilterPokemonWithPaging()
}.liveData.cachedIn(viewModelScope)
Transformations.switchMap(filters){
MutableLiveData<String>()
}
return Transformations.switchMap(search) {search ->
val searchedPokemon =
MutableLiveData<PagingData<PokemonWithTypesAndSpecies>>(pager.value?.filter { it.pokemon.name.contains(search) })
Transformations.switchMap(filters) { filters ->
val pokemon = when {
filters.isNullOrEmpty() -> searchedPokemon
else -> {
Transformations.switchMap(searchedPokemon) { pokemonList ->
val filteredList = pokemonList.filter { pokemon ->
pokemon.matches = 0
val filter = filterTypes(pokemon, filters)
filter
}
maybeSortList(filters, filteredList = filteredList)
}
}
}
pokemon
}
}
}
but the switchmap is giving me an error that
Type inference failed: Cannot infer type parameter Y in
fun <X : Any!, Y : Any!> switchMap
(
source: LiveData<X!>,
switchMapFunction: (input: X!) → LiveData<Y!>!
)
which I think I understand but am not sure how to fix it, also the filter and sort methods won't work anymore and I cant see any good method replacements for it with the PageData, it has a filter but not a sort? any help appreciated
: LiveData<Y!
UPDATE thanks to #Shadow I've rewritten it to implement searching using a mediator live data but im still stuck on filtering
init {
val combinedValues =
MediatorLiveData<Pair<String?, MutableSet<String>?>?>().apply {
addSource(search) {
value = Pair(it, filters.value)
}
addSource(filters) {
value = Pair(search.value, it)
}
}
searchPokemon = Transformations.switchMap(combinedValues) { pair ->
val search = pair?.first
val filters = pair?.second
if (search != null && filters != null) {
searchAndFilterPokemonPager(search)
} else null
}
}
#SuppressLint("DefaultLocale")
private fun searchAndFilterPokemonPager(search: String): LiveData<PagingData<PokemonWithTypesAndSpecies>> {
return Pager(
config = PagingConfig(
pageSize = 50,
enablePlaceholders = false,
maxSize = 200
)
){
searchAllPokemonWithPaging(search)
}.liveData.cachedIn(viewModelScope)
}
#SuppressLint("DefaultLocale")
private fun searchAllPokemonWithPaging(search: String): PagingSource<Int, PokemonWithTypesAndSpecies> {
return repository.searchPokemonWithTypesAndSpeciesWithPaging(search)
}
I do not believe you can sort correctly on the receiving end when you are using a paging source. This is because paging will return a chunk of data from the database in whichever order it is, or in whichever order you specify in its query directly.
Say you have a list of names in the database and you want to display them sorted alphabetically.
Unless you actually specify that sorting in the query itself, the default query will fetch the X first names (X being the paging size you have configured) it finds in whichever order the database is using for the results of that query.
You can sort the results alright, but it will only sort those X first names it returned. That means if you had any names that started with A and they happened to not come in that chunk of names, they will only show when you have scrolled far enough for them to be loaded by the pager, and only then they will show sorted correctly in the present list. You might see names moving around as a result of this whenever a new page is loaded into your list.
That's for sorting, now for search.
What I ended up doing for my search was to just throw away the database's own capability for searching. You can use " LIKE " in queries directly, but unless your search structure is very basic it will be useless. There is also Fts4 available: https://developer.android.com/reference/androidx/room/Fts4
But it is such a PIA to setup and make use of that I ended up seeing absolutely no reward worth the effort for my case.
So I just do the search however I want on the receiving end instead using a Transformations.switchMap to trigger a new data fetch from the database whenever the user input changes coupled with the filtering on the data I receive.
You already have part of this implemented, just take out the contents of the Transformations.switchMap(filters) and simply return the data, then you conduct the search on the results returned in the observer that is attached to the searchAndFilterPokemonPager call.
Filter is the same logic as search too, but I would suggest to make sure to filter first before searching since typically search is input driven and if you don't add a debouncer it will be triggering a new search for every character the user enters or deletes.
In short:
sort in the query directly so the results you receive are already sorted
implement a switchMap attached to the filter value to trigger a new data fetch with the new filter value taken into account
implement a switchMap just like the filter, but for the search input
filter the returned data right before you submit to your list/recyclerview adapter
same as above, but for search
so this is at least partially possible using flows but sorting isnt possible see here https://issuetracker.google.com/issues/175430431, i havent figured out sorting yet but searching and filtering are possible, so for filtering i have it very much the same the query is a LIKE query that triggers by some livedata (the search value) emitting new data heres the search method
#SuppressLint("DefaultLocale")
private fun searchPokemonPager(search: String): LiveData<PagingData<PokemonWithTypesAndSpeciesForList>> {
return Pager(
config = PagingConfig(
pageSize = 50,
enablePlaceholders = false,
maxSize = 200
)
) {
searchAllPokemonWithPaging(search)
}.liveData.cachedIn(viewModelScope)
}
#SuppressLint("DefaultLocale")
private fun searchAllPokemonWithPaging(search: String): PagingSource<Int, PokemonWithTypesAndSpeciesForList> {
return repository.searchPokemonWithTypesAndSpeciesWithPaging(search)
}
and the repo is calling the dao, now to do the filtering #Shadow suggested using the pager this works but is much easier with flow using combine, my filters are live data and flow has some convenient extensions like asFlow so it becomes as easy as
#SuppressLint("DefaultLocale")
private fun searchAndFilterPokemonPager(search: String): Flow<PagingData<PokemonWithTypesAndSpeciesForList>> {
val pager = Pager(
config = PagingConfig(
pageSize = 50,
enablePlaceholders = false,
maxSize = 200
)
) {
searchAllPokemonWithPaging(search)
}.flow.cachedIn(viewModelScope).combine(filters.asFlow()){ pagingData, filters ->
pagingData.filter { filterTypesForFlow(it, filters) }
}
return pager
}
obviously here I've changed the return types so we also have to fix our mediator live data emitting our search and filters as it expects a live data, later ill fix this so it all uses flow
init {
val combinedValues =
MediatorLiveData<Pair<String?, MutableSet<String>?>?>().apply {
addSource(search) {
value = Pair(it, filters.value)
}
addSource(filters) {
value = Pair(search.value, it)
}
}
searchPokemon = Transformations.switchMap(combinedValues) { pair ->
val search = pair?.first
val filters = pair?.second
if (search != null && filters != null) {
searchAndFilterPokemonPager(search).asLiveData()
} else null
}
}
here we just use the asLiveData extesnion