I'm building a debt application in Android using Dagger 2, Room and MVVM. My problem lies with the reactivity of my main view, where a list of debts are shown and you can tick them off.
When this activity is launched all the debts are loaded correctly, however when I insert a new debt the view is not updated accordingly.
The strange part is that when I tick one of them off the view refreshes as expected.
After much debugging I can infer that the problem has to do with the Lifecycle of the ViewModel, because the debts are created using a background job. If I hardcode in the constructor of the ViewModel a new insertion in the database the view updates as expected.
Here is my code for the Room Dao:
@Dao
interface DebtDao {
@Insert(onConflict = OnConflictStrategy.ABORT)
fun insert(debt: Debt)
@Query("SELECT * FROM debts WHERE id=:debtId")
fun findOne(debtId: Long): Debt
@Query("SELECT * FROM debts")
fun findAll(): LiveData<List<Debt>>
@Query("""
SELECT
debts.*,
p.name AS product_name,
d.name AS debtor_name,
c.name AS creditor_name
FROM debts
INNER JOIN products p ON debts.product_id = p.id
INNER JOIN debtors d ON debts.debtor_id = d.id
INNER JOIN creditors c ON debts.creditor_id = c.id
""")
fun findAllWithProductDebtorAndCreditor(): LiveData<List<DebtWithDebtorCreditorAndProduct>>
@Update
fun update(debt: Debt)
}
The activity:
class DebtsListActivity : AppCompatActivity() {
@Inject
lateinit var viewModelFactory: ViewModelProvider.Factory
override fun onCreate(savedInstanceState: Bundle?) {
AndroidInjection.inject(this)
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val mainViewModel = ViewModelProviders.of(this, viewModelFactory).get(DebtsListViewModel::class.java)
val debtsListAdapter = DebtsListAdapter(ArrayList()) {
mainViewModel.payDebt(it.debt.id)
showSuccess("Debt updated with success!")
}
mainViewModel.formattedDebts.observe(this, Observer<List<DebtWithDebtorCreditorAndProduct>> {
if (it != null) {
// This is only invoked when launching initially the activity or then ticking on of the debts as paid not when inserting a new debt
debtsListAdapter.addItems(mainViewModel.getUnpaidDebts())
}
})
rvDebts.layoutManager = LinearLayoutManager(this)
rvDebts.adapter = debtsListAdapter
}
}
The view model:
class DebtsListViewModel @Inject constructor(var debtDao: DebtDao) : ViewModel() {
private var debts: LiveData<List<Debt>> = debtDao.findAll()
var formattedDebts: LiveData<List<DebtWithDebtorCreditorAndProduct>> = Transformations.switchMap(debts) {
debtDao.findAllWithProductDebtorAndCreditor()
}
fun payDebt(debtId: Long) {
val paidDebt = debtDao.findOne(debtId)
debtDao.update(paidDebt.copy(paid = true))
}
fun getUnpaidDebts(): List<DebtWithDebtorCreditorAndProduct> =
formattedDebts.value?.filter { !it.debt.paid }.orEmpty()
}
What I would like to do is to notify a formatted debt list containing all the information I want.
Edit:
This is the code for the background job:
class GenerateDebtJobService : JobService() {
@Inject
lateinit var debtDao: DebtDao
@Inject
lateinit var productDao: ProductDao
@Inject
lateinit var productsDebtorsDao: ProductDebtorDao
@Inject
lateinit var productCreditorsDao: ProductCreditorDao
override fun onStartJob(params: JobParameters): Boolean {
DaggerGraphBuilder.build(applicationContext as FineApplication).inject(this)
val productId = params.extras.getLong("id")
val product = productDao.findOne(productId)
val productCreditor = productCreditorsDao.findOneByProduct(productId)
val debtors = productsDebtorsDao.findAllByProduct(productId)
// When the bill day is reached for a given product the debtors list associated with that product is looped through and a new debt is created
debtors.forEach {
debtDao.insert(Debt(productId = it.productProductId, debtorId = it.productDebtorId, quantity = product.recurringCost, date = DateTime().toString(), creditorId = productCreditor.productCreditorId))
}
return false
}
override fun onStopJob(params: JobParameters): Boolean {
Log.e("GenerateDebtJob", "job finished")
return false
}
companion object {
fun build(application: Application, productId: Long, periodicity: Days, startDay: Days) {
val serviceComponent = ComponentName(application, GenerateDebtJobService::class.java)
val bundle = PersistableBundle()
bundle.putLong("id", productId)
val builder = JobInfo.Builder(productId.toInt(), serviceComponent)
.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)
// .setPeriodic(TimeUnit.DAYS.toMillis(periodicity.days.toLong()))
.setOverrideDeadline(1_000)
.setExtras(bundle)
(application.getSystemService(Context.JOB_SCHEDULER_SERVICE) as JobScheduler).schedule(builder.build())
}
}
}