Building Offline-enabled apps using HAT on SAP Web IDE is deprecated. To build offline-enabled apps, use SAP MDK on SAP Build (cross-platform) or Fiori SDK for iOS and Android (native apps) in the SAP BTP Multi-Cloud Environment. Other alternative solutions are PWA and open-source frameworks like React Native.
In Part 1, we built a simple read-only offline Stock Application. In a real-world scenario, an offline-enabled application would also allow modification of records even without an internet connection. In this blog post, we'll implement CUD (Create, Update, Delete) in our app and explore how it works under the hood with Offline OData.
Let's get right into it and implement create functionality for our Products list. Let's add an 'Add (+)' button that opens a New Product form dialog, a form dialog and the create (Post) function.
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="Stock 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="{path: 'offline>/Products', sorter: [{ path: 'UpdatedTimestamp', descending: true }]}" growing="true" growingThreshold="5">
<headerToolbar>
<Toolbar>
<content>
<Title text="Product List" level="H2"/>
<ToolbarSpacer/>
<Button press="onPressAddProduct" icon="sap-icon://add"/>
</content>
</Toolbar>
</headerToolbar>
<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>ShortDescription}"/>
</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", "sap/m/MessageToast"
], function (Controller, MessageToast) {
"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"
});
},
onPressAddProduct: function () {
if (!this._oAddProductDialog) {
this._oAddProductDialog = sap.ui.xmlfragment(
"zoffline.demo.OfflineDemo.view.fragments.AddProduct",
this
);
this.getView().addDependent(this._oAddProductDialog);
}
this._oAddProductDialog.open();
},
onCloseNewProductDialog: function () {
this.getView().getModel("data").setData({}); //reset data if dialog is closed
this._oAddProductDialog.close();
},
onSubmitNewProduct: function () {
var oPayload = this.getView().getModel("data").getData();
this.getView().getModel("offline").create("/Products", oPayload, {
success: function (oData) {
this.getView().getModel("data").setData({});//reset data model after successful post
this._oAddProductDialog.close(); //close the dialog on success
}.bind(this),
error: function (oErr) {
MessageToast.show("An error occured" + oErr);
}
});
}
});
});
Add a new JSON model in manifest.json
"models": {
"i18n": {..},
"offline": {..},
"data": {
"type": "sap.ui.model.json.JSONModel"
}
}
Add a new 'fragments' folder in the 'view' folder and create an 'AddProduct.fragment.xml' file
AddProduct.fragment.xml
<core:FragmentDefinition xmlns="sap.m" xmlns:l="sap.ui.layout" xmlns:f="sap.ui.layout.form" xmlns:core="sap.ui.core">
<Dialog title="Create Product">
<content>
<f:SimpleForm layout="ResponsiveGridLayout" columnsM="1" labelSpanM="12" columnsL="1" labelSpanL="6">
<f:content>
<Label text="Product Id"/>
<Input value="{data>/ProductId}"/>
<Label text="Product Name"/>
<Input value="{data>/Name}"/>
<Label text="Category"/>
<Input value="{data>/Category}"/>
<Label text="Category Name"/>
<Input value="{data>/CategoryName}"/>
<Label text="Short Description"/>
<Input value="{data>/ShortDescription}"/>
<Label text="Price"/>
<Input value="{data>/Price}" type="Number"/>
</f:content>
</f:SimpleForm>
</content>
<beginButton>
<Button text="Submit" press="onSubmitNewProduct" type="Emphasized"/>
</beginButton>
<endButton>
<Button text="Close" press="onCloseNewProductDialog"/>
</endButton>
</Dialog>
</core:FragmentDefinition>
At this point, we have a basic create function that will create a new Product entity even without network connectivity. Note that the code is the same as a normal application, so I will leave it up to you to implement the update and delete methods.
However, all these changes are only written in the local offline store.
To send requests back to the server, a flush method has to be explicitly called - think of flush as an upload function. A refresh method, on the other hand, downloads data from the backend (new records, updates) into the entity store.
Early on in my learning journey, I had a misconception that the offline store is only used when the device is offline - but this isn't the case. When an offline store is opened for a particular OData service, all operations that are made against that service will be stored in a local request queue until a flush operation is called. The request queue can contain multiple create, update and delete operations.
The above diagram has been simplified for easier understanding. A more extensive sequence flow diagram is available here.
Key points:
Flush and refresh are methods of the sap.OfflineStore and should be called explicitly. When to call these methods is exactly up to your data synchronization strategy. A common strategy would be to call the refresh after a successful flush, to get the latest changes from the backend after submitting your own changes.
In our app, let's modify the submit function and add a simple script that will flush the changes if the device is online, then subsequently call the refresh method.

Home.controller.js
onSubmitNewProduct: function () {
var oPayload = this.getView().getModel("data").getData();
this.getView().getModel("offline").create("/Products", oPayload, {
success: function (oData) {
this.getView().getModel("data").setData({}); //reset data model after successful post
this._oAddProductDialog.close(); //close the dialog on success
if (navigator.onLine) {
//flush if device is online
sap.hybrid.flushStore()
}
}.bind(this),
error: function (oErr) {
MessageToast.show("An error occured" + oErr);
}
});
}
Let's then implement the flush method:
sap-mobile-hybrid.js
flushStore: function () {
var _refreshSuccessCallback= function (o) {
//show a message here
};
var _refreshErrorCallback= function (o) {
};
var _flushStoreCallback = function (o) {
store.refresh(_refreshSuccessCallback, _refreshErrorCallback)
};
var _errorCallback = function (o) {
console.log(o)
};
if (store) {
//offline store is opened
store.flush(_flushStoreCallback, _errorCallback , null);
} else {
console.log("The store must be open before it can be flushed");
}
}
Unlike a standard online application, success and error responses from the backend are not provided to the flush method callbacks. What the callbacks represent is the success (or failure) of the HTTP request (due to network unavailability, for example)
For example, if a new record that violates a business rule or throws an error is created and flushed, the success callback will still be called, as long as the HTTP request for the flush is successfully sent to the backend.
To read and handle error messages, a standard entity set named 'ErrorArchive' is provided. The relevant error responses, and original request body is available to be read, handled and resent to the backend. Try it for yourself. More on the entity properties here.
In addition, trace logs can be recorded in Mobile Services cockpit under Analytics > Network Traces. The HAR file can be downloaded and viewed using a HAR Viewer.
With this in mind, server-generated data (document numbers, for example) are not always readily available after creation in the app, hence the technical design and data flow must be carefully considered for different scenarios in an offline application. Common questions that should be asked include:
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
| User | Count |
|---|---|
| 27 | |
| 24 | |
| 20 | |
| 20 | |
| 14 | |
| 13 | |
| 13 | |
| 12 | |
| 12 | |
| 11 |