Reputation: 11
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
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
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