SureshJoshi.com ▼

Android Adventures - Wax On, Wax Off


2015-10-21

I spent last week at a hardware/firmware nerd conference (Renesas DevCon) in California. As a result, there was no blog post - however I learned/saw a lot of really cool stuff at the conference which is going to trickle down into some of my upcoming products, and will thus also trickle down into posts!

Without any ado… Bryan’s back, and here to talk about car maintenance… Or adding some polish to the Android app he worked on in part 1, part 2, and part 3.

Enter Bryan

Oh.

Oh my.

You’re here already. I’m not dressed.

Welcome to Android Adventures Part Four - This Time, It’s for Real

Today we’re going to be talking about something oft overlooked, but critically important for user experience - polish. [SJ: Note, polish… not Polish]

No fancy libraries today - it’s going to be all UI, baby.

That’s not to say we won’t be adding any new features - notably, we’ll be making our List Filterable, Brita-style, as well as adding some progress dialogs and an increasingly common update pattern called ‘Swipe To Refresh’.

We’ll also be making some small changes to our layouts, and renaming some files so as to be less obtuse and hard to read.

So strap in and get ready to wax on, wax off, and most importantly wax poetic - before you know it, we’ll have beaten the Cobra Kai at the All-Valley Tournament.

Name Changes

You might recall that we named both the XML and the Activity class for the Webview ‘Single List Item’ (or some variation thereof).

That’s a terrible name.

Instead, I renamed the XML to repo_webview and the Activity class to GitRepoWebActivity. That means that we need to make a change in the ListViewAdapter, where we create the Activity in the first place, the AndroidManifest.xml, where we place our tags, and, of course, in our class declaration itself.

I also wanted to start following a more informative convention - putting the type of the class after the class name. That in mind, I changed UserProfile to UserProfileActivity to fit into the pattern of MainActivity, ListViewAdapter, and GitRepoWebActivity.

[SJ: Note… In Android Studio - all of this re-naming takes place with a one-click refactor]

Onwards!

XML Tomfoolery

Let’s start with something familiar.

Right now, our design is looking a little sparse and utilitarian - which is a good description of my last date. Let’s punch it up a bit.

We’re going to be adding a header to all of our Activities! You can really name your activity anything you want, but I called mine GitHub Repo Activity which, if we’re going to be honest, is pretty much nailing it.

I also wanted to introduce you to a neat way of sharing XML elements across multiple layouts.

First, let’s create our header. We’ll be making a new layout file in our res/layout folder I called titlebar.xml. The code is below:

<?xml version="1.0" encoding="utf-8"?>
    <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <TextView
        android:text="GitHub Repo Activity!"
        android:textSize="24sp"
        android:gravity="center"
        android:layout_width="fill_parent"
        android:layout_height="50dp"
        android:scrollbars="vertical" />

    </FrameLayout>

Pretty simple, right? The interesting part is how we’re going to share it across our layouts. That’s done with a single tag:

 <include
        android:id="@+id/titlebar"
        layout="@layout/titlebar" />

If we insert this tag, our titlebar.xml layout will be pulled into any layout we like. We’ll also need to add layout attributes to ensure that all of our elements come underneath our titlebar. For an example, our new repo_webview.xml (The Webview Formerly Known as Single List Item) should look like this:

<?xml version="1.0" encoding="utf-8"?>
    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <include
            android:id="@+id/titlebar"
            layout="@layout/titlebar" />

        <WebView xmlns:android="http://schemas.android.com/apk/res/android"
            android:id="@+id/webview"
            android:layout_below="@id/titlebar"
            android:layout_width="fill_parent"
            android:layout_height="fill_parent" />
    </RelativeLayout>

