NativeScript: Deep dive into Activity/Application overrides

ManDiving
(c) Cody Hough (http://www.flickr.com/people/IvanTortuga/)

NativeScript 1.7.0 released a new feature that several people have been waiting for with baited breath.   Well 1.7.0 is out, and so I just had to see how this new feature worked.

Unfortunately its implementation does leave a bit to be desired; but I fully understand the constraints so at the moment we will over look the bad to see what we get in return for the new ability.

First of all you now have access to the raw JAVA code that makes up the Activity and Application class.  WooHoo!   This will allow you to extend the app easily in the cases you actually need to add something to the onCreate or add some missing callback like onRequestPermissions (Android 6+ permissions).

There are THREE way to extend your NativeScriptApplication or NativeScriptActivity

  1. Directly modify the Java File
  2. Directly modify the JS file
  3. Create a new JS file (and modify the Java File to point to new JS file)

1 & 2 are typically the easiest, as all you do is modify it and you are done.    So lets look into the Java part of this first...

Method 1:

The NativeScriptActivity.java and NativeScriptApplication.java are located at:

/YourAppFolder/platforms/android/src/main/java/com/tns

As you might have guessed this means it will be replaced and or deleted when you do destructive things with your platforms folder, and of course they aren't under version control like the rest of your app.  So sad, we now have another file outside of version control when we finally got all the others under version control in 1.6...

But with that annoyance, comes the GOOD! You now have FULL access to the FULL Java classes that make up the two core startup items which are where you normally need to put any special initialization items.  And in all reality most the time most people won't have to change these files ever!

Lets look at parts of the NativeScriptApplication.java file:

@JavaScriptImplementation(javaScriptFile = "app/tns_modules/application/application.js")
public class NativeScriptApplication extends android.app.Application implements com.tns.NativeScriptHashCodeProvider {

  public void onCreate() {
    java.lang.Object[] params = null;
    com.tns.Platform.callJSMethod(this, "onCreate", void.class, params);
  }

  public void onTrimMemory(int level) {
    java.lang.Object[] params = new Object[1];
    params[0] = level;
    com.tns.Platform.callJSMethod(this, "onTrimMemory", void.class, params);
  }
}

You see that line @JavaScriptImplementation(javaScriptFile = ...) this is where the JavaScript side of the class exists.   As you can see, it is located in your "Application.js" file in the common core folder.  That file now contains the JS side of the class also.   You can point this to another location if you want; BUT you will need to make sure that you implement everything that already exists in the existing class in the Application.js file.

If you are only planning on adding a startup item, then you probably just need to add it to the onCreate and you are done.

Now if you are planning on adding a missing feature; this is where it gets a bit more complicated.   You have two choices, pure Java, or you can have the Java call into your JavaScript class and you can do all your stuff in JavaScript.   If it is pure Java, then you just need to add your new function to the file, and again you are done.   If you want to implement it so that you can manage all your new code on the JavaScript side; then you create your function in Java; but you need to add a couple lines of boilerplate to make everything work.

Lets look at this boiler plate code:

java.lang.Object[] params = null;
// OR
java.lang.Object[] params = new Object[1];

The params is how many parameters you are passing onto your JS side.  Typically it will be the same number that this function received.  So if your function received no parameters, then you are going to pass null in.  If your function received one parameter you use the new Object[1], then the following lines you assign each of the params to the values they need to be for example: params[0] = <your parameter>.

The key line is the last line you see in both function above:

com.tns.Platform.callJSMethod(this, "onTrimMemory", void.class, params);

This is what calls into the JS runtime, the first parameter should always be this.  The second parameter is the name of the function you will be running on the JavaScript side.  So in this case it will be looking for the "onTrimMemory" function in the javascript side of the application class.  The third parameter is what you are expecting in return.  void.class means no return value.  But you can do things like boolean.class to say you are expecting a boolean value back.   And the final parameter is the object array of parameters you are passing into the JavaScript side.  Please note; if you do NOT implement this function on the JavaScript side now; your application WILL crash and burn when it goes to try to call it.  So, after you add it here, you need to add it into the JavaScript side of things.

Method 2:

Now that we have the Java side of the class done, lets look at the JavaScript side of the class.  For the NativeScriptApplication the file is located at: /YourAppFolder/node_modules/tns-core-modules/application/application.android.js and for the NativeScriptActivity it is located at /YourAppFolder/node_modules/tns-core-modules/ui/frame/frame.android.js.

Now as you might have guessed; these are in the core modules; do you know what my number one rule with NativeScript is?  DON'T EVER CHANGE THE CODE INSIDE THE CORE MODULES!!!   Ugh, Ugh, Ugh, the only way to add your code is to change the core modules.  :very very sad face:

Mihail, I love the new feature but I hate where it resides by default!  Can we please, change this...

I now need a 10 minute break to compose myself.    I can't bear to face myself in the mirror as I actually tell you; "you have to change the code in the core modules" to make this work.    So very very sad...   But in all other cases NEVER change the code in the core modules.

! WARNING !
By changing any code in the core modules you risk your application totally creating massive worm holes to suck your phone and computer to another dimension and/or maybe just  crashing.  At the point you update or reinstall the core modules all your changes will be LOST!
! WARNING !

Ok, now with that warning maybe I can face myself again, you will find in the application.js file the following class toward the top:

var NativeScriptApplication = (function (_super) {
    __extends(NativeScriptApplication, _super);
    function NativeScriptApplication() {
        _super.call(this);
        return global.__native(this);
    }
    NativeScriptApplication.prototype.onCreate = function () {
        androidApp.init(this);
        setupOrientationListener(androidApp);
    };
    NativeScriptApplication.prototype.onLowMemory = function () {
        gc();
        java.lang.System.gc();
        _super.prototype.onLowMemory.call(this);
        typedExports.notify({ eventName: typedExports.lowMemoryEvent, object: this, android: this });
    };
    NativeScriptApplication.prototype.onTrimMemory = function (level) {
        gc();
        java.lang.System.gc();
        _super.prototype.onTrimMemory.call(this, level);
    };
    NativeScriptApplication = __decorate([
        JavaProxy("com.tns.NativeScriptApplication")
    ], NativeScriptApplication);
    return NativeScriptApplication;
}(android.app.Application));

As you can see the calls are called into the the function of the same name, and depending on what that function requires you might need to call the _super version of the function as part of your function.

Now to be clear; your best bet is to do only the very very minimal work in this class and have all your code actually in a function inside another file that you use require to load.  So for example say I needed to do some initialization at startup; I would do this:

Line 1: var myAppInitCode = require('myAppInitCode.js');
inside the onCreate() function I would add:  MyAppInitCode.onCreate();

Pretty simple, as an additional safety, mechanism I would also add a dead-mans-switch to this file.  So basically it goes something like this:

var hasRan = false;
exports.onCreate() {
hasRan = true;
// Do Init code
}

setTimeout(function() {
if (hasRan) return;
// ... require dialog code ...
dialog ('Hey stupid, you must have overwrote the application.js file again!'); }
, 1000);

And then you include this file in your app.js file also.   hasRan will be set to true during initialization if your code is getting ran by the init process.  If you messed up and lost your changes; then the setTimeout code will be triggered by the inclusion in the app.js file and of course you will hit your head against the wall and say thank God you thought to verify your code is still running instead of spending hours trying to figure out why feature x broke...  😀

This way in the event the application code is overwritten, it is now fairly simple for you to recover; if you add all your init code directly to the application.js file; you are SOL when you accidentally update to try the 1.7.1 fixes.

So now you can see how to do the first two ways of modifying your application; the third way is "potentially" the better way to do it as it currently stands. However it requires a bit more work; and creates a different way for bad bugs to be introduced.   But then it also allows you keeps the majority of the code under your control and eliminates the majority of the overwriting risks.

Method 3:

The third way is to create your own NativeScriptApplication and NativeScriptActivity class in JavaScript files you have in your project folder.     So the steps to do this are:

  1. Open up the Application.js file and COPY EVERYTHING for the NativeScriptApplication class as it exists in the application.js file; you CAN NOT extend the existing one.   You must have your own version and it MUST implement all the same functionality of the one that you will be "replacing".  (Do you see the risk yet?)
  2. Create a new file in your project to contain the NativeScriptApplication class; you can either have one file for both classes or you can have separate files for both classes; this is really up to you.  (Do you see the risk yet?)
  3. Change the source Java file, than line we spoke about towards the top where it says

@JavaScriptImplementation(javaScriptFile = "app/tns_modules/application/application.js")

And point it to where your file resides.   This will cause the runtimes to load your version of the NativeScriptApplication and/or NativeScriptActivity as the primary classes.  If you mess up; it will use the original versions.  So this might throw you off, if you put the path incorrectly.

Please note that if you prefer TypeScript you can copy the TypeScript code from the frame/application directly from the nativescript repo.  Just make sure when you copy it you include the line:

@JavaProxy("com.tns.NativeScriptActivity")

with it as it is required for the fixing up to make it become the correct class.

Risks:

So now that you see the three ways let me outline the risks for each method:

Method 1: It requires changes to the .JAVA files; these WILL be overwritten and/or deleted when doing anything with the TNS platforms command.   So back them up!    HOWEVER NEVER use the backup file as-is.   When you install a new version of the Runtimes, these original Java files may CHANGE, which means you really need to copy your code out of your old backup version into the new version.   For a real example; I just committed a pull request yesterday to deal with OnRequestPermssions (for android 6).  This means that when/if they accept it, the NativeScriptActivity.java file will CHANGE to include the new code needed to make it work.  If you blindly copy your old backup NativeScriptActivity.java file over the new one you risk crashing, inconstant behavior and/or the feature not working.  So you must "patch" the .JAVA file each time.

Method 2: Has the same issue as #1, other than it is with the JavaScript files.  Make a backup but DO NOT use the backup as-is.  You must copy your code out of the backup and apply it to the new version, otherwise you REALLY risk major issues.  The application & frame files have a lot more changes to them each patch than the .JAVA counterparts.   So if you were to copy your backup from 1.6.0 over the 1.7.0 version your application probably would crash at startup.  Not a good idea.   So to re-iterate; you must copy your changes from the backup to the new version of the file.

Method 3: You not only have the same risks as method 1, but your additional risk is that you don't get a change to the JavaScript NativeScriptActivity or NativeScriptApplication.   Remember, you are pointing the Java to use your version of the JS classes.   Taking my example above; once my pull request is accepted, the Java and JS parts of the Activity class will change.  If you get the new Java changes; and then fix it to point to your awesome version of the JS side of the class; guess what happens?    CRASH at startup!!!   You be missing the all new OnRequestPermission code in your awesome version.    In addition you will also be missing any bug fixes that were done to the classes in the original copies.

So you kinda are deciding which poison pill you want to take.  Me personally, I think three is fraught with more land mines as you have to keep your "versions" of the classes up to date, you mess up here and its game over.  But on the other hand it is safer (for version control) in that none of your code is in the core modules which I REALLY hate having to change...

Some notes:

With the current design is that plugins have no ability to tie in or change these files.  If I create a plugin that replaces any of these files, then I risk overwriting your changes, or a newer version, so these files are really for all practical purposes off limits to plugins.  🙁

For anyone wanting to get the new API 23+ permissions; I do have a pull request in for them to be put into the core files, but if you want it today; go look at the changes I did and you can easily add it to these files I mentioned above.    I also have a NativeScript-permissions plugin that will come out shortly that will make requesting perms easy for everybody including other plugins.  It will return a promise; that promise will be resolved either true or false and then you can choose what to do after that.

 

5 comments

    1. Mihail,

      Awesome, thanks for the warning. This file may be Auto-Generated in the future! Auto-generated; that sounds very interesting..... 🙂

  1. Hi Nathan, I tested method 1 of changing the source :
    public void onCreate() {
    new RuntimeHelper(this).initRuntime();
    java.lang.Object[] args = null;
    com.tns.Runtime.callJSMethod(this, "onCreate", void.class, args);
    Log.d("debug","getInstance");
    }

    It supposed to display the log, but it seems never happened, seems like it never get compiled, (I do the tns run android)

    Is this not the correct way ?

    And what if I add another class in that folder will it be integrated also (instead of changing the existing file ?)

    Thanks

    1. 1. Are you using v1.7?
      2. Have you tried de-installing the app on the phone/emulator?
      3. Sorry, I have no idea if you add any additional Java classes in the folder if they will be compiled

Leave a Reply to Lázaro Menezes Cancel reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.