Pipmin
Pipmin

Reputation: 11

Unable to get the ViewModel to show data after querying the ROOM database with parameters (Kotlin)

First of all I have to say that I am rather new to Kotlin, and after spending 5 days (35+ hours) trying to google this issue and trying countless different options (similar questions on stack overflow, documentation and tutorials found on Google, other Kotlin projects on GitHub, even using my own server and database wondering if the issue has something to do with ROOM) I have to give up and ask for help, as this app is for an assignment I am supposed to finish in a couple of weeks.

Description of the app (Expense tracker):

I feel like I have tried literally everything - Especially Transformations.switchMap as many results seem to point that way, but I haven't made any progress whatsoever. I have browsed through dozens of apps on GitHub to see how theirs work, trying to implement the logic in mine, but even if after all the time I manage to adjust the code so that I get no errors, nothing is still shown on my RecyclerView.

Here are the snippets from the classes that I believe are relevant to this issue (in the order from most relevant to somewhat relevant, some parts of the code omitted to not flood this post completely):

TotalsFragment:

import android.os.Bundle
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.AdapterView
import android.widget.ArrayAdapter
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.example.expensetracker.R
import com.example.expensetracker.model.Category
import com.example.expensetracker.model.Expense
import com.google.android.material.snackbar.Snackbar
import kotlinx.android.synthetic.main.fragment_totals.*
import java.util.*
import kotlin.collections.ArrayList

class TotalsFragment : Fragment() {

    private val totals: MutableList<Expense> = ArrayList()
    private val totalAdapter = ExpenseAdapterTotals(totals)
    private lateinit var viewModel: TotalsViewModel

    // 
    // Bunch of variables omitted 
    //  


    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {

        // Initialize the ViewModel
        viewModel = ViewModelProviders.of(activity as AppCompatActivity).get(TotalsViewModel::class.java)

        // Inflate the layout for this fragment
        return inflater.inflate(R.layout.fragment_totals, container, false)
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        updateUI()
        initViewModel()
        initViews()
        initCategorySpinner()
        initTimeSpinner()

        // For getting data and updating the UI after the button is clicked
        btn_show.setOnClickListener {
            updateRvData()
            updateTotals()
            updateUI()
        }

    }

    private fun initViewModel(){
        viewModel = ViewModelProviders.of(this).get(TotalsViewModel::class.java)

        viewModel.totals.observe(this, Observer {
            if (totals.isNotEmpty()) {
                totals.clear()
            }
            totals.addAll(it!!)

            totalAdapter.notifyDataSetChanged()
        })

    }

    private fun initViews(){
        createItemTouchHelper().attachToRecyclerView(rv_expenses_totals)
        rv_expenses_totals.apply {
            layoutManager = LinearLayoutManager(activity)
            rv_expenses_totals.adapter = totalAdapter
            rv_expenses_totals.addItemDecoration(DividerItemDecoration(this.context, DividerItemDecoration.VERTICAL))
        }
    }
// Code omitted

The part sending the query forward: viewModel.getTotals(queryString)

TotalsViewModel:

import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Transformations
import com.example.expensetracker.database.ExpenseRepository
import com.example.expensetracker.model.Expense
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch

class TotalsViewModel(application: Application) : AndroidViewModel(application) {

    private val ioScope = CoroutineScope(Dispatchers.IO)
    private val expenseRepository = ExpenseRepository(application.applicationContext)

    var query = MutableLiveData<String>()
    val totals: LiveData<List<Expense>> = Transformations.switchMap(query, ::temp)
    private fun temp(query: String) = expenseRepository.getTotals(query)

    fun getTotals(queryString: String) = apply { query.value = queryString }


    fun insertExpense(expense: Expense) {
        ioScope.launch {
            expenseRepository.insertExpense(expense)
        }
    }

    fun deleteExpense(expense: Expense) {
        ioScope.launch {
            expenseRepository.deleteExpense(expense)
        }
    }
}

ExpenseDao:

@Dao
interface ExpenseDao {

    // sort by order they were added, newest on top
    @Query("SELECT * FROM expense_table ORDER BY id DESC LIMIT 15")
    fun getExpensesMain(): LiveData<List<Expense>>

    // get data for totals
    @Query("SELECT * FROM expense_table WHERE :queryString")
    fun getTotals(queryString: String): LiveData<List<Expense>>

// Rest of the queries omitted

ExpenseRepository:

class ExpenseRepository(context: Context) {

