Manage and preview content of your mobile app in web browser with real time updates

Aditya Chauhan
11 min readFeb 1, 2021

This article will show you how to preview your mobile app in the browser. User engagement of app can decrease over a period of time if new content is not pushed and not updated regularly in the app.
App markets are growing internationally and today users from various demographics are using apps. There are different target audiences among a user set too. Different content is targeted for different audiences.
One needs to always accommodate texts and different language features. Some languages may just offer a symbol which translates to a whole sentence in english. But an app should always look good. There can be content limitations like a paragraph on the screen should only be fixed to a number of lines so that it still looks pleasing to the eye and not a clutter. Often the people responsible for pushing the content don’t know how it is going to look in the app. And final result can turn out to be unexpected or ugly.
So, the motivation behind this article is to create a minimalistic content management system in which a person can view how his/her app looks before pushing the content.

An alternate solution to this is to create an exact replica of the mobile app as a responsive web app. Send the data (content) in query params and parsing it at the web app end. But a responsive web app can never perfectly emulate the different mobile device screens.

The core technologies used in this article are:

Obviously you can also learn how to integrate firebase with nodeJS and flutter through this article. So, let's get started!

The first step will be to create a nodeJS backend for our web application.

Create a backend directory:

mkdir broker
cd broker

Add required packages to that directory:

npm install express firebase dotenv cors
npm install nodemon --save-dev
npm install body-parser

Create the folder structure and files:

Go to console.firebase.google.com and create a new project

When you see the firebase SDK configuration like this, click on continue to console

Now go your created project and open the settings of the registered app. You will see the firebase configurations like this

Create environment variables in your .env file

#express server config
PORT=8080
HOST=localhost
HOST_URL=http://localhost:8080/
#firebase database config
API_KEY=AIzaSyCzlZ51QzbIeR0excyX1tvyOOiFB_hn8J0
AUTH_DOMAIN=preview-mobile-via-web.firebaseapp.com
PROJECT_ID=preview-mobile-via-web
STORAGE_BUCKET=preview-mobile-via-web.appspot.com
MESSAGING_SENDER_ID=735635784528
APP_ID=1:735635784528:web:314561c2b7b2c57aa8a255

Create and export config in /config.js

‘use strict’;
const dotenv = require(‘dotenv’);
const assert = require(‘assert’);
dotenv.config();
const {
PORT,
HOST,
HOST_URL,
API_KEY,
AUTH_DOMAIN,
PROJECT_ID,
STORAGE_BUCKET,
MESSAGING_SENDER_ID,
APP_ID,
} = process.env;
assert(PORT, ‘PORT is required’);
assert(HOST, ‘HOST is required’);
module.exports = {
port: PORT,
host: HOST,
url: HOST_URL,
firebaseConfig : {
apiKey: API_KEY,
authDomain: AUTH_DOMAIN,
projectId: PROJECT_ID,
storageBucket: STORAGE_BUCKET,
messagingSenderId: MESSAGING_SENDER_ID,
appId: APP_ID,
},
};

Create express.js server in /index.js

‘use strict’;
const express = require(‘express’);
const cors = require(‘cors’);
const bodyParser = require(‘body-parser’);
const config = require(‘./config’);
const personRoutes = require('./routes/personRoutes');
const app = express();
app.use(express.json());
app.use(cors());
app.use(bodyParser.json());
app.use('/api',personRoutes.routes);
app.listen(config.port, () => console.log(`App is listening on http://localhost:${config.port}`));

Connect firebase database with app in /db.js

const firebase = require(‘firebase’);
const config = require(‘./config’);
const db = firebase.initializeApp(config.firebaseConfig);
module.exports = db;

Create person model as person.js in models folder: models/person.js

class Person {
constructor(id, firstName, lastName, nationality){
this.id = id;
this.firstName = firstName;
this.lastName = lastName;
this.nationality = nationality;
}
}
module.exports = Person;

Create person controller with CRUD methods as personController.js in controllers folder: controllers/personController.js

