LazyColumn item not updated accordingly while list in room table already updated - android-room

When the Icon clicked, viewModel.onLockIconClicked(it) is called to reverse the value of isLock in db.
The Icon is expected to be updated according based on the value of isLock.
I've checked the value did reversed in db table. But LazyColumn not update accordingly.
What did I miss? Thanks a lot!
Ex, initially, Screen: icon = lock and Db: isLock = true,
when Icon clicked, Screen: icon = lock and Db: isLock = false,
while expected is Screen: icon = lock_open and Db: isLock = false.
ListScreen:
#Composable
fun ListScreen(context: Context) {
val viewModel: ListViewModel =
viewModel(factory = ListViewModelFactory(Db.getInstance(context)))
val list by viewModel.list.collectAsState(initial = emptyList())
Scaffold() {
SwipeRefresh(
state = rememberSwipeRefreshState(viewModel.isRefreshing),
onRefresh = { }
) {
LazyColumn(
state = rememberLazyListState(),
) {
items(list) {
Row() {
Icon(
painter = painterResource(if (it.isLock) R.drawable.ic_baseline_lock_24 else R.drawable.ic_baseline_lock_open_24),
contentDescription = null,
modifier = Modifier.clickable() { viewModel.onLockIconClicked(it) }
)
Text(it.code)
}
}
}
}
}
}
ListViewModel:
class ListViewModel(db: Db) : ViewModel() {
private val sumDao = db.sumDao()
val list = sumDao.getAllRows()
var isRefreshing by mutableStateOf(false)
private set
//init
init {
viewModelScope.launch(Dispatchers.IO) {
val initialCodeList = listOf("aaa", "bbb")
for (code in initialCodeList) {
val sum = Sum()
sum.code = code
sumDao.insert(sum)
}
}
}
fun onLockIconClicked(sum: Sum) {
sum.isLock = !sum.isLock
viewModelScope.launch(Dispatchers.IO) {
sumDao.update(sum)
}
}
}
class ListViewModelFactory(private val db: Db) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(ListViewModel::class.java)) {
#Suppress("UNCHECKED_CAST")
return ListViewModel(db) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
}
Sum:
#Entity(tableName = "sum", primaryKeys = ["code"])
data class Sum(
#ColumnInfo(name = "code")
var code: String = "",
#ColumnInfo(name = "is_lock")
var isLock: Boolean = true
)
SumDao:
#Dao
interface SumDao {
#Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(sum: Sum): Long
#Update(onConflict = OnConflictStrategy.REPLACE)
suspend fun update(sum: Sum): Int
#Delete
suspend fun delete(sum: Sum): Int
#Query("select * from sum")
fun getAllRows(): Flow<List<Sum>>
}
Db:
#Database(entities = [Sum::class], version = 1, exportSchema = false)
abstract class Db : RoomDatabase() {
abstract fun sumDao(): SumDao
companion object {
#Volatile
private var INSTANCE: Db? = null
fun getInstance(context: Context): Db {
return INSTANCE ?: synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
Db::class.java,
"db"
)
.fallbackToDestructiveMigration()
.build()
INSTANCE = instance
return instance
}
}
}
}

Consider taking the State-in-Compose for a better understanding of the concepts of state-handling in Compose.
I'm sorry but the information that you have provided is massive, so I can't pinpoint the source of the bug, but here's what you can do for now:
In your Dao class, just replace the words Flow<List<Sum>> with LiveData<List<Sum>>
In your ViewModel, you can get access to the LiveData inside the init like so
var list by mutableStateListOf<Sum>()
init{
sumDao.getAllRows().observeForever{
list = it
}
}
Now, list would ideally be updated every time the value in the databse changes, which infact would trigger recompositions since I am using a direct mutableStateListOf object here.
The problem may lie anywhere:
Since the class Sum is a custom-made class, it may have been experiencing issues triggering recompositions, which is a common problem among new developers, and even some experienced ones nowadays.
Since you are declaring the viewModel inside the Composable, wrong instances of ViewModels may have been passed around, leading to state-inconsistency - always try to declare your viewModels in the top-most layer possibly, i.e., somewhere like the onCreate method of the activity. Fragments are discourages so you should not face any problems over there.
Since you were not actively observing the Flow anywhere, that could have lead to the variable not being updated at all in the ViewModel, which would again lead to UI-inconsistency.

