So I'm trying to build a simple MVVM interface to load trips from the database retrieved by the ViewModel into my TripFragment. However, I keep getting this error saying that my TripViewModel is null:
Attempt to invoke virtual method 'void androidx.lifecycle.LiveData.observe(androidx.lifecycle.LifecycleOwner, androidx.lifecycle.Observer)' on a null object reference
I can't seem to figure out why it thinks that it's null. I believe the issue is in TripViewModel and has something to do with how the fact that it inherits from AndroidViewModel and that I'm passing the application's context in the constructor.
class TripFragment : Fragment()
private var tripViewModel: TripViewModel? = null
private var textViewTripName: TextView? = null
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val view =
inflater.inflate(R.layout.fragment_trip, container, false)
val recyclerView: RecyclerView = view.recycler_view
recyclerView.layoutManager = LinearLayoutManager(context)
recyclerView.setHasFixedSize(true)
val adapter = TripAdapter()
recyclerView.adapter = adapter
tripViewModel = ViewModelProvider(this).get(TripViewModel(activity!!.application)::class.java)
// This is the line where it crashes, it never executes past this
tripViewModel!!.getAll().observe(viewLifecycleOwner, Observer<List<Trip>> {
fun onChanged(trips: List<Trip>) {
adapter.setTrips(trips)
Log.d("TripFragment", "Went through observer")
}
})
return view
}
class TripViewModel(application: Application): AndroidViewModel(application)
private var tripRepository: TripRepository = TripRepository(application)
private var allTrips: LiveData<List<Trip>> = getAll()
fun insert(trip: Trip) {
tripRepository.insert(trip)
}
fun update(trip: Trip) {
tripRepository.update(trip)
}
fun delete(trip: Trip) {
tripRepository.delete(trip)
}
fun clear() {
tripRepository.clear()
}
fun getAll(): LiveData<List<Trip>> {
return allTrips
}
class TripRepository(application: Application)
private lateinit var tripDao: TripDao
private lateinit var allTrips: LiveData<List<Trip>>
init {
CoroutineScope(IO).launch {
tripDao = AppDatabase.getDatabase(application.applicationContext).tripDao()
allTrips = tripDao.getAll()
}
}
fun insert(trip: Trip) {
CoroutineScope(IO).launch {
tripDao.insert(trip)
}
}
fun update(trip: Trip) {
CoroutineScope(IO).launch {
tripDao.update(trip)
}
}
fun delete(trip: Trip) {
CoroutineScope(IO).launch {
tripDao.delete(trip)
}
}
fun clear() {
CoroutineScope(IO).launch {
tripDao.clear()
}
}
fun getAll(): LiveData<List<Trip>> {
return allTrips
}
Trip entity
#Entity
data class Trip(var title: String, var startDate: String, var endDate: String?) {
#PrimaryKey(autoGenerate = true)
var tid: Long = 0
}
EDIT: I've printed a bunch of debug logs and pinpointed the error at this line in TripRepository.
init {
CoroutineScope(IO).launch {
// tripDao is never assigned properly,
tripDao = AppDatabase.getDatabase(application.applicationContext).tripDao()
allTrips = tripDao.getAll()
}
}
The line tripDao = AppDatabase.getDatabase(application.applicationContext).tripDao() causes an error which turns tripDao into a null variable. The problem has something to do with how I fetch the database, so I've attached my AppDatabase class below.
#Database(entities = [Trip::class], version = 2, exportSchema = false)
abstract class AppDatabase : RoomDatabase() {
abstract fun tripDao(): TripDao
companion object {
// Singleton prevents multiple instances of database opening at the
// same time.
#Volatile
private var INSTANCE: AppDatabase? = null
fun getDatabase(context: Context): AppDatabase {
val tempInstance = INSTANCE
if (tempInstance != null) {
Log.d("AppDatabase", "Returning existing database")
return tempInstance
}
synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
AppDatabase::class.java,
"tripweaver_database"
)
.fallbackToDestructiveMigration()
.build()
INSTANCE = instance
Log.d("AppDatabase", "Returning new database")
return instance
}
}
}
}
I couldn't find the solution to fixing my database access code so I restructured my whole codebase according to the architecture components tutorial by Google on Codelabs. Surely enough this helped me fix the issues I was having. Link for whoever needs this tutorial in the future: https://codelabs.developers.google.com/codelabs/android-room-with-a-view-kotlin/#13
In your class TripViewModel, you have:
private var allTrips: LiveData<List<Trip>> = getAll()
fun getAll(): LiveData<List<Trip>> {
return allTrips
}
So you have a circular arrangement here. allTrips is assigned an initial value of whatever getAll() returns, but it's calling getAll() before allTrips is assigned. So you found a way to assign a null to a non-nullable!
Seems like maybe you meant to put allTrips = tripRepository.getAll().
Related
I'm trying to make an Android application using Kotlin, Room Database and both view model and view model factory.
The issue is that when I try to create the object of the entity I am trying to insert, I just get this warning (which i think in this case makes no sense):
Dialog with warning "unexpected tokens (Use ';' to separate expressions on the same line)"
Anyway, here are my Fragment, FragmentViewModel, FragmentViewModelFactory, Entity, Dao and Database files (or relevant parts):
Fragment:
class RegisterFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val binding = DataBindingUtil.inflate<RegisterFragmentBinding>(
inflater,
R.layout.register_fragment,
container,
false
)
val application = requireNotNull(this.activity).application
val dataSource = NbaCafeDB.getInstance(application).usuariDao
val viewModelFactory = RegisterViewModelFactory(dataSource, application)
val registerViewModel =
ViewModelProvider(this, viewModelFactory).get(RegisterViewModel::class.java)
binding.setLifecycleOwner(this)
...
binding.endavantButton.setOnClickListener { View ->
val email = binding.email.text.toString()
val username = binding.registerUser.text.toString()
val password = binding.registerPassword.text.toString()
val confPassword = binding.confirmPassword.text.toString()
if (email != "" && username != "") {
if (registerViewModel.userExists(username)) {
Toast.makeText(context, "Aquest nom d'usuari ja existeix", Toast.LENGTH_LONG)
.show()
} else if (password == confPassword) {
registerViewModel.insert(username, email, password)
} else {
Toast.makeText(context, "Les contrassenyes no coincideixen", Toast.LENGTH_LONG)
.show()
}
}
}
FragmentViewModel:
class RegisterViewModel(
private val dataSource: UsuariDao, application: Application
) : AndroidViewModel(application) {
fun insert(nomUsuari: String, emailUsuari: String, passUsuari: String) {
val usuari: Usuari(nomUsuari, emailUsuari, passUsuari)
viewModelScope.launch {
dataSource.insert(usuari)
}
}
fun userExists(usuariNom: String): Boolean {
return dataSource.userExists(usuariNom)
}
}
FragmentViewModelFactory:
class RegisterViewModelFactory(
private val dataSource: UsuariDao,
private val application: Application
) : ViewModelProvider.Factory {
#Suppress("Unchecked_cast")
override fun <T : ViewModel> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(RegisterViewModel::class.java)) {
return RegisterViewModel(dataSource, application) as T
}
throw IllegalArgumentException("Unknown View Model Class")
}
}
Entity:
#Entity
data class Usuari(
#PrimaryKey
#ColumnInfo(name = "nom_usuari")
var nomUsuari: String,
#ColumnInfo(name = "email_usuari")
var emailUsuari: String,
#ColumnInfo(name = "password_usuari")
var passwordUsuari: String
)
DAO:
#Dao
interface UsuariDao {
#Insert
suspend fun insert(usuari: Usuari)
#Query ("SELECT EXISTS(SELECT * FROM Usuari WHERE nomUsuari = :usuariNom)")
fun userExists(usuariNom: String): Boolean
}
And finally, Database:
#Database(
entities =
[Beguda::class,
Comanda::class,
Postre::class,
Sandwich::class,
Usuari::class],
version = 3,
exportSchema = false
)
abstract class NbaCafeDB : RoomDatabase() {
abstract val begudaDao: BegudaDao
abstract val comandaDao: ComandaDao
abstract val postreDao: PostreDao
abstract val sandwichDao: SandwichDao
abstract val usuariDao: UsuariDao
companion object {
#Volatile
private var INSTANCE: NbaCafeDB? = null
fun getInstance(context: Context): NbaCafeDB {
synchronized(this) {
var instance = INSTANCE
if (instance == null) {
instance = Room.databaseBuilder(
context.applicationContext,
NbaCafeDB::class.java,
"nba_cafe_database"
)
.fallbackToDestructiveMigration()
.allowMainThreadQueries()
.build()
INSTANCE = instance
}
return instance
}
}
}
}
I hope someone has an answer for this, thanks in advance!!
I'm new to android and room. I'm trying to make a local db but I'm struggling at this point where the database is not empty as the image below proves. But when I try to select any data it returns an empty list or null values. Note that the insert query works fine.
Code:
Entity:
#Entity(tableName = "product_table")
#Parcelize
data class Product(
#PrimaryKey
#SerializedName("id")
val id : String,
#SerializedName("title")
val title : String,
#SerializedName("price")
val price : String,
#SerializedName("category")
val category : String,
#SerializedName("description")
val description : String,
#SerializedName("image")
val image : String,
val color : String,
val size : String
): Parcelable
Dao:
#Dao
interface CartDao {
#Query("SELECT * FROM product_table")
fun get_all_carts(): LiveData<List<Product>>
#Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert_item_to_cart(product: Product) : Long
#Delete
suspend fun delete_item_from_cart(product: Product)
#Query("Delete FROM product_table")
fun delete_all_cart()
}
Database:
#Database(entities = [Product::class], version = 1)
abstract class ProductDatabase : RoomDatabase() {
abstract fun productDao(): CartDao
companion object{
#Volatile
private var INSTANCE: ProductDatabase? = null
fun getDataseClient(context: Context) : ProductDatabase {
if (INSTANCE != null) return INSTANCE!!
synchronized(this) {
INSTANCE = Room
.databaseBuilder(context, ProductDatabase::class.java, "product_database")
.fallbackToDestructiveMigration()
.build()
return INSTANCE!!
}
}
}
}
Repository:
class CartRepository {
companion object {
var productDatabase: ProductDatabase? = null
var product: LiveData<Product>? = null
fun initializeDB(context: Context) : ProductDatabase {
return ProductDatabase.getDataseClient(context)
}
fun get_all_data(context: Context) : List<Product> {
productDatabase = initializeDB(context)
var temp_list = emptyList<Product>()
CoroutineScope(Dispatchers.IO).launch {
temp_list = productDatabase!!.productDao().get_all_carts()
}
return temp_list
}
fun get_first_item(context: Context, input_id : String) : Product {
productDatabase = initializeDB(context)
var temp_item : Product = Product("","","","","","","","")
CoroutineScope(Dispatchers.IO).launch {
temp_item = productDatabase!!.productDao().get_item(input_id)
}
return temp_item
}
}
}
View Model:
#HiltViewModel
class CartFragmentViewModel #Inject constructor(
private val productDao : CartDao
) : ViewModel() {
var products_list : MutableLiveData<List<Product>>
init {
products_list = MutableLiveData()
}
fun get_all_products(context: Context) : List<Product>{
return CartRepository.get_all_data(context)
}
fun get_first_item(context: Context, input_id : String) : Product{
return CartRepository.get_first_item(context, input_id)
}
}
Fragment:
class CartFragment #Inject constructor(
) : Fragment(R.layout.cart_fragment_layout) {
lateinit var cart_list : List<Product>
val cart_adapter = CartRecyclerViewAdapter()
val viewModel by activityViewModels<CartFragmentViewModel>()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
rv_cart.adapter = cart_adapter
rv_cart.layoutManager = LinearLayoutManager(
activity?.applicationContext,
LinearLayoutManager.VERTICAL,
false
)
rv_cart.setHasFixedSize(true)
/*cart_list = viewModel.get_all_products(activity?.applicationContext!!)
cart_adapter.submitList(
cart_list
)
cart_adapter.notifyDataSetChanged()
Log.d(TAG, "Fetched Data: {${cart_list.get(1).title}}")*/
var p = viewModel.get_first_item(activity?.applicationContext!!, "1")
cart_adapter.submitList(
listOf(
p
)
)
cart_adapter.notifyDataSetChanged()
var s = p.title
Log.d(TAG, "Fetched Data: {$s}")
/*viewModel.get_first_item(activity?.applicationContext!!).observe(viewLifecycleOwner, Observer {
cart_adapter.submitList(listOf(it))
cart_adapter.notifyDataSetChanged()
})*/
//viewModel.get_first_item(activity?.applicationContext!!)
}
}
There are many comments and logs in the Fragment Class for the sake of trying to figure what the problem is. I can't really know what is happening and when I use LiveData as return type of dao get functions the app crashes. Hope someone can help me out and thanks for your attention.
The problem with your CartRepository methods.
fun get_first_item(context: Context, input_id : String) : Product {
productDatabase = initializeDB(context)
var temp_item : Product = Product("","","","","","","","")
CoroutineScope(Dispatchers.IO).launch {
temp_item = productDatabase!!.productDao().get_item(input_id)
}
return temp_item
}
In the above method, you are fetching an item in a background thread which means it goes into another thread and allows the return temp_item to always returns immediately, without blocking the code to wait for a result so that's why you are getting null and or empty list.
Solution is :
Make all database operation methods in CartRepository as suspended, see below:
Note: I use an object instead of class
object CartRepository {
var productDatabase: ProductDatabase? = null
fun initializeDB(context: Context) : ProductDatabase {
return ProductDatabase.getDataseClient(context)
}
suspend fun get_all_data(context: Context) : List<Product> {
productDatabase = initializeDB(context)
return productDatabase!!.productDao().get_all_carts()
}
suspend fun get_first_item(context: Context, input_id : String) : Product {
productDatabase = initializeDB(context)
return productDatabase!!.productDao().get_item(input_id)
}
}
And in your viewModel call this suspend function in viewModelScope like below:
#HiltViewModel
class CartFragmentViewModel #Inject constructor(
private val productDao : CartDao
) : ViewModel() {
.....
var productData = MutableLiveData<Product>()
fun get_first_item(context: Context, input_id: String) {
viewModelScope.lauch(Dispatchers.IO){
val data = CartRepository.get_first_item(context, input_id)
withContext(Dispatchers.Main){
productData.value = data
}
}
}
....
In your fragment call get_first_item first then observe you data productData and you can do the same things for other database operations also by following all steps.
I hope this will help you if you dont understand any code just let me know in the comments and please ignore the typos
To use a LiveData with room, firstly, you should attach an observer to the live data.
So instead of returning the value of LiveData from the Repository methods, you should return the Live Data object itself,
And then observe that Livedata in your Viewmodel class.
If you need the code sample, you can ask me in the comment section
Happy Coding :D
I am learning to develop in kotlin with android studio, so I don't have any experience.
I am wanting to insert data into a local database using room database, so far I think I'm doing fine. Now I need to be able to consult those data but I cannot do it, I have searched the internet but I have not been able to solve my problem.
I attach the code.
Class #Entity
class TablasBdApp {
#Entity(tableName = TblConteo.TABLE_NAME)
data class TblConteo(
#PrimaryKey(autoGenerate = true) #ColumnInfo(name = "linea_id") val Linea_Id: Int = 0,
#ColumnInfo (name = "articulo") val Articulo : String?
)
{
companion object {
const val TABLE_NAME = "TablaConteo"
}
}
}
Class #Dao
#Dao
public interface ItblConteoDao {
#Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertArticulo(taskTblConteo: TablasBdApp.TblConteo);
#Query("SELECT * FROM " + TablasBdApp.TblConteo.TABLE_NAME + " ORDER BY Linea_Id desc")
fun getConteoArticulos(): LiveData<List<TablasBdApp.TblConteo>>
}
Class #DataBase
class BaseDeDatos {
#Database(entities = [TablasBdApp.TblConteo::class], version = 1)
abstract class PortatilDataBase : RoomDatabase() {
abstract fun itblconteoDao () : ItblConteoDao
companion object {
private const val DATABASE_NAME = "portatildb"
#Volatile
private var INSTANCE: PortatilDataBase? = null
fun getInstance(context: Context): PortatilDataBase? {
INSTANCE ?: synchronized(this) {
INSTANCE = Room.databaseBuilder(
context.applicationContext,
PortatilDataBase::class.java,
DATABASE_NAME
).build()
}
return INSTANCE
}
}
}
}
Class #Repository
class ConteoRepository(application: Application) {
private val itblConteoDao: ItblConteoDao? = BaseDeDatos.PortatilDataBase.getInstance(application)?.itblconteoDao()
fun insert (tblconteo: TablasBdApp.TblConteo){
if(itblConteoDao != null) InsertAsyncTask(itblConteoDao).execute(tblconteo)
}
fun getConteo(): LiveData<List<TablasBdApp.TblConteo>> {
return itblConteoDao?.getConteoArticulos() ?: MutableLiveData<List<TablasBdApp.TblConteo>>()
}
private class InsertAsyncTask(private val itblConteoDao: ItblConteoDao) :
AsyncTask<TablasBdApp.TblConteo, Void, Void>() {
override fun doInBackground(vararg tblconteos: TablasBdApp.TblConteo?): Void? {
for (tblconteo in tblconteos) {
if (tblconteo != null) itblConteoDao.insertArticulo(tblconteo)
}
return null
}
}
}
Class #CustomAdapter
class CustomAdapter(context: Context): BaseAdapter() {
private val mContext: Context
init{
mContext = context
}
override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {
//val repository = ConteoRepository(application = Application())
var textView = TextView(mContext)
val observer = Observer<List<TablasBdApp.TblConteo>> { conteos ->
if (conteos != null) {
var text = ""
for (conteo in conteos) {
text += conteo.Linea_Id.toString() + " " + conteo.Articulo
}
textView.text = text
}
}
//repository.getConteo().observe(this, observer)
return textView
}
override fun getItem(position: Int): Any {
return "Test Articulo"
}
override fun getItemId(position: Int): Long {
return position.toLong()
}
override fun getCount(): Int {
TODO("Not yet implemented")
}
}
Class #Fragment
class CapturaConteoFragment() : Fragment() {
private lateinit var capturaConteoViewModel: CapturaConteoViewModel
#SuppressLint("ResourceType")
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
capturaConteoViewModel =
ViewModelProviders.of(this).get(CapturaConteoViewModel::class.java)
val root = inflater.inflate(R.layout.fragment_capturaconteo, container, false)
//val textView: TextView = root.findViewById(R.id.text_capturaconteo)
capturaConteoViewModel.text.observe(viewLifecycleOwner, Observer {
//textView.text = it
val botonGuardar: Button = root.findViewById(R.id.btn_guardar)
val textoArticulo : EditText = root.findViewById(R.id.edit_art_upc)
var listview: ListView = root.findViewById(R.id.list_conteo)
botonGuardar.setOnClickListener {
if(textoArticulo.getText().toString().trim().isEmpty()){
Toast.makeText(activity, "Captura un articulo", Toast.LENGTH_LONG).show()
textoArticulo.requestFocus()
}
else{
saveConteo(TablasBdApp.TblConteo(Articulo = textoArticulo.text.trim().toString()))
Toast.makeText(activity, "Articulo guardado", Toast.LENGTH_LONG).show()
//var listaconteo = arrayOf(muestraConteo())
var prodAdapter = CustomAdapter(**this**) **<- This is where it marks error.**
listview?.adapter = prodAdapter
}
}
})
return root
}
private fun saveConteo(tblConteo: TablasBdApp.TblConteo) {
val repository = ConteoRepository(application = Application())
repository.insert(tblConteo)
val conteo = repository.getConteo()
}
private fun showConteo (){
val repository = ConteoRepository(application = Application())
repository.getConteo()
}
}
Thanks for your help.
Regards.
In your DAO you are returning LiveData that means you need to observe it and best practice would be through ViewModel which implies you should MVVM architecture, there are plenty of tutorials and courses on it.
Here is something from google Codelabs
Other way would would be to return just list rather than live data of a list. This is bad practice because you will have to refresh it all the time and do a lot of extra work.
PS your adapter needs fixing too my suggestion is follow codelabs or watch complete tutorial on Room + MVVM.
Hope this helps
I have a Fragment with a RecyclerView in it. I use a ViewModel to hold the LiveData to show from a Room database and try to update the RecyclerView by observing the data in the ViewModel. But the Observer only ever gets called once when I open the fragment. I update the Room databse from a different Fragment than the Observer is on.
Wheter I add a new Event or delete or update one, the Observer never gets called! How can I get the Observer to be called properly? Where is my mistake?
Fragment
The code in onViewCreated does not work in onCreate, it return null on the line val recyclerview = upcoming_recycler.
You also see at the end of onViewCreated where I open a new fragment, from which the database gets updated. Note that the UpcomingFragment is in a different FragmentLayout than the EventEditFragment!
class UpcomingFragment : Fragment(R.layout.fragment_upcoming) {
private val clubDb by lazy {
ClubDatabase.getClubDatabase(requireContext().applicationContext)
}
private val eventAdapter = EventAdapter(null, this)
private val upcomingViewModel: UpcomingViewModel by viewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val recyclerView = upcoming_recycler
recyclerView.layoutManager = LinearLayoutManager(context)
recyclerView.setHasFixedSize(true)
upcomingViewModel.eventsToShow.observe(viewLifecycleOwner, Observer { events ->
Log.d(TAG, "Live data changed in upcomingfragment!!!")
eventAdapter.setData(events.toTypedArray())
})
recyclerView.adapter = eventAdapter
// add a new Event
upcoming_fab.setOnClickListener {
parentFragmentManager.beginTransaction()
.replace(R.id.main_fragment_layout_overlay, EventEditFragment())
.addToBackStack(EVENT_EDIT_FRAGMENT)
.commit()
}
// and more stuff...
}
//the rest of the class
}
ViewModel
class UpcomingViewModel(application: Application) : ViewModel() {
val eventsToShow: LiveData<List<Event>>
init {
val roundToDay = SimpleDateFormat("dd.MM.yyy", Locale.GERMAN)
var today = Date()
today = roundToDay.parse(roundToDay.format(today))!!
val tomorrow = Date(today.time + 86400000L)
eventsToShow = ClubDatabase.getClubDatabase(application.applicationContext).clubDao()
.getEventsByClubIdAfterDate(CURRENT_CLUB_ID, tomorrow)
}
}
EventAdapter
class EventAdapter(
private var dataSet: Array<Event>?,
private val onEventItemClickListener: OnEventItemClickListener
) : RecyclerView.Adapter<EventAdapter.EventViewHolder>() {
class EventViewHolder(val view: View) : RecyclerView.ViewHolder(view)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): EventViewHolder {
val view =
LayoutInflater.from(parent.context).inflate(R.layout.event_item_layout, parent, false)
return EventViewHolder(view)
}
override fun onBindViewHolder(holder: EventViewHolder, position: Int) {
// show the item & add onEventItemClickListener for updating
}
fun setData(new: Array<Event>) {
this.dataSet = new
this.notifyDataSetChanged()
}
override fun getItemCount(): Int {
return dataSet?.size ?: 0
}
}
Database
#Database(
entities = [Event::class, Member::class, RequiredMembersForEvents::class, AttendedMembersForEvents::class],
version = 9,
exportSchema = false
)
#TypeConverters(Converters::class)
abstract class ClubDatabase : RoomDatabase() {
abstract fun clubDao(): ClubDao
companion object {
#Volatile
private var INSTANCE: ClubDatabase? = null
fun getClubDatabase(context: Context): ClubDatabase {
return INSTANCE ?: synchronized(this) {
val instance = INSTANCE
return if (instance != null) {
instance
} else {
Room.databaseBuilder(
context.applicationContext,
ClubDatabase::class.java,
"club-db"
)
.allowMainThreadQueries()
.fallbackToDestructiveMigration()
.build()
}
}
}
}
}
DAO
#Dao
interface ClubDao {
#Query("SELECT * FROM events WHERE clubId = :clubId AND dateTimeFrom > :date ORDER BY dateTimeFrom ASC")
fun getEventsByClubIdAfterDate(clubId: String, date: Date): LiveData<List<Event>>
// the rest of the DAO
}
Check your database singleton implementation, since variable INSTANCE there - is always null. You should set it at first time when you've got the instance of the class. Otherwise your app has a deal with different instances of your Database class.
Probably that causes a problem, when though some changes were made to database, but LiveData's observer for these changes was not triggered.
I am trying, without success, to solve a problem for days. I would like to update my recyclerView whenever the records of a particular model change in the Database (DB Room). I use ViewModel to handle the model data and the list of records are stored in LiveData.
Database
#Database(entities = arrayOf(Additive::class), version = ElementDatabase.DB_VERSION, exportSchema = false)
abstract class ElementDatabase() : RoomDatabase() {
companion object {
const val DB_NAME : String = "element_db"
const val DB_VERSION : Int = 1
fun get(appContext : Context) : ElementDatabase {
return Room.databaseBuilder(appContext, ElementDatabase::class.java, DB_NAME).build()
}
}
abstract fun additivesModels() : AdditiveDao
}
Model
#Entity
class Additive {
#PrimaryKey #ColumnInfo(name = "id")
var number : String = ""
var dangerousness : Int = 0
var description : String = ""
var names : String = ""
var notes : String = ""
var risks : String = ""
var advice : String = ""
}
Dao
#Dao
interface AdditiveDao {
#Query("SELECT * FROM Additive")
fun getAllAdditives() : LiveData<List<Additive>>
#Query("SELECT * FROM Additive WHERE id = :arg0")
fun getAdditiveById(id : String) : Additive
#Query("DELETE FROM Additive")
fun deleteAll()
#Insert(onConflict = REPLACE)
fun insert(additive: Additive)
#Update
fun update(additive: Additive)
#Delete
fun delete(additive: Additive)
}
ViewModel
class AdditiveViewModel(application: Application) : AndroidViewModel(application) {
private var elementDatabase : ElementDatabase
private val additivesModels : LiveData<List<Additive>>
init {
this.elementDatabase = ElementDatabase.get(appContext = getApplication())
this.additivesModels = this.elementDatabase.additivesModels().getAllAdditives()
}
fun getAdditivesList() : LiveData<List<Additive>> {
return this.additivesModels
}
fun deleteItem(additive : Additive) {
DeleteAsyncTask(this.elementDatabase).execute(additive)
}
private class DeleteAsyncTask internal constructor(private val db: ElementDatabase) : AsyncTask<Additive, Void, Void>() {
override fun doInBackground(vararg params: Additive): Void? {
db.additivesModels().delete(params[0])
return null
}
}
}
Fragment
class AdditivesFragment : LifecycleFragment() {
private var viewModel : AdditiveViewModel? = null
private var adapter : AdditivesAdapter? = null
companion object {
fun newInstance() : AdditivesFragment {
val f = AdditivesFragment()
val args = Bundle()
f.arguments = args
return f
}
}
override fun onCreateView(inflater: LayoutInflater?, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater?.inflate(R.layout.fragment_additives, container, false)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
this.adapter = AdditivesAdapter(ArrayList<Additive>())
this.additives_list.layoutManager = GridLayoutManager(this.context, 2, GridLayoutManager.VERTICAL, false)
this.additives_list.adapter = this.adapter
this.viewModel = ViewModelProviders.of(this).get(AdditiveViewModel::class.java)
this.viewModel?.getAdditivesList()?.observe(this, Observer<List<Additive>> { additivesList ->
if(additivesList != null) {
this.adapter?.addItems(additivesList)
}
})
super.onActivityCreated(savedInstanceState)
}
}
Now, my question is why is the observer called only once (at the start of the fragment) and then is not called back again? How can I keep the observer constantly listening to the changes in the DB (insert, update, delete) so that my recyclerView instantly can be updated? Thanks a lot for any suggestion.
This is where you made a mistake:
this.viewModel = ViewModelProviders.of(this).get(AdditiveViewModel::class.java)
you are passing this while you are inside the fragment which is pretty disturbing for some people cause it is not a syntax error but logical. You have to pass activity!! instead, it will be like this:
this.viewModel = ViewModelProviders.of(activity!!).get(AdditiveViewModel::class.java)
UPDATE:
Pass viewLifecycleOwner while being inside fragment while observing the Data
mainViewModel.data(viewLifecycleOwner, Observer{})
If you're using fragmentKtx, you can init viewModel this way:
private val viewModel by viewModels<MainViewModel>()
If You've viewModelFactory:
private val viewModel by viewModels<MainViewModel>{
viewModelFactory
}
with this approach you don't need to call:
// you can omit this statement completely
viewModel = ViewModelProviders.of(this).get(AdditiveViewModel::class.java)
You can simply just start observing the data..