Kotlin: How to pass data from recyclerview in fragment to another fragment using viewmodel/interface?

Viewed 4739

I was not able to find an example of passing data from a recyclerview adapter to a fragment using a viewmodel or interface in kotlin. I have tried to do with using just a bundle, but I receive a null bundle in the receiving fragment. Here is my Recyclerview Adapter code snippet.

class QnAMyProfileEditRecyclerViewAdapter(private val context: Context) : RecyclerView.Adapter<QnAMyProfileEditRecyclerViewAdapter.Holder>() {


    private var currentUserQnADataMyProfileEdit = emptyList<QnAData>()

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Holder {

        val view: View = if (viewType == R.layout.rv_item_qn_a_my_profile) {

            LayoutInflater.from(parent.context)
                .inflate(R.layout.rv_item_qn_a_my_profile, parent, false)

        } else {

            LayoutInflater.from(parent.context)
                .inflate(R.layout.rv_footer_new_qna_my_profile, parent, false)

        }
        return Holder(view)
    }

    override fun getItemCount(): Int {
        return currentUserQnADataMyProfileEdit.size + 1
    }

    //implement Floating Action Button at the bottom of the recyclerview
    override fun getItemViewType(position: Int): Int {

        return if (position == currentUserQnADataMyProfileEdit.size) {
            R.layout.rv_footer_new_qna_my_profile
        } else {
            R.layout.rv_item_qn_a_my_profile
        }
    }

    override fun onBindViewHolder(holder: Holder, position: Int) {

        if (position == currentUserQnADataMyProfileEdit.size) {
            holder.viewHolderNewQnA?.setOnClickListener {
                Toast.makeText(context, "New item intent", Toast.LENGTH_SHORT).show()
            }
        } else {
            //Normal bind for RecyclerView
            holder.bindData(currentUserQnADataMyProfileEdit[position])
            holder.viewHolderCRUDQnAMyProfile?.setOnClickListener {


                //Give QnACrud current QnAData --- send bundle
                val bundle = Bundle()
                bundle.putString(
                    "currentQnAQuestion",
                    currentUserQnADataMyProfileEdit[position].question
                )
                bundle.putString(
                    "currentQnAAnswer",
                    currentUserQnADataMyProfileEdit[position].answer
                )
                QnAMyProfileEditCRUDFragment().arguments = bundle
                val activity: AppCompatActivity = context as AppCompatActivity
                activity.supportFragmentManager.beginTransaction()
                    .add(R.id.cl_my_profile_edit, QnAMyProfileEditCRUDFragment())
                    .addToBackStack(null).commit()




            }
        }
    }


    fun setData(updateList: List<QnAData>) {
        this.currentUserQnADataMyProfileEdit = updateList
        notifyDataSetChanged()
    }

    inner class Holder(itemView: View) : RecyclerView.ViewHolder(itemView) {




        val viewHolderQuestion =
            itemView.findViewById(R.id.txt_question_qna_my_profile_edit) as TextView?
        val viewHolderAnswer =
            itemView.findViewById(R.id.txt_answer_qna_my_profile_edit) as TextView?

        val viewHolderNewQnA =
            itemView.findViewById(R.id.fab_new_qna_my_profile_edit) as FloatingActionButton?

        val viewHolderCRUDQnAMyProfile =
            itemView.findViewById(R.id.fab_crud_qna_my_profile_edit) as FloatingActionButton?


        //Bind data with inflaters
        fun bindData(currentUserInList: QnAData) {

            viewHolderQuestion?.text = currentUserInList.question
            viewHolderAnswer?.text = currentUserInList.answer
        }

    }
}

Here is my fragment which is suppose to receive the data(bundle):

class QnAMyProfileEditCRUDFragment : Fragment() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        //Get bundle data and set Edit Text values


        val bundle:Bundle? = arguments //Bundle == null!!!!
        val qnaQuestionData: String? = bundle?.getString("currentQnAQuestion")
        val qnaAnswerData: String? = bundle?.getString("currentQnAAnswer")
        et_qna_question.setText(qnaQuestionData)
        et_qna_answer.setText(qnaAnswerData)


    }

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {



        return inflater.inflate(R.layout.fragment_qn_a_my_profile_edit_c_r_u_d, container, false)
    }

    override fun onAttach(context: Context) {
        super.onAttach(context)


    }

    override fun onActivityCreated(savedInstanceState: Bundle?) {
        super.onActivityCreated(savedInstanceState)

        //Pop fragment if grey area(outside the box) is tapped
        imv_overlay.setOnClickListener {
            fragmentManager?.popBackStack()
        }
    }

}

QnAData is a simple data class with two member question and answer both are string type

3 Answers

Try creating a constructor inside your fragment, then beginTransaction

val fragment = QnAMyProfileEditCRUDFragment.newInstance( 
currentUserQnADataMyProfileEdit[position].question, 
currentUserQnADataMyProfileEdit[position].answer)

activity.supportFragmentManager.beginTransaction()
                    .add(R.id.cl_my_profile_edit,fragment)
                    .addToBackStack(null).commit()

Inside the QnAMyProfileEditCRUDFragment you can just use the values from the constructor

First, there are some refactors to be made on your code:

  1. inner class can lead to memory leak so change your ViewHolder to:
  2. Bring your click listener inside your ViewHolder

ViewHolder

class MyViewHolder(...) : RecyclerView.ViewHolder(...) {

    fun bind(model: Model) {
        ...

        itemView.setOnClickListener {
            callback?.onSomeEventHappened(model)
            //you can also pass lambda here instead of using an interface for callback
        }

    }

}
  1. RecyclerViewAdapter needs to define a protocol to communicate with a fragment:
class Adapter : RecyclerView.Adapter<T>() {

    interface callback {
        fun onSomeEventHappened()
    }


    private var callback: callback? = null

    
    fun setListener(listener: Callback) {
        callback = listener
    }

}
  1. If data is shared between two fragments use SharedViewModel
  2. If you need to pass data you can do it either AndroidNavigationCompoent arguments or passing them when creating the fragment:
val SomeFragment = SomeFragment().apply {
    SomeFragment.arguments = ... //A bundle for example
}

Assuming you have viewModel shared by for your Fragment container activity & child fragments, you can pass that same viewModel to your recyclerview adapter.

class Activity: AppCompatActivity {
   private val viewModel: AnyViewModel by lazy { 
        //Initialization 
   }
   private val adapter = Adapter(viewModel)

   ...
}

class Adapter(viewModel: SomeViewModel) : RecyclerView.Adapter<T>() {

   ...

   override fun onBindViewHolder(holder: ViewHolder, position: Int) {
       holder.onBind(anyList[position])
   }

   inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
      ...


      fun onBind(data: SomeData) {
         btnSomeAction.setOnClickListener {
            //Here change respected data in viewModel or call some method
            //viewModel.anyObject = some_value
            //viewModel.anyMethodCall() 
         }
      }
   }
}

Now you can access viewModel in you adapter and use it update data from Activity or Fragment.

Related