Home > Net >  How to use Adapter.notifyDataSetChanged() in MainActivity?
How to use Adapter.notifyDataSetChanged() in MainActivity?

Time:06-27

I have created a list with a RecyclerView, and would like to be able to update this now in the MainActvitiy, the context comes from a fragment which comes from the MainActivity, but I just can't use

Adapter.notifyDataSetChanged()

In the fragment it works, and it updates the list, how do I get the context from the fragment b.z how can I use Adapter.notifyDataSetChanged() also in the MainActvitiy?

Adapter.kt

class Adapter(val context: Context) : RecyclerView.Adapter<Adapter.ViewHolder>() {

    var dataList = emptyList<VolvicsModel>()

    internal fun setDataList(dataList: List<VolvicsModel>) {
        this.dataList = dataList
        notifyDataSetChanged()
    }

    class ViewHolder(itemView : View) : RecyclerView.ViewHolder(itemView) {
        var title : TextView
        var name : TextView
        var image : ImageView
        var liters: TextView
        var quantity: TextView

        init {
            title = itemView.findViewById(R.id.textView9)
            image = itemView.findViewById(R.id.imageView)
            liters = itemView.findViewById(R.id.liters)
            quantity = itemView.findViewById(R.id.quantity)
            name = itemView.findViewById(R.id.name)

            itemView.setOnLongClickListener {
                Toast.makeText(itemView.context, "${title.text}", Toast.LENGTH_SHORT).show()
                return@setOnLongClickListener true
            }
        }
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        val view = LayoutInflater.from(parent.context).inflate(R.layout.card_user_volvics, parent, false)
        return ViewHolder(view)
    }
    override fun getItemCount() = dataList.size

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        val data = dataList[position]

        holder.title.text = data.name
        holder.liters.text = data.liters
        holder.quantity.text = data.quantity
        holder.name.text = data.sort
        Glide.with(context).load(data.image).into(holder.image)
        
    }

}

dataList.kt

var dataList = mutableListOf<VolvicsModel>()

Fragment.kt

override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
    val view = inflater.inflate(R.layout.fragment_your_volvics, container, false)

    recycleView = view.findViewById(R.id.recyclerview_your_volvics)
    recycleView.layoutManager = GridLayoutManager(view.context, 2)
    recycleView.setHasFixedSize(true)

    VolvicsAdapter = Adapter(view.context)
    recycleView.adapter = VolvicsAdapter

    progressBar = view.findViewById(R.id.progressBar)
    LinearLayout = view.findViewById(R.id.linearLayout)

    database = FirebaseDatabase.getInstance().getReference("Userdata").child(uid!!)
    database.addListenerForSingleValueEvent(object : ValueEventListener {
        override fun onDataChange(snapshot: DataSnapshot) {
            if(snapshot.child("volvics").exists()) {
                dataRequest()
                Adapter.notifyDataSetChanged()
                database.removeEventListener(this)
            }
        }
        override fun onCancelled(error: DatabaseError) {
            println("Error")
        }
    })

    return view
}

CodePudding user response:

In general, globally accessible mutable state (like your dataList) is a dangerous thing to use. It introduces challenges like you observed where it can be changed from somewhere else in the app and other components have no idea that it changed. Rather than trying to add more dependencies between the Activity and Fragment, it is better to try to fix the original problem.

A much better choice would be to store that data in an Activity ViewModel that the Activity and Fragment can both access to trigger changes, and the Fragment can observe and respond to. When the fragment is notified of a change to the data, it can update the data in its adapter.

Here is an example where an Activity ViewModel holds a list of data (strings) and both the Activity and Fragment can add things to that list. The Fragment observes the list and updates the adapter when the data changes. This uses a ListAdapter, so calling notifyDataSetChanged is not necessary.

View Model

The ViewModel holds the array you want to display. It is private data, and the things that need to display that data can observe the LiveData. Anything that wants to change that data should go through here.

