I have a file list, need query data from each file, then render Result list to LazyColumn
But I dont want render list until all the query finished, like this:
class Repo {
private val list = listOf(fileA, fileB)
fun query(key: String): Flow<List<Result>>
= list.asFlow().map{ it.query(key) }.toList()
}
#Composable
fun ListView(repo: Repo){
val res = repo.query("xxx").collectAsState(xxx)
LazyColumn{
items(res){ xxx }
}
}
How can I render a LazyColumn from Flow.collectAsState progressive
class Repo {
private val list = listOf(fileA, fileB)
fun query(key: String): Flow<Result>
= list.asFlow().map{ it.query(key) }
}
#Composable
fun ListView(repo: Repo){
val res = repo.query("xxx").collectAsState(xxx)
LazyColumn{
// ???
items(res){ xxx }
}
}
Just found derivedStateOf function work perfect for this.
class Repo {
private val list = listOf(fileA,fileB)
fun query(key: String): Flow<Result> = list.asFlow().map{xxx}
}
#Composable
fun ListView(repo: Repo){
val flow = repo.query("xxx").collectAsState(xxx)
val update by remember {
val data = mutableListOf<Result>()
derivedStateOf {
flow?.run(data::add)
data
}
}
LazyColumn {
items(update) { data: Result ->
xxxx
}
}
}
Related
I am trying to do some stuff on background and then displaying it to the user, but for some reason this does not work as it should and I am not sure what I am doing wrong.
It is an app with possibility to encrypt images and storing them on app-specific folder and holding the reference inside a database. While presenting it to the user following steps are done:
Get the reference of pictures and metadata from database.
read encrypted images and decrypt them while reading.
Print the pictures in composable.
How it works is:
Composable asks for getting data -> the repository gets the data -> my storage manager reads the files and uses the cryptomanager to decrypt them -> decrypted pictures are stored as live data
But the operation above blocks the interaction with the UI. Here is some Code:
Composable:
#Composable
fun WelcomeView(
viewModel: WelcomeViewModel = hiltViewModel()
) {
LaunchedEffect(Unit) {
viewModel.getGalleryItems()
}
val list = viewModel.images.observeAsState()
Column() {
//this button does not response until the data request and processing is done
Button(onClick = {}){
Text(text = "Click me while pictures are requested")
}
LazyVerticalGrid(columns = GridCells.Adaptive(minSize = 128.dp)) {
if (list.value != null) {
items(list.value as List<GalleryElement>) { item: GalleryElement ->
GalleryItem(element = item)
}
}
}
}
}
Thats the view model:
#HiltViewModel
class WelcomeViewModel #Inject constructor(
private val secretDataManager: SecretDataManager,
) : ViewModel() {
private val _images = MutableLiveData<List<GalleryElement>>()
val images: LiveData<List<GalleryElement>> = _images
suspend fun getGalleryItems() {
viewModelScope.launch {
_images.value = secretDataManager.getImages()
}
}
}
User data manager:
class SecretDataManager #Inject constructor(
private val cryptoManager: CryptoManager,
private val storageManager: StorageManager,
private val repo: EncryptedVaultDataRepo,
#ApplicationContext
private val ctx: Context
) : SecretDataManagerService {
override suspend fun getImages(): List<GalleryElement> {
val result: MutableList<GalleryElement> = mutableListOf()
repo.getAll().forEach {
var image: ByteArray
storageManager.readFile(File("${ctx.filesDir}/${it.name}").toUri()).use { b ->
image = cryptoManager.decrypt(it.iv, b?.readBytes()!!)
}
result.add(GalleryElement(BitmapFactory.decodeByteArray(image, 0, image.size)))
}
return result
}
}
Any ideas what I am doing wrong?
I believe the main problem is that the viewModelScope.launch(){} starts on the Dispatchers.Main(UI) thread. I recommend going to viewModelScope.launch(Dispatchers.IO){}. I am trying to find the documentation to support that but should be an easy change. I also recommended populating the list on the initialization of the view model.
#Composable
fun WelcomeView(
viewModel: WelcomeViewModel = hiltViewModel()
) {
val list = viewModel.images.observeAsState()
Column() {
//this button does not response until the data request and processing is done
Button(onClick = {}){
Text(text = "Click me while pictures are requested")
}
LazyVerticalGrid(columns = GridCells.Adaptive(minSize = 128.dp)) {
if (list.value != null) {
items(list.value as List<GalleryElement>) { item: GalleryElement ->
GalleryItem(element = item)
}
}
}
}
}
#HiltViewModel
class WelcomeViewModel #Inject constructor(
private val secretDataManager: SecretDataManager,
) : ViewModel() {
private val _images = MutableLiveData<List<GalleryElement>>()
val images: LiveData<List<GalleryElement>> = _images
init{
getGalleryImages()
}
fun getGalleryItems() {
viewModelScope.launch(Dispatchers.Default) {
_images.value = secretDataManager.getImages()
}
}
}
I just started learning Android development. I want to find a case, using Viewmodel+Room+Flow+collectAsStateWithLifecycle(), unfortunately I didn't find such a case. So I wrote the following code referring to the documentation and some information on the Internet.
Dao
#Dao
interface IArticleDao {
#Query("select * from article")
fun getAll(): List<Article>
#Insert
suspend fun add(article: Article): Long
}
Repository
private const val TAG = "ArticleRepository"
class ArticleRepository #Inject constructor(
private val articleDao: IArticleDao
) {
private val ioDispatcher = IO
fun add(article: Article): Long {
var id = 0L
CoroutineScope(ioDispatcher).launch {
try {
id = articleDao.add(article)
}catch (e: Exception){
Log.e(TAG, "There is an exception in the new article")
}
}
return id
}
fun getAll(): Flow<List<Article>> =
flow {
emit(
articleDao.getAll()
)
}.flowOn(ioDispatcher)
}
ViewModel
#HiltViewModel
class ArticleViewModel #Inject constructor(private val articleRepository: ArticleRepository) : ViewModel() {
val articles = articleRepository.getAll().stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = emptyList()
)
fun add(article: Article): Long{
return articleRepository.add(article)
}
}
Activity
Scaffold(
content = { innerPadding ->
val articles by articleViewModel.articles.collectAsStateWithLifecycle()
Content(
articles = articles, articleViewModel
)
}
)
#Composable
fun Content(
articles:
List<Article>, viewModel: ArticleViewModel
) {
Column {
Row {
Button(onClick = {
viewModel.add(Article(title = "this is a test", url = "http://www.test4.com/"))
}) {
Text("add")
}
}
Row {
LazyColumn(
modifier = Modifier.fillMaxWidth(),
contentPadding = PaddingValues(16.dp)
) {
items(items = articles, key = { article -> article.id }) { article ->
Row {
Text(text = article.title)
}
}
}
}
}
}
This creates a problem. When a new piece of data is added, a new piece of data is added to the database, but the UI interface is not updated accordingly. Only when the Home button is clicked and the application is opened again can the UI interface be updated.
I know I must be missing something, but I don't know what. Possibly the code in the Repository?
Instead of returning a list in your DAO, try returning a Flow<List<Article>>.
To do that, don't forget to add the following dependency implementation("androidx.room:room-ktx:$room_version")
#Dao
interface IArticleDao {
#Query("select * from article")
fun getAll(): Flow<List<Article>>
#Insert
suspend fun add(article: Article): Long
}
private const val TAG = "ArticleRepository"
class ArticleRepository #Inject constructor(
private val articleDao: IArticleDao
) {
private val ioDispatcher = IO
fun add(article: Article): Long {
var id = 0L
CoroutineScope(ioDispatcher).launch {
try {
id = articleDao.add(article)
}catch (e: Exception){
Log.e(TAG, "There is an exception in the new article")
}
}
return id
}
fun getAll(): Flow<List<Article>> = articleDao.getAll().flowOn(ioDispatcher)
}
Using a flow, you will listen the database changes instead of just querying one time the datas.
As a developer one needs to adapt to change, I read somewhere it says:
If you don’t choose the right architecture for your Android project, you will have a hard time maintaining it as your codebase grows and your team expands.
I wanted to implement Clean Architecture with MVVM
My app data flow will look like this:
Model class
data class Note(
val title: String? = null,
val timestamp: String? = null
)
Dtos
data class NoteRequest(
val title: String? = null,
val timestamp: String? = null
)
and
data class NoteResponse(
val id: String? = null,
val title: String? = null,
val timestamp: String? = null
)
Repository layer is
interface INoteRepository {
fun getNoteListSuccessListener(success: (List<NoteResponse>) -> Unit)
fun deleteNoteSuccessListener(success: (List<NoteResponse>) -> Unit)
fun getNoteList()
fun deleteNoteById(noteId: String)
}
NoteRepositoryImpl is:
class NoteRepositoryImpl: INoteRepository {
private val mFirebaseFirestore = Firebase.firestore
private val mNotesCollectionReference = mFirebaseFirestore.collection(COLLECTION_NOTES)
private val noteList = mutableListOf<NoteResponse>()
private var getNoteListSuccessListener: ((List<NoteResponse>) -> Unit)? = null
private var deleteNoteSuccessListener: ((List<NoteResponse>) -> Unit)? = null
override fun getNoteListSuccessListener(success: (List<NoteResponse>) -> Unit) {
getNoteListSuccessListener = success
}
override fun deleteNoteSuccessListener(success: (List<NoteResponse>) -> Unit) {
deleteNoteSuccessListener = success
}
override fun getNoteList() {
mNotesCollectionReference
.addSnapshotListener { value, _ ->
noteList.clear()
if (value != null) {
for (item in value) {
noteList
.add(item.toNoteResponse())
}
getNoteListSuccessListener?.invoke(noteList)
}
Log.e("NOTE_REPO", "$noteList")
}
}
override fun deleteNoteById(noteId: String) {
mNotesCollectionReference.document(noteId)
.delete()
.addOnSuccessListener {
deleteNoteSuccessListener?.invoke(noteList)
}
}
}
ViewModel layer is:
interface INoteViewModel {
val noteListStateFlow: StateFlow<List<NoteResponse>>
val noteDeletedStateFlow: StateFlow<List<NoteResponse>>
fun getNoteList()
fun deleteNoteById(noteId: String)
}
NoteViewModelImpl is:
class NoteViewModelImpl: ViewModel(), INoteViewModel {
private val mNoteRepository: INoteRepository = NoteRepositoryImpl()
private val _noteListStateFlow = MutableStateFlow<List<NoteResponse>>(mutableListOf())
override val noteListStateFlow: StateFlow<List<NoteResponse>>
get() = _noteListStateFlow.asStateFlow()
private val _noteDeletedStateFlow = MutableStateFlow<List<NoteResponse>>(mutableListOf())
override val noteDeletedStateFlow: StateFlow<List<NoteResponse>>
get() = _noteDeletedStateFlow.asStateFlow()
init {
// getNoteListSuccessListener
mNoteRepository
.getNoteListSuccessListener {
viewModelScope
.launch {
_noteListStateFlow.emit(it)
Log.e("NOTE_G_VM", "$it")
}
}
// deleteNoteSuccessListener
mNoteRepository
.deleteNoteSuccessListener {
viewModelScope
.launch {
_noteDeletedStateFlow.emit(it)
Log.e("NOTE_D_VM", "$it")
}
}
}
override fun getNoteList() {
// Get all notes
mNoteRepository.getNoteList()
}
override fun deleteNoteById(noteId: String) {
mNoteRepository.deleteNoteById(noteId = noteId)
}
}
and last but not least Fragment is:
class HomeFragment : Fragment() {
private lateinit var binding: FragmentHomeBinding
private val viewModel: INoteViewModel by viewModels<NoteViewModelImpl>()
private lateinit var adapter: NoteAdapter
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = FragmentHomeBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val recyclerView = binding.recyclerViewNotes
recyclerView.addOnScrollListener(
ExFABScrollListener(binding.fab)
)
adapter = NoteAdapter{itemView, noteId ->
if (noteId != null) {
showMenu(itemView, noteId)
}
}
recyclerView.adapter = adapter
// initView()
fetchFirestoreData()
binding.fab.setOnClickListener {
val action = HomeFragmentDirections.actionFirstFragmentToSecondFragment()
findNavController().navigate(action)
}
}
private fun fetchFirestoreData() {
// Get note list
viewModel
.getNoteList()
// Create list object
val noteList:MutableList<NoteResponse> = mutableListOf()
// Impose StateFlow
viewModel
.noteListStateFlow
.onEach { data ->
data.forEach {noteResponse ->
noteList.add(noteResponse)
adapter.submitList(noteList)
Log.e("NOTE_H_FRAG", "$noteResponse")
}
}.launchIn(viewLifecycleOwner.lifecycleScope)
}
//In the showMenu function from the previous example:
#SuppressLint("RestrictedApi")
private fun showMenu(v: View, noteId: String) {
val menuBuilder = MenuBuilder(requireContext())
SupportMenuInflater(requireContext()).inflate(R.menu.menu_note_options, menuBuilder)
menuBuilder.setCallback(object : MenuBuilder.Callback {
override fun onMenuItemSelected(menu: MenuBuilder, item: MenuItem): Boolean {
return when(item.itemId){
R.id.option_edit -> {
val action = HomeFragmentDirections.actionFirstFragmentToSecondFragment(noteId = noteId)
findNavController().navigate(action)
true
}
R.id.option_delete -> {
viewModel
.deleteNoteById(noteId = noteId)
// Create list object
val noteList:MutableList<NoteResponse> = mutableListOf()
viewModel
.noteDeletedStateFlow
.onEach {data ->
data.forEach {noteResponse ->
noteList.add(noteResponse)
adapter.submitList(noteList)
Log.e("NOTE_H_FRAG", "$noteResponse")
}
}.launchIn(viewLifecycleOwner.lifecycleScope)
true
} else -> false
}
}
override fun onMenuModeChange(menu: MenuBuilder) {}
})
val menuHelper = MenuPopupHelper(requireContext(), menuBuilder, v)
menuHelper.setForceShowIcon(true) // show icons!!!!!!!!
menuHelper.show()
}
}
With all the above logic I'm facing TWO issues
issue - 1
As mentioned here, I have added SnapshotListener on collection as:
override fun getNoteList() {
mNotesCollectionReference
.addSnapshotListener { value, _ ->
noteList.clear()
if (value != null) {
for (item in value) {
noteList
.add(item.toNoteResponse())
}
getNoteListSuccessListener?.invoke(noteList)
}
Log.e("NOTE_REPO", "$noteList")
}
}
with it if I change values of a document from Firebase Console, I get updated values in Repository and ViewModel, but list of notes is not being updated which is passed to adapter, so all the items are same.
issue - 2
If I delete any item from list/recyclerview using:
R.id.option_delete -> {
viewModel
.deleteNoteById(noteId = noteId)
// Create list object
val noteList:MutableList<NoteResponse> = mutableListOf()
viewModel
.noteDeletedStateFlow
.onEach {data ->
data.forEach {noteResponse ->
noteList.add(noteResponse)
adapter.submitList(noteList)
Log.e("NOTE_H_FRAG", "$noteResponse")
}
}.launchIn(viewLifecycleOwner.lifecycleScope)
still I get updated list(i.e new list of notes excluding deleted note) in Repository and ViewModel, but list of notes is not being updated which is passed to adapter, so all the items are same, no and exclusion of deleted item.
Question Where exactly I'm making mistake to initialize/update adapter? because ViewModel and Repository are working fine.
Make following changes:
In init{} block of NoteViewModelImpl :
// getNoteListSuccessListener
mNoteRepository
.getNoteListSuccessListener{noteResponseList ->
viewModelScope.launch{
_noteListStateFlow.emit(it.toList())
}
}
you must add .toList() if you want to emit list in StateFlow to get notified about updates, and in HomeFragment
private fun fetchFirestoreData() {
// Get note list
viewModel
.getNoteList()
// Impose StateFlow
lifecycleScope.launch {
viewModel.noteListStateFlow.collect { list ->
adapter.submitList(list.toMutableList())
}
}
}
That's it, I hope it works fine.
Try to remove additional lists of items in the fetchFirestoreData() and showMenu() (for item R.id.option_delete) methods of the HomeFragment fragment and see if it works:
// remove `val noteList:MutableList<NoteResponse>` in `fetchFirestoreData()` method
private fun fetchFirestoreData() {
...
// remove this line
val noteList:MutableList<NoteResponse> = mutableListOf()
// Impose StateFlow
viewModel
.noteListStateFlow
.onEach { data ->
adapter.submitList(data)
}.launchIn(viewLifecycleOwner.lifecycleScope)
}
And the same for the delete menu item (R.id.option_delete).
I am learning Android development, and as I saw in many topics, people were talking about that LiveData is not recommended to use anymore. I mean it's not up-to-date, and we should use Flows instead.
I am trying to get data from ROOM database with Flows and then convert them to StateFlow because as I know they are observables, and I also want to add UI states to them. Like when I get data successfully, state would change to Success or if it fails, it changes to Error.
I have a simple app for practicing. It stores subscribers with name and email, and show them in a recyclerview.
I've checked a lot of sites, how to use stateIn method, how to use StateFlows and Flows but didn't succeed. What's the most optimal way to do this?
And also what's the proper way of updating recyclerview adapter? Is it okay to change it all the time in MainActivity to a new adapter?
Here is the project (SubscriberViewModel.kt - line 30):
Project link
If I am doing other stuff wrong, please tell me, I want to learn. I appreciate any kind of help.
DAO:
import androidx.room.*
import kotlinx.coroutines.flow.Flow
#Dao
interface SubscriberDAO {
#Insert
suspend fun insertSubscriber(subscriber : Subscriber) : Long
#Update
suspend fun updateSubscriber(subscriber: Subscriber) : Int
#Delete
suspend fun deleteSubscriber(subscriber: Subscriber) : Int
#Query("DELETE FROM subscriber_data_table")
suspend fun deleteAll() : Int
#Query("SELECT * FROM subscriber_data_table")
fun getAllSubscribers() : Flow<List<Subscriber>>
#Query("SELECT * FROM subscriber_data_table WHERE :id=subscriber_id")
fun getSubscriberById(id : Int) : Flow<Subscriber>
}
ViewModel:
class SubscriberViewModel(private val repository: SubscriberRepository) : ViewModel() {
private var isUpdateOrDelete = false
private lateinit var subscriberToUpdateOrDelete: Subscriber
val inputName = MutableStateFlow("")
val inputEmail = MutableStateFlow("")
private val _isDataAvailable = MutableStateFlow(false)
val isDataAvailable : StateFlow<Boolean>
get() = _isDataAvailable
val saveOrUpdateButtonText = MutableStateFlow("Save")
val deleteOrDeleteAllButtonText = MutableStateFlow("Delete all")
/*
//TODO - How to implement this as StateFlow<SubscriberListUiState> ??
//private val _subscribers : MutableStateFlow<SubscriberListUiState>
//val subscribers : StateFlow<SubscriberListUiState>
get() = _subscribers
*/
private fun clearInput() {
inputName.value = ""
inputEmail.value = ""
isUpdateOrDelete = false
saveOrUpdateButtonText.value = "Save"
deleteOrDeleteAllButtonText.value = "Delete all"
}
fun initUpdateAndDelete(subscriber: Subscriber) {
inputName.value = subscriber.name
inputEmail.value = subscriber.email
isUpdateOrDelete = true
subscriberToUpdateOrDelete = subscriber
saveOrUpdateButtonText.value = "Update"
deleteOrDeleteAllButtonText.value = "Delete"
}
fun saveOrUpdate() {
if (isUpdateOrDelete) {
subscriberToUpdateOrDelete.name = inputName.value
subscriberToUpdateOrDelete.email = inputEmail.value
update(subscriberToUpdateOrDelete)
} else {
val name = inputName.value
val email = inputEmail.value
if (name.isNotBlank() && email.isNotBlank()) {
insert(Subscriber(0, name, email))
}
inputName.value = ""
inputEmail.value = ""
}
}
fun deleteOrDeleteAll() {
if (isUpdateOrDelete) {
delete(subscriberToUpdateOrDelete)
} else {
deleteAll()
}
}
private fun insert(subscriber: Subscriber) = viewModelScope.launch(Dispatchers.IO) {
repository.insert(subscriber)
_isDataAvailable.value = true
}
private fun update(subscriber: Subscriber) = viewModelScope.launch(Dispatchers.IO) {
repository.update(subscriber)
clearInput()
}
private fun delete(subscriber: Subscriber) = viewModelScope.launch(Dispatchers.IO) {
repository.delete(subscriber)
clearInput()
}
private fun deleteAll() = viewModelScope.launch(Dispatchers.IO) {
repository.deleteAll()
//_subscribers.value = SubscriberListUiState.Success(emptyList())
_isDataAvailable.value = false
}
sealed class SubscriberListUiState {
data class Success(val list : List<Subscriber>) : SubscriberListUiState()
data class Error(val msg : String) : SubscriberListUiState()
}
}
MainActivity:
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private lateinit var viewModel: SubscriberViewModel
private lateinit var viewModelFactory: SubscriberViewModelFactory
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
val dao = SubscriberDatabase.getInstance(application).subscriberDAO
viewModelFactory = SubscriberViewModelFactory(SubscriberRepository(dao))
viewModel = ViewModelProvider(this, viewModelFactory)[SubscriberViewModel::class.java]
binding.viewModel = viewModel
binding.lifecycleOwner = this
initRecycleView()
}
private fun initRecycleView() {
binding.recyclerViewSubscribers.layoutManager = LinearLayoutManager(
this#MainActivity,
LinearLayoutManager.VERTICAL, false
)
displaySubscribersList()
}
private fun displaySubscribersList() {
/*
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.subscribers.collect { uiState ->
when (uiState) {
is SubscriberViewModel.SubscriberListUiState.Success -> {
binding.recyclerViewSubscribers.adapter = SubscriberRecyclerViewAdapter(uiState.list) {
subscriber: Subscriber -> listItemClicked(subscriber)
}
}
is SubscriberViewModel.SubscriberListUiState.Error -> {
Toast.makeText(applicationContext,uiState.msg, Toast.LENGTH_LONG).show()
}
}
}
}
}*/
}
private fun listItemClicked(subscriber: Subscriber) {
Toast.makeText(this, "${subscriber.name} is selected", Toast.LENGTH_SHORT).show()
viewModel.initUpdateAndDelete(subscriber)
}
}
You can convert a Flow type into a StateFlow by using stateIn method.
private val coroutineScope = CoroutineScope(Job())
private val flow: Flow<CustomType>
val stateFlow = flow.stateIn(scope = coroutineScope)
In order to transform the CustomType into UIState, you can use the transformLatest method on Flow. It will be something like below:
stateFlow.transformLatest { customType ->
customType.toUiState()
}
Where you can create an extension function to convert CustomType to UiState like this:
fun CustomType.toUiState() = UiState(
x = x,
y = y... and so on.
)
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.