Room cannot verify the data integrity. Looks like you've changed schema.... What's wrong? - kotlin

I am using Room in my app with two entities. The whole implementation is below.
The Problem is, the given scheme is fixed, which means I do not change anything regarding DB. When I provide a new version of my app to Users over Google Play Console, I get the following issue in Cryshlytics although I did not change anything for DB, just edited UI or another things, which definetly nothing have to do with DB:
Fatal Exception: java.lang.IllegalStateException: Room cannot verify the data integrity. Looks like you've changed schema but forgot to update the version number. You can simply fix this by increasing the version number.
at androidx.room.RoomOpenHelper.checkIdentity(RoomOpenHelper.java:154)
at androidx.room.RoomOpenHelper.onOpen(RoomOpenHelper.java:135)
.......
Now I am not sure if I change the version of DB, it would work. What is wrong here?
BTW the DB is called from a Fragment like this
val mainDb: MainRepository by lazy { MainRepository(requireContext()) }
val stateDb: StateRepository by lazy { StateRepository(requireContext()) }
What's wrong here?
AppDatabase:
#Database(entities = [Main::class, State::class], version = 1, exportSchema = false)
abstract class AppDatabase : RoomDatabase() {
abstract val mainDao: MainDao
abstract val stateDao: StateDao
companion object {
private var INSTANCE: AppDatabase? = null
fun getInstance(context: Context): AppDatabase? =
INSTANCE ?: synchronized(AppDatabase::class) {
INSTANCE = Room.databaseBuilder(
context.applicationContext,
AppDatabase::class.java,
MY_DB
).allowMainThreadQueries()
.build()
return INSTANCE
}
}
}
Dao:
#Dao
interface StateDao {
#Query("SELECT * FROM $STATE")
fun getAll(): List<State>
#Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(state: State)
#Update
fun update(state: State)
#Query("DELETE FROM $STATE")
fun drop()
}
#Dao
interface MainDao {
#Query("SELECT * FROM $MAIN")
fun getAll(): List<Main>
#Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(main: Main)
#Update
fun update(main: Main)
#Query("DELETE FROM $MAIN")
fun drop()
}
Main:
#Entity(tableName = MAIN)
data class Main(
#PrimaryKey #ColumnInfo(name = NUMBER) val number: Int,
#ColumnInfo(name = CARD) val car: String? = EMPTY,
#ColumnInfo(name = MODEL) val model: String? = EMPTY
) : Parcelable {
constructor(parcel: Parcel) : this(
parcel.readInt(),
parcel.readString(),
parcel.readString()
)
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeInt(number)
parcel.writeString(car)
parcel.writeString(model)
}
override fun describeContents(): Int {
return 0
}
companion object CREATOR : Parcelable.Creator<Main> {
override fun createFromParcel(parcel: Parcel): Main {
return Main(parcel)
}
override fun newArray(size: Int): Array<Main?> {
return arrayOfNulls(size)
}
}
}
State:
#Entity(tableName = STATE)
data class State(
#PrimaryKey #ColumnInfo(name = NUMBER) val number: Int,
#ColumnInfo(name = STATE) val state: String? = EMPTY
) : Parcelable {
constructor(parcel: Parcel) : this(
parcel.readInt(),
parcel.readString()
)
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeInt(number)
parcel.writeString(question)
}
override fun describeContents(): Int {
return 0
}
companion object CREATOR : Parcelable.Creator<State> {
override fun createFromParcel(parcel: Parcel): State {
return State(parcel)
}
override fun newArray(size: Int): Array<State?> {
return arrayOfNulls(size)
}
}
}
Repository:
class MainRepository(context: Context) {
private val mainDao = AppDatabase.getInstance(context)?.mainDao
fun getAll(): List<Main>? {
return mainDao?.getAll()
}
fun insert(main: Main) {
AsyncInsert(mainDao).execute(main)
}
fun update(main: Main) {
mainDao?.update(main)
}
fun drop() {
mainDao?.drop()
}
private class AsyncInsert(private val dao: MainDao?) : AsyncTask<Main, Void, Void>() {
override fun doInBackground(vararg p0: Main?): Void? {
p0[0]?.let { dao?.insert(it) }
return null
}
}
}
class StateRepository(context: Context) {
private val stateDao = AppDatabase.getInstance(context)?.stateDao
fun drop() {
stateDao?.drop()
}
fun getAll(): List<State>? {
return stateDao?.getAll()
}
fun insert(state: State) {
AsyncInsert(stateDao).execute(state)
}
fun update(state: State) {
stateDao?.update(state)
}
private class AsyncInsert(private val dao: StateDao?) : AsyncTask<State, Void, Void>() {
override fun doInBackground(vararg p0: State?): Void? {
p0[0]?.let { dao?.insert(it) }
return null
}
}
}

