Objective

TLDR: Get your download link  here

The objective of this excercise will be to show how our Barcode Scanner solution could be used in a real-life app that's built using the cross platform framework Ionic.

Ionic is a cross platform framework for hybrid app development, it utilizes html/javascript to present the view of a mobile app, so on paper it's pretty welcoming for javascript developers. It pairs with the PhoneGap/Cordova plugins which give access to the native part of a given platform, therefor the user is capable of building apps that performance wise are very close to the native experience, while providing the user with reusable code that can be used across platforms. Currently Ionic has 2 active versions, Ionic 1 uses angularJS, which is on it's way out, while Ionic 2 is using angularJS 2.

Ionic 2 has embrassed typescript, which is best described as "JavaScript on steroids". It is a strict superset of JavaScript, and adds optional static typing and class-based object-oriented programming to the language. The downside of using it, is maybe the need to "compile" it to regular JavaScript, so the browsers can understand it, which does add complexity to the development process.

There was a plan to show an Ionic 1 example, but  we decided to focus on the future, so we will be presenting only Ionic 2 examples.

First Things First

Navigate your browser here. It's a github repository of the project, so the easiest path to take is to clone the project to your computer. You should already have the neccessary build tools for developing Ionic apps, but in case you missed on that, here's a list of things you will need:

 

Once that is out of the way, you should clone our project to a location of your choosing on your computer. So let's clone it like this:

git clone https://github.com/manateeworks/manateeworks-barcodescanner-ionic2-starter.git mwIonic2Blog

Pasted image at 2017_03_16 10_31 PM_298794634

This should create a folder mwIonic2Blog on your computer, where the files contained in this project will be copied. Go ahead and cd to that folder:

cd mwIonic2Blog

The project has it's dependencies set in the package.json file.

It's very possible that at that time you build this app, the versions that it currently depends on will be deprecated, our team tries to keep projects up to date, but it's quite possible that you may need to bump a version of the dependencies to a more recent one, and it's also good to know what you are building on, so let's have a look in the package.json file:

package.json_2090296229

From the image, you can notice that our project depends on several cordova plugins, one of them - the Manatee Works Cordova plugin, which is the integral part of our app. We install a few other "default" plugins, some are not even necessary but they all get installed if you create a clean ionic 2 app, so we will be including them as well. To install all these dependancies we do:

npm install

This will make npm do its thing, and when done if everything goes as expected you should see a screen similar to:

npm-install_820409183

If there are errors, npm should give a decent debugging info, but this is beyond the scope of this tutorial, so we will just cross fingers and hope it all goes well for you :).

After we've installed our npm dependencies let's build our ionic project:

npm run ionic:build

This will build the ionic app, but we still need to reset the state, so that the platforms will be installed, so let's do:

ionic state reset

This will add the platforms, and the cordova plugins, so you should be looking at an image like this:

state-reset_1037001505

 

This will add platform folders. For each platform there are slight differences, so we will begin with iOS. 

iOS is a little tricky with signing profiles and overall app testing/publishing, so the easiest way to use a signing profile is through XCODE. Navigate to platforms/ios and open the .xcodeproj created for us. To create a signing profile, create one on developer.apple.com, but this is beyond the scope of the app.

Once we have a signing profile for our app, the easiest way to develop an ionic app is to run one on the desired platform with the livereload option active, to achieve this, we can just do:

ionic run ios -l

This will compile our typescript code into normal javascript and copy the files to our device. Keep in mind that the device needs to be unlocked in order to be mounted, and the up run on it.

 

Overview of the solution

 

We will be using sublime for our app development. Let's load the project into sublime and look around. I usually do (this is on mac), like this:

while in the root folder of the project

subl .

After this sublime loads the project structure:

project-structure_1386702776

You could use any text editor or IDE, you can even use XCODE that we previously mentioned, but whatever you chose, you will be interested in the src/app folder. This is where the app files are located.

First thing to note here is the app.module.ts,  this is where we include all of our used components. The manatee-scanner is included here as a provider so we include it with:

import { ManateeScanner} from '../providers/manatee-scanner';

There's a few more imports going on in that file, we will get to them later, for now, let's move to the provider folder where the manatee-scanner provider is located.

In the providers folder you should see two providers: list-factory.ts and manatee-scanner.ts. While list-factory is an important part of the demo app (it stores our lists), we will get to it later, because really the crux of the app is in using the scanner, so let's overview that.

