Loading local JSON issue with Sencha, PhoneGap and Android 4.0.3

This is just a little tip when trying to load local .json files using the Ext.data.Store ajax proxy type. The following code will work in Android 4.0.3+ but you need to be sure to add an extra couple lines to get it to work, noCache: false and enablePagingParams: false. Without those lines the code still works fine in Android 2.3.3 but for Android 4 and above, the ajax fails on loading local .json files because the requests url includes parameters added by Sencha.

Roads DataStore

Ext.define("Roads", {
    extend:"Ext.data.Store",
    requires:"Ext.data.proxy.LocalStorage",
    config:{
        fields: [
            { name: 'id', type: 'int' },
            { name: 'title', type: 'string' },
            { name: 'subtitle', type: 'string' }
        ],
        proxy:{
            type:'ajax',
            url:'resources/data/roads.json',
            reader:{
                type:'json',
                           noCache: false,
            enablePagingParams: false,
                rootProperty:'roads'
            }
        },
        listeners:{
            load:function (s, r) {
                console.log(r)
            }
        }
    }
});

roads.json

{"roads":[
    {
        "id":"i10",
        "title":"Interstate 10 Arizona",
        "subtitle":"Papago Freeway/Maricopa Freeway"
    },
    {
        "id":"i17",
        "title":"Interstate 17 Arizona",
        "subtitle":"Black Canyon Freeway"
    },
    {
        "id":"l101",
        "title":"Arizona State Route 101",
        "subtitle":"Loop 101"
    }
]}

References where the solution was found:

http://www.sencha.com/forum/showthread.php?190878-Android-local-json-store-not-loading

http://www.sencha.com/forum/showthread.php?162322-Sencha-Touch-2-PhoneGap-are-not-working-on-Android-4/page3

-Mister

Handle Android hardware back button using PhoneGap and Sencha Touch 2

Lately I have been experimenting with PhoneGap and Sencha Touch 2 recently and discovered an issue with accessing the hardware back button for Android applications. What you want is the back button to navigate back through the views of the application rather than exit, unless of course you are on the first screen of your application. This is the default user experience on Android phones when pressing the hardware back button, so it’s a good thing to implement it.

