Android ListView Tutorial with Kotlin

In this tutorial, you’ll learn how to use Android’s ListView to easily create scrollable lists, by creating a simple recipe list app using Kotlin. By Joe Howard.

Leave a rating/review
Download materials
Save for later
Share

Update Note: This tutorial is now up to date with the latest version of Android Studio version 3.0.1, and uses Kotlin for app development. Update by Joe Howard. Original tutorial by Odie Edo-Osagie.

How many times have you needed an app to display a group of related items in a list? How about all the time. :]

Displaying a specific list is essential to the function of almost any app that queries a set of data and returns a list of results, so many apps need to do this at one point or another. For instance, maybe you have a chat app that queries a certain social platform’s database to find your friends, and then want to display them in a list that lets you select which friends to connect with.

Any time you need to display a lot of data and make it easy to navigate, you’ve got a job for Android’s ListView, which handily creates scrollable lists.

In recent years, ListView has been supplanted by RecyclerView. Nevertheless, studying ListView still has it’s benefits:

  • You can gain insights into why RecyclerView works the way it does
  • You may run into ListView in legacy code, and it’s best to know how to work with it

By working through this tutorial, you’ll become familiar with ListView, and you’ll do so by creating a recipe list app. Specifically, you’ll learn:

  • How to construct and populate a ListView
  • How to customize the layout
  • How to style and beautify a ListView
  • How to optimize a ListView’s performance

You’re welcome to up your game in the kitchen by learning the recipes too, but maybe wait until you’ve built the app, okay?

Note: If you’re new to Android Development or Kotlin, it’s highly recommended that you start with Beginning Android Development with Kotlin to learn your way around the basic tools and concepts.

Getting Started

To kick things off, start by downloading the materials for this tutorial (you can find a link at the top or bottom of the page) and open Android Studio 3.0.1 or greater.

In the Welcome to Android Studio dialog, select Open an existing Android Studio project.

Welcome to Android Studio

In the following dialog, select the top-level directory of the starter project AllTheRecipes-Starter and click OK.

Open Dialog

Inside the imported project, you’ll find some assets and resources that you’ll use to create your app, such as strings, colors, XML layout files, and fonts. Additionally, there’s some boilerplate code modeling a Recipe and a bare bones MainActivity class.

Build and run. You should see something like this:

First run

Are you ready to get cracking on this list thing? Awesome!

Add Your First ListView

The first order of business is to add a ListView to MainActivity.

Open res/layout/activity_main.xml. As you may know, this is the file that describes the layout of MainActivity. Add a ListView to MainActivity by inserting the following code snippet inside the ConstraintLayout tag:

<ListView
  android:id="@+id/recipe_list_view"
  android:layout_width="0dp"
  android:layout_height="0dp"
  app:layout_constraintBottom_toBottomOf="parent"
  app:layout_constraintEnd_toEndOf="parent"
  app:layout_constraintStart_toStartOf="parent"
  app:layout_constraintTop_toTopOf="parent" />

Open MainActivity and add an instance variable for your ListView with the following line:

private lateinit var listView ListView

Add the following snippet below the existing code inside the onCreate method:

listView = findViewById<ListView>(R.id.recipe_list_view)
// 1
val recipeList = Recipe.getRecipesFromFile("recipes.json", this)
// 2
val listItems = arrayOfNulls<String>(recipeList.size)
// 3
for (i in 0 until recipeList.size) {
  val recipe = recipeList[i]
  listItems[i] = recipe.title
}
// 4
val adapter = ArrayAdapter(this, android.R.layout.simple_list_item_1, listItems)
listView.adapter = adapter

Here’s a breakdown of what’s happening in there:

  1. This loads a list of Recipe objects from a JSON asset in the app. Notice that the starter project contains a Recipe class that models and stores the information about the recipes that will be displayed.
  2. This creates an array of strings that’ll contain the text to be displayed in the ListView.
  3. This populates the ListView’s data source with the titles of the recipes loaded in section one.
  4. This creates and sets a simple adapter for the ListView. The ArrayAdapter takes in the current context, a layout file specifying what each row in the list should look like, and the data that will populate the list as arguments.