Related

performing query operations in Room ORM restarts my application

I am using Room to save objects in the database. and any time I perform an operation like insert or get all my application restarts.
this is my entity class:
#Entity
class Task(
#PrimaryKey val id: Int,
#ColumnInfo(name = "title") val title : String,
#ColumnInfo(name = "detail") val detail : String
)
this is my database class:
#Database(entities = [Task::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
abstract fun taskDao() : TaskDao
companion object{
var INSTANCE: AppDatabase? = null
fun getAppDatabase(context: Context): AppDatabase? {
if (INSTANCE == null) {
INSTANCE = Room.databaseBuilder(
context,
AppDatabase::class.java,
"task-database"
)
.build()
}
return INSTANCE
}
}
}
I try calling it from my AndroidViewModel class like this:
class AddTaskViewModel(application: Application) : AndroidViewModel(application) {
private val context = application
var title : MutableLiveData<String> = MutableLiveData("")
var detail : MutableLiveData<String> = MutableLiveData("")
fun saveTask(){
val task = Task(id = 1, title = title.value!!, detail = detail.value!! )
val taskDao : TaskDao? = AppDatabase.getAppDatabase(context)?.taskDao()
taskDao!!.insertAll(task)
Toast.makeText(context, "it is done", Toast.LENGTH_LONG).show()
}
fun changeTitle(text: String){
title.value = text
}
fun changeDetail(text: String){
detail.value = text
}
}
and at the point where taskDao!!.insertAll(task) is called the app restarts.
I think the issue might be that when you are inserting tasks into your database the id which is supposed to be the primary key is violated, in the sense that it's not unique, so the unique constraints fail
val task = Task(id = 1, title = title.value!!, detail = detail.value!!)
Try incrementing the id for example or letting room do that for you
I had to run the function in a coroutine because it was locking the UI processas seen in this post

How to convert ByteReadChannel into Flow<ByteBuffer>

How can I convert io.ktor.utils.io.ByteReadChannel into kotlinx.coroutines.flow.Flow<java.nio.ByteBuffer>?
I use Ktor with this routing:
post("/upload") {
val channel: ByteReadChannel = call.receiveChannel()
val flow: Flow<ByteBuffer> = channel.asByteBufferFlow() // my custom extension method
transaction.execute {
testDao.saveFile(flow)
}
call.respond("OK")
}
The DAO uses R2DBC and Blob like this:
override suspend fun saveFile(input: Flow<ByteBuffer>) {
val connection = requireR2DBCTransactionConnection()
val publisher: Publisher<ByteBuffer> = input.asPublisher()
val statement: Statement = connection.createStatement("insert into bindata (data) values ($1)")
statement.bind(0, Blob.from(publisher))
val count: Int = statement.execute().awaitFirst().rowsUpdated.awaitFirst()
if (count != 1) {
throw IllegalStateException()
}
}
I tried to write this extension method but I failed:
fun ByteReadChannel.asByteBufferFlow(): Flow<ByteBuffer> = object : AbstractFlow<ByteBuffer>() {
override suspend fun collectSafely(collector: FlowCollector<ByteBuffer>) {
/* I have no idea */
}
}
My main problem is that I have not found any similar sample and both ByteBuffer and ByteReadChannel is new for me.

Android Room Database Drop table UI Test

