Skip to Content
Technical Articles
Author's profile photo Greg Cariño

Let’s build an Offline Hybrid UI5 Application from scratch – Part 1

One feature that I’ve never had the chance to deep-dive into is Offline OData. With the latest updates in Hybrid App Toolkit (HAT) on SAP Web IDE Full-Stack, the creation of offline apps has been made simpler. Read this great blog by Ludo Noens.

In this blog post, I’m going to show how simple it is to build an offline app from scratch using the HAT on Web IDE. I will be using Ludo’s blog as reference so I would recommend you to go through his post first if you haven’t done so.

I’m going to assume you have basic UI5 knowledge, activated the required cloud platform services, configured the sample cloud platform destination and activated the HAT feature as per Ludo’s blog. One change that I’ll make, though, is change the destination URL such that it points to the demo SAP mobile platform instead of directly to the ESPM sample service.

URL:  https://hcpms-<your account number>trial.hanatrial.ondemand.com instead of https://hcpms-<your account number>trial.hanatrial.ondemand.com//SampleServices/ESPM.svc

Feel free to use any destination if you already have a running OData service somewhere but ensure you replace the relevant property values in the examples below.

 

Ensure you have the correct Basic Auth credentials by accessing the URL directly and logging in with the username and password.

Let’s get started!

First, create an application in Web IDE Full-Stack using the SAP UI5 Application Template

Configuring a Hybrid Mobile Application

Next, enable the app as a Hybrid Mobile Application. Right click on your project > Mobile > Enable as Hybrid Mobile Project.


Note: If the ‘Mobile’ option is not on the list, check that you have activated the HAT feature in Web IDE.

Your project structure should now look like this. Additional details are available here.

 

Add the “sap.mobile” section in the manifest.json file to trigger addition of the Kapsel Offline OData plugin during build.

"sap.ui5": {...},
"sap.mobile": {
	"definingRequests": {},
	"stores": []
}

 

Configuring the Data Source and the Offline Store

Next, let’s configure the data source and the offline store so that our app initializes an offline store (local DB) based on the OData service.

Data Source

In manifest.json, add a new model and a data source:

		"dataSources": {
			"offlineService": {
				"uri": "/mssampledata/offline/SampleServices/ESPM.svc/",
				"type": "OData",
				"settings": {
					"odataVersion": "2.0",
					"localUri": "localService/metadata.xml"
				}
			}
		}

Add a new route in neo-app.json. Ensure the target name is the same as the cloud platform destination name.

{
  "path": "/mssampledata/offline",
  "target": {
	"type": "destination",
	"name": "mssampledata"
  },
  "description": "Sample Service"
}

Offline Store Creation

The below code snippets, taken from Ludo’s blog, will basically create and open the offline store before loading the component during first initialization.

Open the sap-mobile-hybrid.js file and replace sap.hybrid.startApp with sap.hybrid.openStore

	if ("serverHost" in context && "serverPort" in context && "https" in context) {
			// start SCPms logon
			sap.hybrid.kapsel.doLogonInit(context, appConfig.appID, sap.hybrid.openStore);
		} else {
			console.error("context data for logon are not complete");
		}

Next, let’s prepare the offline store creation in the openStore function:

    openStore: function () {
    	jQuery.sap.require("sap.ui.thirdparty.datajs");
    	var properties = {
    		"name": "offlineService",
    		"host": sap.hybrid.kapsel.appContext.registrationContext.serverHost,
    		"port": sap.hybrid.kapsel.appContext.registrationContext.serverPort,
    		"https": sap.hybrid.kapsel.appContext.registrationContext.https,
    		"serviceRoot": fiori_client_appConfig.appID + "_mssampledata/SampleServices/ESPM.svc/",
    		"definingRequests": {
    			"productsSet": "/Products/?$expand=StockDetails"
    		}
    	};
    	store = sap.OData.createOfflineStore(properties);
    	var openStoreSuccessCallback = function () {
    		sap.OData.applyHttpClient(); //Offline OData calls can now be made against datajs.
    		sap.hybrid.startApp();
    	}
    	var openStoreErrorCallback = function (error) {
    		alert("An error occurred" + JSON.stringify(error));
    	}
    	store.open(openStoreSuccessCallback, openStoreErrorCallback);
    },

