I'm writing Espresso tests for a navigation drawer that has a long, dynamic list of entries. I'd like to match a navDrawer menu item by Text and not position number. Looking at Google's DataAdapterSample, I would expect I could use this to get a match:
#Test
public void myTest() {
openDrawer();
onRow("Sign In").check(matches(isCompletelyDisplayed()));
}
private static DataInteraction onRow(String str) {
return onData(hasEntry(equalTo("module_name"), is(str)));
}
I'm not getting a match. But in the log I can see what I'm looking for. I get
No data found matching: map containing ["module_name"->is "Sign In"]
contained values: <[
Data: Row 0: {_id:"0", module_name:"Applications", module_secure:"false", headerCollapsible:1, } (class: android.database.MatrixCursor) token: 0,
Data: Row 1: {_id:"1", module_name:"Sign In", module_lock:"false", module_right_text:null, } (class: android.database.MatrixCursor) token: 1,
...
I think the hasEntry() works only for Maps, and it seems to me that items in your navigation drawer are not Maps but rather MatrixCursors.
Just replace the Person class in the example with MatrixCursor class.
For example something like this:
private static DataInteraction onRow(final String str) {
return onData(new BoundedMatcher<Object, MatrixCursor>(MatrixCursor.class) {
#Override
public void describeTo(Description description) {
description.appendText("Matching to MatrixCursor");
}
#Override
protected boolean matchesSafely(MatrixCursor cursor) {
return str.equals(cursor.getString(1));
}
});
}
Here I assume that second column of the cursor contains the text we need to match to. I am assuming this based on the "No data found matching.." error message.
Related
I have a Dictionary app where I want to assign existing synonyms to a word in the dictionary. To accomplish this I am using is using a M:N relationship between the word and synonym tables.
Entities:
#Entity(tableName = "word_table",
indices = #Index(value = "word", unique = true))
public class Word {
#PrimaryKey(autoGenerate = true)
private long id;
private String word;
#Ignore
public Word(String word) {
this.word = word;
}
public Word(long id, String word) {
this.id = id;
this.word = word;
}
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public String getWord() {
return word;
}
public void setWord(String word) {
this.word = word;
}
}
#Entity(tableName = "synonym_table")
public class Synonym {
#PrimaryKey(autoGenerate = true)
private long sid;
private String synonym;
#Ignore
public Synonym(String synonym) {
this.synonym = synonym;
}
public Synonym(long sid, String synonym) {
this.sid = sid;
this.synonym = synonym;
}
public long getSid() {
return sid;
}
public void setSid(long id) {
this.sid = sid;
}
public String getSynonym() {
return synonym;
}
public void setSynonym(String synonym) {
this.synonym = synonym;
}
}
#Entity(tableName = "word_synonym_join_table",
primaryKeys= {"word_id" , "synonym_id"},
foreignKeys = {#ForeignKey(entity = Word.class, parentColumns = "id", childColumns = "word_id"),
#ForeignKey(entity = Synonym.class, parentColumns = "sid", childColumns = "synonym_id")})
public class WordSynonymJoin {
#ColumnInfo(name = "word_id")
private long wordId;
#ColumnInfo(name = "synonym_id")
private long synonymId;
public WordSynonymJoin(long wordId, long synonymId) {
this.wordId = wordId;
this.synonymId = synonymId;
}
public long getWordId() {
return wordId;
}
public void setWordId(long wordId) {
this.wordId = wordId;
}
public long getSynonymId() {
return synonymId;
}
public void setSynonymId(long synonymId) {
this.synonymId = synonymId;
}
}
To retrieve the data for the Word and associated Synonyms, I created a POJO called WordWithSynonyms.
public class WordWithSynonyms {
#Embedded
public Word word;
#Embedded
public WordSynonymJoin wordSynonymJoin;
}
The Daos are as follows:
#Dao
public interface WordDao {
#Query("SELECT * FROM word_table")
public LiveData<List<Word>> getAllWords();
#Query("SELECT * FROM word_table WHERE id =:wordId")
public LiveData<List<Word>> getWordById(long wordId);
#Query("SELECT * from word_table WHERE word =:value")
public LiveData<List<Word>> getWordByValue(String value);
#Insert
public long insert(Word word);
#Delete
public void delete(Word word);
#Update
public void update(Word word);
#Query("DELETE FROM word_table")
public void deleteAll();
}
#Dao
public interface SynonymDao {
#Query("SELECT * FROM synonym_table")
public LiveData<List<Synonym>> getAllSynonyms();
#Query("SELECT * FROM synonym_table WHERE synonym =:value")
public LiveData<List<Synonym>> getSynonymByValue(String value);
#Insert
public void insert(Synonym synonym);
#Delete
public void delete(Synonym synonym);
#Query("DELETE FROM synonym_table")
public void deleteAll();
}
#Dao
public interface WordSynonymJoinDao {
#Query("SELECT * FROM word_table INNER JOIN word_synonym_join_table " +
"ON word_table.id = word_synonym_join_table.word_id " +
"WHERE word_synonym_join_table.synonym_id =:synonymId")
public LiveData<List<WordWithSynonyms>> getWordsBySynonym(long synonymId);
#Query("SELECT * FROM synonym_table INNER JOIN word_synonym_join_table " +
"ON synonym_table.sid = word_synonym_join_table.synonym_id " +
"WHERE word_synonym_join_table.word_id =:wordId")
public LiveData<List<SynonymWithWords>> getSynonymsByWord(long wordId);
#Query("SELECT * FROM synonym_table INNER JOIN word_synonym_join_table " +
"ON synonym_table.sid = word_synonym_join_table.synonym_id " +
"WHERE word_synonym_join_table.word_id !=:wordId")
public LiveData<List<SynonymWithWords>> getSynonymsByNotWord(long wordId);
#Insert
public void insert(WordSynonymJoin wordSynonymJoin);
#Delete
public void delete(WordSynonymJoin wordSynonymJoin);
#Query("DELETE FROM word_synonym_join_table")
public void deleteAll();
}
When I arrive on the Synonyms Activity, i pass the wordId to retrieve the current synonyms for that word through a ViewModel observer.
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_synonym);
Intent intent = getIntent();
wordId = Long.parseLong(intent.getStringExtra(EXTRA_WORD_ID));
//SynonymViewModel synonymViewModel = ViewModelProviders.of(this).get(SynonymViewModel.class);
WordSynonymJoinViewModel wordSynonymJoinViewModel = ViewModelProviders.of(this).get(WordSynonymJoinViewModel.class);
//synonymAdapter = new SynonymListAdapter(this);
synonymAdapter = new SynonymWithWordListAdapter(this);
synonynRecyclerView = findViewById(R.id.recycler_view_syonym);
if (wordId != 0) {
wordSynonymJoinViewModel.getSynonymsByWord(wordId).observe(SynonymActivity.this, new Observer<List<SynonymWithWords>>() {
#Override
public void onChanged(#Nullable List<SynonymWithWords> synonymWithWords) {
synonymAdapter.setSynonyms(synonymWithWords);
synonymAdapter.notifyDataSetChanged();
}
});
}
synonynRecyclerView.setAdapter(synonymAdapter);
synonynRecyclerView.setLayoutManager(new LinearLayoutManager(SynonymActivity.this));
}
I then give the user the opportunity to associate an existing, unassigned synonym from the Synonym table to the Word table.
I retrieve the unused and available Synonyms through a separate ViewModel observer inside of an AlertDialog which uses a spinner to display them via the WordSynonymJoin table using another ViewModel observer.
Finally, inside of that ViewModel observer when the user clicks the OK button on the AlertDialog, a third VieModel observer is ran to do the actual insertion into the WordSynonymJoin table.
case R.id.synonym_assign_synonym:
final WordSynonymJoinViewModel wordSynonymJoinViewModel = ViewModelProviders.of(SynonymActivity.this).get(WordSynonymJoinViewModel.class);
wordSynonymJoinViewModel.getSynonymsByNotWord(wordId).observe(SynonymActivity.this, new Observer<List<SynonymWithWords>>() {
#Override
public void onChanged(#Nullable List<SynonymWithWords> synonymWithWords) {
List<String> synonymsNotAssignList = new ArrayList<>();
for (SynonymWithWords sww : synonymWithWords)
synonymsNotAssignList.add(sww.synonym.getSynonym());
AlertDialog.Builder assignSynonymDialog = new AlertDialog.Builder(SynonymActivity.this);
assignSynonymDialog.setTitle("Select New Category:");
LayoutInflater inflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);
View view = inflater.inflate(R.layout.alert_dialog_spinner_view, null);
final Spinner synonymSpinner = (Spinner) view.findViewById(R.id.alert_dialog_spinner);
final SynonymViewModel synonymViewModel = ViewModelProviders.of(SynonymActivity.this).get(SynonymViewModel.class);
ArrayAdapter<String> spinnerAdapter = new ArrayAdapter(SynonymActivity.this, android.R.layout.simple_spinner_dropdown_item, synonymsNotAssignList);
spinnerAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
synonymSpinner.setAdapter(spinnerAdapter);
synonymSpinner.setSelection(synonymId);
assignSynonymDialog.setView(view);
assignSynonymDialog.setPositiveButton("OK", new DialogInterface.OnClickListener() {
#Override
public void onClick(DialogInterface dialog, int which) {
final String synonymValue = synonymSpinner.getSelectedItem().toString();
// get new synonym id
synonymViewModel.getSynonymByValue(synonymValue).observe(SynonymActivity.this, new Observer<List<Synonym>>() {
#Override
public void onChanged(#Nullable List<Synonym> synonyms) {
long id = 0;
if (!synonyms.get(0).getSynonym().equals(synonymValue)) {
if (synonyms.size() > 1)
Toast.makeText(SynonymActivity.this, "Query found " + synonyms.size() + " which is more than the one expected.", Toast.LENGTH_SHORT).show();
} else {
id = synonyms.get(0).getSid();
}
WordSynonymJoinViewModel wordSynonymJoinViewModel = ViewModelProviders.of(SynonymActivity.this).get(WordSynonymJoinViewModel.class);
wordSynonymJoinViewModel.insert(new WordSynonymJoin(wordId, id));
}
});
}
});
assignSynonymDialog.setNegativeButton("Cancel", null);
assignSynonymDialog.create();
assignSynonymDialog.show();
}
});
return true;
On the first pass, all seems to work well. However, on successive passes where the user continues to add new synonyms to the word, it takes that many clicks on the cancel button of the AlertDialog to exit after each synonym added. 2 synonyms added, 2 click on the cancel to get back to main Activity. 3 synonyms added, 3 clicks on the cancel to remove the AlertDialog.
I am very new to this whole concept of MVVM and Room persistence so I know there will be issues. Here is the code for the AlertDialog for adding existing, unassigned synonyms to the current word.
I don't like how much code is being used for this, but i have not been able to word my searches so that I can find ways around it.
My questions are:
Why is the code cycling +1 every time I enter associate new synonym to the word? Am I suppose to be clearing something out.
Is this coding even remotely right?
This seems like an awful lot of work to accomplish something so seemingly small. I think I have missed something. Have I made this abnormally complicated?
What am I missing that this code looks so cumbersome and unwieldy?
It seems a very cumbersome way to retrieve values and I don't really think i need to observe every query that I ran above. Maybe I am wrong.
Is there a direction of study that will help me understand this better?
Could this be where the Rx Java comes in?
I can certainly provide more code as needed.
Any help would be appreciated.
TRDL: Don't call .observe outside of ON_CREATE state.
You made a LiveData mistake... but you are not alone! That mistake is the most common LiveData mistake on StackOverflow: calling .observe outside of Activity#onCreate(). This includes calling .observe in a click listener, on onResume, broadcast receiver, etc.
The problem I see in most people who uses LivedData for the first time is that they treat LiveData just like a call back, when they are not. LiveData is a stream. LiveData does not notify just one time. The Observers attached to the LiveData will continue to be notified until they are unsubscribed. Also, It is meant to be subscribed at the beginning of the life-cycle, (e.g. Activity#onCreate or Fragment#onViewCreated) and unsubscribed at the end of the life-cycle. LiveData automatically handles the unsubscription part, so all you need to make sure is to subscribe in onCreate.
The fundamental reason you are keep getting +1 Dialog is that the previous observer is not dead and you are keep adding a new subscription to the database each time you repeat the same thing. Try rotating the phone and see if the number of dialog resets back to 1. That's because all of the previous observers are unsubscribed when you rotate the screen and activity is recreated.
Maybe you could call isShowing() and see if any dialog is open, as suggested in another answer. However, doing so is just a work around. What if it was a Toast or something else that you can't check? Besides, you are lucky that you could easily spot this bug. You might be having this duplicate observer bug some place else that is not visually noticeable.
So I think you already know how to use LiveData, but it is just that you need to know how to implement reactive pattern correctly. It would be too much to explain in one writing but let me give you a simple example:
Lets say you have a button that when you press, you fetch some data from DB. In a callback-like design you often call some function in ViewModel and pass a callback instance. For example you have this:
//ViewModel
void getSynonymsByNotWord(WordSynonymJoin word, Callback callback) { ... }
//Activity
void onClick(View v) {
wordSynonymJoinViewModel.changeCurrentSysnonymsByNotWord(wordId, callback);
}
You perform an action to ViewModel and you receive the response through callback. This is perfectly fine for callback. However, you can't do the same with LiveData. When using LiveData, View layer don't expect that there will be a response for each of the action. Instead, View layer should always blindly listen to the response, even before the button is clicked.
//ViewModel
private MutableLiveData wordQuery;
private Livedata synonymsByNotWord = Transformations.switchMap(wordQuery, word -> {
return repository.getSynonymsByWord(word);
});
LiveData getCurrentSynonymsByNotWord() {
return synonymsByNotWord;
}
void changeCurrentSynonymsByNotWord(WordSynonymJoin word) {
wordQuery.postValue(word);
}
//Activity
void onCreate() {
wordSynonymJoinViewModel.getCurrentSynonymsByNotWord().observe(...);
}
void onClick(View v) {
wordSynonymJoinViewModel.changeCurrentSynonymsByNotWord(wordId);
}
And also it is okay to, but you normally don't get ViewModel from ViewModelProviders every time you need a view model. You should just get one view model at onCreate, save it as an activity instance variable, and use the same instance in the rest of the activity.
Here:
wordSynonymJoinViewModel.getSynonymsByNotWord(wordId).observe(SynonymActivity.this, new Observer<List<SynonymWithWords>>() {
you are monitoring for synonyms, but inside of the observing, you show a dialog and allow more synonyms to be added. Everytime a new synonym is added, it creates a new AlertDialog.
So this is why you have to press cancel on each dialog.
To fix that, you can assign your AlertDialog to a field and use the isShowing() method to decide if you want to show another dialog (i.e. don't show another one if one is already showing.
https://developer.android.com/reference/android/app/Dialog.html#isShowing()
As for all your other questions, I'm sorry it's a bit too much for me to unpack.
I can share my thoughts on how I would do this though:
I want to assign existing synonyms to a word in the dictionary.
Perhaps forget the database to start with and create an in memory solution.
Then later you can change this to be persisted.
In memory the structure looks like a Hashtable of Dictionary words and Synonym lookups Map<String, List<String>>.
This Map would be in a class called Repository that exposes someway for you to observe and update it (RxJava Observable) or LiveData like you have already.
Your Fragment would observe this Map displaying it in a RecyclerView using MVVM or MVP whatever you want.
You have a clicklistener on each row of the RecyclerView to add a new synonym. On click opens the dialog (or a new activity/fragment). After the user types the synonym you will save this through the repository to your Map - and therefore the original observer will update the RecyclerView.
You should not get in a loop state of opening multiple dialogs :/
Hope that helps, tbh it sounds like you are on the right track and just need to work at it a bit more.
Help to realize the idea with the application. The application receives JSON from the site, and displays information on the screen (for example, games (image, name of the game, creators, year of release and description)). This is all parted, got the data through the retrofit and output them through recycleview. There is no problem with this, but I can’t think of a filter implementation. The filter must be dynamic, for example the creators and year of release. Activation opens where the CREATORS list goes down and the checkboxes with the studios name go down, and the YEAR of the ISSUE and the checkboxes with the year of release also go after it (only the creators and the year must take information from the data they received from the server via Jason). The idea is to have the first standard check box of the type all, which allows you to immediately output everything that is right at the start of the application, and then click the filter button and choose exactly what interests you. And there should be a button that all this update and returns with specific parameters. And I saw there is a button like a cross on the upper right (above on the actionbar), which possibly cancels everything and sets it back to its original position (all checkboxes only). I really hope for your advice and tips on how to implement this application. Thanks to all
Here is a good example of a filter (I need this one) https://pp.userapi.com/c851332/v851332451/e7308/hhiO3IOHPsg.jpg
in fact, a separate activity, which is dynamically filled with checkboxes on the results obtained from JSON (tobish the name, year, etc.)
POJO class value:
public class Value {
#SerializedName("title")
#Expose
private String title;
#SerializedName("year")
#Expose
private String year;
#SerializedName("genre")
#Expose
private List<String> genre;
#SerializedName("director")
#Expose
private String director;
#SerializedName("desription")
#Expose
private String desription;
#SerializedName("image")
#Expose
private String image;
public String getTitle() {
return title;
}
public String getYear() {
return year;
}
public List<String> getGenre() {
return genre;
}
public String getDirector() {
return director;
}
public String getDesription() {
return desription;
}
public String getImage() {
return image;
}
}
POJO class for list value:
public class Example {
#SerializedName("values")
#Expose
private List<Value> values = null;
public List<Value> getValues() {
return values;
}
}
MainActivity:
public class MainActivity extends Activity {
private static final String TAG = "MoviesApp";
RecyclerView recyclerView;
#Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.menu, menu);
return super.onCreateOptionsMenu(menu);
}
#Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.filters:
Intent intent = new Intent(this, FiltersActivity.class);
startActivity(intent);
return true;
default:
return super.onOptionsItemSelected(item);
}
}
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
recyclerView = findViewById(R.id.rc_movies);
LinearLayoutManager linearLayoutManager = new LinearLayoutManager(this);
recyclerView.setLayoutManager(linearLayoutManager);
recyclerView.setHasFixedSize(true);
JSONPlaceHolderApi service = NetworkService.getJSONApi();
Call <Example> call = service.getValues();
call.enqueue(new Callback<Example>() {
#Override
public void onResponse(Call<Example> call, Response<Example> response) {
Example data = response.body();
if(response.isSuccessful()) {
if(data != null) {
MoviesAdapter moviesAdapter = new MoviesAdapter(MainActivity.this, data);
recyclerView.setAdapter(moviesAdapter);
Log.i(TAG, "Response call");
}else{
Log.i(TAG, "Data is null");
}
}else{
Log.i(TAG, "Response does not successful");
}
}
#Override
public void onFailure(Call<Example> call, Throwable t) {
Log.i(TAG, "Failure call");
}
});
}
}
And i cant make FiltersActivity so that it works according to the condition
Sorry for bad english :C
...
Hi Daniil,
using a second activity for filter is not a bad idea in my opinion.
The problem is to update the list every time you apply a new filter because you have to store your filters values somewhere and then update the list after apply.
I suggest you to try using a shared class between activities (maybe a ViewModel) and if you wanna allows the generation of filters you can build the interface of the second activity in a dynamic way during your network call using the values you have received.
All filters value can be stored inside the viewmodel into an appropriate object during the network call (using a background operation to not freeze the UI).
When you call the second activity the shared class will be interrogated to obtain the filters and in the onCreate method you can generate the corresponding Views programmatically from the data you have received after the interrogation.
Also,to make sure the "apply filter" event will be intercepted by the first activity you can use an Event Bus Library, a LiveData Object or an Observer Pattern (Listener) according to the ones that fit your architecture best. When the first activity receives the event you can force and offline filter operation in your data and update the RecyclerView using a notifyDataSetChange().
Another architectural approach is to use a single activity and two fragments, but it require more code despite it's flexibility.This approach fits best with Viewmodels and Livedata and allows you to manage only one activity lifecycle (no worries about the kill of the non focused activities by the system).
Hope this will give you some hints/help.
Cheers
Hi all I can't think of a better example to illustrate my point so do let me know If my example has some errors. But hopefully this example will get my point through.
class A {
String CATEGORY = "A";
public String getCATEGORY() {
return CATEGORY;
}
}
class B extends A {
String CATEGORY = "B";
#Override
public String getCATEGORY() {
return CATEGORY;
}
}
class C extends A {
String CATEGORY = "C";
#Override
public String getCATEGORY() {
return CATEGORY;
}
}
public class MyClass {
private List<A> array = Arrays.asList(new A(), new B(), new C());
public MyClass() {}
}
Now if I upload MyClass onto firebase using setValue for example, firebase will show me the properties of class A, B and C. However, when I read the data from firebase and call sth like getValue(MyClass.class) the List it returns me are all of type A and the subclasses are not preserved. Is there a workaround to allow firebase to preserve the class types uploaded?
If you use Firebase's default serializer, it simply writes all public properties and fields to the database. Say that you store a single instance of each class, it'd be:
-L1234567890: {
cATEGORY: "A"
},
-L1234567891: {
cATEGORY: "B"
},
-L1234567892: {
cATEGORY: "C"
},
There won't be enough knowledge in the database for the SDK to reinflate the correct sub-class. While you and I can see that the cATEGORY value matches the class name, the Firebase SDK has no such knowledge.
It won't be too hard to write your own custom deserializer for this data though, taking a DataSnapshot with the values above and reinflating the correct class and values.
You could also do a hybrid: detect the class type directly, and then tell Firebase what class to read:
String cat = snapshot.child("cATEGORY").getValue(String.class)
Class clazz = "C".equals(cat) ? C.class : "B".equals(cat) ? B.class : A.clas;
A object = snapshot.getValue(clazz);
I'm developing an app for Android using Xamarin & MvvmCross. A simple version of View Models might look something like this:
public abstract class PersonViewModelBase
{
public string Name { get; set; }
}
public interface ICanRemove
{
MvxCommand RemoveCommand { get; }
}
public class LonelyPersonViewModel : PersonViewModelBase, ICanRemove
{
public bool IsHappy { get; set; }
public MvxCommand RemoveCommand { get { return new MvxCommand(() => RemoveFriend())); } }
private void RemoveFriend()
{
// Sends msg to remove a friend (using IMvxMessenger)
}
}
public class TiredPersonViewModel : PersonViewModelBase, ICanRemove
{
public int HoursSleepNeeded { get; set; }
// [ICanRemove implementation]
}
public class FriendlyPersonViewModel : PersonViewModelBase
{
public List<PersonViewModelBase> Friends { get; set; }
public MvxCommand AddFieldCommand
{
get
{
return new MvxCommand(
() => AddFriend());
}
}
private void AddFriend()
{
}
}
public class FriendsViewModel : MvxViewModel
{
public void Init()
{
Friends.Add(new LonelyPersonViewModel { Name = "Friend 1" });
Friends.Add(new TiredPersonViewModel { Name = "Friend 2" });
var friend3 = new FriendlyPersonViewModel { Name = "Friend 3" };
friend3.Friends.Add(new LonelyPersonViewModel { Name = "SubFriend 1" });
friend3.Friends.Add(new TiredPersonViewModel { Name = "SubFriend 2" });
Friends.Add(friend3);
Friends.Add(new LonelyPersonViewModel { Name = "Friend 4" });
var friend5 = new FriendlyPersonViewModel { Name = "Friend 5" };
var conjoinedFriend1 = new ConjoinedPersonViewModel { Name = "MultiPerson 1" };
conjoinedFriend1.CoPeople.Add(new TiredPersonViewModel { Name = "Jim" });
conjoinedFriend1.CoPeople.Add(new LonelyPersonViewModel { Name = "Nancy" });
friend5.Friends.Add(conjoinedFriend1);
Friends.Add(friend5);
}
public ObservableCollection<PersonViewModelBase> Friends { get; set; } = new ObservableCollection<PersonViewModelBase>();
}
The Friends view will be a ListView that displays the Friends as either a flat item or another list of friends. So it could look like this:
Friend 1
Friend 2
Friend 3 [Add]
SubFriend 1 [Del]
SubFriend 2 [Del]
Friend 4
Friend 5 [Add]
MultiPerson 1 [Del]
Jim
Nancy
The [Add] and [Del] are buttons that bind to the Add/Delete commands on the ViewModels. Currently, I'm using the CustomAdapter from the PolymorphicListView example, so each ViewModel is mapped to a custom layout. However, I don't currently have the full info displaying for FriendlyPersonViewModel or ConjoinedPersonViewModel.
I'm torn on how to continue:
I looked into doing nested listviews and found that it's not a good UX and I didn't go down through actually connecting the custom adapter to the child listviews.
I started the work to flatten the lists manually and having the Friends collection in the FriendsListView handle the Add/Remove. However, I need the subfriends to show up under the proper friend and they are currently just showing up at the bottom of the list, which is not idea, as the actual ViewModels are slightly more complex and would cause quite a bit of redundancy due to the FriendlyPeople names being repeated for each Friend added.
I looked at the old GroupedListView example from the Conference example, but each friend won't actually be in a group, some will be displayed alone.
Below is how I've got my current app working so far. The first Dark Blue section with the "Add" button was used to add the later dark blue sections; same for the Green sections. As I mentioned above, I'd like to have the later Dark Blue sections show up with the first Dark Blue section when I hit Add. I'd like for the first blue/green section to essentially be a header and have the next blue/green section directly below it; then hit add and have the next proper color show up under the right section without another header.
Does anyone have any suggestions or guidance on how to achieve this with Xamarin.Android with MVVMCross?
Thanks!
-g
Afterthought: It looks like there is an InsertItem on ObservableCollection, if I find the location of the last item added for the secrion I want to add the next PersonBase to, will the UI update properly? (Already left desk, or I'd check.)
UPDATE
Simply inserting the item using Insert(idx,item) will in fact update the UI properly and display the new item in the correct location. The next step would be, how do you style the items such that they are obviously a "group"?
I am trying to verify that a ListView does not contain a particular item. Here's the code I'm using:
onData(allOf(is(instanceOf(Contact.class)), is(withContactItemName(is("TestName")))))
.check(doesNotExist());
When the name exists, I correctly get an error because of check(doesNotExist()). When the name does not exist, I get the following error, because allOf(...) doesn't match anything:
Caused by: java.lang.RuntimeException: No data found matching:
(is an instance of layer.sdk.contacts.Contact and is with contact item name:
is "TestName")
How can I get functionality like onData(...).check(doesNotExist())?
EDIT:
I have a terrible hack to get the functionality I'd like by using try/catch and inspecting the event's getCause(). I would love to replace this with a good technique.
According to Espresso samples you must not use onData(...) to check if view doesn't exists in adapter. Check this out - link. Read "Asserting that a data item is not in an adapter" part. You have to use a matcher together with onView() that finds the AdapterView.
Based on Espresso samples from link above:
matcher:
private static Matcher<View> withAdaptedData(final Matcher<Object> dataMatcher) {
return new TypeSafeMatcher<View>() {
#Override
public void describeTo(Description description) {
description.appendText("with class name: ");
dataMatcher.describeTo(description);
}
#Override
public boolean matchesSafely(View view) {
if (!(view instanceof AdapterView)) {
return false;
}
#SuppressWarnings("rawtypes")
Adapter adapter = ((AdapterView) view).getAdapter();
for (int i = 0; i < adapter.getCount(); i++) {
if (dataMatcher.matches(adapter.getItem(i))) {
return true;
}
}
return false;
}
};
}
then onView(...), where R.id.list is the id of your adapter ListView:
#SuppressWarnings("unchecked")
public void testDataItemNotInAdapter(){
onView(withId(R.id.list))
.check(matches(not(withAdaptedData(is(withContactItemName("TestName"))))));
}
And one more suggestion - to avoid writing is(withContactItemName(is("TestName")) add below code to your matcher:
public static Matcher<Object> withContactItemName(String itemText) {
checkArgument( itemText != null );
return withContactItemName(equalTo(itemText));
}
then you'll have more readable and clear code is(withContactItemName("TestName")