As you can see, I’ve added an android:layout_below attribute to our Webview to ensure that it renders underneath the header. Adding this to our user profile activity…

    <?xml version="1.0" encoding="utf-8"?>
    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <include
            android:id="@+id/titlebar"
            layout="@layout/titlebar" />

        <RelativeLayout
            android:id="@+id/header"
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:layout_below="@id/titlebar"
            android:orientation="vertical">


            <ImageView
                android:id="@+id/profile_image"
                android:layout_width="fill_parent"
                android:gravity="center_horizontal"
                android:layout_marginTop="57dp"
                android:layout_height="200dp" />

            <Button
                android:id="@+id/cancel_button"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_alignParentStart="true"
                android:layout_alignParentTop="true"
                android:layout_weight="1"
                android:text="Cancel" />

            <Button
                android:id="@+id/done_button"
                style="?android:textAppearanceSmall"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_alignParentTop="true"
                android:layout_alignParentEnd="true"
                android:layout_weight="1"
                android:text="Done" />

        </RelativeLayout>

        <TextView
            android:id="@+id/username"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="30dp"
            android:layout_marginStart="25dp"
            android:layout_below="@+id/header"
            android:text="Username" />

        <EditText
            android:id="@+id/username_edit"
            android:layout_width="400dp"
            android:layout_height="wrap_content"
            android:layout_marginStart="25dp"
            android:layout_below="@+id/username"
            android:background="@drawable/editbox" />

        <TextView
            android:id="@+id/name"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="30dp"
            android:layout_marginStart="25dp"
            android:layout_below="@+id/username_edit"
            android:text="Name" />

        <EditText
            android:id="@+id/name_edit"
            android:layout_width="400dp"
            android:layout_height="wrap_content"
            android:layout_marginStart="25dp"
            android:layout_below="@+id/name"
            android:background="@drawable/editbox"

            />

        <TextView
            android:id="@+id/email"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="30dp"
            android:layout_marginStart="25dp"
            android:layout_below="@+id/name_edit"
            android:text="Email" />

        <EditText
            android:id="@+id/email_edit"
            android:layout_width="400dp"
            android:layout_height="wrap_content"
            android:layout_marginStart="25dp"
            android:layout_below="@+id/email"
            android:background="@drawable/editbox"

            />

        />

        <TextView
            android:id="@+id/public_repos"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="30dp"
            android:layout_marginStart="25dp"
            android:layout_below="@+id/email_edit"
            android:text="Public Repositories" />

        <EditText
            android:id="@+id/public_repos_edit"
            android:layout_width="400dp"
            android:layout_height="wrap_content"
            android:layout_marginStart="25dp"
            android:layout_below="@+id/public_repos"
            android:background="@drawable/editbox"/>

        <TextView
            android:id="@+id/public_gists"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="30dp"
            android:layout_marginStart="25dp"
            android:layout_below="@+id/public_repos_edit"
            android:text="Public Gists" />

        <EditText
            android:id="@+id/public_gists_edit"
            android:layout_width="400dp"
            android:layout_height="wrap_content"
            android:layout_marginStart="25dp"
            android:layout_below="@+id/public_gists"
            android:background="@drawable/editbox"/>

        <TextView
            android:id="@+id/followers"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="30dp"
            android:layout_marginStart="25dp"
            android:layout_below="@+id/public_gists_edit"
            android:text="Followers" />

        <EditText
            android:id="@+id/followers_edit"
            android:layout_width="400dp"
            android:layout_height="wrap_content"
            android:layout_marginStart="25dp"
            android:layout_below="@+id/followers"
            android:background="@drawable/editbox"

            />


        <TextView
            android:id="@+id/following"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="30dp"
            android:layout_marginStart="25dp"
            android:layout_below="@+id/followers_edit"
            android:text="Following" />

        <EditText
            android:id="@+id/following_edit"
            android:layout_width="400dp"
            android:layout_height="wrap_content"
            android:layout_marginStart="25dp"
            android:layout_below="@+id/following"
            android:background="@drawable/editbox"

            />


    </RelativeLayout>

[SJ: While I like ‘include’ tags as much as anyone because they clean up excess, redundant XML - the downside is that it’s a bit more work to edit the content of an include tag programmatically… As a result, -sometimes- it’s a bit cleaner to duplicate the XML code, in order to reduce the duplicated Java code and reduce overall complexity… No rules behind this, just what the developer feels makes more sense]

Just as before, we’re ensuring that all of our layout elements are fall underneath the header.

You might have noticed that the UserProfile is a bit jumpy when it starts, and it keeps changing sizes depending on the avatar image - can’t have that, so I also made sure to set the size of the ImageView to a constant value. It’ll also come in handy for our placeholder image!

You may also have noticed that I haven’t included the final code for the MainActivity. That’s because there’s another feature I need to talk about first - the SwipeRefreshLayout class.

SwipeRefreshLayout

We’ve all been there. We’re on our phones, checking our inboxes, our feeds, our chats, and in our insatiable lust for content we want to refresh our views to see what’s new.

Some apps still have that ancient ‘Refresh’ button, but nowadays the hip, young thing is to swipe down to refresh the layout. In fact, it’s so common nowadays that Android has written an interface specifically for it - SwipeRefreshLayout.

