Comparing Ionic, Cordova, and React Native for Full-stack NLP with React

Over the past 15 years, since the introduction of the first iPhone, software development has undergone a significant transformation. With the increasing prevalence and expanding capabilities of smartphones, users are shifting their preference towards accessing software services on mobile devices instead of traditional desktops or laptops. This shift is driven by the unique features offered by smartphones, such as geolocation, biometric authentication, and motion sensing, many of which are only recently being adopted by desktop platforms. In certain demographics, smartphones and similar mobile devices have even become the primary means of software consumption, entirely replacing computers.

Companies have recognized and responded to this trend by prioritizing mobile app development. Applications across various domains, including financial brokerage (Robinhood), social media (Instagram), and ride-hailing (Uber), are embracing a mobile-first development approach. Desktop applications, if offered, often serve as secondary companions to the primary mobile app.

For full-stack developers, adapting to these evolving trends is essential. Fortunately, numerous mature and well-supported technologies are available to help web developers leverage their skills for mobile development. This article will delve into three such technologies: Cordova, Ionic, and React Native. React.js, a widely used framework for front-end web development, will serve as the core development technology. While the focus will be on developing an iPhone application, it’s important to note that these technologies are cross-platform and can be compiled for the Android platform as well.

Application Overview

The goal is to build an application that harnesses Natural Language Processing (NLP) to process and curate Twitter feeds. Users will be able to select a set of Twitter handles, retrieve their latest updates using a Twitter API, and categorize the tweets based on sentiment and topic. The application will then present the tweets to the user, allowing them to view them by sentiment or topic.

Back End

The back-end development will precede the front-end and will be kept intentionally simple. It will utilize basic, readily available sentiment analysis and part-of-speech tagging, complemented by some data cleaning to address dataset-specific issues. The open-source NLP library TextBlob will be employed, and the results will be served using Flask.

NLP Concepts: A Brief Introduction

For those unfamiliar with natural language analysis, terms like Sentiment Analysis and Part-of-speech tagging might seem foreign. NLP encompasses technologies that analyze and process human language data. While a broad field, there are common challenges. Human language, unlike programming or numerical data, is less structured and highly contextual. This, coupled with the inherent complexities of language, makes traditional data analysis techniques difficult to apply directly.

Sentiment Analysis focuses on deciphering the emotional tone of a text passage. Although human emotion is subjective, sentiment analysis has significant commercial potential. Applications include classifying product reviews, gauging the mood of emails or speeches, and categorizing song lyrics.

Part-of-speech tagging (POS tagging) aims to identify the grammatical role of words within a sentence. This is more challenging than it appears, as a word’s role can change based on context. Thankfully, sophisticated off-the-shelf models are now readily available in various programming languages.

Flask, TextBlob, and Tweepy

The NLP back end will utilize Flask to build a lightweight server, TextBlob for natural language processing, and Tweepy to interact with the Twitter API. Obtaining a developer key from Twitter is a prerequisite for retrieving tweets.

While more sophisticated back-end implementations with advanced NLP techniques are possible, simplicity will be prioritized for this project.

Back-end Code

Let’s begin coding the back end.

First, install the necessary packages:

1
2
pip install flask flask-cors textblob tweepy
python -m textblob.download_corpora

Next, let’s write the code for the core functionality.

Create a Python script named server.py and import the required libraries:

1
2
3
4
import tweepy

from textblob import TextBlob
from collections import defaultdict

Define some helper functions:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
# simple, average a list of numbers with a guard clause to avoid division by zero
def mean(lst):
    return sum(lst)/len(lst) if len(lst) > 0 else 0

# call the textblob sentiment analysis API and noun phrases API and return it as a dict
def get_sentiment_and_np(sentence):
    blob = TextBlob(sentence)
    return{
        'sentiment': mean([s.sentiment.polarity for s in blob.sentences if s.sentiment.polarity != 0.0]),
        'noun_phrases': list(blob.noun_phrases)
    }

# use the tweepy API to get the last 50 posts from a user’s timeline
# We will want to get the full text if the text is truncated, and we will also remove retweets since they’re not tweets by that particular account.
def get_tweets(handle):
    auth = tweepy.OAuthHandler('YOUR_DEVELOPER_KEY')
    auth.set_access_token('YOUR_DEVELOPER_SECRET_KEY')
    api = tweepy.API(auth)
    tl = api.user_timeline(handle, count=50)
    tweets = []
    for tl_item in tl:
        if 'retweeted_status' in tl_item._json:
            Continue # this is a retweet
        if tl_item._json['truncated']:
            status = api.get_status(tl_item._json['id'], tweet_mode='extended') # get full text
            tweets.append(status._json['full_text'])
        else:
            tweets.append(tl_item._json['text'])
    return tweets

# http and https are sometimes recognized as noun phrases, so we filter it out.
# We also try to skip noun phrases with very short words to avoid certain false positives
# If this were a commercial app, we would want a more sophisticated filtering strategy.
def good_noun_phrase(noun_phrase):
    noun_phrase_list = noun_phrase.split(' ')
    for np in noun_phrase_list:
        if np in {'http', 'https'} or len(np) < 3:
            return False
    return True

With the helper functions in place, combine everything into a couple of functions:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# reshapes the tagged tweets into dictionaries that can be easily consumed by the front-end app
def group_tweets(processed_tweets):
    # Sort it by sentiment
    sentiment_sorted = sorted(processed_tweets, key=lambda x: x['data']['sentiment'])

    # collect tweets by noun phrases. One tweet can be present in the list of more than one noun phrase, obviously.
    tweets_by_np = defaultdict(list)

    for pt in processed_tweets:
        for np in pt['data']['noun_phrases']:
            tweets_by_np[np].append(pt)
    grouped_by_np = {np.title(): tweets for np, tweets in tweets_by_np.items() if len(tweets) > 1 and good_noun_phrase(np)}
    return sentiment_sorted, grouped_by_np

# download, filter, and analyze the tweets
def download_analyze_tweets(accounts):
    processed_tweets = []
    for account in accounts:
        for tweet in get_tweets(account):
            processed_tweet = ' '.join([i for i in tweet.split(' ') if not i.startswith('@')])
            res = get_sentiment_and_np(processed_tweet)
            processed_tweets.append({
                'account': account,
                'tweet': tweet,
                'data': res
            })

    sentiment_sorted, grouped_by_np = group_tweets(processed_tweets)
    return processed_tweets, sentiment_sorted, grouped_by_np

The download_analyze_tweets function can now be executed with a list of desired Twitter handles.

Running the following code:

1
2
3
4
5
6
if __name__ == '__main__':
    accounts = ['@spacex', '@nasa']
    processed_tweets, sentiment_sorted, grouped_by_np = download_analyze_tweets(accounts)
    print(processed_tweets)
    print(sentiment_sorted)
    print(grouped_by_np)

should produce results similar to:

1
2
3
[{'account': '@spacex', 'tweet': 'Falcon 9…
[{'account': '@nasa', 'tweet': 'Our Mars rove…
{'Falcon': [{'account': '@spacex', 'tweet': 'Falc….

Now, construct a simple Flask server. In an empty file named server.py, add the following code:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
from flask import Flask, request, jsonify
from twitter import download_analyze_tweets
from flask_cors import CORS

app = Flask(__name__)

CORS(app)

@app.route('/get_tweets', methods=['POST'])

def get_tweets():
    accounts = request.json['accounts']
    processed_tweets, sentiment_sorted, grouped_by_np = download_analyze_tweets(accounts)
    return jsonify({
        'processedTweets': processed_tweets,
        'sentimentSorted': sentiment_sorted,
        'groupedByNp': grouped_by_np
    })

if __name__ == '__main__':
    app.run(debug=True)

After starting the server, a POST request can be sent to it using an HTTP client. Passing {"accounts": ["@NASA", "@SpaceX"]} as a JSON argument should return data similar to the Twitter analysis code.

Due to networking nuances in phone emulators, deploying the API is recommended. Otherwise, when running on an emulator, requests should be directed to <Your Computer IP>:5000 instead of localhost:5000. Deploying the code allows for requests to a specific URL.

Numerous deployment options exist. For a straightforward, free debug server requiring minimal setup, PythonAnywhere is a good choice and should handle this server out of the box.

With the back-end server complete, let’s move on to the front end, starting with Cordova.

Apache Cordova Implementation

Cordova Overview

Apache Cordova enables web developers to target mobile platforms. Utilizing the web browser capabilities of smartphones, Cordova packages web application code into a native application container. However, Cordova is more than a simple web browser. Through its API, developers can access various smartphone-specific features, including offline support, location services, and device cameras.

For this application, React.js will be used as the JS framework and React-Bootstrap as the CSS framework. Bootstrap, being responsive, already supports smaller screens. Once the application is written, Cordova will be used to compile it into a web application.

App Configuration

The setup process for the Cordova React app is unique. As developer Shubham Patil explains in a Medium article, it involves setting up both a React development environment (using React CLI) and a Cordova development environment (using Cordova CLI), and then merging them.

Begin by executing the following commands in the code folder:

1
2
3
cordova create TwitterCurationCordova

create-react-app twittercurationreact

Once the setup is complete, the contents of the React app’s public and src folders should be moved to the Cordova app. Next, in the package.json file, copy the scripts, browser list, and dependencies from the React project. Additionally, add "homepage": "./" to the root of package.json to ensure compatibility with Cordova.

Modify the public/index.html file to work with Cordova. Copy the meta tags from www/index.html and the script located at the end of the body tag (responsible for loading Cordova.js).

Next, modify src/index.js to detect if it’s running within Cordova. If so, the render code should be executed within the deviceready event handler. Otherwise, render immediately.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
const renderReactDom = () => {
  ReactDOM.render(
    <React.StrictMode>
      <App />
    </React.StrictMode>,
    document.getElementById('root')
  );
}

if (window.cordova) {
  document.addEventListener('deviceready', () => {
    renderReactDom();
  }, false);
} else {
  renderReactDom();
}

Finally, set up the deployment pipeline by adding the following definition to the config.xml file:

<hook type="before_prepare" src="hooks/prebuild.js" />

Then, create the prebuild.js script:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
const path = require('path');
const { exec } = require('child_process');
const fs = require('fs');
const rimraf = require('rimraf');

function renameOutputFolder(buildFolderPath, outputFolderPath) {
    return new Promise((resolve, reject) => {
        fs.rename(buildFolderPath, outputFolderPath, (err) => {
            if (err) {
                reject(err);
            } else {
                resolve('Successfully built!');
            }
        });
    });
}

function execPostReactBuild(buildFolderPath, outputFolderPath) {
    return new Promise((resolve, reject) => {
        if (fs.existsSync(buildFolderPath)) {
            if (fs.existsSync(outputFolderPath)) {
                rimraf(outputFolderPath, (err) => {
                    if (err) {
                        reject(err);
                        return;
                    }
                    renameOutputFolder(buildFolderPath, outputFolderPath)
                        .then(val => resolve(val))
                        .catch(e => reject(e));
                });
            } else {
                renameOutputFolder(buildFolderPath, outputFolderPath)
                    .then(val => resolve(val))
                    .catch(e => reject(e));
            }
        } else {
            reject(new Error('build folder does not exist'));
        }
    });
}

module.exports = () => {
    const projectPath = path.resolve(process.cwd(), './node_modules/.bin/react-scripts');
    return new Promise((resolve, reject) => {
        exec(`${projectPath} build`,
            (error) => {
                if (error) {
                    console.error(error);
                    reject(error);
                    return;
                }
                execPostReactBuild(path.resolve(__dirname, '../build/'), path.join(__dirname, '../www/'))
                    .then((s) => {
                        console.log(s);
                        resolve(s);
                    })
                    .catch((e) => {
                     console.error(e);
                        reject(e);
                    });
            });
    });
};

This script triggers the React build process, placing the build folder in the correct location before the Cordova build begins, automating the deployment.

Now, test the app:

1
2
3
npm install rimraf
npm install
npm run start

This should launch the React app in the browser. Add Cordova:

1
2
3
cordova platform add iOS

cordova run iOS

The React app should now be running in the emulator.

Router and Package Setup

Install the required packages:

1
npm install react-bootstrap react-router react-router-dom

Set up the routing and a simple global state object to be shared by all components. In a production application, a state management system like Redux or MobX would be preferable, but for simplicity, this project will keep it basic.

In App.js, configure the route as follows:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
import {
  BrowserRouter as Router,
  Redirect,
  Route,
} from "react-router-dom";

function App() {
  const [curatedTweets, setCuratedTweets] = useState();
  return <Router>
      <Route path="/" exact render={() => <Input setCuratedTweets={setCuratedTweets} />} />
      <Route path="/display" render={() => <Display curatedTweets={curatedTweets} />} />
      <Route path="*" exact render={() => <Redirect to="/" />} />
  </Router>
}

This introduces two routes, Input and Display. The curatedTweets variable is passed to Display, while setCuratedTweets is passed to Input. This allows the input component to set the curatedTweets variable, which is then accessible to Display for rendering.

Create the component files. Under /src, create a folder named /src/components. Within /src/components, create another folder called /src/components/input and two files inside it: input.js and input.css. Repeat this for the Display component: create /src/components/display and add display.js and display.css.

Create stub components like so:

1
2
3
4
5
import React from react;
import input.css

const Input = () => <div>Input</div>;
export default Input

And similarly for Display:

1
2
3
4
5
import React from react;
import display.css

const Display = () => <div>Display</div>;
export default Display

With the wireframing complete, the app should run. Now, let’s focus on coding the Input page.

Input Page

Functionality Outline

The Input page should allow users to input and edit the Twitter handles they wish to pull data from. A mechanism for users to signal completion is also needed. Once signaled, the curated tweets should be fetched from the Python API, and the app should navigate to the Display component.

File Setup

Import the necessary React Router library component withRouter to enable navigation functionality, along with the required React Bootstrap Components:

1
2
3
4
import React, {useState} from 'react';
import {withRouter} from 'react-router-dom';
import {ListGroup, Button, Form, Container, Row, Col} from 'react-bootstrap';
import './input.css';

Define a stub function for Input. Input receives the setCuratedTweets function and needs the ability to navigate to the display route after setting the curated tweets. Therefore, it will take setCuratedTweets and history (for navigation) from the props.

1
2
3
const Input = ({setCuratedTweets, history}) => {
    return <div>Input</div>
}

Wrap the component with withRouter in the export statement at the end of the file to provide access to the history API:

1
export default withRouter(Input);

Data Containers

Set up the data containers using React Hooks. Since the useState hook has already been imported, add the following code to the Input component’s body:

1
2
const [handles, setHandles] = useState([]);
const [handleText, setHandleText] = useState(‘’);

This creates containers and modifiers for handles (storing the list of handles) and handleText (storing the content of the input textbox).

Next, let’s code the UI components.

UI Components

The UI will be simple. One Bootstrap row will contain the input textbox and two buttons: one to add the input content to the handles list and another to trigger the API call. Another Bootstrap row will display the list of handles using the Bootstrap list group. Here’s the code:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
return (
    <Container className="tl-container">
        <Row>
            <Col>
                <Form.Control type="text" value={handleText} onChange={changeHandler} placeholder="Enter handle to pull" />
            </Col>
        </Row>
        <Row className='input-row'>
            <Col>
                <Button variant="primary" onClick={getPull}>Pull</Button>
                {' '}
                <Button variant="success" onClick={onAddClicked}>Add</Button>
            </Col>
        </Row>
        <Row>
            <Col>
                <ListGroup className="handles-lg">
                    {handles.map((x, i) => <ListGroup.Item key={i}>
                        {x}
                        <span onClick={groupItemClickedBuilder(i)} className="delete-btn-span">
                        <Button variant="danger" size="sm">
                        delete
                        </Button>
                        </span>
                    </ListGroup.Item>)}
                </ListGroup>
            </Col>
        </Row>
    </Container>
); 

In addition to the UI components, implement the three UI event handlers for data changes. The getPull event handler (responsible for the API call) will be implemented in the next section.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// set the handleText to current event value
const textChangeHandler = (e) => {
    e.preventDefault();
    setHandleText(e.target.value);
}

// Add handleText to handles, and then empty the handleText
const onAddClicked = (e) => {
    e.preventDefault();
    const newHandles = [...handles, handleText];
    setHandles(newHandles);
    setHandleText('');
}

// Remove the clicked handle from the list
const groupItemClickedBuilder = (idx) => (e) => {
    e.preventDefault();
    const newHandles = [...handles];
    newHandles.splice(idx, 1);
    setHandles(newHandles);
}

Now, let’s implement the API call.

API Call

The API call involves taking the desired handles, sending them to the Python API in a POST request, and storing the resulting JSON data in the curatedTweets variable. If successful, the app should programmatically navigate to the /display route. Otherwise, log the error to the console for debugging.

Here’s the code:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
const pullAPI = (e) => {
    e.preventDefault();
    fetch('http://prismatic.pythonanywhere.com/get_tweets', {
        method: 'POST',
            mode: 'cors',
            headers: {
            'Accept': 'application/json',
            'Content-Type': 'application/json',
            },
            body: JSON.stringify({
            accounts: handles
            })
        }).then(r=>r.json()).then(j => {
        setCuratedTweets(j);
            history.push('/display');
        })
    .catch(e => {
        console.log(e);
        })
}

At this point, the app should be functional. Add a couple of handles and send a request to the API.

Now, let’s move on to coding the sentiment page.

Sentiment Sorted Mode

Because the Python API already sorts tweets by sentiment, the sentiment page is straightforward.

Functionality Outline

A list interface will display the tweets. Additionally, a couple of navigational components will be included: one to switch to topic grouping mode and another to return to the Input page.

Start by defining the SentimentDisplay sub-component within the display.js file.

SentimentDisplay Component

The SentimentDisplay component will receive the curatedTweets object and display the sentiment-sorted tweets in a list. React-Bootstrap simplifies this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
const SentimentDisplay = ({curatedTweets}) => {
    return <ListGroup>
      {curatedTweets.sentimentSorted.map((x, i) =>
            <ListGroup.Item key={i}>
                <div className="account-div">{x.account}:</div>
                <span className="tweet-span">{x.tweet}</span>
                <span className="sentiments-span">({x.data.sentiment.toPrecision(2)})</span>
            </ListGroup.Item>
        )}
    </ListGroup>
}

Add some styling. In display.css, add the following and import it:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
 .account-div {
    font-size: 12px;
    font-weight: 600;

}

.tweet-span {
    font-size: 11px;
    font-weight: 500;
}

.sentiments-span {
    font-size: 10px;
}

.tl-container {
    margin-top: 10px;
}

.disp-row {
    margin-top: 5px;
}

Now, display the SentimentDisplay component. Modify the Display function as follows:

1
2
3
const Display = ({curatedTweets}) => {
    return <SentimentDisplay curatedTweets={curatedTweets} />
};

Take this opportunity to code the navigational components. Two buttons are needed: “Back to edit” and “Topic Group Mode.”

Implement these buttons in a separate Bootstrap row above the SentimentDisplay component:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
Return <Container className="tl-container">
    <Row>
        <Col>
            <Link to="/"><Button variant='primary'>Back</Button></Link>
            {' '}
           <Button variant='success'>View by Topic</Button>
        </Col>
    </Row>
    <Row className="disp-row">
        <Col>
            <SentimentDisplay curatedTweets={curatedTweets} />
        </Col>
    </Row>
</Container>

Running the app and pulling tweets from a few handles should yield a visually appealing result.

Topic Grouping Mode

Now, implement the slightly more complex Topic Grouping Mode.

Functionality Outline

The goal is to display all noun phrases as an accordion list, rendering the tweets containing each noun phrase when expanded.

Implementing the Switch to Topic Grouping Mode

Begin by creating a stub component:

1
2
3
const TopicDisplay = () => {
    return <div>Topic Display</div>
}

Add logic to control which component is displayed. In the main Display component, add the following lines:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// controls the display mode. Remember to import {useState} from ‘react’
const [displayType, setDisplayType] = useState('Sentiment');

// Switch the Display Mode
const toggleDisplayType = () => {
    setDisplayType(displayType === 'Sentiment' ? 'Topic': 'Sentiment');
}

// determines the text on the mode switch button
const switchStr = displayType === 'Sentiment'? 'View by Topic': 'View by Sentiment'

Then, modify the JSX to incorporate this logic:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
Return <Container className="tl-container">
    <Row>
        <Col>
            <Link to="/"><Button variant='primary'>Back</Button></Link>
            {' '}
            <Button variant='success' onClick={toggleDisplayType}>{switchStr}</Button>
        </Col>
    </Row>
    <Row className="disp-row">
        <Col>
                {
                    displayType === 'Sentiment'?
                    <SentimentDisplay curatedTweets={curatedTweets} />:
                    <TopicDisplay curatedTweets={curatedTweets} />
                }
        </Col>
    </Row>
</Container>

The Topic Group Display stub should now appear when toggled.

The TopicDisplay Component

Code the TopicDisplay component, which will leverage the Bootstrap Accordion List. The implementation is fairly straightforward:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
const TopicDisplay = ({curatedTweets}) => {
    return <Accordion>
        {Object.keys(curatedTweets.groupedByNp).map((x, i) =>
            <Card key={i}>
                <Card.Header>
                    <Accordion.Toggle as={Button} variant="link" eventKey={i}>
                        {x} ({curatedTweets.groupedByNp[x].length})
                    </Accordion.Toggle>
                </Card.Header>
                <Accordion.Collapse eventKey={i}>
                    <Card.Body>
                        <ListGroup>
                            {curatedTweets.groupedByNp[x].map((y, i2) =>
                                <ListGroup.Item key={i2}>
                                    <div className="account-div">{y.account}:</div>
                                    <span className="tweet-span">{y.tweet}</span>
                                 <span className="sentiments-span">({y.data.sentiment.toPrecision(2)})</span>
                                </ListGroup.Item>
                            )}
                        </ListGroup>
                    </Card.Body>
                </Accordion.Collapse>
            </Card>
        )}
  </Accordion>
}

Running the app should now display the Topic Display.

The app is now complete and ready to be built for the emulator.

Running the App in the Emulator

Cordova makes running the app in the emulator simple:

1
2
cordova platform add ios # if you haven’t done so already
cordova run ios

The app should now appear in the emulator. Thanks to Bootstrap’s responsive design, the web app adapts to the iPhone’s width, resulting in a polished appearance.

With the Cordova app complete, let’s explore the Ionic implementation.

Ionic-React Implementation

Ionic Overview

Ionic is a web component library and CLI toolkit that simplifies hybrid application development. Initially built on AngularJS and Cordova, Ionic now offers React.js components and supports Capacitor, a platform similar to Cordova.

Ionic distinguishes itself by providing web components that closely resemble native mobile interfaces. These components automatically adapt their look and feel to the operating system, enhancing the native experience. Additionally, Ionic offers various build tools to streamline application deployment (though this is outside the scope of this article).

This implementation will utilize Ionic’s React components for the UI, leveraging some JavaScript logic from the Cordova section.

App Configuration

First, install the Ionic tools:

1
npm install -g @Ionic/cli native-run cordova-res

Navigate to the project folder and use the Ionic CLI to create a new project folder:

1
ionic start twitter-curation-Ionic blank --type=react --capacitor

Enter the newly created folder:

1
cd twitter-curation-Ionic

Run the blank app:

1
ionic serve

The app is now set up. Before defining routes, note that Ionic initializes the project using TypeScript. While TypeScript is not strictly necessary, it offers some beneficial features and will be used in this implementation.

Router Setup

This implementation will use three routes: input, sentimentDisplay, and topicDisplay. This is to leverage Ionic’s transition and navigation features and because accordion lists are not readily available in Ionic’s component library. While custom implementations are possible, this tutorial will stick to the provided Ionic components.

The App.tsx file should already contain the basic route definitions.

Input Page

Functionality Outline

This implementation will largely mirror the Bootstrap implementation, with a few key differences. TypeScript will be used, bringing type annotations to the code. Ionic components will be used, offering a similar style to Bootstrap but with OS-sensitive styling. Navigation will be handled dynamically using the history API, but accessing it will differ slightly due to Ionic Router.

Setting Up

Create a stub input component. Create a folder named input under pages and a file named Input.tsx within it. Inside Input.tsx, define a React component:

1
2
3
4
5
6
7
import React, {useState} from 'react';

const Input : React.FC = () => {
    return <div>Input</div>;
}

export default Input;

In App.tsx, modify the component to:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
const App: React.FC = () => (
  <IonApp>
    <IonReactRouter>
      <IonRouterOutlet>
        <Route path="/input" component={Input} exact={true} />
        <Route exact path="/" render={() => <Redirect to="/input" />} />
      </IonRouterOutlet>
    </IonReactRouter>
  </IonApp>
);

Refreshing the app should now display the Input stub component.

Data Containers

Create data containers for inputted Twitter handles and the current input box content. Using TypeScript requires adding type annotations to the useState invocation:

1
2
3
4
5
const Input : React.FC = () => {
    const [text, setText] = useState<string>('');
    const [accounts, setAccounts] = useState<Array<string>>([]);
    return <div>Input</div>;
}

Create a data container to hold the API return values. Since this data needs to be shared across routes, define it in App.tsx. Import useState from React in App.tsx and modify the app container function as follows:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
const App: React.FC = () => {
  const [curatedTweets, setCuratedTweets] = useState<CuratedTweets>({} as CuratedTweets);
  return (
  <IonApp>
    <IonReactRouter>
      <IonRouterOutlet>
        <Route path="/input" component={Input} exact={true} />
        <Route exact path="/" render={() => <Redirect to="/input" />} />
      </IonRouterOutlet>
    </IonReactRouter>
  </IonApp>
  );
}

An editor with syntax highlighting (e.g., Visual Studio Code) might flag CuratedTweets because its interface is undefined. Create a folder named interfaces under src and a file named CuratedTweets.tsx within it. Define the CuratedTweets interface as follows:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
 interface TweetRecordData {
    noun_phrases: Array<string>,
    sentiment: number
}

export interface TweetRecord {
    account: string,
    data: TweetRecordData,
    tweet: string
}

export default interface CuratedTweets {
    groupedByNp: Record<string, Array<TweetRecord>>,
    processedTweets: Array<TweetRecord>,
    sentimentSorted: Array<TweetRecord>
}

The app now understands the API return data structure. Import the CuratedTweets interface in App.tsx. App.tsx should now compile without errors.

Pass the setCuratedTweets function to the Input component and make it aware of this function.

Modify the Input route in App.tsx as follows:

1
<Route path="/input" render={() => <Input setCuratedTweets={setCuratedTweets}/>} exact={true} />

The editor should now flag that Input is unaware of the new prop. Define it in Input.tsx.

Import the CuratedTweets interface and define the ContainerProps interface as follows:

1
2
3
interface ContainerProps {
    setCuratedTweets: React.Dispatch<React.SetStateAction<CuratedTweets>>
}

Finally, modify the Input component definition:

1
2
3
4
5
const Input : React.FC<ContainerProps> = ({setCuratedTweets}) => {
    const [text, setText] = useState<string>('');
    const [accounts, setAccounts] = useState<Array<string>>([]);
    return <div>Input</div>;
}

With the data containers defined, move on to building the UI components.

UI Components

Create an input component and a list display component using Ionic’s containers.

Import the necessary library components:

1
import { IonInput, IonItem, IonList, IonButton, IonGrid, IonRow, IonCol } from '@Ionic/react';

Replace the stub component with an IonInput wrapped in an IonGrid:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
return <IonGrid>
    <IonRow>
        <IonCol>
            <IonInput
              value={text}
              placeholder="Enter accounts to pull from"
              onIonChange={e => setText(e.detail.value!)} />
      </IonCol>
    </IonRow>
</IonGrid>

Note that the event listener is onIonChange instead of onChange. Otherwise, it should look familiar.

In a browser, the app’s appearance might differ from the Bootstrap version. However, switching to emulator mode should make the UI clearer. It will look even better when deployed on a mobile device.

Add “Add to list” and “Pull API” buttons using IonButton. Change the input’s IonCol size to 8 and add the buttons with columns:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
<IonCol size="8">
            <IonInput
              value={text}
              placeholder="Enter accounts to pull from"
              onIonChange={e => setText(e.detail.value!)} />
        </IonCol>
        <IonCol size="2">
            <IonButton style={{float: 'right'}} color="primary" size="small" onClick={onAddClicked}>Add</IonButton>
        </IonCol>
        <IonCol size="2">
           <IonButton style={{float: 'right'}} color="success" size="small" onClick={onPullClicked}>Pull</IonButton>
        </IonCol>

While adding buttons, implement their event handlers.

The handler for adding a Twitter handle to the list is straightforward:

1
2
3
4
5
6
7
8
const onAddClicked = () => {
        if (text === undefined || text.length === 0) {
            return;
        }
        const newAccounts: Array<string> = [...accounts, text];
        setAccounts(newAccounts);
        setText('');
    }

Implement the API call in the next section. For now, add a stub function for onPullClicked:

1
const onPullClicked = () => {}

Create the component to display the list of inputted handles. Use IonList within a new IonRow:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
<IonRow>
    <IonCol>
        <IonList>
            {accounts.map((x:string, i:number) => <IonItem key={i}>
                <IonGrid>
                    <IonRow>
                        <IonCol size="8" style={{paddingTop: '12px'}}>{x}</IonCol>
                        <IonCol><IonButton style={{float: 'right'}} color="danger" size="small" onClick={deleteClickedBuilder(i)}>Delete</IonButton></IonCol>
                    </IonRow>
                </IonGrid>
            </IonItem>)}
        </IonList>
    </IonCol>
</IonRow>

Each list item displays the handle and a delete button within its own IonGrid. Implement the deleteClickedHandler:

1
2
3
4
5
const deleteClickedBuilder = (idx: number) => () => {
    const newAccounts: Array<string> = [...accounts];
    newAccounts.splice(idx, 1);
    setAccounts(newAccounts);
}

Saving at this point should display the Input page with all UI components. Handles can be added, deleted, and the API call button should be functional.

Move the inline styles to CSS. Create a file named input.css in the input folder and import it in Input.tsx. Add the following styles:

1
2
3
4
5
6
7
.input-button {
    float: right;
}

.handle-display {
    padding-top: 12px;
}

Add className="input-button" to all IonButtons and className="handle-display" to the IonCol displaying the intended Twitter handle. Saving should result in a visually appealing layout.

API Call

The API call logic remains similar, except for accessing the history component for dynamic route changes. This will be done using the withHistory hook.

Import the hook:

1
import { useHistory } from 'react-router';

Implement the handler in the input component:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
const history = useHistory();

const switchToDisplay = () => {
    history.push('/display');
}

const onPullClicked = () => {
    fetch('http://prismatic.pythonanywhere.com/get_tweets', {
        method: 'POST',
        mode: 'cors',
        headers: {
            'Accept': 'application/json',
            'Content-Type': 'application/json',
        },
        body: JSON.stringify({
            accounts
        })
    }).then(r=>r.json()).then(j => {
        setCuratedTweets(j);
        switchToDisplay();
    })
    .catch(e => {
        console.log(e);
    })

}

Adding a Header

Enhance the UI with a header using Ionic’s header feature. This provides a more natural user experience, especially on mobile devices, where it simulates the native OS’s header.

Modify the component import:

1
import { IonPage, IonHeader, IonToolbar, IonTitle, IonContent, IonInput, IonItem, IonList, IonButton, IonGrid, IonRow, IonCol } from '@Ionic/react';

Wrap the UI in an Ionic page with a header:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
return <IonPage>
    <IonHeader>
      <IonToolbar>
        <IonTitle>Twitter Curation App</IonTitle>
      </IonToolbar>
    </IonHeader>
    <IonContent>
      <IonHeader collapse="condense">
        <IonToolbar>
        <IonTitle size="large">Twitter Curation App</IonTitle>
        </IonToolbar>
      </IonHeader>
       <IonGrid>
        <IonRow>
            <IonCol size="8">
                <IonInput
                value={text}
                placeholder="Enter accounts to pull from"
                onIonChange={e => setText(e.detail.value!)} />
            </IonCol>
            <IonCol size="2">
                <IonButton className="input-button" color="primary" size="small" onClick={onAddClicked}>Add</IonButton>
            </IonCol>

            <IonCol size="2">
            <IonButton className="input-button" color="success" size="small" onClick={onPullClicked}>Pull</IonButton>
            </IonCol>

        </IonRow>

        <IonRow>
            <IonCol>
                <IonList>
                    {accounts.map((x:string, i:number) => <IonItem key={i}>
                        <IonGrid>
                            <IonRow>
                                <IonCol size="8" className="handle-display">{x}</IonCol>
                                <IonCol><IonButton className="input-button" color="danger" size="small" onClick={deleteClickedBuilder(i)}>Delete</IonButton></IonCol>
                            </IonRow>
                        </IonGrid>
                 </IonItem>)}
                </IonList>
            </IonCol>
        </IonRow>
    </IonGrid>
    </IonContent>
  </IonPage>

The app should now have a more polished look.

Sentiment Sorted Page

Functionality Outline

The sentiment sorted page will resemble the Bootstrap version but will use TypeScript and Ionic components. The Topic Display will be implemented as a separate route to leverage Ionic’s navigation features on mobile. Therefore, this page needs the ability to navigate to the Topic Display route.

Route Setup

Create a new folder named sentimentsorted and a file named SentimentSorted.tsx within it. Export a stub component:

1
2
3
4
5
6
import React from 'react';

const SentimentSorted: React.FC = () => {
    return <div>Sentiment Sorted</div>
}
export default SentimentSorted;

In App.tsx, import the component:

1
import SentimentSorted from './pages/sentimentsorted/SentimentSorted';

Add the route:

1
<Route path="/display" render={() => <SentimentSorted curatedTweets={curatedTweets} />} />

A TypeScript error will occur because SentimentSorted doesn’t expect the curatedTweets props. Address this in the next section.

UI Components

Define the container’s props:

1
2
3
4
5
import CuratedTweets from '../../interfaces/CuratedTweets';

interface ContainerProps {
    curatedTweets: CuratedTweets
}

Modify the stub display:

1
2
3
const SentimentSorted: React.FC<ContainerProps> = ({curatedTweets}) => {
    return <div>Sentiment Sorted</div>
}

Everything should now compile.

The display is straightforward, consisting of an IonList with display components:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
return <IonGrid>
    <IonRow>
        <IonCol>
        <IonList>
            {(curatedTweets.sentimentSorted).map((x, i) =>
            <IonItem key={i}>
                <IonLabel className="ion-text-wrap">
                    <h2>{x.account}:</h2>
                    <h3>{x.tweet}</h3>
                    <p>({x.data.sentiment.toPrecision(2)})</p>
                </IonLabel>
            </IonItem>)}
        </IonList>
        </IonCol>
    </IonRow>
</IonGrid>

Saving and pulling tweets using the input component should display them in a list.

Add navigation buttons to the IonGrid:

1
2
3
4
5
6
<IonRow>
    <IonCol>
        <IonButton color='primary' onClick={switchToInput}>Back</IonButton>
        <IonButton color="success" onClick={toggleDisplayType}>{switchStr}</IonButton>
    </IonCol>
</IonRow>

Implement switchToInput using the history API:

1
2
3
const switchToInput = () => {
    history.goBack();
}

Implement ToggleDisplayType:

1
2
3
4
5
const toggleDisplayType = () => {
    setDisplayType(displayType === 'Sentiment' ? 'Topic': 'Sentiment');
}

const switchStr = displayType === 'Sentiment'? 'View by Topic': 'View by Sentiment'

The SentimentDisplay component is now complete. Before implementing the Topic Display Page, create the component for displaying all topics.

Topic Groups Component

Add a topic list display option and conditionally display it. Break out the Sentiment Display list. Rename SentimentDisplay to Display and extract the sentiment display list:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
interface SentimentDisplayProps {
    sentimentSorted: Array<TweetRecord>
}

const SentimentDisplay: React.FC<SentimentDisplayProps> = ({sentimentSorted}) => {
    return <IonList>
        {(sentimentSorted || []).map((x, i) =>
        <IonItem key={i}>
            <IonLabel className="ion-text-wrap">
                <h2>{x.account}:</h2>
                <h3>{x.tweet}</h3>
                <p>({x.data.sentiment.toPrecision(2)})</p>
            </IonLabel>
        </IonItem>)}
    </IonList>
}

Note the use of a class definition from the CuratedTweets interface. These components only need a subset of the CuratedTweets object. The topic list is similar:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
interface TopicDisplayProps {
    groupedByNP: Record<string, Array<TweetRecord>>
}

const TopicDisplay: React.FC<TopicDisplayProps> = ({groupedByNP}) => {
    return <IonList>
        {Object.keys(groupedByNP || {}).map((x, i) =>
        <IonItem key={i}  routerLink={`/topicDisplay/${encodeURIComponent(x)}`}>
            <IonLabel className="ion-text-wrap">
                <h2>{x} ({groupedByNP[x].length})</h2>
            </IonLabel>
        </IonItem>)}
    </IonList>
}

Implement conditional display in the Display Component:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
return (
    <IonGrid>
        <IonRow>
            <IonCol>
                <IonButton color='primary' onClick={switchToInput}>Back</IonButton>
                <IonButton color="success" onClick={toggleDisplayType}>{switchStr}</IonButton>
            </IonCol>
        </IonRow>
        {
            displayType === 'Sentiment'? <SentimentDisplay sentimentSorted={curatedTweets.sentimentSorted} /> :
            <TopicDisplay groupedByNP={curatedTweets.groupedByNp} />
         }
    </IonGrid>
);

Ensure to change the default export. Now, implement the Topic Display Page.

Topic Display Page

Functionality Outline

The topic display page resembles the sentiment display but filters tweets based on the topic specified in the route parameter.

Route Setup

Create a page folder named topicdisplay and a TopicDisplay.tsx file within it. Write a stub component and import it into App.tsx. Set up the routes:

1
<Route path="/topicDisplay/:topic" render={() => <TopicDisplay curatedTweets={curatedTweets} /> } />

Implement the UI component.

UI Components

Define the ContainerProps definition:

1
2
3
4
5
6
7
interface ContainerProps {
    curatedTweets: CuratedTweets
}

const TopicDisplay: React.FC<ContainerProps> = ({curatedTweets}) => {
    Return <div>Topic Display</div>
}

Retrieve the topic from the URL pathname using the history API. Import useHistory, instantiate the history API, and extract the topic from the pathname. Also, implement the switch back functionality:

1
2
3
4
5
6
7
const TopicDisplay: React.FC<ContainerProps> = ({curatedTweets}) => {
    const history = useHistory();
    const switchToDisplay = () => {
        history.goBack();
    }
    const topic = history.location.pathname.split('/')[2];
    const tweets = (curatedTweets.groupedByNp || {})[topic];

With the tweets for the specific topic available, displaying them is simple:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
return (
    <IonGrid>
        <IonRow>
            <IonCol>
                <IonButton color='primary' onClick={switchToDisplay}>Back</IonButton>
            </IonCol>
        </IonRow>
        <IonRow>
            <IonCol>
                <IonList>
                    {(tweets || []).map((x, i) => <IonItem key={i}>
                        <IonLabel className="ion-text-wrap">
                            <h2>{x.account}:</h2>
                            <h3>{x.tweet}</h3>
                            <p>({x.data.sentiment.toPrecision(2)})</p>
                        </IonLabel>
                    </IonItem>)}
                </IonList>
            </IonCol>
        </IonRow>
    </IonGrid>
);

Save and run. The app should be functional.

Running the App in the Emulator

Run a few Ionic commands to add the mobile platform and copy the code, similar to the Cordova setup:

1
2
3
4
5
ionic build # builds the app
ionic cap add ios # adds iOS as one of the platforms, only have to run once
ionic cap copy # copy the build over
ionic cap sync # only need to run this if you added native plugins
ionic cap open ios # run the iOS emulator

The app should now appear in the emulator.

React Native Implementation

React Native Overview

React Native takes a different approach compared to the web-based methods of Cordova and Ionic. It renders React code as native components, offering several advantages.

First, React Native integrates deeply with the underlying operating system, allowing developers to leverage new smartphone and OS-specific features not readily accessible through Cordova or Capacitor. Second, the absence of a web-based rendering engine generally results in faster performance compared to Cordova apps. Finally, the ability to integrate native components provides developers with finer-grained control over their applications.

This implementation will utilize logic from previous sections and employ NativeBase, a React Native component library, for UI development.

App Configuration

Install the necessary React Native components following the instructions here.

Once React Native is installed, initiate the project:

1
react-native init TwitterCurationRN

Allow the setup script to run, creating the project folder. Navigate to the folder and run react-native run-ios to launch the example app in the emulator.

Install NativeBase, the component library:

1
2
npm install --save native-base
react-native link

Install the React Native stack navigator:

1
npm install --save @react-navigation/stack @react-navigation/native

Link and install the native plugins:

1
2
3
4
react-native link
cd ios
pod-install
cd

Router Setup

Use the installed stack navigator for routing.

Import the router components:

1
2
import { NavigationContainer } from '@react-navigation/native';
import {createStackNavigator} from '@react-navigation/stack';

Create a stack navigator:

1
const Stack = createStackNavigator();

Modify the App component to use the stack navigator:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
const App = () => {
  return (
    <>
      <StatusBar barStyle="dark-content" />
        <NavigationContainer>
          <Stack.Navigator initialRouteName="Entry">
            <Stack.Screen name="Entry" component={Entry} />
          </Stack.Navigator>
        </NavigationContainer>
    </>
  );
};

An error will occur because Entry is undefined. Define a stub component to resolve this.

Create a components folder in the project, create a file named Entry.jsx within it, and add a stub component:

1
2
3
4
5
6
import React, {useState} from 'react';
import { Text } from 'native-base';

export default Entry = ({navigation}) => {
    return <Text>Entry</Text>; // All text must be wrapped in a <Text /> component or <TextView /> if youre not using NativeBase.
}

Import the Entry component in the app. It should now build successfully.

Next, code the Input page.

Input Page

Functionality Outline

The implemented page will resemble previous versions but will utilize NativeBase components. Most JavaScript and React APIs, such as hooks and fetch, will remain the same.

The main difference lies in working with the navigation API.

UI Components

Import the necessary NativeBase components: Container, Content, Input, List, ListItem, and Button. These components have analogs in Ionic and Bootstrap React, and NativeBase’s design makes it intuitive for developers familiar with those libraries.

Import the components:

1
2
3
import { Container, Content, Input,

    Item, Button, List, ListItem, Text } from 'native-base';

Define the component:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
return <Container>
        <Content>
          <Item regular>
            <Input placeholder='Input Handles Here' onChangeText={inputChange}
          value={input} />
            <Button primary onPress={onAddClicked}><Text> Add </Text></Button>
            <Text> </Text>
            <Button success onPress={onPullClicked}><Text> Pull </Text></Button>
          </Item>
        <Item>
                <List style={{width: '100%'}}>
                   {handles.map((item) => <ListItem key={item.key}>
                        <Text>{item.key}</Text>
                    </ListItem>)}
                </List>
          </Item>
        </Content>
      </Container>

Implement the state and event handlers:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
const [input, changeInput] = useState('');
    const [handles, changeHandles] = useState([]);
    const inputChange = (text) => {
        changeInput(text);
    }

   const onAddClicked = () => {
        const newHandles = [...handles, {key: input}];
        changeHandles(newHandles);
        changeInput('');
    }

Finally, implement the API call:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
const onPullClicked = () => {
    fetch('http://prismatic.pythonanywhere.com/get_tweets', {
        method: 'POST',
        mode: 'cors',
        headers: {
            'Accept': 'application/json',
            'Content-Type': 'application/json',
        },
        body: JSON.stringify({
            accounts: handles.map(x => x.key)
        })
    }).then(r=>r.json()).then(data => {
        navigation.navigate('SentimentDisplay', { data });
    })
    .catch(e => {
        console.log(e);
    })
}

This implementation mirrors the NativeBase version, except for navigation. The stack navigator provides a “navigation” prop to its components, enabling navigation between routes using .navigate. Data can also be passed to the target component using this mechanism.

Make Entry aware of the navigation component. Modify the component function declaration:

1
export default Entry = ({navigation}) => {

Saving should now display the page.

Sentiment Sorted Page

Functionality Outline

The sentiment page will be implemented similarly to previous sections but with different styling and navigation library usage.

Since React Native lacks CSS, define a StyleSheet object or code styles inline. This implementation will create a global stylesheet to share styling across components.

The StackNavigator provides a built-in Back navigation button, eliminating the need for a custom implementation.

Route Setup

Route definition in StackNavigator is straightforward. Create a new Stack Screen and assign the component to it, similar to React Router.

1
2
3
4
5
6
 <NavigationContainer>
    <Stack.Navigator initialRouteName="Entry">
        <Stack.Screen name="Entry" component={Entry} />
        <Stack.Screen name="SentimentDisplay" component={SentimentDisplay} />
    </Stack.Navigator>
</NavigationContainer>

Create a stub component in components/SentimentDisplay.js:

1
2
3
4
5
6
7
8
import React from 'react';
import {Text} from 'native-base';

const SentimentDisplay = () => {
    return <Text>Sentiment Display</Text>;
}

export default SentimentDisplay;

Import the component:

1
import SentimentDisplay from './components/SentimentDisplay';

Create the global stylesheet.

Global StyleSheet

Create a file named globalStyles.js. Import the StyleSheet component from React Native and define the styles:

1
2
3
4
5
6
import {StyleSheet} from 'react-native';

export default StyleSheet.create({
    tweet: {paddingTop: 5},
    accountName: {fontWeight: '600'},
})

Now, code the UI.

UI Components

The UI component will look familiar, with the exception of how routes are handled. Use StackNavigator’s special props, navigation and route, to access the current application state and navigate to the topic display if the user chooses to view it.

Modify the component definition to access the navigation props:

1
const SentimentDisplay = ({route, navigation}) => {

Implement application state reading and navigation functionality:

1
2
3
4
const {params: {data}} = route;
const viewByTopicClicked = () => {
    navigation.navigate('TopicDisplay', { data });
}

Import the global styles:

1
import globalStyles from './globalStyles';

Import the components:

1
2
import { View } from 'react-native';
import {List, Item, Content, ListItem, Container, Text, Button} from 'native-base'; 

Finally, add the component implementations:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
return <Container>
    <Content>
        <Item>
         <Button primary onPress={viewByTopicClicked}><Text>View By Topic</Text></Button>
        </Item>
        <Item>
            <List style={{width: '100%'}}>
                {data.sentimentSorted.map((item, i) => <ListItem key={i}>
                    <View style={globalStyles.listItem}>
                    <Text style={globalStyles.accountName}>{item.account}:</Text>
                    <Text style={globalStyles.tweet}>{item.tweet} ({item.data.sentiment.toPrecision(2)})</Text>
                    </View>
                </ListItem>)}
            </List>
        </Item>
    </Content>
</Container>;

Save and test the app. The sentiment display should now be functional. Next, move on to the topic grouping page.

Topic Grouping Page

Functionality Outline

The Topic Display will use a handler builder to create a navigation function for navigating to the display page for a specific topic item. It will also define a page-specific stylesheet.

This section introduces TouchableOpacity, a React Native component that functions like a button.

Route Setup

Define the route:

1
2
3
4
5
<Stack.Navigator initialRouteName="Entry">
    <Stack.Screen name="Entry" component={Entry} />
    <Stack.Screen name="SentimentDisplay" component={SentimentDisplay} />
    <Stack.Screen name="TopicDisplay" component={TopicDisplay} />
</Stack.Navigator>

Create the stub component components/TopicDisplay.js:

1
2
3
4
5
6
7
8
import React from 'react';
import {Text} from 'native-base';

const TopicDisplay = () => {
    return <Text>Topic Display</Text>;
} 

export default TopicDisplay;

Import the component:

1
import TopicDisplay from './components/TopicDisplay;

UI Components

Import the necessary library functions:

1
2
3
4
5
6
import {
   View,
   TouchableOpacity,
   StyleSheet
 } from 'react-native';
import {List, Item, Content, ListItem, Container, Text} from 'native-base';

Import the global styles:

1
import globalStyles from './globalStyles';

Define custom styles:

1
2
3
const styles = StyleSheet.create({
    topicName: {fontWeight: '600'},
})

Define the navigation props:

1
export default TopicDisplay = ({route, navigation}) => {

Define the data and action handlers, utilizing a handler builder (a function that returns another function):

1
2
3
4
const {params: {data}} = route;
const specificItemPressedHandlerBuilder = (topic) => () => {
    navigation.navigate('TopicDisplayItem', { data, topic });
}

Finally, define the components, using TouchableOpacity with an onPress handler. TouchableTransparency could also be used, but TouchableOpacity’s click-and-hold animation is better suited for this application.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
return <Container>
    <Content>
        <Item>
            <List style={{width: '100%'}}>
                {Object.keys(data.groupedByNp).map((topic, i) => <ListItem key={i}>
                    <View style={globalStyles.listItem}>
                    <TouchableOpacity onPress={specificItemPressedHandlerBuilder(topic)}>
                        <Text style={styles.topicName}>{topic}</Text>
                    </TouchableOpacity>
                    </View>
                </ListItem>)}
            </List>
        </Item>
    </Content>
 </Container>;

The Topic Grouping Page should now be functional. Move on to the Topic Display Item Page.

Topic Display Item Page

Functionality Outline

The Topic Display Item Page will be straightforward, as all the intricacies have been covered in previous sections.

Route Setup

Add the route definition:

1
<Stack.Screen name="TopicDisplayItem" component={TopicDisplayItem} />

Add the import statement:

1
import TopicDisplayItem from './components/TopicDisplayItem';

Create the stub component, this time importing the necessary NativeBase components and defining the route props:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
import React from 'react';
import {View} from 'react-native';
import {List, Item, Content, ListItem, Container, Text} from 'native-base';
 
import globalStyles from './globalStyles';
 
const TopicDisplayItem = ({route}) => {
    const {params: {data, topic}} = route;
    return <Text>Topic Display Item</Text>;
}
 
export default TopicDisplayItem;

UI Components

The UI Component is simple, with no custom logic.

Implement the UI components:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
return <Container>
    <Content>
        <Item>
            <List style={{width: '100%'}}>
                {data.groupedByNp[topic].map((item, i) => <ListItem key={i}>
                    <View style={globalStyles.listItem}>
                    <Text style={globalStyles.accountName}>{item.account}:</Text>
                    <Text style={globalStyles.tweet}>{item.tweet} ({item.data.sentiment.toPrecision(2)})</Text>
                    </View>
                </ListItem>)}
            </List>
        </Item>
    </Content>
</Container>;

Save the changes. The app should now be fully functional.

Running the App

The app should already be running in the emulator from the previous steps.

Having your app running in the emulator is already taken care of since you’re using React Native. This convenience is a major advantage of React Native’s development environment.

With that, we’ve reached the end of the coding part of this article. Let’s recap what we learned about each technology.

Comparing the Technologies

Cordova: Pros and Cons

Cordova’s biggest strength is the remarkable speed at which a proficient web developer can build a functional and fairly polished app. Web development expertise translates well since you’re essentially coding a web app. The development process is straightforward and fast, and utilizing the Cordova API is intuitive and easy.

However, Cordova’s reliance on web components is its main drawback. Users anticipate a distinct user experience and interface design from mobile apps. When an application resembles a mobile website, the experience can be jarring. Furthermore, many standard app features, like transitions and navigation elements, require manual implementation.

Ionic: Pros and Cons

What impressed me most about Ionic was the abundance of mobile-centric features it provided out of the box. By using familiar web development patterns, I could create an app with a more native mobile feel compared to using Cordova and React-Bootstrap alone. Features like navigation animations, native-styled buttons, and a wide array of user interface elements greatly enhanced the user experience.

Ironically, Ionic’s strengths also contribute to its weaknesses. Firstly, anticipating the app’s behavior across different environments was occasionally tricky. A UI layout might appear differently across various platforms. Secondly, Ionic’s reliance on multiple underlying technologies made accessing some components challenging. Lastly, as a React developer, I found that Ionic-React, initially designed for Angular, had comparatively less documentation and support. However, the Ionic team is clearly attentive to React developers, delivering new features rapidly.

React Native: Pros and Cons

React Native provided a very smooth mobile development experience. Having a direct connection to the emulator eliminated uncertainty regarding the app’s appearance. Debugging was efficient due to the familiar web-based interface and the ability to leverage existing web development debugging techniques. The ecosystem also boasts a robust selection of tools and libraries.

React Native’s proximity to the native interface is its main disadvantage. Many DOM-based libraries are incompatible, necessitating the learning of new libraries and best practices. Styling, without CSS, felt less intuitive. Lastly, becoming acquainted with new components (like View instead of div, mandatory Text wrappers, and the differences between Buttons, TouchableOpacity, and TouchableTransparency) presents a learning curve for those new to React Native’s mechanics.

When to Use Each Technology

Given the distinct strengths and weaknesses of Cordova, Ionic, and React Native, each shines in specific contexts.

Cordova is ideal for existing web-first applications with a strong UI brand identity. It allows access to native features while reusing existing components and preserving the brand. Simple applications using responsive frameworks might require minimal changes for porting. However, the app might resemble a webpage more than a native app, and some expected mobile app components will demand extra coding. Therefore, Cordova is recommended for porting web-first projects to mobile.

Ionic is a strong choice for new app-first projects with a team proficient in web development. Its library facilitates the creation of near-native looking and feeling apps using existing web development skills. Web development best practices translate well, and CSS styling works flawlessly. Moreover, the mobile version will have a more native appearance than a responsive website. However, some React-Ionic-Native API integrations might require manual adjustments, potentially increasing development time. Therefore, Ionic is recommended for new projects aiming for significant code sharing between a mobile-capable web app and a dedicated mobile app.

React Native is worth considering for new applications with an existing native codebase. It’s also suitable for those familiar with React Native or when prioritizing the mobile experience over a hybrid approach. As a web developer primarily, I found the initial learning curve steeper compared to Ionic or Cordova due to differences in component structure and conventions. However, once those nuances are grasped, coding becomes quite smooth, especially with libraries like NativeBase. Considering the development environment and control over the app, React Native is recommended if your primary focus is a mobile application.

Future Topics

One aspect I didn’t have time to cover was accessing native APIs, such as camera, geolocation, or biometrics. One of mobile development’s greatest assets is the availability of a feature-rich API ecosystem not easily accessible from the browser.

In future articles, I’d like to explore how easy it is to build such native API-powered applications using various cross-platform technologies.

Conclusion

Today, we built a Twitter curation app using three cross-platform mobile development technologies. Hopefully, this provided valuable insights into each technology and inspired you to create your own React-based application.

You can find the code for this tutorial on GitHub.

Licensed under CC BY-NC-SA 4.0