providers/manatee-scanner.ts

The provider is pretty simple, we really just expose one method; 

config(), which expects a config function passed as a parameter, this will be called from our component and used to setup the scanner with parser parameters. The provider manatee-scanner also exposes a scanner property, which is basically the mwScanner object injected in our global namespace by the Cordova framework

import { Injectable } from '@angular/core';
import { Http } from '@angular/http';
import 'rxjs/add/operator/map';

declare var mwbScanner:any;

/*
  Generated class for the ManateeScanner provider.

  See https://angular.io/docs/ts/latest/guide/dependency-injection.html
  for more info on providers and Angular 2 DI.
*/
@Injectable()
export class ManateeScanner {

  mwInit : boolean;
  public scanner : any;

  constructor(public http: Http) {
    console.log('Hello ManateeScanner Provider');
    if(typeof mwbScanner != 'undefined' && typeof mwbScanner.startScanning != 'undefined'){
    	this.scanner = mwbScanner;
    }
    else{
    	this.scanner = {};
    }
  }
  config(setFunc){
  	setFunc.call();
  	console.log('setting the function');
  	this.mwInit = true;

  }

}

app/app.component.ts

The manatee-scanner will be used by the app.component, it will basically setup the scanner, with a key and a settings array (which is optional, and if left empty the scanner will initialize with default settings).

import { Component } from '@angular/core';
import { Platform } from 'ionic-angular';
import { StatusBar, Splashscreen } from 'ionic-native';

import { ManateeScanner} from '../providers/manatee-scanner';
import { HomePage } from '../pages/home/home';


declare var device:any;


@Component({
  templateUrl: 'app.html',
  providers : [ManateeScanner]
})
export class MyApp {
  rootPage = HomePage; //root page

  constructor(platform: Platform,manateeScanner:ManateeScanner) {
    platform.ready().then(() => {
      // Okay, so the platform is ready and our plugins are available.
      // Here you can do any higher level native things you might need.

      manateeScanner.config(()=>{
          manateeScanner.scanner.setCallback((result:any) =>{});
          var mw_c : any =  manateeScanner.scanner.getConstants();
          var settings : any = [
                  {"method" : 'MWBenableZoom', "value" : [true]},
                  // {"method" : 'MWBsetZoomLevels', "value" : [200, 400, 1]},
                  // {"method" : 'MWBsetInterfaceOrientation', "value" : [mw_c.OrientationLandscapeLeft]},
                  // {"method" : 'MWBsetOverlayMode', "value" : [mw_c.OverlayModeImage]}
              ];
          var keys : any = {
              'Android'   : "VALID_ANDROID_KEY",
              'iOS'       : "VALID_IOS_KEY",
              'Win32NT'   : "VALID_WIN_WP8_KEY",
              'windows'   : "VALID_WIN_10_UWP_KEY"
          };
          var key : any = (keys[device.platform])?keys[device.platform]:'';

          return manateeScanner.scanner.setKey(key).then((response:any)=>{
              return manateeScanner.scanner.loadSettings(settings).then((response:any)=>{}).catch((reason:any)=>{
                              console.log(reason)
                          });
          });
        });      

      StatusBar.styleDefault();
      Splashscreen.hide();
    });
  }
}

 

A couple of things are going on here:

  • We import the manatee scanner near the top of the file;
  •  in the Component decorator we add it in the providers array;
  • and then in the platform.ready promise function we setup our provider by calling the config function.

config()

The config function does a couple of things:

  • First, we set the callback function of the scanner result to an empty function. We do this to overwrite the default callback function that comes packed with the manateeworks-barcodescanner Cordova plugin. This will leave us without a callback function, but the scanner returns a promise so will just use that later on.
  • Second, we setup a settings array with the desired settings, and we provide our license keys in the keys array. The device.platform variable should be injected by cordova, so depending on which platform the app currently runs, it will pull in the proper key, when we call setKey
  • Third, we call setKey and then once this is resolved we loadSettings.

 

pages/history-list.ts

This is the spot where we will invoke the scanner, get it to scan a barcode, and then print that value in our list (and save it on the device). So let's have a look of the structure of the page:

history-list.html

<!--
  Generated template for the HistoryList page.

  See http://ionicframework.com/docs/v2/components/#navigation for more info on
  Ionic pages and navigation.