A Closer Look

Let’s digress and take a closer look at what’s happening in the openStore function as it is essential in understanding offline store creation.

To create an offline store the sap.OData.createOfflineStore method is used. A ‘properties’ object is passed as an argument and the return type is a sap.OfflineStore object. See sap.OData documentation

There are 2 important parameters in the properties object:

  1. serviceRoot – this identifies the root of an OData service relative to a destination in mobile services. The offline store will be created using the metadata of the OData service which the serviceRoot points to. A service root is unique to an offline store.
  2. Defining Requests – simply put, this property tells the offline store which Entity Sets should be populated with data and be made available offline. In our example above, the offline store will have the Product and Stock data available offline.

Once the offline store is created, it should then be ‘opened’ for offline access. This is done through the sap.OfflineStore.open method. When the offline store is successfully opened, the applyHttpClient and original startApp method are called – more on these in my next blog.

Building the UI

At this stage, our hybrid offline app setup is complete. Let’s create a simple UI for our app.

Let’s create a Stock App that let’s you view the current stock details of a specific product from a Product List. Let’s also add a Supplier List at the bottom.

Home.view.xml

<mvc:View controllerName="zoffline.demo.OfflineDemo.controller.Home" xmlns:html="http://www.w3.org/1999/xhtml" xmlns:mvc="sap.ui.core.mvc"
	displayBlock="true" xmlns="sap.m" xmlns:form="sap.ui.layout.form">
	<App id="idAppControl">
		<pages>
			<Page title="Stok App Offline Demo">
				<content>
					<VBox alignItems="Center">
						<form:SimpleForm id="stockDetailForm" title="Stock Details" layout="ResponsiveGridLayout" labelSpanXL="4" labelSpanL="4" labelSpanM="4"
							labelSpanS="12" adjustLabelSpan="false" emptySpanXL="3" emptySpanL="3" emptySpanM="3" emptySpanS="0" columnsXL="1" columnsL="1" columnsM="1"
							singleContainerFullSize="false">
							<form:content>
								<Label text="Product Id"/>
								<Text text="{offline>ProductId}"/>
								<Label text="Quantity"/>
								<Text text="{path: 'offline>Quantity',  type: 'sap.ui.model.type.Decimal'}"/>
								<Label text="Last Updated"/>
								<Text text="{path: 'offline>UpdatedTimestamp', type: 'sap.ui.model.type.Date', formatOptions: { style: 'short', pattern: 'dd/MM/yyyy'}}"/>
							</form:content>
						</form:SimpleForm>
					</VBox>
					<List items="{offline>/Products}" headerText="Product List" growing="true" growingThreshold="5">
						<ObjectListItem title="{offline>Name}" type="Active" press="onItemPress"
							number="{ parts:[{path:'offline>Price'},{path:'offline>CurrencyCode'}], type: 'sap.ui.model.type.Currency', formatOptions: {showMeasure: false} }"
							numberUnit="{offline>CurrencyCode}">
							<firstStatus>
								<ObjectStatus text="{offline>Category}"/>
							</firstStatus>
							<attributes>
								<ObjectAttribute text="{offline>ProductId}"/>
								<ObjectAttribute text="{offline>LongDescription}"/>
							</attributes>
						</ObjectListItem>
					</List>
						<List items="{offline>/Suppliers}" headerText="Supplier List" growing="true" growingThreshold="5">
						<StandardListItem title="{offline>SupplierName}" />
					</List>
				</content>
			</Page>
		</pages>
	</App>
</mvc:View>

Home.controller.js

sap.ui.define([
	"sap/ui/core/mvc/Controller"
], function (Controller) {
	"use strict";

	return Controller.extend("zoffline.demo.OfflineDemo.controller.Home", {
		onItemPress: function (oEvt) {
			var oContext = oEvt.getSource().getBindingContext("offline");
			this.getView().byId("stockDetailForm").bindElement({
				path: oContext.getPath() + "/StockDetails",
				model: "offline"
			});
			
		}
	});
});

