JUnit test on android app with cache and ArrayAdapter - java

// use case 10b alternate version
// caches a read comment temporarily
public void testCacheReadComment2() throws Throwable{
runTestOnUiThread(new Runnable()
{
#Override
public void run(){
CommentBuilder commentBuilder = new commentBuilder();
Comment comment = commentBuilder.createTopTestComment();
//browse button on main screen
((Button)activity.findViewById(ca.ualberta.cs.team5geotopics.browseButton)).performClick();
//the ListView for the custom adapter
ListView listView = (ListView) activity.findViewById(ca.ualberta.cs.team5geotopics.commentList);
//the custom adapter on the physical screen
ArrayAdapter<Comment> adapter = (ArrayAdapter<Comment>) listView.getAdapter();
adapter.add(comment);
adapter.notifyDataSetChanged();
View view = adapter.getView(adapter.getPosition(comment), null, null);
ViewAsserts.assertOnScreen(listView, view);
//this is the button to view the Top Level comment in the list
ViewAsserts.assertOnScreen(view, (Button) view.viewTopLevelComment);
((Button)view.viewTopLevelComment).performClick();
// is there a way I can get references to the objects
// already instantiated in the test thread?
CacheController cc = activity.getCacheController();
assertTrue(cc.getHistory().contains(comment));
}
});
}
We are using a test driven development style in order to code our project for school. In this test I am trying to prove that after a user views a comment from the list in the adapter, that this comment is cached in a history cache. I'm a little confused about some things and I would like some help, because it would be great if I knew there were no obvious flaws in my test case. these are my questions:
View view = adapter.getView(adapter.getPosition(comment), null, null);
Will this line return the view that is associated with the comment stored in the array adapter? My ArrayAdapter is going to follow a holder patter and I'm not sure if this is the proper way to get access to the buttons I need to mimic the user viewing the comment.
CacheController cc = activity.getCacheController();
I have a CacheController object that is instantiated upon the onCreate() method in our main activity. Now I want to reference this CacheController to see if the history is updated properly. I was just making a new CacheController and mimicking the actions of the CacheController in the test method, but I want to test what happens to my data on the UIthread. So, how do I reference objects in the UI thread?

View view = adapter.getView(adapter.getPosition(comment), null, null);
Will this line return the view that is associated with the comment
stored in the array adapter?
I think it should work, but I don't understand why would you want to access the View.
My ArrayAdapter is going to follow a holder patter and I'm not sure if
this is the proper way to get access to the buttons I need to mimic
the user viewing the comment.
The ArrayAdapter is usually used for a ListView. You should just let ListView handle the click capturing and tell you which element was clicked.
So, how do I reference objects in the UI thread?
You have 2 solutions for this that come to my mind right now:
1) Pass the CacheController instance, for example:
public class YourClass {
private final CacheController cacheController;
public YourClass(final CacheController cacheController) {
this.cacheController = cacheController;
}
public void testCacheReadComment2() throws Throwable {
CacheController cc = this.cacheController;
}
}
2) Singleton: make the CacheController static and put an accessor, for example:
public class CacheController {
private final CacheController instance = new CacheController();
public static CacheController getCacheController() {
return instance;
}
}
In both cases you should be aware about potential multi-threading issues because you're spawning new threads that all share same CacheController instance.

Related

Android Java - Race Condition in OnCreate with two Observers and making lists

