'How to put and retrieve parcelized objects using bundle and safeargs?
I am using Navigation and stumble upon safeargs. I encountered a problem which, based on my observation, has something to do with passing my custom object inside the bundle.
-used parcelable instead of serialization
-checked that I have same name for nav_graph xml arguments and the bundle key in which I pass it as putParcelable
Please check my code:
Inside my local headlines
newsAdapter.setOnItemClickListener { article ->
val bundle = Bundle().apply {
putParcelable("article", article)
Log.d(TAG, "success ---- $article")
}
findNavController().navigate(
R.id.action_localHeadlinesFragment_to_articleFragment, bundle
)
}
Here's my article fragment:
class ArticleFragment : Fragment() {
private lateinit var binding : FragmentArticleBinding
val args : ArticleFragmentArgs by navArgs()
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = FragmentArticleBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val article = args.article
//apply to webview
}
}
When I tried to remove the bundle parameter in my findController.navigate function, it navigates properly so my assumption is that the passing of values is the problem.
Editted: This is the error I am facing:
2022-04-28 10:34:25.592 9987-9987/com.fangzsx.news_app E/AndroidRuntime: FATAL EXCEPTION: main
Process: com.fangzsx.news_app, PID: 9987
java.lang.NullPointerException: Attempt to invoke virtual method 'int java.lang.String.hashCode()' on a null object reference
at com.fangzsx.news_app.model.Article.hashCode(Unknown Source:14)
at androidx.navigation.NavBackStackEntry.hashCode(NavBackStackEntry.kt:232)
at java.util.HashMap.hash(HashMap.java:338)
at java.util.HashMap.put(HashMap.java:611)
at androidx.navigation.NavController.linkChildToParent(NavController.kt:144)
at androidx.navigation.NavController.addEntryToBackStack(NavController.kt:1914)
at androidx.navigation.NavController.addEntryToBackStack$default(NavController.kt:1809)
at androidx.navigation.NavController$navigate$4.invoke(NavController.kt:1717)
at androidx.navigation.NavController$navigate$4.invoke(NavController.kt:1715)
at androidx.navigation.NavController$NavControllerNavigatorState.push(NavController.kt:288)
at androidx.navigation.fragment.FragmentNavigator.navigate(FragmentNavigator.kt:246)
at androidx.navigation.fragment.FragmentNavigator.navigate(FragmentNavigator.kt:162)
at androidx.navigation.NavController.navigateInternal(NavController.kt:261)
at androidx.navigation.NavController.navigate(NavController.kt:1715)
at androidx.navigation.NavController.navigate(NavController.kt:1541)
at androidx.navigation.NavController.navigate(NavController.kt:1468)
at androidx.navigation.NavController.navigate(NavController.kt:1926)
at com.fangzsx.news_app.ui.fragments.LocalHeadlinesFragment$onViewCreated$1.invoke(LocalHeadlinesFragment.kt:45)
at com.fangzsx.news_app.ui.fragments.LocalHeadlinesFragment$onViewCreated$1.invoke(LocalHeadlinesFragment.kt:43)
at com.fangzsx.news_app.adapters.NewsAdapter.onBindViewHolder$lambda-3$lambda-2(NewsAdapter.kt:50)
at com.fangzsx.news_app.adapters.NewsAdapter.$r8$lambda$uO6GOWfSmC-FltVST3LPazI9WMM(Unknown Source:0)
at com.fangzsx.news_app.adapters.NewsAdapter$$ExternalSyntheticLambda0.onClick(Unknown Source:4)
at android.view.View.performClick(View.java:7455)
at com.google.android.material.button.MaterialButton.performClick(MaterialButton.java:1131)
at android.view.View.performClickInternal(View.java:7432)
at android.view.View.access$3700(View.java:835)
at android.view.View$PerformClick.run(View.java:28810)
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:7866)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:550)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1003)
Adapter:
package com.fangzsx.news_app.adapters
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.AsyncListDiffer
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import coil.load
import com.fangzsx.news_app.databinding.NewsItemLayoutBinding
import com.fangzsx.news_app.model.Article
class NewsAdapter : RecyclerView.Adapter<NewsAdapter.ArticleViewHolder>() {
inner class ArticleViewHolder(val binding : NewsItemLayoutBinding) : RecyclerView.ViewHolder(binding.root)
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
}
}
var differ = AsyncListDiffer(this, differCallback)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ArticleViewHolder {
return ArticleViewHolder(
NewsItemLayoutBinding.inflate(
LayoutInflater.from(
parent.context
),
parent, false
)
)
}
override fun onBindViewHolder(holder: ArticleViewHolder, position: Int) {
val article = differ.currentList[position]
holder.binding.apply {
tvTitle.text = "\"${article.title}\""
tvDescription.text = article.description
ivImage.load(article.urlToImage){
crossfade(true)
crossfade(1000)
}
btnReadMore.setOnClickListener {
onItemClickListener?.let {
it(article)
}
}
}
}
private var onItemClickListener : ((Article) -> Unit)? = null
fun setOnItemClickListener(listener : (Article) -> Unit){
onItemClickListener = listener
}
override fun getItemCount(): Int {
return differ.currentList.size
}
}
Article Class:
package com.fangzsx.news_app.model
import android.os.Parcelable
import androidx.room.Entity
import androidx.room.PrimaryKey
import kotlinx.parcelize.Parcelize
@Entity(
tableName = "articles"
)
@Parcelize
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
) : Parcelable
Solution 1:[1]
I don't really see what is your issue because you didn't describe it. As I read through, your code should be working fine. And for your issue I assume that your Article class didn't implement Parcelable
that cause the app to crashes.
Fix it by implementing Parcelable
interface in your Article class or you can use kotlinx.parcelize.Parcelize
annotation.
In your app level build.gradle:
...
apply plugin: 'kotlin-parcelize'
...
Your Article class:
@Parcelize
data class Article(val title: String) : Parcelable
When you use Safe Args you can navigate to a destination with a generated Directions class:
val direction = LocalHeadlinesFragmentDirections.actionLocalHeadlinesFragmentToArticleFragment(article)
findNavController().navigate(direction)
And your navigation.xml
file:
<?xml version="1.0" encoding="utf-8"?>
<navigation
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/mobile_navigation"
app:startDestination="@+id/nav_local_headlines">
<fragment
android:id="@+id/nav_local_headlines"
android:name="...LocalHeadLine"
tools:layout="@layout/fragment_local_headlines">
<action
android:id="@+id/action_localHeadlinesFragment_to_articleFragment"
app:destination="@id/nav_article" />
</fragment
<fragment
android:id="@+id/nav_article"
android:name="...ArticleFragment"
tools:layout="@layout/fragment_article" >
<argument
android:name="article"
app:argType="...Article" />
</fragment>
</navigation>
As per the doc:
After enabling Safe Args, your generated code contains the following type-safe classes and methods for each action as well as with each sending and receiving destination.
A class is created for each destination where an action originates. The name of this class is the name of the originating destination, appended with the word "Directions". For example, if the originating destination is a fragment that is named SpecifyAmountFragment, the generated class would be called SpecifyAmountFragmentDirections.
This class has a method for each action defined in the originating destination.
For each action used to pass the argument, an inner class is created whose name is based on the action. For example, if the action is called confirmationAction, the class is named ConfirmationAction. If your action contains arguments without a defaultValue, then you use the associated action class to set the value of the arguments.
A class is created for the receiving destination. The name of this class is the name of the destination, appended with the word "Args". For example, if the destination fragment is named ConfirmationFragment, the generated class is called ConfirmationFragmentArgs. Use this class's fromBundle() method to retrieve the arguments.
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 |