    private var expenseDao: ExpenseDao

    init {
        val expenseRoomDatabase = ExpenseRoomDatabase.getDatabase(context)
        expenseDao = expenseRoomDatabase!!.expenseDao()
    }

    fun getExpensesMain(): LiveData<List<Expense>> {
        return expenseDao.getExpensesMain()
    }

    fun getTotals(queryString: String): LiveData<List<Expense>> {
        return expenseDao.getTotals(queryString)
    }

// Code omitted

ExpenseRoomDatabase:

@Database(entities = [Expense::class], version = 1, exportSchema = false)
abstract class ExpenseRoomDatabase : RoomDatabase() {

    abstract fun expenseDao(): ExpenseDao

    companion object {
        private const val DATABASE_NAME = "EXPENSE_DATABASE"

        @Volatile
        private var expenseRoomDatabaseInstance: ExpenseRoomDatabase? = null

        fun getDatabase(context: Context): ExpenseRoomDatabase? {
            if (expenseRoomDatabaseInstance == null) {
                synchronized(ExpenseRoomDatabase::class.java) {
                    if (expenseRoomDatabaseInstance == null) {
                        expenseRoomDatabaseInstance = Room.databaseBuilder(
                            context.applicationContext,
                            ExpenseRoomDatabase::class.java, DATABASE_NAME
                        ).build()
                    }
                }
            }
            return expenseRoomDatabaseInstance
        }
    }
}

ExpenseAdapterTotals:

class ExpenseAdapterTotals(private val totals: MutableList<Expense>) : RecyclerView.Adapter<ExpenseAdapterTotals.ViewHolder>() {

    lateinit var context: Context

    override fun getItemCount(): Int {
        return totals.size
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        context = parent.context
        return ViewHolder(LayoutInflater.from(context).inflate(R.layout.item_expense_totals, parent, false))
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        holder.bind(totals[position])
    }

    inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        fun bind(totals: Expense) {
            itemView.tv_expense_totals.text = totals.expense
            itemView.tv_category_totals.text = totals.category
            itemView.tv_date_totals.text = totals.date
            itemView.tv_total_totals.text = totals.total.toString()
        }
    }
}

I have the following dependencies in my app build.gradle:

    //Navigation
    implementation "androidx.navigation:navigation-fragment-ktx:2.0.0"
    implementation "androidx.navigation:navigation-ui-ktx:2.0.0"


    // ViewModel and LiveData
    def lifecycle_version = "2.1.0"
    implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version"

    // Room.
    def room_version = "2.1.0-rc01"
    implementation "androidx.room:room-runtime:$room_version"
    kapt "androidx.room:room-compiler:$room_version"
    implementation "androidx.room:room-ktx:$room_version"
....

So, this code is my most recent attempt but it has changed several times. I am not getting any error messages, but nothing is being shown either.

My goal in a nutshell: When I click the button (btn_show), it should create the query string (which it does) and the RecyclerView in that fragment should update to show the desired results (which it doesn't). I assume the problem is somewhere between the ViewModel and the Fragment, but like I said, I am still a beginner, and this is the first time I am actually working on my purely own app.

Thank you so much in advance for any help and tips, and feel free to ask if I left out anything you'd like to know.

Upvotes: 1

Views: 731

Answers (2)

EpicPandaForce
EpicPandaForce

Reputation: 81578

Replace your ExpenseAdapterTotals: RecyclerView.Adapter< with ExpenseAdapterTotals: ListAdapter<.

Then, remove anything that shows a MutableList, or at least rename it to List.

Now you see that you don't need clear() and addAll(). You can just call submitList() on the ListAdapter and it works.

var query = MutableLiveData<String>()

Make this into a val so that you cannot mess it up by accident.

viewModel.totals.observe(this, Observer {

Should be viewLifecycleOwner if you set up this observer in onViewCreated.


But as the RecyclerView is not showing, I actually think it's probably a question of incorrect layout parameters, for example wrap_content height for the RecyclerView, and now it doesn't update its height because of setHasFixedSize(true).

Upvotes: 0

MakinTosH
MakinTosH

Reputation: 642

Just a few things I noticed: In you totals fragment why are you initializing viewmodel two times in onCreate and the in onViewCreated?

Also you're not submitting your totals values into your adapter. totals.addAll(it!!) this just adds them to the list that you have declared in your totalFragment (you don't need it at all,because you're getting all your totals from viewmodel first of all.)

Upvotes: 0

Related Questions