Our website is made possible by displaying online advertisements to our visitors. Please consider supporting us by disabling your ad blocker.

Override The Android Back Button In A NativeScript With Angular Application

TwitterFacebookRedditLinkedInHacker News

Have you ever needed to perform a certain action when the user tries to hit the back button or exit out of your application on Android devices? For example, what if the user was able to back out of your application and you wanted to show a dialog. Or what if you have a video application and you wanted to pause the video when they tap the back button?

Being able to override the functionality of the back button on Android can do great things for your application as long as you don’t abuse it.

We’re going to see how to create a mobile application with NativeScript and Angular that demonstrates overriding the back button on Android, whether that be hardware or software.

Before we see some code, I want to be clear that you should not abuse the ability to override the back button on Android. This also applies to developer mistakes. If the user cannot exit your application, you risk your application being removed from Google Play or receiving a negative review from the user.

Create a New NativeScript with Angular Application

To keep this tutorial easy to understand, we’re going to work with a fresh project. With the NativeScript CLI installed and configured, execute the following command to create a new project:

tns create android-back-button-project --ng

The above command will create a project titled, android-back-button-project, and it will use Angular rather than NativeScript Core or Vue.js. To be successful with this particular project, we won’t need any external dependencies.

Catching the Back Button Interaction Everywhere

When it comes to overriding the back button, there isn’t a whole lot too it. We can choose to override on a particular page or on all pages. For this example, we’re going to override on all pages.

Take a moment to remember how routing in Angular works. We have an app.component.html file that all routing passes through. For this reason, it makes sense to add our logic to the TypeScript file. Open the project’s app.component.ts file and include the following:

import { Component, OnInit } from "@angular/core";
import * as application from "tns-core-modules/application";

@Component({
    selector: "ns-app",
    templateUrl: "app.component.html"
})
export class AppComponent implements OnInit {

    public constructor() { }

    public ngOnInit() {
        application.android.on(application.AndroidApplication.activityBackPressedEvent, (args: any) => {
            args.cancel = false;
        });
    }

}

In the above code, notice that we are importing application which contains a bunch of TypeScript type definitions for working with native Android and iOS code.

Once the application has initialized, we can set up a listener for Android:

application.android.on(application.AndroidApplication.activityBackPressedEvent, (args: any) => {
    args.cancel = false;
});

The above listener will listen for the activityBackPressedEvent event, which is the back button. When the args.cancel value is false the back button will carry on as normal. When the value is true, the back button won’t do the default activity.

A Realistic Use-Case for Overriding the Android Back Button in NativeScript

The above sample code is great, but it doesn’t really get us anywhere in terms of a use-case. Instead, let’s come up with a quick example that might be beneficial for us.

When routing with NativeScript and Angular, there can be some confusion around child routes. For example, if you have three parent routes and three child routes, the back button, by default, will only navigate the parent routes. This means if you are two levels deep in child routes, the back button will navigate too far. We can actually prevent this behavior by overriding how the back button performs.

We already have a project created, so we’ll take advantage of it. Go ahead and create the following files and folders:

mkdir app/parent1-component
mkdir app/parent2-component
mkdir app/child1-component
mkdir app/child2-component
touch app/parent1-component/parent1.component.ts
touch app/parent1-component/parent1.component.html
touch app/parent2-component/parent2.component.ts
touch app/parent2-component/parent2.component.html
touch app/child1-component/child1.component.ts
touch app/child1-component/child1.component.html
touch app/child2-component/child2.component.ts
touch app/child2-component/child2.component.html

If you don’t have the mkdir and touch commands in your command line, go ahead and create the directories and files however you see fit. The files are empty as of now, but let’s put down the ground work in our routing and modules files.

Open the project’s app/app.module.ts file and include the following:

import { NgModule, NO_ERRORS_SCHEMA } from "@angular/core";
import { NativeScriptModule } from "nativescript-angular/nativescript.module";
import { AppRoutingModule } from "./app.routing";
import { AppComponent } from "./app.component";

