Exercise Wednesday

Warm Up


  • Describe how the ArrayAdapter works to generate items in the ListView.
  • Why is ButterKnife worth implementing in your projects? What does it do?
  • What is an annotation in Java?
  • In terms of testing, what is a 'shadow'? When and why are they used?
  • How do we interact with data in an adapter?

Practice


By now, the concept of an adapter should start feeling familiar: When we have a collection of data, we need some way to bind single instances of that data to a layout. Even though we previously customized our ArrayAdapter, and changed the way data is displayed through String.format(), we still used a standard resource that is part of the Android package, namely simple_list_item_1 to display our data, which works fine for String types, but doesn’t cut the mustard for more complex data types.

If we have an Orange[], then we need to have something that can visually represent an Orange and all the properties we define an Orange as having. In order to represent more complex data, we’ll need to create a custom adapter and a custom layout file - things can get complicated quickly, and we’ll tackle this in full next week.

app_object_overview

So, let’s start simple - let’s create a gridview, fill it with some data, and style that data with a custom typeface to get our feet wet. This exercise is simple, but will teach you work with custom layouts, custom adapters, and passing information to a custom adapter.

Introduction to GridView

A GridView is just that - it is a View that represents data in a two-dimensional scrolling grid. The items in the grid come from the ListAdapter associated with this View. Because a GridView gets its data from a ListAdapter, the only data loaded in memory will be the one displayed on screen. GridViews, much like ListViews reuse and recycle their views for better performance.

GridLayouts are like LinearLayouts or RelativeLayouts - they organize data on the screen in a grid format. But what they do not do is allow for scrolling, and they do not perform memory management tasks for us. Please don’t try and show images in a GridView or GridLayout until you have a better handle on memory management for Images (see our topics on Picasso this coming weekend), otherwise you will almost certainly have an app that crashes consistently with an Out Of Memory error, or performs very sluggishly.

In this example, we’re going to simply display an alphabet on the screen in a grid, as we might do if we were making an app for kids to learn the alphabet.

We’ll style different views with different fonts to differentiate our views.

Let’s get started!

Setup

First, let’s make a new project in Android Studio using all the standard settings, and choosing “Empty Activity” as our first activity. Name your app and package name whatever you like, but let’s keep the main activity called MainActivity to keep things consistent.

When you’re done, jump over to MainActivity.java file. Let’s write out our alphabet first. Let’s use a String[] as we did before to hold our letters.

MainActivity.java

public class MainActivity extends AppCompatActivity {
   GridView gridView;
   String[] letters = new String[] {
           "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z"};

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


Cool. That was easy!

Making our Custom Layout

Time to make a simple layout that can handle our alphabet display. Our layout will be comprised of two files:

  • The container, a RelativeLayout that holds our GridView (this will live in the XML for our MainActivity)
  • The individual grid "item" - the View or Views that get repeated as many times as necessary inside our GridView. (this will live in a separate file.)

Jump over to activity_main.xml. In here, you should see nothing but a simple TextView, stating “Hello World!” Let’s make the following adjustments:

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
   xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:tools="http://schemas.android.com/tools"
   android:id="@+id/activity_main"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   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="com.example.epicodus_staff.myapplication.MainActivity">

   <TextView
       android:text="Learn the Alphabet"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:id="@+id/headerTextView"
       android:layout_alignParentTop="true"
       android:layout_centerHorizontal="true"
       android:textSize="30sp" />

