Android how to handle recyclerview clicks in mvvm architecture - android

I want to handle clicks in my recyclerview rows and currently pass my presenter to my viewholder bindings.
Presenter interface:
interface IMainPresenter{
public void showDetail(Pojo pojo);
}
Viewholder:
class ViewHolder(itemView: View, val parent: ViewGroup?, private val binding: ListMainBinding) : RecyclerView.ViewHolder(itemView) {
fun bind(pojo: Pojo, mainPresenter: IMainPresenter) {
binding.pojo = pojo
binding.mainPresenter = mainPresenter
binding.executePendingBindings()
}
}
and in my layout I call a method of my presenter on the onClick attribute
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<import type="android.view.View" />
<variable
name="mainPresenter"
type="com.noisyninja.androidlistpoc.views.main.IMainPresenter" />
<variable
name="pojo"
type="com.noisyninja.androidlistpoc.model.Pojo" />
</data>
<android.support.v7.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#color/black"
android:onClick="#{(view) -> mainPresenter.showDetail(pojo)}"
...
...
...
I think passing the presenter is not a good design, but what is a better approach to handle clicks from rows which trigger a method in my presenter?

IMHO a better approach would be to add OnItemClickListener to RecyclerView like here, and call
ItemClickSupport.addTo(recyclerView).setOnItemClickListener{
recycler, position, v ->
mainPresenter.showDetail(recycler.getAdapter().getItem(position))
}
during the setup of RecyclerView.

Related

Why does LiveData only effect main layout and doesn't LiveData effect child layout UI in Android Studio?

