Hybrid mobile apps are web applications wrapped up their own browser which can run on any of the mobile platforms (Android, iOS, Windows etc.). The big advantage of a hybrid app is that it has a single code base, whereas a native app must be re-written for each platform.
Apache Cordova is an open-source framework for developing hybrid apps. To use native features (beyond those already available via hmtl5,) apps can make use of plugins. These plugins handle the platform-specific code and are available for single or multiple platforms. Plugins are available to access the GPS, accelerometer or camera, for example.
SAP have written their own plugins, branded as Kapsel, to handle things like logon and offline OData. These bring an enterprise flavour to hybrid apps.
Until recently SAP’s recommended approach was to use the Hybrid Application Toolkit (or HAT) in order to build hybrid apps. The app build took place on the developer’s PC or Mac. The HAT gained a reputation for being tricky to set up and that hindered the use of hybrid apps within the SAP ecosystem.
Fiori Mobile has at its core a cloud-build service, so there is no need to install the HAT. We can trigger the build from Web IDE. We specify the (already deployed) Fiori app(s) that we want to package and after a few minutes we can download the .apk (for Android) and .ipa (for iOS) files ready to be installed on mobile devices.
The cloud build service works well. It typically build for both platforms in about 8 minutes without issueas. There have only been a couple of occasions when builds failed due to technical problems.
There are some facets to the technology which don’t feel mature yet. One area is the offline OData features. When using Fiori Mobile things work differently to the more conventional hybrid and native apps. One example would be the destinations, because Fiori Mobile apps use a generated destination which reference the SAP Cloud Platform (CP) portal service.
My point is not that offline OData seems immature, rather that the offline features can be tricky when used with Fiori Mobile apps. We are still receiving assistance from SAP to get the offline features of our app optimised. This is a shame, because shared data and the handling of delta requests, for example, are two features which justify the ‘middleware’ approach to offline that SAP have taken.
Authentication has also been a challenge. On a previous project I used the Fiori Client in conjunction with SAP Cloud Identity (now officially SAP Cloud Platform Identity Authentication). The Fiori Client stored the username and password on the device so that the user didn’t need to enter them every time their session timed out. We haven’t achieved that yet with our Fiori Mobile app.
If you have production account browse on HCP Cockpit and log in. If you use trial account log in here
As we said before there is two ways to build SAP Fiori mobile application:
If we use SAP Fori Mobile Service to build mobile application first we need to create and publish site where application will be deployed. Otherwise if we use HAT skip this step and continue with Create Fiori Project
Go to Portal Service and create new Site Directory
A popup will be displayed. Enter your Site Name, choose SAP Fiori Launchpad as Template Source and click Create
Following this you will be redirected to SAP Fiori Configuration Cockpit. Close the tab and go back to Admin Space for Portal Service.
Now we need to Publish this Site and Set as default.
Once we create and publish cmbSDKTestSite we can create our Fiori project (if we use Fiori Mobile Service to build mobile application, otherwise if we use HAT we don't need to publish site on Launchpad and creating Fiori project will be our first step).
Go to Services tab again, open SAP Web IDE Full-Stack service, and click on Go to Service
A new tab will be opened. From here we will start to create our Fiori project.
In the SAP Web IDE Workspace we can see all files that our Fiori project inludes.
Open Component.js file.
Component.js is the first point of our application, we can say that it serves as index which encapsulates all our applications details, i.e. view names, routing details, main view, applications type(Full Screen or SplitApp), application service configuration etc..
Here in init function we will configure our plugin. Set properties that we need, set callback functions, call some methods, etc..
sap.ui.define([
"sap/ui/core/UIComponent",
"sap/ui/Device",
"MyFirstFioriApp/MyFirstFioriApp/model/models"
], function (UIComponent, Device, models) {
"use strict";
var oEventBus = sap.ui.getCore().getEventBus();
var scannerIsInitialized = false;
return UIComponent.extend("MyFirstFioriApp.MyFirstFioriApp.Component", {
metadata: {
manifest: "json"
},
/**
* The component is initialized by UI5 automatically during the startup of the app and calls the init method once.
* @public
* @override
*/
init: function () {
// call the base component's init function
UIComponent.prototype.init.apply(this, arguments);
// enable routing
this.getRouter().initialize();
// set the device model
this.setModel(models.createDeviceModel(), "device");
if (!scannerIsInitialized) {
scannerIsInitialized = true;
window.readerConnected = 0;
window.scannerActive = false;
cmbScanner.addOnResume(function (result) {
cmbScanner.setAvailabilityCallback((readerAvailability) => {
if (readerAvailability === cmbScanner.CONSTANTS.AVAILABILITY_AVAILABLE) {
oEventBus.publish("ScanView", "SetConnectionStatus", {
statusText: "AVAILABLE"
});
cmbScanner.connect((result) => {});
} else {
oEventBus.publish("ScanView", "SetConnectionStatus", {
statusText: "NOT AVAILABLE"
});
}
});
if (Device.os.android) {
cmbScanner.connect((result) => {});
}
});
cmbScanner.addOnPause(function (result) {
if (Device.os.android) {
cmbScanner.disconnect();
}
cmbScanner.setAvailabilityCallback();
});
cmbScanner.setPreviewContainerPositionAndSize(0, 0, 100, 50);
cmbScanner.setConnectionStateDidChangeOfReaderCallback((connectionState) => {
if (connectionState === cmbScanner.CONSTANTS.CONNECTION_STATE_CONNECTED) {
oEventBus.publish("ScanView", "SetConnectionStatus", {
statusText: "CONNECTED"
});
if (window.readerConnected != connectionState) {
cmbScanner.setSymbologyEnabled("SYMBOL.DATAMATRIX", true);
cmbScanner.setSymbologyEnabled("SYMBOL.C128", true);
cmbScanner.sendCommand("SET TRIGGER.TYPE 2");
}
} else if (connectionState == cmbScanner.CONSTANTS.CONNECTION_STATE_DISCONNECTED) {
oEventBus.publish("ScanView", "SetConnectionStatus", {
statusText: "DISCONNECTED"
});
} else if (connectionState == cmbScanner.CONSTANTS.CONNECTION_STATE_CONNECTING) {
oEventBus.publish("ScanView", "SetConnectionStatus", {
statusText: "CONNECTING"
});
} else if (connectionState == cmbScanner.CONSTANTS.CONNECTION_STATE_DISCONNECTING) {
oEventBus.publish("ScanView", "SetConnectionStatus", {
statusText: "DISCONNECTING"
});
}
window.readerConnected = connectionState;
});
cmbScanner.setAvailabilityCallback((readerAvailability) => {
if (readerAvailability === cmbScanner.CONSTANTS.AVAILABILITY_AVAILABLE) {
oEventBus.publish("ScanView", "SetConnectionStatus", {
statusText: "AVAILABLE"
});
cmbScanner.connect((result) => {});
} else {
oEventBus.publish("ScanView", "SetConnectionStatus", {
statusText: "NOT AVAILABLE"
});
}
});
cmbScanner.setActiveStartScanningCallback((result) => {
if (result === true)
window.scannerActive = true;
else
window.scannerActive = false;
});
cmbScanner.setPreviewOptions(cmbScanner.CONSTANTS.PREVIEW_OPTIONS.DEFAULTS | cmbScanner.CONSTANTS.PREVIEW_OPTIONS.HARDWARE_TRIGGER);
cmbScanner.setCameraMode(cmbScanner.CONSTANTS.CAMERA_MODES.NO_AIMER);
var sdkKEY = "";
if (Device.os.android)
sdkKEY = this.getModel("i18n").getProperty("MX_MOBILE_LICENSE_ANDROID");
else
sdkKEY = this.getModel("i18n").getProperty("MX_MOBILE_LICENSE_iOS");
cmbScanner.registerSDK(sdkKEY);
cmbScanner.loadScanner(0, (result) => {
cmbScanner.connect((result) => {});
});
oEventBus.subscribe("Component", "LoadScanner", this.loadScanner, this);
}
},
loadScanner: function (sChanel, sEvent, oData) {
cmbScanner.disconnect((result) => {
cmbScanner.loadScanner(oData.selectedDevice, (result) => {
cmbScanner.connect((result) => {});
});
});
},
onExit: function () {
oEventBus.unsubscribe("Component", "LoadScanner", this.loadScanner, this);
cmbScanner.disconnect();
cmbScanner.setAvailabilityCallback();
}
});
});
cmbScanner is an object that represents our plugin. With this object we can access all API methods and Constants from our plugin.
Here we are only configuring reader device, handling connections, availability, etc.. We will do scanning and getting results in other views. If we want to notify users about every connection state changed or other info about reader device we will be using EventBus sap object. With this object we can publish function and call them from any view in the application. In this example on connection state changed with EventBus we are calling SetConnectionStatus function that is implemented in view where scanning is performed and set label text to show user current connection state.
Now open ScanView.view.xml and add this code:
<mvc:View controllerName="MyFirstFioriApp.MyFirstFioriApp.controller.ScanView" xmlns:mvc="sap.ui.core.mvc" displayBlock="true" xmlns="sap.m"
xmlns:core="sap.ui.core">
<Shell id="shell">
<App id="app">
<pages>
<Page id="page" title="{i18n>title}">
<subHeader>
<Toolbar id="__toolbar2" width="100%">
<content>
<FlexBox id="__box0" width="100%" alignContent="Center" alignItems="Start" direction="Column" fitContainer="true" justifyContent="Center">
<items>
<Select id="selectActiveDevice" items="{/Devices}" textAlign="Center" selectedKey="0" change="activeDeviceChanged">
<core:Item text="{text}" key="{key}"/>
</Select>
</items>
</FlexBox>
<Label id="lblStatus" text="DISCONNECTED" width="100%" textAlign="End" design="Bold"/>
</content>
</Toolbar>
</subHeader>
<content>
<FlexBox id="flexBoxContainer" width="100%" alignContent="Start" alignItems="Start" direction="Column" fitContainer="true"/>
</content>
<footer>
<Toolbar id="__toolbar1" width="100%">
<content>
<Button id="btnScan" press="btnScanPress" text="Scan" width="100%"/>
</content>
</Toolbar>
</footer>
</Page>
</pages>
</App>
</Shell>
</mvc:View>
You can design view with CodeEditor or with LayoutEditor.
After that open ScanView.controller.js and add this code
sap.ui.define([
"sap/ui/core/mvc/Controller"
], function (Controller) {
"use strict";
var oEventBus = sap.ui.getCore().getEventBus();
var oModel = new sap.ui.model.json.JSONModel();
oModel.setData({
Devices: [{
key: "0",
text: "MX Device"
}, {
key: "1",
text: "Mobile Camera"
}]
});
return Controller.extend("MyFirstFioriApp.MyFirstFioriApp.controller.ScanView", {
onInit: function () {
this.getView().setModel(oModel);
},
onAfterRendering: function () {
window.scannerActive = false;
switch (window.readerConnected) {
case cmbScanner.CONSTANTS.CONNECTION_STATE_CONNECTED:
this.getView().byId("lblStatus").setText("CONNECTED");
break;
case cmbScanner.CONSTANTS.CONNECTION_STATE_DISCONNECTED:
this.getView().byId("lblStatus").setText("DISCONNECTED");
break;
case cmbScanner.CONSTANTS.CONNECTION_STATE_CONNECTING:
this.getView().byId("lblStatus").setText("CONNECTING");
break;
case cmbScanner.CONSTANTS.CONNECTION_STATE_DISCONNECTING:
this.getView().byId("lblStatus").setText("DISCONNECTING");
break
default:
this.getView().byId("lblStatus").setText("UNKNOWN");
break;
}
oEventBus.subscribe("ScanView", "SetConnectionStatus", this.setConnectionStatus, this);
cmbScanner.setResultCallback((result) => {
if(result && result.readResults && result.readResults.length > 0){
result.readResults.forEach((item, index) => {
if (item.goodRead == true) {
//Perform some action on barcode read
//example:
var verticalLayoutContainer = new sap.ui.layout.VerticalLayout(null, {
width: "100%"
}).addStyleClass("sapUiSmallMarginTop");
verticalLayoutContainer.addContent(new sap.m.Label({
text: item.symbologyString + ":",
textAlign: "Begin",
design: "Bold"
}).addStyleClass("sapUiSmallMarginBegin"));
verticalLayoutContainer.addContent(new sap.m.Text({
text: item.readString,
textAlign: "Begin"
}).addStyleClass("sapUiSmallMarginBegin"));
this.getView().byId("flexBoxContainer").addItem(verticalLayoutContainer);
}
else{
//Perform some action when no barcode is read or just leave it empty
}
});
}
});
},
btnScanPress: function () {
if (window.readerConnected === cmbScanner.CONSTANTS.CONNECTION_STATE_CONNECTED) {
if (window.scannerActive === true) {
cmbScanner.stopScanning();
} else {
cmbScanner.startScanning();
}
}
},
setConnectionStatus: function (sChanel, sEvent, oData) {
this.getView().byId("lblStatus").setText(oData.statusText);
},
activeDeviceChanged: function () {
cmbScanner.disconnect((result) => {
oEventBus.publish("Component", "LoadScanner", {
selectedDevice: parseInt(this.getView().byId("selectActiveDevice").getSelectedKey())
});
});
},
onExit: function () {
oEventBus.unsubscribe("ScanView", "SetConnectionStatus", this.setConnectionStatus, this);
if (window.readerConnected === cmbScanner.CONSTANTS.CONNECTION_STATE_CONNECTED) {
if (window.scannerActive === true) {
cmbScanner.stopScanning();
}
}
cmbScanner.setResultCallback((result) => {
return false;
});
}
});
});
In this view we have a Scan button to startScanning()/stopScanning() and cmbScanner.setResultCallback to handle successful scan results.
In this section we will explain how to build Fiori Mobile Application (generate .ipa and .apk files).
1. Using HAT (Hybrid Application Toolkit)
2. Using Fiori Mobile Service
Before we start to build our Fiori Mobile Application with Fiori Mobile Service we need to deploy our project on SAP HANA Cloud Platform.
If you plan to use the cmbSDK to do mobile scanning with a smartphone or a tablet (without the MX mobile terminal), the SDK requires the installation of a license key. Without a license key, the SDK will still operate, although scanned results will be blurred (the SDK will randomly replace characters in the scan result with an asterisk character).
Contact your Cognex Sales Representative for information on how to obtain a license key including trial licenses which can be used for 30 days to evaluate the SDK.
After obtaining your license key open i18n.properties file in your project on SAP WEB IDE service, and set your obtained keys.
Then go back to the Component.js file and check this code:
...
var sdkKEY = "";
if (Device.os.android)
sdkKEY = this.getModel("i18n").getProperty("MX_MOBILE_LICENSE_ANDROID");
else
sdkKEY = this.getModel("i18n").getProperty("MX_MOBILE_LICENSE_iOS");
cmbScanner.registerSDK(sdkKEY);
...
You can see that you read this key from i18n.properties and call cmbScanner.registerSDK method to register SDK with your license key.