I'm new to Android development as well as test-driven development. I want to write unit tests for the following ListActivity:
public class TrendsMainActivity extends ListActivity {
#Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
String[] list_items = getResources().getStringArray(R.array.trend_menu_names);
setListAdapter(new ArrayAdapter<String>(this, R.layout.main, list_items));
}
#Override
protected void onListItemClick(ListView listView, View view, int position, long id)
{
Intent intent = null;
switch(position)
{
case 0:
intent = new Intent(this, TrendingActivity.class);
break;
case 1:
intent = new Intent(this, SearchActivity.class);
break;
case 2:
intent = new Intent(this, TimelineActivity.class);
break;
}
if(intent != null)
{
startActivity(intent);
}
else
{
Log.e(getClass().getSimpleName(), "There was an error retrieving request.");
}
}}
I have scoured all of the documentation that I can find, but I can not figure out how to test this Activity. The onListItemClick method is not finished, but it gives the idea of what I want to accomplish. I want to test clicking the first item in the ListView, and test that the correct Activity is being started.
How can I accomplish this?
Edit: I want my test to "click" on an item in the ListView. I then want to assert that the activity started is the correct activity (e.g. Clicking ListView item 0 starts the TrendingActivity specifically)
I should say that if you were applying TDD you should have started writing the tests not the application.
Anyway, Android Application Testing Guide contains in chapter 3 two examples that combined together can give you the solution you are looking for. The idea is to use an ActivityMonitor to verify that the expected activity was started.
#UiThreadTest
public void testListItemClickStartsActivity() {
final Instrumentation inst = getInstrumentation();
final IntentFilter intentFilter = new IntentFilter();
// here add conditions to your filter, i.e. intentFilter.addAction()
ActivityMonitor monitor = inst.addMonitor(intentFilter, null, false);
assertEquals(0, monitor.getHits());
// here perform desired click on list
monitor.waitForActivityWithTimeout(5000);
assertEquals(1, monitor.getHits());
inst.removeMonitor(monitor);
}
Related
I am trying to build a very simple App in Android Studio to practice using Intents to send data from one activity to another. On my Main Activity I have a "Total" TextView and a button. When I click the button, it takes me to a AddNumber Activity where I can type in an integer and press a button. This then takes me back to the Main Activity and adds that integer to the total, updating the TextView. I want to be able to do this multiple times while I've got the app open so the total keeps going up.
I have tried using a ViewModel to store the information so it doesn't get reset every time the Main Activity onCreate method is run. However, every time I do this, it works once (e.g. if I add a 7 in my AddNumber Activity, the Main Activity total goes to 7). However, when I try again, the ViewModel seems to get reset so doesn't remember the 7 that I put in initially, so if I put in an 8, the total on the MainActivity just gets set to 8, rather than 15. Am I making a mistake somewhere that is causing the ViewModel to reset? I understood that ViewModels were a way of storing data while the app is open, but I can't seem to make it work.
Thanks so much!
MainActivity.java code:
public class MainActivity extends AppCompatActivity {
TextView totalTextView;
MainActivityViewModel viewModel;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
totalTextView = (TextView) findViewById(R.id.totalTextView);
viewModel = new ViewModelProvider(this).get(MainActivityViewModel.class);
totalTextView.setText(Integer.toString(viewModel.total));
getSetIncomingIntent();
}
public void addNumber(View view){
Intent intent = new Intent(this, AddNumber.class);
startActivity(intent);
}
public void getSetIncomingIntent() {
Intent incomingIntent = getIntent();
if (incomingIntent.hasExtra("value")) {
viewModel.newValue = incomingIntent.getIntExtra("value",0);
viewModel.addNumber();
totalTextView.setText(Integer.toString(viewModel.total));
}
}
}
MainActivityViewModel.java code:
public class MainActivityViewModel extends ViewModel {
int total = 0;
int newValue;
public void addNumber() {
total = total + newValue;
}
}
AddNumber.java code:
public class AddNumber extends AppCompatActivity {
EditText numberEditText;
int value;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_add_number);
numberEditText = (EditText) findViewById(R.id.numberEditText);
}
public void addNumber(View view){
value = Integer.parseInt(numberEditText.getText().toString());
Intent intent = new Intent(this, MainActivity.class);
intent.putExtra("value", value);
startActivity(intent);
}
}
When I click the button, it takes me to a AddNumber Activity where I can type in an integer and press a button. This then takes me back to the Main Activity and adds that integer to the total, updating the TextView.
This is not what is happening. First you create a new AddNumber activity.
public void addNumber(View view){
Intent intent = new Intent(this, AddNumber.class);
startActivity(intent); // This launches a new AddNumber Activity
}
And then here you are creating a new MainActivity instance.
public void addNumber(View view){
value = Integer.parseInt(numberEditText.getText().toString());
Intent intent = new Intent(this, MainActivity.class);
intent.putExtra("value", value);
startActivity(intent); // This launches a new MainActivity
}
So you're basically building a giant stack of MainActivity / AddNumber instances.
You need to start AddNumber with startActivityForResult and then handle that in MainActivity. Then you will be working with the original MainActivity and the original ViewModel and you will be able to update it.
Please refer to the documentation on starting an Activity for result.
Hope that helps!
I have this Activity:
public class WelcomeActivity extends ActivityBase {
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
setContentView(R.layout.welcome);
final OnClickListener Click = new OnClickListener() {
#Override
public void onClick(View v) {
Intent intent = new Intent(WelcomeActivity.this, WelcomeDoneActivity.class);
startActivityForResult(intent, 0);
setResult(RESULT_OK);
finish();
}
};
((TitleBar)findViewById(R.id.theTitleBar)).setOnClickCloseListener(Click);
}
And this test:
#Test
public void clickingLogin_shouldStartLoginActivity() {
WelcomeActivity activity = Robolectric.setupActivity(WelcomeActivity.class);
activity.findViewById(R.id.theTitleBar).performClick();
Intent expectedIntent = new Intent(activity, WelcomeDoneActivity.class);
assertThat(shadowOf(activity).getNextStartedActivity(), equalTo(expectedIntent));
}
how come I get an assertion error?
java.lang.AssertionError:
Expected: <Intent { cmp=com.w/.profile.WelcomeDoneActivity }>
but: was null
Update
I have tried this as well but the startedIntent == null
ShadowActivity shadowActivity = shadowOf(activity);
Intent startedIntent = shadowActivity.getNextStartedActivity();
// ShadowIntent shadowIntent = shadowOf(startedIntent);
// String name = startedIntent.getIntentClass().getName();
// assertThat(shadowIntent.getIntentClass().getName(), equalTo(targetActivityName));
Change:
shadowOf(activity).getNextStartedActivity()
To:
shadowOf(activity).getNextStartedActivityForResult()
In the code you posted, you are setting the onClick on the TitleBar via setOnClickCloseListener.
It looks like your TitleBar is a custom view, and the setOnClickCloseListener to me implies there is a 'close' button or view on the TitleBar that performClick() should be called on, not the TitleBar itself, in order for Robolectric to behave the way you expect.
It's hard to tell without knowing the TitleBar and setOnClickCloseListener implementation, but:
1) If you just want to click the TitleBar to launch the new activity,
then change setOnClickCloseListener to setOnClickListener,
Or
2) If the TitleBar contains a view of the close button/view (or whatever "ClickClose" refers to in setOnClickCloseListener!), delve into the TitleBar layout to find the ID of this view, and call performClick on that, it should resolve your issue.
I am working on an android app that launches two activities using the on click listener everything in my code checks out fine except where the public void onClick(View v) begins I have multiple errors starting on that line and I am unable to run the code? I would kindly appreciate any help as I am fairly new to this. My code is as follows
public class Safaricom extends Activity {
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.safaricom);
Button button1 = (Button)findViewById(R.id.button1);
Button button2 = (Button)findViewById(R.id.button2);
button1.setOnClickListener(buttonClickListener);
button2.setOnClickListener(buttonClickListener);
}
private OnClickListener buttonClickListener = new View.OnClickListener() {
#Override
public void onClick(View v) {
Intent intent = null;
switch(v.getId()){
case R.id.button1:
intent = new Intent(this, Second.class);
break;
case R.id.button2:
intent = new Intent(this, SignUp.class);
break;
}
if (intent != null)
this.startActivity(intent);
}
};
}
The Errors are at two points.
First where it says public void on click view ( The Error is - Multiple Markers at this line - implements android.view.View.OnClickListener.onClick- The method onClick(View) of type new View.OnClickListener(){} must override a superclass )
Second where it says this.startActivity(intent); (The Error is -The method startActivity(Intent) is undefined for the type new View.OnClickListener(){})
Instead of this use v.getContext() or YOUR_ACTIVITY.this
Actually If you read the Docs carefully, you will know that Intent parameters contain Activity so when you are using this it means that you are giving a parameter of type new View.OnClickListener
Well, I can see right off a couple of errors. To make it clearer since it apparently was not clear by simply looking at the code and learning. I added Safaricom.this in each of the new Intent statements. This is because the Intent constructor needs a Context as the first argument and an OnClickListener is not a Context, you need to ge the enclosing Activity which is a context. One other edit, I missed, the startActivity also needs to have Safaricom prepended.
public class Safaricom extends Activity {
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.safaricom);
Button button1 = (Button)findViewById(R.id.button1);
Button button2 = (Button)findViewById(R.id.button2);
button1.setOnClickListener(buttonClickListener);
button2.setOnClickListener(buttonClickListener);
}
private OnClickListener buttonClickListener = new View.OnClickListener() {
#Override
public void onClick(View v) {
Intent intent = null;
switch(v.getId()){
case R.id.button1:
intent = new Intent(Safaricom.this, Second.class);
break;
case R.id.button2:
intent = new Intent(Safaricom.this, SignUp.class);
break;
}
if (intent != null)
Safaricom.this.startActivity(intent);
}
};
}
For the first error
The Error is - Multiple Markers at this line - implements android.view.View.OnClickListener.onClick- The method onClick(View) of type new View.OnClickListener(){} must override a superclass )
Try removing the #override
If that doesn't remove the second error then let us know if there is a different issue arising.
I have my main application class as follows and what I would like to know is how to change one line to call a method either from same class or another class, whilst the others still call activities. Here is the code:
public class InfoActivity extends GDListActivity {
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setTitle(R.string.info_activity_title);
ItemAdapter adapter = new ItemAdapter(this);
adapter.add(createTextItem(R.string.info_about, AboutActivity.class));
adapter.add(createTextItem(R.string.info_terms, TermsActivity.class));
adapter.add(createTextItem(R.string.info_privacy, PrivacyActivity.class));
setListAdapter(adapter);
}
private TextItem createTextItem(int stringId, Class<?> klass) {
final TextItem textItem = new TextItem(getString(stringId));
textItem.setTag(klass);
return textItem;
}
#Override
protected void onListItemClick(ListView l, View v, int position, long id) {
final TextItem textItem = (TextItem) l.getAdapter().getItem(position);
Intent intent = new Intent(InfoActivity.this, (Class<?>) textItem.getTag());
startActivity(intent);
}
}
The line in question:
adapter.add(createTextItem(R.string.info_about, AboutActivity.class));
I would like to call a method which as an example does this:
Intent emailIntent = new Intent(android.content.Intent.ACTION_SEND);
emailIntent.putExtra(android.content.Intent.EXTRA_EMAIL, new String[] { AboutActivity.this.getString(R.string.feedback_email) } );
emailIntent.putExtra(android.content.Intent.EXTRA_SUBJECT, AboutActivity.this.getString(R.string.feedback_subject));
emailIntent.setType("plain/text");
startActivity(emailIntent);
Right now I have this in the onCreate method of AboutActivity, but there naturally is no reason to have this functionality (sending email) within an activity. Instead it can just be ran as is. So, how could I do this?
Thanks!
The other two lines:
adapter.add(createTextItem(R.string.info_terms, TermsActivity.class));
adapter.add(createTextItem(R.string.info_privacy, PrivacyActivity.class));
they can remain the same in terms of functionality. This question is an addendum from this one (which I asked earlier and got answered):
Android - call method instead of class - sdk
How ever you choose to go about this, the trick is to actually move the decision making about what to do when a particular item is clicked down into the onListItemClick method. Here's a simple approach. First, change your createTextItem method to this:
private TextItem createTextItem(int stringId) {
final TextItem textItem = new TextItem(getString(stringId));
return textItem;
}
Then, change your onListItemClick to this:
protected void onListItemClick(ListView l, View v, int position, long id) {
final TextItem textItem = (TextItem) l.getAdapter().getItem(position);
String textItemContents = textItem.getString(); //I don't know if this is actually correct. I don't know what the TextItem class is. But I think you get the idea.
Intent intent = getIntentForString(textItemContents);
startActivity(Intent);
}
Your getIntentForString() method would then look something like this (note we can't use a switch because support for using switch statements with strings has only just recently been added to java):
private Intent getIntentForString(String textViewContents){
if(textViewContents.equals(getString(R.string.info_about))){
Intent emailIntent = new Intent(android.content.Intent.ACTION_SEND);
emailIntent.putExtra(android.content.Intent.EXTRA_EMAIL, new String[] {
AboutActivity.this.getString(R.string.feedback_email) } );
emailIntent.putExtra(android.content.Intent.EXTRA_SUBJECT,
AboutActivity.this.getString(R.string.feedback_subject));
emailIntent.setType("plain/text");
return emailIntent;
}
else if(textViewContents.equals(getString(R.string.info_terms)){
return new Intent(InfoActivity.this, TermsActivity.class);
}
else if(textViewContents.equals(getString(R.string.info_privacy)){
return new Intent(InfoActivity.this, Privacy.class);
}
else{
return null;
}
}
Note this approach has a downfall though. If yous start adding a bunch of different items to your ListView you're going to need to grow and grow your getIntentForString() method. For right now though, this should suffice. If you find yourself adding more options to your ListView though they'll be a more complicated approach that we'll need to take.
I had a very similar problem before where i wanted to show random xml layouts. I've done that - with lots of help by Ben Williams - with a class named DisplayRandomPage.class and I had a main.xml layout with four buttons.
That's the code of the Main.class:
switch (view.getId()) {
case R.id.first_button:
startActivity(new Intent(this, FirstPage.class));
break;
case R.id.second_button:
startActivity(new Intent(this, SecondPage.class));
break;
case R.id.third_button:
startActivity(new Intent(this, ThirdPage.class));
break;
case R.id.random_button:
Intent intent = new Intent(this, DisplayRandomPage.class);
startActivity(intent);
and this is in the DisplayRandomPage.class:
public class DisplayRandomPage extends Activity {
private Integer [] mLinearLayoutIds = {
R.layout.one
R.layout.two,
R.layout.three
};
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Random random = new java.util.Random();
int rand = random.nextInt(3);
setContentView(mLinearLayoutIds[rand]);
}
}
What i'd like to do is creating a DisplaySpecificPage.class. Above I've shown my main.class with the switch-case-clause. So when i click on the first button, it will start the FirstPage.class, clicking the second, it will start SecondPage.class, and so on. So for each xml-file i have to create a new java-class although the different java-classes do all the same. Only the xml-files are different. So i'd like to put something like this:
pseudo-code:
case R.id.first_button:
startActivity(new Intent(this, DisplaySpecificPage.class)) with R.layout.first_page;
break;
how do i pass the ID from the layout (R.layout.first_page) on?
You should change your switch statement to
final Intent intent = new Intent(this, DisplaySpecificPage.class);
switch (view.getId()) {
case R.id.first_button:
intent.putExtra("mylayout", R.layout.one);
break;
case R.id.second_button:
intent.putExtra("mylayout", R.layout.two);
break;
case R.id.third_button:
intent.putExtra("mylayout", R.layout.three);
break;
case R.id.random_button:
intent.putExtra("mylayout", randomNumber);
startActivity(intent);
}
startActivity(intent);
This way you'd start the same activity no matter which button would be pressed, and inside the DisplaySpecificPage activity's onCreate method you should set the content to this passed layout:
final int layout = getIntent().getIntExtra("mylayout", R.layout.one);
setContentView(layout);
The code above passes an extra parameter to the intent when starting the DisplaySpecificPage activity, with the name: "mylayout".
Inside the DisplaySpecificPage class' onCreate method you just retrieve this extra parameter using the getIntExtra method of the passed intent (getIntent() will return it for you), and you set the content view of this DisplaySpecificPage activity to the passed layout by setContentView(layout).
You should make sure though, to always pass a valid layout identifier to that intent, otherwise you'll get exception when trying to inflate it (so randomNumber should be selected properly).
Update
With adding extras to the Intent you can parametrize your activities.
So using intent.putExtra("paramName", paramValue) you'll pass the paramValue value on the name of paramName to the activity you start by this intent.
You want to start the DisplaySpecificPage activity, but want it to have different layout based on which button you click.
So you create an intent:
final Intent intent = new Intent(this, DisplaySpecificPage.class);
Before starting the activity by calling startActivity(intent), you have to put this extra information inside the intent, so the DisplaySpecificPage activity to know which layout it should set as its content view:
So if you pressed the second button, you want the layout of your new activity to be the one defined by the two.xml inside your res/layout folder. It is referenced by as R.layout.two (which is a static int value).
intent.putExtra("mylayout", R.layout.two);
This line puts the layout's value as an extra into the intent object, and now you can start the activity, the layout reference will be passed to it.
The "mylayout" is a name you choose for your parameter. It can be anything (valid), it will be used inside the DisplaySpecificPage activity to retrieve the layout's reference. It is retrieved by this name:
final int layout = getIntent().getIntExtra("mylayout", R.layout.one);
The getIntExtra method gets the integer value from the intent which has the name "mylayout", and if it's not found, then it will return R.layout.one.
If you want to handle this case (when no parameter with name mylayout is set), you can write
final int layout = getIntent().getIntExtra("mylayout", -1);
if (layout < 0)
{
//TODO: no layout reference was passed
}
Here is my final code:
Main.class:
public class Main extends Activity {
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
}
public void change(final View view) {
Intent intent2 = null;
final Intent intent = new Intent(this, DisplaySpecificPage.class);
switch (view.getId()) {
case R.id.first_button:
intent.putExtra("mylayout", R.layout.layout1);
break;
case R.id.second_button:
intent.putExtra("mylayout", R.layout.layout2);
break;
case R.id.third_button:
intent.putExtra("mylayout", R.layout.layout3);
break;
case R.id.random_button:
intent2 = new Intent(this, DisplayRandomPage.class);
startActivity(intent2);
}
// if the Random button was not clicked, if-condition is true.
if (intent2 == null)
startActivity(intent);
}
}
DisplaySpecificPage.class:
public class DisplaySpecificPage extends Activity{
public void onCreate(Bundle savedInstanceState) {
final int layout = getIntent().getIntExtra("mylayout", R.layout.one);
super.onCreate(savedInstanceState);
setContentView(layout);
}
}
DisplayRandomPage.class:
public class DisplayRandomPage extends Activity {
private Integer [] mLinearLayoutIds = {
R.layout.layout1,R.layout.layout2,R.layout.layout3
};
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Random random = new java.util.Random();
int rand = random.nextInt(mLinearLayoutIds.length);
setContentView(mLinearLayoutIds[rand]);
}
}
Big thanks to rekaszeru!