Introduction to Automated Android Testing – Part 5

Datetime:2016-08-22 23:23:53          Topic: Test Engineer  Andrew Development           Share

In part 5, we will take a look at interacting with the Presenter created in part 4 and we will create the UI to display the list of search results.

Creating the UI

For the User Interface we want a simple list that displays the avatar, name and other user information in the list.

Inpart 4, we defined a View contract which the Activity should implement. This is where the Android specific code will be located (things such as visibility changes or any UI changes will be located here). To refresh your memory, this is the View contract we created in this last post:

interface UserSearchContract {
 
    interface View extends MvpView {
        void showSearchResults(List<User> githubUserList);
 
        void showError(String message);
 
        void showLoading();
 
        void hideLoading();
    }
 
    interface Presenter extends MvpPresenter<View> {
        void search(String term);
    }
}

Let’s implement the View!

  1. Create a class called  UserSearchActivity . This class will implement the UserSearchContract . View  contract and extend AppCompatActivity . Define a variable called userSearchPresenter  of type UserSearchContract . Presenter . This is the object that we will interact with in order to perform our network calls.
    public class UserSearchActivityextends AppCompatActivityimplements UserSearchContract.View {
     
        private UserSearchContract.PresenteruserSearchPresenter;
     
        @Override
        protected void onCreate(BundlesavedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_user_search);
            userSearchPresenter = new UserSearchPresenter(Injection.provideUserRepo(), Schedulers.io(),
                    AndroidSchedulers.mainThread());
            userSearchPresenter.attachView(this);
     
        }
     
        @Override
        protected void onDestroy() {
            super.onDestroy();
            userSearchPresenter.detachView();
        }
     
        @Override
        public void showSearchResults(List<User> githubUserList) {
            
        }
     
        @Override
        public void showError(String message) {
        
        }
     
        @Override
        public void showLoading() {
            
        }
     
        @Override
        public void hideLoading() {
        }
    }
    
    In onCreate ( ) , create the presenter object. Provide it with the User repo defined in the Injection class. Pass the io ( )  scheduler and the AndroidSchedulers . mainThread ( )  scheduler so that the RxJava subscriptions know which threads they should perform their work on.

    On the next line, you can see I call userSearchPresenter . attachView ( this ) . This attaches the view to the presenter, so that the presenter can notify the view of any changes. Because the presenter isn’t aware of the activity’s lifecycle , in onDestroy ( ) we need to inform the presenter that the view is no longer in existence, so we should then call userSearchPresenter . detachView ( ) . This will unregister any RxJava subscriptions and prevent memory leaks from occurring.

  2. Create activity_user_search . xml  in the layout folder. This will contain a RecyclerView , a ProgressBar , an error TextView  and a Toolbar . I am using ConstraintLayout  to design my screen, so I won’t go into too much detail as it is mostly drag and drop. (If you want to read more about ConstraintLayout check out my blog post about ithere)
    <?xmlversion="1.0" encoding="utf-8"?>
    <android.support.constraint.ConstraintLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:tools="http://schemas.android.com/tools"
        android:id="@+id/activity_user_search"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context="za.co.riggaroo.gus.presentation.search.UserSearchActivity">
     
        <android.support.v7.widget.Toolbar
            android:id="@+id/toolbar"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:background="?attr/colorPrimary"
            android:minHeight="?attr/actionBarSize"
            android:theme="?attr/actionBarTheme"
            app:layout_constraintHorizontal_bias="0.0"
            app:layout_constraintLeft_toLeftOf="@+id/activity_user_search"
            app:layout_constraintRight_toRightOf="@+id/activity_user_search"
            app:layout_constraintTop_toTopOf="@+id/activity_user_search">
     
        </android.support.v7.widget.Toolbar>
     
        <android.support.v7.widget.RecyclerView
            android:id="@+id/recycler_view_users"
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:layout_marginBottom="16dp"
            android:clipToPadding="false"
            android:scrollbars="vertical"
            app:layoutManager="android.support.v7.widget.LinearLayoutManager"
            app:layout_constraintBottom_toBottomOf="@+id/activity_user_search"
            app:layout_constraintLeft_toLeftOf="@+id/activity_user_search"
            app:layout_constraintRight_toRightOf="@+id/activity_user_search"
            app:layout_constraintTop_toBottomOf="@+id/toolbar"
            tools:listitem="@layout/list_item_user">
     
        </android.support.v7.widget.RecyclerView>
     
        <TextView
            android:id="@+id/text_view_error_msg"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="16dp"
            android:text="@string/search_for_some_users"
            android:visibility="visible"
            app:layout_constraintBottom_toBottomOf="@+id/recycler_view_users"
            app:layout_constraintLeft_toLeftOf="@+id/toolbar"
            app:layout_constraintRight_toRightOf="@+id/recycler_view_users"
            app:layout_constraintTop_toBottomOf="@+id/toolbar"
            tools:text="No Data has loaded"/>
     
        <ProgressBar
            android:id="@+id/progress_bar"
            style="@style/Widget.AppCompat.ProgressBar"
            android:layout_width="60dp"
            android:layout_height="60dp"
            android:layout_marginBottom="16dp"
            android:layout_marginTop="16dp"
            android:visibility="gone"
            app:layout_constraintBottom_toBottomOf="@+id/activity_user_search"
            app:layout_constraintLeft_toLeftOf="@+id/recycler_view_users"
            app:layout_constraintRight_toRightOf="@+id/recycler_view_users"
            app:layout_constraintTop_toBottomOf="@+id/toolbar"
            tools:visibility="visible"/>
     
    </android.support.constraint.ConstraintLayout>
    
  3. We also need to add a SearchView to the toolbar so we have somewhere to type. Add a menu_user_search . xml  file to the menu resource folder. Inside it, you will need to add a SearchView :
    <?xmlversion="1.0" encoding="utf-8"?>
    <menuxmlns:android="http://schemas.android.com/apk/res/android"
          xmlns:app="http://schemas.android.com/apk/res-auto">
        <item
            android:id="@+id/menu_search"
            android:icon="@drawable/ic_search"
            app:showAsAction="always|collapseActionView"
            android:title="@string/search_icon_title"
            app:actionViewClass="android.support.v7.widget.SearchView"/>
    </menu>
    
  4. We need to create a layout that will be used for each item in the RecyclerView. Create a file named list_item_user . xml  in the layout folder. I used ConstraintLayout with one ImageView for the avatar and two TextViews.
    <?xmlversion="1.0" encoding="utf-8"?>
    <android.support.constraint.ConstraintLayoutxmlns:android="http://schemas.android.com/apk/res/android"
                                                xmlns:app="http://schemas.android.com/apk/res-auto"
                                                xmlns:tools="http://schemas.android.com/tools"
                                                android:id="@+id/constraintLayout"
                                                android:layout_width="match_parent"
                                                android:layout_height="wrap_content"
                                                android:orientation="vertical">
     
        <ImageView
            android:id="@+id/imageview_userprofilepic"
            android:layout_width="50dp"
            android:layout_height="50dp"
            android:layout_marginLeft="16dp"
            android:layout_marginStart="16dp"
            android:layout_marginTop="16dp"
            app:layout_constraintLeft_toLeftOf="@+id/constraintLayout"
            app:layout_constraintTop_toTopOf="@+id/constraintLayout"
            app:srcCompat="@mipmap/ic_launcher"/>
     
        <TextView
            android:id="@+id/textview_username"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginLeft="16dp"
            android:layout_marginStart="16dp"
            android:layout_marginTop="16dp"
            android:textAppearance="@style/TextAppearance.AppCompat.Medium"
            app:layout_constraintLeft_toRightOf="@+id/imageview_userprofilepic"
            app:layout_constraintTop_toTopOf="@+id/constraintLayout"
            tools:text="Rebecca Franks"/>
     
        <TextView
            android:id="@+id/textview_user_profile_info"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginEnd="16dp"
            android:layout_marginLeft="16dp"
            android:layout_marginRight="16dp"
            android:layout_marginStart="16dp"
            android:textAppearance="@style/TextAppearance.AppCompat.Caption"
            app:layout_constraintBottom_toBottomOf="@+id/constraintLayout"
            app:layout_constraintLeft_toRightOf="@+id/imageview_userprofilepic"
            app:layout_constraintRight_toRightOf="@+id/constraintLayout"
            app:layout_constraintTop_toBottomOf="@+id/textview_username"
            tools:text="JHB, South Africa. Lots of code, lots and lots and lots of code."/>
    </android.support.constraint.ConstraintLayout>
    
  5. Now that we have all the layouts we need, let’s tie the XML to the Activity. First, in onCreate ( )  we will get references to the views we need.
        private UsersAdapterusersAdapter;
        private SearchViewsearchView;
        private Toolbartoolbar;
        private ProgressBarprogressBar;
        private RecyclerViewrecyclerViewUsers;
        private TextViewtextViewErrorMessage;
     
        @Override
        protected void onCreate(BundlesavedInstanceState) {
            ...
     
            toolbar = (Toolbar) findViewById(R.id.toolbar);
            setSupportActionBar(toolbar);
            progressBar = (ProgressBar) findViewById(R.id.progress_bar);
            textViewErrorMessage = (TextView) findViewById(R.id.text_view_error_msg);
            recyclerViewUsers = (RecyclerView) findViewById(R.id.recycler_view_users);
            usersAdapter = new UsersAdapter(null, this);
            recyclerViewUsers.setAdapter(usersAdapter);
     
        }
    
  6. We need to hook the SearchView up into our activity to make it trigger the presenters search ( )  method. In the onCreateOptionsMenu ( ) , add the following code:
    @Override
        public boolean onCreateOptionsMenu(Menumenu) {
            super.onCreateOptionsMenu(menu);
            getMenuInflater().inflate(R.menu.menu_user_search, menu);
            final MenuItemsearchActionMenuItem = menu.findItem(R.id.menu_search);
            searchView = (SearchView) searchActionMenuItem.getActionView();
            searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
                @Override
                public boolean onQueryTextSubmit(String query) {
                    if (!searchView.isIconified()) {
                        searchView.setIconified(true);
                    }
                    userSearchPresenter.search(query);
                    toolbar.setTitle(query);
                    searchActionMenuItem.collapseActionView();
                    return false;
                }
     
                @Override
                public boolean onQueryTextChange(String s) {
                    return false;
                }
            });
            searchActionMenuItem.expandActionView();
            return true;
        }
    

    This will inflate the correct menu, find the search view and set a query text listener. In this case, only when someone presses submit on the keyboard, we will respond by calling the search presenter with the query. We could do it in onQueryTextChange too but due to rate limiting on the Github API I will stick to onQueryTextSubmit . By default, the item will be expanded.

  7. Next, we will implement the callbacks that the presenter will call when the items are finished loading.
        @Override
        public void showSearchResults(List<User> githubUserList) {
            recyclerViewUsers.setVisibility(View.VISIBLE);
            textViewErrorMessage.setVisibility(View.GONE);
            usersAdapter.setItems(githubUserList);
        }
     
        @Override
        public void showError(String message) {
            textViewErrorMessage.setVisibility(View.VISIBLE);
            recyclerViewUsers.setVisibility(View.GONE);
            textViewErrorMessage.setText(message);
        }
     
        @Override
        public void showLoading() {
            progressBar.setVisibility(View.VISIBLE);
            recyclerViewUsers.setVisibility(View.GONE);
            textViewErrorMessage.setVisibility(View.GONE);
        }
     
        @Override
        public void hideLoading() {
            progressBar.setVisibility(View.GONE);
            recyclerViewUsers.setVisibility(View.VISIBLE);
            textViewErrorMessage.setVisibility(View.GONE);
     
        }
    
    We are basically just toggling visibility of views here and setting the usersAdapter   to the new items that the service returned.
  8. For completeness, here is the UserSearchAdapter  class which is used for the RecyclerView  on the activity:
    class UsersAdapterextends RecyclerView.Adapter<UserViewHolder> {
        private final Contextcontext;
        private List<User> items;
     
        UsersAdapter(List<User> items, Contextcontext) {
            this.items = items;
            this.context = context;
        }
     
        @Override
        public UserViewHolderonCreateViewHolder(ViewGroupparent, int viewType) {
            View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.list_item_user, parent, false);
            return new UserViewHolder(v);
        }
     
        @Override
        public void onBindViewHolder(UserViewHolderholder, int position) {
            Useritem = items.get(position);
     
            holder.textViewBio.setText(item.getBio());
            if (item.getName() != null) {
                holder.textViewName.setText(item.getLogin() + " - " + item.getName());
            } else {
                holder.textViewName.setText(item.getLogin());
            }
            Picasso.with(context).load(item.getAvatarUrl()).into(holder.imageViewAvatar);
        }
     
        @Override
        public int getItemCount() {
            if (items == null) {
                return 0;
            }
            return items.size();
        }
     
        void setItems(List<User> githubUserList) {
            this.items = githubUserList;
            notifyDataSetChanged();
        }
    }
     
     
    class UserViewHolderextends RecyclerView.ViewHolder {
        final TextViewtextViewBio;
        final TextViewtextViewName;
        final ImageViewimageViewAvatar;
     
        UserViewHolder(View v) {
            super(v);
            imageViewAvatar = (ImageView) v.findViewById(R.id.imageview_userprofilepic);
            textViewName = (TextView) v.findViewById(R.id.textview_username);
            textViewBio = (TextView) v.findViewById(R.id.textview_user_profile_info);
        }
    }
    

    Injection class

    public class Injection {
     
        private static final String BASE_URL = "https://api.github.com";
        private static OkHttpClientokHttpClient;
        private static GithubUserRestServiceuserRestService;
        private static RetrofitretrofitInstance;
     
     
        public static UserRepositoryprovideUserRepo() {
            return new UserRepositoryImpl(provideGithubUserRestService());
        }
     
        static GithubUserRestServiceprovideGithubUserRestService() {
            if (userRestService == null) {
                userRestService = getRetrofitInstance().create(GithubUserRestService.class);
            }
            return userRestService;
        }
     
        static OkHttpClientgetOkHttpClient() {
            if (okHttpClient == null) {
                HttpLoggingInterceptorlogging = new HttpLoggingInterceptor();
                logging.setLevel(HttpLoggingInterceptor.Level.BASIC);
                okHttpClient = new OkHttpClient.Builder().addInterceptor(logging).build();
            }
     
            return okHttpClient;
        }
     
        static RetrofitgetRetrofitInstance() {
            if (retrofitInstance == null) {
                Retrofit.Builderretrofit = new Retrofit.Builder().client(Injection.getOkHttpClient()).baseUrl(BASE_URL)
                        .addConverterFactory(GsonConverterFactory.create())
                        .addCallAdapterFactory(RxJavaCallAdapterFactory.create());
                retrofitInstance = retrofit.build();
     
            }
            return retrofitInstance;
        }
    }
    
  9. Now if you run the app, you should be able to search for a username on Github and see results.

Yay! We have a working app. Code for this post can be found here . In the next part, we will look at writing UI tests for the app. Make sure you subscribe so you don’t miss a post!





About List