‘use strict’;
const firebase = require(‘../db’);
const Person = require(‘../models/person’);
const firestore = firebase.firestore();
const addPerson = async (req, res, next) => {
try {
const data = req.body;
await firestore.collection(‘persons’).doc().set(data);
res.send(‘Record saved successfully’);
} catch (error) {
res.status(400).send(error.message);
}
};
const getAllPeople = async (req, res, next) => {
try {
const persons = await firestore.collection(‘persons’);
const data = await persons.get();
const peopleArray = [];
if (data.empty) {
res.status(400).send(‘No person record found’);
} else {
data.forEach(doc => {
const person = new Person(
doc.id,
doc.data().firstName,
doc.data().lastName,
doc.data().nationality,
)
peopleArray.push(person);
});
res.send(peopleArray);
}
} catch (error) {
res.status(400).send(error.message);
}
};
const getPersonById = async (req, res, next) => {
try {
const id = req.params.id;
const person = await firestore.collection(‘persons’).doc(id);
const data = await person.get();
if (!data.exists) {
res.status(400).send(`Student with ${id} not found`);
} else {
res.send(data.data());
}
} catch (error) {
res.status(400).send(error.message);
}
}
const updatePerson = async (req, res, next) => {
try {
const id = req.params.id;
const data = req.body;
const person = await firestore.collection(‘persons’).doc(id);
await person.update(data);
res.send(‘Person updated successfully’)
} catch (error) {
res.status(400).send(error.message);
}
}
const deletePerson = async (req, res, next) => {
try {
const id = req.params.id;
const person = await firestore.collection(‘persons’).doc(id).delete();
res.send(‘Person removed successfully’);
} catch (error) {
res.status(400).send(error.message);
}
}
module.exports = {
addPerson,
getAllPeople,
getPersonById,
updatePerson,
deletePerson
}

Create api routes for the controllers in routes/personRoutes.js

const express = require(‘express’);
const { addPerson, getAllPeople, getPersonById, updatePerson, deletePerson } = require(‘../controllers/personController’);
const router = express.Router();router.post(‘/person’, addPerson);
router.get(‘/people’, getAllPeople);
router.get(‘/person/:id’, getPersonById);
router.put(‘/person/:id’, updatePerson);
router.delete(‘/person/:id’, deletePerson);
module.exports = {
routes: router
};

Go to firebase cloud firestore. And setup a test database in firestore.

Now test your apis using postman

The next step is to create our mobile app

We will be using Flutter based mobile app. Pre-requisite is that flutter should be installed on your machine. You can create your android/ios native apps too.

The reasons I used flutter are:

  • Obviously a cross platform code.
  • Very fast development with hot reloads and painting of the page in milliseconds.
  • Seamless integration of the flutter app with Firebase SDK (both android and ios).
  • Native performance.

Now create your app

flutter create mobile_end

Now go to firebase project settings and click on Add app

Select ios/android. I will be using android here.

Now register app using the same package name by which you have created the app i.e. mobile_end. It is important to use the same package name

Register the app and download the google_services.json file.

Follow the steps mentioned in Add Firebase SDK.

Now we will add the cloud firestore to this app. Add the following dependencies to pubspec.yaml

firebase_core: ^0.5.3
cloud_firestore: ^0.14.4

Now let's create our user model in user.dart

class User {
String firstName;
String lastName;
String id;
String nationality;
User({this.firstName, this.lastName, this.id, this.nationality});
factory User.fromJson(Map<dynamic, dynamic> json) {
return User(
firstName: json['firstName'],
lastName: json['lastName'],
id: json['id'],
nationality: json['nationality']);
}
}

I modified my main.dart to display our users

import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';
import 'package:mobileEnd/User.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Persons',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: MyHomePage(title: 'Mobile app for preview'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
List<User> data = [];
@override
void initState() {
super.initState();
initializeAndFetch();
}
initializeAndFetch() async {
await Firebase.initializeApp();
fetchData();
}
fetchData() {
CollectionReference collectionReference =
FirebaseFirestore.instance.collection('persons');
collectionReference.snapshots().listen((snapshot) {
final List<User> list = [];
snapshot.docs.forEach((element) {
list.add(User.fromJson(element.data()));
});
setState(() {
data = list;
});
});
}
List<Widget> _renderGrid(BuildContext context, List<User> data) {
final List<Widget> tiles = [];
data.forEach((item) {
tiles.add(Container(
height: 50,
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(width: 1.3, color: Color(0xff1f40e6)),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Container(
child: RichText(
text: new TextSpan(
children: [
new TextSpan(
text: item.firstName + ' ',
style: TextStyle(
fontSize: 16,
color: Color(0xff7f7f7f),
fontWeight: FontWeight.w700,
fontStyle: FontStyle.normal,
letterSpacing: 0,
)),
new TextSpan(
text: item.lastName,
style: TextStyle(
color: Color(0xff7f7f7f),
fontSize: 16,
fontWeight: FontWeight.w700,
fontStyle: FontStyle.normal,
letterSpacing: 0,
),
),
],
),
),
),
]),
));
});
return tiles;
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Container(
height: 600,
margin: EdgeInsets.only(right: 28, left: 28),
child: GridView.count(
physics: AlwaysScrollableScrollPhysics(),
crossAxisCount: 1,
primary: false,
padding: EdgeInsets.all(0),
children: _renderGrid(context, data),
),
decoration: BoxDecoration(
color: Colors.white,
boxShadow: [
BoxShadow(
color: Color(0xff1f000000),
blurRadius: 4,
offset: Offset(0, 2),
)
],
),
),
),
);
}
}

