How to pass object in navigation in jetpack compose? - android-jetpack-compose

From the documentation, I can pass string, integer etc. But how can I pass objects on navigation?
Note: If I set the argument type parcelable then the app crashes with java.lang.UnsupportedOperationException: Parcelables don't support default values..
composable(
"vendor/details/{vendor}",
arguments = listOf(navArgument("vendor") {
type = NavType.ParcelableType(Vendor::class.java)
})
) {
// ...
}

The following workarounds based on navigation-compose version 2.4.0-alpha05.
I found 2 workarounds for passing objects.
1. Convert the object into JSON string:
Here we can pass the objects using the JSON string representation of the object.
Example code:
val ROUTE_USER_DETAILS = "user-details/user={user}"
// Pass data (I am using Moshi here)
val user = User(id = 1, name = "John Doe") // User is a data class.
val moshi = Moshi.Builder().build()
val jsonAdapter = moshi.adapter(User::class.java).lenient()
val userJson = jsonAdapter.toJson(user)
navController.navigate(
ROUTE_USER_DETAILS.replace("{user}", userJson)
)
// Receive Data
NavHost {
composable(ROUTE_USER_DETAILS) { backStackEntry ->
val userJson = backStackEntry.arguments?.getString("user")
val moshi = Moshi.Builder().build()
val jsonAdapter = moshi.adapter(User::class.java).lenient()
val userObject = jsonAdapter.fromJson(userJson)
UserDetailsView(userObject) // Here UserDetailsView is a composable.
}
}
// Composable function/view
#Composable
fun UserDetailsView(
user: User
){
// ...
}
2. Passing the object using NavBackStackEntry:
Here we can pass data using navController.currentBackStackEntry and receive data using navController.previousBackStackEntry.
Example code:
val ROUTE_USER_DETAILS = "user-details/{user}"
// Pass data
val user = User(id = 1, name = "John Doe") // User is a parcelable data class.
navController.currentBackStackEntry?.arguments?.putParcelable("user", user)
navController.navigate(ROUTE_USER_DETAILS)
// Receive data
NavHost {
composable(ROUTE_USER_DETAILS) { backStackEntry ->
val userObject = navController.previousBackStackEntry?.arguments?.getParcelable<User>("user")
UserDetailsView(userObject) // Here UserDetailsView is a composable.
}
}
// Composable function/view
#Composable
fun UserDetailsView(
user: User
){
// ...
}
Important Note: The 2nd solution will not work if we pop up back stacks on navigate.

