Extending Application and Activity
This article describes how to create custom
android.app.Application
and
androidx.appcompat.app.AppCompatActivity
implementations in a NativeScript application.
Note: Demo code below is taken from the Android Extend demos for plain JavaScript, TypeScript or Angular applications.
Philosophy
Because NativeScript is a JavaScript-to-Native framework, our
main goal is to make as much as possible from the underlying
native platforms easy to implement in JavaScript. Initially we
discussed the option where developers would write Java code to
achieve some more special cases like custom
androidx.appcompat.app.AppCompatActivity
implementations but then we agreed that we should explore a
JavaScript approach first and only if it is not possible to
fallback to native code. It turned to be pretty easy, especially
with the
Static Binding Generator (SBG)
tool.
Overview
The SBG analyzes JavaScript files and generates the
corresponding Java files (or what we call bindings). Prior to
the 2.1 release, the core modules provided custom
Activity
and
Application
implementations but these were tightly
coupled with the other logic within the modules, making custom
implementations close to impossible to achieve. For 2.1 we made
some refactoring, with the solely purpose to shape the modules
more as a library rather than a framework. In other words - to
decouple the Activity
implementation from the
frame.android.ts
file and to completely remove the need for a custom
Application
class. With these changes, the modules
can now work with custom Activity
implementations.
The modules will still need to get notified for some
Activity
events in order to work properly. These events are described in the AndroidActivityCallbacks interface.
Extending Application
The following steps are needed to create custom native
android.app.Application
object and use it, instead
of the default com.tns.NativeScriptApplication
one,
provided in the empty project template of the Android Runtime.
-
Create a new JavaScript file in your
app
folder - name itapplication.android.js
Note the
*.android
suffix - we want this file packaged for Android only. -
Declare the extend:
const superProto = android.app.Application.prototype; // the first parameter of the `extend` call defines the package and the name for the native *.JAVA file generated. android.app.Application.extend("org.myApp.Application", { onCreate: function() { superProto.onCreate.call(this); // At this point modules have already been initialized // Enter custom initialization code here }, attachBaseContext: function(base) { superProto.attachBaseContext.call(this, base); // This code enables MultiDex support for the application (if needed compile androidx.multidex:multidex) // androidx.multidex.MultiDex.install(this); } });
// the `JavaProxy` decorator specifies the package and the name for the native *.JAVA file generated. @JavaProxy("org.myApp.Application") class Application extends android.app.Application { public onCreate(): void { super.onCreate(); // At this point modules have already been initialized // Enter custom initialization code here } public attachBaseContext(baseContext: android.content.Context) { super.attachBaseContext(baseContext); // This code enables MultiDex support for the application (if needed) // androidx.multidex.MultiDex.install(this); } }
-
Modify the
application
entry within theAndroidManifest.xml
file found in the<application-name>app/App_Resources/Android/
folder:<application android:name="org.myApp.Application" android:allowBackup="true" android:icon="@drawable/icon" android:label="@string/app_name" android:theme="@style/AppTheme" >
This modification is required by the native platform; it basically tells Android that your custom
Application
class will be used as the main entry point of the application. -
In order to build the app with webpack, the extended Android application should be added as an entry to the
webpack.config.js
file.entry: { bundle: entryPath, application: "./application.android", },
In this way, the source code of
application.android.ts
is bundled separately asapplication.js
file which is loaded from the nativeApplication.java
class on launch.The
bundle.js
andvendor.js
files are not loaded early enough in the application launch. That's why the logic inapplication.android.ts
is needed to be bundled separately in order to be loaded as early as needed in the application lifecycle.Note: This approach won't work if
aplication.android.ts
requires external modules.
Extending Activity
The core modules ship with a default
androidx.appcompat.app.AppCompatActivity
implementation, which ensures they alone are sufficient to
bootstrap an empty NativeScript application, without forcing
users to declare their custom Activity
in every
project. When needed, however, users may still specify custom
Activity
implementation and use it to bootstrap the
application. The following code demonstrates how this can be
done:
-
Create a new JavaScript file in your
app
folder - name itactivity.android.js
Note the
*.android
suffix - we want this file packaged for Android only. -
Declare the extend:
const frame = require("tns-core-modules/ui/frame"); const superProto = androidx.appcompat.app.AppCompatActivity.prototype; androidx.appcompat.app.AppCompatActivity.extend("org.myApp.MainActivity", { onCreate: function(savedInstanceState) { // Used to make sure the App is inited in case onCreate is called before the rest of the framework appModule.android.init(this.getApplication()); // Set the isNativeScriptActivity in onCreate (as done in the original NativeScript activity code) // The JS constructor might not be called because the activity is created from Android. this.isNativeScriptActivity = true; if(!this._callbacks) { frame.setActivityCallbacks(this); } // Modules will take care of calling super.onCreate, do not call it here this._callbacks.onCreate(this, savedInstanceState, this.getIntent(), superProto.onCreate); // Add custom initialization logic here }, onNewIntent: function (intent) { this._callbacks.onNewIntent(this, intent, superProto.setIntent, superProto.onNewIntent); }, onSaveInstanceState: function(outState) { this._callbacks.onSaveInstanceState(this, outState, superProto.onSaveInstanceState); }, onStart: function() { this._callbacks.onStart(this, superProto.onStart); }, onStop: function() { this._callbacks.onStop(this, superProto.onStop); }, onDestroy: function() { this._callbacks.onDestroy(this, superProto.onDestroy); }, onPostResume: function () { this._callbacks.onPostResume(this, superProto.onPostResume); }, onBackPressed: function() { this._callbacks.onBackPressed(this, superProto.onBackPressed); }, onRequestPermissionsResult: function (requestCode, permissions, grantResults) { this._callbacks.onRequestPermissionsResult(this, requestCode, permissions, grantResults, undefined); }, onActivityResult: function (requestCode, resultCode, data) { this._callbacks.onActivityResult(this, requestCode, resultCode, data, superProto.onActivityResult); } /* Add any other events you need to capture */ });
import {setActivityCallbacks, AndroidActivityCallbacks} from "tns-core-modules/ui/frame"; @JavaProxy("org.myApp.MainActivity") class Activity extends androidx.appcompat.app.AppCompatActivity { public isNativeScriptActivity; private _callbacks: AndroidActivityCallbacks; public onCreate(savedInstanceState: android.os.Bundle): void { // Set the isNativeScriptActivity in onCreate (as done in the original NativeScript activity code) // The JS constructor might not be called because the activity is created from Android. this.isNativeScriptActivity = true; if (!this._callbacks) { setActivityCallbacks(this); } this._callbacks.onCreate(this, savedInstanceState, this.getIntent(), super.onCreate); } public onSaveInstanceState(outState: android.os.Bundle): void { this._callbacks.onSaveInstanceState(this, outState, super.onSaveInstanceState); } public onStart(): void { this._callbacks.onStart(this, super.onStart); } public onStop(): void { this._callbacks.onStop(this, super.onStop); } public onDestroy(): void { this._callbacks.onDestroy(this, super.onDestroy); } public onBackPressed(): void { this._callbacks.onBackPressed(this, super.onBackPressed); } public onRequestPermissionsResult(requestCode: number, permissions: Array<string>, grantResults: Array<number>): void { this._callbacks.onRequestPermissionsResult(this, requestCode, permissions, grantResults, undefined /*TODO: Enable if needed*/); } public onActivityResult(requestCode: number, resultCode: number, data: android.content.Intent): void { this._callbacks.onActivityResult(this, requestCode, resultCode, data, super.onActivityResult); } }
Note the
this._callbacks
property. It is automatically assigned to your extended class by theframe.setActivityCallbacks
method. It implements the AndroidActivityCallbacks interface and allows the core modules to get notified for importantActivity
events. It is mandatory to call back to the modules through this interface, to ensure their proper initialization. -
Modify the
activity
entry within theAndroidManifest.xml
file found in the<application-name>app/App_Resources/Android/
folder:<activity android:name="org.myApp.MainActivity" android:label="@string/title_activity_kimera" android:configChanges="keyboardHidden|orientation|screenSize">
-
In order to build the app with webpack, the absolute path to the file where the Android activity is extended should be added to the
appComponents
array.const appComponents = [ "tns-core-modules/ui/frame", "tns-core-modules/ui/frame/activity", resolve(__dirname, "app/activity.android.ts"), ];
In this way and with the default config, these components get in the common vendor.js chunk and are required by the
android-app-components-loader
.