Enough talk! Your ListView has all that it needs to function. Build and run the project. You should see something like this:

First ListView

Adapters: Servants of the ListView

Your recipe app is starting to look functional, but not all that appetizing…yet.

In the previous section, you successfully built a list of recipe titles. It works, but it’s nothing to get excited about. What if you needed to show more than just the titles? More than just text? Maybe even add some screen-licking worthy thumbnails?

For these cases, the simple ArrayAdapter you just used won’t cut it. You’ll have to take matters into your own hands and write your own adapter. Well, you won’t actually write your own adapter, per se; you’ll simply extend a regular adapter and make some tweaks.

What Exactly is an Adapter?

An adapter loads the information to be displayed from a data source, such as an array or database query, and creates a view for each item. Then it inserts the views into the ListView.

Adapters not only exist for ListViews, but for other kinds of views as well; ListView is a subclass of AdapterView, so you can populate it by binding it to an adapter.

Adapters

The adapter acts as the middle man between the ListView and data source, or its provider. It works kind of like this:

The ListView asks the adapter what it should display, and the adapter jumps into action:

  • It fetches the items to be displayed from the data source
  • It decides how they should be displayed
  • It passes this information on to the ListView
  • In short, The ListView isn’t very smart, but when given the right inputs it does a fine job. It fully relies on the adapter to tell it what to display and how to display it.

    Building Adapters

    Okay, now that you’ve dabbled in theory, you can get on with building your very own adapter.

    Create a new class by right-clicking on the com.raywenderlich.alltherecipes package and selecting New > Kotlin File/Class. Name it RecipeAdapter and define it with the following:

    class RecipeAdapter : BaseAdapter() {
    }
    

    You’ve made the skeleton of the adapter. It extends the BaseAdapter class, which requires several inherited methods you’ll implement after taking care of one more detail.

    Update the RecipeAdapter class as follows:

    class RecipeAdapter(private val context: Context,
                        private val dataSource: ArrayList<Recipe>) : BaseAdapter() {
      
      private val inflater: LayoutInflater
          = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
    }
    

    In here, you’ve added the properties that will be associated with the adapter and defined a primary constructor for RecipeAdapter.

    Your next step is to implement the adapter methods. Kick it off by placing the following code at the bottom of RecipeAdapter:

    //1
    override fun getCount(): Int {
      return dataSource.size
    }
    
    //2
    override fun getItem(position: Int): Any {
      return dataSource[position]
    }
    
    //3
    override fun getItemId(position: Int): Long {
      return position.toLong()
    }
    
    //4
    override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
      // Get view for row item
      val rowView = inflater.inflate(R.layout.list_item_recipe, parent, false)
        
      return rowView
    }
    

    Here’s a step-by-step breakdown:

    1. getCount() lets ListView know how many items to display, or in other words, it returns the size of your data source.
    2. getItem() returns an item to be placed in a given position from the data source, specifically, Recipe objects obtained from dataSource.
    3. This implements the getItemId() method that defines a unique ID for each row in the list. For simplicity, you just use the position of the item as its ID.
    4. Finally, getView() creates a view to be used as a row in the list. Here you define what information shows and where it sits within the ListView. You also inflate a custom view from the XML layout defined in res/layout/list_item_recipe.xml — more on this in the next section.

    Defining the Layout of the ListView’s Rows

    You probably noticed that the starter project comes with the file res/layout/list_item_recipe.xml that describes how each row in the ListView should look and be laid out.

    Below is an image that shows the layout of the row view and its elements:

    List item row

    Your task is to populate each element of the row view with the relevant recipe data, hence, you’ll define what text goes in the “title” element, the “subtitle” element and so on.

    In the getView() method, add the following code snippet just before the return statement:

    // Get title element
    val titleTextView = rowView.findViewById(R.id.recipe_list_title) as TextView
    
    // Get subtitle element
    val subtitleTextView = rowView.findViewById(R.id.recipe_list_subtitle) as TextView
    
    // Get detail element
    val detailTextView = rowView.findViewById(R.id.recipe_list_detail) as TextView
    
    // Get thumbnail element
    val thumbnailImageView = rowView.findViewById(R.id.recipe_list_thumbnail) as ImageView
    

    This obtains references to each of the elements (or subviews) of the row view, specifically the title, subtitle, detail and thumbnail.

    Now that you’ve got the references sorted out, you need to populate each element with relevant data. To do this, add the following code snippet under the previous one but before the return statement:

    // 1
    val recipe = getItem(position) as Recipe
    
    // 2
    titleTextView.text = recipe.title
    subtitleTextView.text = recipe.description
    detailTextView.text = recipe.label
    
    // 3
    Picasso.with(context).load(recipe.imageUrl).placeholder(R.mipmap.ic_launcher).into(thumbnailImageView)
    

    Here’s what you’re doing in the above snippet:

    1. Getting the corresponding recipe for the current row.
    2. Updating the row view’s text views so they are displaying the recipe.
    3. Making use of the open-source Picasso library for asynchronous image loading — it helps you download the thumbnail images on a separate thread instead of the main thread. You’re also assigning a temporary placeholder for the ImageView to handle slow loading of images.
    Note: You should never perform long-running tasks on the main thread. When you do, you expose yourself to the risk of blocking the UI, and that would make scrolling your lists a nightmare!

    Now open up MainActivity so that you can get rid of the old adapter. In onCreate, replace everything below (but not including) this line:

    val recipeList = Recipe.getRecipesFromFile("recipes.json", this)
    

    With:

    val adapter = RecipeAdapter(this, recipeList)
    listView.adapter = adapter
    

    You just replaced the rather simple ArrayAdapter with your own RecipeAdapter to make the list more informative.

    Build and run and you should see something like this:

    Using recipe adapter

    Now you’re cooking for real! Look at those recipes — thumbnails and descriptions sure make a big difference.

    Styling

    Now that you’ve got the functionality under wraps, it’s time to turn your attention to the finer things in life. In this case, your finer things are elements that make your app more snazzy, such as compelling colors and fancy fonts.

    Start with the fonts. Look for some custom fonts under res/font. You’ll find three font files: josefinsans_bold.ttf, josefinsans_semibolditalic.ttf and quicksand_bold.otf.

    Open RecipeAdapter.java and go to the getView() method. Just before the return statement, add the following:

    val titleTypeFace = ResourcesCompat.getFont(context, R.font.josefinsans_bold)
    titleTextView.typeface = titleTypeFace
        
    val subtitleTypeFace = ResourcesCompat.getFont(context, R.font.josefinsans_semibolditalic)
    subtitleTextView.typeface = subtitleTypeFace
    
    val detailTypeFace = ResourcesCompat.getFont(context, R.font.quicksand_bold)
    detailTextView.typeface = detailTypeFace
    

    In here, you’re assigning a custom font to each of the text views in your rows’ layout. You access the font by creating a Typeface, which specifies the intrinsic style and typeface of the font, by using ResourcesCompat.getFont(). Next you set the typeface for the corresponding TextView to set the custom font.

    Now build and run. Your result should look like this:

    Custom fonts

    On to sprucing up the colors, which are defined in res/values/colors.xml. Open up RecipeAdapter and add the following below the inflater declaration:

    companion object {
      private val LABEL_COLORS = hashMapOf(
          "Low-Carb" to R.color.colorLowCarb,
          "Low-Fat" to R.color.colorLowFat,
          "Low-Sodium" to R.color.colorLowSodium,
          "Medium-Carb" to R.color.colorMediumCarb,
          "Vegetarian" to R.color.colorVegetarian,
          "Balanced" to R.color.colorBalanced
      ) 
    }
    

    You’ve created a hash map that pairs a recipe detail label with the resource id of a color defined in colors.xml.

    Now go to the getView() method, and add this line just above the return statement:

    detailTextView.setTextColor(
        ContextCompat.getColor(context, LABEL_COLORS[recipe.label] ?: R.color.colorPrimary))
    

    Working from the inside out:

    • Here you get the resource id for the color that corresponds to the recipe.label from the LABEL_COLORS hash map.
    • getColor() is used inside of ContextCompat to retrieve the hex color associated with that resource id.
    • Then you set the color property of the detailTextView to the hex color.

    Build and run. Your app should look like this:

    Adding label colors

    User Interaction

    Now your list has function and style. What’s it missing now? Try tapping or long pressing it. There’s not much to thrill and delight the user.

    What could you add here to make the user experience that much more satisfying? Well, when a user taps on a row, don’t you think it’d be nice to show the full recipe, complete with instructions?

    You’ll make use of AdapterView.onItemClickListener and a brand spanking new activity to do this with elegance.

    Make a New Activity

    This activity will display when the user selects an item in the list.

    Right-click on com.raywenderlich.alltherecipes then select New > Activity > EmptyActivity to bring up a dialog. Fill in the Activity Name with RecipeDetailActivity. Leave the automatically populated fields as-is. Check that your settings match these:

    New Activity dialog

    Click Finish.

    Open res/layout/activity_recipe_detail.xml and add a WebView by inserting the following snippet inside the ConstraintLayout tag:

    <WebView
      android:id="@+id/detail_web_view"
      android:layout_width="0dp"
      android:layout_height="0dp"
      app:layout_constraintBottom_toBottomOf="parent"
      app:layout_constraintEnd_toEndOf="parent"
      app:layout_constraintStart_toStartOf="parent"
      app:layout_constraintTop_toTopOf="parent" />
    

    WebView will be used to load and display a webpage containing the selected recipe’s instructions.

    Open up RecipeDetailActivity, and add a WebView reference as a property by adding the following line within the class definition:

    private lateinit var webView: WebView
    

    Add the following below the webView property declaration:

    companion object {
      const val EXTRA_TITLE = "title"
      const val EXTRA_URL = "url"
    
      fun newIntent(context: Context, recipe: Recipe): Intent {
        val detailIntent = Intent(context, RecipeDetailActivity::class.java)
    
        detailIntent.putExtra(EXTRA_TITLE, recipe.title)
        detailIntent.putExtra(EXTRA_URL, recipe.instructionUrl)
          
        return detailIntent
      }
    }
    

    This adds a companion object method to return an Intent for starting the detail activity, and sets up title and url extras in the Intent.

    Head back to MainActivity and add the following to the bottom of the onCreate method:

    val context = this
    listView.setOnItemClickListener { _, _, position, _ ->
      // 1
      val selectedRecipe = recipeList[position]
    
      // 2
      val detailIntent = RecipeDetailActivity.newIntent(context, selectedRecipe)
    
      // 3
      startActivity(detailIntent)
    }
    

    Note: Before you dive into the explanation, make sure you understand the four arguments that are provided by onItemClick; they work as follows:

    • parent: The view where the selection happens — in your case, it’s the ListView
    • view: The selected view (row) within the ListView
    • position: The position of the row in the adapter
    • id: The row id of the selected item

    You’re setting the OnItemClickListener object for the ListView, and inside doing the following:

    1. Get the recipe object for the row that was clicked
    2. Create an intent to navigate to your RecipeDetailActivity to display more information
    3. Launch the RecipeDetailActivity by passing the intent object you just created to the startActivity() method.
    Note: To learn more about intents, check out the awesome Android Intents Tutorial.

    Once again, open RecipeDetailActivity and add the following snippet at the bottom of the onCreate method:

    // 1
    val title = intent.extras.getString(EXTRA_TITLE)
    val url = intent.extras.getString(EXTRA_URL)
    
    // 2
    setTitle(title)
    
    // 3
    webView = findViewById(R.id.detail_web_view)
    
    // 4
    webView.loadUrl(url)
    

    You can see a few things happening here:

    1. You retrieve the recipe data from the Intent passed from MainActivity by using the extras property.
    2. You set the title on the action bar of this activity to the recipe title.
    3. You initialize webView to the web view defined in the XML layout.
    4. You load the recipe web page by calling loadUrl() with the corresponding recipe’s URL on the web view object.

    Build and run. When you click on the first item in the list, you should see something like this:

    Recipe detail

    Optimizing Performance

    Whenever you scroll the ListView, its adapter’s getView() method is called in order to create a row and display it on screen.

    Now, if you look in your getView() method, you’ll notice that each time this method is called, it performs a lookup for each of the row view’s elements by using a call to the findViewById() method.

    These repeated calls can seriously harm the ListView’s performance, especially if your app is running on limited resources and/or you have a very large list. You can avoid this problem by using the View Holder Pattern.

    Implement a ViewHolder Pattern

    To implement the ViewHolder pattern, open RecipeAdapter and add the following after the getView() method definition:

    private class ViewHolder {
      lateinit var titleTextView: TextView
      lateinit var subtitleTextView: TextView
      lateinit var detailTextView: TextView
      lateinit var thumbnailImageView: ImageView
    }
    

    As you can see, you create a class to hold your exact set of component views for each row view. The ViewHolder class stores each of the row’s subviews, and in turn is stored inside the tag field of the layout.

    This means you can immediately access the row’s subviews without the need to look them up repeatedly.

    Now, in getView(), replace everything above (but NOT including) this line:

    val recipe = getItem(position) as Recipe
    

    With:

    val view: View
    val holder: ViewHolder
    
    // 1
    if (convertView == null) {
    
      // 2
      view = inflater.inflate(R.layout.list_item_recipe, parent, false)
    
      // 3
      holder = ViewHolder()
      holder.thumbnailImageView = view.findViewById(R.id.recipe_list_thumbnail) as ImageView
      holder.titleTextView = view.findViewById(R.id.recipe_list_title) as TextView
      holder.subtitleTextView = view.findViewById(R.id.recipe_list_subtitle) as TextView
      holder.detailTextView = view.findViewById(R.id.recipe_list_detail) as TextView
    
      // 4
      view.tag = holder
    } else {
      // 5
      view = convertView
      holder = convertView.tag as ViewHolder
    }
    
    // 6
    val titleTextView = holder.titleTextView
    val subtitleTextView = holder.subtitleTextView
    val detailTextView = holder.detailTextView
    val thumbnailImageView = holder.thumbnailImageView
    

    Here’s the play-by-play of what’s happening above.

    1. Check if the view already exists. If it does, there’s no need to inflate from the layout and call findViewById() again.
    2. If the view doesn’t exist, you inflate the custom row layout from your XML.
    3. Create a new ViewHolder with subviews initialized by using findViewById().
    4. Hang onto this holder for future recycling by using setTag() to set the tag property of the view that the holder belongs to.
    5. Skip all the expensive inflation steps and just get the holder you already made.
    6. Get relevant subviews of the row view.

    Finally, update the return statement of getView() with the line below.

    return view
    

    Build and run. If your app was running a bit slow on the last build, you should see it running smoother now. :]

    Where to Go From Here?

    You can download the completed project using the download button at the top or bottom of this tutorial.

    When you develop for Android, AdapterViews are a common concept that you’ll run into over and over again.

    If you want to know more about the inner workings of the ListView and performance details, check out this article on performance tips for Android ListViews.

    There are other ways to create lists, such as subclassing a ListActivity and ListFragment. Both of these links take you to the official Android developer site so that you can learn more about how they work.

    Both of these alternatives impose the restriction that the respective activity or fragment can only contain a ListView as its child view. Suppose you wanted an activity that had a ListView as well as some other views, it would be impossible with a ListActivity. The same goes for the ListFragment scenario.

    And be sure to check out our RecyclerView and Intermediate RecyclerView tutorials to see the more modern way to show lists on Android. Unlike ListView, RecyclerView enforces the use of the ViewHolder pattern and is much more flexible in terms of layout and animation.

    Feel free to share your feedback, findings or ask any questions in the comments below or in the forums. Talk to you soon!