Run this app in simulator and try changing the data in either firestore or through your api. The changes will be reflected immediately! Magic!

The next step is to create an account on appetize.io

Appetize can run native mobile apps in your browser. You can embed your mobile app in the webpage using an iFrame in your HTML. All you need is a publicKey that is generated when you upload your debug build on appetize.

Appetize provides great support embedding your mobile app. e.g.

  • Different mobile platforms and devices
  • Cross document messaging
  • App permissions
  • Custom launch pages
  • Playback events of the emulator

So let's create our debug build of the mobile app using this command. Your can create your ios build too.

flutter build apk --debug

Sign up for appetize and go to upload link

Select your debug apk and upload it. Note that debug apk is always located under debug folder.

A public key as shown is generated after successful upload

The next step is to create a web front end using ReactJS

npx create-react-app webEnd
cd webEnd
npm install axios

Now we will create CRUD functionalities for our user in the web app

Embed your mobile app in the web page using iFrame. All you need to do is create a public url using the publicKey generated that we created earlier.

https://appetize.io/embed/<publicKey>?device=iphone8&scale=100&centered=true&autoplay=false&orientation=portrait&deviceColor=black&xdocMsg=true

Simply replace <publicKey> with the generated one.

Now create a crud operations file userOps.js

import { webApiGet, webApiPost,webApiPut, webApiDelete } from './methods';const baseUrl = 'http://localhost:8080/api/';export const fetchUsers = () => {
const url = `${baseUrl}people/`;
return webApiGet(url).request;
};
export const createUser = payload => {
const url = `${baseUrl}person/`;
return webApiPost(url, payload).request;
};
export const updateUser = (payload, id) => {
const url = `${baseUrl}person/${id}`;
return webApiPut(url, payload).request;
};
export const deleteUser = id => {
const url = `${baseUrl}person/${id}`;
return webApiDelete(url).request;
};
export const fetchUserById = id => {
const url = `${baseUrl}person/${id}`;
return webApiGet(url).request;
};

Create methods file methods.js

import axios from 'axios';function getConfig() {
const config = {
headers: {
'Content-Type': 'application/json',
'Cache-Control': 'no-store',
},
};
return { config };
}
export function webApiGet(url) {
const config = getConfig();
return {
request: axios.get(url, config.config),
};
}
export function webApiPut(url, data) {
const config = getConfig();
return {
request: axios.put(url, data, config.config),
};
}
export function webApiPost(url, data) {
const config = getConfig();
return {
request: axios.post(url, data, config.config),
};
}
export function webApiDelete(url, data) {
const config = getConfig();
return {
request: axios.delete(url, data, config.config),
};
}

Modify App.js

