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
}
}
Related
I have an API which give me the list of doctors. On it's last page only 1 item is there and other items are null like this:
After this i have used paging library for pagination
my pagingSource code: `
class DocPagingSource(val docRepository: DocRepository): PagingSource<Int, Data>() {
override fun getRefreshKey(state: PagingState<Int, Data>): Int? {
return state.anchorPosition?.let {
state.closestPageToPosition(it)?.prevKey?.plus(1)
?: state.closestPageToPosition(it)?.nextKey?.minus(1)
}
}
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Data> {
return try {
val currentPage = params.key?: 1
val city: String = ""
val response = docRepository.getDoctors(city, currentPage)
val page = Math.ceil(response.body()!!.total.toDouble()/5).toInt()
val data = response.body()!!.data
val responseData = mutableListOf<Data>()
responseData.addAll(data)
LoadResult.Page(
data = responseData,
prevKey = if(currentPage==1) null else -1,
nextKey = if (currentPage== page) null else currentPage.plus(1)
)
}catch (e: HttpException){
LoadResult.Error(e)
}catch (e: Exception){
LoadResult.Error(e)
}
}
`
My paging Adapter Code:
class DocAdapter(val context: Context): PagingDataAdapter<Data, DocAdapter.DocViewHolder>(DiffUtil()) {
private lateinit var binding: ItemDoctorsBinding
inner class DocViewHolder : RecyclerView.ViewHolder(binding.root) {
fun bind(item: Data?) {
binding.apply {
txtDocCity.text = item?.city
txtDocName.text = item?.docName
txtDocFees.text = item?.docConsultationFee
txtDocYOE.text = item?.docYoE
txtDocSpecialisation.text = item?.docSpecialisation
Glide.with(context)
.load(item?.docProfileImgUrl)
.fitCenter()
.diskCacheStrategy(DiskCacheStrategy.ALL)
.into(docPhoto)
}
}
}
override fun onBindViewHolder(holder: DocViewHolder, position: Int) {
val item = getItem(position)
if (item!=null){
holder.bind(getItem(position)!!)
holder.setIsRecyclable(false)
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DocViewHolder {
val inflater = LayoutInflater.from(context)
binding = ItemDoctorsBinding.inflate(inflater, parent, false)
return DocViewHolder()
}
class DiffUtil: androidx.recyclerview.widget.DiffUtil.ItemCallback<Data>(){
override fun areItemsTheSame(oldItem: Data, newItem: Data): Boolean {
return oldItem.docId == newItem.docId
}
override fun areContentsTheSame(oldItem: Data, newItem: Data): Boolean {
return oldItem==newItem
}
}}
what I am getting after reaching my 16th item in doctor list on last page it should show entry till 16th item but after that it also shows again like this:
Also if i dont use holder.setIsRecyclable(false) in pagingAdapter then this android icon not shown but then list is populated with previous doctors:
on the top DR. Sixteen is shown like this:
and in between it again shows like this:
My doctorViewModel Class:
class DocViewModel(val repository: DocRepository): ViewModel() {
val loading = MutableLiveData<Boolean>()
val docList = Pager(PagingConfig(5, maxSize = 100)){
DocPagingSource(repository)
}.flow.cachedIn(viewModelScope)}
My main Activity:
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private lateinit var docViewModel: DocViewModel
private lateinit var docAdapter: DocAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
val docListRepository = DocRepository()
val docFactory = DocViewModelFactory(docListRepository)
docViewModel = ViewModelProvider(this, docFactory).get(DocViewModel::class.java)
docAdapter = DocAdapter(this)
lifecycleScope.launchWhenCreated {
docViewModel.docList.collect{
docAdapter.submitData(it)
}
}
binding.docRecyclerView.apply {
layoutManager = LinearLayoutManager(this#MainActivity)
adapter = docAdapter
setHasFixedSize(true)
}
}}
I have solved this error by applying a condition in my paging source code
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.
This is my main activity of my app. I'm using nav controller.
class MainActivity : AppCompatActivity() {
lateinit var viewModel: NewsViewModel
lateinit var navController: NavController
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val newRepository = NewsRepository(ArticleDatabase(this))
val viewModelProviderFactory = NewsViewModelProviderFactory(newRepository)
viewModel = ViewModelProvider(this , viewModelProviderFactory).get(NewsViewModel::class.java)
// bottomNavigationView.setupWithNavController(newsNavHostFragment.findNavController())
val navHostFragment= supportFragmentManager.findFragmentById(R.id.newsNavHostFragment) as NavHostFragment
navController= navHostFragment.navController
bottomNavigationView.setupWithNavController(navController)
}
This the breaking news fragment. The function is same in other fragments so only posting this one
class BreakingNewsFragment : Fragment (R.layout.fragment_breaking_news) {
lateinit var viewModel: NewsViewModel
lateinit var newsAdapter: NewsAdapter
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)
}
findNavController().navigate(
R.id.action_breakingNewsFragment_to_articleFragment2 , bundle
)
}
viewModel.breakingNews.observe(viewLifecycleOwner, Observer { response ->
when(response) {
is Resource.Success -> {
hideProgressBar()
response.data?.let { newsResponse ->
newsAdapter.differ.submitList(newsResponse.articles)
}
}
is Resource.Error -> {
hideProgressBar()
response.message?.let { message ->
Log.e("Breaking Fragment", "An error occured: $message")
}
}
is Resource.Loading -> {
showProgressBar()
}
}
})
}
private fun hideProgressBar() {
paginationProgressBar.visibility = View.INVISIBLE
}
private fun showProgressBar() {
paginationProgressBar.visibility = View.VISIBLE
}
fun setUpRecyclerView() {
newsAdapter = NewsAdapter()
rvBreakingNews.apply {
adapter = newsAdapter
layoutManager = LinearLayoutManager(activity)
}
}
}
This is made from Youtuber Philip lackner's tutorial. Here's the the link of original project
https://github.com/philipplackner/MVVMNewsApp . His version of fragment is deprecated so i used new fragment view in xml file. so there is an issue in using nav controller.
EDIT -> Forgot to put errors. Here it is
2022-10-17 01:08:09.796 3971-3971/com.arpit.newsapp20 E/AndroidRuntime: FATAL EXCEPTION: main
Process: com.arpit.newsapp20, PID: 3971
java.lang.NullPointerException: Attempt to invoke virtual method 'int java.lang.String.hashCode()' on a null object reference
at com.arpit.newsapp20.models.Article.hashCode(Unknown Source:15)
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.arpit.newsapp20.ui.BreakingNewsFragment$onViewCreated$1.invoke(BreakingNewsFragment.kt:31)
at com.arpit.newsapp20.ui.BreakingNewsFragment$onViewCreated$1.invoke(BreakingNewsFragment.kt:27)
at com.arpit.newsapp20.adapters.NewsAdapter.onBindViewHolder$lambda-2$lambda-1(NewsAdapter.kt:56)
at com.arpit.newsapp20.adapters.NewsAdapter.$r8$lambda$FmTlKYZBcoLQp02jU2NS9dL1z-k(Unknown Source:0)
at com.arpit.newsapp20.adapters.NewsAdapter$$ExternalSyntheticLambda0.onClick(Unknown Source:4)
at android.view.View.performClick(View.java:7441)
at android.view.View.performClickInternal(View.java:7418)
at android.view.View.access$3700(View.java:835)
at android.view.View$PerformClick.run(View.java:28676)
at android.os.Handler.handleCallback(Handler.java:938)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loopOnce(Looper.java:201)
at android.os.Looper.loop(Looper.java:288)
at android.app.ActivityThread.main(ActivityThread.java:7839)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:548)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1003)
Here is the article class
#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
Here is the news adapter class
class NewsAdapter : RecyclerView.Adapter<NewsAdapter.ArticleViewHolder>() {
inner class ArticleViewHolder(itemView: View): RecyclerView.ViewHolder(itemView)
private val differCallback = 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, differCallback)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ArticleViewHolder {
return ArticleViewHolder(
LayoutInflater.from(parent.context).inflate(
R.layout.item_article_preview,
parent,
false
)
)
}
override fun getItemCount(): Int {
return differ.currentList.size
}
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(ivArticleImage)
tvSource.text = article.source.name
tvTitle.text = article.title
tvDescription.text = article.description
tvPublishedAt.text = article.publishedAt
setOnClickListener {
onItemClickListener?.let { it(article) }
}
}
}
fun setOnItemClickListener(listener: (Article) -> Unit) {
onItemClickListener = listener
}
}
You look like to want to pass the clicked news item to the article fragment. But in your click listener you don't receive the clicked item. Instead you pass something else in "it". I think that's why you get a null pointer exception.
You need something like following:
newsAdapter!!.setOnItemClickListener {
val newsItem = // get clicked news item from adapter
newsItem?.let {
val bundle = Bundle().apply {
putSerializable("article" , it)
}
findNavController().navigate(
R.id.action_breakingNewsFragment_to_articleFragment2 , bundle
)
}
}
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?
So I have a RecyclerView which data is populated with data from some Data Class. I want to Apply a search item function for this recyclerview. Back when I don't use Data Classes, I followed this tutorial (The tutorial is in Java).
Now that I'm using a data class since I fetch the data that populate the recyclerview from an API endpoint, I'm quite confused on how to apply the search function in the current recyclerview.
(I fetch the data using library Retrofit if you wondering.)
This is the code snippet from the fragment:
RefundListFragment.kt
private fun fetchRefundListData() {
NetworkConfig().getRefundListDetailService().getRefundList().enqueue(object :
Callback<RefundListPOJODataClass> {
override fun onFailure(call: Call<RefundListPOJODataClass>, t: Throwable) {
...
...
...
}
override fun onResponse(
call: Call<RefundListPOJODataClass>,
response: Response<RefundListPOJODataClass>
) {
binding.refundProgressBar.visibility = View.GONE
binding.rvRefundList.adapter =
response.body()?.let { RefundListAdapter(it, this#RefundListFragment) }
fun filterString(text: String) {
val filteredList: List<RefundListPOJODataClass> = ArrayList()
for (item in response.body()?.data!!) {
if (item != null) {
if (item.buyerName?.toLowerCase()?.contains(text.toLowerCase())!!) {
filteredList.add(item)
}
}
}
adapter.filterList(filteredList)
}
binding.refundListSearchBar.addTextChangedListener(object : TextWatcher {
override fun afterTextChanged(s: Editable?) {
filterString(s.toString())
}
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
}
})
}
})
}
However it returned an error:
Unresolved reference: add
This is the recyclerview Adapter:
RefundListAdapter.kt
class RefundListItemHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
private val refundedOrderId: TextView = itemView.refundedOrderId
private val orderDateTime: TextView = itemView.orderDateTime
private val refundedCustomerName: TextView = itemView.refundedCustomerName
fun bind(
refundHistory: RefundListPOJODataClassDataItem,
clickListener: RefundListOnItemClickListener
) {
refundedOrderId.text = refundHistory.orderId
orderDateTime.text = refundHistory.orderDate
refundedCustomerName.text = refundHistory.buyerName
itemView.setOnClickListener {
clickListener.onItemClicked(refundHistory)
}
}
}
class RefundListAdapter(
private var refundList: RefundListPOJODataClass,
private val itemClickListener: RefundListOnItemClickListener
) : RecyclerView.Adapter<RefundListItemHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RefundListItemHolder {
val v = LayoutInflater.from(parent.context)
.inflate(R.layout.refund_list_item_layout, parent, false)
return RefundListItemHolder(v)
}
override fun getItemCount(): Int {
if (refundList.data != null) {
return refundList.data!!.size
} else {
return 0
}
}
override fun onBindViewHolder(holder: RefundListItemHolder, position: Int) {
val refundHistory = refundList.data?.get(position)
if (refundHistory != null) {
holder.bind(refundHistory, itemClickListener)
}
}
fun filterList(filteredList: List<RefundListPOJODataClass>) { //This is the function I called in the fragment
refundList = filteredList.get(0)
notifyDataSetChanged()
}
}
interface RefundListOnItemClickListener {
fun onItemClicked(refundHistory: RefundListPOJODataClassDataItem)
}
And this is how the data class looks like
RefundListPOJODataClass.kt
data class RefundListPOJODataClass(
#field:SerializedName("data")
val data: List<RefundListPOJODataClassDataItem?>? = null,
#field:SerializedName("error")
val error: Error? = null
)
data class RefundListPOJODataClassError(
#field:SerializedName("msg")
val msg: String? = null,
#field:SerializedName("code")
val code: Int? = null,
#field:SerializedName("status")
val status: Boolean? = null
)
data class RefundListPOJODataClassDataItem(
#field:SerializedName("order_date")
val orderDate: String? = null,
#field:SerializedName("buyer_name")
val buyerName: String? = null,
#field:SerializedName("phone_number")
val phoneNumber: String? = null,
#field:SerializedName("status_refund")
val statusRefund: String? = null,
#field:SerializedName("order_id")
val orderId: String? = null,
#field:SerializedName("refund")
val refund: Int? = null,
#field:SerializedName("refund_date")
val refundDate: String? = null
)
I want to search the recyclerview item by the attribute buyerName. How can I achieve it ? What should i change in my code? If there's any detail I missed to tell, Just let me know.
Your filtered list is of type List, which is immutable.
Change it to array list or MutableList:
val filteredList: ArrayList<RefundListPOJODataClass> = ArrayList()