Parcelables currently don't support default values so you need to pass your object as String value. Yes it is a work around.. So instead of passing object itself as Parcelize object we can turn that object into JSON (String) and pass it through navigation and then parse that JSON back to Object at destination. You can use GSON for object to json string conversion...
Json To Object
fun <A> String.fromJson(type: Class<A>): A {
return Gson().fromJson(this, type)
}
Object To Json String
fun <A> A.toJson(): String? {
return Gson().toJson(this)
}
User NavType.StringType instead of NavType.ParcelableType..
composable("detail/{item}",
arguments = listOf(navArgument("item") {
type = NavType.StringType
})
) {
it.arguments?.getString("item")?.let { jsonString ->
val user = jsonString.fromJson(User::class.java)
DetailScreen( navController = navController, user = user )
}
}
Now navigate by passing string..
val userString = user.toJson()
navController.navigate(detail/$userString")
EDIT: There is also a limit for the Json-String that you can navigate. If the length of the Json-String is tooo long then the NavController won't recognize your Composable Route eventually throw an exception... Another work around would be to use a Global Variable and set its value in before navigating.. then pass this value as arguments in your Composable Functions..
var globalUser : User? = null // global object somewhere in your code
.....
.....
// While Navigating
click { user->
globalUser = user
navController.navigate(detail/$userString")
}
// Composable
composable( "detail") {
DetailScreen(
navController = navController,
globalUser)
}
NOTE :-> ViewModels can also be used to achieve this..

Let me give you very simple answers.
We have different options like.
Using Arguments but issue with this is that you can't share long or complex objects, only simple types like Int, String, etc.
Now you are thinking about converting objects to JsonString and trying to pass it, but this trick only works for small or easy objects.
Exception look like this:
java.lang.IllegalArgumentException: Navigation destination that matches request NavDeepLinkRequest{ uri="VERY LONG OBJECT STRING" } cannot be found in the navigation graph NavGraph(0x0) startDestination={Destination(0x2e9fc7db) route=Screen_A}
Now we have a Parsable Type in navArgument, but we need to put that object in current backStack and need to retrieve from next screen. The problem with this solution is you need keep that screen in your backStack. You can't PopOut your backStack. Like, if you want to popout your Login Screen when you navigate to Main Screen, then you can't retrieve Object from Login Screen to Main Screen.
You need to Create SharedViewModel. Make sure you only use shared state and only use this technique when above two are not suitable for you.

With Arguments:
You can just make this object Serializable and pass it to the backStackEntry arguments, also you can pass String, Long etc :
data class User (val name:String) : java.io.Serializable
val user = User("Bob")
navController.currentBackStackEntry?.arguments?.apply {
putString("your_key", "key value")
putSerializable("USER", user)
)
}
to get value from arguments you need to do next:
navController.previousBackStackEntry?.arguments?.customGetSerializable("USER")
code for customGetSerializable function:
#Suppress("DEPRECATION")
inline fun <reified T : Serializable> Bundle.customGetSerializable(key: String): T? {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) getSerializable(key, T::class.java)
else getSerializable(key) as? T
}
With savedStateHandle
Sometimes you have nullable arguments, so you can use savedStateHandle:
appState.navController.currentBackStackEntry?.savedStateHandle?.set("USER", user)
and get value:
navController.previousBackStackEntry?.savedStateHandle?.get("USER")

