I wanted to add a SearchView to my recyclerview. I wanted it to be at the top and scrollable with the items. To achieve this, I created separate adapter for my header and it contains the Searchview as well. Then I used a ConcatAdapter to combine this header adapter with the contents below it.
Initially I want all the items to be visible under the SearchView from _onBoardingState which is a MutableStateFlow and when user searches for a tag then the results for it get added to _onSearch which is also a MutableStateFlow.
I have this MutableStateFlow, _onBoardingState inside my ViewModel that gets the value from Firestore in the init of ViewModel. The number of results is less (~ 20) so there is no pagination implemented and all items get loaded at once.
Now, whenever user wants to search an item by a tag, the SearchView returns a Flow of the typed value and also a Flow that updates about if the SearchView is still open or closed. I used these extension functions for this:
fun SearchView.getQueryTextChangeStateFlow(onSubmit: ()-> Unit): StateFlow<String> {
val query = MutableStateFlow("")
setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String?): Boolean {
onSubmit()
return true
}
override fun onQueryTextChange(newText: String): Boolean {
query.value = newText
return true
}
})
return query
}
fun SearchView.getActiveStateFlow(): StateFlow<Boolean> {
val isOpen = MutableStateFlow(false)
setOnSearchClickListener {
isOpen.value = true
}
setOnCloseListener {
isOpen.value = false
false
}
return isOpen
}
Inside my ViewModel I have
...
private val _onBoardingState: MutableStateFlow<Model?> = MutableStateFlow(null)
private val _onSearch: MutableStateFlow<Model?> = MutableStateFlow(null)
private val _isActive: MutableStateFlow<Boolean> = MutableStateFlow(false)
fun toggleSearchViewState(isActive: Boolean) {
_isActive.value = isActive
}
val cuurentFlow: Flow<Model?> =
_isActive.flatMapLatest { isActive ->
if (isActive) {
_onSearch
} else {
_onBoardingState
}
}
...
Now the issue here is, whenever the recyclerview is scrolled down, the SearchView gets recycled and hence the setOnCloseListener gets called for it. This causes the _isActive value to be set to false by the Header's Adapter so the value of cuurentFlow gets toggled which should not be happening.
I thought of a solution as to set the setOnCloseListener of SearchView inside the header adapter's onViewRecycled() to null, but this didn't help. Below is code for my Header Adapter as well if needed.
class OnBoardingHeaderAdapter(
private val context: Context,
) : RecyclerView.Adapter<OnBoardingHeaderAdapter.HeaderViewHolder>() {
private var queryTextListener: ((StateFlow<String>) -> Unit)? = null
private var searchViewListener: ((StateFlow<Boolean>) -> Unit)? = null
inner class HeaderViewHolder(binding: OnboardingHeaderItemBinding) :
RecyclerView.ViewHolder(binding.root) {
private val root = binding.headerRoot
val search = binding.search
fun bind(headerMetaData: HeaderMetaData) {
root.visibility =
if (headerMetaData.shouldShow)
View.VISIBLE
else
View.GONE
val searchEditText: EditText =
search.findViewById(androidx.appcompat.R.id.search_src_text)
searchEditText.setHintTextColor(context.resources.getColor(R.color.white))
searchEditText.setTextColor(context.resources.getColor(R.color.white))
}
}
fun setQueryTextListener(listener: (StateFlow<String>) -> Unit) {
this.queryTextListener = listener
}
fun setSearchViewListener(listener: (StateFlow<Boolean>) -> Unit) {
this.searchViewListener = listener
}
private val RECYCLER_COMPARATOR = object : DiffUtil.ItemCallback<HeaderMetaData>() {
override fun areItemsTheSame(oldItem: HeaderMetaData, newItem: HeaderMetaData) =
oldItem.id == newItem.id
override fun areContentsTheSame(oldItem: HeaderMetaData, newItem: HeaderMetaData) =
oldItem == newItem
}
val headerDiffer = AsyncListDiffer(this, RECYCLER_COMPARATOR)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): HeaderViewHolder {
val binding = OnboardingHeaderItemBinding.inflate(
LayoutInflater.from(parent.context), parent, false
)
return HeaderViewHolder(binding)
}
override fun onBindViewHolder(holder: HeaderViewHolder, position: Int) {
//holder.setIsRecyclable(false)
if (position < 1) {
val header = headerDiffer.currentList[position]
holder.bind(header)
}
queryTextListener?.let {
it(holder.search.getQueryTextChangeStateFlow() {
val imm = context.getSystemService(Activity.INPUT_METHOD_SERVICE) as InputMethodManager
val view: View = holder.search
imm.hideSoftInputFromWindow(view.windowToken,0)
})
}
searchViewListener?.let {
it(holder.search.getActiveStateFlow())
}
}
override fun getItemCount(): Int = headerDiffer.currentList.size
override fun onViewRecycled(holder: HeaderViewHolder) {
super.onViewRecycled(holder)
holder.search.setOnCloseListener(null)
}
}
I wanted to know what is the best approach to solve this issue, I think even if i use a recyclerview with multiple view types here for the header then still the recycling issue will be there.
Related
I have a fragment where use creates a budget for a specific category like this:
Here is how it works: user adds a new budget item in NewBudgetFragment. That item gets displayed in BudgetFragment in recyclerview. Budget item has amountSpent variable that should be updated each time user adds a new transaction(this happens in another fragment). But after creating the budget item, if the user spends money on that specific category, the amountSpent doesn't get updated in the recyclerview item. I have used both LiveData and DiffUtil in the BudgetAdapter but I can't figure out why it doesn't get updated.
Here is BudgetAdapter:
class BudgetAdapter() : ListAdapter<Budget, BudgetAdapter.BudgetViewHolder>(DiffCallback()) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BudgetViewHolder {
val binding =
BudgetItemLayoutBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return BudgetViewHolder(binding)
}
override fun onBindViewHolder(holder: BudgetViewHolder, position: Int) {
val currentItem = getItem(position)
holder.bind(currentItem, position)
}
class BudgetViewHolder(val binding: BudgetItemLayoutBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(budget: Budget, position: Int) {
binding.apply {
tvBudgetName.text = budget.name
tvBudgetLimit.text = budget.limit.toString()
tvAmountSpent.text = budget.amountSpent.toString()
tvPercentageSpent.text = ((budget.amountSpent/budget.limit)*100).toInt().toString() + "%"
}
}
}
class DiffCallback : DiffUtil.ItemCallback<Budget>() {
override fun areItemsTheSame(oldItem: Budget, newItem: Budget): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: Budget, newItem: Budget): Boolean {
return oldItem == newItem
}
}
}
This is how new budget item gets created:
NewBudgetFragment:
...
viewModel.transactions.observe(viewLifecycleOwner) { it ->
transactionList = it.filter { it.category == listCategory[selectedCategoryIndex].name }
amountSpent = transactionList.sumOf { it.amount }
}
...
if (budgetName.isNotEmpty() && budgetLimit.isNotEmpty() && budgetCategory != null) {
viewModel.addBudget(
name = budgetName,
limit = budgetLimit.toDouble(),
amountSpent=amountSpent,
category = budgetCategory.name)
This is BudgetFragment.kt where the adapter is:
class BudgetFragment : Fragment(R.layout.fragment_budget),BudgetAdapter.OnItemClickListener {
private lateinit var binding: FragmentBudgetBinding
private val viewModel: SharedViewModel by activityViewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding = FragmentBudgetBinding.bind(view)
val budgetAdapter = BudgetAdapter(this)
val toolbar = binding.toolbar.root
toolbar.title = "Budget"
(requireActivity() as MainActivity).setSupportActionBar(toolbar)
binding.apply {
rvBudget.apply {
adapter = budgetAdapter
setHasFixedSize(true)
}
}
viewModel.budgets.observe(viewLifecycleOwner){
if(it.isNotEmpty()){
binding.rvBudget.visibility = View.VISIBLE
binding.tvNoBudget.visibility = View.INVISIBLE
}else{
binding.rvBudget.visibility = View.INVISIBLE
binding.tvNoBudget.visibility = View.VISIBLE
}
budgetAdapter.submitList(it)
}
binding.btAddBudget.setOnClickListener {
val action = BudgetFragmentDirections.actionBudgetFragmentToNewBudgetFragment()
findNavController().navigate(action)
}
}
Asuming that your Budget Class is a data class, then the content comparison should work, so we should not have any problems with the object itself that is being used by the Adapter and the DiffUtil.
But:
I can't see the ViewModel Code - are you submitting the same List Instance to the Adapter with submitList? For example, are you mutating the items in a private List in the ViewModel and posting the same List on the same Live Data everytime?
If yes, then this is probably the reason why the items in the RecyclerView are not being refreshed. You need to create a new Instance of the List, with the content of the old List and then post this on the LiveData.
Example ViewModel, if you don't want to override the behavior of "submitList" where you clear the previous data and add the new one and then call by yourself notifiyDatasetChanged()
class MyBudgetViewModel : ViewModel() {
// Important that its a data class, to actually have a content sensistive equals comparison
// if you don't have an ID, you have to work with list indexes when finding
// and updating this item
data class Budget(val id: Int, val amount: Double)
private val _budgets = MutableLiveData<List<Budget>>()
val budgets: LiveData<List<Budget>>
get() = _budgets
init {
_budgets.value = listOf(Budget(1, 20.0))
}
fun onBudgetChanged(id: Int, newBudget: Double) {
// depends on your setup and how you fill the initial list, this maybe should never be null
// by the time you call onBudgetChanged or something similar
val oldList = _budgets.value ?: emptyList()
// unused example variable - if you want to copy the list 1 by 1, ArrayList takes another list as Constructor
val newListUnchanged = ArrayList(oldList)
// map returns a new instance of the list.
val newList = oldList.map { oldItem ->
if (oldItem.id == id) {
oldItem.copy(amount = newBudget)
} else oldItem
}
_budgets.value = newList
}
}
I have a RecyclerView where an item can be edited via a DialogFragment, so when an item is clicked a Dialog is shown, then I can change some properties of that item, the issue is that RecyclerView is not updated with the updated properties and I have to force a notifyItemChanged when the Dialog is closed.
When an item in RecyclerView is clicked I set a MutableLiveData in my ViewModel so then it can be manipulated in the Dialog.
My ViewModel looks like this:
#HiltViewModel
class DocumentProductsViewModel #Inject constructor(private val repository: DocumentProductsRepository) :
ViewModel() {
val barcode = MutableLiveData<String>()
private val _selectedProduct = MutableLiveData<DocumentProduct>()
val selectedProduct: LiveData<DocumentProduct> = _selectedProduct
private val _selectedDocumentId = MutableLiveData<Long>()
val selectedDocumentId: LiveData<Long> = _selectedDocumentId
val products: LiveData<List<DocumentProduct>> = _selectedDocumentId.switchMap { documentId ->
repository.getDocumentProducts(documentId).asLiveData()
}
fun insert(documentProduct: DocumentProduct) = viewModelScope.launch {
repository.insert(documentProduct)
}
fun setProductQuantity(quantity: Float) {
_selectedProduct.value = _selectedProduct.value.also {
it?.timestamp = System.currentTimeMillis()
it?.quantity = quantity
}
update()
}
fun start(documentId: Long?) = viewModelScope.launch{
if (documentId == null) {
_selectedDocumentId.value = repository.getHeaderByType("Etichette")?.id
}
documentId?.let { documentId ->
_selectedDocumentId.value = documentId
}
}
fun select(product: DocumentProduct) {
_selectedProduct.value = product
}
fun delete() = viewModelScope.launch {
_selectedProduct.value?.let { repository.delete(it) }
}
private fun update() = viewModelScope.launch {
_selectedProduct.value?.let { repository.update(it) }
}
}
And in my fragment I'm subscribed to products as this:
private fun initRecyclerView() {
binding.rvProducts.adapter = adapter
viewModel.products.observe(viewLifecycleOwner) { products ->
val productsCount = products.count()
binding.tvProductsCount.text =
resources.getQuantityString(R.plurals.articoli, productsCount, productsCount)
// TODO: create amount string and set it with resources
binding.tvProductsAmount.text = productsCount.toEuro()
adapter.submitList(products)
binding.rvProducts.smoothScrollToPosition(adapter.itemCount - 1)
}
initSwipe(adapter)
}
When setProductQuantity is called the RecyclerView remains unchanged until notify is called while delete works fine without the necessity of calling any notify on RecyclerView.
UPDATE:
The item position is actually changed in RecyclerView as it's sorted by it's last changed timestamp BUT not the quantity field.
Here is my Adapter:
class DocumentProductsListAdapter : ListAdapter<DocumentProduct, DocumentProductsListAdapter.ViewHolder>(ProductDiffCallback) {
override fun onCreateViewHolder(viewGroup: ViewGroup, viewType: Int): ViewHolder {
val view: View = LayoutInflater.from(viewGroup.context)
.inflate(R.layout.layout_item, viewGroup, false)
return ViewHolder(view)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val product = getItem(position)
holder.bind(product)
}
class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
val barcode: TextView = itemView.findViewById(R.id.barcode)
val quantity: TextView = itemView.findViewById(R.id.quantity)
val description: TextView = itemView.findViewById(R.id.description)
val unitOfMeasure: TextView = itemView.findViewById(R.id.unitOfMeasure)
fun bind(product: DocumentProduct) {
barcode.text = product.barcode
quantity.text = product.quantity.formatForQta().replace(".", ",")
if (product.labelType != null && product.labelType != "") {
unitOfMeasure.text = product.labelType
} else {
unitOfMeasure.text = product.unitOfMeasure?.lowercase(Locale.ITALIAN)
}
description.text = product.description ?: "-"
}
}
}
object ProductDiffCallback : DiffUtil.ItemCallback<DocumentProduct>() {
override fun areItemsTheSame(oldItem: DocumentProduct, newItem: DocumentProduct): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: DocumentProduct, newItem: DocumentProduct): Boolean {
return oldItem == newItem
}
}
data class DocumentProduct(
#PrimaryKey(autoGenerate = true)
var id: Long,
var barcode: String,
#Json(name = "desc")
var description: String?,
#ColumnInfo(defaultValue = "PZ")
#Json(name = "um")
var unitOfMeasure: String?,
#Json(name = "qta")
var quantity: Float,
#Json(name = "id_testata")
var documentId: Long,
#Json(name = "tipo_frontalino")
var labelType: String?,
var timestamp: Long?
) {
constructor(barcode: String, documentId: Long, labelType: String?) : this(
0,
barcode,
null,
"PZ",
1f,
documentId,
labelType,
null
)
override fun equals(other: Any?): Boolean {
return super.equals(other)
}
override fun hashCode(): Int {
return super.hashCode()
}
}
You have the implementations of areContentsTheSame() and areItemsTheSame() swapped.
areContentsTheSame() is asking if everything in the two items being compared is the same. Therefore, if the class has a proper equals()/hashcode() for all properties used by the ViewHolder, you can use oldItem == newItem. If you use a data class with all relevant properties in the primary constructor, then you don't need to manually override equals()/hashcode().
areItemsTheSame() is asking if the two items represent the same conceptual row item, with possible differences in their details. So it should be oldItem.id == newItem.id.
The problem with your data class is that you are overriding equals()/hashcode() without providing any implementation at all. This is effectively disabling the proper implementations that are provided by the data modifier by calling through to the super implementation in the Any class. You should not override them at all when you use data class.
I am implementing filterable list for RecyclerView using ListAdapter with AsyncDifferConfig.Builder that implements Filterable. When searching and no result match, a TextView will be shown.
adapter.filter.filter(filterConstraint)
// Searched asset may not match any of the available item
if (adapter.itemCount <= 0 && adapter.currentList.isEmpty() && filterConstraint.isNotBlank())
logTxtV.setText(R.string.no_data)
else
logTxtV.text = null
Unfortunately the update of filter did not propagate immediately on adapter's count and list.
The adapter count and list is one step behind.
The TextView should be displaying here already
But it only shows after updating it back and the list is no longer empty at this point
I am not sure if this is because I am using AsyncDifferConfig.Builder instead of regular DiffCallback
ListAdapter class
abstract class FilterableListAdapter<T, VH : RecyclerView.ViewHolder>(
diffCallback: DiffUtil.ItemCallback<T>
) : ListAdapter<T, VH>(AsyncDifferConfig.Builder(diffCallback).build()), Filterable {
/**
* True when the RecyclerView stop observing
* */
protected var isDetached: Boolean = false
private var originalList: List<T> = currentList.toList()
/**
* Abstract method for implementing filter based on a given predicate
* */
abstract fun onFilter(list: List<T>, constraint: String): List<T>
override fun getFilter(): Filter {
return object : Filter() {
override fun performFiltering(constraint: CharSequence?): FilterResults {
return FilterResults().apply {
values = if (constraint.isNullOrEmpty())
originalList
else
onFilter(originalList, constraint.toString())
}
}
#Suppress("UNCHECKED_CAST")
override fun publishResults(constraint: CharSequence?, results: FilterResults?) {
submitList(results?.values as? List<T>, true)
}
}
}
override fun submitList(list: List<T>?) {
submitList(list, false)
}
/**
* This function is responsible for maintaining the
* actual contents for the list for filtering
* The submitList for parent class delegates false
* so that a new contents can be set
* While a filter pass true which make sure original list
* is maintained
*
* #param filtered True if the list was updated using filter interface
* */
private fun submitList(list: List<T>?, filtered: Boolean) {
if (!filtered)
originalList = list ?: listOf()
super.submitList(list)
}
override fun onDetachedFromRecyclerView(recyclerView: RecyclerView) {
super.onDetachedFromRecyclerView(recyclerView)
isDetached = true
}
}
RecyclerView Adapter
class AssetAdapter(private val glide: RequestManager, private val itemListener: ItemListener) :
FilterableListAdapter<AssetDataDomain, AssetAdapter.ItemView>(DiffUtilAsset()) {
inner class ItemView(itemView: AssetCardBinding) : RecyclerView.ViewHolder(itemView.root) {
private val assetName = itemView.assetName
private val assetPrice = itemView.assetPrice
private val assetMarketCap = itemView.assetMarketCap
private val assetPercentChange = itemView.assetPercentChange
private val assetIcon = itemView.assetIcon
private val assetShare = itemView.assetShare
// Full update/binding
fun bind(domain: AssetDataDomain) {
with(itemView.context) {
assetName.text = domain.symbol ?: domain.name
bindNumericData(
domain.metricsDomain.marketDataDomain.priceUsd,
domain.metricsDomain.marketDomain.currentMarketcapUsd,
domain.metricsDomain.marketDataDomain.percentChangeUsdLast24Hours
)
if (!isDetached)
glide
.load(
getString(
R.string.icon_url,
AppConfigs.ICON_BASE_URL,
domain.id
)
)
.into(assetIcon)
assetShare.setOnClickListener {
itemListener.onRequestScreenShot(
itemView,
getString(
R.string.asset_info,
domain.name,
assetPercentChange.text.toString(),
assetPrice.text.toString()
)
)
}
itemView.setOnClickListener {
itemListener.onItemSelected(domain)
}
}
}
// Partial update/binding
fun bindNumericData(priceUsd: Double?, mCap: Double?, percent: Double?) {
with(itemView.context) {
assetPrice.text = getString(
R.string.us_dollars,
NumbersUtil.formatFractional(priceUsd)
)
assetMarketCap.text = getString(
R.string.mcap,
NumbersUtil.formatWithUnit(mCap)
)
assetPercentChange.text = getString(
R.string.percent,
NumbersUtil.formatFractional(percent)
)
AppUtil.displayPercentChange(assetPercentChange, percent)
if (NumbersUtil.isNegative(percent))
assetPrice.setTextColor(Color.RED)
else
assetPrice.setTextColor(Color.GREEN)
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemView =
ItemView(
AssetCardBinding.inflate(
LayoutInflater.from(parent.context), parent, false
)
)
override fun onBindViewHolder(holder: ItemView, position: Int) {
onBindViewHolder(holder, holder.absoluteAdapterPosition, emptyList())
}
override fun onBindViewHolder(holder: ItemView, position: Int, payloads: List<Any>) {
if (payloads.isEmpty() || payloads[0] !is Bundle)
holder.bind(getItem(position)) // Full update/binding
else {
val bundle = payloads[0] as Bundle
if (bundle.containsKey(DiffUtilAsset.ARG_PRICE) ||
bundle.containsKey(DiffUtilAsset.ARG_MARKET_CAP) ||
bundle.containsKey(DiffUtilAsset.ARG_PERCENTAGE))
holder.bindNumericData(
bundle.getDouble(DiffUtilAsset.ARG_PRICE),
bundle.getDouble(DiffUtilAsset.ARG_MARKET_CAP),
bundle.getDouble(DiffUtilAsset.ARG_PERCENTAGE)
) // Partial update/binding
}
}
// Required when setHasStableIds is set to true
override fun getItemId(position: Int): Long {
return currentList[position].id.hashCode().toLong()
}
override fun onDetachedFromRecyclerView(recyclerView: RecyclerView) {
super.onDetachedFromRecyclerView(recyclerView)
isDetached = true
}
override fun onFilter(list: List<AssetDataDomain>, constraint: String): List<AssetDataDomain> {
return list.filter {
it.name.lowercase().contains(constraint.lowercase()) ||
it.symbol?.lowercase()?.contains(constraint.lowercase()) == true
}
}
interface ItemListener {
fun onRequestScreenShot(view: View, description: String)
fun onItemSelected(domain: AssetDataDomain)
}
}
UPDATE:
I can confirm that using DiffCallback instead of AsyncDifferConfig.Builder does not change the behavior and issue. It also seems that currentList is in async thus update on list does not reflect immediately after calling submitList.
I do not know if this is intended behavior but upon overriding onCurrentListChanged the currentList parameter is working correctly.
But the adapter.currentList is behaving like a previousList parameter
When you submit a list to recyclerView, it takes some time to compare items of current list and the previous one (to see if an item is removed, moved or added). so the result is not immediately ready.
you can use a RecyclerView.AdapterDataObserver to be notified of changes in recyclerView (it will tell what happened to items overall, like 5 were added etc)
P.S. if you look at recyclerView source code you will see that the DiffCallBack passed in the constructor, is wrapped in AsyncDifferConfig
I am using recycle view with diffutil in my application. but while I rotating or comeback from another screen the adapter gets updated. why is this happening?.
Here My ViewModel
class FeedsViewModel() : ViewModel() {
private val feedsRepository = FeedsRepository()
val feedsLiveData: MutableLiveData<Resource<UserFeeds>> = MutableLiveData()
init {
val apiParams = HashMap<String, String>()
apiParams["user_id"] = "1"
getFeeds(apiParams,"123"
}
fun getFeeds(apiParams: HashMap<String, String>, token: String) = viewModelScope.launch {
feedsLiveData.postValue(Resource.Loading())
val response = feedsRepository.getFeeds(apiParams, token)
if (response.isSuccessful) {
response.body()?.let { resultResponse ->
feedsLiveData.postValue(Resource.Success(resultResponse))
}
} else {
feedsLiveData.postValue(Resource.Error(response.message()))
}
}
}
I am using fragment to display it
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.recyclerViewFeeds.adapter = feedsAdapter
viewModel.feedsLiveData.observe(viewLifecycleOwner, Observer { response ->
when (response) {
is Resource.Success -> {
binding.progressBar.visibility = View.GONE
response.data?.let { userFeeds ->
feedsAdapter.differ.submitList(userFeeds.userPosts.toList())
binding.nooFeeds.visibility = View.GONE
}
is Resource.Error -> {....}
is Resource.Loading -> {....}
}
})
}
and my adapter
class FeedsAdapter(private val context: Context, private val itemClickListener: FeedsItemCallBack) :
RecyclerView.Adapter<FeedsAdapter.MyViewHolder>() {
class MyViewHolder(val bindin: ItemViewFeedsBinding) : RecyclerView.ViewHolder(bindin.root) {
}
private val differCallback = object : DiffUtil.ItemCallback<UserPost>() {
override fun areItemsTheSame(oldItem: UserPost, newItem: UserPost): Boolean {
return oldItem.postId == newItem.postId
}
override fun areContentsTheSame(oldItem: UserPost, newItem: UserPost): Boolean {
return oldItem == newItem
}
}
val differ = AsyncListDiffer(this, differCallback)
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
var feedsItem = differ.currentList[position]
holder.bindin.feedData = feedsItem;
holder.bindin.executePendingBindings()
}
override fun getItemCount(): Int {
return differ.currentList.size
}
}
Is this implementation is correct?.
Is this issue of ViewModel or adapter?
please help. Thanks in advance
you could distinguish the cases of your activity being created for
the first time and being restored from savedInstanceState. This is
done by overriding onSaveInstanceState and checking the parameter of
onCreate.
You could lock the activity in one orientation by adding
android:screenOrientation="portrait" (or "landscape") to
in your manifest.
You could tell the system that you meant to handle screen changes
for yourself by specifying
android:configChanges="orientation|screenSize" in the
tag. This way the activity will not be recreated, but will receive a
callback instead (which you can ignore as it's not useful for you).
Personally, I'd go with (3). Of course if locking the app to one of the orientations is fine with you, you can also go with (2).
RecyclerView keeps scrolling
I am making a Media Library app. When I use the recyclerview list it scrolls over the same
items again and again. It does not stop when it reaches the bottom of the file list.
class MediaListAdapter(val mediaList: ArrayList<Media>): RecyclerView.Adapter<MediaListAdapter.MediaViewHolder>(){
private var context: Context? = null
fun updateMediaList(newMediaList: List<Media>){
mediaList.clear()
mediaList.addAll(newMediaList)
notifyDataSetChanged()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MediaViewHolder {
context = parent.context
val inflater = LayoutInflater.from(parent.context)
val view = DataBindingUtil.inflate<ItemMediaBinding>(inflater, R.layout.item_media, parent, false)
return MediaViewHolder(view)
}
override fun onBindViewHolder(holder: MediaViewHolder, position: Int) {
holder.view.btnPlayVideo.visibility = View.INVISIBLE
holder.view.mediaItem = mediaList[position]
if(mediaList[position].mediaUrl.contains(".mp4")){
holder.view.btnPlayVideo.visibility = View.VISIBLE
holder.view.btnPlayVideo.setOnClickListener {
context!!.startActivity(Intent(context, VideoViewActivity::class.java).putExtra("videoUrl", mediaList[position].mediaUrl))
}
}
}
override fun getItemCount():Int {
return mediaList.size
}
class MediaViewHolder(var view: ItemMediaBinding) : RecyclerView.ViewHolder(view.root)
}
That is my Adapter class.
The media information is being pulled from a database containing the image urls and other information related to the information.
I have compared my code to a similar solution that works how I want and can't find any reason for the bug.
val media = MutableLiveData<List<Media>>()
val loading = MutableLiveData<Boolean>()
private val mediaList = ArrayList<String>()
fun fetchFromDatabase() {
loading.value = true
loadImages()
launch {
storeMedia(mediaList)
val media = MediaDatabase(getApplication()).mediaDao().getAllMedia()
mediaRetrieved(media)
}
}
private fun mediaRetrieved(mediaList: List<Media>) {
media.value = mediaList
loading.value = false
}
private fun storeMedia(list: List<String>) {
var found = false
launch {
val dao = MediaDatabase(getApplication()).mediaDao().getAllMedia()
for(item in list){
found = false
for(media in dao){
if(item == media.mediaLocation){
found = true
break
}
}
if(!found){
val media = Media(mediaUrl = item)
MediaDatabase(getApplication()).mediaDao().insert(media)
}
}
}
}
That is the code from the viewholder were I am adding data to the recyclerview adapter
Check the size of items that are available is your db/api from wherever you are retrieving media information(May be you are receiving the repeated data).