Android Paging library 3 throws exception when a new item is inserted while scrolling - android-recyclerview

I am developing an Android chat app with Paging library 3 androidx.paging:paging-runtime-ktx:3.0.0-alpha12
The paging library retrieves chat messages from the Room database.
When I insert a new chat message to the ChatMessage table in the Room database, while scrolling recycler view of chat message, it throws an exception as per GIF below. The send button just inserts a new item into the ChatMessage table.
This is the error logs.
E/AndroidRuntime: FATAL EXCEPTION: main
Process: com.beeswork.balance, PID: 24171
java.lang.UnsupportedOperationException: Stable ids are unsupported on PagingDataAdapter.
at androidx.paging.PagingDataAdapter.setHasStableIds(PagingDataAdapter.kt:127)
at com.beeswork.balance.ui.chat.ChatFragment.setupChatRecyclerView(ChatFragment.kt:71)
at com.beeswork.balance.ui.chat.ChatFragment.access$setupChatRecyclerView(ChatFragment.kt:24)
at com.beeswork.balance.ui.chat.ChatFragment$bindUI$1.invokeSuspend(ChatFragment.kt:58)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
at android.os.Handler.handleCallback(Handler.java:751)
at android.os.Handler.dispatchMessage(Handler.java:95)
at android.os.Looper.loop(Looper.java:154)
at android.app.ActivityThread.main(ActivityThread.java:6077)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:866)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:756)
Disconnected from the target VM, address: 'localhost', transport: 'socket'
This is the code to setup the chat recycler view in Chat fragment
private fun setupChatRecyclerView() {
chatPagingAdapter = ChatPagingAdapter()
rvChat.adapter = chatPagingAdapter
val layoutManager = LinearLayoutManager(this#ChatFragment.context)
layoutManager.orientation = LinearLayoutManager.VERTICAL
layoutManager.reverseLayout = true
rvChat.layoutManager = layoutManager
lifecycleScope.launch {
viewModel.chatMessages.collectLatest {
chatPagingAdapter.submitData(it)
}
}
}
This is the chatMessages in ChatViewModel
val chatMessages = Pager(
PagingConfig(
pageSize = 30,
enablePlaceholders = false,
maxSize = 150
)
) {
balanceRepository.getChatMessages(chatId)
}.flow.cachedIn(viewModelScope)
This is the query in ChatDAO
#Query("select * from chatMessage where chatId = :chatId order by case when id is null then 0 else 1 end, id desc, messageId desc")
fun getChatMessages(chatId: Long): PagingSource<Int, ChatMessage>
This is the ChatPagingAdapter extends PagingDataAdapter
class ChatPagingAdapter : PagingDataAdapter<ChatMessage, ChatPagingAdapter.MessageViewHolder>(
diffCallback
) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MessageViewHolder {
return when (viewType) {
ChatMessage.Status.RECEIVED.ordinal -> MessageViewHolder(parent.inflate(R.layout.item_chat_message_received))
ChatMessage.Status.SENT.ordinal -> MessageViewHolder(parent.inflate(R.layout.item_chat_message_sent))
else -> MessageViewHolder(parent.inflate(R.layout.item_chat_message_received))
}
}
override fun onBindViewHolder(holder: MessageViewHolder, position: Int) {
getItem(position)?.let {
when (holder.itemViewType) {
ChatMessage.Status.RECEIVED.ordinal -> holder.bindMessageReceived(it)
ChatMessage.Status.SENT.ordinal -> holder.bindMessageSent(it)
}
}
}
override fun getItemViewType(position: Int): Int {
return getItem(position)?.let {
return if (it.status == ChatMessage.Status.RECEIVED) ChatMessage.Status.RECEIVED.ordinal else ChatMessage.Status.SENT.ordinal
} ?: kotlin.run {
return ChatMessage.Status.RECEIVED.ordinal
}
}
companion object {
private val diffCallback = object : DiffUtil.ItemCallback<ChatMessage>() {
override fun areItemsTheSame(oldItem: ChatMessage, newItem: ChatMessage): Boolean =
oldItem.id == newItem.id
override fun areContentsTheSame(oldItem: ChatMessage, newItem: ChatMessage): Boolean =
oldItem == newItem
}
}
class MessageViewHolder(
itemView: View
) : RecyclerView.ViewHolder(itemView) {
fun bind {...}
}
}
Here are questions
Can I keep my scrolling while inserting a new item to chat message table please?
If not possible, then how can I resolve the exception please?

