How to properly use LiveData Transformation? - android-livedata

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

Related

Compose: LazyColumn recomposes all items on single item update

I am trying to show a list of Orders in a list using LazyColumn. Here is the code:
#Composable
private fun MyOrders(
orders: List<Order>?,
onClick: (String, OrderStatus) -> Unit
) {
orders?.let {
LazyColumn {
items(
items = it,
key = { it.id }
) {
OrderDetails(it, onClick)
}
}
}
}
#Composable
private fun OrderDetails(
order: Order,
onClick: (String, OrderStatus) -> Unit
) {
println("Composing Order Item")
// Item Code Here
}
Here is the way, I call the composable:
orderVm.fetchOrders()
val state by orderVm.state.collectAsState(OrderState.Empty)
if (state.orders.isNotEmpty()) {
MyOrders(state.orders) {
// Handle status change click listener
}
}
I fetch all my orders and show in the LazyColumn. However, when a single order is updated, the entire LazyColumn gets rrecomposed. Here is my ViewModel looks like:
class OrderViewModel(
fetchrderUseCase: FetechOrdersUseCase,
updateStatusUseCase: UpdateorderUseCase
) {
val state = MutableStateFlow(OrderState.Empty)
fun fetchOrders() {
fetchrderUseCase().collect {
state.value = state.value.copy(orders = it.data)
}
}
fun updateStatus(newStatus: OrderStatus) {
updateStatusUseCase(newStatus).collect {
val oldOrders = status.value.orders
status.value = status.value.copy(orders = finalizeOrders(oldOrders))
}
}
}
NOTE: The finalizeOrders() does some list manipulation based on orderId to update one order with the updated one.
This is how my state looks like:
data class OrderState(
val orders: List<Order> = listOf(),
val isLoading: Boolean = false,
val error: String = ""
) {
companion object {
val Empty = FetchOrdersState()
}
}
If I have 10 orders in my DB and I update one's status (let's say 5th item), then OrderDetails gets called for 20 times. Not sure why. Caan I optimize it to make sure only the 5th indexed item will be recomposed and the OrderDetals gets called only with the new order.
Is the Orderclasss stable? If not it could be the reason why all the items get recomposed:
Compose skips the recomposition of a composable if all the inputs are stable and haven't changed. The comparison uses the equals method
This section in the compose's doc explains what are stable types and how to skip recomposition.
Note: If you scroll a lazy list, all invisible items will be destroyed. That means if you scroll back they will be recreated not recomposed (you can't skip recreation even if the input is stable).

How to pass object in navigation in 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)
}

Get count of Observable array before returning it

In my ViewModel file I have an observable array created after applying map on it. Now before returning it I want to check if it has any content or not. If there is nothing in there I want to return it without applying map. Following is my code:
func retrieveDeals(location: CLLocation?) -> Observable<[SaleItem]> {
let specials = nearestFlightSpecials.retrieveNearestFlightSpecials(userLocation: location)
let happyHourDeals = specials.map {
$0.filter { $0.isHappyHour }
}
return happyHourDeals
}
Before I return happyHourDeals I want to check if it contains any element or not. The above array is subscribed in view but I don't want to apply the above logic there. I want to keep it here in ViewModel.
I suspect what you want to do is filter out empty output:
func retrieveDeals(location: CLLocation?) -> Observable<[SaleItem]> {
let specials = nearestFlightSpecials.retrieveNearestFlightSpecials(userLocation: location)
let happyHourDeals = specials.map {
$0.filter { $0.isHappyHour }
}
.filter { !$0.isEmpty } // this is the line you need.
return happyHourDeals
}
Terminology is important here. Observables don't "contain" values. Observables don't return values, they emit events.
Your happyHourDeals will still be returned but with the filter line, it will no longer emit empty arrays. What this means is that whatever is subscribed to the value returned will not be updated if specials.map { $0.filter { $0.isHappyHour } } emits an empty array.

How do I add an external array into my Rx Observable?

I have an Observable and would like it to filter elements that exists in an external array. The problem is that an element obviously can't be compared directly to an array, so I loop the array and then make the comparison. This doesn't work because the return statement needs to be outside of the for loop.
func scanAndFilterCoreData() -> Observable<BleHandler.BlePeripheral> {
let request = NSFetchRequest<LocalDoorCoreDataObject>(entityName: "LocalDoorCoreDataObject")
let result = self.coreDataHandler.fetchAll(fetchRequest: request)
return bleHandler.scan(serviceId: AppSettings.discoverServiceId)
.flatMap{ Observable.from($0) }
.filter { value in
for coreData in result {
return value.peripheral.identifier.uuidString == coreData.dPeripheralId
}
}
}
Being new to Rx I'm thinking there has to be some way to include the external array into the Observable as a second parameter, or how is this done?
You should use map instead of filter:
.map { value in
let uuidString = value.peripheral.identifier.uuidString
return result.filter { $0.dPeripheralId == uuidString }
}

Swift getter + array initialization

How can I do this right? (Error: Variable used within its own initial value):
private var cars = [Car]() {
get { return cars }
}
...works for:
private var connection:Bool! {
get { return connection }
}
Your variable
cars
is a computed property. That been said, it doesnt store any value. For your example you will need to create a variable. Lest called it
private var localCars = [Car]()
This variable is the one that will store all the values you need and then you can use the computed property to get all the information you need from cars like this.
private var cars:Cars! {
get { return localCars }
}
You can also use the set in your computed propert to store the value to localCars
private var cars:Cars! {
get { return localCars }
set { localCars = newValue }
}
If you want to learn what its a computer property check this page. It explains them really well.

Resources