Use Room + Paging3 + LazyColumn but pagination is not implemented - android-jetpack-compose

I have the following code, but I don't think the pagination is implemented.
dao
interface IArticleDao {
#Query(
"""
SELECT * FROM t_article ORDER BY :order DESC
"""
)
fun pagingSource(order: String): PagingSource<Int, Article>
}
Repository
class ArticleRepository #Inject constructor(
private val articleDao: IArticleDao
) {
fun list(order: String) = articleDao.pagingSource(order)
}
ViewModel
#HiltViewModel
class ArticleViewModel #Inject constructor(private val articleRepository: ArticleRepository) : ViewModel() {
fun list(order: String) = Pager(PagingConfig(pageSize = 20)){
articleRepository.list(order)
}.flow.cachedIn(viewModelScope)
}
Screen
val articleViewModel = hiltViewModel<ArticleViewModel>()
val lazyArticleItem = articleViewModel.list("id").collectAsLazyPagingItems()
ArticlePage(lazyArticleItem)
ArticlePage
LazyColumn{
items(...)
when(val state = lazyArticleItem.loadState.append){
is LoadState.Error -> {
println("error")
}
is LoadState.Loading -> {
println("${lazyArticleItem.itemCount}, ${lazyArticleItem.itemSnapshotList}")
}
else -> {}
}
}
lazyArticleItem.itemCount printed the number 668, so I don't think the pagination is working properly, but the data displayed on the UI interface is fine, it's just not paginated by 20 items per page.

In the PagingConfig, you can specify enablePlaceholders, which is true by default (your case). If placeholders are enabled and paging source knows the total number of items, which it knows when it takes data from room database, lazyArticleItem.itemSnapshotList size will be the total size of the source, only the elements that are not yet loaded will be null.
So you can't say that paging is not working based on itemCount. You are also printing itemSnapshotList, are there nulls? You can also try setting enablePlaceholders = false, itemCount then corresponds to the number of loaded items.

Related

Jetpack Compose collectAsState() does not work with Flow combine()

I want to load data from Firestore, and combine it with other data using Flow combine()
ViewModel:
private val userCurrentProject = MutableStateFlow("")
val projects = repository
.listenToProject() //listening via Firestore snapshot listener, no problem here
.combine(userCurrentProject) { projects, currentProjectName ->
// combine works and called normally
projects.map { project ->
project.apply {
isUserCurrentProject = name == currentProjectName
}
}
}
fun setCurrentProject(projectName: String) = viewModelScope.launch {
userCurrentProject.emit(projectName)
}
Composables:
fun ProjectListScreen(navController: NavHostController, viewModel: ProjectsViewModel) {
val projects by viewModel.projects.collectAsState(initial = emptyList())
// This is where the problem started
// Lazy column not updated when projects flow is emitting new value
// Even Timber log does not called
Timber.d("Projects : $projects")
LazyColumn {
items(projects) { project ->
ProjectItem(project = project) {
currentlySelectedProject = project
scope.launch { bottomSheetState.show() }
}
}
}
The flow is working normally, but the state never got updated, I don't know why. Maybe this is a problem with collectAsState()?
But the state is updated when I navigate to next screen (add new project screen), then press back (popBackStack)
NB: using asLiveData() with observeAsState() does not work either.
I've finally found the answer
The culprit is that a State of custom object/class behaves differently than a state of primitives (String, Int, etc.)
For a State of object, you need to use copy()
So I just changed this part of ViewModel
val projects = repository
.listenProject()
.combine(userCurrentProject) { projects, currentProjectName ->
projects.map { project ->
// use copy instead of apply
val isCurrentProject = project.name == currentProjectName
project.copy(isUserCurrentProject = isCurrentProject)
}
}

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).

Equivalent of tuples in Dart [duplicate]