import { Child2Component } from "./child2-component/child2.component";
import { Child1Component } from "./child1-component/child1.component";
import { Parent1Component } from "./parent1-component/parent1.component";
import { Parent2Component } from "./parent2-component/parent2.component";

@NgModule({
    bootstrap: [
        AppComponent
    ],
    imports: [
        NativeScriptModule,
        AppRoutingModule
    ],
    declarations: [
        AppComponent,
        Parent1Component,
        Parent2Component,
        Child1Component,
        Child2Component
    ],
    providers: [],
    schemas: [
        NO_ERRORS_SCHEMA
    ]
})
export class AppModule { }

Notice that we’ve only imported our soon to be created components and added them to the declarations array of the @NgModule block. Similarly, open the project’s app/app.routing.ts file and include the following:

import { NgModule } from "@angular/core";
import { Routes } from "@angular/router";
import { NativeScriptRouterModule } from "nativescript-angular/router";

import { Child2Component } from "./child2-component/child2.component";
import { Child1Component } from "./child1-component/child1.component";
import { Parent1Component } from "./parent1-component/parent1.component";
import { Parent2Component } from "./parent2-component/parent2.component";

const routes: Routes = [
    { path: "", redirectTo: "/parent1", pathMatch: "full" },
    { path: "parent1", component: Parent1Component },
    {
        path: "parent2/:id",
        component: Parent2Component,
        children: [
            { path: "", component: Child1Component },
            { path: "child2/:id", component: Child2Component },
        ]
    }
];

@NgModule({
    imports: [NativeScriptRouterModule.forRoot(routes)],
    exports: [NativeScriptRouterModule]
})
export class AppRoutingModule { }

Again, we’ve imported the components and we’ve designed routes for them. The goal here is to have two parent routes and two child routes so that we can demonstrate navigation in a backwards direction. We’ll start at parent1, navigate to parent2 which defaults to child1, navigate to child2, then completely backwards with the overridden back button.

If you’d like to learn specifically about routing with child components, check out my tutorial titled, Nested Routing in a NativeScript Angular Application for Android and iOS.

We’re going to populate each of the files with code in the order that they are accessed. Starting with the parent1.component.ts file, include the following:

import { Component, OnInit } from "@angular/core";

@Component({
	selector: "Parent1Component",
	moduleId: module.id,
	templateUrl: "./parent1.component.html"
})
export class Parent1Component implements OnInit {

	public constructor() { }

	public ngOnInit() { }

}

There is nothing particularly interesting with the above TypeScript, so we’re going to look at the HTML found in the project’s parent1.component.html file:

<ActionBar title="Parent1" class="action-bar">
</ActionBar>
<GridLayout>
	<ScrollView class="page">
		<StackLayout class="home-panel">
			<Button text="Navigate to Parent2" [nsRouterLink]="['/parent2', 33]"></Button>
		</StackLayout>
	</ScrollView>
</GridLayout>

In the HTML we have a single button that will navigate us to the second parent and pass a value. We don’t technically need to pass any values around, but it is a cool extra thing we can learn about.

Now that we’re on the second parent, we can look at the TypeScript found in the parent2.component.ts file:

import { Component, OnInit } from "@angular/core";
import { ActivatedRoute } from "@angular/router";

@Component({
	selector: "Parent2Component",
	moduleId: module.id,
	templateUrl: "./parent2.component.html"
})
export class Parent2Component implements OnInit {

	public constructor(private route: ActivatedRoute) {
	}

	public ngOnInit() {
		this.route.params.subscribe(params => {
			console.log("Parent2 ID: " + params["id"]);
		});
	}
}

This file is more or less pretty basic as well with the exception that we’re printing out the passed value from the previous parent page. The second parent in this example is only a passthrough so we have the following single line in our parent2.component.html file:

<router-outlet></router-outlet>

The first child will look more or less the same as our second parent. Take a look at the child1.component.ts file:

import { Component, OnInit } from "@angular/core";
import { ActivatedRoute } from "@angular/router";

@Component({
	selector: "Child1Component",
	moduleId: module.id,
	templateUrl: "./child1.component.html"
})
export class Child1Component implements OnInit {

	public constructor(private route: ActivatedRoute) { }

	public ngOnInit() {
		this.route.params.subscribe(params => {
			console.log("Parent2 ID: " + params["id"]);
		});
	}
}

Even though we’re on a child page, we’re choosing to print out the parent ID value. The HTML to pair looks like the following:

<ActionBar title="Child1" class="action-bar">
</ActionBar>
<GridLayout>
	<ScrollView class="page">
		<StackLayout class="home-panel">
			<Button text="Navigate to Child2" [nsRouterLink]="['/parent2/33/child2', 77]"></Button>
		</StackLayout>
	</ScrollView>
</GridLayout>

In this case, we have a single button. We’ve chosen to hard-code values, but we’re navigating to the second child, from the second parent. We’re using the parent ID and passing a new child ID. Again, ID values really have no relevance in this tutorial, but it is something that might value you in the future.

The final child is a bit different. Take the child2.component.ts file for example:

import { Component, OnInit } from "@angular/core";
import { ActivatedRoute } from "@angular/router";

@Component({
	selector: "Child2Component",
	moduleId: module.id,
	templateUrl: "./child2.component.html"
})
export class Child2Component implements OnInit {

	public constructor(private route: ActivatedRoute) { }

	public ngOnInit() {
		this.route.parent.params.subscribe(paramsParent => {
			console.log("Parent2 ID: " + paramsParent["id"]);
			this.route.params.subscribe(paramsChild => {
				console.log("Child2 ID: " + paramsChild["id"]);
			})
		});
	}
}

We’re printing out the parent value that was passed as well as the child value that was passed. This is useful if you have long extravagant routing and components in your application. To wrap things up, let’s look at the child2.component.html file:

<ActionBar title="Child2" class="action-bar">
</ActionBar>

<ScrollView class="page">
	<StackLayout class="home-panel">
		<!--Add your page content here-->
	</StackLayout>
</ScrollView>

It is an empty layout because we don’t really care for this example. Remember, everything I just showed you is more or less setup for the goal that we’re trying to accomplish. We don’t care about ID values, or the components. We just needed some parent routes and some child routes. Remember, the default back button behavior navigates parent routes, not child routes.

Let’s revisit that app.component.ts file from earlier:

import { Component } from "@angular/core";
import * as application from "tns-core-modules/application";
import { RouterExtensions } from "nativescript-angular/router";

@Component({
    selector: "ns-app",
    templateUrl: "app.component.html",
})

export class AppComponent {

    public constructor(private router: RouterExtensions) { }

    public ngOnInit() {
        application.android.on(application.AndroidApplication.activityBackPressedEvent, (args: any) => {
            if (this.router.canGoBack()) {
                args.cancel = true;
                this.router.back();
            } else {
                args.cancel = false;
            }
        });
    }

}

When the Android back button is pressed, we check to see if we can navigate backwards. In other words, is the previous screen a component, or is it going to exit the application.

If we can go back, we’ll use the Angular router to do so, not the default device behavior. If we’re at the end of the line, we’ll let the default behavior take over and exit the application.

Conclusion

You just saw more than you asked for when it came to overriding the Android software and hardware back button functionality in a NativeScript with Angular application. Being able to do this with native code and JavaScript is very useful, but can easily be abused, either on purpose or by accident. Be careful how you’re using the Android back button and make sure it doesn’t ruin your user experience.

If you want to learn more about routing and passing data around, check out my tutorial titled, Navigating a NativeScript App with the Angular Router.

Nic Raboy

Nic Raboy

Nic Raboy is an advocate of modern web and mobile development technologies. He has experience in C#, JavaScript, Golang and a variety of frameworks such as Angular, NativeScript, and Unity. Nic writes about his development experiences related to making web and mobile development easier to understand.