Nothing fancy here, the code is exactly the same as that of an online application. when we access the app on a mobile device or in offline mode, the codebase stays the same, that’s the beauty of Offline OData.

Let’s test it online!

Now let’s make sure the app is working as a webapp in our desktop browser. Run the project as Web Application.

Deploy the Hybrid Application

Time to deploy the project as a hybrid application in Mobile Services.
Right click on the project. Mobile > Build as Packaged App. I will not go into detail as Ludo’s blog already covers this step quite well.

When build is complete, scan the QR code with your device’s QR code reader to download and install the application.

Testing the Application

Open the mobile application and go through the usual login screens. A white screen appears for a few seconds while the offline store is syncing. At this point, the offline store is getting created and the local database tables whose entity sets were defined in the defining request are getting populated with data (remember the openStore function?). After a few seconds, the home screen should load.

Congratulations, you’ve just created an offline application from scratch! Go to airplane mode and test it yourself.

Supplier List empty?

As you can see below, the supplier list does not have any records. This is because we did not include ‘Suppliers’ in the defining requests and hence the Supplier data was not fetched and loaded to the offline store. To fix this, simply add a new defining query for “/Suppliers” in the defining request.

 

Next Steps

We’ve built a simple read-only offline hybrid application from scratch using HAT on SAP Web IDE. In my upcoming blog posts, let’s explore how to implement CRUD, data synchronization, multiple data sources/offline stores and an online/offline scenario. Follow me to stay updated.

Cheers,

Greg

 

Part 2 – Implementing CUD, Flush and Refresh

Additional Readings

Introduction to Offline OData

JSDoc: sap.OData

 

