Getting LiveData values to use with other LiveData values - java

(New to android programming)
I have a RecyclerView of subcategories and their corresponding keywords.
like this:
https://imgur.com/a/bEFS6cm
The subcategories are fetched by observing
subcategoryViewModel.getAllSubcategoriesForCategory(id).observe...
(I am using Room), here I have the id available (it is known which category is chosen at the time of creation of the subcategory fragment).
However, I am having troubles calculating the corresponding keywords. The keywords are the names of the articles contained in each subcategory.
In my ArticleDao I have a function
LiveData<List<String>> getAllArticleNamesById(int subId);
So logically, I just have to get the current subcategory list and get their corresponding keywords in a for loop.
But how do I do that if both the subcategory list and the keywords list are LiveData and I cannot access their values, only their observers can?
I tried putting an observer within an observer but I don't think that's the best idea.
subcategoryViewModel.getAllSubcategoriesForCategory(id).observe(getViewLifecycleOwner(),
new Observer<List<Subcategory>>() {
#Override
public void onChanged(List<Subcategory> subcategories) {
recyclerAdapter.setSubcategories(subcategories);
for (Subcategory sub : subcategories) {
articleViewModel.getAllArticleNamesById(sub.getId()).observe(getViewLifecycleOwner(),
new Observer<List<String>>() {
#Override
public void onChanged(List<String> strings) {
recyclerAdapter.addToKeywordsList(keywordsIntoString(strings));
}
});
}
});
I found some information on LiveData Transformations (map, switchmap) but that doesn't really apply to my problem since its supposed to apply a function on LiveData when otherLiveData changes. (at least from what I understand). I just need to access the current subcategory list and work with the values so I can observe the article names.
Ideally, I would need something like this:
for (Subcategory sub : subcategoryList) {
articleViewModel.getAllArticleNamesById(sub.getId()).observe...
}
So my question is, how do I access the subcategories which I'm already observing? Am I missing something?

Related

What is the correct way to cache and enrich a specific search result?

I have a code that has 2 APIs for the user:
Search API - returns 'shallow movies' results that each one has only few fields (e.g. id, title, subtitle and icon image).
Drill down a specific result - The user sends an id of one of the results he got in the "Search API", then the program will fetch many more data about this result (from DB and other sources) and return 'detailed movies' result that includes both the data from the shallow model and the new data.
Because I don't want to fetch all the shallow result data again I am saving all the shallow results in cache and then in the drill down API, I fetch from cache and find the result that matched the id the user sent.
On the shallow result model, I have many fields (except what I wrote above) that shouldn't be returned in any of the API's and are used only for logs and some other uses.
My problem is that I'm not sure what is the best way to model the shallow result model.
I thought I can have the following Movie class that contains 2 inner classes:
Movie
MovieCache
MovieExtraData (all the fields that shouldn't be saved to cache)
It feels a little weird to me because in this way the Movie model becomes very specific to this flow and I may use it in many other flows too.
Sorry for the length, any suggestions?
Thanks!
As I understood from your description, MovieCache should not be exposed through API to users, however you want MovieExtraData to be shown to users through API.
If it is true, you can have two classes:
Movie will be used for interaction in data layer
MovieDto will be used for interaction with user. This class will have all desired properties to be shown for API
Data Transfer Object (dto) is a design pattern to transfer data between layers in an N-Tier application.
Read more about dto here in wiki
UPDATE
If your loggable class movie does not have the same behaviour with MovieSpecific, then you can create a separate class for logging.
public class Movie
{
public int Id { get; set; }
public string Title { get; set; }
public string SubTitle { get; set; }
}
public class MovieSpecific extends Movie
{
public string AnotherSpecificData { get; set; }
}
public class MovieLogging extends Movie
{
public string AnotherLoggingData { get; set; }
}

Change LiveData source by updating a variable

I want the LiveData source for a RecyclerView to change depending on which list you selected. And that if you've selected a source in this search.
At the moment I can't switch back and forth between the sources. So I can display items from my Room database, but I can't change the source if I've selected another list.
Example: If you selected List 2, the LiveData source will be changed and all items contained in that List 2 will be displayed. Now you should also be able to search for words in this list 2. How can you do this during the runtime of an app?
A part of my current Repository:
public LiveData<List<VocabularyEntity>> getVocabularies(int listNumber, String searchText) {
if (listNumber == 0) {
return listDao.getVocabularies(searchText);
} else {
return listDao.getVocabularyList(listNumber, searchText);
}
}
And a part of my current ViewModel:
public LiveData<List<ListEntity>> getLists() {
return repository.getLists(listNumber, searchText);
}
I do not see any setValue or getValue function that is being called on your LiveData actually.
In order to change the LiveData to interact with the live changes, you need to call the setValue in your LiveData object. Something like the following should fix your problem here I think.
// I am assuming you have this variable declared in your viewmodel
private LiveData<List<ListEntity>> vocabList;
public LiveData<List<ListEntity>> getLists() {
List<ListEntity> vocabListFromDB = repository.getLists(listNumber, searchText);
vocabList.setValue(vocabListFromDB);
return vocabList;
}
And you do not have to return the LiveData object from the repository function anymore.
public List<VocabularyEntity> getVocabularies(int listNumber, String searchText) {
if(listNumber == 0) {
return listDao.getVocabularies(searchText);
} else {
return listDao.getVocabularyList(listNumber, searchText);
}
}
I hope that helps!
I want to share my personal opinion on implementing this actually. I would rather have a ContentObserver instead of a LiveData setup. Implementation with a ContentObserver and a CursorLoader looks like an easier and robust solution in my humble opinion.

Is there a way to convert AbstractBackEndDataProvider to ListProvider?

I have written a custom https://mindbug.in/vaadin/vaadin-dataprovider-example/ CallBackDataProvider that I based on this link here, which is used for a multi-select combo box (an addon https://github.com/bonprix/vaadin-combobox-multiselect from Vaadin's addon directory) for the purpose of providing a item lazy loading.
According to the addon's clear() and selectAll(), it expects a ListDataProvider. I've already set the component's data provider to used the custom data provider above. Whenever a clear or selectAll function is triggered, the Class Cast Exception is being thrown. It is expecting a ListDataProvider.
The very straightforward workaround for this case is to disable the clear and selectAll method by setting the boolean flag to false, but from the user's point of view, this will not be flexible.
Another step attempted is to to convert the stream into a Collection List, yet, it didn't work. It still throws an error.
This is the custom CallbackDataProvider, extended from the AbstractBackendDataProvider:
public ItemDataProvider(ReceiptService receiptService) {
if(receiptService != null){
this.receiptService = receiptService;
}else {
this.receiptService = new ReceiptService();
}
}
#Override
protected Stream<SkusSelectBox> fetchFromBackEnd(Query<SkusSelectBox, String> query) {
stream = receiptService.fetchSkus(query.getFilter().orElse(null), query.getLimit(), query.getOffset(), query.getSortOrders()).stream();
return stream;
}
#Override
protected int sizeInBackEnd(Query<SkusSelectBox, String> query) {
return receiptService.countSkus(query.getFilter().orElse(null));
}
#Override
public Object getId(SkusSelectBox item) {
return item.getItemId();
}
public Stream<SkusSelectBox> getStream(){
return stream;
}
The SkuSelectBox is a simple two string attribute object that retrieves the id and the name.
For this component, I have set the following at the view page:
ItemDataProvider itemDataProvider = new ItemDataProvider(receiptService);
ComboBoxMultiselect<SkusSelectBox> skuSelect = new ComboBoxMultiselect<>("Items");
skuSelect.setPlaceholder("Choose Items");
skuSBox.add(new SkusSelectBox("0", "No data found"));
skuSelect.setWidth(80, Unit.PERCENTAGE);
skuSelect.setRequiredIndicatorVisible(true);
skuSelect.setItemCaptionGenerator(SkusSelectBox::getItemName);
skuSelect.setSelectAllButtonCaption("Select All");
skuSelect.setClearButtonCaption("Clear");
skuSelect.showSelectAllButton(true);
skuSelect.showClearButton(true);
skuSelect.setDataProvider(itemDataProvider);
skuSelect.getDataProvider().refreshAll();
skuSelect.isReadOnly();
skuSelect.setPageLength(20);
if(skuSBox.size() <=1 ){
skuSelect.showSelectAllButton(false);
//skuSelect.showClearButton(false);
}
skuSelect.setResponsive(true);
The selectAll and clear methods are very similar except for the very end of the method:
#Override
public void selectAll(final String filter) {
final ListDataProvider<T> listDataProvider = ((ListDataProvider) getDataProvider());
final Set<String> addedItems = listDataProvider.getItems()
.stream()
.filter(t -> {
final String caption = getItemCaptionGenerator().apply(t);
if (t == null) {
return false;
}
return caption.toLowerCase()
.contains(filter.toLowerCase());
})
.map(t -> itemToKey(t))
.collect(Collectors.toSet());
updateSelection(addedItems, new HashSet<>(), true);
updateSelection(new HashSet<>(), removedItems, true); (this is for clear method)
}
Basically the class cast exception is shown in this error message, referring to either the clear or selectAll, whichever method I was invoking:
java.lang.ClassCastException: com.igi.sycarda.dashboard.hib.utils.ItemDataProvider cannot be cast to com.vaadin.data.provider.ListDataProvider
at org.vaadin.addons.ComboBoxMultiselect$1.clear(ComboBoxMultiselect.java:224)
I'm looking at the selectAll or clear method, when invoked to work as usual as if not using a CallbackDataProvider.
Until the next patch release for the addon is released, I need to put in a workaround for this problem, how can I convert a custom provider to a ListDataProvider either in a quick dirty way or a cleaner way if required?
UPDATE: Normally, I would do a direct fetch from the service class, but when tested with a tenant that has about 20K of item records, the loading of the page and the specific component box is quite slow to load. That CallbackDataProvider is to test this will work for those big amount of records.
The idea with a list data provider is that all items are loaded into memory. It is possible to load all items from a database into memory and then use that to create a list data provider. This does on the other hand defeat the purpose of having a callback data provider.
It's probably more straightforward for you to fetch the items into a list directly from your receiptService rather than going through the existing data provider.
Since there are restrictions or blocks that cause error to approach I was doing, someone just suggested to me to create a view derived from the tables / columns required and used them instead of the normal tables.
After creating a view, I just reverted and removed these lines below to the usual implementation:
skuSelect.setDataProvider(itemDataProvider);
skuSelect.getDataProvider().refreshAll();
skuSelect.isReadOnly();
skuSelect.setPageLength(20);
if(skuSBox.size() <=1 ){
skuSelect.showSelectAllButton(false);
//skuSelect.showClearButton(false);
}
At the time of writing this, we've tested it an hour ago and it solves the problem without sacrificing the performance time taken and creating an additional component. In terms of time measurement, a 20K result set in a view loads in less than 10 seconds vs 7-9 minutes previously.

ISIS: Moving from deprecated #Action(invokeOn=...) to #Action(associateWith=...)

I am working on a project using ISIS 1.16.2. I have a superclass, called ConfigurationItem, which has some common properties (name, createdTimestamp etc.).
For example it has a delete action method, annotated with #Action(invokeOn = InvokeOn.OBJECT_AND_COLLECTION, ...), which I need to be callable from entitys detail view as well as from collection views with selection boxes.
Example:
public class ConfigurationItem {
#Action(
invokeOn = InvokeOn.OBJECT_AND_COLLECTION,
semantics = SemanticsOf.NON_IDEMPOTENT_ARE_YOU_SURE,
domainEvent = DeletedDomainEvent.class)
public Object delete() {
repositoryService.remove(this);
return null;
}
// ...
}
public class ConfigurationItems {
#Action(semantics = SemanticsOf.SAFE)
public List<T> listAll() {
return repositoryService.allInstances(<item-subclass>.class);
}
// ...
}
This works pretty well but the "invokeOn" annotation is now deprecated. The JavaDoc says that one should switch to #Action(associateWith="...") but I don't know how to transfer the semantics of 'InvokeOn' since I have no collection field for reference.
Instead I only have the collection of objects returned by the database retrieve action.
My question is: How do I transfer the deprecated #Action(invokeOn=...) semantics to the new #Action(associateWith="...") concept for collection return values with no backed property field?
Thanks in advance!
Good question, this obviously isn't explained well enough in the Apache Isis documentation.
The #Action(invokeOn=InvokeOn.OBJECT_AND_COLLECTION) has always been a bit of a kludge, because it involves invoking an action against a standalone collection (which is to say, the list of object returned from a previous query). We don't like this because there is no "single" object to invoke the action on.
When we implemented that feature, the support for view models was nowhere near as comprehensive as it now is. So, our recommendation now is, rather than returning a bare standalone collection, instead wrap it in a view model which holds the collection.
The view model then gives us a single target to invoke some behaviour on; the idea being that it is the responsibility of the view model to iterate over all selected items and invoke an action on them.
With your code, we can introduce SomeConfigItems as the view model:
#XmlRootElement("configItems")
public class SomeConfigItems {
#lombok.Getter #lombok.Setter
private List<ConfigurationItem> items = new ArrayList<>();
#Action(
associateWith = "items", // associates with the items collection
semantics = SemanticsOf.NON_IDEMPOTENT_ARE_YOU_SURE,
domainEvent = DeletedDomainEvent.class)
public SomeConfigItems delete(List<ConfigurationItem> items) {
for(ConfigurationItem item: items) {
repositoryService.remove(item);
}
return this;
}
// optionally, select all items for deletion by default
public List<ConfigurationItem> default0Delete() { return getItems(); }
// I don't *think* that a choices method is required, but if present then
// is the potential list of items for the argument
//public List<ConfigurationItem> choices0Delete() { return getItems(); }
}
and then change the ConfigurationItems action to return this view model:
public class ConfigurationItems {
#Action(semantics = SemanticsOf.SAFE)
public SelectedItems listAll() {
List<T> items = repositoryService.allInstances(<item-subclass>.class);
return new SelectedItems(items);
}
}
Now that you have a view model to represent the output, you'll probably find other things you can do with it.
Hope that makes sense!

