Deprecated: Creation of dynamic property wpdb::$categories is deprecated in /var/www/twodee/blog/wp-includes/wp-db.php on line 668

Deprecated: Creation of dynamic property wpdb::$post2cat is deprecated in /var/www/twodee/blog/wp-includes/wp-db.php on line 668

Deprecated: Creation of dynamic property wpdb::$link2cat is deprecated in /var/www/twodee/blog/wp-includes/wp-db.php on line 668

Deprecated: Return type of WP_Theme::offsetExists($offset) should either be compatible with ArrayAccess::offsetExists(mixed $offset): bool, or the #[\ReturnTypeWillChange] attribute should be used to temporarily suppress the notice in /var/www/twodee/blog/wp-includes/class-wp-theme.php on line 554

Deprecated: Return type of WP_Theme::offsetGet($offset) should either be compatible with ArrayAccess::offsetGet(mixed $offset): mixed, or the #[\ReturnTypeWillChange] attribute should be used to temporarily suppress the notice in /var/www/twodee/blog/wp-includes/class-wp-theme.php on line 595

Deprecated: Return type of WP_Theme::offsetSet($offset, $value) should either be compatible with ArrayAccess::offsetSet(mixed $offset, mixed $value): void, or the #[\ReturnTypeWillChange] attribute should be used to temporarily suppress the notice in /var/www/twodee/blog/wp-includes/class-wp-theme.php on line 535

Deprecated: Return type of WP_Theme::offsetUnset($offset) should either be compatible with ArrayAccess::offsetUnset(mixed $offset): void, or the #[\ReturnTypeWillChange] attribute should be used to temporarily suppress the notice in /var/www/twodee/blog/wp-includes/class-wp-theme.php on line 544

Deprecated: Return type of WP_REST_Request::offsetExists($offset) should either be compatible with ArrayAccess::offsetExists(mixed $offset): bool, or the #[\ReturnTypeWillChange] attribute should be used to temporarily suppress the notice in /var/www/twodee/blog/wp-includes/rest-api/class-wp-rest-request.php on line 960

Deprecated: Return type of WP_REST_Request::offsetGet($offset) should either be compatible with ArrayAccess::offsetGet(mixed $offset): mixed, or the #[\ReturnTypeWillChange] attribute should be used to temporarily suppress the notice in /var/www/twodee/blog/wp-includes/rest-api/class-wp-rest-request.php on line 980

Deprecated: Return type of WP_REST_Request::offsetSet($offset, $value) should either be compatible with ArrayAccess::offsetSet(mixed $offset, mixed $value): void, or the #[\ReturnTypeWillChange] attribute should be used to temporarily suppress the notice in /var/www/twodee/blog/wp-includes/rest-api/class-wp-rest-request.php on line 992

Deprecated: Return type of WP_REST_Request::offsetUnset($offset) should either be compatible with ArrayAccess::offsetUnset(mixed $offset): void, or the #[\ReturnTypeWillChange] attribute should be used to temporarily suppress the notice in /var/www/twodee/blog/wp-includes/rest-api/class-wp-rest-request.php on line 1003

Deprecated: Return type of WP_Block_List::current() should either be compatible with Iterator::current(): mixed, or the #[\ReturnTypeWillChange] attribute should be used to temporarily suppress the notice in /var/www/twodee/blog/wp-includes/class-wp-block-list.php on line 151

Deprecated: Return type of WP_Block_List::next() should either be compatible with Iterator::next(): void, or the #[\ReturnTypeWillChange] attribute should be used to temporarily suppress the notice in /var/www/twodee/blog/wp-includes/class-wp-block-list.php on line 175

Deprecated: Return type of WP_Block_List::key() should either be compatible with Iterator::key(): mixed, or the #[\ReturnTypeWillChange] attribute should be used to temporarily suppress the notice in /var/www/twodee/blog/wp-includes/class-wp-block-list.php on line 164

Deprecated: Return type of WP_Block_List::valid() should either be compatible with Iterator::valid(): bool, or the #[\ReturnTypeWillChange] attribute should be used to temporarily suppress the notice in /var/www/twodee/blog/wp-includes/class-wp-block-list.php on line 186

Deprecated: Return type of WP_Block_List::rewind() should either be compatible with Iterator::rewind(): void, or the #[\ReturnTypeWillChange] attribute should be used to temporarily suppress the notice in /var/www/twodee/blog/wp-includes/class-wp-block-list.php on line 138

Deprecated: Return type of WP_Block_List::offsetExists($index) should either be compatible with ArrayAccess::offsetExists(mixed $offset): bool, or the #[\ReturnTypeWillChange] attribute should be used to temporarily suppress the notice in /var/www/twodee/blog/wp-includes/class-wp-block-list.php on line 75

Deprecated: Return type of WP_Block_List::offsetGet($index) should either be compatible with ArrayAccess::offsetGet(mixed $offset): mixed, or the #[\ReturnTypeWillChange] attribute should be used to temporarily suppress the notice in /var/www/twodee/blog/wp-includes/class-wp-block-list.php on line 89