Let’s use it in our application!

[SJ: Ideally, we would never need a manual refresh, but most apps aren’t written well enough for reliable background updates across the board - meaning users don’t really trust most apps enough to remove the need for a manual refresh option. A good counter example of this is Netflix (I don’t even know if there is a manual ‘refresh’ option in my Netflix app). Conveniently, Netflix are heavy users/maintainers of RxJava and RxAndroid which are libraries that will eventually get us away from manual refreshes!!!]

First, we need to wrap the view we want to refresh in a swipe container

  • we’re going to do this in our activity_main.xml
    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:padding="6dip"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context=".MainActivity">


    <include
        android:id="@+id/titlebar"
        layout="@layout/titlebar" />

    <EditText
        android:id="@+id/filterbar"
        android:layout_width="fill_parent"
        android:layout_height="50dp"
        android:layout_below="@id/titlebar"
        android:hint="Filter by Name" />

    <android.support.v4.widget.SwipeRefreshLayout
        android:id="@+id/swipe_container"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_below="@+id/filterbar">

        <ListView xmlns:android="http://schemas.android.com/apk/res/android"
            android:id="@+id/listView"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content" />
    </android.support.v4.widget.SwipeRefreshLayout>

    <TextView
        android:id="@+id/empty"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        android:gravity="center"
        android:text="Swipe down to refresh!"
        android:textSize="16sp" />
    </RelativeLayout>

As you can see, we’ve wrapped up our ListView element within a pair of SwipeRefreshLayout tags. I also included the implementation for the title bar inclusion, and an empty view that we’re going to display as a placeholder for our ListView before we make the API call.

You may also notice a new EditText to our layout. For now, that EditText doesn’t do anything - but we’re going to use it to help filter our list!

Now that we have our layout file in order, we need to start tinkering with the actual Activity. First, our class declaration needs to change.

public class MainActivity extends Activity implements SwipeRefreshLayout.OnRefreshListener {

Note that, in order to implement this class, we need to @Override a method called onRefresh (so fresh you guys). More on that later. We’re also going to want to ButterKnife bind our SwipeRefreshLayout, as per usual.

@Bind(R.id.swipe_container)
SwipeRefreshLayout mSwipeRefreshLayout;

Then, we set our onRefreshListener

mSwipeRefreshLayout.setOnRefreshListener(this);

Just for funsies, we can actually customize how our refresher cycles through colors -

mSwipeRefreshLayout.setColorSchemeResources(android.R.color.holo_blue_bright,
    android.R.color.holo_green_light,
    android.R.color.holo_orange_light,
    android.R.color.holo_red_light);

But this is 100% optional.

We only want to have our application make the HTTP request when the user wants it to, so we’re going to move our entire getFeed call into the onRefresh method, like so:

    @Override
    public void onRefresh() {
        git.getFeed(new Callback<List<GitModel>>() {
            @Override
            public void failure(RetrofitError error) {
                mSwipeRefreshLayout.setRefreshing(false);
                Toast.makeText(getApplicationContext(), "Failed to load", Toast.LENGTH_SHORT).show();
            }

            @Override
            public void success(final List<GitModel> gitmodel, Response response) {
                mSwipeRefreshLayout.setRefreshing(false);
                listAdapter.setGitmodel(gitmodel);
            }
        });
    }

The setRefreshing() method determines whether or not the refreshing animation should be running on-screen - we set it to false as soon as our API call is complete, whether it succeeds or not (we don’t want it to look like the application is refreshing when it really isn’t, which is exactly how I feel about most beers).

We also want to reuse what we learned about Toasts (i.e., they’re delicious) in our last post and have one pop-up if the API request fails, such as if your phone isn’t connected to a network.

[SJ: Toasts are great, except one ‘issue’ with them is that they persist whether the app is open or not (as in, not limited to just the app). If that’s necessary, then fine - if not, try to develop using in-app notifications, like Crouton or Snackbar]

Now, let’s talk about Filterable.

Filters

Oh boy, this one is a doozy.

We’re going to start in the ListViewAdapter class. Just like the SwipeRefreshLayout, Filterable is an interface, which we are going to implement within our ListViewAdapter.

public class ListViewAdapter extends BaseAdapter implements Filterable {

In order to implement Filterable, we really only need to @Override the method ‘getFilter()’. That’s not as easy as it sounds (but it’s not too hard either).

First, the small stuff.

We still want to load our original data, so we’re not going to be changing any variables. We are going to be adding a couple though.

List<GitModel> filteredGitModelList;
private ItemFilter mFilter = new ItemFilter();

You’ll notice that the class ItemFilter doesn’t actually exist, which, again, reminds me of my last date. That’s because we’re going to be making it ourselves, just like my love life.

We’ll also be changing our constructor -

public ListViewAdapter(List<GitModel> gitModelList, Context context) {         this.inflater = LayoutInflater.from(context);
    this.gitModelList = gitModelList;
    this.filteredGitModelList = gitModelList;
    this.context = context;
}

Because we’re really more interested in our filtered list, we need to change our getCount, getItem, getItemId, getView, setGitmodel, and clearGitmodel methods as well.

