This chapter covers:
- Transforming the existing Contacts web app into a native mobile app using Hyperview
- Using Hyperview behaviors to navigate between screens using stacks and modals
- Communicating between screens using the Events system
- Using the same backend to power both the web app and mobile app
Earlier chapters in this book explained the benefits of building apps using the hypermedia architecture. These benefits were demonstrated by building a robust Contacts web application. Then, Chapter 6 argued that hypermedia concepts can and should be applied to platforms other than the Web. We introduced Hyperview as an example of a hypermedia format and client specifically designed for building mobile apps. But you may still be wondering: what is it like to create a fully-featured, production-ready mobile app using Hyperview? Do we have to learn a whole new language and framework? In this chapter, we will show Hyperview in action by porting the Contacts web app to a native mobile app. You will see that many web development techniques (and indeed, much of the code) are completely identical when developing with Hyperview. How is that possible?
Our Contacts web app was built with the principle of HATEOAS (Hypermedia as the Engine of Application State).
All of the app’s features (retrieving, searching, editing, and creating contacts) are implemented in the backend (the
ContactsPython class). Our mobile app, built with Hyperview, also leverages HATEOAS and relies on the backend for all of the app’s logic. That means the
ContactsPython class can power our mobile app the same way it powers the web app, without any changes required.
- The client-server communication in the web app happens using HTTP. The HTTP server for our web app is written using the Flask framework. Hyperview also uses HTTP for client-server communication. So we can re-use the Flask routes and views from the web app for the mobile app as well.
- The web app uses HTML for its hypermedia format, and Hyperview uses HXML. HTML and HXML are different formats, but the base syntax is similar (nested tags with attributes). This means we can use the same templating library (Jinja) for HTML and HXML. Additionally, many of the concepts of htmx are built into HXML. We can directly port web app features implemented with htmx (search, infinite loading) to HXML.
Essentially, we can re-use almost everything from the web app backend, but we will need to replace the HTML templates with HXML templates. Most of the sections in this chapter will assume we have the web contacts app running locally and listening on port 5000. The focus will be on creating new HXML templates for our mobile app’s UI.
Creating a mobile app
But before we dive into HXML, there’s one pesky requirement: the Hyperview client. When developing web applications, you only need to worry about the server because the client (web browser) is universally available. There’s no equivalent Hyperview client installed on every mobile device. Instead, we will create our own Hyperview client, customized to only talk to our server. This client can be packaged up into an Android or iOS mobile app, and distributed through the respective app stores.
Luckily, we don’t need to start from scratch to implement a Hyperview client. The Hyperview code repository comes with a demo backend and a demo client built using Expo. We will use this demo client but point it to our contacts app backend as a starting point.
yarn start, you will be presented with a prompt asking you to open the mobile app using an Android emulator or iOS simulator.
Select an option based on which developer SDK you have installed.
(The screenshots in this chapter will be taken from the iOS simulator.)
With any luck, you will see the Expo mobile app installed in the simulator.
The mobile app will automatically launch and show a screen saying “Network request failed.”
That’s because by default, this app is configured to make a request to http://0.0.0.0:8085/index.xml, but our backend is listening on port 5000.
To fix this, we can make a simple configuration change in the
We’re not up and running yet. With our Hyperview client now pointing to the right endpoint, we see a different error, a “ParseError.” That’s because the backend is responding to requests with HTML content, but the Hyperview client expects an XML response (specifically, HXML). So it’s time to turn our attention to our Flask backend. We will go through the Flask views, and replace the HTML templates with HXML templates. Specifically, let’s support the following features to our mobile app:
- A searchable list of contacts
- Viewing the details of a contact
- Editing a contact
- Deleting a contact
- Adding a new contact
A Searchable List of Contacts
We will start building our Hyperview app with the entrypoint screen, the list of contacts. For the initial version of this screen, let’s support the following features from the web app:
- display a scrollable list of contacts
- “search-as-you-type” field above the list
- “infinite-scroll” to load more contacts as the user scrolls through
Additionally, we will add a “pull-to-refresh” interaction on the list, since users expect this from list UIs in mobile apps.
If you recall, all of the pages in the Contacts web app extended a common base template,
We need a similar base template for the screens of the mobile app.
This base template will contain the style rules or our UI, and a basic structure common to all screens.
Let’s call it
We covered the HXML tags and attributes in the previous chapter.
This template sets up a basic screen layout using
Note that the HXML syntax plays well with the Jinja templating library.
Here, we’re using Jinja’s blocks to define two sections (
content) that will hold the unique content of a screen.
With our base template completed, we can create a template specifically for the contacts list screen.
This template extends the base
layout.xml, and overrides the
content block with a
At first, it might seem strange that the form wraps both the
<text-field> and the
But remember: in Hyperview, the form data gets included in any request originating from a child element.
We will soon add interactions to the list (pull to refresh) that will require the form data.
Note the use of a Jinja
include tag to render the HXML for the rows of contacts in the list (
Just like in the HTML templates, we can use the
include to break up our HXML into smaller pieces.
It also allows the server to respond with just the
rows.xml template for interactions like searching, infinite scroll, and pull-to-refresh.
In the web app, each row in the list showed the contact’s name, phone number, and email address. But in a mobile app, we have less real-estate. It would be hard to cram all this information into one line. Instead, the row just shows the contact’s first and last name, and falls back to email or phone if the name is not set. To render the row, we again make use of Jinja template syntax to render dynamic text with data passed to the template.
We now have templates for the base layout, the contacts screen, and the contact rows.
But we still have to update the Flask views to use these templates.
Let’s take a look at the
contacts() view in its current form, written for the web app:
This view supports fetching a set of contacts based on two query params,
It also decides whether to render the full page (
index.html) or just the contact rows (
rows.html) based on the
This presents a minor problem.
HX-Trigger header is set by the htmx library; there’s no equivalent feature in Hyperview.
Moreover, there are multiple scenarios in Hyperview that require us to respond with just the contact rows:
- loading the next page of contacts
Since we can’t depend on a header like
HX-Trigger, we need a different way to detect if the client needs the full screen or just the rows in the response.
We can do this by introducing a new query param,
When this param has the value
true, the view will respond to the request by rendering the
Otherwise, it will respond with the
There’s one more change we have to make.
Flask assumes that most views will respond with HTML.
So Flask defaults the
Content-Type response header to a value of
But the Hyperview client expects to receive HXML content, indicated by a
Content-Type response header with value
The client will reject responses with a different content type.
To fix this, we need to explicitly set the
Content-Type response header in our Flask views.
We will do this by introducing a new helper function,
As you can see, this helper function uses
render_template() under the hood.
render_template() returns a string.
This helper function uses that string to create an explicit
The response object has a
headers attribute, allowing us to set and change the response headers.
application/xml so that the Hyperview client recognizes the content.
This helper is a drop-in replacement for
render_template in our views.
So all we need to do is update the last line of the
With these changes to the
contacts() view, we can finally see the fruits of our labor.
After restarting the backend and refreshing the screen in our mobile app, we can see the contacts screen!
So far, we have a mobile app that displays a screen with a list of contacts.
But our UI doesn’t support any interactions.
Typing a query in the search field doesn’t filter the list of contacts.
Let’s add a behavior to the search field to implement a search-as-you-type interaction.
This requires expanding
<text-field> to add a
The first thing you’ll notice is that we changed the text field from using a self-closing tag (
<text-field />) to using opening and closing tags (
This allows us to add a child
<behavior> element to define an interaction.
trigger="change" attribute tells Hyperview that a change to the value of the text field will trigger an action.
Any time the user edits the content of the text field by adding or deleting characters, an action will trigger.
The remaining attributes on the
<behavior> element define the action.
action="replace-inner" means the action will update content on the screen, by replacing the HXML content of an element with new content.
replace-inner to do its thing, we need to know two things: the current element on the screen that will be targeted by the action, and the content that will used for the replacement.
target="contacts-list" tells us the ID of the current element.
Note that we set
id="contacts-list" on the
<list> element in
So when the user enters a search query into the text field, Hyperview will replace the content of
<list> (a bunch of
with new content (
<item> elements that match the search query) received in the relative href response
(the domain is inferred from the domain used to fetch the screen).
href includes our
rows_only query param; we want the response to only include the rows and not the entire screen.
That’s all it takes to add search-as-you-type functionality to our mobile app!
As the user types a search query, the client will make requests to the backend and replace the list with the search results.
You may be wondering, how does the backend know the query to use?
href attribute in the behavior does not include the
q param expected by our backend.
But remember, in
index.xml, we wrapped the
<list> elements with a parent
<form> element defines a group of inputs that will be serialized and included in any HTTP requests triggered by its child elements.
In this case, the
<form> element surrounds the search behavior and the text field.
So the value of the
<text-field> will be included in our HTTP request for the search results.
Since we are making a
GET request, the name and value of the text field will be serialized as a query param.
Any existing query params on the
href will be preserved.
This means the actual HTTP request to our backend looks like
Our backend already supports the
q param for searching, so the response will include rows that match the string “Car”.
If the user has hundreds or thousands of contacts, loading them all at once may result in poor app performance. That’s why most mobile apps with long lists implement an interaction known as “infinite scroll”. The app loads a fixed number of initial items in the list, let’s say 100 items. If the user scrolls to the bottom of the list, they see a spinner indicating more content is loading. Once the content is available, the spinner is replaced with the next page of 100 items. These items are appended to the list, they don’t replace the first set of items. So the list now contains 200 items. If the user scrolls to the bottom of the list again, they will see another spinner, and the app will load the next set of content. Infinite scroll improves app performance in two ways:
- The initial request for 100 items will be processed quickly, with predictable latency.
- Subsequent requests can also be fast and predictable.
- If the user doesn’t scroll to the bottom of the list, the app won’t have to make subsequent requests.
Our Flask backend already supports pagination on the
/contacts endpoint via the
page query param.
We just need to modify our HXML templates to make use of this parameter.
To do this, let’s edit
rows.xml to add a new
<item> below the Jinja for-loop:
If the current list of contacts passed to the template is empty, we can assume there’s no more contacts to fetch from the backend.
So we use a Jinja conditional to only include this new
<item> if the list of contacts is non-empty.
<item> element gets an ID and a behavior.
The behavior defines the infinite scroll interaction.
Up until now, we’ve seen
trigger values of
But to implement infinite scroll, we need a way to trigger the action when the user scrolls to the bottom of the list.
visible trigger can be used for this exact purpose.
It will trigger the action when the element with the behavior is visible in the device viewport.
In this case, the new
<item> element is the last item in the list, so the action will trigger when the user scrolls down far enough for the item to enter the viewport.
As soon as the item is visible, the action will make an HTTP GET request, and replace the loading
<item> element with the response content.
Note that our href must include the
rows_only=true query param, so that our response will only include HXML for the contact items, and not the entire screen.
Also, we’re passing the
page query param, incrementing the current page number to ensure we load the next page.
What happens when there’s more than one page of items?
The initial screen will include the first 100 items, plus the “load-more” item at the bottom.
When the user scrolls to the bottom of the screen, Hyperview will request the second page of items (
&page=2), and replace the “load-more” item with the new items.
But this second page of items will include a new “load-more” item.
So once the user scrolls through all of the items from the second page, Hyperview will again request more items (
And once again, the “load-more” item will be replaced with the new items.
This will continue until all of the items will be loaded on the screen.
At that point, there will be no more contacts to return, the response will not include another “load-more” item, and our pagination is over.
Pull-to-refresh is a common interaction in mobile apps, especially on screens featuring dynamic content. It works like this: At the top of a scrolling view, the user pulls the scrolling content downwards with a swipe-down gesture. This reveals a spinner “below” the content. Pulling the content down sufficiently far will trigger a refresh. While the content refreshes, the spinner remains visible on screen, indicating to the user that the action is still taking place. Once the content is refreshed, the content retracts back up to its default position, hiding the spinner and letting the user know that the interaction is done.
This pattern is so common and useful that it’s built in to Hyperview via the
Let’s add pull-to-refresh to our list of contacts to see it in action.
You’ll notice something unusual in the snippet above: rather than adding a
<behavior> element to the
<list>, we added the behavior attributes directly to the
This is a shorthand notation that’s sometimes useful for specifying single behaviors on an element.
It is equivalent to adding a
<behavior> element to the
<list> with the same attributes.
So why did we use the shorthand syntax here?
It has to do with the action,
Remember, this action replaces all child elements of the target with the new content.
<behavior> elements too!
Let’s say our
<list> did contain a
If the user did a search or pull-to-refresh, we would replace the content of
<list> with the content from
<behavior> would no longer be defined on the
<list>, and subsequent attempts to pull-to-refresh would not work.
By defining the behavior as attributes of
<list>, the behavior will persist even when replacing the items in the list.
Generally, we prefer to use explicit
<behavior> elements in my HXML.
It makes it easier to define multiple behaviors, and to move the behavior around while refactoring.
But the shorthand syntax is good to apply in situations like this.
Viewing The Details Of A Contact
Now that our contacts list screen is in good shape, we can start adding other screens to our app.
The natural next step is to create a details screen, which appears when the user taps an item in the contacts list.
Let’s update the template that renders the contact
<item> elements, and add a behavior to show the details screen.
Our Flask backend already has a route for serving the contact details at
In our template, we use a Jinja variable to dynamically generate the URL path for the current contact in the for-loop.
We also used the “push” action to show the details by pushing a new screen onto the stack.
If you reload the app, you can now tap any contact in the list, and Hyperview will open the new screen.
However, the new screen will show an error message.
That’s because our backend is still returning HTML in the response, and the Hyperview client expects HXML.
Let’s update the backend to respond with HXML and the proper headers.
Just like with the
render_to_response() to set the
Content-Type header on the response.
We’re also generating the response from a new HXML template, which we can create now:
The contacts detail screen extends the base
layout.xml template, just like we did in
This time, we’re overriding content in both the
header block and
Overriding the header block lets us add a "Back" button with a behavior.
When pressed, the Hyperview client will unwind the navigation stack and return the user to the contacts list.
Note that triggering this behavior is not the only way to navigate back.
The Hyperview client respects navigation conventions on different platforms.
On iOS, users can also navigate to the previous screen by swiping right from the left edge of the device.
On Android, users can also navigate to the previous screen by pressing the hardware back button.
We don’t need to specify anything extra in the HXML to get these interactions!
With just a few simple changes, we’ve gone from a single-screen app to a multi-screen app.
Note that we didn’t need to change anything in the actual mobile app code to support our new screen.
This is a big deal.
In traditional mobile app development, adding screens can be a significant task.
Developers need to create the new screen, insert it into the appropriate place of the navigation hierarchy, and write code to open the new screen from existing screens.
In Hyperview, we just added a behavior with
Editing a Contact
So far, our app lets us browse a list of contacts, and view details of a specific contact. Wouldn’t it be nice to update the name, phone number, or email of a contact? Let’s add UI to edit contacts as our next enhancement.
First we have to figure out how we want to display the editing UI.
We could push a new editing screen onto the stack, the same way we pushed the contact details screen.
But that’s not the best design from a user-experience perspective.
Pushing new screens makes sense when drilling down into data, like going from a list to a single item.
But editing is not a “drill-down” interaction, it’s a mode switch between viewing and editing.
So instead of pushing a new screen, let’s replace the current screen with the editing UI.
That means we need to add a button and behavior that use the
This button can be added to the header of the contact details screen.
Once again, we’re reusing an existing Flask route (
/contacts/<contact_id>/edit) for the edit UI, and filling in the contact ID using data passed to the Jinja template.
We also need to update the
contacts_edit_get() view to return an XML response based on an HXML template (
I’ll skip the code sample because the needed changes are identical to what we applied to
contacts_view() in the previous section.
Instead, let’s focus on the template for the edit screen.
Since the edit screen needs to send data to the backend, we wrap the entire content section in a
This ensures the form field data will be included in the HTTP requests to our backend.
<form> element, our UI is divided into two sections: the form fields, and the Save button.
The actual form fields are defined in a separate template (
form_fields.xml) and added to the edit screen using a Jinja include tag.
I omitted the code for the contact’s phone number and email address, because they follow the same pattern as the first and last name.
Each contact field has its own
<text-field>, and a
<text> element below it to display possible errors.
<text-field> has two important attributes:
namedefines the name to use when serializing the text-field’s value into form data for HTTP requests. We are using the same names as the web app from previous chapters (
valuedefines the pre-filled data in the text field. Since we are editing an existing contact, it makes sense to pre-fill the text field with the current name, phone, or email.
You might be wondering, why did we choose to define the form fields in a separate template (
To understand that decision, we need to first discuss the “Save” button.
When pressed, the Hyperview client will make an HTTP
POST request to
contacts/<contact_id>/edit, with form data serialized from the
The HXML response will replace the contents of form field container (ID
But what should that response be?
That depends on the validity of the form data:
- If the data is invalid (eg duplicate email address), our UI will remain in the editing mode and show error messages on the invalid fields. This allows the user to correct the errors and try saving again.
- If the data is valid, our backend will persist the edits, and our UI will switch back to a display mode (the contact details UI).
So our backend needs to distinguish between a valid and invalid edit.
To support these two scenarios, let’s make some changes to the existing
contacts_edit_post() view in the Flask app.
This view already contains conditional logic based on whether the contact model
save() fails, we render the
contact.errors will contain error messages for the invalid fields, which will be rendered into the
<text style="edit-field-error"> elements.
save() succeeds, we will also render the
But this time, the template will get a
saved flag, indicating success.
We will update the template to use this flag to implement our desired UI: switching the UI back to display mode.
The Jinja template conditional ensures that our behavior only renders on successful saves, and not when the screen first opens (or the user submits invalid data).
On success, the template includes a behavior that triggers immediately thanks to
The action reloads the current screen with the Contact Details screen (from the
When the user hits “Save”, our backend persists the new contact data, and the screen switches back to the Details screen.
Since the app will make a new HTTP request to get the contact details, it’s guaranteed to show the freshly saved edits.
We now have a working Edit UI in our contacts app. Users can enter the Edit mode by pressing a button on the contact details screen. In the Edit mode, they can update the contact’s data and save it to the backend. If the backend rejects the edits as invalid, the app stays in Edit mode and shows the validation errors. If the backend accepts and persists the edits, the app will switch back to the details mode, showing the updated contact data.
Let’s add one more enhancement to the Edit UI. It would be nice to let the user switch away from the Edit mode without needing to save the contact. This is typically done by providing a “Cancel” action. We can add this as a new button below the “Save” button.
This is the same technique we used to switch from the edit UI to the details UI upon successfully editing the contact.
But pressing “Cancel” will update the UI faster than pressing “Save”.
On save, the app will first make a
POST request to save the data, and then a
GET request for the details screen.
Cancelling skips the
POST, and immediately makes the
Updating the Contacts List
At this point, we can claim to have fully implemented the Edit UI. But there’s a problem. In fact, if we stopped here, users may even consider the app to be buggy! Why? It has to do with syncing the app state across multiple screens. Let’s walk through this series of interactions:
- Launch the app to the Contacts List.
- Press on the contact “Joe Blow” to load his Contact Details.
- Press Edit to switch to the edit mode, and change the contact’s first name to “Joseph”.
- Press Save to switch back to viewing mode. The contact’s name is now “Joseph Blow”.
- Hit the back button to return to the Contacts List.
Did you catch the issue? Our Contacts list is still showing the same list of names as when we launched the app. The contact we just renamed to “Joseph” is still showing up in the list as “Joe”. This is a general problem in Hypermedia applications. The client does not have a notion of shared data across different parts of the UI. Updates in one part of the app will not automatically update other parts of the app. Luckily, there’s a solution to this problem in Hyperview: events. Events are built into the behavior system, and allow lightweight communication between different parts of the UI.
We know enough about Hyperview’s event system to solve the bug in our app.
When the user saves a change to a contact, we need to dispatch an event from the Details screen.
And the Contacts screen needs to listen to that event, and reload itself to reflect the edits.
form_fields.xml template already gets the
saved flag when the backend successfully saves a contact, it’s a good place to dispatch the event:
Now, we just need the contacts list to listen for the
contact-updated event, and reload itself:
Any time the user edits a contact, the Contacts List screen will update to reflect the edits.
The addition of these two
<behavior> elements fixes the bug: the Contacts List screen will correctly show “Joseph Blow” in the list.
Note that we intentionally added the new behavior inside the
The ensures the triggered request will preserve any search query.
To show what we mean, let’s revisit the set of steps that demonstrated the buggy behavior.
Assume that before pressing on “Joe Blow”, the user had searched the contacts by typing “Joe” in the search field.
When the user later updates the contact to “Joseph Blow”, our template dispatches the “contact-updated” event, which triggers the
replace-inner behavior on the contact list screen.
Due to the parent
<form> element, the search query “Joe” will be serialized with the request:
Since the name “Joseph” doesn’t match the query “Joe”, the contact we edited will not appear in the list (until the user clears out the query).
Our app’s state remains consistent across our backend and all active screens.
Events introduce a level of abstraction to behaviors.
So far, we’ve seen that editing a contact will cause the list of contacts to refresh.
But the list of contacts should also refresh after other actions, such as deleting a contact or adding a new contact.
As long as our HXML responses for deletion or creation include a behavior to dispatch a
contact-updated event, then we will get the desired refresh behavior on the contacts list screen.
The screen doesn’t care what causes the
contact-updated event to be dispatched.
It just knows what it needs to do when it happens.
Deleting a Contact
Speaking of deleting a contact, this is a good next feature to implement.
We will let users delete a contact from the Edit UI.
So let’s add a new button to
The HXML for the Delete button is pretty similar to the Save button, but there are a few subtle differences.
Remember, pressing the Save button results in one of two expected outcomes: failing and showing validation errors on the form, or succeeding and switching to the contact details screen.
To support the first outcome (failing and showing validation errors), the save behavior replaces the contents of the
<view id="form-fields"> container with a re-rendered version of
Therefore, using the
replace-inner action makes sense.
Deletion does not involve a validation step, so there’s only one expected outcome: successfully deleting the contact.
When deletion succeeds, the contact no longer exists.
It doesn’t make sense to show the edit UI or contact details for a non-existent contact.
Instead, our app will navigate back to the previous screen (the contacts list).
Our response will only include behaviors that trigger immediately, there’s no UI to change.
Therefore, using the
append action will preserve the current UI while Hyperview runs the actions.
Note that in addition to behavior to navigate back, this template also includes a behavior to dispatch the
In the previous chapter section, we added a behavior to
index.xml to refresh the list when that event is dispatched.
By dispatching the event after a deletion, we will make sure the deleted contact gets removed from the list.
Once again, I’m going to skip over the changes to the Flask backend.
Suffice it to say, we will need to update the
contacts_delete() view to respond with the
And we need to update the route to support
POST in addition to
DELETE, since the Hyperview client only understands
We now have a fully functioning deletion feature!
But it’s not the most user-friendly: it takes one accidental tap to permanently delete a contact.
For destructive actions like deleting a contact, it’s always a good idea to ask the user for confirmation.
We can add a confirmation to the delete behavior by using the
alert system action described in the previous chapter.
As you recall, the
alert action will show a system dialog box with buttons that can trigger other behaviors.
All we have to do is wrap the delete
<behavior> in a behavior that uses
Unlike before, pressing the delete button will not have an immediate effect. Instead, the user will be presented with the dialog box and asked to confirm or cancel. Our core deletion behavior didn’t change, we just chained it from another behavior.
Adding a New Contact
Adding a new contact is the last feature we want to support in our mobile app. And luckily, it’s also the easiest. We can reuse the concepts (and even some templates) from features we’ve already implemented. In particular, adding a new contact is very similar to editing an existing contact. Both features need to:
- Show a form to collect information about the contact
- Have a way to save the entered information
- Show validation errors on the form
- Persist the contact when there are no validation errors
Since the functionality is so similar, I’m going to summarize the changes here without showing the code. Hopefully, you can follow along:
headerblock to add a new “Add” button.
Include a behavior in the button. When pressed, push a new screen as a modal by using
action="new", and request the screen content from
- Override the
Create a template
Override the header block to include a button that closes the modal, using
hv/form_fields.xmltemplate to render empty form fields
- Add a “Add Contact” button below the form fields.
Include a behavior in the button. When pressed, make a
/contacts/new, and use
action="replace-inner"to update the form fields.
- Override the header block to include a button that closes the modal, using
Update the Flask view.
saved=Truewhen rendering the template after successfully persisting the new contact.
form_fields.xml for both editing and adding a contact, we get to reuse some code and ensure the two features have a consistent UI.
Also, our “Add Contact” screen will benefit from the “saved” logic that’s already a part of
After successfully adding a new contact, the screen will dispatch the
contact-updated event, which will refresh the contacts list and show the newly added contact.
The screen will reload itself to show the Contact Details.
Deploying the App
With the completion of the contact creation UI, we have a fully implemented mobile app! It supports searching a list of contacts, viewing the details of a contact, editing and deleting a contact, and adding a new contact. But so far, we’ve been developing the app using a simulator on our desktop computer. How can we see it running on a mobile device? And how can we get it into the hands of our users?
To see the app running on a physical device, let’s take advantage of the Expo platform’s app preview functionality.
- Download the Expo Go app on an Android or iOS device.
Restart the Flask app, binding to an interface accessible on your network.
This might look something like
flask run --host 192.168.7.229, where the host is your computer’s IP address on the network.
Update the Hyperview client code so that
demo/src/constants.js) points to the IP and port that the Flask server is bound to.
yarn startin the Hyperview demo app, you will see a QR code printed in the console, with instructions on how to scan it on Android and iOS.
Once you scan the QR code, the full app will run on the device! As you interact with the app, you will see HTTP requests made to the Flask server. You can even use the physical device during development. Any time you make a change in the HXML, just reload the screen to see the UI updates.
So we have the app running on a physical device, but it’s still not production ready. To get the app into the hands of our users, there’s a few things we need to do:
- Deploy our backend in production. We need to use a production-grade web server like Gunicorn instead of the Flask development server. And we should run our app on a machine reachable on the Internet, most likely using a cloud provider like AWS or Heroku.
Create standalone binary apps.
By following the instructions from the Expo project, we can create a
.apkfile, for the iOS and Android platforms. Remember to update
ENTRY_POINT_URLin the Hyperview client to point to the production backend.
- Submit our binaries to the iOS App Store or Google Play Store, and wait for app approval.
Once the app is approved, congratulations! Our mobile app can be downloaded by Android and iOS users. And here’s the best part: Because our app uses the hypermedia architecture, we can add features to our app by simply updating the backend. The UI and interactions are completely specified with the HXML generated from server-side templates. Want to add a new section to a screen? Just update an existing HXML template. Want to add a new type of screen to the app? Create a new route, view, and HXML template. Then, add a behavior to an existing screen that will open the new screen. To push these changes to your users, you just need to re-deploy the backend. Our app knows how to interpret HXML, and that’s enough for it to understand how to handle the new features.
One Backend, Multiple Hypermedia formats
To create a mobile app using the hypermedia architecture, we started with the web-based contacts app and made a few changes, primarily replacing HTML templates with HXML templates.
But in the process of porting the backend to serve our mobile app, we lost the web application functionality.
Indeed, if you tried to visit
http://0.0.0.0:5000 in a web browser, you would see a jumble of text and XML markup.
That’s because web browsers don’t know how to render plain XML, and they certainly don’t know how to interpret the tags and attributes of HXML to render an app.
It’s a shame, because the Flask code for the web application and mobile app are nearly identical.
The database and model logic are shared, and most of the views are unchanged as well.
At this point you’re surely wondering: is it possible to use the same backend to serve both a web application and mobile app? The answer is yes! In fact, this is one of the benefits of using a hypermedia architecture across multiple platforms. We don’t need to port any client-side logic from one platform to another, we just need to respond to requests with the appropriate Hypermedia format. To do this, we will utilize content negotiation built into HTTP.
What is Content Negotiation?
Imagine a German speaker and Japanese speaker both visit
https://google.com in their web browser.
They will see the Google home page localized in German and Japanese, respectively.
How does Google know to return a different version of the homepage based on the user’s preferred language?
The answer lies in the REST architecture, and how it separates the concepts of resources and representations.
In the REST architecture, the Google homepage is considered to be a single “resource”, represented by a unique URL. However, that single resource can have multiple “representations”. Representations are variations on how the content of the resource is presented to the client. The German and Japanese versions of the Google homepage are two representations of the same resource. To determine the best representation of a resource to return, HTTP clients and servers engage in a process called “content negotiation”. It works like this:
Clients specify the preferred representation through
The server tries to match the preferred representation as best it can, and communicates back the chosen representation using
In the Google homepage example, the German speaker uses a browser that is set to prefer content localized for German.
Every HTTP request made by the web browser will include a header
The server sees the request header, and it will return a response localized for German (if it can).
The HTTP response will include a
Content-Language: de-DE header to inform the client of the language of the response content.
Language is just one factor for resource representation.
More importantly for us, resources can be represented using different content types, such as HTML or HXML.
Content negotiation over content type is done using the
Accept request header and
Content-Type response header.
Web browsers set
text/html as the preferred content type in the
The Hyperview client sets
application/vnd.hyperview+xml as the preferred content type.
This gives our backend a way to distinguish requests coming from a web browser or Hyperview client, and serve the appropriate content to each.
There are two main approaches: fine-grained and global.
Approach 1: Template Switching
When we ported the Contacts app from the web to mobile, we kept all of the Flask views but made some minor changes.
Specifically, we introduced a new function
render_to_response() and called it in the return statement of each view.
Here’s the function again to refresh your memory:
render_to_response() renders a template with the given context, and turns it into an Flask response object with the appropriate Hyperview
Obviously, the implementation is highly-specific to serving our Hyperview mobile app.
But we can modify the function to do content negotiation based on the request’s
Flask’s request object exposes an
accept_mimetypes property to help with content negotiation.
We pass our two content MIME types to
request.accept_mimetypes.best_match() and get back the MIME type that works for our client.
Based on the best matching MIME type, we choose to either render an HTML template or HXML template.
We also make sure to set the
Content-Type header to the appropriate MIME type.
The only difference in our Flask views is that we need to provide both an HTML and HXML template:
After updating all of the Flask views to support both templates, our backend will support both web browsers and our mobile app! This technique works well for the Contacts app because the screens in the mobile app map directly to pages of the web application. Each app has a dedicated page (or screen) for listing contacts, showing and editing details, and creating a new contact. This meant the Flask views could be as-is without major changes. But what if we wanted to re-imagine the Contacts app UI for our mobile app? Perhaps we want the mobile app to use a single screen, with rows that expanded in-line to support viewing and editing the information? In situations where the UI diverges between platforms, Template Switching becomes cumbersome or impossible. We need a different approach to have one backend serve both hypermedia formats.
Approach 2: The Redirect Fork
If you recall, the Contacts web app has an
index view, routed from the root path
When someone requests to the root path of the web application, Flask redirects them to the
This redirect also works in our Hyperview mobile app.
The Hyperview client’s
ENTRY_POINT_URL points to
http://0.0.0.0:5000/, and the server redirects it to
But there’s no law that says we need to redirect to the same path in our web application and mobile app.
What if we used the
Accept header to redirect to decide on the redirect path?
The entrypoint is a fork in the road: if the client wants HTML, we redirect them to one path. If the client wants HXML, we redirect them to a different path. These redirects would be handled by different Flask views:
mobile_contacts() view would render an HXML template with a list of contacts.
Tapping a contact item would open a screen requested from
/mobile/contacts/1, handled by a view
After the initial fork, all subsequent requests from our mobile app go to paths prefixed with
/mobile/, and get handled by mobile-specific Flask views.
Likewise, all subsequent requests from the web app go to paths prefixed with
/web/, and get handled by web-specific Flask views.
(Note that in practice, we would want to separate the web and mobile views into separate parts of our codebase:
mobile_app.py. We may also choose not to prefix the web paths with
/web/, if we want more elegant URLs displayed in the browser’s address bar.)
You may be thinking that the Redirect Fork leads to a lot of code duplication. After all, we need to write double the number of views: one set for the web application, and one set for the mobile app. That is true, which is why the Redirect Fork is only preferred if the two platforms require a disjointed set of view logic. If the apps are similar on both platforms, Template Switching will save a lot of time and keep the apps consistent. Even if we need to use the Redirect Fork, the bulk of the logic in our models can be shared by both sets of views. In practice, you may start out using Template Switching, but then realize you need to implement a fork for platform-specific features. In fact, we’re already doing that in the Contacts app. When porting the app from web to mobile, we didn’t bring over certain features like archiving functionality. The dynamic archive UI is a power feature that wouldn’t make sense on a mobile device. Since our HXML templates don’t expose any entrypoints to the Archive functionality, we can treat it as “web-only” and not worry about supporting it in Hyperview.
- Creating a Hyperview-powered mobile app is as simple as cloning a Git repo and configuring a single entrypoint URL.
- Flask is perfectly suited for serving a Hyperview mobile app. The Jinja templating system can generate dynamic HXML based on the context from a view.
Using elements like
<item>, we can create native-feeling screens in HXML.
- Using behaviors, we can implement interactions on the contacts list such as infinite scroll, search-as-you-type, and pull-to-refresh.
- Events are a client-side feature of Hyperview that allows triggering behaviors across screens. They are useful to keep state in sync throughout the app, such as after editing or deleting a contact.
- A Hyperview-powered mobile app can be bundled and released through the iOS and Android app stores. New screens and features can be added to the app just by updating the backend!
- The same Flask backend can support both web and mobile apps. Using HTTP content negotiation, a server can render either HTML or HXML responses from the same view. More complex apps may require the use of redirects and platform-specific views.