Deprecated: Return type of WP_Block_List::offsetSet($index, $value) should either be compatible with ArrayAccess::offsetSet(mixed $offset, mixed $value): void, or the #[\ReturnTypeWillChange] attribute should be used to temporarily suppress the notice in /var/www/twodee/blog/wp-includes/class-wp-block-list.php on line 110

Deprecated: Return type of WP_Block_List::offsetUnset($index) should either be compatible with ArrayAccess::offsetUnset(mixed $offset): void, or the #[\ReturnTypeWillChange] attribute should be used to temporarily suppress the notice in /var/www/twodee/blog/wp-includes/class-wp-block-list.php on line 127

Deprecated: Return type of WP_Block_List::count() should either be compatible with Countable::count(): int, or the #[\ReturnTypeWillChange] attribute should be used to temporarily suppress the notice in /var/www/twodee/blog/wp-includes/class-wp-block-list.php on line 199

Deprecated: Creation of dynamic property Automattic\Jetpack\Connection\Manager::$error_handler is deprecated in /var/www/twodee/blog/wp-content/plugins/jetpack/vendor/automattic/jetpack-connection/src/class-manager.php on line 91

Deprecated: Creation of dynamic property WPCOM_REST_API_V2_Endpoint_Instagram_Gallery::$is_wpcom is deprecated in /var/www/twodee/blog/wp-content/plugins/jetpack/_inc/lib/core-api/wpcom-endpoints/class-wpcom-rest-api-v2-endpoint-instagram-gallery.php on line 23

Deprecated: Creation of dynamic property WPCOM_REST_API_V2_Endpoint_Mailchimp::$wpcom_is_wpcom_only_endpoint is deprecated in /var/www/twodee/blog/wp-content/plugins/jetpack/_inc/lib/core-api/wpcom-endpoints/class-wpcom-rest-api-v2-endpoint-mailchimp.php on line 17

Deprecated: Creation of dynamic property WPCOM_REST_API_V2_Endpoint_Tweetstorm_Gather::$wpcom_is_wpcom_only_endpoint is deprecated in /var/www/twodee/blog/wp-content/plugins/jetpack/_inc/lib/core-api/wpcom-endpoints/class-wpcom-rest-api-v2-endpoint-tweetstorm-gather.php on line 21

Deprecated: Creation of dynamic property WPCOM_REST_API_V2_Endpoint_Tweetstorm_Gather::$is_wpcom is deprecated in /var/www/twodee/blog/wp-content/plugins/jetpack/_inc/lib/core-api/wpcom-endpoints/class-wpcom-rest-api-v2-endpoint-tweetstorm-gather.php on line 22

Deprecated: Creation of dynamic property WPCOM_REST_API_V2_Endpoint_Gutenberg_Available_Extensions::$wpcom_is_site_specific_endpoint is deprecated in /var/www/twodee/blog/wp-content/plugins/jetpack/_inc/lib/core-api/wpcom-endpoints/gutenberg-available-extensions.php on line 20

Deprecated: Creation of dynamic property WPCOM_REST_API_V2_Endpoint_Memberships::$wpcom_is_wpcom_only_endpoint is deprecated in /var/www/twodee/blog/wp-content/plugins/jetpack/_inc/lib/core-api/wpcom-endpoints/memberships.php on line 23

Deprecated: Creation of dynamic property WPCOM_REST_API_V2_Endpoint_Memberships::$wpcom_is_site_specific_endpoint is deprecated in /var/www/twodee/blog/wp-content/plugins/jetpack/_inc/lib/core-api/wpcom-endpoints/memberships.php on line 24

Deprecated: Creation of dynamic property WPCOM_REST_API_V2_Endpoint_Subscribers::$wpcom_is_wpcom_only_endpoint is deprecated in /var/www/twodee/blog/wp-content/plugins/jetpack/_inc/lib/core-api/wpcom-endpoints/subscribers.php on line 15

Deprecated: Creation of dynamic property Automattic\Jetpack\Sync\Queue::$random_int is deprecated in /var/www/twodee/blog/wp-content/plugins/jetpack/vendor/automattic/jetpack-sync/src/class-queue.php on line 38

Deprecated: Creation of dynamic property Automattic\Jetpack\Sync\Queue::$random_int is deprecated in /var/www/twodee/blog/wp-content/plugins/jetpack/vendor/automattic/jetpack-sync/src/class-queue.php on line 38

Deprecated: Creation of dynamic property WPCOM_Widget_Goodreads::$shelves is deprecated in /var/www/twodee/blog/wp-content/plugins/jetpack/modules/widgets/goodreads.php on line 33

Deprecated: DateTime::__construct(): Passing null to parameter #1 ($datetime) of type string is deprecated in /var/www/twodee/blog/wp-includes/script-loader.php on line 348

Deprecated: Creation of dynamic property Advanced_Editor_Tools::$toolbar_classic_block is deprecated in /var/www/twodee/blog/wp-content/plugins/tinymce-advanced/tinymce-advanced.php on line 347