    @Override
    public int getCount() {
        return filteredGitModelList.size();
    }

    @Override
    public Object getItem(int position) {
        return filteredGitModelList.get(position);
    }

    @Override
    public long getItemId(int position) {
        return position;
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {

        ViewHolder holder;
        if (convertView == null) {
            convertView = inflater.inflate(R.layout.activity_list_view, parent, false);
            holder = new ViewHolder(convertView);
            convertView.setTag(holder);

        } else {
            holder = (ViewHolder) convertView.getTag();
        }
        Picasso.with(inflater.getContext())
                .load(filteredGitModelList.get(position).getOwner().getAvatar_url()) //""+gitmodelList.get(position).getOwner().getAvatar_url()
                .into(holder.image);

        holder.text.setText(" Name: " + filteredGitModelList.get(position).getName()
                + "\t id: " + filteredGitModelList.get(position).getId() + "\n");

        holder.texttwo.setText(filteredGitModelList.get(position).getOwner().getLogin());

        holder.image.setOnClickListener(new MyOnClickListener(filteredGitModelList, position));
        holder.text.setOnClickListener(new MyOnClickListener(filteredGitModelList, position));
        holder.texttwo.setOnClickListener(new MyOnClickListener(filteredGitModelList, position));
        return convertView;

    }
    ...
    public void setGitmodel(List<GitModel> gitModelList) {
        clearGitmodel();
        this.gitModelList = gitModelList;
        this.filteredGitModelList = gitModelList;
        notifyDataSetChanged();
    }

    public void clearGitmodel() {
        this.gitModelList.clear();
        this.filteredGitModelList.clear();
    }

But that’s no big deal. In fact, the getFilter() method is also very simple, just a line of code inside -

public Filter getFilter() {
    return mFilter;
}

The interesting stuff is in our ItemFilter class, which you can view below -

private class ItemFilter extends Filter {
        @Override
        protected FilterResults performFiltering(CharSequence constraint) {

            String filterString = constraint.toString().toLowerCase();

            FilterResults results = new FilterResults();

            final List<GitModel> list = gitModelList;

            int count = list.size();
            final ArrayList<GitModel> nlist = new ArrayList<GitModel>(count);

            GitModel filterableGitModel;

            for (int i = 0; i < count; i++) {
                filterableGitModel = list.get(i);
                if (filterableGitModel.getOwner().getLogin().toLowerCase().contains(filterString)) {
                    nlist.add(filterableGitModel);
                }
            }

            results.values = nlist;
            results.count = nlist.size();

            return results;
        }

        @SuppressWarnings("unchecked")
        @Override
        protected void publishResults(CharSequence constraint, Filter.FilterResults results) {
            filteredGitModelList = (ArrayList<GitModel>) results.values;
            notifyDataSetChanged();
        }

    }

I decided I wanted to filter by Username, but this implementation is extensible to repository, name, id, email, really anything you want!

A brief overview of what’s happening above - our ItemFilter class extends the Filter class which requires that we @Override two methods - performFiltering and publishResults.

publishResults() is quite straightforward - set our data list to the results of our filtering, then notify the adapter that the dataset has changed.

performFiltering() is a little more confusing, but not by too much. The method takes in a sequence of characters, converts them to a lowercase String, then iterates through the original data model, only returning those items that have that sequence of characters in your chosen attribute.

That is, it performs filtering.

While we’re here, we might as well add a placeholder image to our ListView adapter. Grab a photo and put it into your res/drawable folder. I chose a placeholder image (creatively, I named it ‘placeholder’) from the following website - http://nearpictures.com/pages/u/user-image-placeholder/

Then, because Picasso is awesome, we just need to make a tiny change in our getView adapter

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {

        ...

        Picasso.with(inflater.getContext())
                .load(filteredGitModelList.get(position).getOwner().getAvatar_url()) //""+gitmodelList.get(position).getOwner().getAvatar_url()
                .placeholder(R.drawable.placeholder)
                .into(holder.image);

        ...

        return convertView;

    }

And that’s our ListViewAdapter set! Our new code, in its entirety, is below (sans imports, as per usual):

public class ListViewAdapter extends BaseAdapter implements Filterable {

    LayoutInflater inflater;
    List<GitModel> gitModelList;
    List<GitModel> filteredGitModelList;
    Context context;

    private ItemFilter mFilter = new ItemFilter();

    public ListViewAdapter(List<GitModel> gitModelList, Context context) {
        this.inflater = LayoutInflater.from(context);
        this.gitModelList = gitModelList;
        this.filteredGitModelList = gitModelList;
        this.context = context;
    }

       @Override
    public int getCount() {
        return filteredGitModelList.size();
    }

    @Override
    public Object getItem(int position) {
        return filteredGitModelList.get(position);
    }

    @Override
    public long getItemId(int position) {
        return position;
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {

        ViewHolder holder;
        if (convertView == null) {
            convertView = inflater.inflate(R.layout.activity_list_view, parent, false);
            holder = new ViewHolder(convertView);
            convertView.setTag(holder);

        } else {
            holder = (ViewHolder) convertView.getTag();
        }
        Picasso.with(inflater.getContext())
                .load(filteredGitModelList.get(position).getOwner().getAvatar_url()) //""+gitmodelList.get(position).getOwner().getAvatar_url()
                .placeholder(R.drawable.placeholder)
                .into(holder.image);

        holder.text.setText(" Name: " + filteredGitModelList.get(position).getName()
                + "\t id: " + filteredGitModelList.get(position).getId() + "\n");

        holder.texttwo.setText(filteredGitModelList.get(position).getOwner().getLogin());

        holder.image.setOnClickListener(new MyOnClickListener(filteredGitModelList, position));
        holder.text.setOnClickListener(new MyOnClickListener(filteredGitModelList, position));
        holder.texttwo.setOnClickListener(new MyOnClickListener(filteredGitModelList, position));
        return convertView;
    }

    static class ViewHolder {
        @Bind(R.id.image_in_item)
        ImageView image;
        @Bind(R.id.textview_in_item)
        TextView text;
        @Bind(R.id.textview_in_item_two)
        TextView texttwo;

        public ViewHolder(View view) {
            ButterKnife.bind(this, view);
        }
    }

    public void setGitmodel(List<GitModel> gitModelList) {
        clearGitmodel();
        this.gitModelList = gitModelList;
        this.filteredGitModelList = gitModelList;
        notifyDataSetChanged();
    }

    public void clearGitmodel() {
        this.gitModelList.clear();
        this.filteredGitModelList.clear();
    }

    private class MyOnClickListener implements View.OnClickListener {
        List<GitModel> gitModelList;
        int position;

        private MyOnClickListener(List<GitModel> gitModelList, int position) {
            this.gitModelList = gitModelList;
            this.position = position;
        }

        @Override
        public void onClick(View view) {


            if (view instanceof ImageView) {

                String url = gitModelList.get(position).getOwner().getHtml_url();

                // Launching new Activity on selecting single List Item
                Intent i = new Intent(context, GitRepoWebActivity.class);
                // sending data to new activity
                i.putExtra("url", url);
                view.getContext().startActivity(i);

            } else if (view instanceof TextView) {
                String name = gitModelList.get(position).getOwner().getLogin();

                // Launching new Activity on selecting single List Item
                Intent i = new Intent(context, UserProfileActivity.class);
                // sending data to new activity
                i.putExtra("name", name);
                view.getContext().startActivity(i);
            }

        }
    }

    public Filter getFilter() {
        return mFilter;
    }

    private class ItemFilter extends Filter {
        @Override
        protected FilterResults performFiltering(CharSequence constraint) {

            String filterString = constraint.toString().toLowerCase();

            FilterResults results = new FilterResults();

            final List<GitModel> list = gitModelList;

            int count = list.size();
            final ArrayList<GitModel> nlist = new ArrayList<GitModel>(count);

            GitModel filterableGitModel;

            for (int i = 0; i < count; i++) {
                filterableGitModel = list.get(i);
                if (filterableGitModel.getOwner().getLogin().toLowerCase().contains(filterString)) {
                    nlist.add(filterableGitModel);
                }
            }

            results.values = nlist;
            results.count = nlist.size();

            return results;
        }

        @SuppressWarnings("unchecked")
        @Override
        protected void publishResults(CharSequence constraint, Filter.FilterResults results) {
            filteredGitModelList = (ArrayList<GitModel>) results.values;
            notifyDataSetChanged();
        }

      }

    }

“But Bryan”, you might say, “how do I actually interact with this Filter? Also, maybe they just got into an accident or something and forgot to text you.” To which I would say, good point, stranger on the Internet, that’s where our EditText comes in!

Main Activity Redux

We’re finally back to our Main Activity at last. Chekhov’s EditText is finally going to pay off!

We’ll want to @Bind our EditText so we can use it in our Activity.

@Bind(R.id.filterbar)
EditText mFilterString;

Once we have it, we’re going to add a cool Listener called TextChangedListener to it.

mFilterString.addTextChangedListener(searchTextWatcher);

searchTextWatcher doesn’t exist yet - we should fix that! The full code is below:

    private TextWatcher searchTextWatcher = new TextWatcher() {
        @Override
        public void onTextChanged(CharSequence s, int start, int before, int count) {
            // ignore
        }

        @Override
        public void beforeTextChanged(CharSequence s, int start, int count, int after) {
            // ignore
        }

        @Override
        public void afterTextChanged(Editable s) {
            listAdapter.getFilter().filter(s.toString());
        }
    };

As soon as our text has changed, the TextChangedListener passes it into our Filter object, which filters through our dataset, notifies the application of the change, and displays the filtered list in our ListView.

Phew.

Finally, the full MainActivity code is below.

public class MainActivity extends Activity implements SwipeRefreshLayout.OnRefreshListener {

    String API = "https://api.github.com";                         //BASE URL
    GitApi git;
    List<GitModel> placeholderModel;
    ListViewAdapter listAdapter;

    @Bind(R.id.swipe_container)
    SwipeRefreshLayout mSwipeRefreshLayout;

    @Bind(R.id.empty)
    TextView emptyView;
    @Bind(R.id.listView)
    ListView list;
    @Bind(R.id.filterbar)
    EditText mFilterString;


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ButterKnife.bind(this);

        mSwipeRefreshLayout.setOnRefreshListener(this);
        mFilterString.addTextChangedListener(searchTextWatcher);

        placeholderModel = new ArrayList<>();
        listAdapter = new ListViewAdapter(placeholderModel, this);


        list.setEmptyView(emptyView);
        list.setAdapter(listAdapter);


        RestAdapter restAdapter = new RestAdapter.Builder()
                .setEndpoint(API).build();                                        //create an adapter for retrofit with base url

        git = restAdapter.create(GitApi.class);                            //creating a service for adapter with our GET class

        mSwipeRefreshLayout.setColorSchemeResources(android.R.color.holo_blue_bright,
                android.R.color.holo_green_light,
                android.R.color.holo_orange_light,
                android.R.color.holo_red_light);
    }

    @Override
    public void onRefresh() {
        git.getFeed(new Callback<List<GitModel>>() {
            @Override
            public void failure(RetrofitError error) {
                mSwipeRefreshLayout.setRefreshing(false);
                Toast.makeText(getApplicationContext(), "Failed to load", Toast.LENGTH_SHORT).show();
            }

            @Override
            public void success(final List<GitModel> gitmodel, Response response) {
                mSwipeRefreshLayout.setRefreshing(false);
                listAdapter.setGitmodel(gitmodel);
            }
        });
    }

    private TextWatcher searchTextWatcher = new TextWatcher() {
        @Override
        public void onTextChanged(CharSequence s, int start, int before, int count) {
            // ignore
        }

        @Override
        public void beforeTextChanged(CharSequence s, int start, int count, int after) {
            // ignore
        }

        @Override
        public void afterTextChanged(Editable s) {
            listAdapter.getFilter().filter(s.toString());
        }
      };
    }

The final feature we’re going to add today is small, but important. Remember that we’re making network calls throughout this whole application, which is fine if you’ve got a fast network, but less so if the network is inconsistent or even completely disconnected. We need some UI elements to address that issue.

That’s where the ProgressDialog class comes in.

Progress Dialog

So, there’s been a lot of talk about how great these open source libraries are, but let’s not forget that Android is pretty cool too. I know because I tried to write my own pop-up dialog using AsyncTask

  • it was a nightmare.

Little did I know that there’s actually a very simple way to implement a popup to show the user that something is happening (especially important for the WebView - on a slow connection it can take several seconds to load the entire webpage). The class is called ProgressDialog and it’s amazing.

The WebView implementation is a tiny bit more complicated, so let’s start with that one.

We’re going to be adding this block of code to the GitRepoWebActivity -

    mWebView.setWebViewClient(new WebViewClient() {
            private ProgressDialog mProgressDialog;

            @Override
            public void onPageStarted(WebView view, String url, Bitmap favicon) {
                super.onPageStarted(view, url, favicon);
                mProgressDialog = new ProgressDialog(view.getContext());
                mProgressDialog.setMessage("Please wait. Loading...");
                mProgressDialog.setCancelable(true);
                mProgressDialog.setProgressStyle(ProgressDialog.STYLE_SPINNER);
                // Need this to avoid crashes when backing out of web activity too fast
                if (!isFinishing()) {
                    mProgressDialog.show();
                }
            }

            @Override
            public void onPageFinished(WebView view, String url) {
                super.onPageFinished(view, url);
                mProgressDialog.dismiss();
            }

        });

Pay special attention to the ‘if (!isFinishing())’ conditional - without it, if a user tries to exit out of the dialog while the page is loading, or starting to load, you’re stuck in a race condition to see if the application crashes horribly with a WindowManager\$BadTokenException.

The conditional ensures that the progressDialog only shows if the WebView exists and isn’t about to close. We only dismiss the Dialog when we’re sure that the page is done - in the onPageFinished method. Altogether, the GitRepoWebActivity looks like

    public class GitRepoWebActivity extends Activity {
    @Bind(R.id.webview)
    WebView mWebView;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.repo_webview);
        ButterKnife.bind(this);

        String url = getIntent().getStringExtra("url");
        mWebView.loadUrl(url);
        mWebView.setWebViewClient(new WebViewClient() {
            private ProgressDialog mProgressDialog;

            @Override
            public void onPageStarted(WebView view, String url, Bitmap favicon) {
                super.onPageStarted(view, url, favicon);
                mProgressDialog = new ProgressDialog(view.getContext());
                mProgressDialog.setMessage("Please wait. Loading...");
                mProgressDialog.setCancelable(true);
                mProgressDialog.setProgressStyle(ProgressDialog.STYLE_SPINNER);
                // Need this to avoid crashes when backing out of web activity too fast
                if (!isFinishing()) {
                    mProgressDialog.show();
                }
            }

            @Override
            public void onPageFinished(WebView view, String url) {
                super.onPageFinished(view, url);
                mProgressDialog.dismiss();
            }

        });

       }
    }

We’re also making a network call in our User Profile Activity, so we need to implement something like this as well. Problem is, the User Profile isn’t a webpage, so we can hardly put our dismiss() in an onPageFinished method well, turns out, it’s actually even easier.

@Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.user_profile);
        ButterKnife.bind(this);

        ...

        mProgressDialog = new ProgressDialog(this);
        mProgressDialog.setMessage("Please wait. Loading...");
        mProgressDialog.setCancelable(true);
        mProgressDialog.setProgressStyle(ProgressDialog.STYLE_SPINNER);
        // Need this to avoid crashes when backing out of web activity too fast
        if (!isFinishing()) {
            mProgressDialog.show();
        }

        Picasso.with(getApplicationContext())
                .load(R.drawable.placeholder)
                .into(image);

       ...

        git.getUser(name, new Callback<User>() {
            @Override
            public void failure(RetrofitError error) {
                ...
                mProgressDialog.dismiss();
                Toast.makeText(getApplicationContext(), "Failed to load", Toast.LENGTH_SHORT).show();
                ...
            }

            @Override
            public void success(User user, Response response) {
                ...
                mProgressDialog.dismiss();
                ...
            }


        });


       }
    }

