Composing Web Content
or how to use Jetpack Compose to render web pages
The vast majority of news-related apps have a well defined format, that displays a list of items, and, upon click, the selected news is rendered as web page in a WebView
. This approach has neat pros: it’s a fast way to get the data to show, it is a widely adopted solution that works, and any content that already exists for the web can be made available to native mobile users. With the Android WebView
component it’s super easy to take any news RSS feed and make a reader: it only requires loading a URL, after all. On the other hand, there are some cons, that are quite important as well. The first one is the lack of personalisation: we are basically stuck with whatever UI, order of elements, and layout adoption that the web page brings. This first point implies the second and the third as well: we might lack a support for dark mode on the website, and the content UI might not be aligned with our app theme.
Solution Overview
It is clear, from the introduction, that we want to explore something that can take web content and render it using native technologies. The general idea would be to have a component (let’s call it Parser) that takes in the HTML source of a webpage and splits it into a list of types that can be rendered natively.
In order to render the types, we have two solutions on Android, at the time of writing: we can leverage the old but reliable View
system, or we could explore what Jetpack Compose has to offer. As it might be clear from the title, we’ll go with the latter.
Why Jetpack Compose?
Of all the possible solutions using the View
system, two really struck me: using a LinearLayout
to add all the children as they were identified, or rely on a RecyclerView
. After this initial choice, we would need to create XML layouts for all the different child View
s, wrap them in a Compound View
, create the binding logic and at that point, we would be able to add such components either in aLinearLayout
or as elements of RecyclerView
. And, of course, all this work has to be done for every new element we want to support.
If we leverage Jetpack Compose, instead, we have several layers less to care for: while we still have to create the UI, Composables
are written in Kotlin already, making it super easy to bind the data to the logic. Columns
and Rows
are first class citizens in the framework, thus making it much easier to align the content as we wish.
The example
As an example, I took an interesting blog post of an important Italian online newspaper, called Il Post. I chose this specific article as it had a few things I wanted to experiment with: images (and GIFs) are shown in different ways and HTML tags, paragraphs can have a subtitle or not, there is an HTML form and it has quite the linear structure, easy to scan.
Architecture
We won’t dive deep into the implementation details of this specific blog: while web content usually aligns to some element, there isn’t a strict standard that can always be applied, and minor tweaks to the parsing logic would be needed from a website to another, but the general idea is to create a list of elements that can be rendered by Jetpack Compose: a title, a header, images, text, and everything else we might want to support.
The first step I took was to create some basic models that could represent the part of the blog post I wanted my app to support: for instance, there is a Title
, which only contains a String
, exactly like a Header
. We need to create two elements as we can leverage their type to render them in a different way. Here below, there is the complete list of the types defined:
I decided to use Sealed Classes
as I wanted to limit the scope of the components and always be able to understand what has been implemented or not. The use of Sealed Classes
is not mandatory, interfaces
can do the same, it’s just an implementation detail.
The only data class
not extending Component
is Article
: I preferred to keep the List<Components>
wrapped and deal with the specific data type, but it’s not that important.
The next part was to parse the HTML and get the data needed to populate our models. I decided to use JSoup to get the content from the aforementioned post: parsing the DOM is no easy task, but this library is really helpful and by combining selectors and Kotlin, it’s fairly easy to accomplish the task.
In the code here above, we can see that JSOUP takes care of getting the content of a URL for us without the need of any other networking library (line 2): this is quite cool, but it’s worth mentioning that this operation happens on the main thread by default, so it need to be moved to another thread when this code is running on Android.
The parsing itself is really just a simple call to the JSoup API (line 15 and 18) and asks the DOM to return us a specific element with a specific class, like an h1
with the entry-title
class, and grab its content. In many cases specifying the class might not be needed, as there might be only one h1
title, but in the example, I preferred to be safer and be as restrictive as possible.
In the blog post we are using for practice, images are stored in a peculiar way: the URL for the image is not inside the src
attribute, but rather inside the data-src
one, so I used the code below to grab the data:
figure.selectFirst("img").attr("data-src")
Now that we have our models filled with the parsed data, we can render them with Jetpack Compose. The idea is to cycle through the Components
we declared, and based on the type detected, the logic will create a different Composable
to show the data:
After the logic cycles through the Components
, it uses their type to define how to render them using a combination of when
and smart casts:
As we mentioned earlier in this post, by defining two types for the Title
and the Header
, it’s super easy to give to each element its style, dimensions and padding.
Possible extensions
This post only covers the basics that I implemented to test, but there are some other things related to this subject that can be explored: adding links to paragraphs whenever they are detected, highlighting and selecting text, adding more components. These are just few ideas, but it’s fantastic to see how much easier it just became to create highly dynamic UIs with Jetpack Compose.
Conclusion
The code showed in the post is not deep nor complete (it can be found on GitHub), but it gives us a clear picture of what Jetpack Compose can bring to the table: creating such a project would have been painful with the View
system (although doable), but it was pleasantly fast with the new UI framework. At the time of writing, Compose just reached the API stable milestone, and it’s really the right time to explore it.
Huge thanks to Daniele Bonaldo, Ramona Harrison, and Omar Miatello for proofreading this post.