I drop the table and want to test with it.
MIGRATION_1_2 does drop 'b' table which is associated with BDao. So AppDatabase can't get BDao instance. Also, B::class is removed from entities.
#Database(entities = [A::class/*, B::class*/], version = 2)
abstract class AppDatabase : RoomDatabse() {
abstract aDao: ADao
// abstract bDao: BDao
companion object {
fun getDatabase(context: Context): AppDatabse {
...
Room.databaseBuilder(context.applicationContext,
AppDatabase::class.java, DATABASE_NAME)
.addMigrations(MIGRATION_1_2)
.build()
...
}
val MIGRATION_1_2 = object Migration(1, 2) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("""
DROP TABLE 'b'
""")
}
}
}
}
Below is the test code. I can get A Dao but can't get B Dao. How to verify b table is dropped?
#RunWith(AndroidJUnit4::class)
class MigrationTest {
#Rule
#JvmField
val helper = MigrationTestHelper(
InstrumentationRegistry.getInstrumentation(),
AppDatabase::class.java.canonicalName,
FrameworkSQLiteOpenHelperFactory()
)
#Test
fun migrate1To2() {
val db = helper.createDatabase(TEST_DB, 1)
insertAData(db)
isnertBData(db)
db.close()
helper.runMigrationsAndValidate(TEST_DB, 2, true, AppDatabase.MIGRATION_1_2)
helper.closeWhenFinished(database)
// I can test with A.
val adao = database.aDao()
// But I can't test with B.
// val bdao = database.bDao()
}
}
I write the method referenced from https://stackoverflow.com/a/7863401/2423899
private fun AppDatabase.isTableExisting(tableName: String): Boolean {
assert(isOpen)
val query = SupportSQLiteQueryBuilder
.builder("sqlite_master")
.distinct()
.columns(arrayOf("tbl_name"))
.selection("tbl_name=?", arrayOf(tableName))
.create()
val cursor = query(query)
val count = cursor.count > 0
cursor.close()
return count
}
Below code is verifying b table is dropped.
assertThat(database.isTableExsiting("b"), `is`(false))

LiveData Transformation not getting triggered

