DEV Community

Michael Ishri
Michael Ishri

Posted on

Hijack the Back button on Android in NativePHP

While testing my NativePHP Android application, I noticed that the back button / action wasn't behaving the way I would expect for a regular "native" application.

I realised that it was behaving like a browser with it's history stack. If I navigated into sub-menus using the UI then started using the back button / action it would just take me back through my history instead of up the menu tree like a regular app would, so I thought I'd look into how to fix that behaviour.

After a quick Google search, the answer ended up being a simple, single line change. By adding the following to your MainActivity.kt file, we can instead emit an event in JavaScript which can then be reacted to.

webView.evaluateJavascript("window.dispatchEvent(new Event(\'native:backpressed\'))", null)
Enter fullscreen mode Exit fullscreen mode

Caution: If you take this approach, you need to make sure that EVERY page includes a way to handle the back button / action otherwise you could lock your user in a screen without being able to escape (except by killing the app or going to the Android home screen).

Let's we take a look at the original method surrounding this.

onBackPressedDispatcher.addCallback(this) {
    val webView = binding.webView

    if (webView.canGoBack()) {
        webView.goBack()
    } else {
        finish()
    }
}
Enter fullscreen mode Exit fullscreen mode

Then the modified one.

onBackPressedDispatcher.addCallback(this) {
    val webView = binding.webView

    if (webView.canGoBack()) {
        webView.evaluateJavascript("window.dispatchEvent(new Event('native:backpressed'))", null)
    } else {
        finish()
    }
}
Enter fullscreen mode Exit fullscreen mode

Now in all my Blade views, I can simply add.

<script>
    window.addEventListener('native:backpressed', function(event) {
        window.location.href = '/home'
    });
</script>
Enter fullscreen mode Exit fullscreen mode

Caveat

There is one big caveat to making this change. Whenever the NativePHP team push updates, we may need to run native:install --force which will overwrite the changes.

So to workaround this issue, I've created an Artisan console command that can perform the replacement automatically which you would need to run after native:install.

// app/Console/Commands/InterceptBackButton.php

<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;
use Illuminate\Support\Facades\File;

class InterceptBackButton extends Command
{
    public $replaced = false;

    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'android:intercept-back-button';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'Changes the default behaviour of the Android Back button.';

    /**
     * This helper replaces the default behaviour of the back button on Android.
     * Instead of behaving like a browser, this will instead emit a JavaScript event in your application which can be reacted to.
     * You may listen for the `native:backpressed` event.
     */
    public function handle()
    {
        $files = collect(File::allFiles(base_path('nativephp/android/app/src/main')))
            ->filter(fn($file) => $file->getFilename() === 'MainActivity.kt')
            ->map->getRealPath();

        foreach($files as $file) {
            $contents = File::get($file);

            $updated = str_replace(
                'webView.goBack()',
                'webView.evaluateJavascript("window.dispatchEvent(new Event(\'native:backpressed\'))", null)',
                $contents
            );

            if($contents !== $updated) {
                File::put($file, $updated);
                $this->info("Success, performed replacement in: $file");
                $this->replaced = true;
            }
        }

        if($this->replaced == false) {
            $this->error('Fail, replacement of intercept code couldn\'t be performed');
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

I've also extracted the JavaScript portion into the following Blade component.

{{-- resources/views/components/return-to.blade.php --}}

@props(['route'])

<script>
    window.addEventListener('native:backpressed', function(event) {
        window.location.href = '{{ $route }}'
    });
</script>
Enter fullscreen mode Exit fullscreen mode

Which you can use in your view like this;

    ....
    <x-return-to route="{{ route('home') }}" />
</body>
Enter fullscreen mode Exit fullscreen mode

Conclusion

This is really just a proof-of-concept, I haven't performed extensive testing and it may need more work to make it more robust (for example, how do you exit the app once you can't go back any further), but the concept is out there now.

This exercise has really opened my eyes to the possibilities which is really exciting.

Top comments (0)