Rich text battle: TextView vs WebView
An analysis of using TextView versus WebView to display HTML content

 Posted on Feb 28, 2016 by Ha Duy Trung

So, say we have a piece of rich text, most probably in the form of HTML (e.g. a simplified HTML made for mobile reading), what kind of widget is best used to display it? How do we achieve the flexible yet rich reading experience from popular mobile readers like Readability, Pocket?

In this article, we will explore two very popular and powerful widgets that has been available since API 1: TextView and WebView, for the purpose of rich text rendering. The article does not aim to provide a comprehensive comparison, but rather touches on several critical desicion making points.

For the sake of comparison, let’s assume that we are given the task of displaying the following image-intensive article, styled to specific background color, text size and color. We will first try to use available APIs in TextView and WebView to render the given HTML in the same, comparable way, then analyze their performance: memory, GPU and CPU consumption.

The basics

First let’s quickly get through the basics. By the way they are named, it is quite obvious that TextView is meant for text rendering, while WebView is for webpage rendering. With proper use of their APIs however, we can quickly turn both of them into flexible widgets that can render rich text with images.

TextView

TextVew, with default support for Spanned, an interface for markup text, allows very fine-grained format options over any range of text. It also exposes a bunch of styling attributes, e.g. textAppearance, and APIs for controlling text appearance. Check out Flavien Laurent’s and Chiu-Ki Chan’s excellent materials on advanced uses of TextView and Spanned.

When it comes to rich text however, TextView shows certain limitations that we may want to weigh up before considering using it: it only handles a limited set of HTML tags, which should be sufficient in most cases; and we have to handle fetching embedded remote images by ourselves, via an ImageGetter instance; and intercept hyperlinks by using LinkMovementMethod. Whew!

Toggle code

textView.setMovementMethod(new LinkMovementMethod());
textView.setText(Html.fromHtml("<p>Hello World!</p>",
        new PicassoImageGetter(textView), null));

class PicassoImageGetter implements Html.ImageGetter {
    private final TextView mTextView;
    ...
    @Override
    public Drawable getDrawable(String source) {
        URLDrawable d = new URLDrawable(mTextView.getResources(), null);
        new LoadFromUriAsyncTask(mTextView, d)
                .execute(Uri.parse(source));
        return d;
    }
}
class LoadFromUriAsyncTask extends AsyncTask<Uri, Void, Bitmap> {
    private final WeakReference<TextView> mTextViewRef;
    private final URLDrawable mUrlDrawable;
    ...
    @Override
    protected Bitmap doInBackground(Uri... params) {
        try {
            return Picasso.with(mTextViewRef.get().getContext())
                    .load(params[0]).get();
        } catch (IOException e) {
            return null;
        }
    }

    @Override
    protected void onPostExecute(Bitmap result) {
        ...
        TextView textView = mTextViewRef.get();
        mUrlDrawable.mDrawable = new BitmapDrawable(
                textView.getResources(), result);
        ...
        textView.setText(textView.getText());
    }
}
class URLDrawable extends BitmapDrawable {
    private Drawable mDrawable;
    ...
    @Override
    public void draw(Canvas canvas) {
        if(mDrawable != null) {
            mDrawable.draw(canvas);
        }
    }
}

WebView

Meant for HTML display, WebView supports most HTML tags out of the box. We already know how to use a WebView to load a remote webpage with WebView.loadUrl(), but it can also load a local webpage as well: by wrapping HTML string inside a <body> block and loading it via WebView.loadDataWithBaseURL(), where base URL is null. WebView supports zooming (need to enable), and handles images and hyperlinks by default (of course!).

Toggle code

webView.getSettings().setBuiltInZoomControls(true); // optional
webView.loadDataWithBaseURL(null,
        wrapHtml(webView.getContext(), "<p>Hello World!</p>"),
        "text/html", "UTF-8", null);

private String wrapHtml(Context context, String html) {
    return context.getString(R.string.html, html);
}
<resources>
    <string name="html" translatable="false">
        <![CDATA[
        <html>
            <head></head>
            <body>%1$s</body>
        </html>"
        ]]>
    </string>
</resources>

Styling

Now let’s try to style TextView and WebView to render the example article on a teal theme, with some standard paddings around it. We can see that both of them are capable of rendering the page in pretty much the same way, with the exception of TextView ignoring the <hr /> tag it cannot handle.

Styling: TextView (left) vs WebView (right)

While TextView provides many attributes and APIs out of the box for styling, WebView does not provide public APIs for styling its HTML content. However with some basic CSS knowledge, one can instrument given HTML with CSS styles and achieve desired styling as above. We need to be careful on the conversion from CSS metrics to Android metrics though.

Toggle code