   <GridView
       android:layout_width="match_parent"
       android:layout_height="wrap_content"
       android:columnWidth="50sp"
       android:numColumns="auto_fit"
       android:stretchMode="spacingWidthUniform"
       android:id="@+id/baseGridView"
       android:padding="0dp"
       android:gravity="left"
       android:scrollingCache="false"
       android:layout_alignParentTop="true"
       android:layout_alignParentLeft="true"
       android:layout_alignParentStart="true"
       android:layout_marginTop="40sp" />
</RelativeLayout>

If you switch to design view, you should see some placeholder text (don't worry about that for now!) displayed in a grid. That’ll be all we need to do with our GridView for now.

Styling our Grid Item

Now we need to make the layout that will handle our individual alphabet items. We’ll connect the two in a second step shortly, telling the GridView to load the XML for the GridItem for each alphabet letter.

Let’s right click on the res folder and select New > Layout Resource File (or New > XML > Layout XML File on some versions of Android Studio). Leaving the layout type as LinearLayout is fine.

This is the XML you want to end up with:

Note: While you can of course tweak the layout, you’ll definitely want to AVOID having your letters require scrolling. If they do, the variable position (more on that soon) will not correctly retrieve values from your arrays. I am working on a solution for this, but for now, please format your letters that they fit all on one screen.

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

   <TextView
       android:layout_width="60sp"
       android:textSize="15sp"
       android:layout_height="wrap_content"
       android:id="@+id/grid_item_label"
       android:layout_weight="0.8"
       android:gravity="center_horizontal"
       android:text="Letter" />

   <TextView
       android:layout_width="60sp"
       android:textSize="30sp"
       android:layout_height="60sp"
       android:id="@+id/grid_item_letter"
       android:layout_weight="1"
       android:gravity="center_horizontal"
       android:text="A" />

</LinearLayout>


We’ll populate this with dynamic data soon. Just a few more steps, then we can test our app!

Making our custom adapter

Now let’s get a start on making our custom adapter. Create a new class in your main package, name it AlphabetAdapter. Make AlphabetAdapter extend BaseAdapter. We’ve done this before, so if you’re unsure, go back and review how to do this from the Customizing ArrayAdapters lesson.

Similarly to our customized ArrayAdapter, AlphabetAdapter needs to have some member variables. We’ll definitely need a Context here, as well as our String[] that we are passing in from our MainActivity.

We’ll also need a constructor.

This’ll work:

AlphabetAdapter.java

public class AlphabetAdapter extends BaseAdapter {
   private Context mContext;
   private String[] mLetters;

  public AlphabetAdapter (Context context, String[] letters){
       this.mContext = context;
       this.mLetters = letters;
   }
}

But then we see:

adapter-constructor-no-overrides.png

Now we still have a angry, red squiggly line - if we hover over it, we might see that we need to implement a method called getCount(). This should be familiar from customizing our ArrayAdapter. Remember: Any time we extend a class, we agree that we will implement that class' required methods - Android Studio will stay angry at us until we deliver on our responsibilities.

Let’s let the IDE do some busywork for us. Right click inside the class, select generate, then choose Override Methods… and select all 4 methods from the android.widget.Adapter package: getCount(), getItem(), getItemId(), and getView().

Some boilerplate code appears, and red squiggly lines (should) disappear. Boilerplate code is a term you'll hear frequently, so be sure to know what it refers to. Boilerplate is no frills, bare minimum code that is either a.) supplied to you, the developer, via a website, template file or some other form of documentation, or auto-generated by an application such as a Command Line Interface (CLI) or IDE (Integrated Development Environment - memorize these). It's represents a starting point for us, but almost always needs to be edited extensively so that it provides relevant functionality to our app.

As you may now expect, so far all the boilerplate code returns null. Not super interesting or useful.

Let’s make some changes.

We’ll leave getItemId() and getItem() alone for now - we need to implement them, but don’t need to customize them at the moment. First, let’s change getCount(). We’ll want to return the .length of our mLetters array, so make getCount() return that instead of null. getCount() is necessary so that our app knows how many times the GridView should repeat the Grid Item.

getView() is where we’ll want to connect our layout with our grid items. Let’s do that now.

Here’s where we should arrive.

AlphabetAdapter.java

public class AlphabetAdapter extends BaseAdapter {
   private Context mContext;
   private String[] mLetters;

   public AlphabetAdapter (Context context, String[] letters){
        this.mContext = context;
        this.mLetters = letters;
    }
}

@Override
public int getCount() {
   return mLetters.length;
}

@Override
public Object getItem(int position) {
   return null;
}

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

@Override
public View getView(int position, View convertView, ViewGroup parent) {
   LayoutInflater inflater = (LayoutInflater) mContext
           .getSystemService(Context.LAYOUT_INFLATER_SERVICE);

   View gridView;

   if (convertView == null) {
       // get layout from xml file
       gridView = inflater.inflate(R.layout.alphabet_grid_item, null);


           // pull views 
           TextView letterView = (TextView) gridView
                   .findViewById(R.id.grid_item_letter);

           // set values into views
           letterView.setText("A");  // using dummy data for now
       } else {
           gridView = (View) convertView;
       }
       return gridView;
   }
}


Take the time to go through this and try and understand what we are doing here, step by step.

Our getView() method contains some code and arguments we haven’t seen before:

ConvertView: If a View is a ConvertView, it is actually a View that can be converted to a new View, which means new content can get written into the fields contained in said View. What? Why would this be necessary? Well - let’s say you have a list of 150 things to display, but only 10 fit on the screen at any given time. We’re going to start our list at the very top, with the 0th item. As we scroll down, the view that was in the 0th place is now off the screen, and can now be removed from the app’s memory storage, until we start to scroll back up again. When we start our list, the convertView is null, and therefore we need to start building our list from our layout. If it’s not null, then the layout already got built, as some scrolling happened. Makes sense! ConvertView is important for your app’s performance.

LayoutInflater, Viewgroups: We’ll get more into this a little later, but for now it’s enough to know that the Views belong to a Viewgroup. This is how we know which views belong to the unit that gets repeated inside of the layout, and this Viewgroup gets inflated with data. Inflating is the process of sorting out which data goes where. Imagine a truck of groceries arriving at a supermarket. Each item in the truck belongs somewhere in the store. Someone has to determine what goes where, someone who knows what the aisles and departments look like, where the store ends, and how big it is. That person needs to issue instructions to the store’s workers, ensuring they know how to place the goods in the correct locations. Part of this process is the LayoutInflater’s job when it aids in correctly matching data to layout.

Invoking our custom adapter in MainActivity

Now all we need to do is invoke our adapter inside of our MainActivity, and we can test our app. Place this code in your onCreate(), after setContentView(R.layout.activity_main);

MainActivity.java
…

gridView = (GridView) findViewById(R.id.baseGridView);
gridView.setAdapter(new AlphabetAdapter(this, letters));


Here, we are writing the whole GridView into one variable - including the views that will end up getting nested inside it. Then we need to a.) make a local copy and b.) connect that with the GridView, feeding it the correct arguments so the constructor can run boot up an object.

