I have data class, MySection, which has the ID in it. I have another data class MyArticle whdoesn'ton't care about its content
To get the Section,
private fun getSectionFlow(): Flow<List<MySection>> =
callbackFlow {
val listCallBackFlow = object : SectionCallback<List<MySection>>() {
override fun onSuccess(sections: List<MySection>) {
trySend(sections)
}
override fun onError(errorResponse: ErrorResponse) {
close(Exception(errorResponse.reason))
}
}
provider.getSections(
listCallBackFlow
)
awaitClose()
}
And for Article,
private fun getArticleFlow(id: Long): Flow<List<MyArticle>> =
callbackFlow {
val listCallBackFlow = object : ArticleCallback<List<MyArticle>>() {
override fun onSuccess(sections: List<MyArticle>) {
trySend(sections)
}
override fun onError(errorResponse: ErrorResponse) {
close(Exception(errorResponse.reason))
}
}
provider.getArticle(
id, // to get the article list it needs section id
listCallBackFlow
)
awaitClose()
}
And finally, all values need to bind in List<MySectionWithArticle>
data class MySectionWithArticle (
val id: Long,
val title: String,
var myArticlesList: List<MyArticle> = emptyList()
)
So the inputs to getArticleFlow(id) should be the IDs of the values from getSectionFlow()
So the main question is how can I combine two callbackflow functions in such a way it gives me combine values MySection ,List<MyArticle> to MySectionWithArticle?
So for List<MySection>, each MySection need to call getArticleFlow(id) function and then List<MyArticle> combine with MySectionWithArticle data class
How can I achieve this functionality ?
Related
I've been stuck trying to figure out how to update the list that my RecyclerView is showing.
What I'm trying to do is show a subset of a shown list when a spinner is changed. I have a collection of animals in my database and some have their pet attribute set as true and others have it set as false.
Using Room Database with repositories and viewModels, and what I've been trying to piece together is that it's good to have three different lists that I can tune into, so in m
Repository:
class AnimalRepository(private val animalDao: AnimalDao) {
val allAnimals: Flow<List<Animal>> = animalDao.getAnimalsByCategory()
val pets: Flow<List<Animal>> = animalDao.getAnimalsByPetStatus(true)
val nonPets: Flow<List<Animal>> = animalDao.getAnimalsByPetStatus(false)
#Suppress("RedundantSuspendModifier")
#WorkerThread
suspend fun insert(animal: Animal) {
animalDao.insert(animal)
}
#WorkerThread
suspend fun get(id: Int): Animal {
return animalDao.get(id)
}
#WorkerThread
suspend fun delete(id: Int) {
animalDao.delete(id)
}
}
ViewModel
class AnimalViewModel(private val repository: AnimalRepository) : ViewModel() {
var allAnimals: LiveData<List<Animal>> = repository.allAnimals.asLiveData()
val pets: LiveData<List<Animal>> = repository.pets.asLiveData()
val nonPets: LiveData<List<Animal>> = repository.nonPets.asLiveData()
var result: MutableLiveData<Animal> = MutableLiveData<Animal>()
var mode: VIEW_MODES = VIEW_MODES.BOTH
/*
* Launching a new coroutine to insert the data in a non-blocking way
* */
fun insert(animal: Animal) = viewModelScope.launch {
repository.insert(animal)
}
/*
* Launching a new coroutine to get the data in a non-blocking way
* */
fun get(id: Int) = viewModelScope.launch {
result.value = repository.get(id)
}
fun delete(id: Int) = viewModelScope.launch {
repository.delete(id)
}
}
class AnimalViewModelFactory(private val repository: AnimalRepository) : ViewModelProvider.Factory {
override fun <T: ViewModel> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(AnimalViewModel::class.java)) {
#Suppress("UNCHECKED_CAST")
return AnimalViewModel(repository) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
}
In my MainActivity I have it set up where I have an observer on these three lists and depending on which view mode is active (the spinner sets the view mode), that list is fed into the my RecyclerView's ListAdapter's submitList
animalViewModel.allAnimals.observe(this) { animals ->
if (viewMode == VIEW_MODES.BOTH) {
animals.let {
adapter.submitList(it)
// recyclerView.adapter = adapter
}
}
}
animalViewModel.pets.observe(this) { animals ->
if (viewMode == VIEW_MODES.PETS) {
animals.let {
adapter.submitList(it)
// recyclerView.adapter = adapter
}
}
}
animalViewModel.nonPets.observe(this) { animals ->
if (viewMode == VIEW_MODES.NON_PETS) {
animals.let {
adapter.submitList(it)
}
}
}
I am changing the mode with my spinner doing
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
when (position) {
0 -> {
viewMode = VIEW_MODES.BOTH
}
1 -> {
viewMode = VIEW_MODES.PETS
}
2 -> {
viewMode = VIEW_MODES.NON_PETS
}
}
adapter.notifyDataSetChanged()
}
This works fine if add or remove an animal after changing the view mode since the observers fire and the correct one is allowed to populate the adapter, but the notifyDataSetChanged() isn't doing anything and I've been stuck on getting the adapter to update without having to add or remove from the lists
I also tried resetting the adapter in the observer but that didn't do anything either
I am extremely new to kotlin and android programming, and I'm sure that I'm going about this the wrong way, but is there a way force a list refresh?
Update:
I think I may have found a found a solution but I worry that it's hacky. In my ViewModel I am replacing the contents of my allAnimals with the filtered lists
fun showBoth() {
allAnimals = repository.allAnimals.asLiveData()
}
fun showPets() {
allAnimals = repository.pets.asLiveData()
}
fun showNonPets() {
allAnimals = repository.nonPets.asLiveData()
}
and then in my main activity I changed my logic on when handling the spinner change to tell the view model to do its thing and then to remove the observer and slap it back on
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
when (position) {
0 -> {
animalViewModel.showBoth()
}
1 -> {
animalViewModel.showPets()
}
2 -> {
animalViewModel.showNonPets()
}
}
refreshObserver()
}
private fun refreshObserver() {
animalViewModel.allAnimals.removeObservers(this)
animalViewModel.allAnimals.observe(this) { animals ->
animals.let {
adapter.submitList(it)
}
}
}
this seems to work to get the recycler view to update, but is it hacky?
As far as I can see it makes perfect sense that notifyDataSetChanged isn't doing anything, you don't submit any new data before that call. However I think what you're trying to do is to get the adapter to react to a change in viewMode.
If this is the case, I would recommend also having your viewMode as a LiveData object and then expose a single list for your adapter to observe, which changes depending on the viewMode selected.
The Transformations.switchMap(LiveData<X>, Function<X, LiveData<Y>>) method (or its equivalent Kotlin extension function) would probably do most of the work for you here. In summary it maps the values of one LiveData to another. So in your example, you could map your viewMode to one of the allAnimals, pets and nonPets.
Here is a simple pseudocode overview for some clarity:
AnimalViewModel {
val allAnimals: LiveData<List<Animal>>
val pets: LiveData<List<Animal>>
val nonPets: LiveData<List<Animal>>
val modes: MutableLiveData<VIEW_MODES>
val listAnimals = modes.switchMap {
when (it) {
VIEW_MODES.BOTH -> allAnimals
...
}
}
}
fun onItemSelected {
viewModel.onModeChanged(position)
}
viewModel.listAnimals.observe {
adapter.submitList(it)
}
The "proper" way to update views with Android seems to be LiveData. But I can't determine the "proper" way to connect that to a model. Most of the documentation I have seen shows connecting to Room which returns a LiveData object. But (assuming I am not using Room), returning a LiveData object (which is "lifecycle aware", so specific to the activity/view framework of Android) in my model seems to me to violate the separation of concerns?
Here is an example with Activity...
class MainActivity: AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main_activity);
val viewModel = ViewModelProvider(this).get(UserViewModel::class.java)
val nameText = findViewById<TextView>(R.id.nameTextBox)
viewModel.getName().observe(this, { name ->
nameText.value = name
})
}
}
And ViewModel...
class UserViewModel(): ViewModel() {
private val name: MutableLiveData<String> = MutableLiveData()
fun getName() : LiveData<String> {
return name
}
}
But how do I then connect that to my Model without putting a "lifecycle aware" object that is designed for a specific framework in my model (LiveData)...
class UserModel {
val uid
var name
fun queryUserInfo() {
/* API query here ... */
val request = JSONObjectRequest( ...
{ response ->
if( response.name != this.name ) {
this.name = response.name
/* Trigger LiveData update here somehow??? */
}
}
)
}
}
I am thinking I can maybe put an Observable object in my model and then use that to trigger the update of the LiveData in my ViewModel. But don't find any places where anyone else says that is the "right" way of doing it. Or, can I instantiate the LiveData object in the ViewModel from an Observable object in my model?
Or am I just thinking about this wrong or am I missing something?
This is from official documentation. Check comments in code...
UserModel should remain clean
class UserModel {
private val name: String,
private val lastName: String
}
Create repository to catch data from network
class UserRepository {
private val webservice: Webservice = TODO()
fun getUser(userId: String): LiveData<UserModel > {
val data = MutableLiveData<UserModel>() //Livedata that you observe
//you can get the data from api as you want, but it is important that you
//update the LiveDate that you will observe from the ViewModel
//and the same principle is in the relation ViewModel <=> Fragment
webservice.getUser(userId).enqueue(object : Callback<UserModel > {
override fun onResponse(call: Call<User>, response: Response<UserModel >) {
data.value = response.body()
}
// Error case is left out for brevity.
override fun onFailure(call: Call<UserModel >, t: Throwable) {
TODO()
}
})
return data //you will observe this from ViewModel
}
}
The following picture should explain to you what everything looks like
For more details check this:
https://developer.android.com/jetpack/guide
viewmodels-and-livedata-patterns-antipatterns
I have Room Entity Class "Symptom" with name of Symptom and id of it.
#Entity(tableName = "symptoms")
data class Symptom(
#PrimaryKey #NonNull val id: Int,
val name: String) {
override fun toString(): String {
return "Symptom $id: $name"
}
}
I'm getting it in the following classses:
SymptomDao
#Dao
interface SymptomDao {
#Query("SELECT * FROM symptoms WHERE id=:id LIMIT 1")
fun getSymptom(id: Int): Symptom
#Query("SELECT * FROM symptoms")
fun getAllSymptoms(): LiveData<List<Symptom>>
}
SymptomRepository
class SymptomRepository(private val symptomDao: SymptomDao) {
fun getSymptom(id: Int) = symptomDao.getSymptom(id)
fun getAllSymptoms() = symptomDao.getAllSymptoms()
}
SymptomsViewModel
class SymptomsViewModel(symptomRepository: SymptomRepository): ViewModel() {
private val symptomsList = symptomRepository.getAllSymptoms()
private val symptomsItemsList: MutableLiveData<List<SymptomItem>> = MutableLiveData()
fun getAllSymptoms(): LiveData<List<Symptom>> {
return symptomsList
}
fun getAllSymptomsItems(): LiveData<List<SymptomItem>> {
return symptomsItemsList
}
}
I have RecyclerView with list of SymptomItem with Checkboxes to remember which Symptoms of a list users chooses:
data class SymptomItem(
val symptom: Symptom,
var checked: Boolean = false)
Question
My question is how can I get LiveData<List<SymptomItem>> by LiveData<List<Symptom>>? I have just started learning MVVM and I can't find a simply answer how to do that. I have already tried to fill this list in various ways, but It loses checked variable every time I rotate my phone. I'll be grateful for any hints.
You'll need to store which items are checked by storing their Ids in a List within the ViewModel. Then you'll have combine the list of your Symptom objects and the list of which items are checked, and generate the list of SymptomItem objects.
I'm going to use Kotlin Flow to achieve this.
#Dao
interface SymptomDao {
#Query("SELECT * FROM symptoms")
fun flowAllSymptoms(): Flow<List<Symptom>>
}
class SymptomRepository(private val symptomDao: SymptomDao) {
fun flowAllSymptoms() = symptomDao.flowAllSymptoms()
}
class SymptomsViewModel(
private val symptomRepository: SymptomRepository
) : ViewModel() {
private val symptomsListFlow = symptomRepository.flowAllSymptoms()
private val symptomsItemsList: MutableLiveData<List<SymptomItem>> = MutableLiveData()
private var checkedIdsFlow = MutableStateFlow(emptyList<Int>())
init {
viewModelScope.launch {
collectSymptomsItems()
}
}
private suspend fun collectSymptomsItems() =
flowSymptomsItems().collect { symptomsItems ->
symptomsItemsList.postValue(symptomsItems)
}
private fun flowSymptomsItems() =
symptomsListFlow
.combine(checkedIdsFlow) { list, checkedIds ->
list.map { SymptomItem(it, checkedIds.contains(it.id)) }
}
fun checkItem(id: Int) {
(checkedIdsFlow.value as MutableList<Int>).add(id)
checkedIdsFlow.value = checkedIdsFlow.value
}
fun uncheckItem(id: Int) {
(checkedIdsFlow.value as MutableList<Int>).remove(id)
checkedIdsFlow.value = checkedIdsFlow.value
}
fun getSymptomsItems(): LiveData<List<SymptomItem>> {
return symptomsItemsList
}
}
In your Fragment, observe getSymptomsItems() and update your adapter data.
The code is not tested, you may have to make small adjustments to make it compile.
I have three data classes, and I want create a generic function to map them:
data class Visits(
val present: List<Present>,
val past: List<Past>
)
data class Present{
val field1: String,
val field2: String
}
data class Past{
val field1: String,
val field2: String
}
The generic function I had tried something like
private fun doSomething(visits: Visits, position: Int) {
when (position) {
0 -> setItems(visits.present)
1 -> setItems(visits.past)
...
}
}
private fun <T> setItems(visits: List<T>): ArrayList<Something> {
val items: ArrayList<Something> = arrayListOf()
visits.forEach { i ->
items.add(
Something(
i.field1,
i.field2
)
)
}
return items
}
Overall thats the code, I've tried to use something like
inline fun <reified T> doSomethingWithType(list: List<T>) {
// do something with visits list, which can be List<Present> or List<Past>
}
I would like to avoid having duplicated code for setItems function for example, setItemsPresent and setItemsPast.
I would suggest to solve this with an interface,
interface Item {
val field1: String
val field2: String
}
data class Present(
override val field1: String,
override val field2: String
) : Item
data class Past(
override val field1: String,
override val field2: String
) : Item
Then your child fragment can take a list of type List<Item> and work whether they're Present or Past data objects.
There's then not much point to a generic map function, but so you know, what you need is more info about the generic through a generic constraint.
data class Something(
val field1: String,
val field2: String
)
fun <T : Item> mapToSomething(visits: List<T>): List<Something> {
return visits.map { Something(it.field1, it.field2) }
}
I have created a Generic Fragment class to handle all type of responses from server. I want to do some sort of DataTableProvider<*> to hanle any type of response.
How could I achieve this.
class TestFragmentActivity : AppCompatActivity(), DataTableProvider<Any> {
protected val mTabPatientsFragment = TabPatientsFragment()
protected val mTabObservationsFragment = TabObservationsFragment()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_test_fragment)
replaceFragment()
}
private fun replaceFragment(){
supportFragmentManager.beginTransaction().replace(R.id.frame_container,
mTabPatientsFragment).commit()
}
override fun getDataTableListener(mTableFragment: DataTableFragment<Any>): DataTableListener<Any> {
val dataTableId = mTableFragment.dataTableId
if (dataTableId.equals("observations"))
return mTabObservationsFragment
else return mTabPatientsFragment
}
override fun getDataTableConfig(mTableFragment: DataTableFragment<Any>): DataTableConfig {
val dataTableId = mTableFragment.dataTableId
val config = DataTableConfig()
config.noRecordCell = R.layout.cell_no_record
config.showSearch = false
config.showAddButton = false
if (dataTableId.equals("observations"))
{
config.cellResourceId = R.layout.home_observation_cell
} else config.cellResourceId = R.layout.home_patient_cell
return config
}
}
getDataTableListener callback in above fragment has error type mismatch required DataTableListener found TabObservationFragment
TabObservationFragment
class TabObservationFragment : AppBaseFragment(),DataTableListener<Observation>
TabPatientFragment
class TabPatientFragment : AppBaseFragment(),DataTableListener<Patient>
How could I set it to work for all type of responses.
I tried DataTableListener<*> but could not achieve
The error states
projections are not allowed for immediate arguments of a supertype
How could I use DataTableProvider<*> to work for all type of responses
Edit
I have couple of fragment with fragmentViewpager inside TestFragmentActivity .
I have got a structure that helps to implement pagination ,search and implement everything in a fragment. But according to that structure DataTableProvider must be handle in activity and basis of tableId I updated callback of getDataTableListener and getDataTableListener
The above callback should return some type of
Is there a way to achieve callback like below
override fun getDataTableConfig(mTableFragment: DataTableFragment<*>?): DataTableConfig?
override fun getDataTableListener(mTableFragment: DataTableFragment<*>?): DataTableListener<*>?
Quick answer, use "out" modifier:
fun getDataTableListener(mTableFragment: DataTableFragment<Any>): DataTableListener<out Any>
Long answer:
What you are looking for is variance, which can you read about in official kotlin docs:
https://kotlinlang.org/docs/reference/generics.html
Because for example List interface looks like this:
public interface List<out E>
You can do assigement like this:
val list: List<Any> = listOf(1,2,3)
But it is not possible to do:
val mutableList : MutableList<Any> = listOf(1,2,3)
Because MutableList doesn't have "out" modifier. It makes sense, because MutableList can be changed, to MutableList you could add for example String, but it already points to List.
In your example you can use "out" modifier, if, and only if, your DataTableListener doesn't use generic type as input. For example:
interface DataTableListener<T>{
fun assignValue(t:T)
fun getValue():T
}
With interface like above, you still could use "out" modifier in your function, but you won't be able to execute "assignValue" function.
Whole example:
class Patient
class Observation
class DataTableFragment<T>
interface DataTableListener<T> {
fun assignValue(t: T)
fun getValue(): T
}
class TabObservationFragment : DataTableListener<Observation> {
override fun getValue(): Observation {
TODO("Not yet implemented")
}
override fun assignValue(t: Observation) {
TODO("Not yet implemented")
}
}
class TabPatientFragment : DataTableListener<Patient> {
override fun getValue(): Patient {
}
override fun assignValue(t: Patient) {
TODO("Not yet implemented")
}
}
val mTabObservationsFragment = TabObservationFragment()
val mTabPatientsFragment = TabPatientFragment()
fun getDataTableListener(mTableFragment: DataTableFragment<Any>): DataTableListener<out Any> {
val test = "observations"
if (test == "observations")
return mTabObservationsFragment
else return mTabPatientsFragment
}
fun getIt() {
val listener = getDataTableListener(DataTableFragment())
listener.assignValue("test")
}