Paging3 - How to do Reverse Pagination in Chat App - android-paging-3

I am recently migrate to Paging3. However, I noticed that most of the tutorials and guides are mainly on normal pagination (from top to bottom).
I need to implement the REVERSE pagination as user scroll to top boundary, will load for page 2,3,4..
Is there any tutorial/guide for this?
PS: Now the initial loading is working fine, but when I scroll to top-most, I have no idea how to load Page 2 data.
My current approach
PagingSource
class ChatPagingSource(
private val apiService: ApiService,
private val roomId: String
): PagingSource<Int, Message>() {
override fun getRefreshKey(state: PagingState<Int, Message>): Int? = null
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Message> {
return try{
val page = params.key?: 1
val pageSize = params.loadSize
val call = apiService.getMessageFeedPaging(
room_id = roomId,
page = page,
max = pageSize,
exclude = EXCLUDE_TYPE
)
val repoItems = call.messages
val prevKey = if(page > 1) page - 1 else null
val nextKey = if(repoItems.isNotEmpty()) page + 1 else null
if(repoItems.isNotEmpty()) {
val messageList = mutableListOf<Message>()
for (i in repoItems) {
val replyData = Converters.convertReplyDataAPItoReplyData(i.reply_data)
val msg = Converters.convertMessageAPItoMessage(replyData, i, hasError = false)
messageList.add(msg)
}
LoadResult.Page(messageList, prevKey, nextKey)
} else {
LoadResult.Page(listOf(), prevKey, nextKey)
}
}catch (e: Exception) {
LoadResult.Error(e)
}
}
}
Repository
fun loadRemoteMessageStream(roomId: String): LiveData<PagingData<Message>> {
return Pager(
config = PagingConfig(20),
pagingSourceFactory = { ChatPagingSource(apiService, roomId) }
).liveData
}
ViewModel
private val _remoteMessage = chatRepository.loadRemoteMessageStream(currentRoomId)
.cachedIn(viewModelScope)
.let { it as MutableLiveData<PagingData<Message>> }
val remoteMessage: LiveData<PagingData<Message>> = _remoteMessage
Fragment
chatViewModel.remoteMessage.observe(viewLifecycleOwner, {
chatAdapter.submitData(viewLifecycleOwner.lifecycle, it)
})

In case this helps anyone, I will post out my own answer.
The key is to reverse prevKey and nextKey and fixed the pageSize that your API required (in my case is 20).
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Message> {
val pageSize = 20
val prevKey = if(repoItems.isNotEmpty()) page + 1 else null
val nextKey = if(page > 1) page - 1 else null
}
Then, in the recyclerview, you should use stackFromEnd = true so that initially the recyclerview will auto-scroll to bottom.

Related

Adding document İD to firebase document as a field

I have a model with default values. My app gets the data from user through EditTexts and add them to Firebase Firestore. I hava an addData function (in AddAnalyzeActivity) and savefunction (in AddAnalyzeViewModel) for this operation. I'm getting EditText entries in AddAnalyzeActivity and adding them to my model but on this step ı want to add document id to my model but I can't access the documentIds properly in AddAnalyzeActivity. I can only access them with a forEach method when I try to retrieving the mentioned data with retrieveData function (in PairDetailVM) from Firestore but If I try to add document Ids in retrieveData method it only adds default value of documentId.
What I tried to:
Using #DocumentId annotation in my model.
Setting null default value of documentId in my model.
Getting a list of all documents' ids but can't match them with actual items.
Here is the screenShot for logic:
AnalyzeModel:
data class AnalyzeModel(
var concept: String?="",
var reason: String?="",
var result: String?="",
var rrRatio: Double?=0.0,
var tarih: Timestamp=Timestamp.now(),
var tradingViewUrl: String="",
var id : String="")
addData :
fun addData(view: View) {
val tarih = com.google.firebase.Timestamp.now()
val rr = rrText.text.toString()
var doubleRR = rr.toDoubleOrNull()
if (doubleRR == null) { doubleRR = 0.0 }
val analyzeDTO = AnalyzeModel(
conceptText.text.toString(),
reasonForText.text.toString(),
resultAddingText.text.toString(),
doubleRR,
tarih,
chartImage.text.toString()
)
viewModel.save(analyzeDTO)
val intent = Intent(this, PairDetailActivity::class.java)
startActivity(intent)
finish()
}
save :
fun save(data: AnalyzeModel) {
database.collection(dbCollection!!).document("Specified").collection("Pairs")
.document(chosenPair!!)
.collection("Analysis")
.add(data)
.addOnFailureListener { exception ->
exception.printStackTrace()
Toast.makeText(getApplication(), exception.localizedMessage, Toast.LENGTH_LONG).show()
}
}
retrieveData:
private fun retrieveData() {
val docRef = collectionRef.orderBy("tarih", Query.Direction.DESCENDING)
docRef.addSnapshotListener { value, error ->
try {
if (value != null && !value.isEmpty) {
val allAnalysis= ArrayList<AnalyzeModel>()
val documents = value.documents
documents.forEach {
val analyze = it.toObject(AnalyzeModel::class.java)
if (analyze!=null){
allAnalysis.add(analyze)
}
}
list.value = allAnalysis
} else if (error != null) {
Toast.makeText(Application(), error.localizedMessage, Toast.LENGTH_LONG).show()
}
} catch (e: Exception) {
e.printStackTrace()
}
}
}
If you want to save the document ID into the document itself, consider separating the creation of the new DocumentReference from writing to it, by using set instead of add.
fun save(data: AnalyzeModel) {
val newRef = database.collection(dbCollection!!).document("Specified").collection("Pairs")
.document(chosenPair!!)
.collection("Analysis")
.document() // 👈 generates a new reference with a unique ID
data.id = newRef.id // 👈 set the ID into your object
newRef.set(data) // 👈 writes the data to the new reference
.addOnFailureListener { exception ->
exception.printStackTrace()
Toast.makeText(getApplication(), exception.localizedMessage, Toast.LENGTH_LONG).show()
}
}
Also see the second snippet in the documentation on adding a document

firebase Storage sync

I am trying to get the image_list in Uri format.
In the process of retrieving photos from firebase storage, a coroutine was used to solve the async.
private fun test() {
val storageReference =
FirebaseStorage.getInstance().reference.child("Ballad_images")
val temp_time = ArrayList<String>()
val temp_time_3 = ArrayList<String>()
val image_list = ArrayList<String>()
storageReference.listAll().addOnSuccessListener { listResult ->
GlobalScope.launch() {
val job1 = launch {
for (fileRef in listResult.items) {
fileRef.downloadUrl.addOnSuccessListener { Uri ->
image_list.add(Uri.toString())
image_list.sort()
}
}
}
val job2 = launch {
for (fileRef in listResult.items) {
temp_time.add(fileRef.name)
}//for
for (i in 0..temp_time.size - 1) {
var temp_time_2 = temp_time.get(i).split('.')//2021 05 29 -1 jpg
temp_time_3.add(temp_time_2[1])
//05
}//for
}
runBlocking {
job1.join()
println("image_list : " + image_list.size)
job2.join()
println("temp_time : " + temp_time_3)
//delay(4000L)
println("image_list : " + image_list.size)
}
}//GlobalScope
}
}
end job1.join()
After finishing job2.join()
If you give it a delay, the data will come out.
When I see it, in join1
I think this is happening because of fileRef.downloadUrl.addOnSuccessListener
Is there a way to populate the image_list from job1?

How to sum of values from Firebase Database in Kotlin?

