Can you create cross-platform native apps with only one language?

Can you create cross-platform native apps with only one language?

·

8 min read

Whoa hold on! What are you talking about?

Trust me it's possible... Alright the title was a little catchy I know it is not actually native more of like a native feeling. But hold on don't go away yet. I have a question! What are your needs in this native app?

Does it includes one of the following?

  • Integration with other apps
  • Hardware specific feature
  • System settings

Then yes native like apps are not for you. Native apps are mostly developed due to hardware specific feature.

But if you don't need any of the above then this blog is for you.

So what is it you hyped me for? 🤔

I'm talking about Hybrid apps, specificly it's called Progressive Web Application, PWA for short. It is developed in JavaScript. It is also...

  • A small cross-platform app
  • Less network traffic in comparison with websites
  • The cool way to develop applications on the Web!

Yes PWAs are running in browsers which are cross platform so you don't have to learn multiple languages or pay multiple developer teams to build an app for every one.

Do you want to see a live demo of a PWA?

This sounds promising right? Well it is until your user uses the browser with PWA support.

Képernyőfotó 2022-01-16 - 11.48.21.png

Safari, Firefox and Opera doesn't support it on desktop. On mobile almost every browsers support it except Opera. Yes I know Firefox has an extension which will grant you access to the features of PWA, but not everybody uses extensions or know how do they work.

Firefox PWA extension

Good news is most users use chrome for browsing: Képernyőfotó 2022-01-16 - 20.02.27.png

PWA uses most features of ServiceWorker API

So if your user's browser doesn't support PWA API it may have some good features still.

ServiceWorker_API_caniuse.png

SIDE NOTE: My opionon is web will take over more users in the near future due to it's cross availability. Also we web developers are waiting for browsers to implement more features securely. And let the backward compatibilites behind from 1980s and 1990s (like WebGL 1.0). Tell me what you think about this in the comments please.

Talking about features what do we have?

  • Every API your browser support! (USB, Bluetooth, HID, File, etc..)
  • Caching this will reduce traffic a lot, pointless requests just to load the view of your app are gone with cache!
  • Offline usability, because of cached data!
  • Your users can decide to use your app with or withouth installing it.
  • The app can send notifications to your users (push notifications are possible too)!
  • You can run the app with a native app feeling so you don't see the search bar and arrows etc. there are multiple options fullscreen, standalone or minimal ui.
  • Desktop icon. Cool right!? 😎

What do you need to run a PWA? 🚀

  • Manifest file
  • Service Worker
  • Any kind of web app developed or bundled in JavaScript

Manifest file

It is a json file describing basic settings for your app, like how should it be displayed, what name, icon and theme color you want. There is a deep description about it on web.dev .

Service Worker script

A script running in the background. It caches data and will refresh the content off that cache when it is needed. When is it needed? Whenever you want it to, for example if there is any bytes whom changed in your Service Worker file or on every Monday. But you have the power in your hand to tell the app. The following code can be found at Google's developer site. Also web.dev has a pretty good explanation with a game helping to visualize the process of the service worker.

The Code

Main.js

if ('serviceWorker' in navigator) {
  window.addEventListener('load', function() {
    navigator.serviceWorker.register('/SW.js').then(function(registration) {
      // Registration was successful
      console.log('ServiceWorker registration successful with scope: ', registration.scope);
    }, function(err) {
      // registration failed :(
      console.log('ServiceWorker registration failed: ', err);
    });
  });
}

Only fires when browser supports ServiceWorker API:

