1

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())
        }
    }

}
César Alberca
  • 2,321
  • 2
  • 20
  • 32
  • 1
    Is the `formattedDebts` Transformations code executed when you insert new debt? – John O'Reilly Jan 15 '18 at 17:13
  • It's not because the LiveData does not detect any changes. I must say the more I look it the more I think it has to do with a dependency injection problem. Here is a similar problem, however I haven't got it working: https://stackoverflow.com/questions/45196872/livedata-not-updating-data-from-one-activity-to-another-activity-android – César Alberca Jan 16 '18 at 14:34
  • 1
    Can you show code of background job? Cause I already have something like this in place which is working alright. Secondly make sure you are maintaining the same singleton by debugging or logging instance of `DAO` from `Activity` and Background Job. – Akshay Chordiya Jan 17 '18 at 07:44
  • Ok, finally shedding some light on the problem! Thank you! It seems that the repository has two separate instances. One for the app itself and other one for the job. The thing is that I need a repository injected in the job because I need to insert a new record. How can I make the repository in the job be the same as the app? If I remove `DaggerGraphBuilder.build(applicationContext as FineApplication).inject(this)` I get an error when running the job. – César Alberca Jan 17 '18 at 09:13
  • Hey @CésarAlberca, did you ever come up with a solution to this issue? I've got exactly the same issue right now, and I can't seem to find any solution. – Nev Sep 03 '18 at 14:26
  • @Nev, sorry I decided to move away from Dagger and go with Koin (https://github.com/InsertKoinIO/koin), sooo much happy now ;) – César Alberca Sep 03 '18 at 19:50
  • 1
    Hey @CésarAlberca, thanks for the reply. I'll take a look at Koin! – Nev Sep 05 '18 at 11:26

0 Answers0