-->
<ion-header>

  <ion-navbar>
    <ion-title>{{list_name}}</ion-title>
  </ion-navbar>

</ion-header>


<ion-content padding>

  <ion-list> 
	  <ion-item-sliding *ngFor="let item of list_data; let i = index"> 
	    <ion-item (click)="viewData(item)"> {{item.code}} </ion-item>
	    <ion-item-options>
	      <button icon-only color="danger" (click)="removeItem(i)"><ion-icon name="trash"></ion-icon></button>
	    </ion-item-options>
	  </ion-item-sliding>

  </ion-list>


<ion-fab bottom right>
	<button ion-fab (click)="startScanner($event)" >
	<ion-icon [name]="scannerActive"></ion-icon> </button>
</ion-fab>
</ion-content>

If you look closer, a couple of things are clear,

  • We print out the previous scans that have been done with the scanner (obviously this will be empty for a new list);
  • We setup a button that starts the scanner <button ion-fab (click)="startScanner($event)" >

Let's see how this view gets used by it's component:

history-list.ts

import { Component, NgZone } from '@angular/core';
import { NavController, NavParams } from 'ionic-angular';
import { ListsFactory} from '../../providers/lists-factory';
import { ManateeScanner} from '../../providers/manatee-scanner';
import { ViewCodePage} from '../view-code/view-code';

@Component({
  selector: 'page-history-list',
  templateUrl: 'history-list.html'
})
export class HistoryListPage {
	list:any;
	id:number;
	list_name:string;
	list_data:any[];
	scannerActive:string ="barcode";	

	constructor(public navCtrl: NavController, public navParams: NavParams, private listsFactory : ListsFactory, private zone: NgZone,private manateeScanner : ManateeScanner) {}

	ionViewDidLoad() {

		this.id = this.navParams.data.listId;

		this.listsFactory.getItem(this.navParams.data.listId).then(list=>{

			this.list = list;
			this.list_name = this.list.name;
			if (this.list.data)
				this.list_data = this.list.data;
			else
				this.list_data = [];
		});
	}
	viewData(item){
		this.navCtrl.push(ViewCodePage,{
			item : item
		});		
	}
	removeItem(item_id){

		this.list_data.splice(item_id,1);
		this.list.data = this.list_data;
		this.listsFactory.setItem(this.id,this.list).then(response=>{
			console.log('saved the list');
		});
	}
	startScanner(event){

		if(typeof this.manateeScanner.scanner != 'undefined' && typeof this.manateeScanner.scanner.startScanning != 'undefined'){
			if (this.scannerActive =="barcode"){
				this.scannerActive = "power";
				this.manateeScanner.scanner.startScanning(0,0,100,50).then(response =>{
				  if(response && response.code){
				    this.zone.run(() => {
				    	this.list_data.push(response);
				    	this.list.data = this.list_data;
				    	this.scannerActive = "barcode";
				    	this.listsFactory.setItem(this.id,this.list).then(response=>{})
		       	
				    });
				  }
				  else{
				    this.zone.run(() => {
				    	this.scannerActive = "barcode";
				    });		      	
				  }
				});
			}
			else{
				this.manateeScanner.scanner.closeScanner();
				this.scannerActive = "barcode";
			}
			
		}
		else{
			this.startDummy();
		}

	}
	startDummy(){
		let timestamp = function(){ return Number(new Date); }
		this.scannerActive = "power";
		let response:any = {
			"code":"mwbScanner not available @ " + timestamp(),
			"location" : {},
			"type" : "dummy"
		};

		this.list_data.push(response);
		this.list.data = this.list_data;
		this.listsFactory.setItem(this.id,this.list).then(response=>{
			this.scannerActive = "barcode";
		})
	}	

}

Now, obviously there's a lot of things going on here, so let's start from the top;

  • the view loads we pull the history list data, from the device (this is the data that we print in the history-list view).
  • we provide methods for deleting a list item and viewing a list item
  • most importantly we provide the startScanner method which is bind to the click event of the button we mentioned earlier in the history-list.html view
  • the startScanner function simply uses the methods provided by mwbScanner which is now available through the property on the provider (this.manateeworks.scanner).
  • the result from the scan is handled in the promise function and we use zone.js to update the view list 
  • there is also a startDummy function, which is used during app development on a browser. Ionic provides a method to run the app on a browser on the development computer, and it's much easier to style the app with Chrome/Firefox/Safari than doing it directly on the device, even though the device is livereloaded each time there is a change in one of the typescript files.