Related

Kotlin: I have a problem while switching from one Activity to another?

I have a problem when I want to switch between two activities.
This is my code on the first activity. I want to switch to the CatDoorActivity.
modifier = Modifier.clickable {
val intent = Intent(this, CatDoorActivity::class.java)
intent.putExtra("UUID", deviceUUID).putExtra("TITLE", deviceTitle)
this.startActivity(intent)
}
My Problem is this exception after running that code:
android.content.ActivityNotFoundException: Unable to find explicit activity class {com.example.mycatbellproject/com.example.mycatbellproject.CatDoorActivity}; have you declared this activity in your AndroidManifest.xml?
at android.app.Instrumentation.checkStartActivityResult(Instrumentation.java:2065)
at android.app.Instrumentation.execStartActivity(Instrumentation.java:1727)
at android.app.Activity.startActivityForResult(Activity.java:5314)
at androidx.activity.ComponentActivity.startActivityForResult(ComponentActivity.java:705)
at android.app.Activity.startActivityForResult(Activity.java:5272)
at androidx.activity.ComponentActivity.startActivityForResult(ComponentActivity.java:686)
at android.app.Activity.startActivity(Activity.java:5658)
at android.app.Activity.startActivity(Activity.java:5611)
at com.example.mycatbellproject.MainActivity$CardItem$1.invoke(MainActivity.kt:255)
at com.example.mycatbellproject.MainActivity$CardItem$1.invoke(MainActivity.kt:254)
at androidx.compose.foundation.ClickableKt$clickable$4$gesture$1$2.invoke-k-4lQ0M(Clickable.kt:153)
at androidx.compose.foundation.ClickableKt$clickable$4$gesture$1$2.invoke(Clickable.kt:142)
at androidx.compose.foundation.gestures.TapGestureDetectorKt$detectTapAndPress$2$1$1.invokeSuspend(TapGestureDetector.kt:223)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at kotlinx.coroutines.DispatchedTaskKt.resume(DispatchedTask.kt:178)
at kotlinx.coroutines.DispatchedTaskKt.dispatch(DispatchedTask.kt:166)
at kotlinx.coroutines.CancellableContinuationImpl.dispatchResume(CancellableContinuationImpl.kt:397)
at kotlinx.coroutines.CancellableContinuationImpl.resumeImpl(CancellableContinuationImpl.kt:431)
at kotlinx.coroutines.CancellableContinuationImpl.resumeImpl$default(CancellableContinuationImpl.kt:420)
at kotlinx.coroutines.CancellableContinuationImpl.resumeWith(CancellableContinuationImpl.kt:328)
at androidx.compose.ui.input.pointer.SuspendingPointerInputFilter$PointerEventHandlerCoroutine.offerPointerEvent(SuspendingPointerInputFilter.kt:511)
at androidx.compose.ui.input.pointer.SuspendingPointerInputFilter.dispatchPointerEvent(SuspendingPointerInputFilter.kt:406)
at androidx.compose.ui.input.pointer.SuspendingPointerInputFilter.onPointerEvent-H0pRuoY(SuspendingPointerInputFilter.kt:419)
at androidx.compose.ui.input.pointer.Node.dispatchMainEventPass(HitPathTracker.kt:310)
at androidx.compose.ui.input.pointer.Node.dispatchMainEventPass(HitPathTracker.kt:297)
at androidx.compose.ui.input.pointer.Node.dispatchMainEventPass(HitPathTracker.kt:297)
at androidx.compose.ui.input.pointer.Node.dispatchMainEventPass(HitPathTracker.kt:297)
at androidx.compose.ui.input.pointer.Node.dispatchMainEventPass(HitPathTracker.kt:297)
at androidx.compose.ui.input.pointer.NodeParent.dispatchMainEventPass(HitPathTracker.kt:179)
at androidx.compose.ui.input.pointer.HitPathTracker.dispatchChanges(HitPathTracker.kt:98)
at androidx.compose.ui.input.pointer.PointerInputEventProcessor.process-BIzXfog(PointerInputEventProcessor.kt:80)
at androidx.compose.ui.platform.AndroidComposeView.sendMotionEvent-8iAsVTc(AndroidComposeView.android.kt:1205)
at androidx.compose.ui.platform.AndroidComposeView.handleMotionEvent-8iAsVTc(AndroidComposeView.android.kt:1155)
at androidx.compose.ui.platform.AndroidComposeView.dispatchTouchEvent(AndroidComposeView.android.kt:1095)
at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:3118)
at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2799)
at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:3118)
at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2799)
at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:3118)
at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2799)
at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:3118)
E/AndroidRuntime: at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2799)
at com.android.internal.policy.DecorView.superDispatchTouchEvent(DecorView.java:488)
at com.android.internal.policy.PhoneWindow.superDispatchTouchEvent(PhoneWindow.java:1871)
at android.app.Activity.dispatchTouchEvent(Activity.java:4125)
at com.android.internal.policy.DecorView.dispatchTouchEvent(DecorView.java:446)
at android.view.View.dispatchPointerEvent(View.java:14568)
at android.view.ViewRootImpl$ViewPostImeInputStage.processPointerEvent(ViewRootImpl.java:6016)
at android.view.ViewRootImpl$ViewPostImeInputStage.onProcess(ViewRootImpl.java:5819)
at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:5310)
at android.view.ViewRootImpl$InputStage.onDeliverToNext(ViewRootImpl.java:5367)
at android.view.ViewRootImpl$InputStage.forward(ViewRootImpl.java:5333)
at android.view.ViewRootImpl$AsyncInputStage.forward(ViewRootImpl.java:5485)
at android.view.ViewRootImpl$InputStage.apply(ViewRootImpl.java:5341)
at android.view.ViewRootImpl$AsyncInputStage.apply(ViewRootImpl.java:5542)
at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:5314)
at android.view.ViewRootImpl$InputStage.onDeliverToNext(ViewRootImpl.java:5367)
at android.view.ViewRootImpl$InputStage.forward(ViewRootImpl.java:5333)
at android.view.ViewRootImpl$InputStage.apply(ViewRootImpl.java:5341)
at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:5314)
at android.view.ViewRootImpl.deliverInputEvent(ViewRootImpl.java:8080)
at android.view.ViewRootImpl.doProcessInputEvents(ViewRootImpl.java:8031)
at android.view.ViewRootImpl.enqueueInputEvent(ViewRootImpl.java:7992)
at android.view.ViewRootImpl$WindowInputEventReceiver.onInputEvent(ViewRootImpl.java:8203)
at android.view.InputEventReceiver.dispatchInputEvent(InputEventReceiver.java:220)
at android.os.MessageQueue.nativePollOnce(Native Method)
at android.os.MessageQueue.next(MessageQueue.java:335)
at android.os.Looper.loop(Looper.java:183)
at android.app.ActivityThread.main(ActivityThread.java:7656)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:592)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:947)
Suppressed: kotlinx.coroutines.DiagnosticCoroutineContextException: [androidx.compose.runtime.BroadcastFrameClock#f19bf6c, StandaloneCoroutine{Cancelling}#fd19935, AndroidUiDispatcher#cea91ca]
I/Process: Sending signal. PID: 7109 SIG: 9
I have added the activity to the AndroidManifest.xml:
<activity
android:name=".CatDoorActivity"
android:configChanges="orientation"
android:exported="false"
android:theme="#style/Theme.MyCatbellProject"/>
The bottom of the exception says something about coroutines. I use them in for my room database. Do I get a problem intending a new activity and switching to it while using coroutines in my room database in the first activity? I use the room database viewmodel in my first activity but I dont use that on my second activity.
That is my second activity so far:
class CatDoorActivity : ComponentActivity(){
private var uuid: String = ""
private var title: String = ""
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if(intent.extras != null) {
uuid = intent.extras!!.get("UUID") as String
title = intent.extras!!.get("TITLE") as String
}
setContent {
MyCatbellProjectTheme {
// A surface container using the 'background' color from the theme
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colors.background
) {
MenuScreenUI()
}
}
}
}
...
The Repository of my room db uses the coroutines:
class DeviceRepository(application: Application) {
val searchResults = MutableLiveData<List<Device>>()
private var deviceDao: DeviceDao?
private val coroutineScope = CoroutineScope(Dispatchers.Main)
val allDevices: LiveData<List<Device>>?
fun insertDevice(newDevice: Device) {
coroutineScope.launch(Dispatchers.IO) {
asyncInsert(newDevice)
}
}
private suspend fun asyncInsert(device: Device) {
deviceDao?.addDevice(device)
}
fun deleteDevice(id: Int) {
coroutineScope.launch(Dispatchers.IO) {
asyncDelete(id)
}
}
private suspend fun asyncDelete(id: Int) {
deviceDao?.deleteDevice(id)
}
fun findDevice(name: String) {
coroutineScope.launch(Dispatchers.Main) {
searchResults.value = asyncFind(name).await()
}
}
private suspend fun asyncFind(name: String): Deferred<List<Device>?> =
coroutineScope.async(Dispatchers.IO) {
return#async deviceDao?.findDevice(name)
}
init {
val db: DeviceRoomDatabase? = DeviceRoomDatabase.getDatabse(application)
deviceDao = db?.deviceDao()
allDevices = deviceDao?.getAllDevices()
}
}

