This is part 2 of a series on Mobile app development using Webview. Want to read more? Here ya go:
- Part 1: Mobile App Development Strategy Overview
- Part 2: Android App Code Examples (YOU ARE HERE)
- Part 3: iPhone/IOS Code Examples (stay tuned)
Android App Development: Introduction
In the first part of this article, we discussed the design for a pretty standard app. In this article, we will discuss how you can implement an easy Android App using Webview for Development. Hopefully, this article will help you with your own mobile app.
- What this article covers: Basic functionality that most all WebView apps may implement around the WebView. This is the boilerplate code written in a device’s native language.
- Not covered: Anything related to your content, design, or concept. Anything that appears inside the WebView, coded with Javascript, HTML, and CSS. That’s up to you.
This article will focus on Java code for an Android WebView application.
We will create an app that uses a WebView to “frame” in the Human Element website. Remember, this is just a “boiler-plate” template for any app; you could substitute the Human Element url with ANY url to create an entirely different app. For example, if your app needs to present internal page content instead of any old url out there on the internet, then YES, you can do that instead (I discuss where your web files can live in the first part of this article). The example in this article uses centralized hosting, so all the web files live at redesign-human-element.ws.hedev.io.
Giving away secrets?
In a golden age of owning “Intellectual Property,” why would we give away “secrets?” It took me a long time to put together this code and get it to work properly, but now, you can copy and paste it in just a few minutes. What’s in it for me?
There will always be the next barrier that must be overcome, ALWAYS. But when solutions are shared, we all progress FASTER because we can all benefit from someone else’s progress. Teach a man how to fish and one day he may teach you how to fish better… or discover the next, new, better alternative. So by helping you, essentially I’m helping myself.
This philosophy probably seems moronic when you’re reclining in your personal jet air-liner, miles above reality. But whether you like it or not, eventually, you have to depend on someone else. You can’t help but live in the same world with everyone. I prefer to live with educated, happy people like me. I’ll share these “secrets” so maybe we can progress together. That’s up to you. But here’s one of those opportunities.
On this page
Complete code examples (5 files)
And now, the main attraction. If you are trying to build your own app, then this is what you came here for. Who cares about this paragraph, just give me the code samples. I know how it is. That’s why I included COMPLETE examples, instead of just bits and pieces.
If you copy the following code into your own application, then you should have a working app (so you can focus more on building your content inside the WebView). I tested this code on a real android phone, and it works… for me, at least.
MainActivity.java
This is where most of your code logic will go. This is the only Java file in this example. The other files are all XML configuration files:
Events
Standard Android events: You can write logic into these events to override what happens when they get triggered.
- onCreate Entry point for the application
- onBackPressed Fires when the user hits the back button on their device
- onConfigurationChanged Fires when the screen orientation changes, eg: vertical to horizontal…
- onSaveInstanceState Fires before the current activity is placed in a background state
- onCreateOptionsMenu Fires when the options menu is created
- onOptionsItemSelected Fires when the user presses one of the menu options
Methods
Here are some methods that I defined for this boiler-plate app:
- isFileUrl(String url, String extension) Returns true if url ends with extension. Otherwise, returns false
- handleExternalDeviceActivity(String url) Opens the url OUTSIDE of the app, in a separate activity. For example, inside a separate browser app.
- hasInternetConnection(Context context) Returns true if an internet connection is available
- noInternetMessage() Displays an alert popup to tell the user there is no internet connection
- buildView(final Bundle savedInstanceState) Setup the main WebView to display the primary app url
- buildExternalView(String externalUrl) Setup the external WebView to display some url, outside of the app’s domain
- closeExternalView() Close the external WebView so the user sees the primary WebView again (return to the main app)
Fields
I defined some “module-level” fields. “module-level” is a an old text book term that means the variable can be accessed anywhere within the current class. That “m” before the field name stands for “module-level” and helps me keep track of these types of variables throughout the project.
- mViewUrl The primary url where your main app’s web files are located
- mViewQs Any query string parameters you want to attach to your mViewUrl. I set “d=android” so that I can send the device type to the web files. For example, “d=android” or “d=ios”.
- mNochaceQs When the app starts, a “nocache” value is appended to the mViewUrl. This helps prevent the app content from being cached if dynamic content changes. I have the “nocache” value change every hour.
- mWebView The primary WebView object
- mExternalView The secondary (external) WebView object
- mActionBar The action bar object. The action bar only appears when the external WebView is open. Then main WebView doesn’t have a visible action bar
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
|
package com.example.helloworld; import java.util.Calendar; import java.util.Date; import android.support.v7.app.ActionBarActivity; import android.app.AlertDialog; import android.app.ProgressDialog; import android.content.Context; import android.content.DialogInterface; import android.content.DialogInterface.OnCancelListener; import android.content.Intent; import android.content.res.Configuration; import android.graphics.drawable.ColorDrawable; import android.net.ConnectivityManager; import android.net.Uri; import android.os.Bundle; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.webkit.WebBackForwardList; import android.webkit.WebSettings; import android.webkit.WebView; import android.webkit.WebViewClient; public class MainActivity extends ActionBarActivity { //fields private final String mViewQs = "?d=android" ; private final String mNochaceQs = "&nocache=" ; protected WebView mWebView; protected WebView mExternalView; protected android.support.v7.app.ActionBar mActionBar; //properties public String getViewUrl(){ return mViewUrl;} @Override protected void onCreate(Bundle savedInstanceState) { super .onCreate(savedInstanceState); //hide the action bar at the top of the screen mActionBar = getSupportActionBar(); mActionBar.hide(); mActionBar.setHomeButtonEnabled( true ); //define action bar color mActionBar.setBackgroundDrawable( new ColorDrawable( 0xff2d5b8e )); //2d5b8e //define action bar icon mActionBar.setIcon(R.drawable.ic_launcher); //set the main view setContentView(R.layout.activity_main); //build the main view buildView(savedInstanceState); } //find out if this URL is a link to a file (eg: PDF) by checking the extension at the end of the url private boolean isFileUrl(String url, String extension){ boolean isFile= false ; url=url.toLowerCase();extension=extension.toLowerCase(); //if contains extension if (url.contains(extension)){ //if also contains ? query string if (url.contains( "?" )){ //strip off query string url=url.substring( 0 ,url.indexOf( "?" )); } //if ends in file extension if (url.lastIndexOf(extension)==url.length()-extension.length()){ //then yep. This is a file isFile= true ; } } return isFile; } //pass any url to this function to auto-handle certain activities (eg: opening PDF documents or making phone calls) protected void handleExternalDeviceActivity(String url){ //pass this special URL to an external activity (outside of the app) Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); startActivity(intent); } //verify that an internet connection is available protected boolean hasInternetConnection(Context context) { boolean isConnected= false ; //if connectivity object is available ConnectivityManager con_manager=(ConnectivityManager)context.getSystemService(Context.CONNECTIVITY_SERVICE); if (con_manager.getActiveNetworkInfo() != null ){ //if network is available if (con_manager.getActiveNetworkInfo().isAvailable()){ //if connected if (con_manager.getActiveNetworkInfo().isConnected()){ //yep... there is connectivity isConnected= true ; } } } return isConnected; } //show something if there is no internet connectivity protected void noInternetMessage(){ //build an alert box AlertDialog.Builder alertbox = new AlertDialog.Builder( this ); // Set the message to display alertbox.setMessage(R.string.err_connection_summary); // Add a neutral button to the alert box and assign a click listener alertbox.setNeutralButton(R.string.btn_ok, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface arg0, int arg1) { //quit the app finish(); } }); alertbox.setOnCancelListener( new OnCancelListener() { @Override public void onCancel(DialogInterface arg0) { //quit the app finish(); } }); // show the alert box alertbox.show(); } protected void buildView( final Bundle savedInstanceState){ //if there is still internet connectivity if (hasInternetConnection( this )){ //get the webview object mWebView = ((WebView)findViewById(R.id.webview)); //if web view is NOT already initialized (don't re-init just because orientation changed) if (savedInstanceState== null ){ //ALLOW JAVASCRIPT TO RUN INSIDE THE WEBVIEW //========================================== WebSettings webSettings = mWebView.getSettings(); webSettings.setJavaScriptEnabled( true ); //INIT LOADING INDICATOR OBJECT //============================= final ProgressDialog pd = ProgressDialog.show( this , "" , "Loading..." , true ); //KEY WEBVIEW EVENTS (OVERRIDES) //============================== mWebView.setWebViewClient( new WebViewClient() { /*@Override public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) { }*/ @Override public boolean shouldOverrideUrlLoading(WebView view, String url) { //override webview url loading (distinguish between internal and external urls) boolean ret = true ; //if there is still internet connectivity if (hasInternetConnection(view.getContext())){ //if the new url is on the same webview page if (url.indexOf(mViewUrl)== 0 ) { //load the new page inside the webview NOT another browser activity ret = false ; } else { //the new url is NOT on a webview page... //if the url ends in .pdf... (PDF files don't open inside webviews) if (isFileUrl(url, ".pdf" )){ //launch an external Activity that handles URLs, such as a browser application handleExternalDeviceActivity(url); } else { //not a PDF url... //if this is a telephone number url (starts with "tel:") if (url.indexOf( "tel:" )== 0 ){ //launch an external Activity that handles telephone numbers to make a call handleExternalDeviceActivity(url); } else { //if this is a mailto link if (url.indexOf( "mailto:" )== 0 ){ //launch an external Activity that handles email mailto links handleExternalDeviceActivity(url); } else { //try to load the external url in the alternate webview buildExternalView(url); } } } } } else { //no internet connectivity... noInternetMessage(); } return ret; } @Override public void onPageFinished(WebView view, String url) { //hide the loading indicator when the webview is fully loaded if (pd.isShowing()&&pd!= null ){ pd.dismiss(); //clear the webview cache super .onPageFinished(view, url); view.clearCache( true ); } } }); //LOAD THE APP PAGE INTO THE WEB VIEW //=================================== Calendar now = Calendar.getInstance(); Date date = new Date(); now.setTime(date); String nocache = now.get(Calendar.YEAR) + "_" + now.get(Calendar.MONTH) + "_" + now.get(Calendar.DAY_OF_MONTH) + "_" + now.get(Calendar.HOUR_OF_DAY) + "_" + now.get(Calendar.MINUTE) + "_" + now.get(Calendar.SECOND); mWebView.loadUrl(mViewUrl + mViewQs + mNochaceQs + nocache); } else { //RESTORE THE INSTANCE STATE //========================== mWebView.restoreState(savedInstanceState); } } else { //no internet connection... noInternetMessage(); } } protected void buildExternalView(String externalUrl){ //if web view is NOT already initialized (don't re-init just because orientation changed) if (mExternalView== null ){ //get the webview object mExternalView = ((WebView)findViewById(R.id.externalView)); //ALLOW JAVASCRIPT TO RUN INSIDE THE WEBVIEW //========================================== WebSettings webSettings = mExternalView.getSettings(); webSettings.setJavaScriptEnabled( true ); //INIT LOADING INDICATOR OBJECT //============================= final ProgressDialog pd = ProgressDialog.show( this , "" , "Loading More..." , true ); //KEY WEBVIEW EVENTS (OVERRIDES) //============================== mExternalView.setWebViewClient( new WebViewClient() { @Override public boolean shouldOverrideUrlLoading(WebView view, String url) { //override webview url loading (distinguish between internal and external urls) boolean ret = true ; //if the url ends in .pdf... (PDF files don't open inside webviews) if (isFileUrl(url, ".pdf" )){ //launch an external Activity that handles URLs, such as a browser application handleExternalDeviceActivity(url); } else { //if this is a telephone number url (starts with "tel:") if (url.indexOf( "tel:" )== 0 ){ //launch an external Activity that handles telephone numbers to make a call handleExternalDeviceActivity(url); } else { //if this is a mailto link if (url.indexOf( "mailto:" )== 0 ){ //launch an external Activity that handles email mailto links handleExternalDeviceActivity(url); } else { //just handle this url inside this same web view ret= false ; } } } return ret; } @Override public void onPageFinished(WebView view, String url) { //hide the loading indicator when the webview is fully loaded if (pd.isShowing()&&pd!= null ){ pd.dismiss(); //clear the webview cache super .onPageFinished(view, url); view.clearCache( true ); //try to make this webview visible view.setVisibility(View.VISIBLE); view.bringToFront(); //show the action bar at the top of the screen mActionBar.show(); } //set the new page title setTitle(view.getTitle()); } }); } else { //mExternalView already created... } //LOAD THE EXTERNAL PAGE INTO THE WEB VIEW //======================================== mExternalView.loadUrl(externalUrl); } //close the external webview and go back to the main app webview private void closeExternalView(){ //show the main webview mWebView.bringToFront(); //clear browse history from external web view mExternalView.loadUrl( "about:blank" ); mExternalView.clearHistory(); mExternalView.clearCache( true ); //hide the externalView and the action bar mExternalView.setVisibility(View.GONE); mExternalView = null ; mActionBar.hide(); } @Override public void onBackPressed() { //if NOT in the external view mode if (mExternalView== null ){ if (mWebView.canGoBack()){ //the back button should go to previous webview page instead of close the app mWebView.goBack(); } } else { //in the external view mode... //if there is a previous external page if (mExternalView.canGoBack()){ //get the last url WebBackForwardList backForwardList = mExternalView.copyBackForwardList(); String historyUrl = backForwardList.getItemAtIndex(backForwardList.getCurrentIndex()- 1 ).getUrl(); //if NOT going back to about:blank (about:blank was inserted into the list at the point at which the external view was last closed) if (!historyUrl.equals( "about:blank" )){ //the back button should go to previous page instead of doing nothing mExternalView.goBack(); } else { //instead of going back to about:blank, go back to the main view closeExternalView(); } } else { //there is no previous external page... //close the external view and return to the main app view closeExternalView(); } } } @Override public void onConfigurationChanged(Configuration newConfig){ super .onConfigurationChanged(newConfig); } @Override protected void onSaveInstanceState(Bundle outState){ //save the state of the WebView mWebView.saveState(outState); } @Override public boolean onCreateOptionsMenu(Menu menu) { // Inflate the menu; this adds items to the action bar if it is present. getMenuInflater().inflate(R.menu.main, menu); return true ; } @Override public boolean onOptionsItemSelected(MenuItem item) { //handle action bar event options switch (item.getItemId()) { //if the main app icon was clicked in the action bar (while viewing an external webview page) case android.R.id.home: //switch back to the main view and exit the external view... closeExternalView(); return true ; //if the drop down "Return" item was clicked case R.id.action_returnapp: //switch back to the main view and exit the external view... closeExternalView(); return true ; //if the user clicked "Open in Browser" case R.id.action_openinbrowser: //open the external view in a real browser outside of the app handleExternalDeviceActivity(mExternalView.getUrl()); //close the external view and return to the main app view closeExternalView(); return true ; default : return super .onOptionsItemSelected(item); } } } |
AndroidManifest.xml
This is where you define your app’s key package identifier. I have the com.example.helloworld package name. But obviously, you will have something different.
Your app may be on its first release. If so, then your versionCode will probably be “1” and your versionName could be something like “alpha“, “lolly pop“, “1“, or some other name that you choose.
Also, you must tell the applications what permissions get used. At minimum, you will need two:
- INTERNET If you are accessing ANY external urls, then you need to enable this permission.
- ACCESS_NETWORK_STATE If you need to detect if the device HAS access to internet, then you also need this permission. My hasInternetConnection method depends on being able to detect connectivity.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
<? xml version = "1.0" encoding = "utf-8" ?> package = "com.example.helloworld" android:versionCode = "8" android:versionName = "1.7" > < uses-sdk android:minSdkVersion = "8" android:targetSdkVersion = "21" /> < uses-permission android:name = "android.permission.INTERNET" /> < uses-permission android:name = "android.permission.ACCESS_NETWORK_STATE" /> < application android:allowBackup = "true" android:icon = "@drawable/ic_launcher" android:label = "@string/app_name" android:theme = "@style/AppTheme" > < activity android:name = ".MainActivity" android:label = "@string/app_name" android:configChanges = "orientation|screenSize" > < intent-filter > < action android:name = "android.intent.action.MAIN" /> < category android:name = "android.intent.category.LAUNCHER" /> </ intent-filter > </ activity > </ application > </ manifest > |
res/menu/main.xml
Two menu items are defined in this file. These menu items appear in the mActionBar.
Remember, this action bar is only visible IF the external web view is open.
The purpose of these two menu items is:
- Return Go back to the main app and hide the external web view.
- Open in Browser Open the current external view into a browser (outside of the Hello World app).
You can see how the app detects when these menu items get pressed AND carries out their actions: Check out the onOptionsItemSelected event, that handles option item presses in MainActivity.java
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
|
tools:context = "com.example.helloworld.MainActivity" > < item android:id = "@+id/action_openinbrowser" android:orderInCategory = "100" android:title = "@string/action_openinbrowser" app:showAsAction = "never" /> < item android:id = "@+id/action_returnapp" android:orderInCategory = "200" android:title = "@string/action_returnapp" app:showAsAction = "never" /> </ menu > |
res/values/strings.xml
Are you familiar with the concept of localization? This is a strategy for defining all of your text values in one file so that this file can be substituted with alternate language files. Android localizes some of its string values inside a file aptly named, strings.xml. You can see string definitions for some of my app’s strings here.
01
02
03
04
05
06
07
08
09
10
11
12
|
<? xml version = "1.0" encoding = "utf-8" ?> < resources > < string name = "app_name" >Hello World</ string > < string name = "hello_world" >Hello world!</ string > < string name = "action_settings" >Settings</ string > < string name = "action_returnapp" >Return</ string > < string name = "btn_ok" >Ok</ string > < string name = "action_openinbrowser" >Open in Browser</ string > < string name = "err_connection_summary" >Having trouble reaching Hello World. Please check your internet connection then restart the app.</ string > </ resources > |
res/layout/activity_main.xml
Here, I define both of my WebViews, primary and external. I also set the dimensions of the WebViews. I want them both to fill the screen ENTIRELY, so I set them both to fill_parent. I also define their ID’s which I use to grab them inside the MainActivity.java file.
Note: If you work inside Eclipse, you may see the xmlns:android attribute cause XML syntax errors. But you can easily remove the error if you you go to Project > Clean in Eclipse.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
|
android:layout_width = "match_parent" android:layout_height = "match_parent" android:paddingBottom = "@dimen/activity_vertical_margin" android:paddingLeft = "@dimen/activity_horizontal_margin" android:paddingRight = "@dimen/activity_horizontal_margin" android:paddingTop = "@dimen/activity_vertical_margin" tools:context = "com.example.helloworld.MainActivity" > android:id = "@+id/externalView" android:layout_width = "fill_parent" android:layout_height = "fill_parent" /> android:id = "@+id/webview" android:layout_width = "fill_parent" android:layout_height = "fill_parent" /> </ RelativeLayout > |
That’s all for now. Hopefully, this article was a good launching point for your Android app. Stay tuned for a similar article for iOS apps.