teaching machines

SENG 440: Lecture 11 – Resources

March 21, 2019 by . Filed under lectures, semester1-2019, seng440.

Dear students,

Today we wrap up our Lately app by presenting its master+detail UI on tablets as a single screen, showing both the list of headlines and the selected article. We’ll also add some labels to the UI to explain the widgets. Both of these tasks will accomplished with the help of Android’s resource system.

Before we forget, here’s your TODO for next time:

Let’s finish up Lately.

Resources

Resources is a term that has been used for decades to refer to non-code files that ship with our programs. They generally include images, sounds, fonts, and dumps of static text. When I was a kid, I would tinker with a program called ResEdit to tweak a Mac program’s resources. It’s a lot easier to hack static content than machine code. On Android, resources also include layouts, animation specifications, styling information, and various configuration.

In an Android project’s source, the resources live in the res folder, with specialized subfolders for the various types of resources, including drawable for images, layout for UI hierarchies, menu for lists of menu items, values for string constants, and raw for miscellaneous items like videos, to name a few. But the notion of a neatly organized file system for the resources is erased when the project is built. The Android Asset Packaging Tool (aapt) will compile the resources down into a compressed archive. To refer to resources within a running (and therefore built) app, we use R.type.id instead of a path.

For the most part, the behavior of aapt is not terribly important. However, you should be aware the compilation phase may resize your images. If this is not what you want, place them in drawable-nodpi. Also, if the hierarchical file system-like organization of your resources is important, then you should use the assets directory instead of res. I used assets for a name-learning app that I wrote several years ago. Each course that I teach gets its own directory, and the student photos get dropped in, with each image file named according to the student. The AssetManager class lets me dynamically query for a listing of all the course directories and all the students in the course. If I dropped them in res instead, I’d have to hardcode the IDs to each of the images in the code, which would be unsustainable.

Let’s add some labels to our UI that use string resources instead of hardcoded text. In res/strings.xml, we place the following elements:

<string name="sourcesText">Sources</string>
<string name="headlinesText">Headlines</string>

In the UI, instead of hardcoding the text of our TextViews, we point to the string resources:

<TextView
  android:id="@+id/sourcesLabel"
  android:text="@string/sourcesText" />
<TextView
  android:id="@+id/headlinesLabel"
  android:text="@string/headlinesText" />

At present, there’s not a lot of justification for factoring these strings out to a resource. Let’s go find some justification.

Configuration-sensitive Resources

Suppose we’re gearing up to sell our app in another country. If that country doesn’t speak English, our users there won’t understand our strings. We’d like to provide translations in a sustainable manner—which means without conditional statements.

Good news. The resource system can make choosing the correct translation automatic. We simply provide a translated version of the strings in a resource directory qualified by the device’s current configuration. To provide Spanish translates, we create the file values-es/strings.xml with the following contents:

<resources>
  <string name="sources_label">Fuentes</string>
  <string name="headlines_label">Titulares</string>
</resources>

To force our app to draw the strings from this file, we do nothing within our app itself. Instead, we change the language in the Settings app. This forces a configuration change. Our app restarts, but this time the resource manager finds that values-es is a better match for our strings, and it draws from that directory instead. If no resource for a particular configuration is found, it falls back on the default.

Tablet Layout

We’ll use this same configuration-sensitive resource system to achieve our goal of getting a master+detail layout on tablets. We create a new layout for the main activity that is only used on large screens in landscape mode, which we accomplish by placing this layout in res/layout-large-land. The view shows the headlines list on the left and a placeholder FrameLayout on the right:

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:app="http://schemas.android.com/apk/res-auto"
  xmlns:tools="http://schemas.android.com/tools"
  android:layout_width="match_parent"
  android:layout_height="match_parent">

  <fragment xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/headlinesFragment"
    android:name="org.twodee.lately.HeadlinesFragment"
    android:layout_width="0dp"
    android:layout_height="match_parent"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintEnd_toStartOf="@+id/guideline"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent"
    app:layout_constraintVertical_bias="1.0" />

  <android.support.constraint.Guideline
    android:id="@+id/guideline"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:orientation="vertical"
    app:layout_constraintGuide_percent="0.25" />

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

</android.support.constraint.ConstraintLayout>

When MainActivity starts up, the resource manager will automatically choose the layout that matches the current device configuration.

Handling Clicks

Currently, when the user clicks on a headline, we force a new browser activity to appear. That’s only appropriate for phones. If we’re on a tablet, we don’t ever want a browser activity to appear. We’ve got to fix this, and we’ll start by moving the click handling code out of the headlines fragment. We can use our UrlKeeper interface to push the responsiblity of acting on a click to the activity:

var headlines: List<Headline> = listOf()
  set(value) {
    field = value
    headlinesPicker.adapter = HeadlineAdapter(context!!, field) {
      (activity as UrlKeeper).url = it.url
    }
  }

Our main activity, which owns the headline fragment on both phone and tablet, will respond in its setter for url:

class MainActivity : FragmentActivity(), UrlKeeper {
  ...

  override var url: String = ""
    set(value) {
      field = value
      val intent = Intent(this, BrowserActivity::class.java)
      intent.putExtra("url", url)
      startActivity(intent)
    }
}

This is still a phone-only solution, as it creates a new activity. But we have at least pushed the code up to the party that is informed enough to decide what should happen when a headline is clicked on. The main activity must check if it’s in a tablet context. One way to do so is to query the view that’s loaded. Let’s see if it has FrameLayout placeholder:

private var fragmentContainer: FrameLayout? = null

override fun onCreate(savedInstanceState: Bundle?) {
  super.onCreate(savedInstanceState)
  setContentView(R.layout.activity_main)
  fragmentContainer = findViewById(R.id.fragmentContainer)
}

Then we can respond accordingly in the setter. If there is no placeholder, we’re on a phone and fire off an intent. Otherwise, we load the browser fragment.

override var url: String = ""
  set(value) {
    field = value
    if (fragmentContainer == null) {
      val intent = Intent(this, BrowserActivity::class.java)
      intent.putExtra("url", url)
      startActivity(intent)
    } else {
      // load fragment dynamically
    }
  }

To load the fragment dynamically, we submit a transaction to the fragment manager:

val transaction = supportFragmentManager.beginTransaction()
val fragment = BrowserFragment()
transaction.replace(R.id.fragmentContainer, fragment)
transaction.commit()

We called replace, but we can also add or remove fragments dynamically. We can also make fragments fade in and out:

transaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE)

By default, the new fragment is considered a minor tweak to the existing UI. But if your fragments are fullscreen, you want to count them as a page in your app’s navigation history, which will allow the back button to return to whatever previous fragment we were on. We call addToBackStack to enable this:

transaction.addToBackStack(null)

Note that adding a backstack entry is probably not appropriate in Lately. It leads to stale selection in the headlines list.

All told, our code dealing with transaction looks like this:

transaction.replace(R.id.fragmentContainer, fragment)
transaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE)
transaction.addToBackStack(null)
transaction.commit()

Any time we have a sequence of method invocations on an object in Kotlin, we can tidy things up with the apply method, which lets us execute a block with the object as its scope:

transaction.apply {
  replace(R.id.fragmentContainer, fragment)
  setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE)
  addToBackStack(null)
  commit()
}

I really, really like apply.

Wel, we’ve done it. Our master+detail UI is achieved in two different ways depending on the screen size, and the only conditional statement is in our click handler.

That’s enough for today. See you next time!

Sincerely,

P.S. It’s time for a haiku!

Some write, “Hello, world”
Others write, “Holá, mundo”
I write XML