webView.loadDataWithBaseURL(null,
                wrapHtml(webView.getContext(),
                        "<p>Hello World!</p>",
                        android.R.attr.textColorTertiary,
                        android.R.attr.textColorLink,
                        getResources().getDimension(R.dimen.text_size),
                        getResources().getDimension(R.dimen.activity_horizontal_margin)),
                "text/html", "UTF-8", null);

private String wrapHtml(Context context, String html,
                        @AttrRes int textColor,
                        @AttrRes int linkColor,
                        float textSize,
                        float margin) {
    return context.getString(R.string.html,
            html,
            toHtmlColor(context, textColor),
            toHtmlColor(context, linkColor),
            toHtmlPx(context, textSize),
            toHtmlPx(context, margin));
}

private String toHtmlColor(Context context, @AttrRes int colorAttr) {
    return String.format("%06X", 0xFFFFFF &
            ContextCompat.getColor(context, getIdRes(context, colorAttr)));
}

private float toHtmlPx(Context context, float dimen) {
    return dimen / context.getResources().getDisplayMetrics().density;
}

@IdRes
private int getIdRes(Context context, @AttrRes int attrRes) {
    TypedArray ta = context.getTheme().obtainStyledAttributes(new int[]{attrRes});
    int resId = ta.getResourceId(0, 0);
    ta.recycle();
    return resId;
}
<resources>
    <string name="html" translatable="false">
        <![CDATA[
        <html>
            <head>
                <style type="text/css">
                    body {
                        font-size: %4$fpx;
                        color: #%2$s;
                        margin: %5$fpx %5$fpx %5$fpx %5$fpx;
                    }
                    a {color: #%3$s;}
                    img {display: inline; height: auto; max-width: 100%%;}
                    pre {white-space: pre-wrap;}
                    iframe {width: 90vw; height: 50.625vw;} /* 16:9 */
                </style>
            </head>
            <body>%1$s</body>
            </html>"
        ]]>
    </string>
</resources>

Performance

Using the above techniques to style TextView and WebView to display the sample HTML content yields the following performance statistics.

Performance: TextView (left) vs WebView (right) (click for full size)

As seen from the performance monitor screenshots, TextView consumes significantly more memory than WebView, as it needs to hold bitmaps from all loaded images once they are fetched, regardless of whether they are visible on screen.

The example article has 7 images of various sizes with a combined file sizes of 2MB which would become bitmaps in memory. The amount of memory needed depends on how we sample or resize the fetched images, but all of them will need to be in memory at the same time regardless. If we have an article which holds an arbitrary number of images, we may run into the dreaded OutOfMemoryException very quickly. Thus for this use case, TextView is a clear no go.

On the other hand, WebView historically has been optimized to be memory efficient. Under the hood (highly recommended read!), it loads content into tiles visible on screen and recycles them as we scroll, resulting in incredibly low memory profile. However, it needs to use more GPU and CPU to process and draw those tiles into pixels on the fly, probably explaining why it consumes more GPU and CPU than TextView.

So the trade-off here is between memory versus CPU & GPU consumption.

Rendering

Applying above techniques to style WebView or handle images in TextView does not come for free. By ‘preprocessing’ our content for rendering, we inherently add certain inital delay to our user experience. This initial delay may vary depending on devices. On high end devices it may not be noticeable, but we all know how many low end Android devices are around! Using a progress indicator when applicable would surely smoothen the experience.

Another side effect of WebView is that users may see some pixelated effects while scrolling as the tiles are recycled to render new content.

Bottom line and gotchas

Both widgets have its highs and lows when it comes to rich text rendering. For arbitrary HTML, my choice would be to favor WebView over TextView for its low memory consumption and native support for HTML content. Basic HTML and CSS knowledge would be needed, but knowing them will benefit you anyway. If the HTML content is known to be limited to certain tags without images (e.g. forum posts), TextView would be a more sensible choice.

The article explores a simple layout where both widgets occupy the whole screen. Things may change in a much more complicated way when we place them in a layout hierarchy together with other widgets. Try it out and profile to see what works for you!

Concurrent Android UI automation with Jenkins
Using multiple Android devices to execute concurrent Calabash Android automation tests via Jenkins CI

 Posted on Apr 15, 2015 by Ha Duy Trung

PropertyGuru is a regional company, serving multiple markets in Asia. To provide a consistent but local user experience for all our markets, many of our products share the same UI logic with variants, or flavors. As the product grows, or more variants are introduced for new market, the time it takes to test all those variants increases propotionally with the number of variants. Even with the support of UI automation frameworks, such as Calabash Android, an original daily automation suite of 2 hours can quickly grow into a staggering 8-hour job for 4 product variants.

This blog post introduces an approach to drastically reduce automation time in such cases, by concurrently executing UI automation for different product variants via a Continuous Integration (CI) server. We choose to run UI automation tests on actual devices rather than emulators for several reasons:

Even though the blog uses Calabash Android automation framework and Jenkins CI, the general approach can be applied to any automation tests executing via Android Debug Bridge (ADB) on any CI server.

Set up Jenkins slaves and slave group

Jenkins slave is the first thing one needs to set up to prepare for distributed builds, the process of delegating the build workload to multiple ‘slaves’. One can configure Jenkins slaves/nodes by navigating through Jenkins -> Manage Jenkins -> Manage Nodes.

By definition, a Jenkins slave is a computer that is set up to offload build tasks from the main computer that hosts Jenkins. In our case, the build task is automation test, while the ‘computer’ that runs it is a physical Android device. For this, all the slaves can be running on the same machine that have physical Android devices plugged in via USB cables, which allows ADB commands. The master Jenkins communicates with these Android agents via this machine.

Below is our setup for a slave connecting to a Samsung S3.

As all slaves run on the same machine, it is recommended that different file system roots are used for different slave nodes, as they will be likely where the test results are stored. Having them all point to the same root may result in test results being overriden by different nodes. This can be set via Remote FS root configuration.

Remote FS root

A slave needs to have a directory dedicated to Jenkins. Specify the absolute path of this work directory on the slave, such as '/var/jenkins' or 'c:\jenkins'. This should be a path local to the slave machine. There's no need for this path to be visible from the master, under normal circumstances.

Slaves do not maintain important data (other than active workspaces of projects last built on it), so you can possibly set the slave workspace to a temporary directory. The only downside of doing this is that you may lose the up-to-date workspace if the slave is turned off.

While separate FS roots and multi-processors allow concurrent Calabash executions, ADB_DEVICE_ARG environment variable is needed to instruct Calabash which device it should send ADB commands to, in case of multiple connected devices. Under the hood, Calabash automates UI via ADB commands. The value for this environment variable can be found via adb devices command. As one device maps directly to one slave node, ADB_DEVICE_ARG should be set at node level via ‘Environment variables’ configuration, which will be then available at job level.

Environment variables

These key-value pairs apply for every build on this node and override any global values. They can be used in Jenkins' configuration (as $key or ${key}) and be will added to the environment for processes launched from the build.

Similar setup for a slave connecting to Nexus 4.

If you notice, all the slave nodes are configured with the same label ‘android-group’. This serves the purpose of grouping them so that when needed, we can conveniently summon all nodes under a specific group.

Labels

Labels (AKA tags) are used for grouping multiple slaves into one logical group. Use spaces between each label. For instance 'regression java6' will assign a node the labels 'regression' and 'java6'.

For example, if you have multiple Windows slaves and you have jobs that require Windows, then you can configure all your Windows slaves to have the label 'windows', then tie the job to the 'windows' label. This allows your job to run on any of your Windows slaves but not on anywhere else.

Now if we check ‘android-group’, we can see all the node tagged with label ‘android-group’ listed here. Adding another device to this group is as easy as copying an existing node and updating its ADB_DEVICE_ARG.

Set up concurrent job using slave group

With our Jenkins slaves and slave group in place, we can now set up a Jenkins job executing Calabash Android using the connected devices, via their respective slaves, represented by the group label ‘android-group’.

By checking both ‘Execute concurrent builds if necessary’ and ‘Restrict where this project can be run’, we allow the job to be executed concurrently, subject to node availability, using only nodes tagged with ‘android-group’. The restriction is to ensure that only nodes with connected devices are used for automation, as we may have other nodes with no connected devices used for other purposes.

Execute concurrent builds if necessary

If this option is checked, Jenkins will schedule and execute multiple builds concurrently (provided that you have sufficient executors and incoming build requests.) This is useful on builds and test jobs that take a long time ... It is also very useful with parameterized builds, whose individual executions are independent from each other...

Now if we have 4 requests for automation, and 3 connected devices, it will take 2 rounds for them to clear all requests (the first round all 3 devices are utilized, 1 request is queued and picked up whenever a device becomes available again). This means that we can reduce the example automation time for 4 product flavors from 8 hours to 4 hours, or even less if 1 or more devices are added.

Trigger concurrent downstream jobs via upstream job

With the setup so far, we are already capable of manually triggering concurrent automations using as many connected devices as we want (subject to available USB ports!). But why manual trigger, if we can go all the way? Using ‘Jenkins Parameterized Trigger Plugin’, one can set up an upstream job to trigger multiple downstream jobs with parameters.

For example, if we want to test multiple app flavors concurrently using all available devices, set up an upstream job which executes the following:

The above configuration will trigger 3 concurrent ‘MOBILE_TEST’ downstream jobs (configured to ‘Execute concurrent builds if necessary’), each will test a specific app flavor ‘cherry’, ‘tomato’ and ‘raspberry’, sent to downstream job via a parameter called flavor. The upstream job will be blocked and wait for all downstream jobs to complete to set build status accordingly.

It is recommended that this upstream job runs on a different node/group than its downstream job, as otherwise it will occupy an available device without using it.

Now let’s recycle all those used devices and put their final miles to good use!