I subscribed to ids and search in the ui but i wasn't getting any results so i stepped through with the debugger and found out that the transformation is not getting triggered after the first time. So when i call setIds the first time ids gets updated but for every call after the first one the transformation won't trigger. Same goes for the search.
Any ideas what might possible go wrong?
class MyViewModel : ViewModel() {
private val repository = Repository.sharedInstance
var recentRadius: LiveData<List<RecentRadius>>?
var recentRoute: LiveData<List<RecentRoute>>?
init {
recentRadius = repository.recentRadius()
recentRoute = repository.recentRoute()
}
private val idsInput = MutableLiveData<String>()
fun setIdsInput(textId: String) {
idsInput.value = textId
}
val ids: LiveData<List<String>> = Transformations.switchMap(idsInput) { id ->
repository.ids(id)
}
private val searchInput = MutableLiveData<Search>()
fun setSearchInput(search: Search) {
searchInput.value = search
}
val search: LiveData<SearchResult> = Transformations.switchMap(searchInput) { search ->
when (search.type) {
SearchType.ID -> repository.id(search)
SearchType.RADIUS -> repository.radius(search)
SearchType.ROUTE -> repository.route(search)
}
}
}
The most common reason why transformation don't get triggered is when there is no Observer observing it or the input LiveData is not getting changed.
Below example illustrates use of map when observer is attached in the activity.
Activity
class MainActivity : AppCompatActivity() {
lateinit var mBinding : ActivityMainBinding
private val mViewModel : MainViewModel by lazy {
getViewModel { MainViewModel(this.application) }
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mBinding = DataBindingUtil.setContentView(this, R.layout.activity_main)
mBinding.vm = mViewModel
// adding obeserver
mViewModel.videoName.observe(this, Observer<String> { value ->
value?.let {
//Toast.makeText(this, it, Toast.LENGTH_LONG).show()
}
})
}
}
ViewModel with map
class MainViewModel(val appContext : Application) : AndroidViewModel(appContext) {
private val TAG = "MainViewModel"
var videoData = MutableLiveData<VideoDownload>()
var videoName : LiveData<String>
init {
// Update the data
videoName = Transformations.map(videoData) { "updated : "+it.webUrl }
}
fun onActionClick(v : View) {
// change data
videoData.value = VideoDownload(System.currentTimeMillis().toString())
}
fun onReActionClick(v : View) {
// check data
Toast.makeText(appContext, videoName.value, Toast.LENGTH_LONG).show()
}
}
ViewModel with switchMap
class MainViewModel(val appContext : Application) : AndroidViewModel(appContext) {
private val TAG = "MainViewModel"
var videoData = MutableLiveData<VideoDownload>()
var videoName : LiveData<String>
init {
// Update the data
videoName = Transformations.switchMap(videoData) { modData(it.webUrl) }
}
private fun modData(str: String): LiveData<String> {
val liveData = MutableLiveData<String>()
liveData.value = "switchmap : "+str
return liveData
}
fun onActionClick(v : View) {
// change data
videoData.value = VideoDownload(System.currentTimeMillis().toString())
}
fun onReActionClick(v : View) {
// check data
Toast.makeText(appContext, videoName.value, Toast.LENGTH_LONG).show()
}
}
for me, it was because the observer owner was a fragment. It stopped triggering when navigating to different fragments. I changed the observer owner to the activity and it triggered as expected.
itemsViewModel.items.observe(requireActivity(), Observer {
The view model was defined as a class property:
private val itemsViewModel: ItemsViewModel by lazy {
ViewModelProvider(requireActivity()).get(ItemsViewModel::class.java)
}
If you really want it to be triggered.
fun <X, Y> LiveData<X>.forceMap(
mapFunction: (X) -> Y
): LiveData<Y> {
val result = MutableLiveData<Y>()
this.observeForever {x->
if (x != null) {
result.value = mapFunction.invoke(x)
}
}
return result
}

Cannot call a function from init block because of val property

I'd like to initialize my class's properties.
Because I'm using heavily the functional elements of Kotlin, I'd like to put these initializations to well named functions, to increase readability of my code.
The problem is that I cannot assign a val property, if the code is not in the init block, but in function which is called from the init block.
Is it possible to take apart initialization of a class, to different functions, if the properties are vals?
Here is the code:
val socket: DatagramSocket = DatagramSocket()
val data: ByteArray = "Cassiopeiae server discovery packet".toByteArray()
val broadcastAddresses: List<InetAddress>
init {
socket.broadcast = true
val interfaceAddresses = ArrayList<InterfaceAddress>()
collectValidNetworkInterfaces(interfaceAddresses)
collectBroadcastAddresses(interfaceAddresses)
}
private fun collectValidNetworkInterfaces(interfaceAddresses: ArrayList<InterfaceAddress>) {
NetworkInterface.getNetworkInterfaces().toList()
.filter { validInterface(it) }
.forEach { nInterface -> nInterface.interfaceAddresses.toCollection(interfaceAddresses) }
}
private fun collectBroadcastAddresses(interfaceAddresses: ArrayList<InterfaceAddress>) {
broadcastAddresses = interfaceAddresses
.filter { address -> address.broadcast != null }
.map { it.broadcast }
}
Of course it's not compiling, because collectBroadcastAddresses function tries to reassign the broadcastAddresses val. Although I don't want to put the code of this function to the init block, because it's not obvious what the code is doing, and the function name tells it very nicely.
What can I do in such cases? I'd like to keep my code clean, this is the most important point!
One way of approaching the problem is to use pure functions to initialize fields:
class Operation {
val socket = DatagramSocket().apply { broadcast = true }
val data: ByteArray = "Cassiopeiae server discovery packet".toByteArray()
val broadcastAddresses = collectBroadcastAddresses(collectValidNetworkInterfaces())
private fun collectValidNetworkInterfaces() =
NetworkInterface.getNetworkInterfaces().toList()
.filter { validInterface(it) }
.flatMap { nInterface -> nInterface.interfaceAddresses }
private fun validInterface(it: NetworkInterface?) = true
private fun collectBroadcastAddresses(interfaceAddresses: List<InterfaceAddress>) {
interfaceAddresses
.filter { address -> address.broadcast != null }
.map { it.broadcast }
}
}
Notice how the socket field initialization uses apply extension.
I often find it useful to extract collection manipulation routines into extension methods:
class Operation {
val socket = DatagramSocket().apply { broadcast = true }
val data: ByteArray = "Cassiopeiae server discovery packet".toByteArray()
val broadcastAddresses = NetworkInterface.getNetworkInterfaces()
.collectValidNetworkInterfaces { validInterface(it) }
.collectBroadcastAddresses()
private fun validInterface(it: NetworkInterface?) = true
}
fun Iterable<InterfaceAddress>.collectBroadcastAddresses(): List<InetAddress> =
filter { address -> address.broadcast != null }.map { it.broadcast }
fun Enumeration<NetworkInterface>.collectValidNetworkInterfaces(isValid: (NetworkInterface) -> Boolean = { true }) =
toList()
.filter { isValid(it) }
.flatMap { nInterface -> nInterface.interfaceAddresses }

Resources