Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

How to Build a PWA, an iOS App, and an Android App From One Codebase (2 of 2)

DZone's Guide to

How to Build a PWA, an iOS App, and an Android App From One Codebase (2 of 2)

We wrap up this quick series by demonstrating how to create three apps from one codebase using Angular and NativeScript.

· Web Dev Zone ·
Free Resource

Deploy code to production now. Release to users when ready. Learn how to separate code deployment from user-facing feature releases with LaunchDarkly.

Welcome back! If you missed Part 1, you can check it out here!

Building Your Web UI

In the starter app, the vast majority of your code is in a src/app/barcelona folder, as that’s the code that builds up the player list you saw in your app earlier. In this section, you’re going to create a brand-new component for the web, and in the next section, you’ll see just how easy it is to get that same component working in a native iOS and Android app.

Let’s start by scaffolding some files. To do so, start by using the cd command to navigate to your src/app folder.

cd /src/app

Next, make a new folder named list, and create the following files in that folder.

. app
└── list
    ├── list.common.ts
    ├── list.component.css
    ├── list.component.html
    ├── list.component.ts
    ├── list.module.ts
    └── list.service.ts

TIP: When you get more comfortable working with NativeScript schematics, there are a series of commands you can use to help generate components and modules. See the NativeScript schematics documentation for more information.

Here’s what to put in those files as a first step. Don’t worry too much about exactly what this code is doing, as we’ll discuss the important things to note momentarily.

list.common.ts

import { Routes } from '@angular/router';

import { ListComponent } from './list.component';
import { ListService } from './list.service';

export const COMPONENT_DECLARATIONS: any[] = [
  ListComponent
];

export const PROVIDERS_DECLARATIONS: any[] = [
  ListService
];

export const ROUTES: Routes = [
  { path: 'list', component: ListComponent },
];

list.component.css

ul {
  list-style: none;
  padding: 0;
  margin: 0;
}

li {
  border-style: solid;
  border-width: 0 0 1px 0;
  border-color: #C0C0C0;
  display: flex;
  align-items: center;
  cursor: pointer;
}

li.selected {
  background-color: #C0C0C0;
}

img {
  height: 96px;
  width: 96px;
}

list.component.html

<ul>
  <li *ngFor="let item of items" [class.selected]="item.selected" (click)="itemTapped(item)">
    <img [src]="item.image">
    <span>{{ item.name }}</span>
  </li>
</ul>

list.component.ts

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

import { ListService } from './list.service';

@Component({
  selector: 'app-list',
  templateUrl: './list.component.html',
  styleUrls: ['./list.component.css']
})
export class ListComponent implements OnInit {
  items: any[];

  constructor(private listService: ListService) { }

  ngOnInit() {
    this.listService.get().subscribe((data: any) => {
      this.items = data;
    });
  }

  itemTapped(item) {
    this.listService.toggleSelected(item);
  }
}

list.module.ts

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { HttpClientModule } from '@angular/common/http';
import { RouterModule } from '@angular/router';

import { ROUTES, COMPONENT_DECLARATIONS, PROVIDERS_DECLARATIONS } from './list.common';

@NgModule({
  imports: [
    CommonModule,
    HttpClientModule,
    RouterModule.forRoot(ROUTES)
  ],
  exports: [
    RouterModule
  ],
  declarations: [
    ...COMPONENT_DECLARATIONS
  ],
  providers: [
    ...PROVIDERS_DECLARATIONS
  ]
})
export class ListModule { }

list.service.ts

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { map } from 'rxjs/operators';

@Injectable()
export class ListService {
  saved: any[];

  constructor(private http: HttpClient) {
    let saved = localStorage.getItem('items');
    if (saved) {
      this.saved = JSON.parse(saved);
    } else {
      this.saved = [];
    }
  }

  get() {
    return this.http.get('https://rawgit.com/tjvantoll/ShinyDex/master/assets/151.json')
      .pipe(
        map((data: any) => {
          const returnData = [];
          data.results.forEach((item) => {
            item.selected = this.saved.indexOf(item.id) != -1;
            returnData.push(item);
          })
          return returnData;
        })
      );
  }

  toggleSelected(item) {
    if (item.selected) {
      this.saved.splice(this.saved.indexOf(item.id), 1);
    } else {
      this.saved.push(item.id);
    }

    item.selected = !item.selected;
    this.save();
  }

  save() {
    localStorage.setItem('items', JSON.stringify(this.saved));
  }
}

Again, don’t worry if you don’t understand everything that’s going on in this code. For now, all you need to know is that this is a fairly straightforward Angular component that loads data from an API and shows it in a list.

To activate this new component so you can try it out, first, add this new component to your app.module.ts file so Angular knows about it. Here’s what your new app.module.ts file should look like.

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';

import { AppRoutingModule } from './app.routing';
import { AppComponent } from './app.component';
import { ListModule } from './list/list.module';