That pretty much covers setting up the scanner and using it. Now let's see the other components that are part of the solution.

pages/home

This is the component that is loaded first, it handles the mutiple lists that we can create as containers for our scan results. In the view of the home component we have:

<ion-header>
  <ion-navbar>
    <ion-title>
      MWB Scanner 
    </ion-title>

  </ion-navbar>
</ion-header>
<ion-content padding>
  <ion-list> 
	  <ion-item-sliding *ngFor="let item of lists; let i = index" #slidingList>
	    <ion-item (click)="navSomewhere(i)"> {{item.name}} </ion-item>
	    <ion-item-options>
	      <button icon-only color="danger" (click)="removeItem(i)" side="right"><ion-icon name="trash"></ion-icon></button>
	      <button icon-only color="danger" (click)="editListModal(i,item,slidingList)"><ion-icon name="create"></ion-icon></button>
	    </ion-item-options>
	  </ion-item-sliding>
  </ion-list>


<ion-fab bottom right>
	<button ion-fab (click)="showNewListModal()">
		<ion-icon name="add"></ion-icon></button> 
</ion-fab>		  

</ion-content>

A couple of things here are worth mentioning:

  • we provide option buttons for each list element, one lets us edit the list's name, the other deletes the list. The option elements are accessed by swiping the list item
  • we provide a button to create a new list
  • when a tap/click occurs on the list item, it navigates to the list-history component by invoking the navSomewhere() method

The component for this view looks like:

import { Component, NgZone } from '@angular/core';
import { ModalController, NavController, ItemSliding } from 'ionic-angular';
import { HistoryListPage } from '../history-list/history-list';
import { ListsFactory} from '../../providers/lists-factory';
import { ListModify} from './listmodify';




@Component({
  selector: 'page-home',
  templateUrl: 'home.html'
})
export class HomePage {
	constructor(public navCtrl: NavController,public modalCtrl: ModalController, private zone: NgZone, private listsFactory:ListsFactory) {
		this.listsFactory.getAll().then(lists=>{
			this.lists = lists;
		});
	}
	historyListPage = HistoryListPage;
	lists:any;

	navSomewhere(id){
		this.navCtrl.push(HistoryListPage,{
			listId : id
		});
	}

	showNewListModal(){
		let newListModal = this.modalCtrl.create(ListModify,{});
		newListModal.present();
		newListModal.onDidDismiss(data => {
			if(data)
				this.lists = data.lists;
		})
	}
	editListModal(id,list,slidingList : ItemSliding){
		let editListModal = this.modalCtrl.create(ListModify,{id:id,list:list});
		editListModal.present();
		editListModal.onDidDismiss(data => {
			if(data)
				this.lists = data.lists;

			slidingList.close();
		})

	}

	removeItem(id){
		console.log(id);
		this.listsFactory.removeItem(id).then(result=>{
			this.lists = result;
		})
	}
}

You can notice right away:

  • navSomewhere() method, which will navigate our app to the list-history component,
  • showNewListModal() which will start a modal window that will let us create the list by providing a list's name,
  • editListModal() - presents a modal view for modifying a list. Both showNewListModal and editListModal are based on the listmodify page
  • removeItem() - which deletes the list

home/listmodify

This is a component that we use for showing a modal windows, it's basically a form with a form group (just the name of the list). It takes care of the actions like saving the list and validating the user input. It does that by providing a few methods:

import { Component } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { NavParams, ViewController } from 'ionic-angular';
import { ListsFactory} from '../../providers/lists-factory';

@Component({
	selector: 'list-modify',
	templateUrl: 'listmodify.html'
})
export class ListModify {

editListName: FormGroup;
id:Number;
lists:any;
list:any;
mode:string = 'add';

