Android Room Database Drop table UI Test - android-room

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

Related

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

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.

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.

Improving DSL syntax

To start learning design of DSLs using Kotlin language features, I have
the attempt below at a toy DSL for creating groups of members with members
having names. I am looking for pointers/hints on the following
How can I avoid having to separate groups by a semicolon, if no semicolon the compiler gives
Groups.kt:31:45: error: unresolved reference: member
val grp = group { member { name ("Bob") } member { name ("Sandy") } }
Can i get to use a lambda for setting name instead of function call?
Can I avoid having to have name be mutable in class MEMBER?
My code is
fun group(create: GROUP.() -> Unit) = GROUP().apply(create)
class GROUP {
private val members = mutableSetOf<MEMBER>()
fun member(create: MEMBER.() -> Unit) {
val member = MEMBER()
member.create()
members.add(member)
}
override fun toString() = members.toString()
}
class MEMBER() {
var name = ""
set(value) {
field = value
}
fun name(nameToSet: String) {
name = nameToSet
}
override fun toString() = "MEMBER(" + name + ")"
}
fun main(args: Array<String>) {
val grp = group { member { name ("Bob") }; member { name ("Sandy") } }
println(grp)
}
Currently the output of above code is
[MEMBER(Bob), MEMBER(Sandy)]
How can I avoid having to separate groups by a semicolon
By using the idiomatic format, using separate lines. After all, the whole point of a DSL is to make the code very readable by showing the hierarchical structure, and doing everything on a single line defeats the wole purpose:
val grp = group {
member {
name ("Bob")
}
member {
name ("Sandy")
}
}
Can I get to use a lambda for setting name instead of function call?
It would be more logical and idiomatic to remove the name function and to simply assign a value to the property:
name = "Bob"
But yes, you can also replace your name function by
fun name(block: () -> String) {
this.name = block()
}
and use
name {
"Sandy"
}
Can I avoid having to have name be mutable in class MEMBER?
Yes: the lambda passed to the member() function would customize an additional MemberBuilder class, that would be mutable, but would allow to create an immutable MEMBER:
fun group(create: GROUP.() -> Unit) = GROUP().apply(create)
class GROUP {
private val members = mutableSetOf<MEMBER>()
fun member(configure: MemberBuilder.() -> Unit) {
val memberBuilder = MemberBuilder()
memberBuilder.configure()
members.add(memberBuilder.build())
}
override fun toString() = members.toString()
}
class MEMBER(val name: String) {
override fun toString() = "MEMBER($name)"
}
class MemberBuilder {
var name = "";
fun build() = MEMBER(name)
}
fun main(args: Array<String>) {
val grp = group {
member {
name = "Bob"
}
member {
name = "Sandy"
}
}
println(grp)
}
Also, note that classes, by convention, are PascalCased, not ALL_CAPS.

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
}

Resources