How Can I make This Algorithm Generic?

I am working on an object cache of CMS objects. I need to maintain a parent/child relationship between a Product and child objects (Options and Attributes). The parent (Product) is illustrated in the first code sample.
It is easy enough to do, but I am looking for a way to make the assignment of the child to the parent, as shown in the 2nd code block, generic.
Since all CMS objects extend CMSContent, I can use ProductID. However, is there a way to make the field (e.g. ProductAttribute) generic so that I can put the algorithm in a method and call the method with a parent and child object to make the attribute assignment?
I know that an ORM framework like Hibernate is appropriate here, but that won't fit since I have a fixed database structure.
public class Product extends CMSContent {
private List<ProductAttribute> productAttributes;
private List<ProductOptions> productOptions;
// getters,setters
}
Algorithm to match them up.
// attach Product Attributes to Product
for (Product p : listP) {
Map<String, Object> parameters = new HashMap<String, Object>();
for (ProductAttribute po : listPA) {
parameters.put("pid", p.getPid());
parameters.put("ref", po.getRid());
int i = jdbcTemplate.queryForInt(sqlAttr, parameters); // select count(*), 1 if matched.
if (i == 1) {
p.getProductAttributes().add(po); // generic field?
}
}
}
Wouldn't this two Methods in Product help
public void add(ProductAttribute item){
productAttributes.add(item);
}
public void add(ProductOption item){
productOption.add(item);
}
so you should be able to just add a ProductAttribute or a ProductOption

Categories