Introduction
Creating native applications for every available platform on the market can be very expensive. Therefore, many developers or small businesses choose and maintain only one platform for which they develop their applications. However, there are more effective ways of cross-platform development and they are discussed in this article.
Let’s pretend that we need to program a mobile client for e-commerce shop with clothing for instance. Our priority is to run this application on numerous smartphones and tablets and coding it in a native way. In addition, we require to keep our client brand consistent for every platform. Later on, the application will be optimized for product/clothing download and saved locally to database in order to keep this application functional even in off-line mode. However, our intension is to save time and money on development, and prevent spending additional resources on maintenance and porting of client’s future development.
So let’s create a prototype of client for e-commerce application. In this case, the client will connect through Internet to the e-commerce shop, download a list of products and enable users its viewing.
Background
To overcome platform differences, we will use Moscrif SDK (available free for open source and non-commercial projects). It allows us to create native client app using web technologies (JavaScript and json) and run it on Android (1.6 and newer), Symbian (S^1 or newer), Samsung bada and Windows Mobile 5+ (iOS is coming soon).
Server Side
In this article, we will simulate real online store by simple ASP.NET files. The getProducts.aspx will return a list of 100 products (product name, price, out-of-stock status, etc.). The list will be simple JSON.
getProducts.aspx
private Random rnd = new Random();
void Page_Load()
{
Response.Clear();
StringBuilder sb = new StringBuilder("[");
for (int i = 1; i <= 94; i++)
{
sb.Append("{");
sb.AppendFormat("id: {0}, ", i);
sb.AppendFormat("name: \"{0}\", ", String.Format("Product{0}", i));
sb.AppendFormat("description: \"{0}\", ", Ipsum.GetWords(13 + rnd.Next(10)));
sb.AppendFormat("price: {0:0.00}, ",
((90 + rnd.Next(750)) / 10.0) + (rnd.Next(100) > 50 ? 0.05 : 0.00) );
sb.AppendFormat("oos: {0}, ", rnd.Next(100) > 50 ? 1 : 0);
sb.AppendLine("},");
}
sb.Append("]");
Response.Write(sb.ToString());
}
The getProductImage.aspx will return product thumbnail image.
getProductImage.aspx
void Page_Load()
{
try
{
Response.Clear();
string productId = Request["id"];
string path = Server.MapPath(Path.GetDirectoryName(Request.Path)) +
"\\" + Request["id"] + ".jpg";
Bitmap bmp = CreateThumbnail(path, Int32.Parse(Request["w"]),
Int32.Parse(Request["h"]));
bmp.Save(Response.OutputStream, System.Drawing.Imaging.ImageFormat.Jpeg);
bmp.Dispose();
}
catch (Exception ex)
{
Response.StatusDescription = ex.Message;
}
}
Client Side
Mobile Client will be programmed in (extended) JavaScript and Moscrif SDK will ensure compatibility with each mobile platform. Installation of Moscrif SDK is simple and requires .NET, or Mono and Gtk# environment. You can also watch the installation here. Practically, it means that JavaScript codes will be interpreted into virtual machine of any mobile device. Furthermore, Moscrif IDE will be responsible for creating installation package (apk, sisx, cab and zip). Included file eShop.zip is possible to load in IDE through menu Project->Import Project.
E-Commerce mobile client will not be using any classes from the Moscrif framework; consequently, client has to be created from scratch. Basic screen navigation is covered by two methods, which is the push method (for various screens / views); and the pop method (for switching from current screen view to previous view):
app.push = function(view)
{
assert view != null : "View is required";
this._views.push(view);
this.add(view.native || view, #front);
}
app.pop = function()
{
if (this._views.length == 0) {
this._quit = true;
return null;
}
var view = this._views.pop();
view.native.detach();
if (this._views.length == 0) {
this._quit = true;
return null;
}
return view;
}
Applying the mobile client to different resolutions is done by vector graphics (SVG). Also, every other part of UI will be calculated based on resolution of the device. For instance, height of the lower bar will be pro-rated to 1/10 from a display height:
app.buildToolBar = function(buttons, paint = null)
{
const icons = {
#quit: Path.fromSVG("M 119.96875 -0.375 C 110.00577
-0.375 101.875 7.2547812 101.875 16.5625 L 101.875 145.75
C 101.875 155.0408 110.00577 162.65625 119.96875 162.65625
L 136.03125 162.65625 C 145.97615 162.65625 154.125 155.0408
154.125 145.75 L 154.125 16.5625 C 154.125 7.2547812 145.97615
-0.375 136.03125 -0.375 L 119.96875 -0.375 z M 75.875 26.5
C 31.177216 45.20005 -4.6259293e-018 87.05178 0 135.875
C 0 202.06134 57.282755 255.625 128 255.625 C 198.64492 255.625
255.94575 202.04442 256 135.875 L 255.9375 135.875 C 255.9375
87.0687 224.77346 45.2313 180.09375 26.53125 L 180.09375 70.40625
C 201.5024 85.33244 215.50825 108.95031 215.5625 135.875
C 215.39976 181.11051 176.35028 217.77605 128 217.84375
C 79.631647 217.77575 40.509826 181.11051 40.4375 135.875
C 40.4375 108.96723 54.484434 85.34937 75.875 70.40625 L 75.875 26.5 z"),
#back: Path.fromSVG("M 263.3125 540.1875 C 259.0635 540.1875
255.625 543.626 255.625 547.875 C 255.625 552.124 259.0635 555.5625
263.3125 555.5625 C 267.5615 555.5625 271 552.124 271 547.875
C 271 543.626 267.5615 540.1875 263.3125 540.1875 z
M 263.3125 541.3125 C 266.9335 541.3125 269.875 544.254 269.875
547.875 C 269.875 551.497 266.9345 554.4375 263.3125 554.4375
C 259.6905 554.4375 256.75 551.498 256.75 547.875 C 256.75 544.254
259.6915 541.3125 263.3125 541.3125 z M 262.96875 544.15625
C 262.80187 544.15625 262.628 544.21575 262.5 544.34375 L 259.4375
547.40625 C 259.4295 547.41225 259.4425 547.4305 259.4375 547.4375
C 259.4225 547.4505 259.386 547.45375 259.375 547.46875
C 259.368 547.47875 259.382 547.487 259.375 547.5 C 259.365
547.513 259.35175 547.5465 259.34375 547.5625 C 259.33675 547.5765
259.3185 547.58175 259.3125 547.59375 C 259.3075 547.60575 259.3165
547.616 259.3125 547.625 C 259.3095 547.633 259.28325 547.65125
259.28125 547.65625 C 259.27425 547.66925 259.28525 547.6735
259.28125 547.6875 C 259.27525 547.7055 259.255 547.70075 259.25
547.71875 C 259.246 547.73175 259.252 547.76725 259.25 547.78125
C 259.248 547.79825 259.25 547.82475 259.25 547.84375 C 259.248
547.85775 259.25 547.861 259.25 547.875 C 259.249 547.893 259.248
547.9195 259.25 547.9375 C 259.252 547.9505 259.248 547.95375 259.25
547.96875 C 259.252 547.98475 259.245 547.984 259.25 548 C 259.254
548.014 259.27825 548.0485 259.28125 548.0625 C 259.28625 548.0795
259.27425 548.07775 259.28125 548.09375 C 259.28225 548.09875
259.27925 548.122 259.28125 548.125 C 259.28425 548.134 259.3085
548.14825 259.3125 548.15625 C 259.3195 548.17125 259.33575 548.1715
259.34375 548.1875 C 259.35175 548.2005 259.33475 548.236 259.34375
548.25 C 259.35175 548.262 259.363 548.27225 259.375 548.28125
C 259.385 548.29625 259.4245 548.2985 259.4375 548.3125 C 259.4425
548.3175 259.4315 548.33775 259.4375 548.34375 L 262.5
551.375 C 262.628 551.503 262.80275
551.59375 262.96875 551.59375 C 263.13575 551.59375 263.27925 551.503
263.40625 551.375 C 263.66025 551.121 263.66025 550.72475 263.40625
550.46875 L 261.46875 548.53125 L 267.53125 548.53125
C 267.71225 548.53125
267.85175 548.46275 267.96875 548.34375 C 268.08675 548.22575 268.1875
548.055 268.1875 547.875 C 268.1875 547.739 268.1325 547.605 268.0625
547.5 C 267.9445 547.327 267.75725 547.21875 267.53125
547.21875 L 261.46875
547.21875 L 263.40625 545.28125 C 263.66025 545.02625 263.66025
544.59875 263.40625 544.34375 C 263.27875 544.21575 263.13563
544.15625 262.96875 544.15625 z"),
#menu: Path.fromSVG("M 384.125 667.96875 C 383.575 667.96875 383.125
668.41975 383.125 668.96875 L 383.125 670.96875 C 383.125 671.51875
383.575 671.96875 384.125 671.96875 L 398.125 671.96875
C 398.675 671.96875 399.125 671.51875 399.125 670.96875
L 399.125 668.96875 C 399.125 668.41975 398.675 667.96875 398.125
667.96875 L 384.125 667.96875 z M 384.125 673.96875
C 383.575 673.96875 383.125 674.41975 383.125 674.96875
L 383.125 676.96875 C 383.125 677.51875 383.575 677.96875
384.125 677.96875 L 398.125 677.96875 C 398.675 677.96875 399.125
677.51875 399.125 676.96875 L 399.125 674.96875
C 399.125 674.41975 398.675 673.96875 398.125 673.96875
L 384.125 673.96875 z M 384.125 679.96875 C 383.575 679.96875
383.125 680.41975 383.125 680.96875
L 383.125 682.96875 C 383.125 683.51875 383.575 683.96875
384.125 683.96875 L 398.125 683.96875 C 398.675 683.96875
399.125 683.51875 399.125 682.96875 L 399.125 680.96875
C 399.125 680.41975 398.675 679.96875 398.125 679.96875
L 384.125 679.96875 z"),
#refresh: Path.fromSVG("M 551.6875 477.375 C 548.2465 477.375 545.46875
480.183 545.46875 483.625 L 544.53125 483.625 C 544.09925
483.619 543.92925 483.91425 544.15625 484.28125 L 546 487.25
C 546.228 487.616 546.60775 487.612 546.84375 487.25 L 548.75
484.3125 C 548.985 483.9515 548.806 483.66225 548.375
483.65625 L 547.375 483.65625 C 547.37467 483.64609 547.375
483.63526 547.375 483.625 C 547.374 481.243 549.3045 479.3125
551.6875 479.3125 C 552.7415 479.3125 553.71775 479.71575
554.46875 480.34375 L 555.5625 478.71875 C
554.5005 477.87975 553.1425 477.375 551.6875 477.375
z M 557.03125 479.3125 C 556.87887 479.31075 556.743 479.3815
556.625 479.5625 L 554.6875 482.5 C 554.4525 482.863 554.6315
483.15125 555.0625 483.15625 L 556.0625 483.1875
C 556.06282 483.19759 556.0625 483.20857 556.0625 483.21875 C 556.0635
485.60175 554.133 487.53125 551.75 487.53125 C 550.696
487.53125 549.751 487.13 549 486.5 L 547.875 488.09375
C 548.937 488.93275 550.295 489.4375 551.75 489.4375 C 555.191
489.4375 558 486.66175 558 483.21875 L 558.90625
483.21875 C 559.33625 483.22475 559.50825 482.9305 559.28125 482.5625
L 557.4375 479.59375 C 557.324 479.41075 557.18363
479.31425 557.03125 479.3125 z");
};
if (paint == null) {
paint = new Paint();
paint.color = 0xffffffff;
}
var bar = new View();
bar.visible = true;
bar.stretch = #horz;
bar.height = System.height / 10;
bar.onDraw = :sender, canvas { canvas.clear(0xff041b33); }
for(var def in buttons) {
var button = new Button(icons[def.icon], paint);
button.height = button.width = bar.height - app.scaleFactor*12;
button.onClick = def.onClick;
bar.add(button.native, #front);
}
var layout = new StackLayout();
layout.align = #center;
layout.pack = #center;
layout.orientation = #horz;
layout.setMargin(app.scaleFactor*6,
app.scaleFactor*6, app.scaleFactor*6, app.scaleFactor*6);
layout.spacer = app.scaleFactor*20;
bar.layout = layout;
return bar;
}
The main list will be displayed by “HomeView“ class. It will read the list from the servers and match it up with products’ images which will be performed simultaneously when required:
function _doProductDraw(item, canvas)
{
var product = item.product;
canvas.clear(0xeeffffff); if (product.image == null) {
product.image = #loading;
item.tick = System.tick;
item.angle = 0;
function onSuccess(image) {
product.image = image;
item.invalidate();
}
function onError(message) {
product.image = #error;
log("ERROR", message);
}
Server.retrieveProductImage
(product, PRODUCT_IMAGE_WIDTH, 184, onSuccess, onError);
} else if (product.image == #loading) {
var matrix = new Matrix();
matrix.setRotate(item.angle, app.loading.width / 2,
app.loading.height / 2);
matrix.postTranslate((PRODUCT_IMAGE_WIDTH -
app.loading.width)/2, (item.height - app.loading.height)/2);
canvas.drawBitmapMatrix(app.loading, matrix);
} else if (product.image instanceof Bitmap) {
canvas.drawBitmap(product.image, 0, 0);
}
canvas.drawText(product.name, 150, 30, this._productHeadPaint);
canvas.drawTextBox(product.description, 150, 40,
System.width - 5*app.scaleFactor, item.height,
this._productDescrPaint, #start);
canvas.drawText(String.printf("$ %.2f", product.price),
150, item.height - 15, this._productPricePaint);
canvas.drawLine(0, item.height, item.width, item.height, this._productHeadPaint);
if (product.oos) Button._drawPath(canvas, this._oosIcon,
this._productHeadPaint, 32, System.width - (40+40+40) -
5*app.scaleFactor, item.height - 40);
Button._drawPath(canvas, this._cartIcon, this._productHeadPaint,
32, System.width - (40+40) - 5*app.scaleFactor, item.height - 40);
Button._drawPath(canvas, this._detailIcon, this._productHeadPaint,
32, System.width - 40 - 5*app.scaleFactor, item.height - 40);
}
The product details are implemented by ProductView class. Currently, this class shows only a chosen product, product description, price, etc. For demonstration purposes, we will show icons of “add to cart”, product review, etc. Once we click on the arrow located in the right bottom of row, we will see a detailed description of our product.
function _onProductsRetrieved(products)
{
this._list.angle = -1; for(var p in products) {
var item = new View();
item.visible = true;
item.stretch = #horz;
item.height = PRODUCT_HEIGHT;
item.product = p;
item.onProcess = function(sender) {
if (sender.product.image ==
#loading && System.tick - sender.tick > 40) {
sender.tick = System.tick; sender.angle += 15;
sender.invalidate();
}
}
item.onPointerReleased = function(sender, x, y) {
if (x > System.width - 120 -
5*app.scaleFactor && y > sender.height - 40)
app.push(new ProductView(sender.product)); }
item.onDraw = function(sender, canvas)
{ this super._doProductDraw(sender, canvas); }
this._list.add(item);
}
}
Accessing files from server will be accomplished by using “Server” class- simple web client, who gets access to data located on the server (JSON), and then, it will send callbacks as parameters.
class Server
{
const HOST = "services.moscrif.com";
const PORT = 80;
const RES_PRODUCTS = "/sample-eshop/getProducts.aspx";
const RES_PRODUCT_IMAGE = "/sample-eshop/getProductImage.aspx?id=%d&w=%d&h=%d";
function retrieveProducts(onSuccess, onError)
{
var wc = new WebClient();
wc.open(Server.HOST, Server.PORT, false, ""); wc.onError = function() { onError(this.errorMessage); }
wc.onReceiving = function(received, total)
{ console << String.printf
("Receving products data (%d bytes)\n", received); }
wc.onReceived = function() {
var receivedContent = this.data.toString("utf8");
onSuccess(parseData(receivedContent));
}
wc.getData(Server.RES_PRODUCTS);
}
function retrieveProductImage(product, width, height, onSuccess, onError)
{
var wc = new WebClient();
wc.open(Server.HOST, Server.PORT, false, "");
wc.onError = function() { onError(String.printf
("Error retrieving productId=%d! %s", product.id, this.errorMessage)); }
wc.onReceiving = function(received, total) {
console << String.printf
("Receiving image data (%d bytes)\n", received) << "\n";
}
wc.onReceived = function() {
var img = null;
try {
img = Bitmap.fromBytes(this.data);
onSuccess(img);
} catch(e) {
onError(String.printf
("Error loading image for productId=%d! %s",
product.id, e.message));
}
}
wc.getData(String.printf(Server.RES_PRODUCT_IMAGE,
product.id, width, height));
}
}
You can also watch this video to see how the installation files are generated.
Conclusion
Mobile client is only a prototype of real application which can be further optimized or developed with additional features like “add to cart”, product review, etc. Please stay tuned for future articles where we will demonstrate how to save data locally using SQLite database.