Now I am not sure if I change the version of DB, it would work. What is wrong here?
Changing the version would probably not work as the schema, as far as Room is concerned, has changed.
There is either a bug or the schema has been changed.
However, changing the version, would, with a Migration that does nothing (so as to not get a "no migration specified" error), then fail but importantly with an expected (what Room expects the schema to be according to the Entities) found (the schema that exists) discrepancy. This, if there is no bug, could then be used to ascertain what has been changed.

Related

How to use LifecycleScope to execute coroutine

I am discovering Kotlin and android app dev. I fail to get data from my room database (because of Cannot access database on the main thread). So I try with lifecyclescope.
The concerned code, in Fragment onViewCreated function, is :
lifecycleScope.launch {
withContext(Dispatchers.Default) {
val accountConfiguration = viewModel.get();
println("{${accountConfiguration}}")
}
}
The called function (in viewModel) is :
fun get() = viewModelScope.launch {
repository.get()
}
There is the "full" code (simplified), Entity & DAO :
#Entity
data class AccountConfiguration(
#PrimaryKey val server_address: String,
#ColumnInfo(name = "user_name") val user_name: String,
// [...]
)
#Dao
interface AccountConfigurationDao {
#Query("SELECT * FROM accountconfiguration LIMIT 1")
fun flow(): Flow<AccountConfiguration?>
#Query("SELECT * FROM accountconfiguration LIMIT 1")
suspend fun get(): AccountConfiguration?
// [...]
}
Repository :
package fr.bux.rollingdashboard
import androidx.annotation.WorkerThread
import kotlinx.coroutines.flow.Flow
class AccountConfigurationRepository(private val accountConfigurationDao: AccountConfigurationDao) {
val accountConfiguration: Flow<AccountConfiguration?> = accountConfigurationDao.flow()
// [...]
#Suppress("RedundantSuspendModifier")
#WorkerThread
suspend fun get() : AccountConfiguration? {
return accountConfigurationDao.get()
}
}
ViewModel & Factory :
class AccountConfigurationViewModel(private val repository: AccountConfigurationRepository) : ViewModel() {
val accountConfiguration: LiveData<AccountConfiguration?> = repository.accountConfiguration.asLiveData()
// [...]
fun get() = viewModelScope.launch {
repository.get()
}
// [...]
}
class AccountConfigurationViewModelFactory(private val repository: AccountConfigurationRepository) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(AccountConfigurationViewModel::class.java)) {
#Suppress("UNCHECKED_CAST")
return AccountConfigurationViewModel(repository) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
}
Fragment :
class AccountConfigurationFragment : Fragment() {
private var _binding: AccountConfigurationFragmentBinding? = null
// This property is only valid between onCreateView and
// onDestroyView.
private val binding get() = _binding!!
private val viewModel: AccountConfigurationViewModel by activityViewModels {
AccountConfigurationViewModelFactory(
(activity?.application as RollingDashboardApplication).account_configuration_repository
)
}
lateinit var accountConfiguration: AccountConfiguration
// [...]
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.buttonGoBackMain.setOnClickListener {
findNavController().navigate(R.id.action_AccountConfigurationFragment_to_DashboardFragment)
}
lifecycleScope.launch {
withContext(Dispatchers.Default) {
val accountConfiguration = viewModel.get();
println("{${accountConfiguration}}")
}
}
binding.buttonSave.setOnClickListener {
save()
}
}
// [...]
}
In your current code,
lifecycleScope.launch {
withContext(Dispatchers.Default) {
val accountConfiguration = viewModel.get();
println("{${accountConfiguration}}")
}
}
viewModel.get() is not a suspend function, so it returns immediately and proceeds to the next line. It actually returns the Job created by viewModelScope.launch().
If you want your coroutine to wait for the result before continuing you should make the get() function suspend and return the AccountConfiguration?
suspend fun get(): AccountConfiguration? {
return repository.get()
}
You need not change dispatchers to Dispatchers.Default because Room itself will switch to a background thread before executing any database operation.
Right now if there is a configuration change while coroutines inside lifecyclerScope are running, everything will get cancelled and restarted.
A better way would have been to put the suspending calls inside the ViewModel and expose a LiveData/Flow to the UI.
The problem is the viewModel function :
fun get() = viewModelScope.launch {
repository.get()
}
This function must be the coroutine instead launch the coroutine itself. Correct code is :
suspend fun get(): AccountConfiguration? {
return repository.get()
}

