hi I'm getting information from web with jsoup and coroutine and I want to show data in recyclerview
All the information is well received but the RecyclerView does not show anything and the view is not updated
fun myCoroutine(): ArrayList<DataModel> {
val listx = arrayListOf<DataModel>()
GlobalScope.launch { // launch new coroutine in background and continue
Log.d("asdasdasd", "start")
var doc: Document = Jsoup.connect("http://5743.zanjan.medu.ir").timeout(0).maxBodySize(0).ignoreHttpErrors(true).sslSocketFactory(setTrustAllCerts()).get()
val table: Elements = doc.select("table[class=\"table table-striped table-hover\"]")
for (myTable in table) {
val rows: Elements = myTable.select("tr")
for (i in 1 until rows.size) {
val row: Element = rows.get(i)
val cols: Elements = row.select("td")
val href: Elements = row.select("a")
val strhref: String = href.attr("href")
listx.add(DataModel(cols.get(2).text(),strhref))
Log.d("asdasf",cols.get(2).text())
}
}
}
return listx
}
private fun getData() {
itemsData = ArrayList()
itemsData = myCoroutine()
adapter.notifyDataSetChanged()
adapter = RVAdapter(itemsData)
}
and this is oncreate
var itemsData = ArrayList<DataModel>()
adapter = RVAdapter(itemsData)
val llm = LinearLayoutManager(this)
itemsrv.setHasFixedSize(true)
itemsrv.layoutManager = llm
getData()
itemsrv.adapter = adapter
This code has numerous bugs (getData, for instance, never sets the adapter onto the RecyclerView), but the biggest issue is that you're not actually waiting for listx to be populated - you're returning it immediately before it's populated. You need to either move the population of the adapter to the coroutine and run that part on the UI thread dispatcher, or use a callback, or dispatch it to the UI thread. Launching a coroutine and returning immediately doesn't make the data get populated when something tries to use it.
Related
I have a room database where I have songs associated with artists and when I change artist from the Main Activity overflow menu, the fragment with the recyclerview showing a list of songs doesn't update unless I navigate away from the fragment and back again. I thought my observing of the list was sufficient because it worked for other changes being made but not this time.
How do I get it to update with the new artist's songs when data changes?
songViewModel
//default artist name for this phase of production
var artistName = "Ear Kitty"
private val _artistNameLive = MutableLiveData<String>(artistName)
val artistNameLive: LiveData<String>
get() = _artistNameLive
private var _allSongs : MutableLiveData<List<SongWithRatings>> = repository.getArtistSongsWithRatings(artistNameLive.value.toString()).asLiveData() as MutableLiveData<List<SongWithRatings>>
val allSongs: LiveData<List<SongWithRatings>>
get() = _allSongs
fun changeArtist(artist: String){
_artistNameLive.value = artist
artistName = artist
updateAllSongs()
}
fun updateAllSongs() = viewModelScope.launch {
run {
_allSongs = repository.getArtistSongsWithRatings(artistNameLive.value.toString())
.asLiveData() as MutableLiveData<List<SongWithRatings>>
}
}
MainFragment
The observer worked fin when changes were made to all songs but not when it was updated entirely with a different artist.
songViewModel.allSongs.observe(viewLifecycleOwner) { song ->
// Update the cached copy of the songs in the adapter.
Log.d("LiveDataDebug","Main Fragment Observer called")
Log.d("LiveDataDebug",song[0].song.songTitle)
song.let { adapter.submitList(it) }
}
I find a bunch of answers saying to make a function in the Fragment that I call from Main Activity but I don't know where that function would go since I can't make the adapter a member outside of the onViewCreated. At first I'd get an error saying the adapter may have changed then I tried this below and got a null pointer exception.
MainFragment
lateinit var adapter: ItemAdapter
fun notifyThisFragment(){
adapter.notifyDataSetChanged()
}
MainActivity
songViewModel.changeArtist(artistList[which])
val navHostController = supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
//I get the null pointer exception on the line below
val mainFrag: MainFragment = navHostController.childFragmentManager.findFragmentById(R.id.mainFragment) as MainFragment
mainFrag.notifyThisFragment()
As I understand it, my main activity hosts the navHostFragment which is the parent fragment to my MainFragment. Am I getting something wrong there? Is there a different way I should be doing this? I'm trying to follow the suggested architecture rather than do a bunch of weird work abounds. Am I supposed to only get allSongs from the db one time and filter it in songViewModel? I don't understand why allSongs.observe isn't getting the change.
I ended up solving the problem by making an allArtistSongsWithRatings that I use for my list adapter. I filter the list from allSongsWithRatings to avoid extra db queries. This was unsuccessful at first because I was returning null when I'd try and filter allSongsWithRatings. It turns out I needed to observe allSongsWithRatings BEFORE filtering it because of the way the LiveData works. I observe allSongsWithRatings in MainActivity and initializeArtist.
Main Activity
//initialize viewModel
songViewModel.allSongsWithRatings.observe(this){
songViewModel.initializeWithArtist()
}
Main Fragment
songViewModel.allArtistSongsWithRatings.observe(viewLifecycleOwner) { song ->
// Update the cached copy of the songs in the adapter.
Log.d("LiveDataDebug","Main Fragment Observer called")
//Log.d("LiveDataDebug",song[0].song.songTitle)
song.let { adapter.submitList(it) }
}
SongViewModel
var artistName = "Ear Kitty"
private val _artistNameLive = MutableLiveData<String>(artistName)
val artistNameLive: LiveData<String>
get() = _artistNameLive
private val _allSongsWithRatings : MutableLiveData<List<SongWithRatings>> = repository.allSongsWithRatings.asLiveData() as MutableLiveData<List<SongWithRatings>>
val allSongsWithRatings: LiveData<List<SongWithRatings>>
get() = _allSongsWithRatings
private val _allArtistSongsWithRatings = (artistNameLive.value?.let {
repository.getArtistSongsWithRatings(
it
).asLiveData()
} )as MutableLiveData<List<SongWithRatings>>
val allArtistSongsWithRatings: LiveData<List<SongWithRatings>>
get() = _allArtistSongsWithRatings
fun changeArtist(artist: String){
_artistNameLive.value = artist
artistName = artist
initializeWithArtist()
}
fun initializeWithArtist(){
var newList = mutableListOf<SongWithRatings>()
newList = allSongsWithRatings.value as MutableList<SongWithRatings>
//make sure the list isn't empty
if(newList.isNullOrEmpty()){
//handle empty list error
}else{
//list sorted by performance rating
_allArtistSongsWithRatings.value = newList.filter { it.song.artistName == artistNameLive.value } as MutableList<SongWithRatings>
allArtistSongsWithRatings.value
}
}
Using kotlin.
I want to make childRecyclerView in parentRecyclerView.
Maybe I think this problem is from recyclerView adapter.
Data is getting from Firebase. (Cloud Firestore)
I completed 'parent RecyclerView'.
And I also wrote adapting code for childRecyclerView at parentRecyclerView's adapter.
In parentRecyclerView, I asked to receive childRecyclerView's Firebase Data
And I checked through the log that this data was well received.
But my app show only parentRecyclerView's content.
This code is adapting code for ChildRecyclerView in ParentRecyclerView's adapter.
And I write this code at ParentRecyclerView's bind() function. This fun is also contain showing ParentRecyclerView's content(Like Glide for parentRecyclerView's ImageView).
val db = FirebaseFirestore.getInstance()
val queryText2: Query = db.collection("spiceTable")
var queryText3: Query
val storageRef2 = Firebase.storage.reference.child("scentnote/spiceimage")
var j: Int = 1
var detailSpiceList = mutableListOf<SpiceDetailValue>()
queryText2.get().addOnSuccessListener { documents ->
//Loop in parentRecyclerView's content
.addOnSuccessListener { documentx ->
//Getting ChildRecyclerView's content from Firebase
}.addOnCompleteListener {
nAdapter = context?.let { NoteSubRecyclerviewAdapter(it, detailSpiceList) }
fragment2NoteParentItemBinding?.fragment2NoteChildList?.adapter = nAdapter
val gridLayoutManager = GridLayoutManager(context, 4)
fragment2NoteParentItemBinding?.fragment2NoteChildList?.layoutManager = gridLayoutManager
}
detailSpiceList = mutableListOf<SpiceDetailValue>()
From ".addOnCompleteListener", To "~gridLayoutManager}" code is same with adapting ParentRecyclerView at my Fragment. (in code, fragment2Note)
I try to Log at end of addOnSuccessListener and start of addOnCompleteListener.
And addOnCompleteListener's Log is shown faster than addOnSuccessListener's Log.
I don't know why this code work like this.
And also i don't know why this adapter can't work.
At Logcat,
No adapter attached; skipping layout
this log is shown several time but i don't know their reason.
Please help...
And I'm Sorry because I'm not good at English
Please do like this
Declare adapter at the first stage with empty data.
val db = FirebaseFirestore.getInstance()
val queryText2: Query = db.collection("spiceTable")
var queryText3: Query
val storageRef2 = Firebase.storage.reference.child("scentnote/spiceimage")
var j: Int = 1
var detailSpiceList = mutableListOf<SpiceDetailValue>()
nAdapter = context?.let { NoteSubRecyclerviewAdapter(it, detailSpiceList) }
fragment2NoteParentItemBinding?.fragment2NoteChildList?.adapter = nAdapter
val gridLayoutManager = GridLayoutManager(context, 4)
fragment2NoteParentItemBinding?.fragment2NoteChildList?.layoutManager = gridLayoutManager
queryText2.get().addOnSuccessListener { documents ->
//Loop in parentRecyclerView's content
.addOnSuccessListener { documentx ->
//Getting ChildRecyclerView's content from Firebase
// After getting data, you asssign data to adapter
adapter.setItem(detailSpiceList); // You need to create function to attach data to adapter. (it is easy and common way)
}.addOnCompleteListener {
}
I am drawing a list of devices
#Composable
fun DeviceListScreen(){
val model: DeviceListViewModel = hiltViewModel()
val myDevices: List<MyDevice> by model.myDevices.observeAsState(emptyList())
for(device in myDevices)
Device(device)
}
In model I have a livedata
private val items: List<MyDevice> = ArrayList()
private val _myDevices = MutableLiveData<List<MyDevice>> (emptyList())
val myDevices: LiveData<List<MyDevice>> = _myDevices
I change content of an item then update live data
items[0].signal = 54
_myDevices.value = items
However data is not updating in ui.
I guess this is because the pointer to list was not changed and number of items in the list also is not changes and thus compose does not update this data.
Update 12-08-2022
I've just encountered the same problem you had. I have successfully solved it, so the following solution that I have adapted for your problem should also work fine:
fun updateSignal(idOfSelectedDevice: Int) {
_myDevices.value = myDevices.value?.map { device ->
if (device.id == idOfSelectedDevice) device.copy(
signal = <YOUR_DESIRED_VALUE>
) else device
}
}
You could try wrapping items in its own data class:
data class ItemsList(val devices: List<MyDevice> = ArrayList())
Then change items to private val items: ItemsList = ItemsList()
Since you are using your own data class for items, you can then access the copy function which copies an existing object into a new object and should therefore trigger the update of the liveData object:
_myDevices.value = _myDevices.value?.copy(items = items.devices.apply {
this[0].signal = 54
})
In my Fragment for my Android app, I'm using Firebase Realtime Database and Moshi to save and load the data I get from my RecyclerView.
These are the functions I use for this task:
private fun saveData() {
val moshi = Moshi.Builder().add(BigDecimalAdapter).add(KotlinJsonAdapterFactory()).build()
val listMyData = Types.newParameterizedType(List::class.java, ItemCard::class.java)
val jsonAdapter: JsonAdapter<ArrayList<ItemCard>> = moshi.adapter(listMyData)
val json = jsonAdapter.toJson(dataList)
userInfo.child("jsonData").setValue(json)
}
private fun loadData(json: String) = lifecycleScope.launch(Dispatchers.IO) {
if (json != "") {
val type: Type = object : TypeToken<List<ItemCard>>() {}.type
val moshi = Moshi.Builder().add(BigDecimalAdapter).add(KotlinJsonAdapterFactory()).build()
val jsonAdapter: JsonAdapter<ArrayList<ItemCard>> = moshi.adapter(type)
dataList = jsonAdapter.fromJson(json)!!
if (dataList == null) {
dataList = arrayListOf<ItemCard>()
}
}
}
private fun buildRecyclerView() {
recyclerView = rootView.findViewById(R.id.main_recycler_view)
recyclerView.setHasFixedSize(true)
recyclerViewLayoutManager = LinearLayoutManager(this#Main.requireContext())
adapter = MainAdapter(dataList, this)
recyclerView.layoutManager = recyclerViewLayoutManager
recyclerView.adapter = adapter
}
In my onViewCreated, I having this for loading the data and building the RecyclerView:
userInfo.addValueEventListener(object : ValueEventListener {
override fun onDataChange(dataSnapshot: DataSnapshot) {
for (postSnapshot in dataSnapshot.children) {
when (postSnapshot.key) {
"jsonData" -> {
loadData(postSnapshot.value.toString())
buildRecyclerView()
}
}
}
}
override fun onCancelled(error: DatabaseError) {}
})
Everything works as I want, however, there's a delay/lag when I go to this specific fragment. There are a total of three fragments in my app. The other two work smoothly with no delay/lag, but when I click on the button or slide the screen to go to this fragment, there's a delay in the change of the UI.
What can I do to make the performance better? Where should I put my addValueEventListener? I only want it to get triggered when the fragment is first created and when the jsonData child gets changed. I believe in my onViewCreated the listener is being triggered multiple times. Is there anything else I can add to my code or modify to make the performance better when saving and loading the RecyclerView data?
when you create a listerner in onViewCreated run then make sure that you remove listener when fragment is not attach.Realtime event listerner return a string that you can easily = to your pojo class like:
ItemCard message = messageSnapshot.getValue(ItemCard.class);
this way is to saving manual converting the list effort.Last important thing that if recyclerview is initialize then don't initialize when data change only notifyDataSetChange.when you adding data in the list then make sure that the
list.clear();
otherwise you data is duplicate because on addValueEventListener return the whole data.
hi im using recyclerview in a fragment and coroutine, but recyclerview not update and is empty
this is my code that fetch data from web and i wrote that in OnCreateView
launch {
val operation = async(Dispatchers.IO) {
Log.d("asdasdasd", "start")
var doc: Document = Jsoup.connect("http://5743.zanjan.medu.ir").timeout(0).maxBodySize(0).ignoreHttpErrors(true).get()
val table: Elements = doc.select("table[class=\"table table-striped table-hover\"]")
for (myTable in table) {
val rows: Elements = myTable.select("tr")
for (i in 1 until rows.size) {
val row: Element = rows.get(i)
val cols: Elements = row.select("td")
val href: Elements = row.select("a")
val strhref: String = href.attr("href")
itemsData.add(CircularModel(cols.get(2).text(),strhref,"2019",""))
}
}
}
operation.await() // wait for result of I/O operation without blocking the main thread
// update views
activity?.runOnUiThread{
adapter = CircularAdapter(itemsData, this#CircularFragment)
adapter.notifyDataSetChanged()
rc.adapter = adapter
}
}
i checked and itemsData isnt empty but i dont know why recyclerview not updated
Try using withContext(Dispatchers.Main) instead of activity?.runOnUiThread{ syntax to access Main thread from the coroutine
Try to call adapter.notifyDataSetChanged() after setting adapter to your Recycler