sorry if this is a convoluted question. Working on creating an app for a college course and I'm running into (what appears to be) a race condition in my OnCreate method.
TL;DR - sometimes my spinner populates and I can get an index from it. Sometimes it's not populated yet when trying to get a specific index. Details and code below.
The app is a "course scheduler" for a college student.
I'm creating an Activity that displays existing course information and allows you to edit it. In the OnCreate method for this Activity, I am filling a spinner for "Mentors" for the course and a spinner for which "Term" the course belongs in. This information is being pulled from a Room DB.
I have a seperate activity for a new course and for editing a course. For the "new course" activity, everything works fine. I getAllMentors() or getAllTerms() successfully and fill the spinner list.
For the "Edit Course" Activity, there's an extra step involved and it seems to be causing me some issues.
When editing a course, I pass the intent from the originating Activity with all the necessary EXTRAS. This is successful.
In OnCreate for EditCourseActivity, I do the following:
I get the mentorID from the EXTRA that's passed in from the originating Activity.
I access my MentorViewModel and call my getAllMentors() method which returns LiveData> of all mentors in the db.
because it returns LiveData, I use an observer and loop through the LiveData adding the Name of each mentor to a List and the
entire mentor to a List.
I populate my spinner with the information in List full of mentor names.
then I do a for loop, looping through List looking for one that has the same id as what I grabbed form the EXTRA in step 1.
If I find a match in that list, I call a getMentorName() method to snag their name as a string.
I have a methond getIndex(spinner, string) that will loop through the provided spinner, trying to find a match for the string that's
passed in (mentors name) that I grabbed that should match the ID of
the mentor assigned to the course. This method returns index location
of the matched string in the spinner.
I set the spinner selection to the index found.
I do basically the same process for term.
Me being a new developer, I'm not used to OnCreate running the code synchronously.
Because of this, it appears that I have a race condition somewhere between populating the List of mentor names that populates the spinner, and calling my getIndex() method.
Sometimes the spinner is populated and getIndex works properly and sets the correct mentor. Sometimes the spinner is empty and my getIndex() returns -1 (which it should do in a no-find situation) that populates the spinner with the first item in the list (once it's populated).
protected void onCreate(Bundle savedInstanceState) {
//////////////////////////Handling Mentor spinner menu/////////////////////////////////////////////////
int mentorId = courseData.getIntExtra(EXTRA_COURSE_MENTOR_ID, -1);
final ArrayAdapter<String> sp_CourseMentorAdapter = new ArrayAdapter<String>(this, android.R.layout.simple_spinner_item, mentorNameList);
sp_CourseMentorAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
sp_CourseMentor.setAdapter(sp_CourseMentorAdapter);
final MentorViewModel mentorViewModel = ViewModelProviders.of(this).get(MentorViewModel.class);
//Mentor test = mentorViewModel.getMentorById(mentorId);
mentorViewModel.getAllMentors().observe(this, new Observer<List<Mentor>>() {
#Override
public void onChanged(#Nullable List<Mentor> mentorList) {
if (mentorList != null) {
for (Mentor m : mentorList) {
mentorNameList.add(m.getMentor_name());
mentorListMentor.add(m);
}
}
sp_CourseMentorAdapter.notifyDataSetChanged();
}
});
for(Mentor m: mentorListMentor){
if (m.getMentor_id()==mentorId){
String test = m.getMentor_name();
int spinnerSelectionM2 = getIndexM(sp_CourseMentor, test);
sp_CourseMentor.setSelection(spinnerSelectionM2);
}
}
Is there a way to get them to run asynchronously? Somehow to get the observer doing my getAllMentors() to complete first and populate the spinner, THEN have the for loop run?
Or a better way to handle this?
Thanks in advance.
Room always runs the code on a separated thread, not the Main/UI thread. You can change that behavior with
allowMainThreadQueries()
after initializating your database. This will make the query run first, populate your list and then run your for-loop code. I do not recommend this approach, since it is a bad practice to make queries on the UI thread.
You have two options:
Change your foor loop to a function and call it after adding the values from the observer:
mentorViewModel.getAllMentors().observe(this, new Observer<List<Mentor>>() {
#Override
public void onChanged(#Nullable List<Mentor> mentorList) {
if (mentorList != null) {
for (Mentor m : mentorList) {
mentorNameList.add(m.getMentor_name());
mentorListMentor.add(m);
}
lookForMentor();
}
}
});
private void lookForMentor() {
for(Mentor m: mentorListMentor){
if (m.getMentor_id()==mentorId){
String test = m.getMentor_name();
int spinnerSelectionM2 = getIndexM(sp_CourseMentor, test);
sp_CourseMentor.setSelection(spinnerSelectionM2);
}
}
}
Put the for inside the observer, change the Room DAO to return a List and use LiveData on your own viewmodel:
MentorViewModel.java:
MentorViewModel extends ViewModel {
private MutableLiveData<List<Mentor>> _mentorsLiveData = new MutableLiveData<List<Mentor>>();
public LiveData<List<Mentor>> mentorsLiveData = (LiveData) _mentorsLiveData;
void getAllMentors(){
//room db query
_mentorsLiveData.postValue(mentorsList);
}
}
EditActivity.java:
mentorsViewModel.getAllMentors();
mentorViewModel.mentorsLiveData.observe(this, new Observer<List<Mentor>>() {
#Override
public void onChanged(#Nullable List<Mentor> mentorList) {
mentorsListMentor.addAll(mentorList);
sp_CourseMentorAdapter.notifyDataSetChanged();
for(Mentor m: mentorListMentor){
if (m.getMentor_id()==mentorId){
String test = m.getMentor_name();
int spinnerSelectionM2 = getIndexM(sp_CourseMentor, test);
sp_CourseMentor.setSelection(spinnerSelectionM2);
}
}
}
}
});

Android adapter best practice when using attributes from activity

My Adapter requires a context in order to apply resources to views, therefore when instantiating it, I might do the following within my Activity:
myAdapter = new MyAdapter(this);
As my adapter also needs data from an activity, I might do this:
myAdapter = new MyAdapter(myItemsArrayList,this);
As my adapter might also need to know which items in the ArrayList are selected, I might pass it that list too:
myAdapter = new MyAdapter(myArrayItemsList,mySelectedItemsArrayList,this);
And as there may be other states (e.g. whether to display photos in a list of people, the constructor call is starting to get quite lengthy:
myAdapter = new MyAdapter(myArrayItemsList,mySelectedItemsArrayList,myPreference1,myPreference2,this);
Given that the only place this adapter will be used is from a particular activity, how bad would it be to just make those attributes in the activity public, so that I can access them via the activity that has been passed (e.g myActivity.myArrayItemsList)?
Many thanks in advance for any advice!
Given that the only place this adapter will be used is from a particular activity, how bad would it be to just make those attributes in the activity public, so that I can access them via the activity that has been passed (e.g myActivity.myArrayItemsList)?
That's a bad code and bad behavior. You're code will be tightly coupled. And usually, you will borrow the same behavior to your next project.
Instead of passing each state to your constructor, you can simplify it by passing a State object to your adapter. Create the State class something like this:
public class State {
List<String> selectedItems;
boolean displayPeople;
}
then you can create a simple constructor like this:
State state = new State();
state.selectedItems = mSelectedItems;
state.displayPeople = true;
myAdapter = new MyAdapter(items, state, this);
So, whenever you need to update a new state, you just need to add it the State class and update the Adapter according to it.
Considering you are using the Item object for myArrayItemsList.
So your list should look like this:
ArrayList<Item> myArrayItemsList = new ArrayList();
and then you want to add the selected items in the list you could add a boolean to the Item object ex:
public class Item {
private String itemName;
private boolean selected = false;
public Item(){}
public String getItemName() {
return itemName;
}
public void setItemName(String itemName) {
this.itemName= itemName;
}
public boolean isSelected () {
return selected;
}
public void setSelected(boolean selected) {
this.selected= selected;
}
}
and just check your item list in the adapter if the item is selected.
So your adapter would only pass two parameters:
MyAdapter myAdapter = new MyAdapter(myArrayItemsList, this);
But then again you want to pass only one parameter in adapter, you can set your ArrayList to static
public static ArrayList<Item> myArrayItemsList = new ArrayList();
and pass only this your adapter
MyAdapter myAdapter = new MyAdapter(this);
used the static ArrayList in your adapter but it is not advisable using those static data because the data could be Garbage Collected in the memory.

How to sort a recyclerview based on one list and not the other?

I am fairly new to Android Development and I have been stuck at this for some time. I've tried searching but nothing that really answers my question.
Scenario:
I have a Recycler view w/ an Adapter & viewholder (the usual)
My Adapter takes 2 lists.
exampleListA & exampleListB.
exampleListA is a Fruit object & exampleListB is a Pricing Object
These lists are coming back from a network call, where Fruit objects are coming in first.
My problem:I need to sort the list by the Pricing object, from least to highest. I tried Collections.sort(exampleListB ...) , the list gets sorted (I did a log) but it seems the recyclerview will always make exampleListA take importance over the other.
What is the best way, maybe even simplest (though I am not afraid to do some work) to sort exampleListA in order of a property in exampleListB ??
Anything would help, thanks!
public class GroceriesAdapter extends RecyclerView.Adapter<GroceriesAdapter.Viewholder>{
///.... generic stuff
public GrocereisAdapter(ArrayList<Fruit> fruitList, ArrayList<Pricing> pricingList){
this.fruitList = fruitList;
this.pricingList = pricingList;
void onBindViewHolder(Viewholder holder, int position){
Fruit fruit = fruitList.get(position);
Pricing price = pricingList.get(position);
holder.fruitTitle.setText(fruit.getName);
holder.pricingPrice.setText(price.getPrice);
public class MainActivity ....
//General OnCreate & variables...
ArrayList<Fruit> fruitList = new ArrayList();
ArrayList<Pricing> priceList = new ArrayList();
public void loadData(){
//Does Rest Call..
fruitList.addAll(restResponse.getFruitObjects);
priceList.addAll(restResponse.getPriceObjects);
now originally I thought this would work.. but it isn't. But just so you know what I Tried;
//After rest call, before adding them to lists.
Collections.sort(priceList, new Comparator<Pricing>) {
//general code that comes up when calling this..
return Double.compare(price.getPrice, p1.getPrice);
Thats pretty much it, nothing too too fancy. just taking properties from one list and the other and creating my views. *(Note edited for the project I'm working on)

How to get a view from within Espresso to pass into an IdlingResource?

I essentially have a custom IdlingResource that takes a View a constructor argument. I can't find anywhere that really talks about how to implement it.
I'm trying to use this answer: https://stackoverflow.com/a/32763454/1193321
As you can see, it takes a ViewPager, but when I'm registering the IdlingResource in my test class, I'm not sure how I can get my view.
I've tried findViewById() and I've tried getting the currently running activity and then calling findViewById() on that, with no luck.
Anyone know what to do in this scenario?
Figured it out. To get the view to pass into an idling resource, all you have to do is take the member variable of your ActivityTestRule
For example:
#Rule
public ActivityTestRule<MainActivity> activityTestRule = new ActivityTestRule<>(
MainActivity.class);
and then just call getActivity().findViewById(R.id.viewId)
So the end result is:
activityTestRule.getActivity().findViewById(R.id.viewId);
The accepted answer works as long as a test is running in the same activity. However, if the test navigates to another activity activityTestRule.getActivity() will return the wrong activity (the first one). To address this, one can create a helper method returning an actual activity:
public Activity getCurrentActivity() {
final Activity[] currentActivity = new Activity[1];
InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
#Override
public void run() {
Collection<Activity> allActivities = ActivityLifecycleMonitorRegistry.getInstance()
.getActivitiesInStage(Stage.RESUMED);
if (!allActivities.isEmpty()) {
currentActivity[0] = allActivities.iterator().next();
}
}
});
return currentActivity[0];
}
And then it could be used as the following:
Activity currentActivity = getCurrentActivity();
if (currentActivity != null) {
currentActivity.findViewById(R.id.viewId);
}
If you are using ActivityScenarioRule from androidx.test.ext.junit.rules (since ActivityTestRule "will be deprecated and eventually removed from library in the future"), you can get your Activity instance and call findViewById method:
import androidx.test.ext.junit.rules.activityScenarioRule
import androidx.test.ext.junit.runners.AndroidJUnit4
#RunWith(AndroidJUnit4::class) {
#get: Rule
var testRule = activityScenarioRule<MainActivity>()
#Test
fun mainTestCase() {
testRule.scenario.onActivity { activity ->
val view = activity.findViewById<YourView>(R.id.view)
}
}
}
I haven't already used IdilingResources in Espresso, but did you saw these articles:
Espresso: Custom Idling Resource by Chiuki
Wait for it...a deep dive into Espresso's Idling Resources
Also please check official Android Docs: Idling Resources (reference)
To answer your question,
the best way to do it is passing in an instance of one of the Views into the class's constructor. Check: Calling findViewById() from outside an activity
another way is getting view by context. Check android - How to get view from context?
Here's an exmple taken from a link above:
Starting with a context, the root view of the
associated activity can be had by
View rootView = ((Activity)_context).Window.DecorView.FindViewById(Android.Resource.Id.Content);
In Raw Android it'd look something like:
View rootView = ((Activity)mContext).getWindow().getDecorView().findViewById(android.R.id.content)
Then simply call the findViewById on this
View v = rootView.findViewById(R.id.your_view_id);
This might be also useful: How to call getResources() from a class which has no context?
Hope it help

Understanding memory leaks in Android application

I found the article "Avoiding memory leaks", where it is said that the following code:
private static Drawable sBackground;
#Override
protected void onCreate(Bundle state) {
super.onCreate(state);
TextView label = new TextView(this);
label.setText("Leaks are bad");
if (sBackground == null) {
sBackground = getDrawable(R.drawable.large_bitmap);
}
label.setBackgroundDrawable(sBackground);
setContentView(label);
}
is not a good idea, since:
When the screen orientation changes the system will, by default,
destroy the current activity and create a new one while preserving its
state. In doing so, Android will reload the application's UI from the
resources.
So the above code:
...leaks the first activity created upon the first screen orientation change. When a Drawable is attached to a view, the view is
set as a callback on the drawable. In the code snippet above, this
means the drawable has a reference to the TextView which itself has a
reference to the activity (the Context) which in turns has references
to pretty much anything (depending on your code.)
But, when screen orientation changes, the method setBackgroundDrawable(Drawable background) is called, which in turn calls:
background.setCallback(this);
The method Drawable.setCallback() is definied in the following way:
public final void setCallback(Callback cb) {
mCallback = new WeakReference<Callback>(cb);
}
So, now background should release the old reference to the previous TextView, and a new reference to the new TextView should be created.
So, it seems like changing screen orientation leaks a reference only until the activity is newly created.
Where am I going wrong ?
You are absolutely right. However, there is one subtle point: the article is from 2009. Back then, the implementation of setCallback was different:
Android <= 2.3.7:
public final void setCallback(Callback cb) {
mCallback = cb;
}
Android >= 4.0.1:
public final void setCallback(Callback cb) {
mCallback = new WeakReference<Callback>(cb);
}
Grepcode shows no source code of intermediate versions, this is the only diff I could quickly find.
So, again, you're absolutely right in this specific case (if you're targeting >14 that is). However, it is still very important to really think about what is actually happening when you keep a static reference to such items (like you did). There are numerous cases where you certainly could be leaking the Context.

Categories