Titanium Mobile Synchronous Network Request Workaround
While using Appcelerator’s Titanium Mobile for a Mobile App I am developing in partnership with a local company in Tulsa, OK I found that there is no support for synchronous network requests. This can be trouble some if you are wanting to follow DRY (Don’t Repeat Yourself) coding and using a JavaScript based framework/library in your app. I found a solution that works for the app I am developing and wanted to share with other developers.
Note: This code is written for Titanium Mobile 0.9.x.
What is the issue?
Here is an example of a typical XHR request based on the KitchenSink example application:
1 2 3 4 5 6 7 8 | var xhr = Ti.Network.createHTTPClient() xhr.onload = function () { Ti.API.log('I should be first'); }; // xhr.open(METHOD, URL, ASYNC_Request(BOOL)); xhr.open('POST', "http:example.com/getData", false); xhr.send(); Ti.API.log('I should be second') |
The result in Titanium’s Log is:
'I should be second' 'I should be first'
Notice that the “I should be second” is called before the “I should be first” is called. This is due to the asynchronous request not stopping the script from running while a remote request is being processed. This stops the ability to create a general app-wide function that processes your Network request since it will finish and return the function before the XHR request data has been returned.
This requires a lot of copy and pasting of your XHR code for every request you make which if you have a change to make you are going to be spending quite a bit of time searching through your code to fix a problem.
What are our needs?
For our app we have a “common.js” file that has all regularly used functions in it. This allows us to follow the DRY method and limit the lines of code written. Since all data for our app is coming from a remote API call for every function in the app we will be making a remote call, processing the data and displaying the results to the user.
When making a XHR request we have the following needs for each request made:
- Set the current API URL
- Set the headers to get JSON data from the API
- Set the headers for Authorization
- If an error occurs either force a login or display error notification to the user
- Process the data – returned in JSON string so it needs to be converted to JavaScript Object
- Return processed data to the original js function call
What is the workaround?
Simply put: Callback function.
First step create a common.js file that contains the wrapper function for XHR requests. Note: we ran into issues including this in the app.js so a separate include file seems to do the trick best. I included some extra functions we use to show the portability we have been trying to achieve.
1 2 3 4 5 6 7 8 9 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 | var apiServerUrl = "http://example.com/api/"; var apiKey = "&apikey=demoApp"; /** * Create new Object (mainly for name spacing purposes */ var myFunctions = {} /** * create a full URL with server, path and API key */ myFunctions.prepUrl = function(path) { return apiServerUrl+path+apiKey; } /** * reusable XHR function * method: GET / POST * path: local/ads * data: object * callBack: function to call when request is complete * failMsg: array to pass to myFunctions.notice when request fails for Alert */ myFunctions.remoteRequest = function(method,path,data,callBack,failMsg) { json = false; // create new Network Client xhr = Ti.Network.createHTTPClient(); // get the full url to API server url = myFunctions.prepUrl(path); xhr.onload = function(e) { json = myFunctions.jsonParse(e.responseText); // // this is the function to call in the main javascript file // callBack(json); return true; }; xhr.onerror = function(e) { myFunctions.networkError(xhr.status, url, failMsg); return false; }; xhr.open(method,url); xhr.setRequestHeader("contentType","application/json; charset=utf-8"); // set authentication username = Ti.App.Properties.getString('username'); password = Ti.App.Properties.getString('password'); xhr.setRequestHeader('Authorization','Basic '+Ti.Utils.base64encode(username+':'+password)); if (data == undefined) { data = false; } xhr.send(data); } /** * Parse JSON data - could be replaced with JSON2.js library */ myFunctions.jsonParse = function(json) { return eval('(' + json + ')'); } /** * display alert to user */ myFunctions.notice = function (msg,title) { Ti.UI.createAlertDialog({ title: title, message: msg }).show(); } |
The key to the above is the callBack function parameter that is passed to the networkRequest function and call in line 41.
Now for the login.js file that Titanium is using to display the current window modal view to login a user.
1 2 3 4 5 6 7 8 9 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 | Ti.include("common.js"); var win = Ti.UI.currentWindow; win.setBackgroundColor('#fff'); win.title = "Login"; var actInd = Ti.UI.createActivityIndicator({ bottom:10, height:50, width:10, style:Ti.UI.iPhone.ActivityIndicatorStyle.BIG }); var u = Ti.UI.createTextField({ id:'username', value:'', color:'#336699', returnKeyType:Ti.UI.RETURNKEY_NEXT, keyboardType:Ti.UI.KEYBOARD_ASCII, hintText:'Email Address', height:40, clearOnEdit:false, fontSize:20, borderStyle:Ti.UI.INPUT_BORDERSTYLE_ROUNDED, clearButtonMode:Ti.UI.INPUT_BUTTONMODE_ALWAYS, top:40, width:250 }); win.add(u); u.addEventListener('return',function(e) { p.focus(); }); var p = Ti.UI.createTextField({ id:'password', value:'', color:'#336699', returnKeyType:Ti.UI.RETURNKEY_GO, keyboardType:Ti.UI.KEYBOARD_ASCII, hintText:'Password', height:40, passwordMask: true, clearOnEdit:true, fontSize:20, borderStyle:Ti.UI.INPUT_BORDERSTYLE_ROUNDED, clearButtonMode:Ti.UI.INPUT_BUTTONMODE_ALWAYS, top:90, width:250 }); win.add(p); var submit_button = Ti.UI.createButton({ id:'submit_button', title:'Login', color:'#336699', height:32, width:100, fontSize:12, fontWeight:'bold', top:150 }); win.add(submit_button); var navActInd = Titanium.UI.createActivityIndicator(); win.setRightNavButton(navActInd); var label = Ti.UI.createLabel({ top:10, color:'#777', height:'auto', width:300, font:{ fontSize:15 } }); win.add(label); /** * This is where the networkRequest function is going to be used. */ submit_button.addEventListener('click',function(e) { u.blur(); p.blur(); if (p.value.length < 1 || u.value.length < 1) { myFunctions.notice("Please enter both username and password", "Login Error"); return; } navActInd.show(); failMsg = ["Username/Password Failed", "Login Error"]; r = myFunctions.remoteRequest("GET","user/verify?",false,callBack_login,failMsg) if (!r) { navActInd.hide(); return; } }); /** * Once the XHR request is complete this function will be called. You can put all your post data display events within this function. */ var callBack_login = function(data) { Ti.App.Properties.setString('username', u.value); Ti.App.Properties.setString('password', p.value); navActInd.hide(); Ti.UI.currentWindow.close(); Ti.UI.close(); } |
In line 103 we call the myFunctions.remoteRequest() function. Notice that the callBack_login does not include “’s.
So far this solution has been working great for our development. We hope to see Titanium Mobile support synchronous requests in the future, but our current modular development allows us make the switch quite easily.
I am open to any questions or comments that you may have about our solution.