Application Crashes Accepting Photo In Landscape Mode

I've got an activity which uses camera intent. If I take a picture vertically and accept to display it in an imageview vertically there is no problem. If i take a picture horizontally and accept it while having the mobile horizontally it crashes, but if I turn it to vertical it works. Does anybody know why this might be happening?
It does not have much sense cause when the app crashes the only thing it says is that the photoFile is null, it seems as if there was no picture but in fact there is a picture.
Here is the code from the activity:
private const val FILE_NAME = "photo.jpg"
class TextCameraActivity : AppCompatActivity(), TextToSpeech.OnInitListener {
private lateinit var binding: ActivityTextCameraBinding
private lateinit var bitmap : Bitmap
private lateinit var photoFile: File
private var tts: TextToSpeech? = null
private var locale : Locale = Locale("es", "ES")
private var progressBar : ProgressBar? = null
private var i = 0
private val handler = Handler()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityTextCameraBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.btn7.setOnClickListener {
val takePictureIntent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
photoFile = getPhotoFile(FILE_NAME)
val fileProvider = FileProvider.getUriForFile(this, "com.example.fileprovider", photoFile)
takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, fileProvider)
if(takePictureIntent.resolveActivity(this.packageManager) != null) {
startActivityForResult(takePictureIntent, 42)
} else {
Toast.makeText(this, "Unable to open camera", Toast.LENGTH_SHORT).show()
Log.d("mensaje", "?????Unable to open camera")
}
}
tts = TextToSpeech(this, this)
progressBar = binding.progressBar
binding.btnReconocerImagen.setOnClickListener {
progressBar!!.visibility = View.VISIBLE
i = progressBar!!.progress
Thread(Runnable {
while (i < 10) {
i += 1
handler.post(Runnable {
progressBar!!.progress = i
})
try {
Thread.sleep(100)
} catch (e: InterruptedException) {
e.printStackTrace()
}
}
progressBar!!.visibility = View.INVISIBLE
}).start()
recognizeText()
}
val actionBar = supportActionBar
actionBar?.setDisplayHomeAsUpEnabled(true)
}
private fun getPhotoFile(fileName:String):File {
val storageDirectory = getExternalFilesDir(Environment.DIRECTORY_PICTURES)
return File.createTempFile(fileName, ".jpg", storageDirectory)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.getItemId()) {
android.R.id.home -> {
finish()
return true
}
}
return super.onOptionsItemSelected(item)
}
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
return true
}
override fun onInit(status: Int) {
if (status == TextToSpeech.SUCCESS) {
// set US English as language for tts
val result = tts!!.setLanguage(locale)
if (result == TextToSpeech.LANG_MISSING_DATA || result == TextToSpeech.LANG_NOT_SUPPORTED) {
Log.e("TTS","The Language specified is not supported!")
} else {
binding.btnReconocerImagen.isEnabled = true
}
} else {
Log.e("TTS", "Initilization Failed!")
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if(requestCode == 42 && resultCode == Activity.RESULT_OK) {
bitmap = BitmapFactory.decodeFile(photoFile.absolutePath)
binding.imageView.setImageBitmap(bitmap)
} else {
super.onActivityResult(requestCode, resultCode, data)
}
}
private fun recognizeText() {
try {
val image = InputImage.fromBitmap(bitmap, 0)
val recognizer = TextRecognition.getClient(TextRecognizerOptions.DEFAULT_OPTIONS)
val result = recognizer.process(image)
.addOnSuccessListener { visionText ->
for (block in visionText.textBlocks) {
val boundingBox = block.boundingBox
val cornerPoints = block.cornerPoints
val text = block.text
Log.d("mensaje", "he encontrado $text")
binding.tvTextoReconocido.text = "El texto reconocido es: $text"
tts!!.speak("El texto reconocido es $text", TextToSpeech.QUEUE_FLUSH, null, "")
for (line in block.lines) {
// ...
for (element in line.elements) {
// ...
}
}
}
}
.addOnFailureListener { e ->
}
} catch (e : Exception) {
Log.d("mensaje", "NO HAY IMAGEN SELECCIONADA")
Toast.makeText(this,"NO HAS SELECCIONADO NINGUNA IMAGEN PARA RECONOCER", Toast.LENGTH_SHORT).show()
}
}
}
Error:
E/AndroidRuntime: FATAL EXCEPTION: main
Process: com.example.appdigitalinkrecognition, PID: 30407
java.lang.RuntimeException: Unable to resume activity {com.example.appdigitalinkrecognition/com.example.appdigitalinkrecognition.TextCameraActivity}: java.lang.RuntimeException: Failure delivering result ResultInfo{who=null, request=42, result=-1, data=null} to activity {com.example.appdigitalinkrecognition/com.example.appdigitalinkrecognition.TextCameraActivity}: kotlin.UninitializedPropertyAccessException: lateinit property photoFile has not been initialized
at android.app.ActivityThread.performResumeActivity(ActivityThread.java:4918)
at android.app.ActivityThread.handleResumeActivity(ActivityThread.java:4955)
at android.app.servertransaction.ResumeActivityItem.execute(ResumeActivityItem.java:52)
at android.app.servertransaction.TransactionExecutor.executeLifecycleState(TransactionExecutor.java:176)
at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:97)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2336)
at android.os.Handler.dispatchMessage(Handler.java:106)
at android.os.Looper.loop(Looper.java:246)
at android.app.ActivityThread.main(ActivityThread.java:8653)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:602)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1130)
Caused by: java.lang.RuntimeException: Failure delivering result ResultInfo{who=null, request=42, result=-1, data=null} to activity {com.example.appdigitalinkrecognition/com.example.appdigitalinkrecognition.TextCameraActivity}: kotlin.UninitializedPropertyAccessException: lateinit property photoFile has not been initialized
at android.app.ActivityThread.deliverResults(ActivityThread.java:5590)
at android.app.ActivityThread.performResumeActivity(ActivityThread.java:4905)
at android.app.ActivityThread.handleResumeActivity(ActivityThread.java:4955) 
at android.app.servertransaction.ResumeActivityItem.execute(ResumeActivityItem.java:52) 
at android.app.servertransaction.TransactionExecutor.executeLifecycleState(TransactionExecutor.java:176) 
at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:97) 
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2336) 
at android.os.Handler.dispatchMessage(Handler.java:106) 
at android.os.Looper.loop(Looper.java:246) 
at android.app.ActivityThread.main(ActivityThread.java:8653) 
at java.lang.reflect.Method.invoke(Native Method) 
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:602) 
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1130) 
Caused by: kotlin.UninitializedPropertyAccessException: lateinit property photoFile has not been initialized
at com.example.appdigitalinkrecognition.TextCameraActivity.onActivityResult(TextCameraActivity.kt:143)
at android.app.Activity.dispatchActivityResult(Activity.java:8550)
at android.app.ActivityThread.deliverResults(ActivityThread.java:5583)
at android.app.ActivityThread.performResumeActivity(ActivityThread.java:4905) 
at android.app.ActivityThread.handleResumeActivity(ActivityThread.java:4955) 
at android.app.servertransaction.ResumeActivityItem.execute(ResumeActivityItem.java:52) 
at android.app.servertransaction.TransactionExecutor.executeLifecycleState(TransactionExecutor.java:176) 
at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:97) 
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2336) 
at android.os.Handler.dispatchMessage(Handler.java:106) 
at android.os.Looper.loop(Looper.java:246) 
at android.app.ActivityThread.main(ActivityThread.java:8653) 
at java.lang.reflect.Method.invoke(Native Method) 
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:602) 
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1130)
```` 
If I had to guess, you're getting different results because changing the orientation destroys an Activity, so a new one can be created to match the new orientation. So maybe that's happening in the background - but if you take a photo vertically, there's no orientation change so the Activity isn't destroyed
That's an issue because like the docs say:
Note: Since your process and activity can be destroyed between when you call launch() and when the onActivityResult() callback is triggered, any additional state needed to handle the result must be saved and restored separately from these APIs.
This is the closest thing I can find to them explicitly saying "onActivityResult can run before onCreate", but if that's what's happening to you, then that's why you're getting that lateinit property photoFile has not been initialized error.
It might also be because you're not actually initialising it in onCreate, you're doing it in a click listener - so even if onCreate does run first, the button would need to be clicked again, and onActivityResult definitely runs before that. But just moving it into onCreate might not be enough
I think to handle this properly, you'll need to think about your "show the photo" task having two parts - creating the path (in onCreate) and getting a result (through onActivityResult). You can't guarantee the order of those two things, so when you do one, you'll have to check if the other part has already been completed - if it has, you can show the image.
You could do something like this:
private var pathInitialised = false
private var photoTaken = false
override fun onCreate(savedInstanceState: Bundle?) {
...
// don't do this in the click listener - it needs to be available when the activity starts
photoFile = getPhotoFile(FILE_NAME)
// set the flag, and try to show the pic if ready
pathInitialised = true
tryDisplayPhoto()
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if(requestCode == 42 && resultCode == Activity.RESULT_OK) {
// set the flag, and try to show the pic if ready
photoTaken = true
tryDisplayPhoto()
} else {
super.onActivityResult(requestCode, resultCode, data)
}
}
private fun tryDisplayPhoto() {
if (pathInitialised && photoTaken) {
// now you know that the path is available and a photo needs to be shown
bitmap = BitmapFactory.decodeFile(photoFile.absolutePath)
binding.imageView.setImageBitmap(bitmap)
}
}
So now, no matter what order those two methods are called, they both try to display a pic - and that will only happen when the last of those methods is called, and all the pieces are in place
Declaring photoFile as optional should solve the problem:
private var photoFile: File? = null
You will need to add a null-check in your onActivityResult:
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if(requestCode == 42 && resultCode == Activity.RESULT_OK) {
photoFile?.let { photo ->
bitmap = BitmapFactory.decodeFile(photo.absolutePath)
binding.imageView.setImageBitmap(bitmap)
} ?: run {
// Photo is not defined...
}
} else {
super.onActivityResult(requestCode, resultCode, data)
}
}

