teaching machines

SENG 440: Lecture 10 – Master + Detail via Fragments

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

Dear students,

Today we extend our Lately app by applying a master+detail UI to it. It will show both a list of headlines and a selected article. One phones, these two views will appear as separate screens, but on a tablet, they will both appear on the same screen. We will use Android’s Fragment class to compose our UI out of independent pieces. We’ll only handle the phone portion today and save tablets for next time.

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

Let’s continue making our app.

Master+detail

The master+detail UI pattern is one we see in email clients, in slide design software, and in multipage document viewers. Some portion of the screen shows a master list of titles or thumbnails. When the user clicks on an entry, the item is expanded in detail on the rest of the screen.

We want our Lately app to employ this master+detail pattern. The headlines will be listed in the master view, and an individual article will appear in the detail view. On the phone, the two views will be in separate activities, but on a tablet, both views will be on the screen at the same time.

Fragment-ation

In the early days of Android, screens were small. But when tablets appeared, we suddenly had a lot more space to fill. One approach to provide a larger UI was to specify both small and large versions of an activity’s layout, and rely on the Android resource manager to load up the appropriate version in onCreate. This is a workable solution, but it leads to some awkwardness. In particular, the activity must include a lot of conditional logic to arbitrate between its behavior on small screens and its behavior on large screens.

The Android folks provided a solution. Instead of the Activity being a manager for the whole screen, they allowed us to create a mini-manager for some portion of the screen. A mini-manager is called a Fragment.

I was hoping I could avoid discussing fragments in lecture. Their complexity is daunting and doesn’t make for an intellectually stimulating presentation. However, they are hard to avoid in modern Android development. Indeed, it seems that a lot of Android development is easier if your app is a single activity that shows its many screens using fragments. So, we will examine them today. Not in detail, but in master+detail. We will use fragments to implement a master+detail UI.

These are the rough steps we’ll follow:

Let’s get fragmenting.

BrowserFragment

Previously we used a implicit intent to show our news articles. But we don’t really want a full blown web browser, just an article reader. So, we’ll use a WebView to a render a page within our app. Here’s the XML:

<?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"
  tools:context=".BrowserFragment">

  <WebView
    android:id="@+id/webView"
    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>

</android.support.constraint.ConstraintLayout>

Who will manage this UI? Not a full-blown activity, but a Fragment, a self-contained and reusable sub-activity. We create one like this:

class BrowserFragment : Fragment() {
  private lateinit var webView: WebView

  override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
    val view = inflater.inflate(R.layout.fragment_browser, container, false)

    webView = view.findViewById(R.id.webView)
    webView.loadUrl("https://www.google.com")

    return view
  }
}

The fragment feels much like an activity with some types and names different. But we will be able to embed this fragment in several contexts.

However, a stock WebView is a very disappointing thing. If the page you are loading incurs any redirects, those redirects will open in an external browser. We fix this by giving the view a WebViewClient:

webView.webViewClient = WebViewClient()

If you search for this issue on StackOverflow, you will see code like this suggested in many answers:

private class HelloWebViewClient extends WebViewClient {
  @Override
  public boolean shouldOverrideUrlLoading(WebView view, String url) {
    view.loadUrl(url);
    return true;
  }
}

This is a bad suggestion. A method named shouldOverrideUrlLoading should not be triggering a side effect like opening a web browser. Either the method should only return false, or we should just let the default implementation of WebViewClient do the job.

We may also want to enable Javascript for our pages to load properly:

webView.settings.javaScriptEnabled = true

Finally, we’ll find that some pages result in an error about cleartext traffic. In Marshmallow, the Android devs added the ability to block unencrypted requests sent over the network—like HTTP requests. The setting defaulted to allow cleartext traffic. In Pie, the default was changed to block cleartext traffic. We override this default via the usesCleartextTraffic attribute in the application element in the manifest:

<application
  android:usesCleartextTraffic="true">

This is a good reminder that you can’t really make an app once and expect it to work for very long. We must keep up with the developments of the systems atop which we build.

BrowserActivity

In the phone context, we still need a dedicated activity to show this fragment. It gets its UI entirely from the fragment:

<?xml version="1.0" encoding="utf-8"?>
<fragment xmlns:android="http://schemas.android.com/apk/res/android"
  android:id="@+id/browserFragment"
  android:name="org.twodee.lately.BrowserFragment"
  android:layout_width="match_parent"
  android:layout_height="match_parent" />

