How Do We Handle Runtime Errors From Room-Generated Paging Code?
Asked Answered
K

0

6

In this sample project, I have a DAO that is using Paging 2 on an FTS4 table to search it, using a user-supplied search expression:

  @Query("SELECT snippet(paragraphsFts) FROM paragraphs JOIN paragraphsFts "+
      "ON paragraphs.id == paragraphsFts.rowid WHERE paragraphsFts.prose "+
      "MATCH :search ORDER BY sequence")
  abstract fun filtered(search: String): DataSource.Factory<Int, String>

I have a BookRepository with a filtered() function that just passes through the call to the DAO, and I have a SearchViewModel that converts the DataSource.Factory to a LiveData:

class SearchViewModel(search: String, repo: BookRepository) : ViewModel() {
  val paragraphs = repo.filtered(search).toLiveData(pageSize = 15)
}

And I have a fragment that observes the results:

    vm.paragraphs.observe(this.viewLifecycleOwner) {
      adapter.submitList(it)
    }

This works fine, so long as the user does not enter a search expression with a syntax error. In that case, Room throws an exception, and I cannot see where I can catch it.

2020-08-30 08:50:50.923 13948-14019 E/AndroidRuntime: FATAL EXCEPTION: arch_disk_io_3
    Process: com.commonsware.room.pagedfts, PID: 13948
    android.database.sqlite.SQLiteException: malformed MATCH expression: [-9] (code 1 SQLITE_ERROR)
        at android.database.sqlite.SQLiteConnection.nativeExecuteForCursorWindow(Native Method)
        at android.database.sqlite.SQLiteConnection.executeForCursorWindow(SQLiteConnection.java:942)
        at android.database.sqlite.SQLiteSession.executeForCursorWindow(SQLiteSession.java:838)
        at android.database.sqlite.SQLiteQuery.fillWindow(SQLiteQuery.java:62)
        at android.database.sqlite.SQLiteCursor.fillWindow(SQLiteCursor.java:153)
        at android.database.sqlite.SQLiteCursor.getCount(SQLiteCursor.java:140)
        at android.database.AbstractCursor.moveToPosition(AbstractCursor.java:232)
        at android.database.AbstractCursor.moveToFirst(AbstractCursor.java:271)
        at androidx.room.paging.LimitOffsetDataSource.countItems(LimitOffsetDataSource.java:89)
        at androidx.room.paging.LimitOffsetDataSource.loadInitial(LimitOffsetDataSource.java:119)
        at androidx.paging.PositionalDataSource.dispatchLoadInitial(PositionalDataSource.java:286)
        at androidx.paging.TiledPagedList.<init>(TiledPagedList.java:107)
        at androidx.paging.PagedList.create(PagedList.java:229)
        at androidx.paging.PagedList$Builder.build(PagedList.java:388)
        at androidx.paging.LivePagedListBuilder$1.compute(LivePagedListBuilder.java:206)
        at androidx.paging.LivePagedListBuilder$1.compute(LivePagedListBuilder.java:171)
        at androidx.lifecycle.ComputableLiveData$2.run(ComputableLiveData.java:101)
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167)
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
        at java.lang.Thread.run(Thread.java:919)

The exception is occurring on a background thread used by the LiveData created by toLiveData(). So a try/catch in my paragraphs declaration will not work, as the exception is not thrown during creation of the LiveData. Wrapping my observe() call in a try/catch does not work, as the exception is not thrown on that thread.

If I were not using Paging, and instead had my DAO's filtered() function be a suspend function, I would get the exception directly and can catch and handle it in the viewmodel. Or, if filtered() were a blocking call, I would get the exception directly. Or, if filtered() returned an RxJava type, I would get the exception through normal RxJava exception handling approaches (e.g., onError lambdas). But I cannot quite figure out where I should be intercepting this exception when using Paging.

It's possible that Paging 3 has a solution for this, which would be great to know, though Paging 3 right now is still in alpha.

While I have not tried it, I suspect that if I supply my own Executor to toLiveData() that I could catch the exception there. Even if that is a possible workound, that seems like I am relying on a side effect (replacing how we schedule work on threads to be able to catch an exception).

Kindliness answered 30/8, 2020 at 13:13 Comment(3)
Paging2 never really had the concept of errors built into it, and Paging3 tries to address this by adding an explicit error result type to PagingSource to signal to presentation layer that the error is recoverable. I'm on mobile so I haven't had a chance to try out your sample yet, but wanted to at least answer the latter question! My initial thought is to see if we can either wrap the DataSource generated by Room, or setup some specific error handling for a DAO.Spiky
Why do you not using LIKE instead of MATCH to prevent this error?Valkyrie
@aminography: Because I specifically want to use MATCH to enable full FTS syntax and to behave like a normal search engine. Suppose the user types in foo. LIKE '%foo%' matches food, which is not typical search engine behavior. SQLite's documentation for FTS emphasizes the use of MATCH.Kindliness

© 2022 - 2024 — McMap. All rights reserved.