Rather than needing to hook into completely new methods, we can just show and dismiss the progress dialog in our existing ones! I also took the opportunity to add some Toasts (I’m so hungry you guys) and add our placeholder image from before.

The final code for the GitRepoWebActivity is below.

public class UserProfileActivity extends Activity implements Validator.ValidationListener {
    static final String API = "https://api.github.com";                         //BASE URL
    Validator validator;
    private ProgressDialog mProgressDialog;


    @Bind(R.id.profile_image)
    ImageView image;

    @NotEmpty
    @Bind(R.id.username_edit)
    EditText username_edit;

    @NotEmpty
    @Bind(R.id.name_edit)
    EditText name_edit;

    @NotEmpty
    @Email
    @Bind(R.id.email_edit)
    EditText email_edit;

    @NotEmpty
    @Bind(R.id.public_repos_edit)
    EditText public_repos_edit;

    @NotEmpty
    @Bind(R.id.public_gists_edit)
    EditText public_gists_edit;

    @NotEmpty
    @Bind(R.id.followers_edit)
    EditText followers_edit;

    @NotEmpty
    @Bind(R.id.following_edit)
    EditText following_edit;


    @Bind(R.id.done_button)
    Button done_button;

    @Bind(R.id.cancel_button)
    Button cancel_button;

