Learning Kotlin: Simple ListView Adapter in Anko

Most applications- web or otherwise- deal with extracting data from a model and putting it into a view of some kind. The bare minimum for any application developer to understand about any framework is how they can take an arbitrary list of items and display them to a user.

Single Page Application frameworks often have concepts called "repeaters". There are other ways to access data from a model and display it to a user, but most commonly, the core of the feature- or even the first iteration of the feature- is going to be a curated data table or list of some kind.

Android applications are slightly more complicated: there are ListViews, and TableViews, and various other means of converting data into a drawn screen. Additionally, because you're running on an embedded device, the Android SDK demands abstraction in order to limit the consumption of resources. What follows is the abstract class, and the necessary overrides, for an Adapter

BaseAdapter()  
- getItem(index)
- getItemId(index) : Long
- getCount() : Int
- getView(index, recycledview, parent) : View

Essentially, we're writing the glue between our data and how we want our data to be displayed. For a web developer, this can be likened unto the guts of a repeater: we're saying how we want to draw each item in a list.

Anko does a lot of great things for developers, but it's lacking in shorthand for an adapter. Part of this is because it risks shorthanding important design and optimization choices:

  • Do you want to access the data cursor directly, or dump the results of a query to an in-memory list object? The answer will depend on the type of data you're accessing and the demands of your user interaction.

  • How complex is your view? Do you make certain decisions based on different data that comes up? You may need to expand your Adapter to handle this.

  • The recycledview parameter in BaseAdapter() is provided specifically so the developer can optimize view code by reusing old Views and simply redefining their parameters, instead of rebuilding the entire view from scratch. This business logic can be very complex and specific to the particular implementation.

Thus, I'm providing a simple example in Kotlin and Anko involving building an Adapter for use in a database-querying Android application.

Step 1: Prerequisites

Construct a basic UI/Activity pair following my previous article. This involves defining a standard FooActivity, a FooActivityUI that extends AnkoComponent<FooActivity>. If you've already done this, you're ahead of the game.

If you need to set up Anko, follow the Anko README. TL;DR - Get Android Studio, add the magic lines to your app.gradle (including the anko-sqlite libraries).

Additionally, set up your database context as per the Anko SQLite Documentation. Obviously, you can change MyDatabase in all its forms to fit your particular needs. The only thing that's absolutely necessary is that your application's Context needs to have the database parameter defined as your Anko SQLite helper.

Step 2: Make Your Activity Fetch Your Data

Since your Activity is the only place you can use your database context without going out of your way to shuffle it off to other classes, we can treat the Activity class like our controller and give it a fetch function for our data:

class MainActivity : Activity() {  
  // ...
  fun getDataFromProvider() : List<Map<String, Any>> {
    database.use {
      // Remember onCreate in the Anko SQLite docs?
      // Your Table Name Here
      select("myTable").exec() {
        parseList(
          object : MapRowParser<Map<String, Any>> {
            override fun parseRow(columns : Map<String, Any>) : Map<String, Any> {
              return columns
            }
          }
        )
      }
    }
  }
}

Woah! That's a lot of stuff! What does it all mean?

First, we're calling database.use { }: That is an inline function that opens and closes the connection to the database for us.

Second, when our connection is open, we're calling select("myTable").exec(), which builds and executes the query SELECT * FROM myTable. More details about using where and other SQL clauses are available in the Anko SQLite docs.

Third, in our exec() { ... } block, now we have an open Cursor object. We can apply the Parse Query Result portion of the Anko docs to convert it to a List of Map objects. Here, to keep it simple, we define a simple pass-through "parser" that doesn't actually do any legwork, and just returns the map of columns.

Step 3: The Adapter Class

The adapter class is going to be the next thing you'll want to implement:

class MyAdapter(val activity : MainActivity) : BaseAdapter() {  
  var list : List<Map<String, Any>> = activity.getDataFromProvider()