Referencing the fragment in a layout is one way to load it. Let’s call this approach static loading. Later on we’ll look at a way to dynamically load fragments.

The activity class itself does nothing interesting beyond extend FragmentActivity:

class BrowserActivity : FragmentActivity() {
  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_browser)
  }
}

Now, we trigger our new activity-with-a-fragment back in MainActivity instead of the external browser:

val intent = Intent(this, BrowserActivity::class.java)
intent.putExtra("url", it.url)
startActivity(intent)

Fragment Calls Activity?

We have a situation. Our fragment calls a loadUrl method on its WebView. But that URL is known in the activity, not in the fragment. Fragments are meant to be reusable by many activities, so we want to avoid making too many assumptions about the context. Communicating the other direction is generally safer; an activity knows about its fragments. We could ask the activity to push the URL down to the fragment. However, today we will have the fragment pull the information down from the activity. How? Through an abstraction.

The only behavior that our fragment needs from its context is a URL, so let’s create an interface that imposes a url property on its implementers:

interface UrlKeeper {
  var url: String
}

We’ll have the browser activity also implement this interface:

class BrowserActivity : FragmentActivity(), UrlKeeper {
  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    url = intent.getStringExtra("url")
    setContentView(R.layout.activity_browser)
  }

  override lateinit var url: String
}

The browser fragment can now grab the URL from its hosting activity. We tweak the loading in onCreateView:

val keeper = activity as UrlKeeper
webView.loadUrl(keeper.url)

The browser fragment is not tightly coupled to BrowserActivity. Whew.

HeadlinesFragment

The primary use case of fragments is to decouple sections of the interface from an activity. So, just as we created a mini-manager for the browser, we’ll recreate a mini-manager for the headlines picker. We’ll first move all the UI into layout/fragment_headlines.xml:

<?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"
  tools:context=".MainActivity">

  <Spinner
    android:id="@+id/sourcesPicker"
    android:layout_width="0dp"
    android:layout_height="wrap_content"
    android:layout_marginStart="8dp"
    android:layout_marginTop="8dp"
    android:layout_marginEnd="8dp"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent" />

  <android.support.v7.widget.RecyclerView
    android:id="@+id/headlinesPicker"
    android:layout_width="0dp"
    android:layout_height="0dp"
    android:layout_marginTop="8dp"
    android:layout_marginBottom="8dp"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toBottomOf="@+id/sourcesPicker" />
</android.support.constraint.ConstraintLayout>

Similarly, we move all of our code from our MainActivity into HeadlinesFragment. A fragment is not a Context, but it has a context property. We have to make some minor adjustments:

class HeadlinesFragment : Fragment() {
  private lateinit var headlinesPicker: RecyclerView

  override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
    val view = inflater.inflate(R.layout.fragment_headlines, container, false)

    headlinesPicker = view.findViewById(R.id.headlinesPicker)
    val layoutManager = LinearLayoutManager(context)
    headlinesPicker.layoutManager = layoutManager
    val decoration = DividerItemDecoration(context, layoutManager.orientation)
    headlinesPicker.addItemDecoration(decoration)

    val parameters = mapOf("source" to "bbc-news", "apiKey" to KEY)
    val url = parameterizeUrl("https://newsapi.org/v2/top-headlines", parameters)
    HeadlinesDownloader(this).execute(url)

    return view
  }

  var headlines: List<Headline> = listOf()
    set(value) {
      field = value
      headlinesPicker.adapter = HeadlineAdapter(context!!, field) {
        val intent = Intent(context, BrowserActivity::class.java)
        intent.putExtra("url", it.url)
        startActivity(intent)
      }
    }
}

Our MainActivity gets a new UI that statically loads the fragment:

<?xml version="1.0" encoding="utf-8"?>
<fragment xmlns:android="http://schemas.android.com/apk/res/android"
  android:name="org.twodee.lately.HeadlinesFragment"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  android:id="@+id/headlinesFragment"/>

We switch its superclass to FragmentActivity.

Now we have a decoupled list that works well enough on our phone. But ugh, that was a lot of work to decouple the independent sections of our UI. We’ll justify this headache by reusing these subactivities into one activity on the tablet.

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

Sincerely,

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

One king or many?
My father-in-law says one
One per watershed