    @Override
    public void onValidationSucceeded() {
        Toast.makeText(this, "Yay! we got it right!", Toast.LENGTH_SHORT).show();
        finish();
    }

    @Override
    public void onValidationFailed(List<ValidationError> errors) {
        for (ValidationError error : errors) {
            View view = error.getView();
            String message = error.getCollatedErrorMessage(this);

            // Display error messages ;)
            if (view instanceof EditText) {
                ((EditText) view).setError(message);
            } else {
                Toast.makeText(this, message, Toast.LENGTH_LONG).show();
            }
        }
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.user_profile);
        ButterKnife.bind(this);

        validator = new Validator(this);
        validator.setValidationListener(this);
        // Code…

        mProgressDialog = new ProgressDialog(this);
        mProgressDialog.setMessage("Please wait. Loading...");
        mProgressDialog.setCancelable(true);
        mProgressDialog.setProgressStyle(ProgressDialog.STYLE_SPINNER);
        // Need this to avoid crashes when backing out of web activity too fast
        if (!isFinishing()) {
            mProgressDialog.show();
        }

        Picasso.with(getApplicationContext())
                .load(R.drawable.placeholder)
                .into(image);

        Intent i = getIntent();
        String name = i.getStringExtra("name");

        RestAdapter restAdapter = new RestAdapter.Builder()
                .setEndpoint(API).build();                                        //create an adapter for retrofit with base url