class MainViewModel : ViewModel() {
    private val dataListLiveData = MutableLiveData<List<String>>()
    val dataList: LiveData<List<String>>
        get() = dataListLiveData

    // This is the source of truth - anything that changes it should go through
    // the ViewModel so that its observers can be notified
    private val myData = mutableListOf<String>()

    // Anything that triggers a change to the data, either by calling an API
    // like firebase or locally editing it, should call through the ViewModel
    fun addEntry(e: String) {
        myData.add(e)
        dataListLiveData.postValue(myData)
    }

    fun getDataFromFirebase() {
        // asynchronous calls to Firebase could be triggered here, and when
        // complete they can add data to the list and post the updated list
        // to the LiveData
    }
}

Activity

The Activity here gets the ViewModel and adds some data to it with a delay in a coroutine. The ViewModel instance here will be the same one that the Fragment gets.

class MainActivity : AppCompatActivity() {

    private val model: MainViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        if (savedInstanceState == null) {
            supportFragmentManager
                .beginTransaction()
                .setReorderingAllowed(true)
                .add(R.id.fragment, ListFragment())
                .commit()
        }

        // slowly add data to the list, one entry every 2 seconds
        lifecycleScope.launch {
            for(i in 1..5) {
                delay(2000)
                model.addEntry("Data $i from the activity")
            }
        }
    }
}

Fragment

The Fragment retrieves the Activity ViewModel and observes the LiveData. When the data changes, the Fragment updates the adapter/views. The Fragment can also update the data by calling methods on the ViewModel.

class ListFragment : Fragment() {

    private var _binding: FragmentListBinding? = null
    private val binding get() = _binding!!

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        _binding = FragmentListBinding.inflate(inflater, container, false)
        return binding.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        val model: MainViewModel by activityViewModels()
        val adapter = MyListAdapter()
        binding.dataList.adapter = adapter
        binding.dataList.layoutManager = LinearLayoutManager(requireContext())

        // The fragment observes the list LiveData in the
        // ViewModel and updates the adapter when new data
        // is posted
        model.dataList.observe(viewLifecycleOwner) { newList ->
            // put it in the adapter. Using ListAdapter means
            // we don't need to call notifyDataSetChanged, it figures
            // that out automatically
            adapter.submitList(newList.toList())
        }

        lifecycleScope.launch {
            for(i in 1..3) {
                delay(1200)
                model.addEntry("Data $i from the fragment")
            }
        }

    }

    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null
    }
}

Adapter

The adapter here inherits from ListAdapter, so you can use submitList when there is new data and do not need to manually call notifyDataSetChanged.

class MyListAdapter : ListAdapter<String, MyListAdapter.ViewHolder>(DIFF_CALLBACK) {

    class ViewHolder(val binding: ListRowBinding) : RecyclerView.ViewHolder(binding.root)

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        val binding = ListRowBinding.inflate(
            LayoutInflater.from(parent.context), parent, false
        )
        return ViewHolder(binding)
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        holder.binding.item.text = getItem(position)
    }

    companion object {
        val DIFF_CALLBACK = object : DiffUtil.ItemCallback<String>() {
            override fun areItemsTheSame(oldItem: String, newItem: String): Boolean {
                return oldItem == newItem
            }

            override fun areContentsTheSame(oldItem: String, newItem: String): Boolean {
                return oldItem == newItem
            }
        }
    }
}

Gradle

Gradle file dependencies are pretty standard, but if you are missing the -ktx ones some of the methods above won't be present. You'll also need to activate View Binding to use this example.

android {
    ...
    buildFeatures {
        viewBinding true
    }
}

dependencies {
    implementation 'androidx.core:core-ktx:1.8.0'
    implementation 'androidx.appcompat:appcompat:1.4.2'
    implementation 'com.google.android.material:material:1.6.1'
    implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
    implementation 'androidx.activity:activity-ktx:1.4.0'
    implementation 'androidx.fragment:fragment-ktx:1.4.1'
}
  • Related