#HiltViewModel
class JobViewModel : ViewModel() {
var jobs by mutableStateOf<Job?>(null)
private set
fun allJob(job:Job)
{
Toast.makeText(context,"addJob ${job.companyName}", Toast.LENGTH_LONG).show()
jobs=job
}
#Composable
fun HomeNavGraph(navController: NavHostController,
) {
val jobViewModel:JobViewModel= viewModel() // note:- same jobViewModel pass
in argument because instance should be same , otherwise value will null
val context = LocalContext.current
NavHost(
navController = navController,
startDestination = NavigationItems.Jobs.route
) {
composable(
route = NavigationItems.Jobs.route
) {
JobsScreen(navController,jobViewModel)
}
composable(
route= NavigationItems.JobDescriptionScreen.route
)
{
JobDescriptionScreen(jobViewModel=jobViewModel)
}
}
}
}
in function argument (jobViewModel: JobViewModel)
items(lazyJobItems) {
job -> Surface(modifier = Modifier.clickable {
if (job != null) {
jobViewModel.allJob(job=job)
navController.navigate(NavigationItems.JobDescriptionScreen.route)
}

Related

How to use Sealed Class Data from Flow in Swift in Kotlin Mutliplatform Mobile?

I unable to access the sealed class data observed in a Flow from Swift ..
sealed class NetworkStatus<T> {
data class Loading<T>(var loading: Boolean) : NetworkStatus<T> ()
data class CustomSignal<T>(var signal: String) : NetworkStatus<T>()
data class CustomSignalDetailed<T>(var signal: ErrorCaseData) : NetworkStatus<T>()
data class Failure<T>(val e: Throwable) : NetworkStatus<T>()
data class Data<T> (val data: T ) : NetworkStatus<T>()
companion object {
fun <T>loading(isLoading: Boolean): NetworkStatus<T> = Loading(isLoading)
fun <T> customstatus(signal: String): NetworkStatus<T> = CustomSignal(signal)
fun <T> customStatusDetailed(signals: ErrorCaseData): NetworkStatus<T> = CustomSignalDetailed(signals)
fun <T> failure(e: Throwable): NetworkStatus<T> = Failure(e)
fun <T> data(data: T): NetworkStatus<T> = Data<T>(data)
}
}
https://gist.github.com/RageshAntony/a4fd357973485b5fb8aef0e189ee9e7e
In the above gist ....
In MainActivity.kt , I used sealed class in flow for Data
Now I need the same in Swift but it's confusing
Even I used something as CFlow wrapping.. still confusion
In Swift , I can't get object type only as nsarray and also NetworkStatus.data is not accessible
.. I tried manual typecast (in line 8 , contentview.swift )
Still data object don't have any type ...
Please help me how to implement the same flow in Swift like in MainActivity.kt
Start from this sealed class declared in the shared code:
sealed class KMMResult<out Value>
data class SuccessKMMResult<Value>(
val value: Value
): KMMResult<Value>()
data class ErrorKMMResult<Value>(
val throwable: Throwable
): KMMResult<Value>()
and this method that randomly returns you a KMMResult with Success or Failure:
fun getRandomIntWrappedInResult(): KMMResult<Int> {
val isSuccess = Random.nextBoolean()
return if(isSuccess) {
SuccessKMMResult(Random.nextInt(until = 10))
} else {
ErrorKMMResult(RuntimeException("There was an error, Int not generated"))
}
}
You can use it like this on Android:
val randomInt: KMMResult<Int> = getRandomIntWrappedInResult()
val randomIntText: String = when (randomInt) {
is KMMResult.ErrorKMMResult -> {
"Error: ${randomInt.throwable.message}"
}
is KMMResult.SuccessKMMResult -> {
"Success: ${randomInt.value}"
}
}
and like this on iOS:
let randomInt: KMMResult<KotlinInt> = RandomNumberGeneratorKt.getRandomIntWrappedInIntResult()
let randomIntText: String
switch randomInt {
case let error as KMMResultErrorKMMResult<KotlinInt>:
randomIntText = "Error: \(error.throwable.message ?? error.throwable.description())"
case let success as KMMResultSuccessKMMResult<KotlinInt>:
randomIntText = "Success: \(success.value)"
default:
randomIntText = "This never happens"
}
This is not the best solution, I suggest you to create a Swift enum like:
enum SwiftResult<Value> {
case error(String)
case success(Value)
}
and convert KMMResult to SwiftResult using:
func toSwiftResult<Value>(kmmResult: KMMResult<Value>) -> SwiftResult<Value> {
if let successResult = kmmResult as? KMMResultSuccessKMMResult<Value> {
return SwiftResult.success(successResult.value!)
}
if let errorResult = kmmResult as? KMMResultErrorKMMResult {
return SwiftResult.error(errorResult.throwable.message ?? errorResult.throwable.description())
}
return SwiftResult.error("Unexpected error converting to SwiftResult")
}
Unfortunately, you need to do this manually for each sealed class, so I suggest you to use the library moko-kswift that does this job for you.
P.S. Avoid use List<Something> as a generic type because Something will be erased and when seeing the type from Swift you will just see NSArray. You can use two classes, one for single item and one for List, still generic on Something, or you can create a wrapper data class around your list, say ListWrapper, so the type won't be erased, because your original List<Something> would be inside ListWrapper which will be the type for NetworkStatus.
So use NetworkStatus<ListWrapper<Something>> instead of NetworkStatus<List<Something>>
Another P.S. Your function getAllCategories() in the gist is returning a flow, so it shouldn't be a suspend function.

Kotlin No setter/field for Value found on class Object warning for Firebase

I'm trying to fetch all messages data inside my ChatMessage data class from my Firebase Database. In the logs, I can see that data is indeed fetched inside my Snapshot, but it isn't getting assigned to my variables in the data class. I'm new to Kotlin & Firebase, so I don't really understand why is it happening?
After I did some logs searching I found out that I have been receiving this warning:
W/ClassMapper: No setter/field for -Llk34LtqGPJ3bwPrYRi found on class com.idiotboxes.socialowl.models.ChatMessage
Did some searching for this error here & found it is a very common problem indeed. But my issue, none of the solutions have a data class format like mine.
I understand that the warning is saying I need to setup getters & setters in my data class, but I'm fairly new to Kotlin & I don't know how to implement getter-setters in my data class.
Here's how my data class ChatMessage looks like:
package com.idiotboxes.socialowl.models
data class ChatMessage(val messageID: String, val fromID: String, val toID:
String,val message: String, val timeStamp: Long){
//No argument constructor
constructor(): this("","","","",-1)
}
Here is how my Firebase Database node looks like:
Database Structure
EDIT: Here's extra code to help you understand where the problem could be
My Callback interface
interface FirebaseCallback {
fun onCallback(latestMessage: ChatMessage)
}
My function which reads the Data from Firebase
private fun readFirebaseData(firebaseCallback: FirebaseCallback){
//Signed In user's ID
val fromID = FirebaseAuth.getInstance().uid
Log.w(TAG, "from ID is: $fromID (Currently Signed-In user)")
//Get Database Reference
val msgRef = FirebaseDatabase.getInstance().getReference("/Latest-messages/$fromID"/* Reference to current user's database entry */)
//Set a Listener for new messages
msgRef.addChildEventListener(object: ChildEventListener {
//adds new row when someone messages you for the first time, i.e. New Child is Added
override fun onChildAdded(p0: DataSnapshot, p1: String?) {
Log.w(TAG, "Snapshot captured from Firebase Database is: $p0")
//Convert snapshot to messages
//val latestMessage = p0.getValue(ChatMessage::class.java) ?: return
val callbackData: ChatMessage = p0.getValue(ChatMessage::class.java) ?: return
//TODO Implement Callback
firebaseCallback.onCallback(callbackData)
}
//Updates existing rows latest messages when user receives new message i.e. Latest Message child is Changed.
override fun onChildChanged(p0: DataSnapshot, p1: String?) {
val latestMessage: ChatMessage = p0.getValue(ChatMessage::class.java) ?: return//If Null then return
//Update the Existing row with new message
latestMessagesHashMap[p0.key!!] = latestMessage
updateRecyclerView()
}
--Some redundant methods of ChildEventListener--
})
latest_messages_recycler_view.adapter = adapter
//Recycler View Bottom Line Border
latest_messages_recycler_view.addItemDecoration(DividerItemDecoration(activity, DividerItemDecoration.VERTICAL))
}
Function where I attempt to retrieve data from my callback
private fun showLatestMessages(){
readFirebaseData(object : FirebaseCallback{
override fun onCallback(latestMessage: ChatMessage) {
//TODO latestMessage is not null. But blank values are being filled in the Chat Message model class
Log.w(TAG, "NotNull latestMessage values are fromID: ${latestMessage.fromID} toID: ${latestMessage.toID} Typed Message: ${latestMessage.message} TimeStamp: ${latestMessage.timeStamp}")
//Add the new message row
adapter.add(LatestMessagesItems(latestMessage, context ?: return))
}
})
//Set OnClick Listener on Recycler View Items
adapter.setOnItemClickListener { item, view ->
//Grab the User details from the item that is clicked.
val userItem = item as LatestMessagesItems
//Start Chat Activity with the clicked User
val startChat = Intent(activity, ChatActivity::class.java)
startChat.putExtra(USER_KEY, userItem.recipientUser)
startActivity(startChat)
}
}
private fun updateRecyclerView(){
//Clear existing rows
adapter.clear()
//Fetch all Latest Messages from HashMap
latestMessagesHashMap.values.forEach {
adapter.add(LatestMessagesItems(it, context?: return))
}
}
My messages item for my recyclerview
class LatestMessagesItems(val latestMessage: ChatMessage, val ctx: Context): Item<ViewHolder>(){
lateinit var recipientUser: User
override fun getLayout(): Int {
return R.layout.latest_message_row
}
override fun bind(viewHolder: ViewHolder, position: Int) {
Log.w(TAG, "Fetched latest Message is: $latestMessage")
//Null Check
val recipientID: String? = if (latestMessage.fromID == FirebaseAuth.getInstance().uid) {
latestMessage.toID
} else {
latestMessage.fromID
}
//Fetch the recipient user details
val fetchRef = FirebaseDatabase.getInstance().getReference("/users/$recipientID")
fetchRef.addListenerForSingleValueEvent(object: ValueEventListener{
override fun onDataChange(p0: DataSnapshot) {
Log.w(TAG, "Fetched Recipient ID is: $recipientID")
//Fetch user details in User model class
recipientUser = p0.getValue(User::class.java) ?: return
Log.w(TAG, "Fetched Recipient User Name is: ${recipientUser.username}")
//Fill the User Details
viewHolder.itemView.recipient_username.text = recipientUser.username //Username
Glide.with(ctx).load(recipientUser.profileImage).into(viewHolder.itemView.recipient_profile_image) //Profile Pic
}
override fun onCancelled(p0: DatabaseError) {
}
})
//Latest Message Received
viewHolder.itemView.latest_received_message.text = latestMessage.message
}
}
And Finally I updated my Model Class according to some of the suggestions posted here.
ChatMessage.kt
package com.idiotboxes.socialowl.models
data class ChatMessage(var messageID: String? = null, var fromID: String? = null , var toID: String? = null ,var message: String? = null , var timeStamp: Long? = null){
//No argument constructor
constructor(): this("","","","",-1)
}
Yet, the problem still persists.
I can't say for sure it's the case but I think that you should write to variables (var), not values (val).
This is because Kotlin automatically generates getters/setters for var, which is considered mutable, and doesn't for val which is considered immutable.
The same happens for private vars as Kotlin doesn't provide getters/setters for them outside their local scope (e.g. for Firebase to access them).

How to properly use LiveData Transformation?

I have a MutableLiveData
val myMutableDataList = MutableLiveData<List<LiveData<Task>>>().apply {
value = emptyList()
}
I have an ArrayList which contains LiveData
var list:ArrayList<LiveData<Task>>
whenever an item of my list is changed I want to send that list tomyMutableDataList and notifi that it was changed.
Currently I use this
` list.forEach {
it.observeForever {
myMutableDataList.postValue(list)
}
}`
Transformation gives me an error

Swift: Convert Foo<String> to Foo<Any>

here is my code
class Foo<T> {
}
class Main {
static func test() {
var foo: Foo<Any>
var bar = Foo<String>()
//Do all my stuff with foo necessitating String
foo = bar
}
}
When I try to assign foo = bar I get an error Cannot assign a value of type Foo<String> to a value of type Foo<Any>.
I don't understand why I got this error, since String conforms to Any.
If I do exactly the same thing but using Array, I don't have any error
static func test() {
var foo: Array<Any>
var bar = Array<String>()
//Do all my stuff with foo necessitating String
foo = bar
}
Does anyone know what's wrong with my code?
Thanks
Arrays and dictionaries are special types that have this kind of behavior built in. This does, however, not apply to custom generic types. The type Foo<Any> is not a supertype (or in this case superclass) of Foo<String> even though Any is a supertype of String. Therefore, you cannot assign variables of these types to each other.
Depending on your particular case the solution outlined in Swift Cast Generics Type might work for you. When Foo wraps a value of type T you can add a generic constructor that converts the value from a differently typed instance of Foo.
class Foo<T> {
var val: T
init(val: T) {
self.val = val
}
init<O>(other: Foo<O>) {
self.val = other.val as! T
}
}
class Main {
static func test() {
var foo: Foo<Any>
var bar = Foo<String>(val: "abc")
//Do all my stuff with foo necessitating String
foo = Foo<Any>(other: bar)
}
}
I have improved on #hennes answer by adding the ability to define how you want to convert from type O to type T
class Foo<T> {
var val: T
init(val: T) {
self.val = val
}
init<O>(other:Foo<O>, conversion:(O) -> T) {
self.val = conversion(other.val)
}
}
class Main {
static func test() {
var foo: Foo<Int>
var bar = Foo<String>(val: "10")
//Do all my stuff with foo necessitating String
foo = Foo<Int>(other: bar, conversion: {(val) in
return Int(val.toInt()!)
})
print(foo.val) //prints 10
print(foo.val.dynamicType) //prints Swift.Int
}
}
This gives you the ability to convert between two types that don't support casting to each other. Additionally, it gives a compiler warning when the conversion is illegal, as opposed to crashing due to the forced cast.
No idea why there is a difference in compiler warnings for 1 or >1 line closures but there are.
Bad compiler warning for 1-line closure
Good compiler warning for 2-line closure

Is it possible to generalize and/or simplifiy these two "computed properties"

I have a swift project where I'm using Notifications to pass data around. When I post the notification I'm encoding ["data" : data ] in the message
data is of the type ParsedMessage of which I have multiple subtypes that follow from it.
When I receive the notification I'm using the block style method to deal with the notification. I did this because I wanted to try to avoid the issues with misspelling or forgetting add the : at the end of the selector method. I'm starting to think I'll end up with clearer code if I use selectors but as I'm exploring some of the swift funkiness I wanted to try this route at least.
So in any case I have two blocks to handle data which subclasses ParsedMessage, that being AHRSMessage and LocationMessage
// Define a block to be called by the notification center
lazy var ahrsDataHandler : notificationBlock = { notification in
if let d : Dictionary = notification?.userInfo,
msg : AHRSMessage = d["data"] as? AHRSMessage
{
println (msg.roll)
self.expectation1!.fulfill()
} else {
println("\(__FILE__)::\(__LINE__)Could not parse AHRS Data correctly")
}
}
// Define a block to be called by the notification center
lazy var locationDataHandlerBlock : notificationBlock = { notification in
if let d : Dictionary = notification?.userInfo,
msg : LocationMessage = d["data"] as? LocationMessage
{
println("Latitude: \(msg.lat)")
self.expectation1!.fulfill()
} else {
println("\(__FILE__)::\(__LINE__)Could not parse Location Data correctly")
}
}
Eventually these two computed properties are passed into a call to:
addObserverForName(...) which takes a block.
Is there a way to use Generics and/or something i missed to simplify this code?
Calling if let d : Dictionary = notification?.userInfo,
msg : AHRSMessage = d["data"] as? AHRSMessage seems kind of unweildly
I'm wondering if there is some function I can construct where I could pass in a closure and a type or something and it would "generate" a similar block here to what I've created.
func generateNotificationBlock<LocationMessage>() or something but I've just ended up confusing myself.
Thanks
So I went the other direction and made a class extension to Notifications which worked great!
/**
These class extensions make it easier to work with Notificationss
*/
extension NSNotification {
/**
If the notificaiton has a UserInfo field that is equal to ["data" : AHRSMessage ], this function
wiill extract the Location message otherwise it will return nil
:returns: userInfo["data"] extracted as a AHRSMessage
*/
var asAHRSMessage : AHRSMessage? {
get { return self.asParsedMessage() }
}
/**
If the notificaiton has a UserInfo field that is equal to ["data" : LocationMessage ], this function
wiill extract the Location message otherwise it will return nil
:returns: userInfo["data"] extracted as a LocationMessage
*/
var asLocationMessage : LocationMessage? {
get {
return self.asParsedMessage()
}
}
/**
If the notificaiton has a UserInfo field that is equal to ["data" : ParsedMessage ], this function
wiill extract the Location message otherwise it will return nil
:returns: userInfo["data"] extracted as a ParsedMessage
*/
private func asParsedMessage<T>() -> T? {
if let d : Dictionary = self.userInfo,
msg : T = d["data"] as? T
{
return msg
}
return nil
}
}

Resources