        GitApi git = restAdapter.create(GitApi.class);


        git.getUser(name, new Callback<User>() {
            @Override
            public void failure(RetrofitError error) {
                mProgressDialog.dismiss();
                Toast.makeText(getApplicationContext(), "Failed to load", Toast.LENGTH_SHORT).show();
                done_button.setOnClickListener(new View.OnClickListener() {
                    public void onClick(View v) {
                        Toast.makeText(getApplicationContext(), "Cannot validate unloaded content", Toast.LENGTH_SHORT).show();
                    }
                });
                cancel_button.setOnClickListener(new View.OnClickListener() {
                    public void onClick(View v) {
                        finish();
                    }
                });


            }

            @Override
            public void success(User user, Response response) {

                Picasso.with(getApplicationContext())
                        .load(user.getAvatar())
                        .into(image);
                username_edit.setText(user.getLogin());
                name_edit.setText(user.getName());
                email_edit.setText(user.getEmail());
                public_repos_edit.setText(user.getPublicRepos());
                public_gists_edit.setText(user.getPublicGists());
                followers_edit.setText(String.valueOf(user.getFollowers()));
                following_edit.setText(String.valueOf(user.getFollowing()));
                cancel_button.setOnClickListener(new View.OnClickListener() {
                    public void onClick(View v) {
                        finish();
                    }
                });

                done_button.setOnClickListener(new View.OnClickListener() {
                    public void onClick(View v) {
                        validator.validate();
                    }
                });

                mProgressDialog.dismiss();
            }
        });
       }
    }

Oh My God, Finally

And with that, we are done! Polishing is complete, at least, for the bare minimum acceptable shininess.

Of course, there’s still more to improve and you should try to be best, ’cause you’re only a man, and a man’s gotta learn to take it. Try to believe, though the going gets rough, that you gotta hang tough to make it. History repeats itself, try and you’ll succeed, never doubt that you’re the one, and you can have your dreams.

You’re the best around. Nothing’s gonna ever keep you down.

God, what a great movie.

Join us next time for Android Adventures Part 5 - Son of Android Adventures, where we’re going to dive into the exciting realm of best practices!

Feature Photo credit: Trekman / CC BY-NC-SA