I have a Firebase Database with several purchases in Android app, points value is assigned for each purchase, then entries are showing by following way:
When userId is log in, a button appears and when user click on it the "showInfoClient" function is called and it must show user and the amount of points. Code is:
private val database = Firebase.database
private val myref = database.getReference("compras")
fun showInfoClient(view: View) {
val userlog = FirebaseAuth.getInstance().currentUser?.displayName
val alertDialogInfo = AlertDialog.Builder(this).create()
myref.orderByChild("userId").equalTo(userlog).addListenerForSingleValueEvent(object :
ValueEventListener {
override fun onDataChange(dataSnapshot: DataSnapshot) {
var sum = 0
for (data in dataSnapshot.children) {
sum += data.child("puntos").getValue(Int::class.java)!!
Toast.makeText(this#Principal, sum, Toast.LENGTH_SHORT).show()
}
}
override fun onCancelled(databaseError: DatabaseError) {}
})
alertDialogInfo.setTitle(userlog)
alertDialogInfo.setMessage("Puntos: ") // + sum
alertDialogInfo.setButton(
AlertDialog.BUTTON_POSITIVE, "OK"
) { dialog, _ ->; dialog.dismiss() }
alertDialogInfo.show()
val btnPositive = alertDialogInfo.getButton(AlertDialog.BUTTON_POSITIVE)
val layoutParams = btnPositive.layoutParams as LinearLayout.LayoutParams
layoutParams.weight = 100f
btnPositive.layoutParams = layoutParams
}
I have tried to use different options but i´m not able to set "sum" value on
alertDialogInfo.setMessage("Puntos: $sum")
Thanks in advance
Any code that needs data from the database needs to be inside onDataChange or be called from there. Code outside of onDataChange will (most likely) run before the data is loaded.
So:
val userlog = FirebaseAuth.getInstance().currentUser?.displayName
val alertDialogInfo = AlertDialog.Builder(this).create()
myref.orderByChild("userId").equalTo(userlog).addListenerForSingleValueEvent(object :
ValueEventListener {
override fun onDataChange(dataSnapshot: DataSnapshot) {
var sum = 0
for (data in dataSnapshot.children) {
sum += data.child("puntos").getValue(Int::class.java)!!
}
alertDialogInfo.setTitle(userlog)
alertDialogInfo.setMessage("Puntos: ") // + sum
alertDialogInfo.setButton(
AlertDialog.BUTTON_POSITIVE, "OK"
) { dialog, _ ->; dialog.dismiss() }
alertDialogInfo.show()
val btnPositive = alertDialogInfo.getButton(AlertDialog.BUTTON_POSITIVE)
val layoutParams = btnPositive.layoutParams as LinearLayout.LayoutParams
layoutParams.weight = 100f
btnPositive.layoutParams = layoutParams
}
override fun onCancelled(databaseError: DatabaseError) {
throw databaseError.toException(); // never ignore errors
}
})
For a longer explanation see:
getContactsFromFirebase() method return an empty list
Setting Singleton property value in Firebase Listener

Fragment already added issue, when using navigation with bottom navigation bar android

I have a bottom navigation view which I have implemented in androidx navigation, but the problem is it is showing me the error of
java.lang.IllegalStateException: Fragment already added: BaseFragment{27d5b00 (2156a830-7756-4fc9-bc63-7c6f3d6705f0) id=0x7f08008c android:switcher:2131230860:0}
I have a base fragment which provides views to different fragments
class MainActivity : AppCompatActivity(),
ViewPager.OnPageChangeListener,
BottomNavigationView.OnNavigationItemReselectedListener,
BottomNavigationView.OnNavigationItemSelectedListener {
// overall back stack of containers
private val backStack = Stack<Int>()
// list of base destination containers
private val fragments = listOf(
BaseFragment.newInstance(R.layout.content_home_base, R.id.toolbar_home, R.id.nav_host_home),
BaseFragment.newInstance(R.layout.content_library_base, R.id.toolbar_library, R.id.nav_host_library),
BaseFragment.newInstance(R.layout.content_settings_base, R.id.toolbar_settings, R.id.nav_host_settings))
// map of navigation_id to container index
private val indexToPage = mapOf(0 to R.id.home, 1 to R.id.library, 2 to R.id.settings)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// setup main view pager
main_pager.addOnPageChangeListener(this)
main_pager.adapter = ViewPagerAdapter()
main_pager.post(this::checkDeepLink)
main_pager.offscreenPageLimit = fragments.size
// set bottom nav
bottom_nav.setOnNavigationItemSelectedListener(this)
bottom_nav.setOnNavigationItemReselectedListener(this)
// initialize backStack with elements
if (backStack.empty()) backStack.push(0)
}
/// BottomNavigationView ItemSelected Implementation
override fun onNavigationItemSelected(item: MenuItem): Boolean {
val position = indexToPage.values.indexOf(item.itemId)
if (main_pager.currentItem != position) setItem(position)
return true
}
override fun onNavigationItemReselected(item: MenuItem) {
val position = indexToPage.values.indexOf(item.itemId)
val fragment = fragments[position]
fragment.popToRoot()
}
override fun onBackPressed() {
val fragment = fragments[main_pager.currentItem]
val hadNestedFragments = fragment.onBackPressed()
// if no fragments were popped
if (!hadNestedFragments) {
if (backStack.size > 1) {
// remove current position from stack
backStack.pop()
// set the next item in stack as current
main_pager.currentItem = backStack.peek()
} else super.onBackPressed()
}
}
/// OnPageSelected Listener Implementation
override fun onPageScrollStateChanged(state: Int) {}
override fun onPageScrolled(p0: Int, p1: Float, p2: Int) {}
override fun onPageSelected(page: Int) {
val itemId = indexToPage[page] ?: R.id.home
if (bottom_nav.selectedItemId != itemId) bottom_nav.selectedItemId = itemId
}
private fun setItem(position: Int) {
main_pager.currentItem = position
backStack.push(position)
}
private fun checkDeepLink() {
fragments.forEachIndexed { index, fragment ->
val hasDeepLink = fragment.handleDeepLink(intent)
if (hasDeepLink) setItem(index)
}
}
inner class ViewPagerAdapter : FragmentPagerAdapter(supportFragmentManager) {
override fun getItem(position: Int): Fragment = fragments[position]
override fun getCount(): Int = fragments.size
}
}
Base Fragment Class :
class BaseFragment: Fragment() {
private val defaultInt = -1
private var layoutRes: Int = -1
private var toolbarId: Int = -1
private var navHostId: Int = -1
private val appBarConfig = AppBarConfiguration(rootDestinations)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
arguments?.let {
layoutRes = it.getInt(KEY_LAYOUT)
toolbarId = it.getInt(KEY_TOOLBAR)
navHostId = it.getInt(KEY_NAV_HOST)
} ?: return
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
// Inflate the layout for this fragment
return if (layoutRes == defaultInt) null
else inflater.inflate(layoutRes, container, false)
}
override fun onStart() {
super.onStart()
// return early if no arguments were parsed
if (toolbarId == defaultInt || navHostId == defaultInt) return
// setup navigation with toolbar
val toolbar = requireActivity().findViewById<Toolbar>(toolbarId)
val navController = requireActivity().findNavController(navHostId)
NavigationUI.setupWithNavController(toolbar, navController, appBarConfig)
// NavigationUI.setupWithNavController(toolbar,navController)
}
fun onBackPressed(): Boolean {
return requireActivity()
.findNavController(navHostId)
.navigateUp(appBarConfig)
}
fun popToRoot() {
val navController = requireActivity().findNavController(navHostId)
navController.popBackStack(navController.graph.startDestination, false)
}
fun handleDeepLink(intent: Intent) = requireActivity().findNavController(navHostId).handleDeepLink(intent)
companion object {
private const val KEY_LAYOUT = "layout_key"
private const val KEY_TOOLBAR = "toolbar_key"
private const val KEY_NAV_HOST = "nav_host_key"
fun newInstance(layoutRes: Int, toolbarId: Int, navHostId: Int) = BaseFragment().apply {
arguments = Bundle().apply {
putInt(KEY_LAYOUT, layoutRes)
putInt(KEY_TOOLBAR, toolbarId)
putInt(KEY_NAV_HOST, navHostId)
}
}
}
}
I tried empty the stack first but that did not work, i have three navgraphs for my three viewpager elements/ fragments.
We reverted back back to 2.1.0 (nav library) and it worked again
Error is not quite connected with Navigation library. It's most probably fragment's error itself. Try testing that fragment in empty activity without using Navigation.
Navigation library is somehow treating error wrongly.
p.s. in my case it was just and view's id missing in layout file.
Thanks for the support, I found the solution I created different navigation graphs for each page.

