I am currently building a fragment, and within the fragment contains 2 paging adapter sharing the same data. When I update the data, only one of the paging adapter receives the updated data while the other does not.
On first start up. for reference (backdrop - top adapter/viewpager, poster - bottom adapter/viewpager)
first item =
second item =
third item =
when I change the movie category, only the bottom paging adapter receive the updated data
updated first item =
updated second item =
updated third item =
as seen in the image, the bottom paging adapter have successfully updated its image while the top paging adapter does not
additional information:
I am using Remote Mediator to store network data to ROOM Database.
Both top and bottom list is displayed using viewpager2, page transformer is applied to both viewpager. To add on, top viewpager scroll is dependent on the bottom viewpager (i.e. top viewpager will only scroll if bottom view pager scrolls).
depending on which paging adapter's submitData function is called first, its data will be updated, while the paging adapter after it won't update its data.
for the case below, the bottom viewPager(poster) will be updated while the top viewPager(backdrop) won't be updated.
collectLatestLifecycleFlow(viewModel.movieList) { pagingData ->
posterPagingAdapter.submitData(pagingData)
backdropPagingAdapter.submitData(pagingData)
}
I expect both paging adapter to be updated when I update the movie category.
so far as a temporary solution I created two flow collectors to collect the data so that both viewpager can update their data, however this is inefficient as it will do another Api Call which is redundant.
collectLatestLifecycleFlow(viewModel.movieList) { pagingData ->
backdropPagingAdapter.submitData(pagingData)
}
collectLatestLifecycleFlow(viewModel.movieList) { pagingData ->
posterPagingAdapter.submitData(pagingData)
}
below is the code I use to in ViewModel to get the data
val movieList = selectedMovieCategory.flatMapLatest {
Timber.i("selected category: $it")
when (it) {
MoviePagingCategory.NOW_PLAYING.categoryName -> {
moviesUseCase.getNowPlayingPagingData(loadSinglePage = true)
}
MoviePagingCategory.POPULAR.categoryName -> {
moviesUseCase.getPopularMoviesPagingData(loadSinglePage = true)
}
MoviePagingCategory.TRENDING.categoryName -> {
moviesUseCase.getTrendingMoviesPagingData(loadSinglePage = true)
}
else -> throw Exception("No listed category")
}.cachedIn(viewModelScope)
}
for PagingAdapter's diff util. do note that id here is a manually generated id instead of movieId provided by the Api. the reason for this is to store multiple movie types in single room table.
companion object DiffUtilCallback : DiffUtil.ItemCallback<Result>() {
override fun areItemsTheSame(oldItem: Result, newItem: Result): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: Result, newItem: Result): Boolean {
return oldItem == newItem
}
}
Related
So I am building this basic weight-tracking app with a Room database, and two recyclerviews.
I am trying to learn how to navigate from the starting recyclerview (which shows a list of the
lowest weight entries of each date) to another recyclerview that shows every entry that was listed
on that date that was clicked in the first recyclerview.
I can only see the results I want if I manually set the date in the viewmodel.
I can't seem to figure out how to achieve this without manually setting the date and I was hoping
someone could help.
I have tried countless times to research this but all I find is how to navigate to a specific
item. Any help is definitely appreciated.
Here are my relevant code snippets.
My Dao Query:
#Query(
"SELECT date_of_weight_entry as date, weight_entry as weight, day_of_weight_entry as day, \n" +
" weight_entry_notes as notes, \n" +
" time_of_weight_entry as time FROM weight \n" +
" where date = :dateOfEntry"
)
fun getAllEntriesForEachDateFragment(dateOfEntry: String): Flow<List<WeightEntriesForEachDate>>
The relevant code from my viewmodel:
var _dateOfEntry = MutableLiveData<String>()
var dateOfEntry: LiveData<String> = _dateOfEntry
var entriesForEachDate: LiveData<List<WeightEntriesForEachDate>> = weightDao
.getAllEntriesForEachDateFragment(_dateOfEntry.value.toString()).asLiveData()
fun getSelectedDate(selectedDate: String):String {
_dateOfEntry.value = selectedDate
return selectedDate
}
This is from the first recyclerview adapter's on Bind View Holder:
(When using Logcat, the dateOfEntry in viewmodel is set correctly, but results do not get
displayed correctly in the next recyclerview after being clicked),
(disregard the navigation arg as it used for a different part of the app):
override fun onBindViewHolder(holder: MinWeightRecordViewHolder, position: Int) {
val current = getItem(position)
holder.itemView.setOnClickListener {
viewModel.getSelectedDate(holder.itemView.tv_weight_record_date.text.toString())
val action = LowestWeightForEachDayFragmentDirections
.actionLowestWeightForEachDayFragmentToEntriesByDateFragment(
date = viewModel.dateOfEntry.value.toString() <-DISREGARD THIS
)
holder.itemView.findNavController().navigate(action)
}
Here is the OnViewCreated code from my entries by date fragment:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val dateAdapter = EntriesByDateAdapter(viewModel)
binding.rvWeightRecordByEntryForListedDate.adapter = dateAdapter
viewModel.entriesForEachDate.observe(this.viewLifecycleOwner) { entries ->
entries
dateAdapter.submitList(entries)
}
binding.rvWeightRecordByEntryForListedDate.layoutManager =
LinearLayoutManager(this.context)
}
Now if I go back into my viewmodel and manually set the date (like below) it gives me the
correct info in the second recyclerview:
var _dateOfEntry = MutableLiveData<String>("02-03-2023")
var dateOfEntry: LiveData<String> = _dateOfEntry
I just need help figuring out what step I am missing. Hopefully these code snippets were enough.
I am making a dynamic UI using kotlin and Jetpack compose and storing the information in an object box database.
The aim is that i will have a composable that starts off with 1 initial item that is empty and when the contents of the textbox have been filled in would allow the red "+" button to be clicked and then another textfield would appear. These values will need to be able to be edited constantly all the way until the final composable value is stored. The button changes colour currently and the states are fine with the button so i can add and remove rows
The data comes in as a string and is converted into a Hashmap<Int, String>. The int is used to store the position in the map being edited and the string would be the text value.
Using log messages i see that the information is updated in the list and for recomp sake i instantly store the value of the edited list in a converted json string.
At the moment:
When i scroll past the composable it resets and looks like the initial state (even if i have added multiple rows)
Log messages show that my hashmap has the values from before e.g. {"0":"asdfdsa"} but the previous positions are ignored and as the previous information would still be present but not shown on the UI when i enter it into the first field again (the others are not visible at the time) {"0":"asdfdsa","0":"hello"}. This would later cause an error when trying to save new data to the list because of the duplicate key
In the composables my hashmap is called textFields and is defined like this. Number is used to determine how many textfields to draw on the screen
val textFields = remember { getDataStringToMap(data.dataItem.dataValue) }
val number = remember { mutableStateOf(textFields.size) }
the method to getDataStringToMap is created like this
private fun getDataMapToString(textFieldsMap: HashMap<Int, String>): String {
val gson = Gson()
val newMap = hashMapOf<Int, String>()
for (value in textFieldsMap){
if (value.value .isNotBlank()){
newMap[value.key] = value.value
}
}
return gson.toJson(newMap)
}
and the method to getDataStringToMap is created like this (I explicitly define the empty hashmap type because its more readable for me if i can see it)
private fun getDataStringToMap(textsFieldsString: String): HashMap<Int, String> {
val gson = Gson()
return if (textsFieldsString.isBlank()) {
hashMapOf<Int, String>(0 to "")
} else {
val mapType = HashMap<Int, String>().javaClass
gson.fromJson(textsFieldsString, mapType)
}
the composables for the textfields are called like this
items(number.value) { index ->
listItem(
itemValue = textFields[index].orEmpty(),
changeValue = {
textFields[index] = it
setDataValue(getDataMapToString(textFields))
},
addItem = {
columnHeight.value += itemHeight
scope.launch {
scrollState.animateScrollBy(itemHeight)
}
},
deleteItem = {
columnHeight.value -= itemHeight
scope.launch {
scrollState.animateScrollBy(-itemHeight)
}
},
lastItem = index == number.value - 1,
index = index
)
}
Edited 30/12/2022
Answer from #Arthur Kasparian solved issues. Change to rememberSaveable retains the UiState even on scroll and recomp.
Now just to sort out which specific elements are removed and shown after :D
The problem is that remember alone does not save values on configuration changes, whereas rememberSaveable does.
You can read more about this here.
I'm using Paging Library to load data from network using ItemKeyedDataSource. After fetching items user can edit them, this updates are done inside in Memory cache (no database like Room is used).
Now since the PagedList itself cannot be updated (discussed here) I have to recreate PagedList and pass it to the PagedListAdapter.
The update itself is no problem but after updating the recyclerView with the new PagedList, the list jumps to the beginning of the list destroying previous scroll position. Is there anyway to update PagedList while keeping scroll position (like how it works with Room)?
DataSource is implemented this way:
public class MentionKeyedDataSource extends ItemKeyedDataSource<Long, Mention> {
private Repository repository;
...
private List<Mention> cachedItems;
public MentionKeyedDataSource(Repository repository, ..., List<Mention> cachedItems){
super();
this.repository = repository;
this.teamId = teamId;
this.inboxId = inboxId;
this.filter = filter;
this.cachedItems = new ArrayList<>(cachedItems);
}
#Override
public void loadInitial(#NonNull LoadInitialParams<Long> params, final #NonNull ItemKeyedDataSource.LoadInitialCallback<Mention> callback) {
Observable.just(cachedItems)
.filter(() -> return cachedItems != null && !cachedItems.isEmpty())
.switchIfEmpty(repository.getItems(..., params.requestedLoadSize).map(...))
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(response -> callback.onResult(response.data.list));
}
#Override
public void loadAfter(#NonNull LoadParams<Long> params, final #NonNull ItemKeyedDataSource.LoadCallback<Mention> callback) {
repository.getOlderItems(..., params.key, params.requestedLoadSize)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(response -> callback.onResult(response.data.list));
}
#Override
public void loadBefore(#NonNull LoadParams<Long> params, final #NonNull ItemKeyedDataSource.LoadCallback<Mention> callback) {
repository.getNewerItems(..., params.key, params.requestedLoadSize)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(response -> callback.onResult(response.data.list));
}
#NonNull
#Override
public Long getKey(#NonNull Mention item) {
return item.id;
}
}
The PagedList created like this:
PagedList.Config config = new PagedList.Config.Builder()
.setPageSize(PAGE_SIZE)
.setInitialLoadSizeHint(preFetchedItems != null && !preFetchedItems.isEmpty()
? preFetchedItems.size()
: PAGE_SIZE * 2
).build();
pagedMentionsList = new PagedList.Builder<>(new MentionKeyedDataSource(mRepository, team.id, inbox.id, mCurrentFilter, preFetchedItems)
, config)
.setFetchExecutor(ApplicationThreadPool.getBackgroundThreadExecutor())
.setNotifyExecutor(ApplicationThreadPool.getUIThreadExecutor())
.build();
The PagedListAdapter is created like this:
public class ItemAdapter extends PagedListAdapter<Item, ItemAdapter.ItemHolder> { //Adapter from google guide, Nothing special here.. }
mAdapter = new ItemAdapter(new DiffUtil.ItemCallback<Mention>() {
#Override
public boolean areItemsTheSame(Item oldItem, Item newItem) {
return oldItem.id == newItem.id;
}
#Override
public boolean areContentsTheSame(Item oldItem, Item newItem) {
return oldItem.equals(newItem);
}
});
, and updated like this:
mAdapter.submitList(pagedList);
You should use a blocking call on your observable. If you don't submit the result in the same thread as loadInitial, loadAfter or loadBefore, what happens is that the adapter will compute the diff of the existing list items against an empty list first, and then against the newly loaded items. So effectively it's as if all items were deleted and then inserted again, that is why the list seems to jump to the beginning.
You're not using androidx.paging.ItemKeyedDataSource.LoadInitialParams#requestedInitialKey in your implementation of loadInitial, and I think you should be.
I took a look at another implementation of ItemKeyedDataSource, the one used by autogenerated Room DAO code: LimitOffsetDataSource. Its implementation of loadInitial contains (Apache 2.0 licensed code follows):
// bound the size requested, based on known count
final int firstLoadPosition = computeInitialLoadPosition(params, totalCount);
final int firstLoadSize = computeInitialLoadSize(params, firstLoadPosition, totalCount);
... where those functions do something with params.requestedStartPosition, params.requestedLoadSize and params.pageSize.
So what's going wrong?
Whenever you pass a new PagedList, you need to make sure that it contains the elements that the user is currently scrolled to. Otherwise, your PagedListAdapter will treat this as a removal of these elements. Then, later, when your loadAfter or loadBefore items load those elements, it will treat them as a subsequent insertion of these elements. You need to avoid doing this removal and insertion of any visible items. Since it sounds like you're scrolling to the top, maybe you're accidentally removing all items and inserting them all.
The way I think this works when using Room with PagedLists is:
The database is updated.
A Room observer invalidates the data source.
The PagedListAdapter code spots the invalidation and uses the factory to create a new data source, and calls loadInitial with the params.requestedStartPosition set to a visible element.
A new PagedList is provided to the PagedListAdapter, who runs the diff checking code to see what's actually changed. Usually, nothing has changed to what's visible, but maybe an element has been inserted, changed or removed. Everything outside the initial load is treated as being removed - this shouldn't be noticeable in the UI.
When scrolling, the PagedListAdapter code can spot that new items need to be loaded, and call loadBefore or loadAfter.
When these complete, an entire new PagedList is provided to the PagedListAdapter, who runs the diff checking code to see what's actually changed. Usually - just an insertion.
I'm not sure how that corresponds to what you're trying to do, but maybe that helps? Whenever you provide a new PagedList, it will be diffed against the previous one, and you want to make sure that there's no spurious insertions or deletions, or it can get really confused.
Other ideas
I've also seen issues where PAGE_SIZE is not big enough. The docs recommend several times the maximum number of elements that can be visible at a time.
This also happens when DiffUtil.ItemCallback is not correctly implemented. And by correct implementation I mean, you should properly check whether the oldItem and newItem are same or not and accordingly return true or false from areItemsTheSame() and areContentsTheSame() methods.
For example, if I always return false from both of these methods like:
DiffUtil.ItemCallback<Mention>() {
#Override
public boolean areItemsTheSame(Item oldItem, Item newItem) {
return false;
}
#Override
public boolean areContentsTheSame(Item oldItem, Item newItem) {
return false;
}
}
The library thinks that all the items are new therefore it jumps to the top to display all the new items.
So make sure you carefully check the oldItem and newItem and properly return true or false based on your comparisons
I'm trying to download a csv file after applying filters to the DataProvider.
For some reason the filtered results are shown in the Grid, but the downloaded csv file still contains all data.
#AutoView
class FinancialTransactionsView : VerticalLayout(), View {
private val grid: Grid<FinancialTransaction>
private val yearField: ComboBox<Int>
private val dataProvider = DataProvider.ofCollection(FinancialTransaction.findAll())
private val fileDownloader: FileDownloader
init {
label("Financial Transactions") {
styleName = ValoTheme.LABEL_H1
}
yearField = comboBox("Select Year") {
setItems(listOf(2016, 2017, 2018))
addSelectionListener {
// Filter the data based on the selected year
if (it.value != it.oldValue) setDataProvider()
}
}
// Create FileDownloader and initialize with all contents in the DataProvider
fileDownloader = FileDownloader(createCsvResource())
val downloadButton = button("Download csv") {
styleName = ValoTheme.BUTTON_PRIMARY
onLeftClick {
// The idea here is to assign values from the filtered DataProvider to the FileDownloader
fileDownloader.fileDownloadResource = createCsvResource()
}
}
fileDownloader.extend(downloadButton)
fileDownloader.fileDownloadResource = createCsvResource()
grid = grid(dataProvider = dataProvider) {
expandRatio = 1f
setSizeFull()
addColumnFor(FinancialTransaction::companyId)
addColumnFor(FinancialTransaction::fiscalYear)
addColumnFor(FinancialTransaction::fiscalPeriod)
addColumnFor(FinancialTransaction::currency)
addColumnFor(FinancialTransaction::finalizedDebitAmountInCurrency)
addColumnFor(FinancialTransaction::finalizedCreditAmountInCurrency)
appendHeaderRow().generateFilterComponents(this, FinancialTransaction::class)
}
}
private fun createCsvResource(): StreamResource {
return StreamResource(StreamResource.StreamSource {
val csv = dataProvider.items.toList().toCsv()
try {
return#StreamSource csv.byteInputStream()
} catch (e: IOException) {
e.printStackTrace()
return#StreamSource null
}
}, "financial_transactions.csv")
}
private fun setDataProvider() {
dataProvider.clearFilters()
if (!yearField.isEmpty)
dataProvider.setFilterByValue(FinancialTransaction::fiscalYear, yearField.value)
}
}
toCsv() is an extension function List<FinancialTransaction> which returns a string containing csv data.
What can I do to get the filtered results in my csv file?
val csv = dataProvider.items.toList().toCsv()
I am not Kotlin guy, but I assume dataProvider.items is a shorthand to dataProvider.getItems() in Java, i.e. this method (and you use ListDataProvider)
https://vaadin.com/download/release/8.4/8.4.1/docs/api/com/vaadin/data/provider/ListDataProvider.html#getItems--
In Vaadin getItems() returns all items by passing all filters.
So instead you should do either of the following
dataProvider.fetch(..)
https://vaadin.com/download/release/8.4/8.4.1/docs/api/com/vaadin/data/provider/DataProvider.html#fetch-com.vaadin.data.provider.Query-
Where you give the filters you want to apply in the query, or
grid.getDataCommunicator.fetchItemsWithRange(..)
https://vaadin.com/download/release/8.4/8.4.1/docs/api/com/vaadin/data/provider/DataCommunicator.html#fetchItemsWithRange-int-int-
Which returns list of items with filters you have set applied, which I think is ideal for you
Thank you for using Vaadin-on-Kotlin!
I've just updated the Databases Guide which should hopefully answer all of your questions. If not, just let me know and I'll update the guides accordingly.
The ListDataProvider.items will not apply any filters and will always return all items.
You need to use the getAll() extension function in order to obey the filters set by the Grid.
This is now explained in the Exporting data from DataProviders chapter of the Databases Guide.
In your code, both the grid and the yearField will set the filter to the same data provider,
thus overwriting values set by each other. Please read the Chaining Data Providers chapter in the Databases Guide to learn how to AND multiple filters set by multiple components.
When you use private val dataProvider = DataProvider.ofCollection(FinancialTransaction.findAll()), that will load all transactions from the database in-memory. You can use a more memory-efficient way: private val dataProvider = FinancialTransaction.dataProvider (given that FinancialTransaction is an Entity)
Please let me know if this answers your questions. Thanks!
How should I proceed when I want to generate table from list of lists which contains only strings(ex. data from csv). Names of columns don't matter. From all examples provided I saw only binding table items to specific model(which doesn't fit there as I have unknown number and names of columns).
If you already know the column names and data type, I would suggest to hard code that. If you know nothing about the format and simply want to create a TableView with completely dynamic columns, you can use the index in the csv data as an extractor to create StringProperty values for your data:
class MyView : View() {
val data = FXCollections.observableArrayList<List<String>>()
val csvController: CsvController by inject()
init {
runAsync {
csvController.loadData()
} ui { entries ->
// Generate columns based on the first row
entries.first().forEachIndexed { colIndex, name ->
root.column(name, String::class) {
value { row ->
SimpleStringProperty(row.value[colIndex])
}
}
}
// Assign the extracted entries to our list, skip first row
data.setAll(entries.drop(1))
}
}
override val root = tableview(data)
}
class CsvController : Controller() {
// Load data from CSV file here, we'll use som static data
// where the first row is the headers
fun loadData() = listOf(
listOf("Name", "Age"),
listOf("John", "42"),
listOf("Jane", "24")
)
}
This approach would only be good for visualizing the data in a CSV file. If you need to edit or manipulate the data, knowledge of the data types up front would yield a less flimsy application IMO :)