View not updating after first time triggering Live

2019-08-06 06:08发布

问题:

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

}