SENG 440: Lecture 11 – Resources
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:
- For an extra credit half-pakipaki, attend the Dehyping Neural Networks seminar on Friday, 29 March, from 10-11 AM in Jack Erskine 101. Write down a short response on a quarter sheet to be turned in next Tuesday.
- Read The iPhone Is Dead. Long Live the Rectangle on the maturation of mobile devices.
- On a quarter sheet of paper to be turned in at the beginning of the next class, describe your position on the themes of this article. Do you think we’ll see any more life-changing innovations with mobile computing? Why or why not? Make some predictions. Which personal technology innovations do you expect to see in your lifetime?
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 TextView
s, 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!
P.S. It’s time for a haiku!
Some write, “Hello, world”
Others write, “Holá, mundo”
I write XML