I'm trying to view my files using recycler view and MVVM, the problem is that memoryViewModel
doesn't reflect the changes on memoryItemsRecyclerViewAdapter
memoryViewModel!!.mutableLiveData?.observe(this.viewLifecycleOwner) {
memoryItemsRecyclerViewAdapter.notifyDataSetChanged() //this should show the list items of the recycler view
Log.d("view model result", it.size.toString()) // log message shows the list items
}
so, the final question is why the mutableLiveData!!.postValue(mutableList)
don't update the recycler view?
here is a sample of my code
the fragment
class MemoryFragment: Fragment() {
companion object{
private var rootDirectory: java.io.File? = null
var currentFolder = rootDirectory
private var filesList: MutableList<File?>? = mutableListOf()
private var memoryViewModel: MemoryViewModel? = null
}
private var sharedPreferences: SharedPreferences? = null
private lateinit var listView: RecyclerView
private lateinit var pathsRecyclerView: RecyclerView
private lateinit var searchView: androidx.appcompat.widget.SearchView
private lateinit var refreshSwipe: SwipeRefreshLayout
private lateinit var memoryItemsRecyclerViewAdapter: MemoryItemsRecyclerViewAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
sharedPreferences = androidx.preference.PreferenceManager
.getDefaultSharedPreferences(requireContext())
val memoryViewModelProvider = MemoryViewModelProvider()
memoryViewModel = ViewModelProvider(this, memoryViewModelProvider).get(MemoryViewModel::class.java)
// rootDirectory = Environment.getExternalStorageDirectory()
// currentFolder = rootDirectory
// val rootFoldersList = rootDirectory?.listFiles()?.toMutableList()
// for (item in rootFoldersList!!)
// if(item.isDirectory)
// filesList?.add(File(R.drawable.ic_folders, item, item.totalSpace))
// else
// filesList?.add(File(R.drawable.ic_file, item, item.totalSpace))
// memoryViewModel = MemoryViewModel(filesList!!)
// MemoryViewModel.lazyMemoryViewModel
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view = LayoutInflater.from(requireContext()).inflate(R.layout.fragment_memory, container, false)
listView = view.findViewById(R.id.list_view)
pathsRecyclerView = view.findViewById(R.id.recycler_view)
searchView = view.findViewById(R.id.search_view)
refreshSwipe = view.findViewById(R.id.swipe_refresh)
listView.layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
memoryItemsRecyclerViewAdapter = MemoryItemsRecyclerViewAdapter(memoryViewModel!!, requireContext(), filesList!!)
listView.adapter = memoryItemsRecyclerViewAdapter
memoryViewModel!!.mutableLiveData?.observe(this.viewLifecycleOwner) {
memoryItemsRecyclerViewAdapter.notifyDataSetChanged()
Log.d("view model result", it.size.toString())
}
return view
}
}
the viewModel
class MemoryViewModel(private var mutableList: MutableList<com.example.everyentertainment.models.File?>?): ViewModel() {
private val foldersPath: MutableList<String?>? = null
private val foldersName: MutableList<String?>? = null
private var rootDirectory: File? = null
var mutableLiveData: MutableLiveData<MutableList<com.example.everyentertainment.models.File?>>? = null
companion object{
var currentFolder: File? = null
}
init {
val job = viewModelScope.launch(Dispatchers.IO) {
if (mutableLiveData == null) mutableLiveData = MutableLiveData()
if(mutableList == null) mutableList = mutableListOf()
initializeMemoryFragment()
}
job.start()
job.invokeOnCompletion {
mutableLiveData!!.postValue(mutableList)//postValue() doesn't update UI also I tried mutableLiveData.value = mutableList but it throws OnCompletionHandlerException
Log.d("mutable list size", mutableList!!.size.toString())
Log.d("mutable live data size", mutableLiveData!!.value.toString())
}
}
fun initializeMemoryFragment(){
rootDirectory = getExternalStorageDirectory()
currentFolder = rootDirectory
val filesList = rootDirectory!!.listFiles()
for(position in filesList!!.indices)
if(filesList[position].isDirectory)
mutableList!!.add(com.example.everyentertainment.models.File(R.drawable.ic_folders,
filesList[position], getFolderSize(filesList[position])))
else
mutableList!!.add(com.example.everyentertainment.models.File(R.drawable.ic_file,
filesList[position], getFolderSize(filesList[position])))
}
fun getFolderSize(file: File): Long{
if(!file.exists())
return 0
if(!file.isDirectory)
return file.length()
val dirs: MutableList<File> = LinkedList()
dirs.add(file)
var result: Long = 0
while (!dirs.isEmpty()) {
val dir = dirs.removeAt(0)
if (!dir.exists()) continue
val listFiles = dir.listFiles()
if (listFiles == null || listFiles.isEmpty()) continue
for (child in listFiles) {
result = child.length()
if (child.isDirectory) dirs.add(child)
}
}
return result
}
and finally the recycler view adapter
class MemoryItemsRecyclerViewAdapter(private val memoryViewModel: MemoryViewModel, private val context: Context, private val dataSet: MutableList<File?>?):
RecyclerView.Adapter<MemoryItemsRecyclerViewAdapter.ViewHolder>() {
/**
* Provide a reference to the type of views that you are using
* (custom ViewHolder).
*/
class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
val fileNameTextView: TextView = view.findViewById(R.id.name_text_view)
val sizeTextView: TextView = view.findViewById(R.id.size_text_view)
val numberOfFilesTextView: TextView = view.findViewById(R.id.number_of_files_text_view)
val dateTextView: TextView = view.findViewById(R.id.date_text_view)
val imageView: ImageView = view.findViewById(R.id.image_view)
init {
// Define click listener for the ViewHolder's View.
}
}
// Create new views (invoked by the layout manager)
override fun onCreateViewHolder(viewGroup: ViewGroup, viewType: Int): ViewHolder {
// Create a new view, which defines the UI of the list item
val view = LayoutInflater.from(viewGroup.context).inflate(R.layout.memory_list_view_item, viewGroup, false)
return ViewHolder(view)
}
// Replace the contents of a view (invoked by the layout manager)
override fun onBindViewHolder(viewHolder: ViewHolder, position: Int) {
// Get element from your dataset at this position and replace the
// contents of the view with that element
val file = dataSet!![position]
viewHolder.fileNameTextView.text = file!!.file.name
viewHolder.sizeTextView.text = memoryViewModel.readableFileSize(dataSet[position]!!.size)
viewHolder.dateTextView.text = memoryViewModel.getFolderDateModified(file.file)
viewHolder.numberOfFilesTextView.text = memoryViewModel.getSubFoldersQuantity(context, file.file)
if(file.file.isDirectory)
viewHolder.imageView.setImageResource(R.drawable.ic_folders)
else
viewHolder.imageView.setImageResource(R.drawable.ic_file)
}
// Return the size of your dataset (invoked by the layout manager)
override fun getItemCount() = dataSet!!.size
}
CodePudding user response:
Your adapter is hardcoded to display the contents of dataSet
(you fetch from it in onBindViewHolder
, you reference it in getItemCount
) and you never change those contents, so there's never anything to update. The RecyclerView
will only ever display what you first passed in. You need to make the adapter's data updateable, I'd recommend something like this:
class MemoryItemsRecyclerViewAdapter(
private val memoryViewModel: MemoryViewModel,
private val context: Context,
private var dataSet: List<File?> = emptyList() // this is a var now, and not a mutable list
) : RecyclerView.Adapter<MemoryItemsRecyclerViewAdapter.ViewHolder>() {
// this function replaces the current data with the new set, and refreshes the UI
fun setData(newData: List<File?>) {
dataSet = newData
notifyDataSetChanged()
}
...
}
Now you have a way to update the adapter, and it takes care of updating the UI itself - it's better to have the logic (like calling notifyDataSetChanged()
) in here, because really the adapter should be deciding what's changed and how to handle it.
I made dataSet
a var
so you can just swap the old list for the new one, and made them immutable List
s since they don't need to be mutable. I added emptyList()
as a default - it doesn't need to be nullable, and you're treating it as non-null (with !!
everywhere) anyway, so just make it non-null. If you really want, you can make it a mutable list and do it that way - in that case, use mutableListOf()
for the empty default instead
So now you can update the adapter, you just need to do that in your observer function:
memoryViewModel!!.mutableLiveData?.observe(this.viewLifecycleOwner) { newData ->
// when a new list comes in, set it on the adapter
memoryItemsRecyclerViewAdapter.setData(newData)
Log.d("view model result", newData.size.toString())
}
and that's it. Your observer just reacts to new values as they come in (which includes any current value, when you first observe
the LiveData
) so you just need to do whatever with them
(Also, you're really complicating things by making everything nullable, especially when you're assuming they're all non-null when you access them anyway with !!
)