Skip to Content
Technical Articles

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

 

27 Comments
You must be Logged on to comment or reply to a post.
  • 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.

    • 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

    • 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'
          		}
  • 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

    • 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

      • 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

  • 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

  • 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!

    • 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

  • 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

  • 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

    • 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

  • 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