Run your app - and congrats! You should see the a GridView on the screen - repeating the letter A over and over again. Now you can tweak your layout if you’d like to make any changes.

gridview-dummydata

Making our Data Dynamic

Once our dummy data is up and running, we can use position variable to retrieve Strings from the letters array - just like we did in the customized ArrayAdapter.

Head back over to your adapter, and try and see if you can implement that logic now.

When you are done, you should see something like this when your app runs:

gridview-dynamic-data-no-typeface

Good stuff!

So far so good, we have a custom adapter and a custom layout. Now, let’s add the finishing touches and learn how to apply a different TypeFace to a View that is not directly referenced in an activity’s XML file, but instead nested inside of a view.

If you take a look back at the first Custom Fonts lesson, you’ll see that we were able to run get CreateFromAssets() method, retrieve the typeface, then apply it to a view inside of our activity’s onCreate() method.

But the view that we are looking to style is not accessible in the same way here - we cannot directly reference it in the MainActivity, as the Views are part of the GridView’s custom layout. Oh no!

And if we try and apply a font to the whole GridView, or call CreateFromAssets() in the adapter, we’ll see that this won’t work either, as this method only runs inside of an Activity, not an Adapter. Dang.

But there is a solution. Since we wrote the adapter ourselves, including a new constructor - there is no reason why we can’t pass the Typeface we want to use to the Adapter as an argument to the constructor, and then apply it inside the adapter when the views are bound! This is great! We can pass all kinds of information around through constructors. This will be super useful later on.

Here’s how:

First, let’s download a font (.ttf please!) we like from FontSquirrel or DaFont.com, and place it in the assets/fonts folder as we have done previously. Refer back to the earlier lesson if you are unsure how to do this correctly.

Once that is completed, return back to your adapter and create a new member variable, called mTypeface.

AlphabetAdapter.java

private Context mContext;
private String[] mLetters;
private Typeface mTypeface;


Edit your constructor and your parameter list accordingly.

AlphabetAdapter.java
public AlphabetAdapter (Context context, String[] letters, Typeface typeface){
    this.mContext = context;
    this.mLetters = letters;
    this.mTypeface = typeface;
}

Let’s move down to where the text for the view is set, and we can now apply our new typeface.

AlphabetAdapter.java
// set values into views
letterView.setText(mLetters[position]);
letterView.setTypeface(mTypeface);

Now, let’s return back to our MainActivity, where we can now pull in our Typeface from our assets/fonts folder and pass it to our adapter as an argument to the constructor.

In our onCreate(), let’s declare and initialize a new variable of Typeface type that can hold our typeface.

Now, all we need to do is to pass this typeface to our adapter through our constructor. Run your app and see your typeface show up in your super custom layout.

MainActivity.java
   @Override
   protected void onCreate(Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);
       setContentView(R.layout.activity_main);

       Typeface typeface = Typeface.createFromAsset(getAssets(), "fonts/musicnet.ttf"); //change 

       gridView = (GridView) findViewById(R.id.baseGridView);

       gridView.setAdapter(new AlphabetAdapter(this, letters, typeface));
   }
}

gridview-final-dynamic-typeface

Awesome! While the final result may not seem all that spectacular, you successfully learned to further enhance your apps with custom adapters, custom layouts, and to pass important data to your nested views via adapters and their constructors. Nice job!