Play framework: How to process server errors in HTTP filters?

I am trying to create an HTTP filter that logs some info about the request, say headers, and a limited (so the memory doesn't explode) part of the request body in case of an error.
To do that I've followed the docs (https://www.playframework.com/documentation/2.6.x/ScalaHttpFilters) and came up with something like this:
class RequestErrorLogFilter #Inject()(actorSystem: ActorSystem)(implicit ec: ExecutionContext)
extends EssentialFilter {
private val logger = org.slf4j.LoggerFactory.getLogger("application.AccumulatorFlowFilter")
private implicit val logging = Logging(actorSystem.eventStream, logger.getName)
override def apply(next: EssentialAction): EssentialAction = new EssentialAction {
override def apply(request: RequestHeader): Accumulator[ByteString, Result] = {
val accumulator: Accumulator[ByteString, Result] = next(request)
val data = ArrayBuffer.empty[ByteString]
var totalSize = 0
val maxSize = 1024
val flow: Flow[ByteString, ByteString, NotUsed] = Flow[ByteString]
.map((in: ByteString) => {
val left = maxSize - totalSize
if (left > 0) {
val takeNow =
if (in.size > left) {
in.slice(0, left)
} else {
in
}
data.append(takeNow)
totalSize += takeNow.size
}
in
})
val accumulatorWithResult = accumulator.through(flow).map { result =>
// this code doesn't get executed in case of an exception in a controller
logger.info(s"The flow has completed and the result is $result")
if (result.header.status >= 400) {
val headerText = data.map(_.utf8String).mkString("")
logger.warn(s"There was an error. Request head: $headerText")
}
result
}
accumulatorWithResult
}
}
}
This works fine for client errors (like 400 - bad request), or for any error returned from a controller, but in case of an exception inside of a controller, filter's "callback" isn't executed, so there's no opportunity to log what happened.
And there's a same problem with a much simpler "AccessLogHttpFilter" which I think is a recommended solution to create an access log with play applications:
class LoggingFilter #Inject() (val mat: Materializer, implicit val ec: ExecutionContext)
extends Filter {
def apply(nextFilter: RequestHeader => Future[Result])
(requestHeader: RequestHeader): Future[Result] = {
val startTime = System.currentTimeMillis
nextFilter(requestHeader).map { result =>
val endTime = System.currentTimeMillis
val requestTime = endTime - startTime
Logger.info(s"${requestHeader.method} ${requestHeader.uri} took ${requestTime}ms and " +
s"returned ${result.header.status}")
result.withHeaders("Request-Time" -> requestTime.toString)
}
}
}
Is there a way to make play invoke http filter code even in case of exceptions?
Is there some other workaround?
Figured it out.
For an EssentialFilter to handle an error you need to add a .recover() call to an accumulator:
class RequestErrorLogFilter #Inject()(actorSystem: ActorSystem)(implicit ec: ExecutionContext)
extends EssentialFilter {
private val logger = org.slf4j.LoggerFactory.getLogger("application.AccumulatorFlowFilter")
private implicit val logging = Logging(actorSystem.eventStream, logger.getName)
override def apply(next: EssentialAction): EssentialAction = new EssentialAction {
override def apply(request: RequestHeader): Accumulator[ByteString, Result] = {
val accumulator: Accumulator[ByteString, Result] = next(request)
val data = ArrayBuffer.empty[ByteString]
var totalSize = 0
val maxSize = 1024
val flow: Flow[ByteString, ByteString, NotUsed] = Flow[ByteString]
.map((in: ByteString) => {
val left = maxSize - totalSize
if (left > 0) {
val takeNow =
if (in.size > left) {
in.slice(0, left)
} else {
in
}
data.append(takeNow)
totalSize += takeNow.size
}
in
})
val accumulatorWithResult: Accumulator[ByteString, Result] = accumulator.through(flow).map { result =>
logger.info(s"The flow has completed and the result is $result")
if (result.header.status >= 400) {
val headerText = data.map(_.utf8String).mkString("")
logger.warn(s"There was an error. Request head: $headerText")
}
result
}
accumulatorWithResult.recover {
case error =>
val headerText = data.map(_.utf8String).mkString("")
logger.warn(s"There was an error: $error. Request head: $headerText")
throw error
}
}
}
}
And for a simple Filter you need a .failed.foreach call on the result future:
class LoggingFilter #Inject() (val mat: Materializer, implicit val ec: ExecutionContext)
extends Filter {
def apply(nextFilter: RequestHeader => Future[Result])
(requestHeader: RequestHeader): Future[Result] = {
val startTime = System.currentTimeMillis
val eventualResult = nextFilter(requestHeader)
eventualResult.failed.foreach { error: Throwable =>
val endTime = System.currentTimeMillis
val requestTime = endTime - startTime
Logger.info(s"${requestHeader.method} ${requestHeader.uri} took ${requestTime}ms and " +
s"returned 500 $error")
}
eventualResult.map { result =>
val endTime = System.currentTimeMillis
val requestTime = endTime - startTime
Logger.info(s"${requestHeader.method} ${requestHeader.uri} took ${requestTime}ms and " +
s"returned ${result.header.status}")
result.withHeaders("Request-Time" -> requestTime.toString)
}
}
}

Resources