@NgModule({
  declarations: [
    AppComponent,
  ],
  imports: [
    BrowserModule,
    AppRoutingModule,
    ListModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

Next, change the default path in your app.routes.ts file, so Angular navigates to the new list component by default. The new app.routes.ts file should look like this.

import { Routes } from '@angular/router';

export const ROUTES: Routes = [
  { path: '', redirectTo: '/list', pathMatch: 'full' },
];

Finally, just so this app looks a little nicer, paste the following code in your src/styles.css file, which is the place you add global CSS for your Angular apps.

html, body { margin: 0; }
body {
  font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
}

With this in place, return to your terminal or command prompt and run ng serve. After the command runs, open your browser and visit localhost:4200, and you should now see a simple little checklist app that looks like this.

web-in-action

The app you now have is a really simple app that allows users to select items. If you look at the code in list.service.ts, you can see that the app also remembers the user’s selections using localStorage—meaning, all selections remain when the user returns to the app.

At this point, you have a very simple mobile app. If you’d like, you could follow one of many online guides for giving this app a service worker and making it into a Progressive Web App — after all, at the moment, you’re just using the Angular CLI to build a web app.

The real fun of this workflow though is in just how easy it is to turn a functional app like this into a native iOS and Android app. Let’s look at how to do that.

Creating Your iOS and Android App

When converting a web interface to a mobile UI using NativeScript schematics, your first task is to identify which code you can reuse and which code you cannot. Let’s return to the list of files that make up this component.

. app
└── list
    ├── list.common.ts
    ├── list.component.css
    ├── list.component.html
    ├── list.component.ts
    ├── list.module.ts
    └── list.service.ts

For this example you’ll need to create a NativeScript-specific markup file (.html), styling file (.css), and module file (.module.ts). To do that, go ahead and create three new files, list.component.tns.css, list.component.tns.html, and list.module.tns.ts. Your file tree should now look like this.

. app
└── list
    ├── list.common.ts
    ├── list.component.css
    ├── list.component.html
    ├── list.component.tns.css (new)
    ├── list.component.tns.html (new)
    ├── list.component.ts
    ├── list.module.ts
    ├── list.module.tns.ts (new)
    └── list.service.ts

Remember that the Angular CLI will automatically grab the code in your .tns.* files when building for NativeScript, and your non-.tns files when building for the web. Therefore, the .tns files are where you need to put your code that’s specific to NativeScript.

To do that, start by opening your list.module.tns.ts file and paste in the following code.

import { NgModule, NO_ERRORS_SCHEMA } from '@angular/core';
import { NativeScriptCommonModule } from 'nativescript-angular/common';
import { NativeScriptHttpClientModule } from 'nativescript-angular/http-client';
import { NativeScriptRouterModule } from 'nativescript-angular/router';

import { ROUTES, COMPONENT_DECLARATIONS, PROVIDERS_DECLARATIONS } from './list.common';

@NgModule({
  imports: [
    NativeScriptCommonModule,
    NativeScriptHttpClientModule,
    NativeScriptRouterModule,
    NativeScriptRouterModule.forRoot(ROUTES)
  ],
  exports: [
    NativeScriptRouterModule
  ],
  declarations: [
    ...COMPONENT_DECLARATIONS
  ],
  providers: [
    ...PROVIDERS_DECLARATIONS
  ],
  schemas: [
    NO_ERRORS_SCHEMA
  ]
})
export class ListModule { }

You need a NativeScript-specific module file so that you can declare NativeScript-specific imports, such as NativeScriptHttpClientModule, and NativeScriptRouterModule. However, note how both your list.module.ts and list.module.tns.ts files pull routes, declarations, and providers from your list.common.ts file. This gives you the ability to add those declarations in one place, without having to change two different module files every time you need to make a small update.

The next file to change is your app’s markup file. To do that, open your list.component.tns.html file and paste in the following code.

<ListView [items]="items">
  <ng-template let-item="item">
    <FlexboxLayout [class.selected]="item.selected" (tap)="itemTapped(item)">
      <Image [src]="item.image"></Image>
      <Label [text]="item.name"></Label>
    </FlexboxLayout>
  </ng-template>
</ListView>

TIP: If you need help learning these NativeScript user interface components, try NativeScript Playground, and, specifically, try out the components pane on the bottom-left-hand side of the screen.

Next, to style these components, paste the following code into your list.component.tns.css file.

FlexboxLayout {
  padding: 5;
  align-items: center;
}
.selected {
  background-color: #C0C0C0;
}
Image {
  height: 80;
  width: 80;
}

TIP: If you’re a fan of SASS, you can use it with NativeScript schematics and share CSS variables such as colors. Check out the instructions here on the NativeScript documentation for the next steps you’ll need to take.

With all this in place, your last step is to import your ListModule in your app.module.tns.ts file, exactly like you did with your app.module.ts file. To do so, replace the contents of your app.component.tns.ts file with the following code.

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 { ListModule } from './list/list.module';

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

And with that, you should have a functioning NativeScript app, right? Actually, there’s one last change you need to make, and to show it, let’s introduce the concept of helper files.

Using Helper Files

When taking a code-sharing approach, sometimes you need to completely split your implementations for web and native. Your user interfaces, for example, always need different implementations, as you need to use DOM nodes for the web, and NativeScript user-interface controls for mobile.

However, there are often times where you can share almost all of your code but need slightly different implementations for the web and mobile. There’s actually one example of this in your sample app, and it’s in your list.service.ts file.

If you open list.service.ts, you’ll see two different references to localStorage—one in the constructor…

let saved = localStorage.getItem('items');

…and another in the save() method.

localStorage.setItem('items', JSON.stringify(this.saved));

The problem here is localStorage is a browser API. It works great on the web, but it does not exist in NativeScript apps because NativeScript apps do not run in a browser.

You have a few different ways you could handle this. You could create a list.service.tns.ts file, and create a separate implementation of this service that works for your mobile app. However, if you do that, you need to duplicate a lot of code that is the same across both platforms, such as the code that calls your backend over HTTP and parses the data.

When you hit these scenarios another option you have is to create helper files. That is, create two files with identical APIs, and put your web implementation of those APIs in one file, and your NativeScript implementation of those APIs in another.

To do this for your service, create two new files named list.helper.ts and list.helper.tns.ts. Your new folder structure should now look like this.

. app
└── list
    ├── list.common.ts
    ├── list.component.css
    ├── list.component.html
    ├── list.component.tns.css
    ├── list.component.tns.html
    ├── list.component.ts
    ├── list.helper.tns.ts (new)
    ├── list.helper.ts (new)
    ├── list.module.ts
    ├── list.module.tns.ts
    └── list.service.ts

TIP: In a real-world app you would probably want to move the service and its helper files to its own folder, both to break up the folder structure, and to make the service reusable. For this tutorial though it’s easiest for us to keep everything in one place.

Open your list.helper.ts file and paste in the following code. This is the same localStorage code you had in your service extracted into a helper.

export class ListHelper {
  readItems() {
    let saved = localStorage.getItem('items');
    if (saved) {
      return JSON.parse(saved);
    } else {
      return [];
    }
  }

  writeItems(items) {
    localStorage.setItem('items', JSON.stringify(items));
  }
}

Next, open your list.helper.tns.ts file and paste in the following code. This code follows the same API as the web helper but instead uses some of NativeScript’s built-in modules to accomplish the same task for your iOS and Android apps.

import { knownFolders, File } from 'tns-core-modules/file-system';

export class ListHelper {
  saveFile: File;

  constructor() {
    this.saveFile = knownFolders.documents().getFile('items.json');
  }

  readItems() {
    const items = this.saveFile.readTextSync();
    return items ? JSON.parse(items) : [];
  }

  writeItems(items) {
    this.saveFile.writeText(JSON.stringify(items));
  }
}

Your last step here is changing your service to utilize these new helpers, which you can do by replacing the code in your list.service.ts file with the code below, which makes use of the new helpers.

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { map } from 'rxjs/operators';

import { ListHelper } from './list.helper';

@Injectable()
export class ListService {
  saved: any[];
  helper: ListHelper;

  constructor(private http: HttpClient) {
    this.helper = new ListHelper();
    this.saved = this.helper.readItems();
  }

  get() {
    return this.http.get('https://rawgit.com/tjvantoll/ShinyDex/master/assets/151.json')
      .pipe(
        map((data: any) => {
          const returnData = [];
          data.results.forEach((item) => {
            item.selected = this.saved.indexOf(item.id) != -1;
            returnData.push(item);
          })
          return returnData;
        })
      );
  }

  toggleSelected(item) {
    if (item.selected) {
      this.saved.splice(this.saved.indexOf(item.id), 1);
    } else {
      this.saved.push(item.id);
    }

    item.selected = !item.selected;
    this.save();
  }

  save() {
    this.helper.writeItems(this.saved);
  }
}

Now that you have all your code in place, go ahead and run your app on iOS or Android using one of the following commands.

npm run ios
npm run android

You should see an app that looks something like this.

apps-in-action

Although this app is simple, it’s important to remember what you’re seeing here: these are native iOS and Android apps, using native iOS and Android user interface controls. And not only did you build these apps with Angular and TypeScript, you even shared a good chunk of your code across all three platforms.

NOTE: You can learn more about using helper files to split your code from this article on the NativeScript documentation.

The Big Picture

In this article, we looked at how to build a simple component that shares code across web and native apps. Although we focused on one component for this article, the approach is flexible enough to build apps of any scale.

Depending on your needs, you might want to create relatively identical apps for both the web and mobile. Or, you might want to create very different apps that share the same language, framework, underlying infrastructure, and service layer.

After all, the power of using NativeScript schematics is not just code sharing — you also gain a ton from using one language and one framework to build for three platforms. Hopefully, you’re as excited as me about the awesome things you can build with this technology stack.

Resources

Deploy code to production now. Release to users when ready. Learn how to separate code deployment from user-facing feature releases with LaunchDarkly.

Topics:
web dev ,pwa ,mobile application development ,nativescript ,tutorial

Published at DZone with permission of

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}