Deprecated: Creation of dynamic property Advanced_Editor_Tools::$toolbar_block is deprecated in /var/www/twodee/blog/wp-content/plugins/tinymce-advanced/tinymce-advanced.php on line 349

Deprecated: Creation of dynamic property Advanced_Editor_Tools::$toolbar_block_side is deprecated in /var/www/twodee/blog/wp-content/plugins/tinymce-advanced/tinymce-advanced.php on line 350

Deprecated: Creation of dynamic property Advanced_Editor_Tools::$panels_block is deprecated in /var/www/twodee/blog/wp-content/plugins/tinymce-advanced/tinymce-advanced.php on line 351

Deprecated: Creation of dynamic property Advanced_Editor_Tools::$used_block_buttons is deprecated in /var/www/twodee/blog/wp-content/plugins/tinymce-advanced/tinymce-advanced.php on line 354

Deprecated: Creation of dynamic property WP_Block_Type::$plan_check is deprecated in /var/www/twodee/blog/wp-includes/class-wp-block-type.php on line 333

Deprecated: Creation of dynamic property WP_Block_Type::$plan_check is deprecated in /var/www/twodee/blog/wp-includes/class-wp-block-type.php on line 333

Deprecated: Creation of dynamic property WP_Block_Type::$plan_check is deprecated in /var/www/twodee/blog/wp-includes/class-wp-block-type.php on line 333

Deprecated: Creation of dynamic property WP_Block_Type::$plan_check is deprecated in /var/www/twodee/blog/wp-includes/class-wp-block-type.php on line 333

Deprecated: Creation of dynamic property WP_Block_Type::$plan_check is deprecated in /var/www/twodee/blog/wp-includes/class-wp-block-type.php on line 333

Deprecated: Creation of dynamic property WP_Block_Type::$plan_check is deprecated in /var/www/twodee/blog/wp-includes/class-wp-block-type.php on line 333

Deprecated: Creation of dynamic property WP_Term::$object_id is deprecated in /var/www/twodee/blog/wp-includes/class-wp-term.php on line 198

Deprecated: Creation of dynamic property WP_Term::$object_id is deprecated in /var/www/twodee/blog/wp-includes/class-wp-term.php on line 198

Deprecated: Creation of dynamic property WP_Term::$object_id is deprecated in /var/www/twodee/blog/wp-includes/class-wp-term.php on line 198
» SENG 440: Lecture 10 – Master + Detail via Fragments

teaching machines

SENG 440: Lecture 10 – Master + Detail via Fragments

March 21, 2019 by . Filed under
Deprecated: Creation of dynamic property WP_Term::$cat_ID is deprecated in /var/www/twodee/blog/wp-includes/category.php on line 378

Deprecated: Creation of dynamic property WP_Term::$category_count is deprecated in /var/www/twodee/blog/wp-includes/category.php on line 379

Deprecated: Creation of dynamic property WP_Term::$category_description is deprecated in /var/www/twodee/blog/wp-includes/category.php on line 380

Deprecated: Creation of dynamic property WP_Term::$cat_name is deprecated in /var/www/twodee/blog/wp-includes/category.php on line 381

Deprecated: Creation of dynamic property WP_Term::$category_nicename is deprecated in /var/www/twodee/blog/wp-includes/category.php on line 382

Deprecated: Creation of dynamic property WP_Term::$category_parent is deprecated in /var/www/twodee/blog/wp-includes/category.php on line 383

Deprecated: Creation of dynamic property WP_Term::$cat_ID is deprecated in /var/www/twodee/blog/wp-includes/category.php on line 378

Deprecated: Creation of dynamic property WP_Term::$category_count is deprecated in /var/www/twodee/blog/wp-includes/category.php on line 379

Deprecated: Creation of dynamic property WP_Term::$category_description is deprecated in /var/www/twodee/blog/wp-includes/category.php on line 380

Deprecated: Creation of dynamic property WP_Term::$cat_name is deprecated in /var/www/twodee/blog/wp-includes/category.php on line 381

Deprecated: Creation of dynamic property WP_Term::$category_nicename is deprecated in /var/www/twodee/blog/wp-includes/category.php on line 382

Deprecated: Creation of dynamic property WP_Term::$category_parent is deprecated in /var/www/twodee/blog/wp-includes/category.php on line 383

Deprecated: Creation of dynamic property WP_Term::$cat_ID is deprecated in /var/www/twodee/blog/wp-includes/category.php on line 378

Deprecated: Creation of dynamic property WP_Term::$category_count is deprecated in /var/www/twodee/blog/wp-includes/category.php on line 379

Deprecated: Creation of dynamic property WP_Term::$category_description is deprecated in /var/www/twodee/blog/wp-includes/category.php on line 380

Deprecated: Creation of dynamic property WP_Term::$cat_name is deprecated in /var/www/twodee/blog/wp-includes/category.php on line 381

Deprecated: Creation of dynamic property WP_Term::$category_nicename is deprecated in /var/www/twodee/blog/wp-includes/category.php on line 382

Deprecated: Creation of dynamic property WP_Term::$category_parent is deprecated in /var/www/twodee/blog/wp-includes/category.php on line 383
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