0

I have a LazyColumn connected to a Room database query. I'm expanding on the "Room With a View" sample (https://developer.android.com/codelabs/android-room-with-a-view-kotlin#0), but I'm using LazyColumn instead of a RecyclerView to display a list of table entries.

After inserting an entry (Word), I want the LazyColumn to scroll automatically to the row with the newly inserted entry (or at least make it visible). Note that the list query is sorted alphabetically, the new row will not appear at the end of the list.

// table:
@Entity(tableName = "word_table")
class Word(
    @PrimaryKey @ColumnInfo(name = "word") val text: String
)

// DAO:
@Query("SELECT * FROM word_table ORDER BY word COLLATE NOCASE ASC")
fun getAlphabetizedWords(): Flow<List<Word>>

@Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insert(word: Word)

// repository:
val allWords: Flow<List<Word>> = wordDao.getAlphabetizedWords()

suspend fun insert(word: Word) {
    wordDao.insert(word)
}

// viewModel:
val allWords: LiveData<List<Word>> = repository.allWords.asLiveData()

fun insert(word: Word) = viewModelScope.launch {
    repository.insert(word)
}

// simplified composable:
@Composable
fun WordList() {
   val list by mWordViewModel.allWords.observeAsState(listOf())

   LazyColumn() {
      items(list) { word ->
         Row() {
            Text(word.text)
         }
      }
   }
}

Otherwise everything is working fine, the repository and view model are implemented following general guidelines. Any ideas?

dslamnig
  • 83
  • 1
  • 10
  • I think you are looking for the ListState object. If you go into the LazyColumn object you will see it as one of the optional parameters. ```val listState = rememberLazyListState()``` – eimmer Oct 27 '22 at 19:28
  • Yeah, I have the state in the actual code, and the items are indexed, but I don't know how to use them to get desired behavior. – dslamnig Oct 27 '22 at 19:33

3 Answers3

1
val listState = rememberLazyListState(initialFirstVisibleItemIndex = (list.size - 1))

LazyColumn(state = listState){
    items(list){
        ...
    }
}

You can use the ListState to manipulate scroll position, a bit like you use to with LayoutManager.

eimmer
  • 1,537
  • 1
  • 10
  • 29
  • I see - that might work for an unordered list where the last entry is at the end. But this is an ordered query: ORDER BY word COLLATE NOCASE ASC – dslamnig Oct 27 '22 at 19:47
  • Tried your suggestion, I get this: ``` E/AndroidRuntime: FATAL EXCEPTION: main Process: com.slamnig.jetpacko, PID: 24756 java.lang.IllegalArgumentException: Index should be non-negative (-1) ``` – dslamnig Oct 27 '22 at 19:55
  • Because your list is empty... – eimmer Oct 28 '22 at 16:29
  • 1) Use a ViewModel 2) The state emitted should include the new list and the index of any new item. 3) Use the index to determine where to scroll – eimmer Oct 28 '22 at 16:34
  • The db table is not empty, but the list that Compose sees initially is. I added a check so the index is never negative, but then the code does nothing. Re 2), the problem is how to get the index. I'm trying to find something better than iterating through the list and counting. – dslamnig Oct 28 '22 at 17:39
  • If you are using a collection like a List, it provides methods to do that work for you. – eimmer Oct 29 '22 at 03:03
0

The workflow I would use:

  1. You may compare the old list with the new list like so:

    firstList.filter { it.name !in secondList.map { item -> item.name } }
    
  2. Then use a for loop to count until you find word in list

  3. According to position scroll to position according to number

F.Mysir
  • 2,838
  • 28
  • 39
  • Yeah, this is "old school" and that's how I'm probably going to implement it. I don't even have to compare lists because I know the table id of the inserted row. First I was hoping there was some magic in Jetpack Compose that would do it - seems there's none. Then I tried to find a way to query the room for the row _index_ in the ordered selection, but it seems that the Android version of SQLite does not support windows... Will do some more research in that direction. – dslamnig Oct 28 '22 at 15:48
0

Here's my own take. On insert, the row id is stored in a variable. When the layout recomposes, the index of the last inserted row is found by list search and the LazyColumn is scrolled to the index.

Edit: I modified the code in my answer to be more generalized and in line with the Kotlin idiom.

// table:
@Entity(tableName = "word_table")
class Word(
    val text: String,
    // primary key is autogenerated, also variable 
    // because it has to be modified:
    @PrimaryKey(autoGenerate = true) var id: Long = 0L
)

// DAO: 
@Query("SELECT * FROM word_table ORDER BY text COLLATE LOCALIZED ASC")
fun getAlphabetizedWords(): Flow<List<Word>>

@Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insertBase(word: Word) : Long

suspend fun insert(word: Word)
{
    // explicitly store inserted row id in entity object,
    // it will not happen automatically:
    word.id = insertBase(word)
}

// repository:
val mAllWords: Flow<List<Word>> = wordDao.getAlphabetizedWords()

suspend fun insert(word: Word) 
{
    wordDao.insert(word)
}

// viewModel:
val mAllWords: LiveData<List<Word>> = repository.mAllWords.asLiveData()
var mInsertedId: Long = -1L; // the id of the last inserted row

fun insert(word: Word) = viewModelScope.launch 
{
    repository.insert(word)
    mInsertedId = word.id // store id
}

fun getInsertedId() : Long
{
    val id = mInsertedId
    mInsertedId = -1L // clear stored value
    return id
}

// helper used by the "Add" button composable (not shown here):
fun addWordToDb(text: String)
{
    if(!text.isBlank())
        mWordViewModel.insert(Word(text))
}

// simplified composable:
@Composable
fun WordList(viewModel: WordViewModel) 
{
   val list by viewModel.allWords.observeAsState(listOf())
   val listState = rememberLazyListState()

    // scroll to inserted item:
    LaunchedEffect(list)
    {
        val insertedId = viewModel.getInsertedId()

        if(insertedId != -1L){
            // find index of last inserted item in list:
            val index = list.indexOfFirst{ it.id == insertedId }

            if(index != -1)
                listState.animateScrollToItem(index)
        }
    }

   // display list:
   LazyColumn(state = listState) 
   {
      items(list) { word ->
         Row() {
            Text(word.text)
         }
      }
   }
}

Thanks to F.Mysir and eimmer for their input. BTW, I tried to get the index from SQLite/Room like this:

@Query("SELECT COUNT(*) FROM word_table WHERE LOWER(text) < LOWER(:searchString)")
suspend fun getWordIndex(searchString: String): Int

... and it (almost) works for ASCII, but fails for strings with non-ASCII characters, even if the database locale is set to a language with a different alphabet.

dslamnig
  • 83
  • 1
  • 10