My example is a modification of the Notes application Sencha Touch 2 example (found at http://miamicoder.com/2012/how-to-create-a-sencha-touch-2-app-part-1/) for PhoneGap delivery. This is a great multi-part series that walks you through the Sencha Touch 2 MVC framework to create a simple Notes application.
I used a Sencha Routes to navigate the history and a PhoneGap method for overriding the back button behavior for Android.

The first step was getting Sencha to play nice with PhoneGap when developing for Android 4+. The trick is to import each of your JS files within the index.html.

<!DOCTYPE html>
 <html>
 <head>
     <meta charset="utf-8">
     <meta name="viewport" content="width=device-width, initial-scale=1">

     <title>PhoneGap Notes Application</title>
     <link rel="stylesheet" href="css/master.css" type="text/css">
     <link rel="stylesheet" href="lib/resources/css/sencha-touch.css" type="text/css">

     <script type="text/javascript" charset="utf-8" src="lib/cordova-1.8.1.js"></script>

     <script type="text/javascript" src="lib/builds/sencha-touch-all-compat.js"></script>

     <!-- this had to be done for Android 4+ to work -->
     <script type="text/javascript" src="app/model/Note.js"></script>
     <script type="text/javascript" src="app/store/Notes.js"></script>
     <script type="text/javascript" src="app/controller/Notes.js"></script>
     <script type="text/javascript" src="app/view/NotesListContainer.js"></script>
     <script type="text/javascript" src="app/view/NotesList.js"></script>
     <script type="text/javascript" src="app/view/NoteEditor.js"></script>

     <script type="text/javascript" src="app.js"></script>
 </head>
 <body></body>
 </html>

Now we set up the app.js file to add the hooks for capturing the Android back button:

app.js

Ext.application({
    name: "NotesApp",
    models: ["Note"],
    stores: ["Notes"],
    controllers: ["Notes"],
    views: ["NotesList", "NotesListContainer", "NoteEditor"],

    launch: function () {

        var notesListContainer = {
            xtype: "noteslistcontainer"
        };
        var noteEditor = {
            xtype: "noteeditor"
        };

        Ext.Viewport.add([notesListContainer, noteEditor]);

        // set up a listener to handle the back button for Android 
        if (Ext.os.is('Android')) {
          document.addEventListener("backbutton", Ext.bind(onBackKeyDown, this), false);  // add back button listener

          function onBackKeyDown(e) {
              e.preventDefault();

              // you are at the home screen
              if (Ext.Viewport.getActiveItem().xtype == notesListContainer.xtype ) {
                  navigator.app.exitApp();
              } else {
                  this.getApplication().getHistory().add(Ext.create('Ext.app.Action', {
                      url: 'noteslistcontainer'
                  }));
              }
          }
       }
    }
});

I used the Ext.bind method so I could have access to “this” within the scope of the “onBackKeyDown” function. Notice that if the application is not currently on the “notesListContainer” view port we add a new url to the application history. This is part of using Sencha Routes, we will later use this in the main controller to navigate the application in the Notes.js controller file:

app/controller/Notes.js

Ext.define("NotesApp.controller.Notes", {

    extend: "Ext.app.Controller",
    config: {
        refs: {
            // We're going to lookup our views by xtype.
            notesListContainer: "noteslistcontainer",
            noteEditor: "noteeditor"
        },
        control: {
            notesListContainer: {
                // The commands fired by the notes list container.
                newNoteCommand: "onNewNoteCommand",
                editNoteCommand: "onEditNoteCommand"
            },
            noteEditor: {
                // The commands fired by the note editor.
                saveNoteCommand: "onSaveNoteCommand",
                deleteNoteCommand: "onDeleteNoteCommand",
                backToHomeCommand: "onBackToHomeCommand"
            }
        },
        routes: {
            'noteslistcontainer': 'activateNotesListenerPage'
        }
    },

    // override back button navigation and navigate back if viewing note editor
    activateNotesListenerPage: function ()
    {
        this.activateNotesList();
    },

    // handle new note command
    onNewNoteCommand: function ()
    {
        console.log("onNewNoteCommand");

        var now = new Date();
        var noteId = (now.getTime()).toString() + (this.getRandomInt(0, 100)).toString();

        var newNote = Ext.create("NotesApp.model.Note", {
            id: noteId,
            dateCreated: now,
            title: "",
            narrative: ""
        });

        this.activateNoteEditor(newNote);
    },

    // handle edit note command
    onEditNoteCommand: function (list, record) {

        console.log("onEditNoteCommand");
        this.activateNoteEditor(record);

    },

    // handle save note command
    onSaveNoteCommand: function () {

        console.log("onSaveNoteCommand");

        var noteEditor = this.getNoteEditor();

        var currentNote = noteEditor.getRecord();
        var newValues = noteEditor.getValues();

        // Update the current note's fields with form values.
        currentNote.set("title", newValues.title);
        currentNote.set("narrative", newValues.narrative);

        var errors = currentNote.validate();

        if (!errors.isValid()) {
            Ext.Msg.alert('Wait!', errors.getByField("title")[0].getMessage(), Ext.emptyFn);
            currentNote.reject();
            return;
        }

        var notesStore = Ext.getStore("Notes");

        if (null == notesStore.findRecord('id', currentNote.data.id)) {
            notesStore.add(currentNote);
        }

        notesStore.sync();

        notesStore.sort([{ property: 'dateCreated', direction: 'DESC'}]);

        this.activateNotesList();
    },
    onDeleteNoteCommand: function() {
        console.debug("onDeleteNoteCommand");

        navigator.notification.confirm(
            'You want to delete this note?',  // message
            Ext.bind(onConfirm, this),              // callback to invoke with index of button pressed
            'Warning',            // title
            'No,Yes'          // buttonLabels
        );

        // process the confirmation dialog result
        function onConfirm(selectedButtonIndex) {
            if(selectedButtonIndex == '2') {
                var noteEditor = this.getNoteEditor();
                var currentNote = noteEditor.getRecord();
                var notesStore = Ext.getStore("Notes");
                notesStore.remove(currentNote);
                notesStore.sync();
                this.activateNotesList();
            }
        }
    },

    onBackToHomeCommand: function ()
    {
        console.log("onBackToHomeCommand");
        this.activateNotesList();
    },

    // EditNote Class functions
    getRandomInt: function (min, max)
    {
        return Math.floor(Math.random() * (max - min + 1)) + min;
    },

    activateNoteEditor: function (record)
    {
        var noteEditor = this.getNoteEditor();
        noteEditor.setRecord(record); // load() is deprecated.

        this.getApplication().getHistory().add(Ext.create('Ext.app.Action', {
            url: 'noteslistcontainer/noteeditor'
        }));

        Ext.Viewport.animateActiveItem(noteEditor, this.slideLeftTransition);
    },

    activateNotesList: function ()
    {
        Ext.Viewport.animateActiveItem(this.getNotesListContainer(), this.slideRightTransition);
    },

    // transitions
    slideRightTransition: { type: 'slide', direction: 'right' },
    slideLeftTransition: { type: 'slide', direction: 'left' },

    // Base Class functions.
    launch: function () {
        this.callParent(arguments);
        Ext.getStore("Notes").load();
        console.log("launch");
    },
    init: function () {
        this.callParent(arguments);
        console.log("init");
    }
});

Here we setup a route within the config that will call the method “activateNotesList” when the url “noteslistcontainer” is added to the history. If the user creates/edits a new note, the method in the controller that navigates the application to the NoteEditor view adds to the history a new url for “noteslistcontainer/noteeditor”.

Hitting the Android hardware back button from the NoteEditor view is captured by our “onBackKeyDown” handler in the app.js file. Since the active container is not the NotesListContainer view, the history url will be set to “noteslistcontainer” and the method “activateNotesList” will be fired. If our active container was the NotesListContainer, the application would exit as normal.

That is pretty much it. The rest of the code isn’t modified from the Notes example (I didn’t do step 5 of that series). I am sure there is probably more than a few ways to implement the proper behavior for the Android hardware back button, but for me this works. If you have any other suggestions or improvements, do pass them along.

Source References:

Sencha Touch Routes and Back Button
How to create a Sencha Touch 2 app

-Mister