MvxAutoCompleteTextView dropdown re-opens on every keypress - android

I have a problem whereby upon every keypress from the user, the AutoCompleteTextView quickly hides and re-appears again (with an updated set of values).
Please suggest where my problem could be and whether you see any other problems with the below code.
Binding in the View:
bindingSet
.Bind(emailAutoCompleteTextView)
.For(t => t.Text)
.To(vm => vm.Email);
bindingSet
.Bind(emailAutoCompleteTextView)
.For(t => t.PartialText)
.To(vm => vm.CurrentEmailEntry);
bindingSet
.Bind(emailAutoCompleteTextView)
.For(t => t.ItemsSource)
.To(vm => vm.CurrentEmailAutoCompleteSuggestions);
AXML Layout:
<mvvmcross.droid.support.v7.appcompat.widget.MvxAppCompatAutoCompleteTextView
android:id="#+id/EmailAutoCompleteTextView"
android:layout_marginTop="#dimen/PaddingBetweenUserInputFields"
android:completionThreshold="1"
android:inputType="textEmailAddress" />
View Model Code:
private string _currentEmailEntry;
public string CurrentEmailEntry
{
get
{
return _currentEmailEntry;
}
set
{
_currentEmailEntry = value;
if (value == string.Empty)
{
_currentEmailEntry = null;
}
CurrentEmailAutoCompleteSuggestions = _emailAutoCompleteList
.Where(email => email.StartsWith(_currentEmailEntry, StringComparison.OrdinalIgnoreCase))
.ToArray();
RaisePropertyChanged(nameof(CurrentEmailEntry));
}
}
private static readonly string[] _emailAutoCompleteList = {"Gordon", "Gordy", "Go", "Freeman", "Is", "Alive"};
private IList<string> _currentEmailAutoCompleteSuggestions = _emailAutoCompleteList.ToList();
public IList<string> CurrentEmailAutoCompleteSuggestions
{
get { return _currentEmailAutoCompleteSuggestions; }
set
{
if (ReferenceEquals(_currentEmailAutoCompleteSuggestions, value))
return;
_currentEmailAutoCompleteSuggestions = value;
RaisePropertyChanged(nameof(CurrentEmailAutoCompleteSuggestions));
}
}
I use MvvmCross 4.0 (upgrade is not an option).
NB: I tried using an ObservableCollection instead of an IList and remove/add items to it (not re-assign the collection itself) but after that the setter for CurrentEmailEntry stopped receiving values after the user typed the first character into the text view. The code inside MvxFilteringAdapter seems to be stuck waiting on a reset event.

Since no one answered either here or on the MvvmCross Xamarin Slack channel I eventually discovered the solution myself.
It was the re-creation of the list bound to the ItemsSource that led to the strange behaviour in question.
The ObservableCollection with Clear()/Add() instead of re-creating was indeed the way to go. The stuck behaviour (waiting on the reset event) described in the last question paragraph was caused by the absence of proper thread dispatching (my application is multi-threaded).
As soon as I wrapped my observable collection with a proxy collection that always raised the CollectionChanged event on the UI thread, the problem disappeared.
Leaving this here for the benefit of future generations.

Related

ViewModel not updating the view on postValue

