submitList() in recyclerview with diffutil causes recyclerview to blink - kotlin

I have a recyclerview that's been updated from remote source ,so it is working pretty well , the only annoying thing is that when the screen is pulled for a refresh and a get request is triggered the recyclerview is updated even though nothing has changed in the adapter causing the screen to blinks and that's annoying.
i tried messing with the diffutils but no result achieved
in my main fragment:
viewModel.fetchTopHeadline()
followed by an observer
topHeadline.observe(this#FeedFragment, Observer {
if(it == null) return#Observer
topHeadlineAdapter.submitList(it)
})
my Adapter:
class TopHeadlineAdapter:ListAdapter
and finally my diffutilCallback class :
class TopHeadlineDiffCallback :DiffUtil.ItemCallback<Article>(){
override fun areItemsTheSame(oldItem: Article, newItem: Article): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: Article, newItem: Article): Boolean {
return oldItem == newItem
}
}

Related

Jetpack Compose not updating / recomposing Flow List Values from Room DB when DB Data is getting changed

I'm trying to show a List of Items in my Android App. I'm using Jetpack Compose, Flows and RoomDB.
When launching the Activity all Items are shown without any problems, the Flow get's items collected and they are displayed.
But when I change some properties of the Item in the Database, the changes are not displayed. In my case I change the item to deleted, but it's still displayed as not deleted.
When I look at the Database Inspector, the value is changed in the database and set to deleted.
When I log collecting the flow, the change is getting emitted (It says the Item is set to deleted)
But Jetpack Compose is not recomposing the change.
If I delete an element from / add an element to the List (in the DB) the UI gets updated and recomposed.
So I can only assume that the problem must lie in the recomposition or handling of the flow.
Here my Code:
My Activity:
#AndroidEntryPoint
class StockTakingHistoryActivity : ComponentActivity() {
private val viewModel: StockTakingHistoryViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel.stockList = ...
setContent {
LaunchedEffect(Unit) {
viewModel.getStockListItems(viewModel.stockList!!.uuid)
}
Surface(color = MaterialTheme.colors.background) {
Content(viewModel.stockListItems)
}
}
}
}
...
#Composable
private fun Content(items: List<StockListItem>) {
...
LazyColumn {
items(items) { item ->
HistoryItem(stockListItem = item)
}
}
}
}
...
#Composable
private fun HistoryItem(stockListItem: StockListItem) {
...
Text(text = stockListItem.deleted)
...
Button(onClick = {
viewModel.deleteItem(stockListItem)
}) {
Text(text = "Set to deleted!")
}
}
}
My ViewModel:
var stockListItems by mutableStateOf(emptyList<StockListItem>())
fun getStockListItems(uuid: String) {
viewModelScope.launch {
stockListItemRepository.findByUUID(uuid).collect { items ->
Log.d("StockTakingHistoryViewModel", "items changed! ${items.map { it.deleted }}")
stockListItems = items
}
}
}
fun deleteItem(stockListItem: StockListItem) {
viewModelScope.launch(Dispatchers.IO) {
stockListItemRepo.update(item.copy(deleted = true);
}
}
The Repository:
fun findByUUID(uuid: String): Flow<List<StockListItem>> {
return dao.findByUUID(uuid)
}
The Dao behind the Repository Request:
#Query("select * from StockListItem where stockListUUID = :uuid order by createdAt desc limit 30")
fun findByUUID(uuid: String): Flow<List<StockListItem>>
I would be very happy if someone could help me! Thank you!
Considering you can collect a flow as state (via collectAsState) I'd consider going that route for getting the list rather than calling collect in the viewModel and updating the stockListItems as there are fewer moving parts for things to go wrong.
For example something like the following:
setContent {
val stockListItems = viewModel.getStockListItemsFlow(uuid).collectAsState(initial = emptyList())
Surface(color = MaterialTheme.colors.background) {
Content(stockListItems)
}
}
Found the Problem: The equals() method of StockListItem only compared the primary key.

Why is setContent{} being called twice?

I've recently started my first project using Jetpack Compose (with minimal Android dev experience).
For checking performance, I logged each call to any function / composable, with some unexpected behavior at startup or orientation change (but without further interacting with the app):
I do understand oncreate / super being called (again, in case of orientation change), but why is it that setContent {} is being called twice?
override fun onCreate(savedInstanceState: Bundle?) {
Log.v(tag, "oncreate")
super.onCreate(savedInstanceState)
Log.v(tag, "oncreatesuper")
setContent {
Log.v(tag, "setting content")
Content()
}
}
and then
#Composable
private fun Content() {
val arrayOfNodes = rememberSaveable { mutableListOf<Wurzel>() }
val toggleSizeInputDialog = rememberSaveable { mutableStateOf(false) }
(...some more...)
val currentConfig = LocalConfiguration.current
val title = stringResource(id = R.string.title)
Log.v(tag, "recomposing content")
MyTheme {
when (currentConfig.orientation) {
Configuration.ORIENTATION_LANDSCAPE -> {
Scaffold(...........)
My project is far to small to see any perfomance issues, however I'd like to find out the reason for this behavior for future reference or whether I severely misunderstood the compose architecture.
setComponent() is not being called twice, but the composable function you pass in it is. It just seems like some event causes recomposition of the content composable.

Why is data being shown when screen rotates in jetpack compose

I'm facing this issue where the data I'm retrieving from an API, https://randomuser.me/api/ at first compose it doesn't load.
But every time I rotate the screen the data updates.
First load
After screen rotation
View
class MainActivity : ComponentActivity() {
private val userViewModel : UserViewModel by viewModels()
private var userList: List<UserModel> = listOf()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
userViewModel.userModel.observe(this, Observer {
userList = it
})
userViewModel.onCreate()
setContent {
ListUsers(userList = userList)
}
}
}
ViewModel
class UserViewModel : ViewModel() {
val userModel = MutableLiveData<List<UserModel>>()
var getRandomUsersUseCase = RandomUsersUseCase()
fun onCreate() {
viewModelScope.launch {
val result = getRandomUsersUseCase()
if(!result.isNullOrEmpty()){
userModel.postValue(result)
}
}
}
}
Use State to ensure the data changes trigger recomposition of the Composable.
If you use another observable type such as LiveData in Compose, you
should convert it to State before reading it in a composable using
a composable extension function like LiveData.observeAsState().
Changes to your code would be,
val userListState by userViewModel.userModel.observeAsState()
setContent {
ListUsers(userList = userListState)
}
Why does it shows the data during rotation?
When rotating the screen or during any other configuration changes, the activity will be recreated.
More info on that here - Docs
In most cases, you would not require data to be changed when the screen rotates.
If you want to persist the data even after screen rotation, move the code inside onCreate() in your UserViewModel to the init block, like this.
init {
getData()
}
fun getData() {
viewModelScope.launch {
val result = getRandomUsersUseCase()
if(!result.isNullOrEmpty()){
userModel.postValue(result)
}
}
}
If you need to refresh the data on any other event like button click, swipe to refresh, etc, just call the getData() again on the event handler.
P.S: Check correct imports are added as required.
import androidx.compose.runtime.setValue
import androidx.compose.runtime.getValue

Avoid fragment recreation when opening from notification navigation component

I want when I click on a notification to open a fragment and not recreate it. I am using navigation component and using NavDeepLinkBuilder
val pendingIntent = NavDeepLinkBuilder(this)
.setComponentName(MainActivity::class.java)
.setGraph(R.navigation.workouts_graph)
.setDestination(R.id.workoutFragment)
.createPendingIntent()
My case is I have a fragment and when you exit the app, there is a notification which when you click on it, it should return you to that same fragment. Problem is every time i click on it it's creating this fragment again, I don't want to be recreated.
I had the same issue. Looks like there is not an option to use the NavDeepLinkBuilder without clearing the stack according to the documentation
I'm not sure the exact nature of your action, but I'll make two assumptions:
You pass the destination id to your MainActivity to navigate.
Your MainActivity is using ViewBinding and has a NavHostFragment
You will have to create the pending intent like:
val intent = Intent(this, MainActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_SINGLE_TOP
putExtra("destination", R.id.workoutFragment)
}
val pendingIntent = PendingIntent.getActivity(this, 0, intent, 0);
And in your MainActivity, you can handle both cases (app was already open, app was not already open)
override fun onStart() {
super.onStart()
// called when application was not open
intent?.let { processIntent(it) }
}
override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent)
// called when application was open
intent?.let { processIntent(it) }
}
private fun processIntent(intent: Intent) {
intent.extras?.getInt("destination")?.let {
intent.removeExtra("destination")
binding.navHostFragment.findNavController().navigate(it)
}
}

Android/Kotlin - local BroadcastReceiver is never activated by DownloadManager

I'm trying to detect when a DownloadManager download is completed. Unfortunately, the local BroadcastReceiver I'm trying to use for that never gets called. I have seen multiple similar questions, but none solved my problem; also, if the BroadcastReceiver isn't local but declared in the manifest it DOES get called, so I don't think it's an issue with the DownloadManager.
I can't use an external BroadcastReceiver because I need to update the UI (more specifically, open another activity) when the download is completed, and as far as I know that can't be done from an external receiver (please correct me if I'm wrong there).
The DownloadManager call:
private fun download() {
val mobileNetSSDConfigRequest: DownloadManager.Request = DownloadManager.Request(
Uri.parse("https://s3.eu-west-3.amazonaws.com/gotcha-weights/weights/MobileNetSSD/MobileNetSSD.prototxt")
)
mobileNetSSDConfigRequest.setDescription("Downloading MobileNetSSD configuration")
mobileNetSSDConfigRequest.setTitle("MobileNetSSD configuration")
mobileNetSSDConfigRequest.setDestinationInExternalPublicDir(
"Android/data/giorgioghisotti.unipr.it.gotcha/files/weights/", "MobileNetSSD.prototxt")
val manager: DownloadManager = getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
manager.enqueue(mobileNetSSDConfigRequest)
}
which is called upon being granted permissions:
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
when (requestCode) {
PERMISSION_REQUEST_CODE -> {
if (grantResults.isEmpty()
|| grantResults[0] != PackageManager.PERMISSION_GRANTED
|| grantResults[1] != PackageManager.PERMISSION_GRANTED
|| grantResults[2] != PackageManager.PERMISSION_GRANTED
|| grantResults[3] != PackageManager.PERMISSION_GRANTED
|| grantResults[4] != PackageManager.PERMISSION_GRANTED) {
Toast.makeText(this,
"Sorry, this app requires camera and storage access to work!",
Toast.LENGTH_LONG).show()
finish()
} else {
val mobileSSDConfig = File(sDir + mobileNetSSDConfigPath)
if (!mobileSSDConfig.exists()) download()
else {
val myIntent = Intent(this, MainMenu::class.java)
this.startActivity(myIntent)
}
}
}
}
}
The BroadcastIntent is declared like so (Splash is the name of the activity):
private val broadcastReceiver = object: BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
when (intent?.action) {
DownloadManager.ACTION_DOWNLOAD_COMPLETE -> {
val myIntent = Intent(this#Splash, MainMenu::class.java)
this#Splash.startActivity(myIntent)
}
}
}
}
and is registered in the activity's onCreate() method:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
LocalBroadcastManager.getInstance(this).registerReceiver(
broadcastReceiver, IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE)
)
ActivityCompat.requestPermissions(this, arrayOf(android.Manifest.permission.CAMERA,
android.Manifest.permission.READ_EXTERNAL_STORAGE,
android.Manifest.permission.INTERNET,
android.Manifest.permission.ACCESS_NETWORK_STATE,
android.Manifest.permission.WRITE_EXTERNAL_STORAGE), PERMISSION_REQUEST_CODE)
}
I tried registering the receiver in the onResume method and declaring it inside the onCreate method, no change. As far as I can tell, I'm doing this exactly as I saw it done in a few accepted answers and I can't see the problem. I know for a fact that the BroadcastReceiver is never called, I checked through debugging and with all sorts of console logs. The DownloadManager seems to work as intended since the file is correctly downloaded and external services are called correctly.
What am I missing?
I had a similar issue and turned out that calling simply registerReceiver(broadcastReceiver, IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE), without getting an instance of the LocalBroadcastManager solved the problem.
So possibly the issue was that the receiver was being registered on the wrong context object.
[remember also to take care of the un-registration of the receiver]
I did my like this
public void onResume()
{
super.onResume();
registerReceiver(broadcastReceiver, IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE))
}
public void onPause()
{
super.onPause();
unregisterReceiver(broadcastReceiver);
}