Assigned Tags

      32 Comments
      You must be Logged on to comment or reply to a post.
      Author's profile photo Jorge Cabanas
      Jorge Cabanas

      Really good job,

      I look forward the second part 🙂

      Thank you very much!
      Jorge

      Author's profile photo Greg Cariño
      Greg Cariño
      Blog Post Author

      Glad you liked it Jorge. Thanks for reading!

      Author's profile photo Mike Doyle
      Mike Doyle

      Nice debut, Greg, it's great to see you sharing your expertise.  The first of many great blog posts I hope!

      Author's profile photo Greg Cariño
      Greg Cariño
      Blog Post Author

      Thanks Mike! More to come 🙂

      Author's profile photo Rakesh Narayan
      Rakesh Narayan

      Hello Greg – Thanks for the nice blog.

      One Question : Why does the build fail (Build Packaged App) with Error: Build Failed as the CBS build job id was not found for this Application.

      ~ Rakesh Narayan.

      Author's profile photo Greg Cariño
      Greg Cariño
      Blog Post Author

      Hi Rakesh,

      Thanks for reading and apologies for the late response. Is this still an issue? I haven't seen this error before, might be a mobile services hiccup. Let me know how i can help if the issue still persists.

      Cheers

      Author's profile photo Angel Gutierrez Bellido
      Angel Gutierrez Bellido

      Hi Greg Cariño,

      I can filter data from the entity before download to device

      Author's profile photo Greg Carino
      Greg Carino

      Hi Angel,

      Yes you can add OData query options in the defining request to filter data you download to the device instead of the whole data set, for example the defining request below only downloads records with category 'Notebooks'. Keep in mind that the defining request cannot be changed once the offline store is opened, and that the same query (with filter options) will be called when you do a refresh.

      "definingRequests": {
          			"productsSet": "/Products/?$filter=Category eq 'Notebooks'
          		}
      Author's profile photo Angel Gutierrez Bellido
      Angel Gutierrez Bellido

      Hi Grep.

      Have you got any idea about filter  to range dates in the defining request?

      Author's profile photo Devisha Bhavik
      Devisha Bhavik

      Hi Greg,

       

      Very nice blog!!

       

      Regarding the offline capability, will it still work in below scenario?

       

      1. I have downloaded the app on the device and access it first time. this time it will go through the normal login process and downloads the data.
      2. Device is turned off on that day.
      3. Next day, I start the device and there is no internet connection.
      4. At this time, when I launch the app in device, will it again go through login process or will it continue working based on the data downloaded yesterday and keep working with the app?

      I have read in your blog as well as in Ludo's blog that once we have opened the app and login is completed, THEN device is gone to offline mode to keep working on it.

      However, my question is, when device is on airplane mode, can I launch the app without login process and based on the previously stored data, will I be able to continue with the app?

       

      Thanks,

      Bhavik

      Author's profile photo Greg Cariño
      Greg Cariño
      Blog Post Author

      Hi Devisha,

      Thanks for reading.

      Yes, when there's no internet connection the app is opened immediately with the previously stored data in the offline store. When online, the login process is triggered when the user registration has expired - you can configure and delete registrations in mobile services.

      Cheers,

      Greg

      Author's profile photo Devisha Bhavik
      Devisha Bhavik

      Thanks for the quick response.

       

      So, If I read it correctly, in the event of no internet connection, App skips the login process and let user to work with based on the previously saved data. Once online, it can detect and ask user to login and then sync the data.

       

      Will this be handled automatically through Mobile service or do we have to put actual code in the app to make it work that way?

       

      Thanks,

      Bhavik

      Author's profile photo Angel Gutierrez Bellido
      Angel Gutierrez Bellido

      Hi Greg Cariño,

       

      How do i can to disable or hide Passcode Screen. 

      Author's profile photo Greg Cariño
      Greg Cariño
      Blog Post Author

      Hi Angel,

      Go to your app config in Mobile Services and go to Client Policies. There is a checkbox there to disable the passcode screen.

      Greg

      Author's profile photo Angel Gutierrez Bellido
      Angel Gutierrez Bellido

      I have found the option enable Passcode policy. I have unchecked that option, but that no function when i generate and install my app in device.

       

      Author's profile photo Devisha Bhavik
      Devisha Bhavik

      Hi Greg,

       

      I tried doing this POC. However, when I try to Build the packaged App, it is giving me an error saying: "Fail to get minimum OS versions:"

      And dropdown for Minimum OS version is empty. It is not letting me go further without this information.

       

      Not sure where do we need to maintain this information?

      Thanks,

      Bhavik

      Author's profile photo Mathias Ingemann Mortensen
      Mathias Ingemann Mortensen

      Hello Greg

      I have a problem when trying to enable the project as “Hybrid Mobile Project”. Why can this be?

      I have tried to make a new project from scratch, make another workspace, turned on/off the HAT extension, and even deleted my trial account.

      The “Mobile” option is only visible right after refreshing the browser. However it is empty, and disappears when right clicking the project another time.

       

      Thanks!

      Author's profile photo Mathias Ingemann Mortensen
      Mathias Ingemann Mortensen

      Resolved the issue!

      In Cloud Cockpit > Destinations, the "mobileservices" were missing.

      Author's profile photo Angel Gutierrez Bellido
      Angel Gutierrez Bellido

      Hi Grep.

      Do you know to keep the session active in offline app. Currently, the app show screen login when if i inactive for 5 min.

      Author's profile photo Greg Cariño
      Greg Cariño
      Blog Post Author

      Hi Angel,

       

      This is because of SAML expiry with SAP ID service or your Identity Provider. I would recommend you to use OAuth security as the authentication method to keep the session active.

      If you can modify your IdP, then you can consider increasing the expiry of the SAML token.

       

      Regards,

      Greg

      Author's profile photo Angel Gutierrez Bellido
      Angel Gutierrez Bellido

      Hi Greg.

      Thank so much. Could you please share the steps for implement that.

       

      Regards,

      Angel

      Author's profile photo Angel Gutierrez Bellido
      Angel Gutierrez Bellido

      Hello Greg,

      I need you helping with this error

       

       

      Regards,

      Angel

      Author's profile photo Angel Gutierrez Bellido
      Angel Gutierrez Bellido

      Hello Greg.

       

      my application sometimes do not sync the data. But in the console show this message “MAF LogonCoreCDVPlugin: Pause event successfully set.”

       

       

      Regards,

      Angel

      Author's profile photo Andrea Jafjaf
      Andrea Jafjaf

      Hi Greg,

      is the architecture for such a hybrid app like described in this article?

      Especially this part: "Even when the device has network connectivity, your application will not access data that is part of the offline store directly from the back-end server but will use the local database."

      Regards, Andrea

      Author's profile photo Greg Cariño
      Greg Cariño
      Blog Post Author

      Hi Andrea,

       

      Yes you are correct. Once the app is offline-enabled, all calls to the service will be routed to the offline store instead (with or without connectivity). This ensures your offline store is always in sync.

      You can also check part 2 of my blog for more information on how the offline store store works.

       

      Cheers,

      Greg

      Author's profile photo Angel Gutierrez Bellido
      Angel Gutierrez Bellido

      Hi Greg,

       

      Please, could you help me this error?

      Regards, Angel.

      Author's profile photo Frank Yang
      Frank Yang

      Hi Greg, thanks for sharing your expertise! Fantastic blog!

      Just a quick question. i would like to use an OData filter in the defining requests, with the filter parameters being a variable e.g. an username returned from the userapi. Will this be possible or am I stuck with fetching the entire data set?

      Best regards,

      Frank

      Author's profile photo Archit Wahi
      Archit Wahi

      I am using the sampleservices product set and facing an issue while doing a create. The model.create method goes into sucess callback but the success object is returned as blank

      the value for all properties is returned as null despite sending the entered values correctly during create call. Hence i am seeing blank line item getting added into the master list of products.

       

      Same entry if create from desktop version adds an entry correctly

      Author's profile photo André Bastos
      André Bastos

      Hi Greg! Hope you doing well.

       

      So, I have the following scenario. I have a service that contains some EntitySets. But, I wanna only one of these sets to be available offline. The other ones I want to be requested only on a online scenario.

       

      But, as you explained for the Suplier list, the “online” sets are not specified on defining requests and then they aren't available on the app.

       

      So, any idea to solve this?

       

      Best regards,

       

      André

      Author's profile photo Greg Cariño
      Greg Cariño
      Blog Post Author

      Hi Andre,

      There are many ways to achieve this.

      An approach that I can recommend that doesn't require a lot of handling during runtime is to define two models that point to the same Odata service: one for offline calls and another for online-only calls.

      Note that you need two separate routes in neo-app.json. Using my example in the blog, you need to add another another path for "/mssampledata/online".

      Hope this helps,

      Greg

      Author's profile photo André Bastos
      André Bastos

      Hi Greg, how are you?

      Your tip worked really well. Really thanks for the help ?

      Greetings,

      André

      Author's profile photo Saurabh Singh
      Saurabh Singh

      Hi Greg Cariño,

      I am unable to load application. Device ends up showing white screen.

       

      appRouters.js

      var mobile_appRoutes = [
      	{
      		"path": "/services/userapi",
      		"manual": true
      	},
      	{
      		"path": "/odata_destn",
      		"destination": "odata_destn"
      	}
      ];

      sap-mobile-hybrid.js

      var userModel = new sap.ui.model.json.JSONModel("/services/userapi/attributes");
      var properties = {
      			"name": "store_mainService",
      			"host": sap.hybrid.kapsel.appContext.registrationContext.serverHost,
      			"port": sap.hybrid.kapsel.appContext.registrationContext.serverPort,
      			"https": sap.hybrid.kapsel.appContext.registrationContext.https,
      			"serviceRoot": fiori_client_appConfig.appID + "_" + mobile_appRoutes[1].destination +
      				"/odata/namespace/odataservicename;v=1/",
      			"definingRequests": {
      		         "Customers": "/Customer(userid='" + oUserModel.getData().uid + "')/Set"
      			}
      		};

      Destinations: