While I can scroll to a specific index of my RecyclerView I cannot scroll to an item that satisfies a given Matcher that relies upon the values of data bound values.
If I hard code the layouts of the items inserted into the RecyclerView with values then the Matcher finds them but if these values are filled through data binding the values the Matcher is comparing are all "empty". This happens even though the pending bindings have all been completed and no amount of waiting (or sleeping) or anything will make them available.
It is also interesting to note that Espresso's "withText" Matcher can 'see' the values when run as a simple assertion but when used as a Matcher in the "scrollTo(...)" method it fails to find them.
It's worth noting that the "scrollTo(...)" method does cause my adapter's "onCreateViewHolder(...)" to be run again but the adapter's "onBindViewHolder(...)" is also run before the checks start and inserting IdlingResource blockers in these methods does not help.
Any idea what is happening here?
Edit: I have made a simple stand alone project that illustrates this issue in an attempt to isolate the problem.
Adding specific code as an example
The "text_row_item"s Layout file looks like this:
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable
name="text"
type="String"
/>
</data>
<TextView
android:id="#+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="#{text}"
/>
</layout>
My Adapter looks like this:
class CustomAdapter(private val dataSet: List<String>) :
RecyclerView.Adapter<CustomAdapter.CustomViewHolder>() {
override fun onCreateViewHolder(viewGroup: ViewGroup, viewType: Int): CustomViewHolder {
val binding: ViewDataBinding = DataBindingUtil.inflate(
LayoutInflater.from(viewGroup.context),
R.layout.text_row_item,
viewGroup,
false)
binding.executePendingBindings()
return CustomViewHolder(binding)
}
override fun onBindViewHolder(holder: CustomViewHolder, position: Int) {
holder.bind(dataSet[position])
}
inner class CustomViewHolder(private val binding: ViewDataBinding)
: RecyclerView.ViewHolder(binding.root) {
fun bind(item: String) {
binding.setVariable(BR.text, item)
}
}
override fun getItemCount() = dataSet.size
}
MainActivity.kt:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val recyclerView = findViewById<RecyclerView>(R.id.recycler_view)
recyclerView.layoutManager = LinearLayoutManager(this)
recyclerView.adapter = CustomAdapter(listOf(
"W00t",
"W01t",
.
.
.
"W41t",
"W42t"
))
}
}
And my actual Espresso test file itself contains:
#Test
fun checkW00_42t() {
withText("W00t").assertAny(isDisplayed())
checkText("W00t")
checkText("W01t")
.
.
.
checkText("W41t")
checkText("W42t")
}
private fun checkText(text: String) {
Espresso.onView(withId(R.id.recycler_view))
.perform(RecyclerViewActions.scrollTo<RecyclerView.ViewHolder>(
hasDescendant(withText(text))))
withText(text).assertAny(isDisplayed())
And this is what this app looks like at the time the test is run:
There are 43 "W00t"s added to the RecyclerView so that it will need to be scrolled to see the last ones however it fails to find even the first one that doesn't require scrolling at all.
It's worth noting that the first check (which doesn't use "scrollTo(...)") passes:
withText("W00t").assertAny(isDisplayed())
However attempting to scroll to the exact same text fails:
checkText("W00t")
It is also worth noting that if instead of using DataBinding one were to just assign the text in onBindViewHolder, as in:
viewHolder.textView.text = dataSet[position]
the scrollTo(...) works.
Oh my goodness, I think I have it!
I had to move executePendingBindings() from onCreateViewHolder(...) to CustomViewHolder():
override fun onCreateViewHolder(viewGroup: ViewGroup, viewType: Int): CustomViewHolder {
val binding: ViewDataBinding = DataBindingUtil.inflate(...)
// binding.executePendingBindings() // ** remove here **
return CustomViewHolder(binding)
}
inner class CustomViewHolder(private val binding: ViewDataBinding)
: RecyclerView.ViewHolder(binding.root) {
fun bind(item: String) {
binding.setVariable(BR.text, item)
binding.executePendingBindings() // ** insert here **
}
}
Related
I am trying to pass an ArrayList<Profile> in a bundle from one fragment to another using the Navigation Graph, but I am getting this error Type mismatch: inferred type is Array<Profile> but Array<(out) Parcelable!>? was expected I have already passed on the navigation the type of argument that I want to pass. What am I missing? Here my code
Code that passes the argument
emptyHomeViewModel.playerByIDLiveData.observe(viewLifecycleOwner) { profile ->
if (profile != null) {
profilesList.add(profile)
bundle = Bundle().apply {
putSerializable("user", profilesList)
}
findNavController().navigate(
R.id.action_emptyHomeFragment_to_selectUserFragment,
bundle
)
Navigation XML for the fragment that will receive
<fragment
android:id="#+id/selectUserFragment"
android:name="com.example.dota2statistics.SelectUserFragment"
android:label="fragment_select_user"
tools:layout="#layout/fragment_select_user" >
<argument
android:name="user"
app:argType="com.example.dota2statistics.data.models.byID.Profile[]" />
</fragment>
Code of the fragment that receives the ArrayList
class SelectUserFragment : Fragment(R.layout.fragment_select_user) {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val args : SelectUserFragmentArgs by navArgs()
val profilesList = args.user
Log.i("Profiles", "onViewCreated: ${profilesList[0].personaname} ================")
}
add this plugin
plugins {
id("kotlin-parcelize")
}
then make your class parcelable for example
import kotlinx.parcelize.Parcelize
#Parcelize
class User(val firstName: String, val lastName: String, val age: Int): Parcelable
Safe args only allows passing Array so before adding bundle we have to convert ArrayList to Array
bundle.putParcelableArray("user", profilesList.toTypedArray())
Then when getting the argument we can convert it back to ArrayList
val list: ArrayList<Profile> = ArrayList(args.user.toList())
Current issue is a bit complicated, so I will outline some words with "" so whoever reads this could be able to understand text below.
I have a "page viewer", which implements "recycler view adapter".
Each page has 4 textviews inside it, i pass text to "adapter" through "constructor parameter".
My goal is to get position of textviews (filled with text), and use it to set margin to views in main fragment (which contains page viewer).
I tried to do it with help of "view model live data", so i passed setters for "view model strings live data" as "lambda" to "recycler view adapter" and I tried to "set text" and "get position" for "strings live data" in onBindViewHolder fun. Here is the adapter:
class PagerAdapter(private val spannableStrings: List<List<SpannableStringBuilder>>,
val liveDataSetters: List<(Int)->Unit>) : RecyclerView.Adapter<PagerAdapter.ViewHolder>() {
var location1 = IntArray(2)
var location2 = IntArray(2)
var location3 = IntArray(2)
var location4 = IntArray(2)
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
var textView1: TextView? = null
var textView2: TextView? = null
var textView3: TextView? = null
var textView4: TextView? = null
init {
textView1 = itemView.findViewById(R.id.textview1)
textView2 = itemView.findViewById(R.id.textview2)
textView3 = itemView.findViewById(R.id.textview3)
textView4 = itemView.findViewById(R.id.textview4)
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder =
ViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.recyclerview_item, parent, false))
override fun getItemCount(): Int = spannableStrings.size
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
if (position==0) {
holder.textView1?.text = spannableStrings[0][0]
holder.textView2?.text = spannableStrings[0][1]
holder.textView3?.text = spannableStrings[0][2]
holder.textView4?.text = spannableStrings[0][3]
holder.textView1?.getLocationOnScreen(location1)
liveDataSetters[0](location1[1])
holder.textView2?.getLocationOnScreen(location2)
liveDataSetters[1](location2[1])
holder.textView3?.getLocationOnScreen(location3)
liveDataSetters[2](location3[1])
holder.textView4?.getLocationOnScreen(location4)
liveDataSetters[3](location4[1])
}
else {
holder.textView1?.text = spannableStrings[1][0]
holder.textView2?.text = spannableStrings[1][1]
holder.textView3?.text = ""
holder.textView4?.text = ""
holder.textView1?.getLocationOnScreen(location1)
liveDataSetters[0](location1[0])
holder.textView2?.getLocationOnScreen(location2)
liveDataSetters[1](location2[0])
}
}
}
Here is code snippet regarding this from main fragment:
binding.viewpager2.adapter= activity?.let {
PagerAdapter(listOf(
listOf(text1a, text2a, text3a, text4a), listOf(text1b, text2b)
),
listOf(viewModel.setLocation1, viewModel.setLocation2, viewModel.setLocation3, viewModel.setLocation4)
)
}
TabLayoutMediator(binding.tabLayout, binding.viewpager2) { tab, position ->
}.attach()
Code snippet from view model:
private var _location1 = MutableLiveData<String>()
val location1: LiveData<String> = _location1
val setLocation1: (Int) -> Unit = { value -> location1.value=value.toString()+"dp"}
...
And finally xml:
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="viewModel"
type="com.example.project.ui.main.view.viewModel" />
</data>
...
<ImageView
android:id="#+id/pointer1a"
android:layout_width="42dp"
android:layout_height="26dp"
android:layout_marginTop="#{viewModel.location1}"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
...
However, what i recieve on launch is:
error: cannot find symbol
import com.example.project.databinding.FragmentBindingImpl;
As far as i understand, no live data could be passed to xml margin top parameter, because whenever i set this data to, let's say, random textview as text, everythin works fine. How can i solve this case? Maybe there are any alternatives or workarounds that i am missing?
I use LiveData in my layout file, and add observe event for some LiveData variable , you can see Code C.
1: Why can I use assign either this.viewLifecycleOwner or this to binding.lifecycleOwner in Code A?
2: I think mHomeViewModel.listVoiceBySort.observe(this) {... } in Code B can work well, but in fact, it failed, why?
Code A
binding.lifecycleOwner = this.viewLifecycleOwner //It can work well
binding.lifecycleOwner = this //It can work well
Code B
mHomeViewModel.listVoiceBySort.observe(viewLifecycleOwner) { //It can work well
mHomeViewModel.listVoiceBySort.observe(this) { //It cannot work
Code C
class FragmentHome : Fragment() {
private lateinit var binding: LayoutHomeBinding
private val mHomeViewModel by lazy {
getViewModel {
HomeViewModel(mActivity.application, provideRepository(mContext))
}
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
binding = DataBindingUtil.inflate(
inflater, R.layout.layout_home, container, false
)
binding.lifecycleOwner = this.viewLifecycleOwner
//binding.lifecycleOwner = this //It can work well
binding.aHomeViewModel = mHomeViewModel
mHomeViewModel.listVoiceBySort.observe(viewLifecycleOwner) {
//mHomeViewModel.listVoiceBySort.observe(this) { //It cannot work
myAdapter.submitList(it)
}
...
return binding.root
}
private fun showActionMenu() {
val view = mActivity.findViewById<View>(R.id.menuMoreAction) ?: return
PopupMenu(mContext, view).run {
menuInflater.inflate(R.menu.menu_option_action, menu)
for (item in menu){
if (item.itemId == R.id.menuMoreActionShowCheckBox){
mHomeViewModel.displayCheckBox.observe(this#FragmentHome){
//mHomeViewModel.displayCheckBox.observe(this#FragmentHome.viewLifecycleOwner){ //It can work well
if (it){
item.title =mContext.getString(R.string.menuMoreActionHideCheckBox)
}else{
item.title =mContext.getString(R.string.menuMoreActionShowCheckBox)
}
}
}
}
...
}
}
...
}
viewLifecycleOwner is available only after view of the Fragment is being inflated. Referencing the Fragment this for lifecycleOwner will use the Fragment lifecycle.
You can use this (referencing the Fragment) before view of the fragment is inflated, in onAttach() and onCreateView(). While viewLifecycle can be used in and after any point after view is completely created, after onViewCreated().
1: Why can I use assign either this.viewLifecycleOwner or this to binding.lifecycleOwner in Code A?
Because they are both LifecycleOwner. Although you generally want to use viewLifecycleOwner from onViewCreated, otherwise you can get bugs on fragments that are in "limbo state" (replace().addToBackStack()'d).
2.) private fun showActionMenu() { ... observe(
You generally shouldn't "start observing" things that are triggered by side-effects, otherwise you will end up with observers that don't unregister even after your PopupMenu is dismissed, for example.
I have followed the instructions in Google Codelab about the Saved state module.
My gradle dependency is:
implementation "androidx.lifecycle:lifecycle-viewmodel-savedstate:1.0.0-rc03"
My View Model factory is:
class MyViewModelFactory(val repository: AppRepository, owner: SavedStateRegistryOwner,
defaultArgs: Bundle? = null) : AbstractSavedStateViewModelFactory(owner, defaultArgs) {
override fun <T : ViewModel?> create(
key: String,
modelClass: Class<T>,
handle: SavedStateHandle): T {
return MyViewModel(repository, handle) as T
}
}
My View model:
class MyViewModel constructor(val repository: AppRepository, private val savedStateHandle: SavedStateHandle): ViewModel() {
fun getMyParameter(): LiveData<Int?> {
return savedStateHandle.getLiveData(MY_FIELD)
}
fun setMyParameter(val: Int) {
savedStateHandle.set(MY_FIELD, val)
}
}
My Fragment:
class MyFragment : androidx.fragment.app.Fragment() {
override fun onCreate(savedInstanceState: Bundle?) {
arguments?.let {
var myField = it.getInt(MY_FIELD)
activitiesViewModel.setMySavedValue(myField ?: 0)
}
}
}
When the app is being used, the live data for the saved state field updates correctly. But as soon as I put the app in background (and I set "don't keep activities" in developer options), and then re open app, the live data shows a value as if it was never set. In other words, the saved state handle seems to forget the field I am trying to save.
Any thoughts?
I had the same problem. It was solved by the fact that I stopped requesting saved values from SavedStateHandle in the onRestoreInstanceState method. This call is correctly used in the onCreate method.
I want to refactor the following code so that I can reuse it with different ViewHolder types:
class TestsAdapter(
private val clickListener: ClickListener
) : PagedListAdapter<RecyclerViewTestInfo, TestsViewHolder>(diffCallback) {
override fun getItemCount(): Int {
return super.getItemCount()
}
override fun onBindViewHolder(holder: TestsViewHolder, position: Int) {
val testInfo = getItem(position) as RecyclerViewTestInfo
with(holder) {
bindTo(testInfo)
testInfo.let {
itemView.setOnClickListener {
clickListener(testInfo)
}
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TestsViewHolder =
TestsViewHolder(parent)
}
It isn't clear to me though how you handle the creation of an instance for a generic type. In the code, onCreateViewHolder is initialized with a specific ViewHolder. How would I do this with a generic?
The problem you are facing here is that you can't directly initialize an instance from a generic type, you need to have at least a Class object. There is a solution, however I wouldn't recommend to use it, since it adds an undesirable layer of complexity.
Pass class type to constructor:
class Test<A : DiffUtil.Callback, B : RecyclerView.ViewHolder?>(type: Class<B>): PagedListAdapter<A, B>(diff)
There using reflection you will be able to create a new instance, however you need to know exactly the constructor, for example in your case you have a constructor with a single parameter of ViewGroup type:
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): B =
type.getConstructor(ViewGroup::class.java).newInstance(parent)
This solution is undesirable, since there are no compile-time checks and when someone will create a new ViewHolder with a different constructor he will get a runtime error.