rminaj
rminaj

Reputation: 575

Getting "no such table: room_table_modification_log" on app rotation

I think I already figured out that I need to save something on onSaveInstanceState somewhere, but I don't know what and how. I'm guessing it's related to the database because Room was mentioned on the error.

My app is using NavGraph to map the screens, so I don't know if I should save all my Fragments one by one or there's some NavGraph related solution that I could use. And for the ViewModels, I'm already using something that looks like:

private val actDevInfVM: ActuatorDeviceInfoViewModel by viewModels {
    ActuatorDeviceInfoViewModel.ActuatorDeviceInfoViewModelFactory((ctx.application as MyApp).actuatorDeviceInfoRepo)
}

to load the ViewModels on a Fragment or an Activity, I tried replacing it with:

private val actDevInfVM: ActuatorDeviceInfoViewModel by navGraphViewModels(R.id.main_nav_graph) {
    defaultViewModelProviderFactory
}

But I got a different set of errors, but for another ViewModel it seems. I got an error that looks like:

Cannot create an instance of class com.my.package.name.viewmodel.SensorViewModel

My ViewModels looks like:

class ActuatorDeviceInfoViewModel(private val repo: ActuatorDeviceInfoRepo) : ViewModel(),
    IViewModel<ActuatorDeviceInfo> {
    private val _items = MutableStateFlow<List<ActuatorDeviceInfo>>(listOf())
    override val items: StateFlow<List<ActuatorDeviceInfo>> = _items

    fun fetchAll() {
        viewModelScope.launch(Dispatchers.Main) {
            repo.getAllSub
                .flowOn(Dispatchers.IO)
                .catch { exception -> exception.localizedMessage?.let { Log.e("TAG", it) } }
                .stateIn(viewModelScope, SharingStarted.Eagerly, emptyList())
                .collect { _items.value = it }
        }
    }

    fun getAll(): StateFlow<List<ActuatorDeviceInfo>> {
        return _items
    }

    fun getAllLst(): List<ActuatorDeviceInfo> {
        return repo.getAllLst()
    }

    fun getAllWithEdgeDeviceId(edgeDeviceId: String): List<ActuatorDeviceInfo> {
        return runBlocking {
            repo.getAllWithEdgeDeviceId(edgeDeviceId)
        }
    }

    fun insert(item: ActuatorDeviceInfo) = viewModelScope.launch {
        repo.insert(item)
    }

    override fun insertReturnId(item: ActuatorDeviceInfo): Long {
        return runBlocking {
            repo.insertReturnId(item)
        }
    }

    override fun update(item: ActuatorDeviceInfo) = viewModelScope.launch {
        repo.update(item)
    }

    override fun insertOrUpdate(item: ActuatorDeviceInfo) = viewModelScope.launch {
        repo.insertOrUpdate(item)
    }

    fun delete(item: ActuatorDeviceInfo) = viewModelScope.launch {
        repo.delete(item)
    }

    class ActuatorDeviceInfoViewModelFactory(private val repo: ActuatorDeviceInfoRepo) :
        ViewModelProvider.Factory {
        override fun <T : ViewModel> create(modelClass: Class<T>): T {
            if (modelClass.isAssignableFrom(ActuatorDeviceInfoViewModel::class.java)) {
                @Suppress("UNCHECKED_CAST")
                return ActuatorDeviceInfoViewModel(repo) as T
            }
            throw IllegalArgumentException("Unknown VieModel Class")
        }
    }

    companion object {
        const val TAG = "ActuatorDeviceInfoViewModel"
    }
}

Then on MyApp, I have this code:

class MyApp : Application() {
    private val applicationScope = CoroutineScope(SupervisorJob())
    
    val actuatorDeviceInfoRepo by lazy { ActuatorDeviceInfoRepo(database.actuatorDeviceInfoDao()) }
    val sensorRepo by lazy { SensorRepo(database.sensorDao()) }
    ...

    fun dbClose() {
        database.close()
    }
}

dbClose() is called on MainActivitys onDestroy()

Then this is what AppDatabase looks like:

@Database(
    entities = [
        ActuatorDeviceInfo::class,
        Sensor::class,
        ... a few more data classes ...
    ], version = 1, exportSchema = false
)
@TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() {
    abstract fun actuatorDeviceInfoDao(): ActuatorDeviceInfoDao
    abstract fun sensorDao(): SensorDao
    ... a few more dao ...

    companion object {

        @Volatile
        private var INSTANCE: AppDatabase? = null

        fun getDatabase(context: Context, scope: CoroutineScope): AppDatabase {
            val queryInterceptor = LoggingQueryInterceptor()
            return INSTANCE ?: synchronized(this) {
                val instance = Room.databaseBuilder(
                    context.applicationContext,
                    AppDatabase::class.java,
                    "some_db_name"
                )
                    .addCallback(AppDbCallback(scope))
                    .setQueryCallback(queryInterceptor, Executors.newSingleThreadExecutor())
                    .build()

                INSTANCE = instance

                instance
            }
        }
    }
}

Upvotes: 0

Views: 169

Answers (2)

Zain
Zain

Reputation: 40878

dbClose() is called on MainActivitys onDestroy()

So, the reason you got no such table: room_table_modification_log on rotation because onDestroy() lifecycle callback gets triggered by the system in device orientation changes; this means that the database will be closed on every orientation change, and that will be cumbersome because:

  • Opening and closing databases is not a light-weight operation and can consume device resources.
  • Probably you'd get an exception if the database is quite large and you are trying to access the database before it's been opened correctly; or even that can introduce latency and a bad user experience until you grab the required data.
  • The orientation is a little time in terms of the entire app lifetime in order to do that heavy operation.
  • In general, there is no need to worry about closing SQLite Database (or its abstraction, Room). Check 1 & 2 questions for that.

So, it's recommended one of according to your case:

  • Keep the database open, don't worry about that especially if it's tied to the app's lifecycle.
  • If the database is tied to a component lifecycle (such as activity, fragment, service..etc), you'd close that when it's destroyed, but you have to differ that from the device orientation case using the isFinishing(), something like:
override fun onDestroy() {
    if(isFinishing)
        Log.d("TAG", "onDestroy: Finishing... you can close the database")
    else
        Log.d("TAG", "onDestroy: something else like configuration change, keep the database open")
    super.onDestroy()
}

Upvotes: 0

ea_
ea_

Reputation: 216

Not sure if this answers your question, but to avoid activity to reload when screen rotates, add following in your Manifest:

<activity>
(your activity)
android:configChanges="keyboardHidden|orientation|screenSize">
</activity>

To do something when screen orientation changes:

@Override public void onConfigurationChanged(Configuration newConfig) {
    super.onConfigurationChanged(newConfig);

    Toast.makeText(this, "config changed", Toast.LENGTH_SHORT).show();

    // Checks the orientation of the screen
    if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) {
        Toast.makeText(this, "landscape", Toast.LENGTH_SHORT).show();
    } else if (newConfig.orientation == Configuration.ORIENTATION_PORTRAIT){
        Toast.makeText(this, "portrait", Toast.LENGTH_SHORT).show();
    } }

Upvotes: 1

Related Questions