androidkotlinandroid-roomandroid-viewmodelscreen-orientation

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

Solution

  • 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();
        } }