Is there a way to return several values in a function return statement (other than returning an object) like we can do in Go (or some other languages)?
For example, in Go we can do:
func vals() (int, int) {
return 3, 7
}
Can this be done in Dart? Something like this:
int, String foo() {
return 42, "foobar";
}
Dart doesn't support multiple return values.
You can return an array,
List foo() {
return [42, "foobar"];
}
or if you want the values be typed use a Tuple class like the package https://pub.dartlang.org/packages/tuple provides.
See also either for a way to return a value or an error.
I'd like to add that one of the main use-cases for multiple return values in Go is error handling which Dart handle's in its own way with Exceptions and failed promises.
Of course this leaves a few other use-cases, so let's see how code looks when using explicit tuples:
import 'package:tuple/tuple.dart';
Tuple2<int, String> demo() {
return new Tuple2(42, "life is good");
}
void main() {
final result = demo();
if (result.item1 > 20) {
print(result.item2);
}
}
Not quite as concise, but it's clean and expressive code. What I like most about it is that it doesn't need to change much once your quick experimental project really takes off and you start adding features and need to add more structure to keep on top of things.
class FormatResult {
bool changed;
String result;
FormatResult(this.changed, this.result);
}
FormatResult powerFormatter(String text) {
bool changed = false;
String result = text;
// secret implementation magic
// ...
return new FormatResult(changed, result);
}
void main() {
String draftCode = "print('Hello World.');";
final reformatted = powerFormatter(draftCode);
if (reformatted.changed) {
// some expensive operation involving servers in the cloud.
}
}
So, yes, it's not much of an improvement over Java, but it works, it is clear, and reasonably efficient for building UIs. And I really like how I can quickly hack things together (sometimes starting on DartPad in a break at work) and then add structure later when I know that the project will live on and grow.
Create a class:
import 'dart:core';
class Tuple<T1, T2> {
final T1 item1;
final T2 item2;
Tuple({
this.item1,
this.item2,
});
factory Tuple.fromJson(Map<String, dynamic> json) {
return Tuple(
item1: json['item1'],
item2: json['item2'],
);
}
}
Call it however you want!
Tuple<double, double>(i1, i2);
or
Tuple<double, double>.fromJson(jsonData);
You can create a class to return multiple values
Ej:
class NewClass {
final int number;
final String text;
NewClass(this.number, this.text);
}
Function that generates the values:
NewClass buildValues() {
return NewClass(42, 'foobar');
}
Print:
void printValues() {
print('${this.buildValues().number} ${this.buildValues().text}');
// 42 foobar
}
The proper way to return multiple values would be to store those values in a class, whether your own custom class or a Tuple. However, defining a separate class for every function is very inconvenient, and using Tuples can be error-prone since the members won't have meaningful names.
Another (admittedly gross and not very Dart-istic) approach is try to mimic the output-parameter approach typically used by C and C++. For example:
class OutputParameter<T> {
T value;
OutputParameter(this.value);
}
void foo(
OutputParameter<int> intOut,
OutputParameter<String>? optionalStringOut,
) {
intOut.value = 42;
optionalStringOut?.value = 'foobar';
}
void main() {
var theInt = OutputParameter(0);
var theString = OutputParameter('');
foo(theInt, theString);
print(theInt.value); // Prints: 42
print(theString.value); // Prints: foobar
}
It certainly can be a bit inconvenient for callers to have to use variable.value everywhere, but in some cases it might be worth the trade-off.
you can use dartz package for Returning multiple data types
https://www.youtube.com/watch?v=8yMXUC4W1cc&t=110s
Dart is finalizing records, a fancier tuple essentially.
Should be in a stable release a month from the time of writing.
I'll try to update, it's already available with experiments flags.
you can use Set<Object> for returning multiple values,
Set<object> foo() {
return {'my string',0}
}
print(foo().first) //prints 'my string'
print(foo().last) //prints 0
In this type of situation in Dart, an easy solution could return a list then accessing the returned list as per your requirement. You can access the specific value by the index or the whole list by a simple for loop.
List func() {
return [false, 30, "Ashraful"];
}
void main() {
final list = func();
// to access specific list item
var item = list[2];
// to check runtime type
print(item.runtimeType);
// to access the whole list
for(int i=0; i<list.length; i++) {
print(list[i]);
}
}

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)
}

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).

Resources