I don't know if it is a good way that implementing comparison in the each item, not in areItemsTheSame, areContentsTheSame.
But I am trying to do it.
sealed class StatusListData(val type: Int) {
object SearchData : StatusListData(0)
object TimeFilterData : StatusListData(1)
data class CategoryFilterData(val categoryFilterList: List<CheckBoxFilter>) : StatusListData(2)
data class PageTitleData(
#StringRes val titleRes: Int,
var currentItemCount: Int,
var totalItemCount: Int,
val showTotal: Boolean,
) : StatusListData(3)
data class StatusData(
val receptionKey: String,
val name: String,
val phoneNumber: String,
val address: String,
val addressOld: String,
val amount: Int,
val deliveryMethod: DeliveryMethodType,
val deliveryMemo: String?,
val startAt: LocalDateTime?,
val estimatedEndAt: LocalDateTime?,
val endAt: LocalDateTime?,
var detailStatus: ReceptionUiType,
val prescriptionId: String?,
val prescriptionUrl: String,
val createdAt: LocalDateTime?,
val approvedAt: LocalDateTime?,
val dispensedAt: LocalDateTime?,
val deliveryAt: LocalDateTime?,
val completedAt: LocalDateTime?,
val canceledAt: LocalDateTime?,
val failReason: String = "",
var faxMode: Boolean = false,
var isConfirmed: Boolean = false
) : StatusListData(4){
#Deprecated("not used", ReplaceWith("isConfirmed = boolean"))
fun markAsRead(boolean: Boolean){
isConfirmed = boolean
}
}
}
class StatusListDiffCallback : DiffUtil.ItemCallback<StatusListData>() {
override fun areItemsTheSame(oldItem: StatusListData, newItem: StatusListData): Boolean {
val isSameType = oldItem.type == newItem.type
if (!isSameType) return false
return when (oldItem) {
is StatusListData.SearchData, is StatusListData.TimeFilterData -> true
is StatusListData.CategoryFilterData -> oldItem.categoryFilterList.map { it.nameRes } == (newItem as StatusListData.CategoryFilterData).categoryFilterList.map { it.nameRes }
is StatusListData.PageTitleData -> oldItem.titleRes == (newItem as StatusListData.PageTitleData).titleRes
is StatusListData.StatusData -> oldItem.receptionKey == (newItem as StatusListData.StatusData).receptionKey
}
}
override fun areContentsTheSame(oldItem: StatusListData, newItem: StatusListData): Boolean {
val isSameType = oldItem.type == newItem.type
if (!isSameType) return false
return when (oldItem) {
is StatusListData.SearchData, is StatusListData.TimeFilterData -> oldItem == newItem
is StatusListData.CategoryFilterData -> oldItem.categoryFilterList.map { it.nameRes } == (newItem as StatusListData.CategoryFilterData).categoryFilterList.map { it.nameRes }
is StatusListData.PageTitleData -> oldItem == (newItem as StatusListData.PageTitleData)
is StatusListData.StatusData -> oldItem == (newItem as StatusListData.StatusData)
}
}
}
to something like
interface ListType{
fun isSame(newItem: StatusListData): Boolean
}
sealed class StatusListData(val type: Int), ListType{
object SearchData : StatusListData(0)
override isSame(newItem: StatusListData): Boolean{
// TODO:
}
}
object TimeFilterData : StatusListData(1){
override isSame(newItem: StatusListData): Boolean{
// TODO:
}
}
// ...
}
And
class StatusListDiffCallback : DiffUtil.ItemCallback<StatusListData>() {
override fun areItemsTheSame(oldItem: StatusListData, newItem: StatusListData): Boolean {
val isSameType = oldItem.type == newItem.type
if (!isSameType) return false
return newItem.isTheSame(oldItem)
}
Is it better? If yes, then How can I implement it?
What I saw in the replace condition using polymorphism is something like this
sealed class StatusListData(val type: Int){
object SearchData : StatusListData(0), ListType
override isSame(newItem: StatusListData): Boolean{
// TODO:
}
}
...
So that, when I pass any of the class then it will pass. However, when it is sealed class and it inherits the interface, then,
fun pass(item: StatusListData)
this won't work with
pass(item as SearchData)
pass(item as CategoryFilterData)
...
then, I should
pass(item as StatusListData)
and then,
override fun pass(item: StatusListData){
val mItem = item as SearchData
// TODO: something
}
override fun pass(item: StatusListData){
val mItem = item as CategoryFilterData
// TODO: something
}
is this a good way?? Is there any better solution and should I leave it?
Related
My app crashes when I attempt to click on articles published days ago, however it works fine when I tried to do it on a more recent article, here is an image for reference.
The app crashes when I scroll down and click on past news, the app also seems to be quite laggy and unresponsive, would appreciate any advice.
The error seems to lie on the Onclicklistener portion of the code, I uploaded all the codes which I think are relevant.
Runtime error
From the errors below, the classes highlighted were the BreakingNews and the NewsAdapter.
java.lang.NullPointerException: Attempt to invoke virtual method 'int java.lang.Object.hashCode()' on a null object reference
at com.example.thenewsapplication.ROOM.Source.hashCode(Unknown Source:2)
at com.example.thenewsapplication.ROOM.Article.hashCode(Unknown Source:71)
at androidx.navigation.NavBackStackEntry.hashCode(NavBackStackEntry.kt:256)
at java.util.HashMap.hash(HashMap.java:338)
at java.util.HashMap.put(HashMap.java:611)
at androidx.navigation.NavController.linkChildToParent(NavController.kt:143)
at androidx.navigation.NavController.addEntryToBackStack(NavController.kt:1918)
at androidx.navigation.NavController.addEntryToBackStack$default(NavController.kt:1813)
at androidx.navigation.NavController$navigate$4.invoke(NavController.kt:1721)
at androidx.navigation.NavController$navigate$4.invoke(NavController.kt:1719)
at androidx.navigation.NavController$NavControllerNavigatorState.push(NavController.kt:287)
at androidx.navigation.fragment.FragmentNavigator.navigate(FragmentNavigator.kt:198)
at androidx.navigation.fragment.FragmentNavigator.navigate(FragmentNavigator.kt:164)
at androidx.navigation.NavController.navigateInternal(NavController.kt:260)
at androidx.navigation.NavController.navigate(NavController.kt:1719)
at androidx.navigation.NavController.navigate(NavController.kt:1545)
at androidx.navigation.NavController.navigate(NavController.kt:1472)
at androidx.navigation.NavController.navigate(NavController.kt:1454)
at com.example.thenewsapplication.Fragments.BreakingNews$onViewCreated$1.invoke(BreakingNews.kt:57)
at com.example.thenewsapplication.Fragments.BreakingNews$onViewCreated$1.invoke(BreakingNews.kt:53)
at com.example.thenewsapplication.Adapter.NewsAdapter.onBindViewHolder$lambda-2$lambda-1(NewsAdapter.kt:82)
at com.example.thenewsapplication.Adapter.NewsAdapter.$r8$lambda$xRXjhIuiNyf8fdAGPo8jTchti_k(Unknown Source:0)
at com.example.thenewsapplication.Adapter.NewsAdapter$$ExternalSyntheticLambda0.onClick(Unknown Source:4)
at android.view.View.performClick(View.java:7448)
at android.view.View.performClickInternal(View.java:7425)
at android.view.View.access$3600(View.java:810)
at android.view.View$PerformClick.run(View.java:28305)
at android.os.Handler.handleCallback(Handler.java:938)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loop(Looper.java:223)
at android.app.ActivityThread.main(ActivityThread.java:7656)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:592)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:947)
Article class
(The article itself)
I extended the serialization class so I can pass info between fragments via Bundle
data class Article(
#PrimaryKey(autoGenerate = true)
var id: Int? =null,
val author: String?,
val content: String?,
val description: String?,
val publishedAt: String?,
val source: Source?,
val title: String?,
val url: String,
val urlToImage: String?
) : Serializable
Source (for the source datatype in the Article data class)
data class Source(
val id: Any,
val name: String
)
TypeConverters (Convert the Source datatype to a primative type)
class Converters {
#TypeConverter
fun fromSource(source: Source): String{
return source.name
}
#TypeConverter
fun toSource(name: String): Source {
return Source(name, name)
}
}
Adapter (Adapter for the BreakingNews class RecyclerView)
class NewsAdapter: RecyclerView.Adapter<NewsAdapter.ArticleViewHolder>() {
class ArticleViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView){
val ivArticleImage = itemView.findViewById<ImageView>(R.id.ivArticleImage)
val tvSource = itemView.findViewById<TextView>(R.id.tvSource)
val tvTitle = itemView.findViewById<TextView>(R.id.tvTitle)
val tvDescription = itemView.findViewById<TextView>(R.id.tvDescription)
val tvPublishedAt = itemView.findViewById<TextView>(R.id.tvPublishedAt)
}
private val diffcallback = object : DiffUtil.ItemCallback<Article>(){
override fun areItemsTheSame(oldItem: Article, newItem: Article): Boolean {
return oldItem.url == newItem.url
}
override fun areContentsTheSame(oldItem: Article, newItem: Article): Boolean {
return oldItem == newItem
}
}
val differ = AsyncListDiffer(this, diffcallback)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ArticleViewHolder {
return ArticleViewHolder(
LayoutInflater.from(parent.context).inflate(
R.layout.item_article_preview,
parent,
false
)
)
}
fun setOnItemClickListener(listen: (Article) -> Unit){
Log.d("NewsAdapter", "setOnItem")
onItemClickListener = listen
}
private var onItemClickListener: ((Article) -> Unit)? = null
override fun onBindViewHolder(holder: ArticleViewHolder, position: Int) {
val article = differ.currentList[position]
holder.itemView.apply {
Glide.with(this).load(article.urlToImage).into(holder.ivArticleImage)
holder.tvSource.text = article.source?.name
holder.tvTitle.text = article.title
holder.tvDescription.text = article.description
holder.tvPublishedAt.text = article.publishedAt
setOnClickListener{
Log.d("NewsAdapter", "onBindViewHolder")
onItemClickListener?.let {
it(article)
}
}
}
}
override fun getItemCount(): Int {
return differ.currentList.size
}
}
BreakingNews (The Fragment/UI)
class BreakingNews: Fragment(R.layout.breakingnews) {
lateinit var viewModel: NewsViewModel
lateinit var newsAdapter: NewsAdapter
lateinit var binding: BreakingnewsBinding
val TAG = "BreakingNewsFragment"
var isLoading = false
var isLastPage = false
var isScrolling = false
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = BreakingnewsBinding.inflate(inflater,container,false);
val view = binding.root;
return view;
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel = (activity as MainActivity).viewModel
setupRecyclerView()
newsAdapter.setOnItemClickListener {
val bundle = Bundle().apply {
putSerializable("article",it) //Passing data between activity or fragments, work based on key/value pairs. Since we have denoted our Article Class to be of type Serializable, we can use this to pass data.
}
findNavController().navigate(
R.id.action_breakingNews_to_newsDetailsFragment,
bundle
)
}
viewModel.breakingNews.observe(viewLifecycleOwner,Observer{response ->
when (response){
is Resource.Success -> {
hideProgressBar()
response.data?.let {newsResponse ->
newsAdapter.differ.submitList(newsResponse.articles.toList())
val totalPages = newsResponse.totalResults / QUERY_PAGE_SIZE + 2
isLastPage = viewModel.breakingNewsPage == totalPages
}
}
is Resource.Error -> {
showProgressBar()
response.message?.let {
Log.e(TAG,"An error occured $it")
}
}
is Resource.Loading -> {
hideProgressBar()
}
}
})
}
private fun hideProgressBar() {
binding.paginationProgressBar.visibility = View.INVISIBLE
isLoading = false
}
private fun showProgressBar() {
binding.paginationProgressBar.visibility = View.VISIBLE
isLoading = true
}
var scrollListener = object : RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
super.onScrollStateChanged(recyclerView, newState)
if (newState == AbsListView.OnScrollListener.SCROLL_STATE_TOUCH_SCROLL) {
isScrolling = true
}
}
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
val layoutManager = recyclerView.layoutManager as LinearLayoutManager
val firstVisibleItemPosition = layoutManager.findFirstVisibleItemPosition()
val visibleItemCount = layoutManager.childCount
val totalItemCount = layoutManager.itemCount
val isNotLoadingAndNotLastPage = !isLoading && !isLastPage
val isAtLastItem = firstVisibleItemPosition + visibleItemCount >= totalItemCount
val isNotAtBeginning = firstVisibleItemPosition >= 0
val isTotalMoreThanVisible = totalItemCount >= QUERY_PAGE_SIZE
val shouldPaginate =
isNotLoadingAndNotLastPage && isAtLastItem && isNotAtBeginning && isTotalMoreThanVisible && isScrolling
if (shouldPaginate) {
viewModel.getBreakingNews("us")
isScrolling = false
}
}
}
private fun setupRecyclerView() {
newsAdapter = NewsAdapter()
binding.breakingNews.apply {
adapter = newsAdapter
layoutManager = LinearLayoutManager(activity)
addOnScrollListener(this#BreakingNews.scrollListener)
}
}
}
enter code here
I think it's not about being old article and new article, it's about that articles have id of null. To prevent that organize your data classes like below:
#Entity(tableName = "articles")
data class Article(
#PrimaryKey(autoGenerate = true)
var id: Int? = null,
val author: String?,
val content: String?,
val description: String?,
val publishedAt: String?,
val source: Source?,
val title: String?,
val url: String?,
val urlToImage: String?
): Serializable {
override fun hashCode(): Int {
var result = id.hashCode()
if(url.isNullOrEmpty()){
result = 31 * result + url.hashCode()
}
return result
}
}
data class Source(
val id: String,
val name: String
): Serializable {
override fun hashCode(): Int {
var result = id.hashCode()
if(name.isNullOrEmpty()){
result = 31 * result + name.hashCode()
}
return result
}
}
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 developing tvshows app where I am implementing following logic user search tvshows and filtered result has to show in recyclerview but I want to implement filtering functionality in viewmodel
how can I achieve that
below interface class
interface ApiInterface {
#GET("search/shows")
suspend fun searchShows( #Query("q") query: String): Call<TvMazeResponse>
}
below TvRepository.kt
class TvRepository(private val apiInterface: ApiInterface) {
suspend fun getShows() = apiInterface.searchShows("")
}
below adapter class
class TvAdapter : RecyclerView.Adapter<TvAdapter.ViewHolder>(), Filterable {
lateinit var tvMazeList: MutableList<TvMazeResponse>
lateinit var filterResult: ArrayList<TvMazeResponse>
override fun getItemCount(): Int =
filterResult.size
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder(
LayoutInflater.from(parent.context).inflate(
R.layout.tv_item, parent,
false
)
)
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(filterResult[position])
}
fun addData(list: List<TvMazeResponse>) {
tvMazeList = list as MutableList<TvMazeResponse>
filterResult = tvMazeList as ArrayList<TvMazeResponse>
notifyDataSetChanged()
}
override fun getFilter(): Filter {
return object : Filter() {
override fun performFiltering(constraint: CharSequence?): FilterResults {
val charString = constraint?.toString() ?: ""
if (charString.isEmpty()) filterResult =
tvMazeList as ArrayList<TvMazeResponse> else {
val filteredList = ArrayList<TvMazeResponse>()
tvMazeList
.filter {
(it.name.contains(constraint!!)) or
(it.language.contains(constraint))
}
.forEach { filteredList.add(it) }
filterResult = filteredList
}
return FilterResults().apply { values = filterResult }
}
override fun publishResults(constraint: CharSequence?, results: FilterResults?) {
filterResult = if (results?.values == null)
ArrayList()
else
results.values as ArrayList<TvMazeResponse>
notifyDataSetChanged()
}
}
}
class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
fun bind(result: TvMazeResponse) {
with(itemView) {
Picasso.get().load(result.image.medium).into(imageView)
}
}
}
}
below Constants.kt
object Constants {
const val BASE_URL = "https://api.tvmaze.com/"
}
below TvMazeResponse.kt
data class TvMazeResponse(
#SerializedName("averageRuntime")
val averageRuntime: Int,
#SerializedName("dvdCountry")
val dvdCountry: Any,
#SerializedName("externals")
val externals: Externals,
#SerializedName("genres")
val genres: List<String>,
#SerializedName("id")
val id: Int,
#SerializedName("image")
val image: Image,
#SerializedName("language")
val language: String,
#SerializedName("_links")
val links: Links,
#SerializedName("name")
val name: String,
#SerializedName("network")
val network: Network,
#SerializedName("officialSite")
val officialSite: String,
#SerializedName("premiered")
val premiered: String,
#SerializedName("rating")
val rating: Rating,
#SerializedName("runtime")
val runtime: Int,
#SerializedName("schedule")
val schedule: Schedule,
#SerializedName("status")
val status: String,
#SerializedName("summary")
val summary: String,
#SerializedName("type")
val type: String,
#SerializedName("updated")
val updated: Int,
#SerializedName("url")
val url: String,
#SerializedName("webChannel")
val webChannel: Any,
#SerializedName("weight")
val weight: Int
)
below TvViewModel.kt
class TvViewModel(apiInterface: ApiInterface) : ViewModel() {
}
I want to implement filter and search function in viewmodel how can I achieve that any help and tips greatly appreciated
In TvRepository change the getShows function to
suspend fun getShows(searchString:String) = apiInterface.searchShows(searchString)
Then in the ViewModel change the constructor to get an instance of the TVRepository and call API as shown below
class TvViewModel( tvRepository: TvRepository) : ViewModel() {
fun getShows(searchParameter:String){
viewModelScope.launch(Dispatchers.IO){
val response= tvRepository.getShows().awaitResponse()
if(response.isSuccessful{
//api success you can get result from response.body
}
else{
//api failed
}
}
}
}
I need to set the name of chosen category, below is the category object code and methods I call by clicking.
Grateful for helping.
categoryItem object:
#Entity(tableName = "category_table")
#Parcelize
data class CategoryItem(
val categoryName: String,
val categoryNumber: Int,
val categoryShown: Boolean = false,
#PrimaryKey(autoGenerate = true) var id: Int = 0
) : Parcelable {
}
In Menu:
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.action_cat1 -> {
viewModel.onChooseCategoryClick(1)
true
}
R.id.action_cat2 -> {
viewModel.onChooseCategoryClick(2)
true
}
onChooseCategoryClick() in ViewModel:
fun onChooseCategoryClick(chosenCategory: Int) = viewModelScope.launch {
preferencesManager.updateCategoryChosen(chosenCategory)
// what to type here
}
in preferencesManager:
suspend fun updateCategoryChosen(categoryChosen: Int) {
dataStore.edit { preferences ->
preferences[PreferencesKeys.CATEGORY_CHOSEN] = categoryChosen
}
}
The choosenCategory value is the same as the id of one of the items on your menu items. So you can find it using when:
when(choosenCategory){
R.id.action_cat1->
R.id.action_cat2->
}
My diffcallback areContentsTheSame(oldItem: ItemModel, newItem: ItemModel) always receives the same content. I use the status to check, but each time the status is the same. Even though the status is actually changing. I intend to display progress for each item. So I regularly send the current progress through a status. Using diffcallback, it should check that status for an item are not the same and then update that item only. But it seems newItem and oldItem it receives are same.
I have a custom model ItemModel
data class ItemModel(val id: String, var title: String) {
var clickListener: ClickListener? = null
var status: Status? = null
interface ClickListener{
fun onItemClick(view: View, item: ItemModel)
fun onClick(view: View, item: ItemModel)
}
companion object {
val STATUS_CHANGED = 1
val diffCallback = object : DiffUtil.ItemCallback<ItemModel>() {
override fun areItemsTheSame(oldItem: ItemModel, newItem: ItemModel): Boolean {
// Log.i("DiffUtil", "SameItem? old status: ${oldItem.status}, new Status: ${newItem.status}")
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: ItemModel, newItem: ItemModel): Boolean {
// Log.i("DiffUtil", "Checking status: new: ${newItem.status}, old ${oldItem.status}")
return oldItem.status == newItem.status
}
override fun getChangePayload(oldItem: ItemModel, newItem: ItemModel): Any? {
Log.i("DiffUtil", "Payload change: ${newItem.status?.state}")
if (oldItem.status != newItem.status) {
return STATUS_CHANGED
}
return null
}
}
}
}
And Status data class
data class Status(val max: Int, val progress: Int, val state: State = State.NONE)
This is my ViewModel class; I use Observable.intervalRange to generate different numbers and change the status of a single list item. But it seems that diffcallback is not working properly.
class FunViewModel : ViewModel() {
private val itemModels: MutableLiveData<List<ItemModel>> = MutableLiveData()
fun items(): LiveData<List<ItemModel>> {
return itemModels
}
private val compositeDisposable = CompositeDisposable()
fun initialize(itemList: List<ItemModel>) {
itemModels.value = itemList
}
private fun updateItem(item: ItemModel, status: Status) {
val currentItems = mutableListOf<ItemModel>()
if (itemModels.value == null) return
currentItems.addAll(itemModels.value!!)
Log.i("UpdateItem", "Current item: ${item.id}")
if (currentItems.isNotEmpty()) {
for ((index, el) in currentItems.withIndex()) {
// Log.i("UpdateItem", "searching: ${el.id}")
if (el.id == item.id) {
val currentItem = currentItems.removeAt(index)
Log.i("UpdateItem", "old status: ${currentItem.status}")
currentItem.status = status
currentItems.add(index, currentItem)
break
}
}
itemModels.value = currentItems
Log.i("UpdateItem", "new status: ${items().value?.get(0)?.status}")
}
}
fun startProgress(item: ItemModel) {
val disposable = getProgress(item.id).map { progress ->
val status = Status(progress.second.toInt(), progress.third.toInt(), State.IN_PROGRESS)
status
}.subscribeOn(Schedulers.single())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({ status ->
updateItem(item, status)
// Log.i("FunViewModel:", "Status: progress: ${status.progress}, State: ${status.state}")
},
{ throwable: Throwable? -> Log.e("FunViewModel", "Unable to process progress!", throwable) },
{
val status = Status(20, 20, State.COMPLETED)
updateItem(item, status)
Log.i("FunViewModel:", "Status: progress: ${status.progress}, State: ${status.state}")
})
compositeDisposable.add(disposable)
}
private fun getProgress(id: String): Observable<Triple<String, Long, Long>> {
return Observable.intervalRange(0, 20, 300, 500, TimeUnit.MILLISECONDS)
.map { num -> Triple<String, Long, Long>(id, 20, num) }
}
override fun onCleared() {
super.onCleared()
if (!compositeDisposable.isDisposed) compositeDisposable.dispose()
}
}
Well, I discovered the problem was that I was copying the original contents in a way that does not do actual content copy but referencing. So I fixed the problem by copying the item contents into a new itemModel instance. With this done, diff util is able to differentiate properly.
Instead of this,
private fun updateItem(item: ItemModel, status: Status) {
......................................................
for ((index, el) in currentItems.withIndex()) {
if (el.id == item.id) {
val currentItem = currentItems.removeAt(index)
currentItem.status = status
currentItems.add(index, currentItem)
break
}
}
itemModels.value = currentItems
......................................
}
I did this,
private fun updateItem(item: ItemModel, status: Status) {
...............
if (el.id == item.id) {
currentItems.removeAt(index)
val currentItem = ItemModel(item.id, "${item.title}, ${status.progress}")
currentItem.status = status
currentItems.add(index, currentItem)
break
}
...........
}