I am trying to do an async call and then update a RecyclerView. Much like what's outlined in this question: RecyclerView element update + async network call
However, when I try to do this, I get this error:
android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
Here is my code (the main issue is in the setAlbums function):
class AlbumActivity : AppCompatActivity() {
protected lateinit var adapter: MyRecyclerViewAdapter
protected lateinit var recyclerView: RecyclerView
var animalNames = listOf("nothing")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_album)
recyclerView = findViewById<RecyclerView>(R.id.rvAnimals)
recyclerView.layoutManager = LinearLayoutManager(this)
adapter = MyRecyclerViewAdapter(this, animalNames)
recyclerView.adapter = adapter
urlCall("https://rss.itunes.apple.com/api/v1/us/apple-music/coming-soon/all/10/explicit.json")
}
private fun urlCall(url: String) {
val client = OkHttpClient()
val request = Request.Builder().url(url).build()
client.newCall(request).enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {}
override fun onResponse(call: Call, response: Response) = getJSON(response.body()?.string())
})
}
fun getJSON(data: String?) {
val gson = Gson()
val allAlbums = ArrayList<Album>()
val jsonResponse = JSONObject(data)
val feed = jsonResponse.getJSONObject("feed")
val albums = feed.getJSONArray("results")
for (i in 0 until albums.length()) {
val album = albums.getJSONObject(i)
allAlbums.add(gson.fromJson(album.toString(), Album::class.java))
}
setAlbums(allAlbums)
}
fun setAlbums(albums: ArrayList<*>) {
animalNames = listOf("sue", "betsie")
adapter.notifyDataSetChanged() // This is where I am telling the adapter the data has changed
}
internal inner class Album {
var artistName: String? = null
var name: String? = null
}
}
Does anyone know the issue I am having?
You need to execute your wanted code on your main thread like this :
runOnUiThread(new Runnable() {
#Override
public void run() {
//update your UI
}
});
Your Callback functions will be called on a background thread. In part, that is because OkHttp is not an Android-specific library, so it has no idea about Android's main application thread.
You will need to do something to update the UI in the main application thread. Modern options include:
Have your Callback update a MutableLiveData that your UI observes, as then the UI will get updates on the main application thread
Use existing recipes for using OkHttp with RxJava
Related
I have a main activity with a heading and a search field (edit text), I want to be able to search and the results are immediately shown in the fragment, like an onChange instead of waiting for the user to click a button to filter results. (which is in the activity).
I can get it working if I include the Edit Text in my fragment too, but I don't want it that way for design purposes, I'd like to retrieve the user values as they are typed from the activity, and get them in my fragment to filter results
I've tried Bundles but could not get it working, and also not sure If i could use Bundles to get the results as they are being input.
Here's a screenshot to help understand better
You can make it happen using ViewModel + MVVM architecture.
MainActivity:
binding.editText.addTextChangedListener(object: TextWatcher {
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
}
override fun afterTextChanged(s: Editable?) {
viewModel.updateSearchText(s)
}
})
ViewModel:
private val _searchText = MutableLiveData<Editable?>()
val searchText: LiveData<Editable?> get() = _searchText
fun updateSearchText(text: Editable?) {
_searchText.value = s
}
Fragment:
searchText.observe(viewLifecycleOwner) {
// TODO: handle the searched query using [it] keyword.
}
If you don't know what View Model is or how to implement it, use the official Google tutorial: https://developer.android.com/codelabs/basic-android-kotlin-training-viewmodel
Another way to achieve this (besides using an Android ViewModel) is use the Fragment Result API.
For instance, if you place the EditText into a fragment (let's call it QueryFragment), you can get the result of the QueryFragment in your SearchResults fragment like so:
// In QueryFragment
editText.addTextChangedListener(object: TextWatcher {
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) { }
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { }
override fun afterTextChanged(s: Editable?) {
setFragmentResult("searchQueryRequestKey", bundleOf("searchQuery" to s.toString()))
}
})
// In SearchResultsFragment
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Retrieve the searched string from QueryFragment
// Use the Kotlin extension in the fragment-ktx artifact
setFragmentResultListener("searchQueryRequestKey") { requestKey, bundle ->
val searchQuery = bundle.getString("searchQuery")
// Perform the search using the searchQuery and display the search results
}
}
I have two queries:
Query firstQuery = ref.orderBy("name", Query.Direction.ASCENDING).limit(10);
getData(firstQuery);
Query secondQuery = ref.orderBy("price", Query.Direction.ASCENDING).limit(10);
getMoreData(secondQuery);
The first method looks like this:
private void getData(Query query) {
firestoreRecyclerOptions = new FirestoreRecyclerOptions.Builder<ModelClass>().setQuery(query, ModelClass.class).build();
myFirestoreRecyclerAdapter = new MyFirestoreRecyclerAdapter(firestoreRecyclerOptions);
recyclerView.setAdapter(myFirestoreRecyclerAdapter);
}
And here is the second method.
private void getMoreData(Query query) {
firestoreRecyclerOptions = new FirestoreRecyclerOptions.Builder<ModelClass>().setQuery(query, ModelClass.class).build();
myFirestoreRecyclerAdapter = new MyFirestoreRecyclerAdapter(firestoreRecyclerOptions);
recyclerView.setAdapter(myFirestoreRecyclerAdapter);
}
Both variables are declared as global:
private FirestoreRecyclerOptions<ModelClass> firestoreRecyclerOptions;
private MyFirestoreRecyclerAdapter myFirestoreRecyclerAdapter;
When the app starts, the elements are displayed in the RecyclerView using the first method. What I want to achieve is that on a button click, when the getMoreData() method is triggered to add the result from the second query in the same adapter, ending up having 20 elements. Now, when I click the button, the elements from the second query will override the first ones.
There is nothing built-in to combine two queries in a FirestoreRecyclerAdapter.
The best I can think of is creating a List/array of the combined results in your app code and then using an array adapter. It's not ideal, since you won't be using FirebaseUI.
Alternatively, have a look at FirebaseUIs FirestorePagingAdapter, which combines multiples pages of (non-realtime) DocumentSnapshots in a single recycler view.
I ended up using a modified version of adapter class from the Friendly-eats code lab sample.
The following class allows you add an initial query and then set another one using the FirestoreAdapter.setQuery(query) method.
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.OnLifecycleEvent
import androidx.recyclerview.widget.RecyclerView
import com.google.firebase.firestore.*
import com.google.firebase.firestore.EventListener
import java.util.*
/**
* RecyclerView adapter for displaying the results of a Firestore [Query].
*
* Note that this class forgoes some efficiency to gain simplicity. For example, the result of
* [DocumentSnapshot.toObject] is not cached so the same object may be deserialized
* many times as the user scrolls.
*
*
* See the adapter classes in FirebaseUI (https://github.com/firebase/FirebaseUI-Android/tree/master/firestore) for a
* more efficient implementation of a Firestore RecyclerView Adapter.
*/
abstract class FirestoreAdapter<VH : RecyclerView.ViewHolder>(private var query: Query,
private val lifecycleOwner: LifecycleOwner)
: RecyclerView.Adapter<VH>(), EventListener<QuerySnapshot>, LifecycleObserver {
private var listener: ListenerRegistration? = null
private val snapshots = ArrayList<DocumentSnapshot>()
#OnLifecycleEvent(Lifecycle.Event.ON_START)
fun startListening() {
if (listener == null) {
listener = query.addSnapshotListener(this)
}
}
#OnLifecycleEvent(Lifecycle.Event.ON_STOP)
fun stopListening() {
listener?.apply {
remove()
listener = null
}
snapshots.clear()
notifyDataSetChanged()
}
#OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
internal fun cleanup(source: LifecycleOwner) {
source.lifecycle.removeObserver(this)
}
override fun onEvent(snapshot: QuerySnapshot?, error: FirebaseFirestoreException?) {
when {
error != null -> onError(error)
else -> {
// Dispatch the event
snapshot?.apply {
for (change in documentChanges) {
when (change.type) {
DocumentChange.Type.ADDED -> onDocumentAdded(change)
DocumentChange.Type.MODIFIED -> onDocumentModified(change)
DocumentChange.Type.REMOVED -> onDocumentRemoved(change)
}
}
onDataChanged()
}
}
}
}
protected fun onDocumentAdded(change: DocumentChange) {
snapshots.add(change.newIndex, change.document)
notifyItemInserted(change.newIndex)
}
protected fun onDocumentModified(change: DocumentChange) {
if (change.oldIndex == change.newIndex) {
// Item changed but remained in same position
snapshots[change.oldIndex] = change.document
notifyItemChanged(change.oldIndex)
} else {
// Item changed and changed position
snapshots.removeAt(change.oldIndex)
snapshots.add(change.newIndex, change.document)
notifyItemMoved(change.oldIndex, change.newIndex)
}
}
protected fun onDocumentRemoved(change: DocumentChange) {
snapshots.removeAt(change.oldIndex)
notifyItemRemoved(change.oldIndex)
}
fun setQuery(query: Query) {
stopListening()
// Clear existing data
snapshots.clear()
notifyDataSetChanged()
// Listen to new query
this.query = query
startListening()
}
override fun getItemCount(): Int = snapshots.size
protected fun getSnapshot(index: Int): DocumentSnapshot = snapshots[index]
protected open fun onError(exception: FirebaseFirestoreException) {}
protected open fun onDataChanged() {}
}
I am using altbeacon library in one of my android projects. In that project BeaconConsumer is already implemented in one of the activities. When I try to implement beacon detection in my part of the code beacon is not detecting.
But when i remove the BeaconConsumer code from the previously written code, then my code works and detects beacon.
This is the log when the beacon is not detecting. Why are there multiple consumers and is it the problem? If so, how can I remove multiple consumers.
I/System.out: ---------onBeaconServiceConnect
D/BeaconParser: Parsing beacon layout: m:2-3=beac,i:4-19,i:20-21,i:22-23,p:24-24,d:25-25
D/BeaconParser: Parsing beacon layout: x,s:0-1=feaa,m:2-2=20,d:3-3,d:4-5,d:6-7,d:8-11,d:12-15
D/BeaconParser: Parsing beacon layout: s:0-1=feaa,m:2-2=00,p:3-3:-41,i:4-13,i:14-19
D/BeaconParser: Parsing beacon layout: s:0-1=feaa,m:2-2=10,p:3-3:-41,i:4-20v
D/BeaconParser: Parsing beacon layout: m:2-3=0215,i:4-19,i:20-21,i:22-23,p:24-24
D/BeaconLocalBroadcastProcessor: Register calls: global=6 instance=6
D/BeaconLocalBroadcastProcessor: Register calls: global=7 instance=7
D/BeaconManager: consumer count is now: 2
D/BeaconManager: This consumer is not bound. Binding now: MainActivity#11b9235
D/BeaconManager: Not starting beacon scanning service. Using scheduled jobs
D/BeaconLocalBroadcastProcessor: Register calls: global=8 instance=8
D/BeaconManager: consumer count is now: 3
D/BeaconManager: Unbinding
D/BeaconManager: Not unbinding from scanning service as we are using scan jobs.
D/BeaconManager: Before unbind, consumer count is 3
D/BeaconManager: After unbind, consumer count is 2
This is the class i am using
class BeaconsDataSource(private val context: Context) : BeaconConsumer, RangeNotifier,MonitorNotifier {
private val beaconManager: BeaconManager =
BeaconManager.getInstanceForApplication(context)
private val region = Region("myRegion", null, null, null)
private lateinit var data: Step
private var listener: ((Step) -> Unit)? = null
override fun getApplicationContext(): Context = context
override fun unbindService(p0: ServiceConnection?) {
applicationContext.unbindService(p0!!)
}
override fun bindService(p0: Intent?, p1: ServiceConnection?, p2: Int): Boolean {
return applicationContext.bindService(p0, p1!!, p2)
}
override fun onBeaconServiceConnect() {
BeaconManager.setDebug(true)
beaconManager.beaconParsers
.add(BeaconParser().setBeaconLayout("m:2-3=beac,i:4-19,i:20-21,i:22-23,p:24-24,d:25-25"))
beaconManager.beaconParsers
.add(BeaconParser().setBeaconLayout("x,s:0-1=feaa,m:2-2=20,d:3-3,d:4-5,d:6-7,d:8-11,d:12-15"))
beaconManager.beaconParsers
.add(BeaconParser().setBeaconLayout("s:0-1=feaa,m:2-2=00,p:3-3:-41,i:4-13,i:14-19"))
beaconManager.beaconParsers.add(BeaconParser().setBeaconLayout("s:0-1=feaa,m:2-2=10,p:3-3:-41,i:4-20v"))
beaconManager.beaconParsers
.add(BeaconParser().setBeaconLayout("m:2-3=0215,i:4-19,i:20-21,i:22-23,p:24-24"))
beaconManager.removeAllMonitorNotifiers()
beaconManager.removeAllRangeNotifiers()
beaconManager.addMonitorNotifier(this)
beaconManager.addRangeNotifier(this)
try {
beaconManager.startMonitoringBeaconsInRegion(region)
beaconManager.startRangingBeaconsInRegion(region)
} catch (e: RemoteException) {
e.printStackTrace()
}
}
override fun didRangeBeaconsInRegion(p0: MutableCollection<Beacon>, p1: Region?) {
data = Step.ENTER_NUMBER
notifyListeners()
}
private fun notifyListeners() {
listener?.invoke(data)
}
fun requestBeaconsUpdate(listener: (Step) -> Unit) {
this.listener = listener
beaconManager.bind(this)
}
fun stopBeaconsUpdate() {
beaconManager.unbind(this)
}
override fun didDetermineStateForRegion(p0: Int, p1: Region?) {
val msg = "---------did determine state for region $p0"
Log.d("TAG", msg)
}
override fun didEnterRegion(p0: Region?) {
val msg = "-----------did enter region"
val zone = p0.toString()
Log.d("TAG", "----------Enter in region")
val text = "Enter in $zone"
Log.d("TAG", msg)
}
override fun didExitRegion(p0: Region?) {
val msg = "------------did exit region"
Log.d("TAG", msg)
}
}
This is my livedata class which i am calling from viewmodel
class BeaconsLiveData() : LiveData<Step>() {
private val beaconDataSource = BeaconsDataSource(App.getAppContext())
private val listener = { data: Step ->
value = data
}
override fun onActive() {
super.onActive()
beaconDataSource.requestBeaconsUpdate(listener)
}
override fun onInactive() {
super.onInactive()
beaconDataSource.stopBeaconsUpdate()
}
}
It is hard to say what is causing the multiple bind operations but that likely is contributing to your detection problems. I suspect it has something to do with the LiveData lifecycle. Debugging this is unfortunately more involved than possible (for me) looking at the code snippet.
In general you should design your system so it calls beaconManager.bind(...) and .unbind(...) infrequently. Doing so frequently causes problems as it will start and stop services which is a heavy weight mechanism.
I did create a working Kotlin sample app here in case that is helpful to you. This sample demonstrates using MutableLiveData to share beacon detection data from a central class that is doing the detection to your Activity. The central class in this case is an Android application class, but it could just as well be any singleton with a long lifecycle. The key thing is that it calls bind(...) once when it starts up, and won't call it again until you are done working with beacons.
Edit
I've created a demo project on Github showing the exact problem. Git Project.
I've written an expandable recyclerView in Kotlin Every row has a play button which uses TextToSpeech. The text of the play button should change to stop whilst its playing, then change back to play when it finishes.
When I call notifyItemChanged within onStart and onDone of setOnUtteranceProgressListener, onBindViewHolder is not called and the rows in the recyclerView will no longer expand and collapse correctly.
t1 = TextToSpeech(context, TextToSpeech.OnInitListener { status ->
if (status != TextToSpeech.ERROR) {
t1?.setOnUtteranceProgressListener(object : UtteranceProgressListener() {
override fun onStart(utteranceId: String?) {
recyclerView.adapter.notifyItemChanged(position)
}
override fun onStop(utteranceId: String?, interrupted: Boolean) {
super.onStop(utteranceId, interrupted)
onDone(utteranceId)
}
override fun onDone(utteranceId: String?) {
val temp = position
position = -1
recyclerView.adapter.notifyItemChanged(temp)
}
override fun onError(utteranceId: String?) {}
// override fun onError(utteranceId: String?, errorCode: Int) {
// super.onError(utteranceId, errorCode)
// }
})
}
})
onBindViewHolder:
override fun onBindViewHolder(holder: RabbanaDuasViewHolder, position: Int){
if (Values.playingPos == position) {
holder.cmdPlay.text = context.getString(R.string.stop_icon)
}
else {
holder.cmdPlay.text = context.getString(R.string.play_icon)
}
}
How can I call notifyItemChanged(position) from within setOnUtteranceProgressListener or what callback can I use so that notifyItemChanged only executes when it's safe to execute?
I tried to replicate your issue and I came to know that it is not working because methods of UtteranceProgressListener is not called on main thread and that's why onBindViewHolder method of the adapter is not called.
This worked for me and should work for you too:
Use runOnUiThread{} method to perform actions on RecyclerView like this:
t1.setOnUtteranceProgressListener(object : UtteranceProgressListener() {
override fun onError(utteranceId: String?) {
}
override fun onStart(utteranceId: String?) {
runOnUiThread {
recyclerView.adapter?.notifyItemChanged(position)
}
}
override fun onStop(utteranceId: String?, interrupted: Boolean) {
super.onStop(utteranceId, interrupted)
onDone(utteranceId)
}
override fun onDone(utteranceId: String?) {
val temp = position
position = -1
runOnUiThread {
recyclerView.adapter?.notifyItemChanged(temp)
}
}
}
I solved the problem using runOnUiThread thanks to Birju Vachhani.
For a full working demo of not just the problem I had, but how to correctly expand and collapse rows in a RecyclerView (no onClick events in onBindViewHolder) see my Gitlab Demo Project.
I want to know what is the best approach to display some sort of message in the view from the ViewModel. My ViewModel is making a POST call and "onResult" I want to pop up a message to the user containing a certain message.
This is my ViewModel:
public class RegisterViewModel extends ViewModel implements Observable {
.
.
.
public void registerUser(PostUserRegDao postUserRegDao) {
repository.executeRegistration(postUserRegDao).enqueue(new Callback<RegistratedUserDTO>() {
#Override
public void onResponse(Call<RegistratedUserDTO> call, Response<RegistratedUserDTO> response) {
RegistratedUserDTO registratedUserDTO = response.body();
/// here I want to set the message and send it to the Activity
if (registratedUserDTO.getRegisterUserResultDTO().getError() != null) {
}
}
});
}
And my Activity:
public class RegisterActivity extends BaseActivity {
#Override
protected int layoutRes() {
return R.layout.activity_register;
}
#Override
protected void onCreate(Bundle savedInstanceState) {
AndroidInjection.inject(this);
super.onCreate(savedInstanceState);
ActivityRegisterBinding binding = DataBindingUtil.setContentView(this, layoutRes());
binding.setViewModel(mRegisterViewModel);
}
What would the best approach be in this case?
We can use a SingleLiveEvent class as a solution. But it is a LiveData that will only send an update once. In my personal experience, using an Event Wrapper class with MutableLiveData is the best solution.
Here is a simple code sample.
Step 1 :
Create an Event class (this is a boilerplate code you can reuse for any android project).
open class Event<out T>(private val content: T) {
var hasBeenHandled = false
private set // Allow external read but not write
/**
* Returns the content and prevents its use again.
*/
fun getContentIfNotHandled(): T? {
return if (hasBeenHandled) {
null
} else {
hasBeenHandled = true
content
}
}
/**
* Returns the content, even if it's already been handled.
*/
fun peekContent(): T = content
}
Step 2 :
At the top of your View Model class, define a MutableLiveData with wrapper (I used a String here, but you can use your required data type), and a corresponding live data for encapsulation.
private val statusMessage = MutableLiveData<Event<String>>()
val message : LiveData<Event<String>>
get() = statusMessage
Step 3 :
You can update the status message within the functions of the ViewModel like this:
statusMessage.value = Event("User Updated Successfully")
Step 4 :
Write code to observe the live data from the View (activity or fragment)
yourViewModel.message.observe(this, Observer {
it.getContentIfNotHandled()?.let {
Toast.makeText(this, it, Toast.LENGTH_LONG).show()
}
})
Display Toast/snackbar message in view (Activity/Fragment) from viewmodel using LiveData.
Step:
Add LiveData into your viewmodel
View just observe LiveData and update view related task
For example:
In Viewmodel:
var status = MutableLiveData<Boolean?>()
//In your network successfull response
status.value = true
In your Activity or fragment:
yourViewModelObject.status.observe(this, Observer { status ->
status?.let {
//Reset status value at first to prevent multitriggering
//and to be available to trigger action again
yourViewModelObject.status.value = null
//Display Toast or snackbar
}
})