Struggling to access Spinner outside of my recycler view

I have tried two different ways to access my spinner. Without success thus far.
I want to load the data for each driver as chosen.
To give an idea of my app.
Code for adapter:
class TableViewAdapter(var tripsheetlist: Tripsheetlist) : RecyclerView.Adapter<TableViewAdapter.RowViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RowViewHolder {
val itemView = LayoutInflater.from(parent.context).inflate(R.layout.table_list_item, parent, false)
return RowViewHolder(itemView) }
override fun getItemCount(): Int { return tripsheetlist.videos.size + 1 // one more to add header row
}
override fun onBindViewHolder(holder: RowViewHolder, position: Int) {
val rowPos = holder.adapterPosition
if (rowPos == 0) {
// Header Cells. Main Headings appear here
holder.itemView.apply {
setHeaderBg(txtWOrder)
setHeaderBg(txtDElNote)
setHeaderBg(txtCompany)
// setHeaderBg(txtAddress)
setHeaderBg(txtWeight)
setHeaderBg(txtbutton1)
setHeaderBg(txtbutton2)
setHeaderBg(txttvdone)
txtWOrder.text = "WOrder"
txtDElNote.text = "DElNote"
txtCompany.text = "Company"
// txtAddress.text = "Address"
txtWeight.text = "Weight"
txtbutton1.text = "Delivered"
txtbutton2.text = "Exception"
txttvdone.text = ""
}
} else {
val modal = tripsheetlist.videos[rowPos -1]
holder.itemView.apply {
setContentBg(txtWOrder)
setContentBg(txtDElNote)
setContentBg(txtCompany)
setContentBg(txtWeight)
setContentBg(txtbutton1)
setContentBg(txtbutton2)
setContentBg(txttvdone)
val list : MutableList<String> = ArrayList()
list.add("Deon")
list.add("Leon")
list.add("David")
list.add("Dick")
println(list)
val spinner : Spinner = findViewById(R.id.spnDriver)
spinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener{
override fun onItemSelected(p0: AdapterView<*>?, p1: View?, p2: Int, p3: Long) {
val item :String = list[p2]
if (item == "David")
{
txtWOrder.text = modal.WOrder.toString()
txtDElNote.text = modal.DElNote.toString()
txtCompany.text = modal.name.toString()
txtWeight.text = modal.id.toString()
}
}
override fun onNothingSelected(p0: AdapterView<*>?) {
}
}
I did it like this as a test for now. As I will get the drivers from my JSON. I don't have access to it yet so that is why the static values.
The problem I am getting now is: findViewById(R.id.spnDriver) must not be null
I first had my spinner class in my main activity and passed it over like this:
val list : MutableList<String> = ArrayList()
list.add("Deon")
list.add("Leon")
list.add("David")
list.add("Dick")
list.add("Jim")
list.add("Harry")
val adapter = ArrayAdapter( this, androidx.appcompat.R.layout.support_simple_spinner_dropdown_item, list)
val spinner: Spinner = findViewById(R.id.spnDriver)
spinner.adapter = adapter
spinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener{
override fun onItemSelected(p0: AdapterView<*>?, p1: View?, p2: Int, p3: Long) {
val item :String = list[p2]
Toast.makeText(this#MainActivity, "Driver $item selected", Toast.LENGTH_SHORT).show()
}
override fun onNothingSelected(p0: AdapterView<*>?) {
//empty
}
// insert code that activates data pull of tripsheet for driver= actifavte by method the class/object that activates the data pull. so datapuul(Driver)
}
limitDropDownHeight(spinner)
//drivers end
val btnLoadData: Button = findViewById(R.id.btnLoadData)
// weightsum(tvTotalweight, Tripsheetlist)
// totaldelNotes(tvTotaldelv,Tripsheetlist)
// setData(btnLoadData, Tripsheetlist )
fetchJson(spinner)
}
private fun fetchJson(spinner: Spinner) {
println("Attempting to Fetch JSON")
val url = "https://api.letsbuildthatapp.com/youtube/home_feed"
val request = Request.Builder().url(url).build()
val client = OkHttpClient()
client.newCall(request).enqueue(object: Callback {
override fun onFailure(call: Call, e: IOException) {
println("Failed to execute request") }
override fun onResponse(call: Call, response: Response) {
val body = response.body?.string()
println(body)
val gson = GsonBuilder().create()
val tripsheetlist = gson.fromJson(body, Tripsheetlist::class.java)
runOnUiThread {
recyclerViewTripsheetlist.adapter = TableViewAdapter(tripsheetlist, spinner)
}
}
})
}
In my Adapter class I then called it with : val spinner = spnDriver
This led to a different error: AppCompatSpinner.setOnItemSelectedListener(android.widget.AdapterView$OnItemSelectedListener)' on a null object reference
But seems like it passed the val spinner =spnDriver without a problem.
Thank you for all input and help.
I found a solution. What I did was to keep the spinner inside my MainActivity and then just pass the result of the spinner to the adapter - where I wanted to use it.

RecyclerView AsyncListDiffer and data source consistency state lose with onClickListener

I have data source(in that example it's just a var myState: List)
class MainActivity : AppCompatActivity() {
var generation: Int = 0
var myState: List<User> = emptyList()
val userAdapter = UserAdapter {
val index = myState.indexOf(it)
if (index == -1)
println("🔥 not found")
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val recycler = findViewById<RecyclerView>(R.id.rvContent)
recycler.layoutManager = LinearLayoutManager(this)
recycler.adapter = userAdapter
Thread {
while (true) {
generateNewData()
Handler(mainLooper).post {
userAdapter.submit(myState)
}
sleep(3000L)
}
}.start()
}
fun generateNewData() {
generation++
myState = (0..5000).map { User("$generation", it) }
}
}
I have RecyclerView, and AsyncListDiffer connected to it
data class User(val name: String, val id: Int) {
val createdTime = System.currentTimeMillis()
}
data class UserViewHolder(val view: View) : RecyclerView.ViewHolder(view) {
fun bindTo(user: User, action: (User) -> Unit) {
val textView = view.findViewById<TextView>(R.id.title)
textView.text = "${user.name} ${user.id} ${user.createdTime}"
textView.setOnClickListener { action(user) }
}
}
class UserAdapter(val action: (User) -> Unit) : RecyclerView.Adapter<UserViewHolder>() {
val differ: AsyncListDiffer<User> = AsyncListDiffer(this, DIFF_CALLBACK);
object DIFF_CALLBACK : DiffUtil.ItemCallback<User>() {
override fun areItemsTheSame(oldItem: User, newItem: User): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: User, newItem: User): Boolean {
return oldItem == newItem
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UserViewHolder {
return UserViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.item, parent, false))
}
override fun onBindViewHolder(holder: UserViewHolder, position: Int) {
val user = differ.currentList[position]
holder.bindTo(user, action = action)
}
fun submit(list: List<User>) {
differ.submitList(list)
}
override fun getItemCount(): Int {
return differ.currentList.size
}
}
I have OnClickListener binded to every item on RecyclerView
{
val index = myState.indexOf(it)
if (index == -1)
println("🔥 not found")}
That listener checks if item that was clicked is exists in the data source, and if not, outputs it to the console.
Every few seconds data in the source are changed, and pushed to
a AsyncListDiffer via submitList method, some how internally it uses other thread to match data and pass that diffed data
to the RecyclerView, and that takes some time;
If I starts clicking on the items non-stop, and the click event occurs at the same time when the differ inserts new data, then I get into a non-consistent state.
So, how to handle that?
Ignore a click with inconsistent data?(cons: User can see some strange behaviour like list item not collapse/expand, no navigation happen, etc)
Try to find a similar item in the new data by separate fields(positions/etc), and use it?(cons: same as 1. but less probability)
Block OnClickListener events until the data is consistent in both the Recycler and the data source? (cons: same as above, and also lag with action user performed until data became consistent again)
Something else? What is a best way to solve that?

Kotlin RecyclerView not updating after data changes

I am using RecyclerView to display a dynamic list of data and after I call an api I need to update my RecyclerView UI but the items in my RecyclerView does not change...
Below is my how I init my RecyclerView in my Fragment:-
forwardedList.layoutManager = LinearLayoutManager(context!!, RecyclerView.VERTICAL, false)
adapter = ForwardListAdapter(SmsHelper.getForwardedSms(context!!))
forwardedList.adapter = adapter
Below is my custom RecyclerView Adapter:-
class ForwardListAdapter(val forwardedList: List<SmsData>) : RecyclerView.Adapter<ForwardListAdapter.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ForwardListAdapter.ViewHolder {
val v = LayoutInflater.from(parent.context).inflate(R.layout.forwarded_item, parent, false)
return ViewHolder(v)
}
override fun onBindViewHolder(holder: ForwardListAdapter.ViewHolder, position: Int) {
holder.bindItems(forwardedList[position])
}
override fun getItemCount(): Int {
return forwardedList.size
}
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
fun bindItems(sms: SmsData) {
val simSlotText: TextView = itemView.findViewById(R.id.simSlot)
val senderText: TextView = itemView.findViewById(R.id.sender)
simSlotText.text = "[SIM ${sms.simSlot}] "
senderText.text = sms.sender
}
}
}
I am currently updating my RecyclerView from SmsHelper class as below:-
val fragments = mainActivity!!.supportFragmentManager.fragments
for (f in fragments) {
if (f.isVisible) {
if (f.javaClass.simpleName.equals("ForwardedFragment")) {
val fg = f as ForwardedFragment
fg.adapter.notifyDataSetChanged() <----- HERE
} else if (f.javaClass.simpleName.equals("FailedFragment")) {
val fg = f as FailedFragment
fg.adapter.notifyDataSetChanged()
}
}
}
As I observed, you did not really change the adapter's data but only called notifyDataSetChanged. You cannot just expect the data to be changed automatically like that since notifyDataSetChanged only:
Notifies the attached observers that the underlying data has been changed and any View reflecting the data set should refresh itself.
You need to change the data by yourself first, then call notifyDataSetChanged.
class ForwardListAdapter(private val forwardedList: MutableList<SmsData>) : RecyclerView.Adapter<ForwardListAdapter.ViewHolder>() {
// ...
fun setData(data: List<SmsData>) {
forwardedList.run {
clear()
addAll(data)
}
}
// ...
}
Then do it like this:
adapter.run {
setData(...) // Set the new data
notifyDataSetChanged(); // notify changed
}