import React, { useEffect, useState } from 'react';
import './App.css';
import { createUser, deleteUser, fetchUsers, updateUser } from './userOps';
const url = 'https://appetize.io/embed/1fchugudzjegpvqkeck0nke73g?device=iphone8&scale=100&centered=true&autoplay=false&orientation=portrait&deviceColor=black&xdocMsg=true';function App() {
const [users, setUsers] = useState([]);
const [userObj, setUserObj] = useState({
firstName: '',
lastName: '',
nationality: ''
});
useEffect(() => {
fetchUsersMethod();
}, []);
const fetchUsersMethod = async () => {
const users = await fetchUsers();
setUsers(users.data);
}
const handleDelete = async event => {
const { currentTarget: { dataset: { id } } } = event;
await deleteUser(id);
const users = await fetchUsers();
setUsers(users.data);
}
const handleFirstNameChange = async (event, user) => {
const value = event.target.value;
const userIndex = users.findIndex(item => item.id === user.id);
const usersCopy = [...users];
usersCopy[userIndex].firstName = value;
setUsers(usersCopy);
const payload = {
firstName: value,
id: user.id,
lastName: user.lastName,
nationality: user.nationality,
};
await updateUser(payload, user.id);
}
const handleLastNameChange = async (event, user) => {
const value = event.target.value;
const userIndex = users.findIndex(item => item.id === user.id);
const usersCopy = [...users];
usersCopy[userIndex].lastName = value;
setUsers(usersCopy);
const payload = {
firstName: user.firstName,
id: user.id,
lastName: value,
nationality: user.nationality,
};
await updateUser(payload, user.id);
}
const handleNationalityChange = async (event, user) => {
const value = event.target.value;
const userIndex = users.findIndex(item => item.id === user.id);
const usersCopy = [...users];
usersCopy[userIndex].nationality = value;
setUsers(usersCopy);
const payload = {
firstName: user.findIndex,
id: user.id,
lastName: user.lastName,
nationality: value,
};
await updateUser(payload, user.id);
}
const setUser = (e, field) => {
const value = e.target.value;
const user = { ...userObj };
switch (field) {
case 'firstName': {
user.firstName = value;
setUserObj(user);
break;
}
case 'lastName': {
user.lastName = value;
setUserObj(user);
break;
}
case 'nationality': {
user.nationality = value;
setUserObj(user);
break;
}
default: break;
}
}
const addUser = async () => {
await createUser(userObj);
const users = await fetchUsers();
setUsers(users.data);
}
return (
<div className="App">
<div className="left_container">
<div className="user_table">
{users.map((user, index) => (
<div className="user_row" key={index}>
<div className="user_details">
<div className="field"><input value={user.firstName} type="text" onChange={(e) => handleFirstNameChange(e, user)} /></div>
<div className="field"><input value={user.lastName} type="text" onChange={(e) => handleLastNameChange(e, user)} /></div>
<div className="field"><input value={user.nationality} type="text" onChange={(e) => handleNationalityChange(e, user)} /></div>
</div>
<div className="user_ops">
<button className="delete" data-id={user.id} onClick={handleDelete}>DELETE</button>
</div>
</div>
))}
</div>
<div className="add_user">
<div className="add_user_row">
<span>First name: </span>
<input type="text" onChange={(e) => setUser(e, 'firstName')} />
</div>
<div className="add_user_row">
<span>Last name: </span>
<input type="text" onChange={(e) => setUser(e, 'lastName')} />
</div>
<div className="add_user_row">
<span>Nationality: </span>
<input type="text" onChange={(e) => setUser(e, 'nationality')} />
</div>
<button onClick={addUser}>Add user</button>
</div>
</div>
<div className="iframe_container" >
<iframe title="appetize_-io" src={url} width="100%" height="100%" frameBorder="0"></iframe>
</div>
</div>
);
}
export default App;

And the index.css

body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}
.App {
display: flex;
justify-content: space-between;
align-items: center;
}
.iframe_container {
width: 40%;
height: 100%;
}
.left_container {
width: 50%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.left_container .user_table {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
height: auto;
overflow: scroll;
border: 1px solid black;
padding: 20px;
}
.left_container .user_table .user_row {
margin: 20px;
display: flex;
justify-content: space-between;
align-items: center;
font-size: 16px;
width: 100%;
}
.left_container .user_table .user_row .user_details {
display: flex;
justify-content: space-between;
align-items: center;
width: 70%;
}
.left_container .user_table .user_row .user_details .field {
width: 33%;
}
.left_container .add_user {
display: flex;
flex-direction: column;
margin-top: 40px;
}
.left_container .add_user button {
margin-top: 10px;
}

Make sure your nodeJS backend is running. You should see the user table fetched from the cloud firestore database.

You will see the screen like this with “Tap to Play” option. The free version of appetize embedding does not allow autoplay option on page-load.

Click on “Tap to Play” to install the app on the embedded simulator. You can then manage the content via the table. Try creating, deleting, updating the user table. See magic!

You can also update the content directly in firestore and it will get updated in the emulator. But it won't be very easy for people who manage content to understand the NoSql documents and relations once the app scales up. That's why we have created a reactJS front end.

Thanks!

--

--