ViewPager2 and FragmentStatePagerAdapter

I want to set the adapter of ViewPager2 to FragmentStatePagerAdapter but I get this error:
Type mismatch. Required: (RecyclerView.Adapter<RecyclerView.ViewHolder!>?..RecyclerView.Adapter<*>?) Found: ViewPager2Adapter
My ViewPagerAdapter class is
class ViewPager2Adapter(fm:FragmentManager) :FragmentStatePagerAdapter(fm) {
override fun getItem(position: Int): Fragment {
return when(position) {
0 -> {
MyScansListFragment()
}
1 -> {
PurchasedItemsFragment()
}
else -> {
Fragment()
}
}
}
override fun getCount(): Int {
return 2
}
override fun getItemPosition(`object`: Any): Int {
return POSITION_NONE
}}
And in the oncreateView() :
val viewPager2Adapter = ViewPager2Adapter(activity?.supportFragmentManager!!)
binding!!.viewPager.adapter = viewPager2Adapter
okay, let's change the code a little bit.
First of all, FragmentStatePagerAdapter has been deprecated.
FragmentStatePagerAdapter & FragmentPagerAdapter have been recently deprecated, and your code must look something like this. FragmentStatePagerAdapter and if you get your cursor over it and see details, there will be a statement "Deprecated Switch to androidx.viewpager2.widget.ViewPager2 and use androidx.viewpager2.adapter.FragmentStateAdapter instead."
try the following code.
class ViewPager2Adapter(private val listFragment: ArrayList<Fragment>,
fm: FragmentManager,
lifecycle: Lifecycle) : FragmentStateAdapter(fm, lifecycle) {
override fun getItemCount(): Int {
return listFragment.size
}
override fun createFragment(position: Int): Fragment {
return listFragment[position]
}
}
so, this is now kind of your universal viewpager adapter.
The next thing is we require fragments to be passed in here.
//I don't think you need Fragment() but since it's there in your list.
val fragmentList = listOf(MyScansListFragment(), PurchasedItemsFragment(),Fragment())
val viewPager2Adapter = ViewPager2Adapter(fragmentList, activity?.supportFragmentManager!!, lifecycle)
binding!!.viewPager.adapter = viewPager2Adapter

Android Room Database VIewModel is not reflecting latest Inserted Data?

I am a beginner in Android Development. So please help me to find out solution to my problem.
I am making Music app as practice project. As user search for an artist, API fetches results and results get stored in room database. I want to display results stored in room through ViewModel. But ViewModel is not showing results synchronously as data is inserted. At first search, it shows nothing. But after subsequent searches, it displays results.
Api call and insert data in Room.
artistResultsCallback.enqueue(object : Callback<ResponseModel>{
override fun onResponse(call: Call<ResponseModel>, response: Response<ResponseModel>) {
if(response.isSuccessful) {
hideProgressBar()
artistRepository.insertList(response.body()!!.getArtistModel()!!)
}
}
override fun onFailure(call: Call<ResponseModel>, t: Throwable) {
hideProgressBar()
Log.e("MainActivity","An error occurred")
}
})
my dao
#Dao
interface ArtistDao {
#Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertArtists(artistList:List<Artist>)
#Query("SELECT * FROM Artists WHERE artistName LIKE :name")
fun getArtists(name:String):LiveData<List<Artist>>
#Query("DELETE FROM Artists")
fun deleteArtists()
}
my ViewModel
class ArtistViewModel(): ViewModel() {
fun getArtists(application: Application,name:String):LiveData<List<Artist>>{
var artistRepository =ArtistRepository(application)
var artistResults:LiveData<List<Artist>> = artistRepository.getResultsList(name)
return artistResults
}
}
my database
#Database(entities = [Artist::class],version = 3)
abstract class ArtistDatabase: RoomDatabase() {
abstract fun artistDao(): ArtistDao
companion object {
#Volatile
private var artistDb: ArtistDatabase? = null
fun getDatabaseInstance(context: Context): ArtistDatabase {
return artistDb ?: synchronized(this) {
artistDb ?: Room.databaseBuilder(context, ArtistDatabase::class.java, Constants.ARTIST_DATABASE).fallbackToDestructiveMigration().build()
}
}
}
}
My repository:
class ArtistRepository(application: Application) {
private var artistDao: ArtistDao?
init {
val artistDb= ArtistDatabase.getDatabaseInstance(application)
artistDao= artistDb.artistDao()
}
fun getResultsList(name:String):LiveData<List<Artist>>{
return artistDao!!.getArtists(name)
}
fun insertList(artist:List<Artist>){
CoroutineScope(Dispatchers.IO).launch{
artistDao!!.deleteArtists()
artistDao!!.insertArtists(artist)
}
}
}
Activity showing results
artistRecyclerView = findViewById(R.id.rv_artist)
var getIntent = intent.getStringExtra(Constants.ARTIST_NAME)
search_results_for.text="Search Results for: $getIntent"
artistViewModel = ViewModelProviders.of(this).get(ArtistViewModel::class.java)
var artistViewModelResults:LiveData<List<Artist>> = artistViewModel.getArtists(application,getIntent!!)
artistViewModelResults.observe(this, object:Observer<List<Artist>>{
override fun onChanged(t: List<Artist>) {
setUpUi(t)
}
})