I used the lifecycle callback onCreate to fetch data like below
mWeOutViewModel.getPlaceListLiveData()
.observe(this, weOutItemViewModels -> {
AppLogger.i(getCustomTag() + "adding items " + weOutItemViewModels.size());
if (weOutItemViewModels != null && weOutItemViewModels.size() > 0)
mWeOutListAdapter.addToExisting(weOutItemViewModels);
});
As you can see the AppLogger output the initial size which is 0 when the fragment is displayed, then I fetch the data and call postValue (setValue crashes the app and it expected because I fetch data from the internet using a background thread). So I call post value like below :
private void updatePlaces(List<WeOutGroupedViewModels> weOutGroupedViewModels) {
List<WeOutGroupedViewModels> oldList = placeMutableLiveData.getValue();
oldList.addAll(weOutGroupedViewModels);
AppLogger.i(TAG +" updating places "+oldList.size());
placeMutableLiveData.postValue(oldList);
}
As you can see the other AppLogger before postValue, the size of the list is displayed(not empty), but nothing happens until the app crashes and nothing is shown in the logs. I have no ways of debugging since even on debug mode nothing happens. The post value doesn't trigger the observer.
I initialize the mutableLivedata like this :
private final MutableLiveData<List<WeOutGroupedViewModels>> placeMutableLiveData = new MutableLiveData<>();
and access like this :
public LiveData<List<WeOutGroupedViewModels>> getPlaceListLiveData() {
return placeMutableLiveData;
}
Event when I make the livedata public to access directly the livedata, there is no change (just in case someone thinks that's is where the issue comes from)
Instead of placeMutableLiveData.postValue(oldList);
I recommend using
placeMutableLiveData.postValue(Collections.unmodifiableList(new ArrayList<>(newList));
That way, the next time you access this list, you won't be able to mutate it in place, which is a good thing. You're not supposed to mutate the list inside a reactive state holder (MutableLiveData).
So theoretically it should look like this:
private void updatePlaces(List<WeOutGroupedViewModels> weOutGroupedViewModels) {
List<WeOutGroupedViewModels> newList = new ArrayList<>(placeMutableLiveData.getValue());
newList.addAll(weOutGroupedViewModels);
AppLogger.i(TAG +" updating places "+newList.size());
placeMutableLiveData.postValue(Collections.unmodifiableList(newList));
}

RadListView not responding to new items in an ObservableArray

I'm trying to render a list of images, using RadListView. Being that i had some bizarre behavior when the data was coming from a normal array, i decided to try ObservableArray, as recommended in the docs.(specifically tns-vue)
The problem is, that pushing a new item to the model, doesn't update the view. The item is there, but nothing is shown.
This is my RadListView:
<RadListView layout="grid" ref="newImages" for="image in newImages">
<v-template>
<ImageComponent
showDate="false"
:onLongPress="()=>{onLongPress(image)}"
:image="image"
></ImageComponent>
</v-template>
</RadListView>
The "newImages" array:
data() {
return {
newImages: new ObservableArray([]),
};
}
I add items to the array, using the camera plugin:
openGallery() {
var that = this;
console.log(that.newImages.length);
var context = imagepicker.create({ mode: "multiple" }); // use "multiple" for multiple selection
context
.authorize()
.then(function() {
return context.present();
})
.then(function(selection) {
const images = [];
selection.forEach(function(selected) {
const image = that.createNewFileSchema(selected._android);
images.push(image);
});
that.newImages.push(images)//This adds the images to the array, but UI doesn't respond to the change.
})
.catch(function(e) {
alert(e);
});
},
What could be the problem here?
Your pushing arrays to a data arrays, to make the virtual DOM notice these changes, you probably wan't to use a deep watcher, calling a method returning the updated array.
You would have the same problem with Objects, but then you would be able to use:
this.$set(<object>, <key>, <value>)
I'm unsure if there is a better way for arrays, but you could try a watcher as said
watch: {
newImages: {
handler: function(<value>, <oldValue>) {},
deep: true
}
}
UPDATED - You can use this.$set for arrays
https://v2.vuejs.org/v2/api/#Vue-set
/* this.$set(<Array>, <Index>, <Value>) */
this.$set(this.newImages, this.newImages.length, [...newArrWithImages])
This guy explains reactively updating arrays: Vuejs and Vue.set(), update array

How to order data on the fly

I'm a beginner with react native or firebase so I don't really know how to explain this but I have no idea on how to order received data from the database.
Let's say I have a database:
appname
items
-some_random_generated_string
-name: "someString"
-value: "999"
-type: 0
-some_random_generated_string
-name: "someString"
-value: "999"
-type: 0
I've tried already with all order types and also setting .indexOn rule but haven't come to a solution. Also tried adding an id and then order with it and didn't come to a solution.
I guess this is accessing the database to get items so I also tried ordering them on the same line but nothing worked except limiting the amount of data.
let itemsRef = db.ref('items');
then I have:
componentDidMount() {
itemsRef.on('value', snapshot => {
let data = snapshot.val();
let items = Object.values(data);
this.setState({ items });
});
}
and I'm adding like this:
let addItem= (item, value, type) => {
db.ref('/items').push({
name: item,
value: value,
type: type,
});
};
Basically what I want to achieve is to display data in reversed order than it was added so the last one added would be shown on the top.
You could do it in two ways.
First simply call .reverse() on your current array. If you call the push method to add new items, usually the key that's assigned to each child garanties they are stored in chronological order. Therefore, calling it as such should be good enough:
componentDidMount() {
itemsRef.on('value', snapshot => {
let data = snapshot.val();
let items = Object.values(data);
items.rerverse();
this.setState({ items });
});
}
Though i don't know if let items = Object.values(data); garanties on every browser that your data are ordered as intended. By the way Object.values() opposed to Object.keys() is not supported on many old browsers, but if you're using it in ReactNative then it's fine.
If you want to be fully sure it's properly ordered, rewrite it as such:
componentDidMount() {
itemsRef.on('value', snapshot => {
let items = [];
// forEach always send you the children in the right order
snapshot.forEach(snap => {
items.push(snap.val())
})
items.rerverse();
this.setState({ items });
});
}
Finally another way to do it if you don't trust firebase's auto generated ids, is to add objects as such:
let addItem = (item, value, type) => {
db.ref('/items').push({
name: item,
value: value,
type: type,
timestamp: new Date().getTime()
});
};
and to use firebase's function orderByChild when you fetch the data, using timestamp as the child, and then .reverse() it again like we did previously.

When realmResults.asObservable is called?

I'm implementing a chat client.
Everytime user clicks Send message button - I perform a Realm insert of this message.
I have a service 'waiting' for this change to send this message via socket.
Like this:
Observable<RealmResults<RealmMessage>> observable = getUnsentMessages();
subscribeUnsendMessages = observable
...
.subscribe(message -> {
launchMessageSending(message);
});
and method for getting this observable looks like this:
public Observable<RealmResults<RealmMessage>> getUnsentMessages() {
final Realm instance = getRealmInstance();
return instance.where(RealmMessage.class)
...
.findAllAsync()
.asObservable()
.filter(o -> ... )
.doOnUnsubscribe(instance::close);
}
And here is the problem - there is a corner case, when I perform two operations roughly at the same time.
The second one looks like this:
public boolean shouldTrack(#NonNull RealmChat chat) {
getRealmInstance().executeTransactionAsync(realm ->{
RealmTrackedState trackedStates = realm.where(RealmTrackedState.class).findFirst();
trackedStates.getRealmChats().add(chat);
});
return true;
}
The above method is a tracking cache for remembering if I tracked given chat or not.
(This is a bit more complicated than this, bear with me).
This PROBABLY causes the observer to react on the second database change.
Because these two operations happen almost at the same time - the second notification is still "valid" because there is an unsent message.
So, questions:
Am I right?
If so - how should I handle this?
I don't really want to 'delay' the shouldTrack(); method because I'm afraid that the same problem will occur in a different way.
Maybe create a flag for this message being 'handled', so the second will be ignored - but this is a bit nasty I guess. The second 'send' shouldn't happen.
EDIT:
Here is my TrackedState object
public class RealmATrackedState extends RealmObject {
#PrimaryKey
private int id = 1;
private RealmList<RealmChat> realmChats;
private boolean isSomething;
}
Is updating such object (as posted above) causing RealmMessage table be notified?

Search from List which is in ViewModel (Xamarin Android )

I am new to Xamarin Android Development and I am using MvvmCross for binding data.I have SerachView on action-bar.I want to search data from list which is in ViewModel.How can I implement that ? I have searched for this issue on internet but all have used adapter and i want to search list-item without using adapter from ViewModel.I am not getting any idea how to do that.So anyone can suggest me an easy way?
Any suggestion or advice will be appreciated.
It is pretty simple.
Your SearchView is bound to a string property which you are using for filtering. Here I assume it is called SearchQuery.
It is not clear what criteria you want to use for filtering, I will assume that the ViewModel has a Name property, where the SearchQuery will be contained in that name.
So your ViewModel would look something like:
public class SearchViewModel : MvxViewModel
{
public string SearchQuery
{
get { return _searchQuery; }
set {
_searchQuery = value;
RaisePropertyChanged(() => SearchQuery);
RaisePropertyChanged(() => FilteredResults);
}
}
public List<ListItemViewModel> UnfilteredResults
{
get { return _unfilteredResults; }
set {
_unfilteredResults = value;
RaisePropertyChanged(() => UnfilteredResults);
RaisePropertyChanged(() => FilteredResults);
}
}
public List<ListItemViewModel> FilteredResults
{
get
{
if (string.IsNullOrEmpty(SearchQuery))
return UnfilteredResults;
return UnfilteredResults
.Where(r => r.Name.Contains(SearchQuery)).ToList();
}
}
}
So what happens is, whenever you enter a new value into the search box, it will trigger the PropertyChanged event on FilteredResults and use a simple LINQ query to filter the results.
If you don't want to swap out the entire list every time, you can do this with an ObservableCollection and add and remove items in that instead.
EDIT:
So as stated above you just bind the MvxListView to the new items source. Assuming you are using a AXML layout for your view:
<MvxListView
..
local:MvxBind="ItemsSource FilteredResults; ItemClick ResultClickedCommand" />
As for the SearchView, I just looked, there does not seem to be any code in MvvmCross to easily bind to that, and it does not inherit from EditText, so you need to do something like described here: https://stackoverflow.com/a/22501906/368379
Easiest way is probably simply to implement the SearchView.IOnQueryTextListener interface and in there set the new string you receive in the implementation on your ViewModel.

Categories

Resources