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

About these ads

7 thoughts on “Handle Android hardware back button using PhoneGap and Sencha Touch 2

  1. Hi, I’m new on phonegap dev. I’m using dreamweaver cs6 to build mobile solution with phonegap as a compiler. So the output can handle for many devices (android, blackberry, iOS, etc..)

    I’m starting with simple site. One site have a multiple page.

    This is the design of my site.

    Page1 | Page2 | ….. Page 5
    My question is when the position in the Page5, and when I push the backbutton from device, it will back to prev page and its good.

    But when the position in Page1, and again I push the backbutton, the prev page still show.

    What I want is when the position in Page1 and when I push the backbutton, the application shoild be closed.

    Would you help me to solve my problem?

    Sorry for my english, ’cause I’m from Indonesia.

    Thankyou very much.

    • You need to listen for the back button action and then call to the Android system to close the application, try something like this in your index.html or wherever main JavaScript file. You will probably need to check or keep track of which page you are on so you know when to fire the command to exit:

      <script type="text/javascript">
               // Call onDeviceReady when PhoneGap is loaded.
               //
               // At this point, the document has loaded but phonegap-1.0.0.js has not.
               // When PhoneGap is loaded and talking with the native device,
               // it will call the event `deviceready`.
               //
               document.addEventListener("deviceready", onDeviceReady, true);
      
               // set up a listener to handle the back button on Android
               document.addEventListener("backbutton", onBackKeyDown, false);  // add back button listener
      
               // PhoneGap is loaded and it is now safe to make calls PhoneGap methods
               function onDeviceReady()
               {
                  navigator.notification.alert('Your application is ready!');
               }
      
               function onBackKeyDown(e)
               {
                   navigator.app.exitApp(); // and exit the application
               }
          </script> 
      
  2. Great, Thanks!
    In the end I didn’t do all that coding, just an Alert asking if you wanted to close the app.
    Maybe for version 1.2 of the app :)

  3. hey there, do u need to do sencha build for this set of code before deploying it to android phone?

Comments are closed.