Parsing api using Retrofit and GSON

I'm parsing API and it's logging in the logcat, but I have a problem while retrieving it and using that data in a recycleview. These are my code snippets:
class MainActivity : AppCompatActivity() {
private val users = arrayListOf<User>()
private lateinit var adapter: RecyclerViewAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
init()
}
private fun init() {
adapter = RecyclerViewAdapter(users)
recyclerView.layoutManager = LinearLayoutManager(this)
recyclerView.adapter = adapter
val myViewModel : UsersViewModel by viewModel()
myViewModel.getAllUsers().observe(this, Observer {
users.add(it)
adapter.notifyDataSetChanged()
})
myViewModel.getUsers()
d("allUsers", users.size.toString())
}
}
I cannot set the data in a recyclerview, can anyone give me a hint? I could not find a proper source or code snippet to understand how I'm able to parse the data using a converter.
class UsersRequest {
private var retrofit = Retrofit.Builder()
.baseUrl("https://reqres.in/api/")
.addConverterFactory(ScalarsConverterFactory.create())
.build()
private var service = retrofit.create(ApiService::class.java)
interface ApiService {
#GET("users?page=1")
fun getRequest(): Call<String>
}
fun getRequest(callback: CustomCallback) {
val call = service.getRequest()
call.enqueue(onCallback(callback))
}
private fun onCallback(callback: CustomCallback): Callback<String> = object : Callback<String> {
override fun onFailure(call: Call<String>, t: Throwable) {
d("response", "${t.message}")
callback.onFailure(t.message.toString())
}
override fun onResponse(call: Call<String>, response: Response<String>) {
d("response", "${response.body()}")
callback.onSuccess(response.body().toString())
}
}
}
class RecyclerViewAdapter(private val users: ArrayList<User>) :
RecyclerView.Adapter<RecyclerViewAdapter.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder(
LayoutInflater.from(parent.context).inflate(
R.layout.user_layout,
parent,
false
)
)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
return holder.onBind()
}
override fun getItemCount() = users.size
private lateinit var user:User
inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
fun onBind() {
user = users[adapterPosition]
itemView.name.text = user.first_name
}
}
}
User(
val id: Int,
val email: String,
val first_name: String,
val last_name: String,
val avatar: String
)
with adapter class RecyclerViewAdapter(private val users: MutableList<User>) : RecyclerView.Adapter<RecyclerViewAdapter.ViewHolder>() { ...
Add this method to your adapter :
fun setUsers(usersList: List<User>) {
this.users.clear()
this.users.addAll(usersList)
notifyDataSetChanged()
}
and in MainActivity simply put :
myViewModel.getAllUsers().observe(this, Observer {
users -> hideLoading() // If you have a progress bar, here you can hide it
users?.let {
adapter.setUsers(users) }
})

Kotlin: How to fetch data from web service and display it in app?

Hello :) I'm working on app that shows popular movies and some details about each of them. I created a RecyclerView where information should be displayed. I'm stuck with getting and displaying data. I'm using https://www.themoviedb.org/ page for api.
I was following these steps: http://imakeanapp.com/make-a-movies-app-using-tmdb-api-part-4-networking-using-retrofit-library/ , but it's written in Java and I need code in Kotlin. I converted by myself a part of the code, here is what I have:
in Movie.kt
data class Movie (
#SerializedName("id") val id: Int,
#SerializedName("title") val title: String,
#SerializedName("poster_path") val posterPath: String,
#SerializedName("release_date") val releaseDate: String,
#SerializedName("vote_average") val rating: Float
)
in MoviesResponse.kt
data class MoviesResponse (
#SerializedName("page") val page: Int,
#SerializedName("total_results") val totalResults: Int,
#SerializedName("results") val movies: List<Movie>,
#SerializedName("total_pages") val totalPages: Int
)
in TMDbApi.kt
interface TMDbApi {
#GET("movie/popular")
fun getPopularMovies (
#Query("api_key") apiKey: String,
#Query("language") language: String,
#Query("page") page: Int
): Call<List<MoviesResponse>>
}
in OnGetMoviesCallback.kt
interface OnGetMoviesCallback {
fun onSuccess(movies: List<Movie>)
fun onError()
}
in MainAdapter.kt
class MainAdapter: RecyclerView.Adapter<CustomHolder>(){
var movies: List<Movie> = listOf()
fun MainAdapter(movies: List<Movie>) {
this.movies = movies
}
override fun getItemCount(): Int {
return movies.size
}
override fun onBindViewHolder(holder: CustomHolder, position: Int) {
holder.bind(movies.get(position))
holder?.setOnClick()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CustomHolder {
val layoutInflater = LayoutInflater.from(parent?.context)
val cellForRow = layoutInflater.inflate(R.layout.movie_row, parent, false)
return CustomHolder(cellForRow)
}
}
class CustomHolder(view: View): RecyclerView.ViewHolder(view){
fun bind(result: Movie){
itemView.title.text = result.title
itemView.release_date.text = result.releaseDate
}
}
in MoviesRepository.kt
val BASE_URL: String = "https://api.themoviedb.org/3/"
val LANGUAGE: String = "en-US"
data class MoviesRepository (
val repositroy: MoviesRepository,
val api: TMDbApi
)
object getInstance{
val retrofit: TMDbApi = Retrofit.Builder()
.addConverterFactory(GsonConverterFactory.create())
.client(OkHttpClient().newBuilder().build())
.baseUrl(BASE_URL)
.build()
.create(TMDbApi::class.java)
}
in AllMoviesActivity.kt
class AllMoviesActivity : AppCompatActivity(), Callback<List<MoviesResponse>> {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_all_movies)
rw_main.layoutManager = LinearLayoutManager(this)
rw_main.adapter = MainAdapter()
getMovies()
}
private fun getMovies() {
getInstance.retrofit.getPopularMovies("2B0b0e8d104f0d6130a4fc67848f89e107", LANGUAGE, 1).enqueue(this)
}
override fun onResponse(call: retrofit2.Call<List<MoviesResponse>>, response: Response<List<MoviesResponse>>) {
val moviesResponse = response.body() ?: listOf()
Log.d("Results", moviesResponse.toString())
}
override fun onFailure(call: retrofit2.Call<List<MoviesResponse>>, t: Throwable) {
Toast.makeText(this, "Failed", Toast.LENGTH_SHORT).show()
}
}
And I have added in Manifest file Internet permission. When I run this, I get 'Failed' Toast and I don't see RecyclerView. Can you tell me what I'm missing or what's wrong? I've been searching on the Internet solutions but with no results. Hope you could help me. Thanks!
Looks like you are missing something small. After making a call to the API on Postman, Here's what you expect on your response body:
{
"page": 1,
"total_results": 10000,
"total_pages": 500,
"results": [...
}
Having brought that up, lets digest your lines, in TMDbApi.kt, you have indicated that your expected response is a List<MoviesResponse> which conflicts the response you receive as it is a JSON of type MoviesResponse,
Fix:
...): Call<List<MoviesResponse>>
}
to:
): Call<MoviesResponse>
}
The rest are small fixes to align your response handling.
Head over to AllMoviesActivity.kt and change the following:
class AllMoviesActivity : AppCompatActivity(), Callback<List<MoviesResponse>> {
to
class MainActivity : AppCompatActivity(), Callback<MoviesResponse>
Then on the callback functions, change signatures like so:
override fun onResponse(call: retrofit2.Call<List<MoviesResponse>>, response: Response<List<MoviesResponse>>) {
val moviesResponse = response.body() ?: listOf() ...
to:
override fun onResponse(call: Call<MoviesResponse>, response:Response<MoviesResponse>
) {
val moviesResponse = response.body()//type will be inferred ...
Then finally,
override fun onFailure(call: retrofit2.Call<List<MoviesResponse>>, t: Throwable) {
to:
override fun onFailure(call: Call<MoviesResponse>, t: Throwable) {
That's the basic to get the response and keep the Failure toast at bay.
Parting shot
Make the logs your friend, they can reveal the messy & smallest misses