A client came to me with the question if I could wrap their web application into a native app. Now, of course there is a lot of things to say about this, I did advice them against it, but for reasons they still wanted it and so I started building.
The web application was already optimised to be responsive and worked well in all modern browsers across a variety of mobile devices. From that I got that it shouldn’t be too hard to just put it in a WebView and that is it, under the hood it would use the same render engine as the stock browsers, right? WRONG! There are differences between the browser you use and the one used in the WebView as we will learn in a little bit.
I must say, all in all, everything went very smooth, except for a few minor speed bumps. But those speed bumps did take some time to overcome. To prevent you from making the same mistakes – or Googling for them for hours, like I did – I will highlight some of them in this post. I have separated them into the specific platforms and under that the specific
Beware; while the app is Forms, there will be a lot of custom renderer magic going on in here. The first part takes place in my PCL, the Android, iOS and UWP parts are my platform projects.
Cross-platform hybrid app logic
Before I go over the platform details, let me go over some general logistics I had to implement to make sure everything ran smooth. This is done in the shared code and mainly has to do with navigation.
As you might imagine, you want to act upon certain URL’s in your app. For instance, we decided when a user downloaded a PDF we didn’t want to redirect them to the OS browser and show it, but we wanted to show it inside the app. Or when first loading the page, I didn’t want to show the blank white page, but show a custom loading spinner. To do this, I simply implemented the OnNavigating and OnNavigated events of the Xamarin.Forms WebView.
Disclaimer; like a few more things in this post this isn’t the most beautiful code I have written, but then again a hybrid app isn’t the most beautiful thing either. At least, that is what I tell myself as an excuse.
Here, I take the URL we are navigating to, convert it to lowercase to make it case insensitive and then I check for certain values and based on that value I act.
There is a couple of interesting things in here. First, look at the final check I do; if our URL is not from our web application, we open it in the OS browser. While the web application is perfectly responsive and all, there are one or two links that link to an associated portal, not optimised for mobile. These I wanted to opened outside our app, or in case of iOS in a SFSafariViewController. Because of this check, some third-party URLs that were loaded (like Vimeo video’s) were suddenly opened in the external browser as well! To prevent this, I added the first check, to just ignore these URLs. Then there is a check to open PDFs inside our app, which was another requirement.
Please note that the Navigating and Navigated events seem to differ in regard to when they are triggered between the different platforms. Although I didn’t get to put my finger on it exactly, there have been some minor tweaks I had to do for the different platforms.
Android specific logic
For Android there is the most work to do. All of the below code will be in the Android platform project, mostly in a custom renderer for the WebView.
Upload image through camera with a input type=”file” control
On iOS this works out-of-the-box, no problem. Whenever you click on a file upload control on the web application you will presented with a menu which lets you choose between the camera or the gallery (or even more). On Android however, you will only be presented with the gallery options, the camera is nowhere to be found. Luckily, we can add it ourselves, but it takes a little love.
First let us have a look at the web application side. Make sure you have your input element like this:
<input type="file" accept="image/*" capture="camera">
<!-- or this might work as well -->
<input type="file" accept="image/*;capture=camera">
Now we have to go over to our Android app and implement some magic here. In short we will need to detect when the user has tapped the input element in our page and add an extra intent to it. If you do not already have one, you need to create a custom renderer for the WebView on Android. Then we need to add our own WebChromeClient to pick up the event of a user tapping a upload control. We do this by acting on the FileChooser events.
In our inherited WebChromeClient we register a callback to our own action when the FileChooser is opened. To serve both Android 5 up and above, we will need two separate events as seen below.
To finish implementing this, you need to catch the result of this action by overriding the OnActivityResult method. This has to be done on the Activity that launched the FileChooser. When using traditional Xamarin this should be no problem. But since we are using Forms, we only have on Activity. So go over to the MainActivity and implement it like underneath.
This checks if the result code is the one we are expecting and then sets the result to either our taken image of the one chosen from the filesystem. This is then invoked on the FileChooserWebChromeClient we defined way above. It does not deserve a beauty price, but it gets the job done.
Another issue that took me some time is this one. Somewhere in the web application a view was shown with a rather small font-size, namely somewhere below 8. It is this view, that caused the page to look distorted. Everything went out of place and it didn’t look like it was supposed to.
A handy trick here is to know that you can inspect your DOM from a remote desktop. For Android (>= 4.4) you can use Chrome to do this very easily. In your app code add this line: WebView.SetWebContentsDebuggingEnabled(true);. You can see it in the code block right above this section.
Of course you should check if you are on a device that is running Android 4.4 at the least and only enable this for you to debug, so wrap it in a debug compiler directive for instance.
After you have done this, run your app, make sure you’re connected to your desktop somehow, and go into Chrome. Go the this URL: chrome://inspect and find your device then click ‘Inspect’. You can now go through it like you would any other tab in Chrome. Awesome!
More on remote debugging a WebView can be found in the Android documentation.
Back to the initial problem. After debugging it like this, I noticed that the font-size would not go below 8px. Even if I did set it to a smaller one, nothing would happen. After using my Google superpowers I got to the WebView (or actually WebSettings) documentation page and learned that it has a SetMinimumFontSize method. Why one would want a minimum font-size beats me, but it is there the possibility to set one. Looking for the Xamarin equivalent brought me to their documentation page, which states that the default value is 8px, so that was a big a-ha moment there. Setting it back to 1 allowed the view to return to it’s original styling.
Another issue regarding fonts had to do with text zoom. The built-in browser does not take in account the phone settings regarding the font size. However, the WebView browser engine does. So when we started testing the application, all of a sudden there was one device that showed everything blown up. This particular user had set the font to 125% which caused no change in his every day browser, but did give problems in our hybrid app.
This is very easy to overcome, we just need to set the text zoom to a fixed value of a 100%. This will then disregard any setting the phone might have. Do it like this; Control.Settings.TextZoom = 100;
In the initial version of the Android app performed very slow. As it turns out, there are a couple of options that you can play with to improve this. One of the most important ones is hardware acceleration. You can turn this on or off at any level: application, activity, window or even per (Web)View control. As of Android API 19 Google has introduced a new Chromium engine for rendering the WebView which performs better with hard ware acceleration enabled.
To do this you can use the following code;
To set it at application level, go into your androidmanifest.xml file and add an attribute to your application tag like so:
I will skip the other levels for now, you can probably figure out how to set those yourself.
Other settings that could speed up your hybrid application are: SetRenderPriority (set it to high) and some people report setting the CacheMode to NoCache also helped.
iOS & UWP
Personally, I am not a big fan of hybrid applications, that is why I chose Xamarin in the first place. But for this application it was the first logical step to take. Only a short time into the project I started to see why I don’t like hybrid apps; there is a lot of hacky code needed to make everything go fluently. I hope you save(d) some time by finding this blogpost and do not have to spend hours on Google and documentation pages to find the little details to make this work.