I have a ViewPager2 in my app that gets populated with fragments dynamically based on a response from a websocket request. I also have a AppCompatImageView that has an image set in the XML layout(but later gets a new bitmap to display dynamically).
Now I have a problem that the ViewPager2 and the AppCompatImageView does not show when the app starts, the only way to show them is to force a focus change like opening a popupmenu or alertdialog.
The really weird thing is that I have another imageview in the layout that is set to a static color that is always shown...
Can someone give me a suggestion why those two views gets hidden (like if they were set to View.INVISIBLE, even though they aren't) on app launch, and even better, why do they get displayed after a focus change?
Could the fact that they get populated dynamically interfere in some way with them being rendered correctly?
I realized that my problems was because I had two race conditions.
The first was due to this code:
class ViewPagerFragmentAdapter(fragmentActivity: FragmentActivity) : FragmentStateAdapter(fragmentActivity) {
private var arrayList: MutableList<Fragment> = mutableListOf()
fun addFragment(fragment: Fragment) {
arrayList.add(fragment)
}
fun removeFragment(index: Int) {
arrayList.removeAt(index)
}
override fun getItemCount(): Int {
return arrayList.size
}
override fun createFragment(position: Int): Fragment {
return arrayList[position]
}
}
This is the adapter for the ViewPager2 and to solve it I hade to change the addFragment function to:
fun addFragment(fragment: Fragment) {
arrayList.add(fragment)
this.notifyItemInserted(this.itemCount - 1)
}
This due to the fact that if my view rendered after my websocket request finished then I got content but if it rendered before I got the reply I would only have an empty viewpager. By adding the notify call the viewpager gets notified when a fragment is added.(probably need something similar on the remove function as well)
To solve the AppCompatImageView I only changed app:srcCompat="..." in the XML to android:src="...", don't ask me why but then the view rendered...
Related
I develop an Android-App which has a RecyclerView, each item displayed in the RecyclerView represents a video saved locally. Now, when a button is clicked in one of the items in the RecyclerView, the file should be uploaded to a server. Now my question: How can I implement a solution to do this using MVVM?
I also created a little image which describes my problem:
Note: A short, conceptual answeser is sufficient
your adapter takes in a callback method to invoke once the button is tapped, the fragment then does this upload by using the VM, once that is done, success or failure you alert your UI to make whatever changes are needed. there's no interaction between your viewmodel and your adapter directly
class MyAdapter constructor(
private val callback: (item: YourType) -> Unit,
) : RecyclerView.Adapter.... {
then, you would call this in onBind or wherever you're setting up your button for the adapter, by calling:
myButton.setOnClickListener {
callback.invoke(yourItem)
}
usually this is in the format of:
override fun onBindViewHolder(holder: SomeViewHolder, position: Int) {
val yourItem: YourType = items[position]
....
yourButton.setOnClickListener {
callback.invoke(yourItem)
}
}
when you create your adapter, you now do:
myAdapter = MyAdapter () { callback ->
//here, callback will be of type `YourType`
//here, you can do whatever service call you want with your viewmodel, because this is in your fragment
}
In summary, the logic for what has to happen when an item is tapped is now shifted on to the class making use of the adapter, usually the fragment or the activity. This makes it so that you can easily reuse your adapter, because it doesn't contain any actual business logic inside of it - your adapter basically informs your fragment that:
"something was tapped, here's the corresponding item".
Your adapter notifies your fragment that an item was tapped, your fragment can then decide what to do, in this case it can shift this along to the Viewmodel so that it can then perform any network operation you need and inform the UI of the result of that operation, usually this is done by observing on to live data
I was trying to see when binding.invalidateAll() is needed to refresh the UI when the data changes.
I have 2 examples where I change the data, in one of them the UI data changes automatically and in the other, I need to use invalidateAll() to see the changes
First of all, below is the XML of the TextView I'm testing on:
...
<TextView
android:id="#+id/name_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="#{mainActivity.myName.name}" />
First Example
In the first example, if I change the data inside onCreate() or onResume() the data changes directly without the need for invalidateAll(), please find the code below:
private lateinit var binding: ActivityMainBinding
var myName = MyName(name = "Test")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
binding.mainActivity = this;
myName.name = "Data is visible directly"
}
Here, once the UI is drawn to the user, I see the TextView's text directly set as "Data is visible directly" without the need to call binding.invalidateAll()
Second Example
In the second example, I only change the text when a button is clicked as in the below code:
...
fun onButtonClickedChangeData(view:View){
myName.name = "Data is visible only after invalidateAll is called"
binding.invalidateAll() // without it the text won't change
}
In the second example, I have to use binding.invalidateAll() for the text of the TextView to change, if I don't use invalidateAll() the text will not change.
My First Speculation (which seems to be wrong)
I first thought maybe binding.invalidateAll() is needed when we change the data while the activity is already running and the UI is already visible to the user, and that invalidateAll() won't be needed if the data changes before the UI is drawn yet to the user (giving that the UI is only visible to the user in onResumue() according to my knowledge).
But when I tried to update the data inside onResumue() the data changed directly as well without the need for binding.invalidateAll()
I have a RecyclerView that contains views that can be quite complex and their inflation make the scrolling choppy if they are inflated just in time. My idea was that I could pre-inflate the views in a background thread. I've tried it and it works really nice on the emulator as well as on my Pixel 3. To be clear, they are only inflated and not added to the parent ahead of time.
I found this SO thread, where they say that there seems to be an issue with Samsung Galaxy phones, but the thread is almost 10 years old, so i am not sure if this is still an issue.
Google of course says not to do it. See here. Although the reasons they mention (mostly memory leaks) don't apply in my case. Or at least I don't see a possible Activity leak here.
This is simplified what I am doing:
class MyRecyclerAdapter(
private var items: List<Item>
) : RecyclerView.Adapter<MyViewHolder>() {
// Map to hold the pre-cached views
private val viewCache = mutableMapOf<Int, Deferred<View?>>()
companion object {
// Only inflate one view at a time
val serialCoroutineDispatcher = Executors.newFixedThreadPool(1).asCoroutineDispatcher()
}
init {
items.forEachIndexed { index, it ->
viewCache[index] = CoroutineScope(serialCoroutineDispatcher).async {
createViewForItem(it) // View is created asynchronously here
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
runBlocking {
MyViewHolder(
// If no view in cache, inflate here
viewCache.remove(viewType)?.await() ?: createViewForItem(items[viewType])
)
}
...
Has anyone tried something similar? Or can someone speak to why I should not do it (besides leaking the Activity which is not an issue here)?
FYI: I'm aware of AsyncLayoutInflater, but I cannot use it for other reasons.
I am working on a dynamic list which is viewed on RecyclerView. So far it is possible to add and remove elements. Also the user can change the content of every single element through several popUpWindows. However, MyViewHolder class got quite long due to many onClickListeners. So I carried MyRecyclerViewAdapter class in a separate file from the rest of the activity. Now,
Is it a good practice to keep MyViewHolder class long with many click listeners (doing the most of the work inside the Adapter object), or should I retrieve the relevant data from MyRecyclerViewAdapter somehow and do the 'delete, add, edit text' work inside onCreate section?
What are the most efficient, simple and fast solutions to show a totally new and different view when all elements are deleted? I tried VISIBLE, GONE solution but MyAdapter is in a separate file and I don't know how to communicate with the onCreate section to transfer real-time size of the dynamic list.
1/ if your actions are about interact with the item component (add, delete, edit, get content...) in a list your should put the function in Adapter, click on ViewHolder should only give its position in adapter. Solution here : RecyclerView itemClickListener in Kotlin
2/ why use VISIBLE, GONE ? when you delete a item that mean you delete the item in your data list so just reload the view after that, adapter will auto show the remain data
Answers:
Question 1: Kiet Phan's recommendation of carrying the logic part inside onBindViewHolder function was quite helpful.
Question 2: If you have already passed the current context to the primary constructor of the separate class (which is defined in another file), it is possible in Kotlin to reach the content of currentActivity by the code segment: (context as Activity).
So the solution and the general structure of my adapter was like this:
import ...
class MyAdapter (var ctx: Context, var list: ArrayList<SomeObject>)
: RecyclerView.Adapter<MyAdapter.MyViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyAdapter.MyViewHolder {
return MyViewHolder(LayoutInflater.from(ctx).inflate(R.layout.singleListElement, parent,false))
}
override fun getItemCount(): Int {
return list.size
}
override fun onBindViewHolder(holder: MyAdapter.MyViewHolder, position: Int) {
//Do the work here, onClickListeners, if statements etc.
//...
list.remove(index)
if(list.size == 0) {
(ctx as Activity).findViewById<TextView>(R.id.listEmpty).visibility=View.VISIBLE
}
} //Close onBindViewHolder
inner class MyViewHolder (v: View) : RecyclerView.ViewHolder(v){
val someView: Textview
init { someView = v.findViewById(R.id.someViewId) }
}
}
It is not easy to write code into this window. When I press Tab button, browser exits the edit screen and selects another part of this webpage. So the code looks like this. Maybe Copy/Paste would do better.
Improvements and more effective solutions for the empty list case are welcome. Thank you.
I'm using a RecyclerView to display some data from a Firestore database. I'm using as an adapter, the FirestoreRecyclerAdapter for obvious reasons. I'm successfully displaying all 35 items in my RecyclerView. The problem is, I cannot scroll to a specific position. This is what I have tried:
recyclerView = locationsFragmentView.findViewById(R.id.recycler_view);
recyclerView.setLayoutManager(new LinearLayoutManager(context));
FirestoreRecyclerOptions<MyModelClass> options = new FirestoreRecyclerOptions.Builder<MyModelClass>().setQuery(query, MyModelClass.class).build();
adapter = new MyFirestoreRecyclerAdapter(options);
recyclerView.setAdapter(adapter);
recyclerView.setHasFixedSize(false);
recyclerView.getLayoutManager().scrollToPosition(10);
Everytime I open my app, I'm always positioned at the first position and not on the 10'th as I specified in the scrollToPosition() method.
I have also used:
recyclerView.scrollToPosition(10);
and
recyclerView.smoothScrollToPosition(10);
But without luck. How do I scroll to specific position? Thanks!
The recyclerView doesn't scroll because it's still empty when you call recyclerView.scrollToPosition(10);, you should move that code after the recyclerView gets populated from the Firebase response, probably inside a callback.
Instead of using recyclerView.scrollToPosition(10); try to use below code to set position in recyclerview
recyclerView.smoothScrollToPosition(10);
In my case, I didn't want move to a specific position, instead I just wanted to keep the original position after returning from a different Activity or returning from background.
I just added this empty listener and it's working.
listAdapter.snapshots.addChangeEventListener(object : ChangeEventListener {
override fun onChildChanged(
type: ChangeEventType,
snapshot: DocumentSnapshot,
newIndex: Int,
oldIndex: Int
) {
}
override fun onDataChanged() {
}
override fun onError(e: FirebaseFirestoreException) {
}
})
Here listAdapter is FirestoreRecyclerAdapter.
Note: There must have some negative consequences on doing this.
I solved my problem with a Simpler solution.
I just called adapter.startListening(); in onViewCreated() instead of onStart() and called adapter.stopListening(); in onDestroyView() instead of onStop()
That prevented the entire list from regenerating while coming back from the next activity and thus retained the scroll position where it was previously.
Source: https://github.com/firebase/FirebaseUI-Android/issues/998#issuecomment-342413662