 constructor(private viewCtrl: ViewController, params: NavParams, formBuilder: FormBuilder, private listsFactory : ListsFactory) {

	this.listsFactory.getAll().then(lists=>{
		this.lists = lists;

	});


	if(params.data.list){
		this.mode = 'edit';
		this.id = params.data.id;
		this.list = params.data.list;
	}


	this.editListName = formBuilder.group({
		listName : [(this.mode=='add')?'':params.data.list.name,Validators.compose([Validators.maxLength(30), Validators.pattern('[a-zA-Z0-9 ]*'), Validators.required])]
	})

 }
 dismiss(){
 	this.viewCtrl.dismiss(false);
 }
 save(){


 	if(this.mode == 'add'){
	 	if(this.editListName.valid){
			let id:any = this.lists.length;
			this.listsFactory.setItem(id,{"name":this.editListName.value.listName}).then(lists=>{
				this.lists = lists;
				this.viewCtrl.dismiss({"lists":this.lists});
			});
	 	}
 	}
 	else{
	 	if(this.editListName.valid){

	 		let list:any = this.list;
	 		
	 		list.name = this.editListName.value.listName;

			this.listsFactory.setItem(this.id,list).then(lists=>{
				this.lists = lists;
				this.viewCtrl.dismiss({"lists":this.lists});
			});
	 	} 		

 	}

 }
}

So if it's a new list, we don't send the list id as a parameter and this automatically clears the form for input. The editlistName form item is a part of a formBuilder group and appropriate validation measures are imposed on it (we want to know if the user is trying to input a list with an empty name, we also limit it to 30 alphanumerical characters)

save() method gives us two options:

  • adding a new list
  • editing an old one.

If it's an old list then we change the listName and we use the listFactory provider to save it. If it's a new list, we just add create a new id and add it with the listFactory provider (action is similar to the edit action).

Last but not least, let's mention the listFactory provider

providers/list-factory.ts

Main thing to mention here is that we employ the help of the storage object.

import { Storage } from '@ionic/storage';

will import the Storage, which we can use in our listFactory as such:

import { Injectable } from '@angular/core';
import { Http } from '@angular/http';
import 'rxjs/add/operator/map';
import { Storage } from '@ionic/storage';


/*
  Generated class for the ListsFactory provider.

  See https://angular.io/docs/ts/latest/guide/dependency-injection.html
  for more info on providers and Angular 2 DI.
*/
@Injectable()
export class ListsFactory {

	storage:Storage = new Storage();
	lists:any[] = [];
	list:any;

	constructor(public http: Http) {
		console.log('Hello ListsFactory Provider');


		this.storage.get('lists').then( lists => {
			this.lists = lists;
		}).catch(error =>{
			console.log(error);
		})
	}
	setItem(id,list){
		if(Array.isArray(this.lists))
			this.lists[id] = list;
		else{
			this.lists = [];
		}
		return this.saveLists();


	}
	removeItem(id){
		this.lists.splice(id,1);
		return this.saveLists();
	}
	saveLists(){

		return this.storage.set("lists" , this.lists).then(result =>{
			console.log('response result'+result);
			return this.lists;
		});		

	}
	getItem(id){

		return this.storage.get("lists").then(lists => {
			if(!Array.isArray(lists))
				lists = [];

			this.list = lists[id];
			return this.list;
		});
	}
	getAll(){
        return this.storage.get('lists').then(lists => {

			if(!Array.isArray(lists))
				lists = [];

			this.lists = lists;
			return this.lists;
        });
	}

}

We then add a couple of methods that deal with both adding lists and actually saving items (barcode scan results) to a list

  • setItem() handles both: setting a list or adding items (barcode results) to a list, this can be observed both in listmodify.ts (where we save lists) and history-list.ts where we save barcode results
  • getAll() will pull all the lists
  • removeItem() will remove a list item

Conclusion

When all is said and done in the end we are left with a demo app capable of:

  • Creating lists
  • Invoking a Manatee Works Scanner
  • Storing results from a scan action
  • Viewing results
  • Deleting both results and lists that contain those results

All of this while using Ionic 2 as a platform. The tutorial should show some basic operations and options that an ionic developer has while using our scanner, it's by no means meant as a production app, and it probably contains more than a few bugs, but it does get the point across.

Happy coding with the Manatee Works Barcode Scanner

Ionic 2 Demo App that utilizes Manatee Works Barcode Scanner and stores barcode scan results in a list.
xcode,sublime,ionic,scanner
2017-03-20
2017-03-20
Manateeworks Ionic Starter App
Manateeworks Ionic Starter App
ManateeWorks
info@manateeworks.com
Mobile apps need to be increasingly more engaging to succeed. Manatee Works can help. Our mobile barcode scanner SDK is available on all major mobile development platforms.
ManateeWorks