if ('serviceWorker' in navigator) {

On page load we register the SW.js file which is a Promise with parameters of 2 callbacks (success, error):

 navigator.serviceWorker.register('/SW.js')
 .then(function(registration) {
     // Do smh on success
   }, function(err) {
     // Do smh on error
 });

SW.js

var CACHE_NAME = "my-site-cache-v1";
var urlsToCache = ["/", "/styles/main.css", "/script/main.js"];

self.addEventListener("install", function (event) {
  // Perform install steps
  event.waitUntil(
    caches.open(CACHE_NAME).then(function (cache) {
      console.log("Opened cache");
      return cache.addAll(urlsToCache);
    })
  );
});

self.addEventListener("fetch", function (event) {
  event.respondWith(
    caches.match(event.request).then(function (response) {
      // Cache hit - return response
      if (response) {
        return response;
      }

      return fetch(event.request).then(function (response) {
        // Check if we received a valid response
        if (!response || response.status !== 200 || response.type !== "basic") {
          return response;
        }

        // IMPORTANT: Clone the response. A response is a stream
        // and because we want the browser to consume the response
        // as well as the cache consuming the response, we need
        // to clone it so we have two streams.
        var responseToCache = response.clone();

        caches.open(CACHE_NAME).then(function (cache) {
          cache.put(event.request, responseToCache);
        });

        return response;
      });
    })
  );
});

self.addEventListener("activate", function (event) {
  console.log("Cache is updated!");
});

We need to specify a name for the cache file(s):

var CACHE_NAME = "my-site-cache-v1";

Also specify the caching files:

var urlsToCache = ["/", "/styles/main.css", "/script/main.js"];

The Installing process it has an event listener which will be called on install:

self.addEventListener("install", function (event) {

We wait until our Promise fullfills. caches.open will create a cache with our specified name, which will be available in the promise callback. Then we can add all our file names to be cached in an array. And that cache will be returned.

event.waitUntil(
    caches.open(CACHE_NAME).then(function (cache) {
      console.log("Opened cache");
      return cache.addAll(urlsToCache);
    })
  );

NOTE: If any of the files are failed to be cached the installation process it will fail. For now the error handling doesn't handling this in the API.

Get the offline content calls the fetch event listener:

self.addEventListener("fetch", function (event) {

We need to repsond to the event:

event.respondWith(

Check with a promise if the requested cache exists, if yes return it.

caches.match(event.request).then(function (response) {
      // Cache hit - return response
      if (response) {
        return response;
      }

If the requested cache is not available we can make a network request by returning a fetch pulling the request.

return fetch(event.request);

If the requested cache is not available and you want to cumultatively cache those files. Make a network request with fetch and in the promise we can cache them. An error handling is needed here, in case the files failed to load we return them immediatelly:

return fetch(event.request).then(function (response) {
        // Check if we received a valid response if not  return instantly
        if (!response || response.status !== 200 || response.type !== "basic") {
          return response;
        }

        // Create another stream
        var responseToCache = response.clone();

        caches.open(CACHE_NAME).then(function (cache) {
          cache.put(event.request, responseToCache);
        });

        return response;
      });

IMPORTANT: Clone the response with response.clone(). A response is a stream and because we want the browser to consume the response as well as the cache consuming the response, we need to clone it so we have two streams.

The cache in the above code happens the following we open the cache by name and then put the file with the cache.put(file, stream):

cache.put(event.request, responseToCache);

Make it look fancy

You have the possibility to create

  • installers for your app
  • custom installer button

Installer.js

let deferredPrompt;

window.addEventListener('beforeinstallprompt', (e) => {
  // Prevent Chrome 67 and earlier from automatically showing the prompt
  e.preventDefault();
  // Stash the event so it can be triggered later.
  deferredPrompt = e;
});

btnAdd.addEventListener('click', (e) => {
  // hide our user interface that shows our A2HS button
  btnAdd.style.display = 'none';
  // Show the prompt
  deferredPrompt.prompt();
  // Wait for the user to respond to the prompt
  deferredPrompt.userChoice
    .then((choiceResult) => {
      if (choiceResult.outcome === 'accepted') {
        console.log('User accepted the A2HS prompt');
      } else {
        console.log('User dismissed the A2HS prompt');
      }
      deferredPrompt = null;
    });
});

You have 2 event listeners "beforeinstallprompt" and "appinstalled". If the app is not installed then the "beforeinstallprompt" will be fired and the event in the parameter will be the property which we can later prompt so lets save and call it now deferredprompt because we need it later when the user presses the install button:

let deferredPrompt;

window.addEventListener('beforeinstallprompt', (e) => {
  // Prevent Chrome 67 and earlier from automatically showing the prompt
  e.preventDefault();
  // Stash the event so it can be triggered later.
  deferredPrompt = e;
});

NOTE: If you don't want the installer to fire immediatelly call preventDefault() on the event. (We are creating an installer flow here so we gon do that.)

In the event listener of the button we can call the prompt() and a prompt will popup:

deferredPrompt.prompt();

We can listen the outcome of the prompt and do anything with it, maybe greet the user or appreciate the installation. the result will be the choiceResult parameter's outcome property. Here we only log it to console:

deferredPrompt.userChoice
    .then((choiceResult) => {
      if (choiceResult.outcome === 'accepted') {
        console.log('User accepted the A2HS prompt');
      } else {
        console.log('User dismissed the A2HS prompt');
      }
    });

To prevent installation fire again we empty the deferredprompt variable with null after choiceResult received:

deferredPrompt = null;

🎉 Now you should have a working PWA 🎉

PLUS: With a state management library like React you have to store the deferredprompt in the state like so:

App.jsx

import React, { Component } from "react";
import { checkInstallPWA, installPwa } from "path to  dir/PWA.js";

class App extends Component {
  constructor(props) {
    super(props);
    this.state = {
      deferredPrompt: false,
    }
    checkInstallPWA((param) => this.setState({ deferredPrompt: param }));
  }

  render() {
    const { deferredPrompt } = this.state;
    return (
        <main className="App">
          {deferredpropmt && (
            <button className="install" onClick={() => installPwa(deferredPrompt)}>
              install
            </button>
          )}
        </main>
  }
}

PWA.js

export const checkInstallPWA = (func) => {
  window.addEventListener("beforeinstallprompt", (e) => {
    e.preventDefault();
    func(e);

    console.log("PWA: Installable");
  });

  window.addEventListener("appinstalled", (evt) => {
    func(false);

    console.log("PWA INSTALL: Success");
  });
};


export const installPwa = (deferredPrompt) => {
  deferredPrompt.prompt();

  deferredPrompt.userChoice
  .then((choiceResult) => {
    if (choiceResult.outcome === 'accepted') {
      console.log('User accepted the A2HS prompt');
    } else {
      console.log('User dismissed the A2HS prompt');
    }
  });
}

This was a long read I know but thanks for reading trough!

Please leave your opinion in the comments and thanks for all the awesome readers who claps/likes/loves my post.

END