  override fun getView(i : Int, v : View?, parent : ViewGroup?) : View {
    val item = getItem(i)
    return with(parent!!.context) {
      relativeLayout {
        textView(item["student_name"]) {
          textSize = 32f
        }
        textView(item["student_age"]) {
          textSize = 16f
        }.lparams { 
          alignParentBottom()
          alignParentRight() 
        }
      }
    }
  }

  override fun getItem(position : Int) : Map<String, Any> {
    return list.get(position)
  }

  override fun getCount() : Int {
    return list.size
  }

  override fun getItemId(position : Int) : Long {
    return getItem(position).get("_id") as Long;
  }

}

There are a few things going on here:

  • Our getItem, getCount, and getItemId functions are as simple as possible. We correlate position to actual position in the list, we return the length of the list for the count, and we return the _id SQL column when we want the item's ID. Honestly, there are very few instances where this isn't what you want, so you could almost use this as boilerplate.

  • Assuming we have a table of student_name and student_age, we construct a RelativeLayout containing their names and ages.

Step 4: Baking the Adapter into our UI

In our view class, we create a small shell and a listView to hold our information:

class MainActivityUI : AnkoComponent<MainActivity> {  
  var mAdapter : ReminderAdapter? = null;

  override fun createView(ui : AnkoContext<MainActivity>) = with(ui) {
    mAdapter = MyAdapter(owner)
    verticalLayout {
      relativeLayout {
        textView("Students").lparams {
          centerHorizontally()
        }
      }
      listView {
        adapter = mAdapter
      }
    }
  }
}

Which produces a layout like so (note: the floating action button is created using a different set of Anko view logic):

screenshot of finished demo application

Great! What else do we need to do? Well, this is great for generating a view once, but you'll want your adapter to be able to respond to changes:

Step 5: Responding to Changes

The first thing we'll want to do is create a custom function in our adapter class which will refresh the information for us:

class MyAdapter(val activity : MainActivity) : BaseAdapter() {  
  var list : List<Map<String, Any>> = activity.getDataFromProvider()
  // ...
  fun rebuild() {
    list = activity.getDataFromProvider()
    notifyDataSetChanged()
  }
}

Wow! It turns out that implementing that getDataFromProvider() function in our Activity was a really good idea, because now we can call it to get fresh changes. Could this get more complicated? Absolutely. We could be getting a stream of changes from our data provider instead, and we'd have to do a lot more complicated things in our Adapter in order to keep our Model up to date.

Note that notifyDataSetChanged() is the preferred way to mark your ListView in need of refreshing. It's a very common answer to a common antipattern of recreating an Adapter with fresh information.

What if we want to provide a hook to delete an item? A strict model-view-controller separation says that we should probably do this:

// in MainActivity
fun deleteWithId(id: Long) {  
  datbase.use {
    delete("myTable", """_id = $id""")
  }
}

Then, in our Adapter:

// in MyAdapter
fun deleteItem(index : Int) {  
  activity.deleteWithId(
    getItemId(index)
  )
  rebuild()
}

Aha! Our rebuild method comes in useful here! Note that we could achieve the same result without having to do another database query by just removing the item from our list and marking notifyDataSetChanged(). However, if we have a Service that's updating the database as well, then we'd need to rebuild from the source of truth anyway.

Now we can hook up our deleteItem(index) to our view:

// in MainActivityUI#createView
// ...

listView {  
  adapter = mAdapter

  onItemLongClick { adapterView, view, i, l ->
    alert {
      title("Delete")
      message("Do you want to delete this student?")
      positiveButton {
        mAdapter!!.deleteItem(i)
      }
      negativeButton {  }
    }.show()
    true
  }
}

When we long click an item, i passes the index of the long-clicked item to our handler, which creates a confirmation dialog and calls our deleteItem(index) method in the adapter. This automatically marks our adapter as dirty, so our view updates automatically.

Stay tuned for more updates!

How to Cite Republished Information

Do not use this information as legal advice.
If you need advice, call or email a licensed attorney.
No attorney-client relationship is created by your use of this site, including any communication with the author in comments.