FragmentHome load layout_home.xml, and layout_home.xml displays a recyclerview and a button named btnMain
recyclerview include the item layout layout_voice_item.xml, it displays a button named btnChild。
I use displayCheckBox : LiveData<Boolean> to control whether both btnMain and btnChild are shown or not with the code android:visibility="#{!aHomeViewModel.displayCheckBox? View.VISIBLE: View.GONE}".
I find btnMain can be shown or not when I change the value of displayCheckBox, but btnChild keep to show, why?
FragmentHome.kt
class FragmentHome : Fragment() {
private lateinit var binding: LayoutHomeBinding
private val mHomeViewModel by lazy {
getViewModel {
HomeViewModel(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.aHomeViewModel=mHomeViewModel
val adapter = VoiceAdapters(mHomeViewModel)
binding.mvoiceList.adapter=adapter
mHomeViewModel.listVoiceBySort.observe(viewLifecycleOwner){
adapter.submitList(it)
}
...
return binding.root
}
}
HomeViewModel.kt
class HomeViewModel(private val mDBVoiceRepository: DBVoiceRepository) : ViewModel() {
private val _displayCheckBox = MutableLiveData<Boolean>(true)
val displayCheckBox : LiveData<Boolean> = _displayCheckBox
fun setCheckBox(isDisplay:Boolean){
_displayCheckBox.value = isDisplay
}
...
}
VoiceAdapters.kt
class VoiceAdapters (private val aHomeViewModel: HomeViewModel):
ListAdapter<MVoice, VoiceAdapters.VoiceViewHolder>(MVoiceDiffCallback()) {
...
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VoiceViewHolder {
return VoiceViewHolder(
LayoutVoiceItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
)
}
override fun onBindViewHolder(holder: VoiceViewHolder, position: Int) {
val aMVoice = getItem(position)
holder.bind(aHomeViewModel, aMVoice)
}
inner class VoiceViewHolder (private val binding: LayoutVoiceItemBinding):
RecyclerView.ViewHolder(binding.root) {
fun bind(mHomeViewModel: HomeViewModel, aMVoice: MVoice) {
binding.aHomeViewModel = mHomeViewModel
binding.executePendingBindings()
}
}
...
}
class MVoiceDiffCallback : DiffUtil.ItemCallback<MVoice>() {
...
}
layout_home.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>
<import type="android.view.View" />
<variable name="aHomeViewModel"
type="info.dodata.voicerecorder.viewcontrol.HomeViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/mvoice_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:listitem="#layout/layout_voice_item"
/>
<Button
android:id="#+id/btnMain"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="#{!aHomeViewModel.displayCheckBox? View.VISIBLE: View.GONE}"
/>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
layout_voice_item.xml
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<import type="android.view.View" />
<variable name="aHomeViewModel"
type="info.dodata.voicerecorder.viewcontrol.HomeViewModel" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<Button
android:id="#+id/btnChild"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:visibility="#{!aHomeViewModel.displayCheckBox? View.VISIBLE: View.GONE}"
/>
</LinearLayout>
</layout>
Giving a LifecycleOwner to LayoutVoiceItemBinding might helps.
voiceViewHolder.binding.lifecycleOwner = yourLifecycleOwner
The view holder pattern was not always part of android and before its wide adoption, a naive list implementation would have resulted in an inflated view rendered for each value in the submitted list. That is extremely memory inefficient.
With the adoption of a view holder pattern, views (the visual items) are only inflated up to the maximum that can be visible on the screen (and one or two extra for smooth scrolling)
The adapter however needs to be made aware of changes in the list's data.
https://developer.android.com/guide/topics/ui/layout/recyclerview#Adapter
If the list needs an update, call a notification method on the
RecyclerView.Adapter object, such as notifyItemChanged(). The layout
manager then rebinds any affected view holders, allowing their data to
be updated.
Observing the displayCheckBox : LiveData<Boolean> and calling adapter.notifyItemChanged might be the issue at hand.
I leave with another reference reiterating that view binding will not observe the live data but reduces the boilerplate code.
https://medium.com/androiddevelopers/android-data-binding-recyclerview-db7c40d9f0e4
What’s Left?
All the boilerplate from the RecyclerView is now handled
and all you have left to do is the hard part: loading data off the UI
thread, notifying the adapter when there is a data change, etc.
Android Data Binding only reduces the boring part.

Why the recycler view test is not passed?

I've written the test, testing if the recycler view is displayed (id: comments_view), but it always fails and I've no idea why. When I'm checking for layout (id: cm), the test passes.
I have the following fragment code:
<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="post"
type="com.example.kotlinpostapi.apiObjects.Post" />
<variable
name="comments"
type="java.util.List" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".views.MainActivity"
android:id="#+id/cm">
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/comments_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
The test code (I'm navigating to the fragment from another one):
#RunWith(AndroidJUnit4::class)
class CommentsListTest{
#get: Rule
val activityScenario = ActivityScenarioRule(MainActivity::class.java)
#Test
fun testCommentsAreDisplayed() {
onView(withId(R.id.posts_view)).perform(actionOnItemAtPosition<PostAdapter.PostsViewHolder>(0, MyMatchers.clickChildView(R.id.show_comments_button)))
//it fails
onView(withId(R.id.comments_view)).check(matches(isDisplayed()))
//it passes
onView(withId(R.id.cm)).check(matches(isDisplayed()))
}
}
How is it possible, and how can I test my recycler view?
The height of the RecyclerView is set to wrap_content and if the element is not visible at least 90% the test fails.
What you could do is to check one of the RecyclerView children.
I firstly declare the following method:
fun nthChildOf(parentMatcher: Matcher<View?>, childPosition: Int): Matcher<View?>? {
return object : TypeSafeMatcher<View>() {
override fun describeTo(description: Description) {
description.appendText("with $childPosition child view of type parentMatcher")
}
override fun matchesSafely(view: View): Boolean {
if (view.parent !is ViewGroup) {
return parentMatcher.matches(view.parent)
}
val group = view.parent as ViewGroup
return parentMatcher.matches(view.parent) && group.getChildAt(childPosition) == view
}
}
}
with this you can check whether its first child is displayed:
onView(nthChildOf(withId(R.id.comments_view), 0)).check(matches(isDisplayed()))
And to check one element of its children (recyclerview_element_id for example):
onView(allOf(
withId(R.id.recyclerview_element_id),
isDescendantOfA(
nthChildOf(withId(R.id.comments_view), 0))
)).check(matches(isDisplayed()))
Another thing you could try if your RecyclerView expands to the available space of the screen is to change the layout of the RecyclerView to have all the constraints set and with and height to 0dp:
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/comments_view"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
  app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
I have it this way and doing:
onView(withId(R.id.myRecyclerviewId)).check(matches(isDisplayed()))
works for me.

Data Binding onClick not working

I am getting started for using DataBinding and something is wrong with my onClick.
GameViewModel.java
public void onClickItem(int row, int col){
Log.d("click","row: "+row+" col: "+col);
}
#BindingAdapter("load_image")
public static void loadImage(ImageView view,int imageId) {
view.setImageResource(getDrawable(imageId));
}
GameFragment.java
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
//View view=inflater.inflate(R.layout.fragment_game, container, false);
FragmentGameBinding binding = DataBindingUtil.inflate(inflater, R.layout.fragment_game, container, false);
View view = binding.getRoot();
ButterKnife.bind(this,view);
binding.setGameViewModel(gameViewModel);
gameViewModel= ViewModelProviders.of(getActivity()).get(GameViewModel.class);
gameViewModel.init();
return view;
}
fragment_game.xml
<?xml version="1.0" encoding="utf-8"?>
<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"
tools:context=".view.GameFragment">
<data>
<import type="android.support.v4.app.Fragment"/>
<import type="android.view.View"/>
<variable
name="gameViewModel"
type="harkor.addus.viewmodel.GameViewModel" />
</data>
<android.support.constraint.ConstraintLayout
(...)>
<TextView
(...)>
<android.support.constraint.ConstraintLayout
(...)>
<ImageView
android:id="#+id/image_puzzle11"
android:src="#android:color/holo_green_dark"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginEnd="1dp"
android:layout_marginRight="1dp"
android:onClick="#{() -> gameViewModel.onClickItem(1,1)}"
app:load_image="#{0}"
app:layout_constraintBottom_toTopOf="#+id/image_puzzle21"
app:layout_constraintDimensionRatio="w,1:1"
app:layout_constraintEnd_toStartOf="#+id/image_puzzle12"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
(...)
load_image is working, but onClick do nothing...
No error in compilation, no crash when button is clicking on device, no result in console...
Please check with below code:
You have written as to call on Click of image as :
android:onClick="#{() -> gameViewModel.onClickItem(1,1)}"
Try to write as below and check again :
android:onClick="#{(v) -> gameViewModel.onClickItem(1,1)}"
As per the Guidance This is not the way to achieve the Architecture Principles we can work as below as per the MVVM Architecture:
1. Create an Interface
2. Define Interface as handler inside the Layout File as :
<variable
name="handler"
type="com.cityguide.interfaces.MustVisitItemListener"></variable>
3.Now we are using this handler to define onclick as :
android:onClick="#{(v) ->handler.onGalleryItemClick(v,currentPosition,photo)}"
Implement the Handler with our java Class or Activity class before bind the Handler with View as below:
private MustVisitItemListener mItemListener;
mItemListener = new MustVisitItemListener() { };
5.Set the Interface handler with bind object as below:
mbinding.setHandler(mItemListener);
The easiest way is to set the view model and calling the proper method in the View's onClick from the layout:
Your xml:
<data>
<variable
name="viewModel"
type="co.package.MyViewModel" />
</data>
<com.google.android.material.textfield.TextInputEditText
android:id="#+id/my_id"
android:layout_width="#dimen/full_width"
android:layout_height="wrap_content"
android:onClick="#{() -> viewModel.doSomething()}" />
But if for any reason you need to call a method from your fragment or activity, the best suggestion is to create an interface to handle the method, implement the method and set it to the layout as follows:
Your xml
<data>
<variable
name="myHandlers"
type="co.package.MyHandlersListener" />
</data>
<com.google.android.material.textfield.TextInputEditText
android:id="#+id/my_id"
android:layout_width="#dimen/full_width"
android:layout_height="wrap_content"
android:onClick="#{() -> myHandlers.doSomething()}" />
And within your Fragment or Activity you create the interface and then implement it:
Your activity/fragment:
/* My Handler Methods */
interface MyHandlersListener {
fun doSomething()
}
Then implement the listener, taking into consideration that the method something is defined and implemented within your activity/fragment class:
private val myHandlersListener: MyHandlersListener = object : MyHandlersListener {
override fun doSomething() {
something()
}
}
And using databinding, you can set the handler into your layout class (this can be done within the onCreate or onCreateView method depending if you are using and activity or fragment respectively):
myBinding.myHandlers = myHandlersListener
In this way it works perfectly and you follow the guide given by Android's team:
https://developer.android.com/topic/libraries/data-binding/expressions#method_references

MVVM BindingAdapters not showing ProgressBar

I'm trying to create a simple example with databinding and BindingAdapters in order to show/hide a ProgressBar depending on TextView if it's empty or not. Below you can see my code. What I'm doing wrong?
loading_state.xml
<?xml version="1.0" encoding="utf-8"?>
<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="textString"
type="String"/>
</data>
<android.support.constraint.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
app:visibleGone="#{textString==null}">
<ProgressBar
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="#{textString}"/>
</android.support.constraint.ConstraintLayout>
</layout>
My BindingAdapter
object BindingAdapters {
#JvmStatic
#BindingAdapter("visibleGone")
fun showHide(view: View, visible: Boolean){
view.visibility = if (visible) View.VISIBLE else View.GONE
}
}
I include the layout in my second fragment in order to check the textview text
<include layout="#layout/loading_state"
app:textString="#{textView2.text.toString()}"/>
and also in my SecondFramgent class I take the value from MainFragment class (I'm using the new Navigation component)
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
val txtFromMain = SecondFragmentArgs.fromBundle(arguments)
textView2.text = txtFromMain.txtFromMain
}
What am I missing?
Thank you very much.
For those facing the same issue you can find my solutions matches in my case below:
I had to change my BindingAdapter.
#BindingAdapter("visibleGone")
fun showHide(view: View, visible: String){
view.visibility = if (visible.isEmpty()) View.VISIBLE else View.GONE
}
You did not set two-way databinding for TV, thus string is not getting updated inside databinding
<TextView android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="#{textString}"/>
Change android:text="#{textString}" to android:text="#={textString}"
This is first look of the problem, does it help?

Method call (that has been defined in fragment) on button click using android data binding in not working.?

I want to call onButtonclick(View v) using android binding in the layout xml file.
How to achieve button click using android binding in this case?
I followed as below but it didn't work. Any suggestions ? Thanks in Advance.
Layout1.xml
<data>
<variable
name="myFrag"
type="com.myapp.Fragment1" />
</data>
...
<Button
android:id="#+id/step_button"
style="#style/button_style"
android:onClick="#{myFrag :: onButtonclick}"/>
Fragment1.java
public class Fragment1 extends Fragment {
.....
public void onButtonclick(View v)
{
myStdent.setId("No ID");
}
.....
}
Layout1Binding myBinding = DataBindingUtil.inflate(inflater, R.layout.layout1.xml, container, false);
myBinding. setMyFrag(this);
Above lines of code has solved the problem. I have added these lines in onCreateView.
I think you are missing
http://prntscr.com/fm6yih (init variable for databinding object)
In your fragment layout xml, define a fragment variable. Then in your fragment class, map the xml variable to the fragment using binding.setVariable. Now you should be able to use all fragment variables and methods in xml.
In fragment layout xml:
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
tools:context="com.example.MyFragment">
<data>
<variable
name="frag"
type="com.example.MyFragment" />
</data>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="#{() -> frag.yourMethod()}" />
</RelativeLayout>
</layout>
In fragment:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val inflater = context?.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
val binding = DataBindingUtil.inflate<ViewDataBinding>(inflater, R.layout.fragment_my, myFragmentContainer, true)
binding.setVariable(BR.frag, this#MyFragment)
}
}

Categories

Resources