DEV Community

loading...
Cover image for Creating an Android App with React Native, NativeBase and WatermelonDB

Creating an Android App with React Native, NativeBase and WatermelonDB

cjx3711 profile image Chai Jia Xun Originally published at chaijiaxun.com on ・14 min read

My motivation

I used to code android apps 10 years ago. Back then Google had not much opinion on how apps should be coded and I was very happy to code terrible apps with eclipse and Java. About 3 years ago, I wanted to code another app, after years in web development. I figured I’d try this newfangled Kotlin thing with the cool new recommended MVVC and Room ORM that Google has been trying to push. It was a terrible mistake. It was so painfully slow to make any progress on my app at all, and I spent so much of my time trying to figure out how to implement everything. So much so that each time I found the motivation to start work on the project, I would lose it again after spending a whole weekend on it and barely making any progress. To be fair to Kotlin and native development, this is mostly because I am primarily a web developer and didn’t have someone to mentor me on Android dev. So it was a very different paradigm for me.

On multiple occasions, I considered switching to React Native or some other cross-platform framework. But the issue with those is that they didn’t come with the default android app feel, and would look a lot more like a bootstrap web app unless I hired a designer. Also, I had already sunk so much effort into my app, why would I want to recode the entire thing?

Recently, I had found my motivation again to start working on my app. But just thinking of having to figure out Kotlin again put me off. Then I decided to instead look into the feasibility of React Native once again, and I was taken by surprise. The developer community around the framework has grown significantly with libraries like NativeBase and WatermelonDB since I last looked at it. This alleviated many concerns I had with the feasibility of coding an app in React Native. This meant that redoing my app in react native would probably only take a week or so to get to the point I was already at.

The Technologies

If you are reading this, I assume you’d most likely know what these technologies are, but here’s a quick overview anyway.

React Native is a way to code native apps with react code, making it easy for web developers to make native apps. It allows deploying to both android and iOS with a single codebase (for the most part)

Native Base is a UI library that sits on top of React Native to make the UI coding easier, and more consistent. Instead of having to define different styles for both iOS and Android, you just need to code with their set of components and it’ll automatically take on each OSes native look.

WatermelonDB is an observable lazy loading database for web or mobile applications that are designed to be offline-first. This also ensures the app is performant even with a large amount of data. It comes with functionality to make syncing with a server easy if it is ever required.

It comes with functionality to make syncing with a server easy if it is ever required.

All these projects are open-sourced too, which is great.

The Project and Requirements

What this project is

  • To keep things simple, we’ll make a classic todo app.
  • The focus on this tutorial is to get a basic app that creates a basic UI, and touches on the database.
  • We will have only a single page
  • The app will be persistent, and will save the data to the database.

What this is not

  • This is not a UI/UX tutorial, you will see some terrible UI, you have been warned.
  • This will not be using multiple screens as that requires a navigation library like React Navigation
  • This will not be using complex forms as that will require a good form handling library like formik
  • This will have no state management like Redux

you are making a real app, I will provide additional recommended libraries at the end of this post. For this tutorial, I will stick to React Native, NativeBase, and WatermelonDB.

Platform

I will be coding this on macOS, and targeting the Android platform. Since this is React Native, you could code on either Linux or Windows and target iOS or web as well. You would probably just need to change some configurations, but coding on MacOS for Android is the configuration I have chosen to use. I have also tested this on Linux and it seems to work fine.

Required Software

Android Studio with Android SDK >= 28

node >= 10.21

npm >= 6.9.0

JDK >= 1.8

Recommended Knowledge

How to code react (not strictly necessary)

How to use git (also not really necessary)

How to use the terminal

How databases work

What an app is

How to use a text editor

How to use a keyboard

How to use your computer

The Process

For each of these sections, I will be providing a link to the relevant commit so that you can see the changes from the previous state.

Here's a link to the repo if that's all you want.

There will be a total of 5 sections to this tutorial:

  1. Starting a basic react native app
  2. Getting started with NativeBase
  3. Installing WatermelonDB
  4. Creating the schema and tables
  5. Hooking everything up

Section 1: Starting a basic react native app

💡 Code changes for this section 💡

To start off, make sure you have the required software installed. Since this is an Android tutorial, you will also need to create an Android Virtual Device to run your app on. You can follow the instructions on the official react-native documentation here.

Once you have node, watchman, jdk, and the Android Virtual Device set up, we will create a new app.

We’ll use the built-in react-native cli to create the app. You can call this app whatever you want but we’ll call ours BadTodo.

npx react-native init BadTodo
Enter fullscreen mode Exit fullscreen mode

Creating an Android App with React Native, NativeBase and WatermelonDB

Notice there are a bunch of errors related to iOS. Well since I am not coding for iOS, I am going to ignore these errors.

After that, we need to run the metro js server that the app will use to get the javascript code. First, navigate to the new folder cd BadTodo and run the following command,

npx react-native start
Enter fullscreen mode Exit fullscreen mode

Then we open a new terminal and run the android app.

npx react-native run-android
Enter fullscreen mode Exit fullscreen mode

You should end up with something that looks like this:

Creating an Android App with React Native, NativeBase and WatermelonDB
Left: metro server, Middle: run-android, Right: Android emulator

We now have a basic react native app running in the android emulator. This is the point where you should probably make this a git repo.

git init
git add -A
git commit -m “Initial React Native Commit”
Enter fullscreen mode Exit fullscreen mode

In the next section, we’ll install NativeBase and create a basic todo page.


Section 2: Getting started with NativeBase

💡 Code changes for this section 💡

First, we’re going to install nativebase, simply run this command in the same repo.

npm install native-base --save
Enter fullscreen mode Exit fullscreen mode

Creating an Android App with React Native, NativeBase and WatermelonDB

NativeBase does not automatically link the peer dependencies, so run this to link them.

npx react-native link
Enter fullscreen mode Exit fullscreen mode

You will see that native-base has been added to your package.json. Here’s the commit where that happens.

Creating an Android App with React Native, NativeBase and WatermelonDB

Now that we have that up, we’re going to change the home screen to something less template looking and more bad-todo looking.

Open up App.js and replace the entire thing with the following code

import React, { Component } from 'react';
import { Container, Header, Title, Content, Footer, FooterTab, Button, Right, Body, Text, ListItem, CheckBox, Grid, Row, Form, Input, Item, Label } from 'native-base';

export default class BadTodo extends Component {
 render() {
   return (
     <Container>
       <Header>
         <Body>
           <Title>Really Bad Todo</Title>
         </Body>
         <Right />
       </Header>
       <Content padder>
         <Text>
           There are { 0 } tasks
         </Text>
         <ListItem>
           <Grid>
             <Row>
               <CheckBox checked={false}/>
               <Body>
                 <Text>Need to do this</Text>
               </Body>
             </Row>
           </Grid>
         </ListItem>
         <ListItem>
           <Grid>
             <Row>
               <CheckBox checked={false}/>
               <Body>
                 <Text>Need to do that</Text>
               </Body>
             </Row>
           </Grid>
         </ListItem>
         <Form>
           <Item floatingLabel>
             <Label>New Task Name</Label>
             <Input/>
           </Item>
         </Form>
       </Content>
       <Footer>
         <FooterTab>
           <Button full>
             <Text>Add Task</Text>
           </Button>
         </FooterTab>
       </Footer>
     </Container>
   );
 }
}

Enter fullscreen mode Exit fullscreen mode

You can then reload the app (press r on the metro window) and you should get something like this.

Creating an Android App with React Native, NativeBase and WatermelonDB

I hope that the code layout and the results are self-explanatory. You can see that NativeBase takes all the pain of styling out of your code and it looks somewhat like native android with very little code. The best part is that if you were coding for the iPhone as well, it would be the same code! For a list of components and example code, you can check out their official documentation.

We’ll hook this up to something more dynamic in section 5, after we set up WatermelonDB.


Section 3: Installing WatermelonDB

💡 Code changes for this section 💡

You can follow the official WatermelonDB docs to get it installed, but I will be covering the steps here as well.

First, install WatermelonDB with these commands. You should still be in the root directory of your app.

yarn add @nozbe/watermelondb
yarn add @nozbe/with-observables
yarn add --dev @babel/plugin-proposal-decorators

Enter fullscreen mode Exit fullscreen mode

Next, create a .babelrc file in the root directory.

touch .babelrc
Enter fullscreen mode Exit fullscreen mode

Then paste the following snippet in the newly created file.

{
  "presets": ["module:metro-react-native-babel-preset"],
  "plugins": [
    ["@babel/plugin-proposal-decorators", { "legacy": true }]
  ]
}

Enter fullscreen mode Exit fullscreen mode

❓ If you are confused as to where to put the code, check out this commit on github.

In the android/settings.gradle, add the following code:

include ':watermelondb'
project(':watermelondb').projectDir =
    new File(rootProject.projectDir, '../node_modules/@nozbe/watermelondb/native/android')

Enter fullscreen mode Exit fullscreen mode

In android/app/build.gradle, add the following.

❗ Note that there are 2 build.gradle files, please make sure you are editing the correct one.

apply plugin: 'kotlin-android'
// ...
dependencies {
    // ...
    implementation project(':watermelondb')
}

Enter fullscreen mode Exit fullscreen mode

In android/build.gradle, add Kotlin support to the project

buildscript {
    ext {
        // ...
        kotlin_version = '1.3.21'
    }
    dependencies {
        // ...
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
    }
}

Enter fullscreen mode Exit fullscreen mode

In android/app/src/main/java/{YOUR_APP_PACKAGE}/MainApplication.java, add:

import com.nozbe.watermelondb.WatermelonDBPackage; // This line
// ...
List<ReactPackage> packages = new PackageList(this).getPackages();
packages.add(new WatermelonDBPackage()); // This line
Enter fullscreen mode Exit fullscreen mode

Now that this is done, you should rerun your android app by running:

npx react-native run-android
Enter fullscreen mode Exit fullscreen mode

Everything should compile without errors. If not, please refer to the troubleshooting section at the bottom of this post


Section 4: Creating the schema and tables

💡 Code changes for this section 💡

If you’ve used an ORM before, WatermelonDB should be pretty familiar to use. If not, here are some basic concepts.

There are two main things you will need to create, the schema, and the models. The schema is usually the thing that generates the database tables and fields, while the model is the object that you get when you query the database from the ORM. Some ORMs automatically create the schema for you given the model file, but watermelon does not.

Watermelon only has three column types. Boolean, Number, and String. Dates are stored as numbers. IDs are stored as strings. You can store json objects as strings as well, but it’s not generally recommended as WatermelonDB is unable to index json keys. (Some DBs like Postgres can though!)

Let’s begin by creating a new folder src/db. This is where our database stuff will go.

Next, we’ll create our schema in this file src/db/schema.js

import { appSchema, tableSchema } from '@nozbe/watermelondb'

export const mySchema = appSchema({
 version: 1, // Used for migrations
 tables: [
   tableSchema({
     name: 'tasks', // Convention is to be plural lowercase of the model
     columns: [
       { name: 'description', type: 'string' },
       { name: 'is_complete', type: 'boolean' },
       // These will be dates but we define it as numbers
       { name: 'created_at', type: 'number' },
       { name: 'updated_at', type: 'number' }
     ]
   })
 ]
})

Enter fullscreen mode Exit fullscreen mode

Then we’ll create the Task model under src/db/Task.js

import { Model } from '@nozbe/watermelondb'
import { field, date, readonly, action } from '@nozbe/watermelondb/decorators'

// Naming convention is CamelCase singular
export default class Task extends Model {
 static table = 'tasks'

 // These are our own fields
 @field('description') description
 @field('is_complete') isComplete

 // These are special fields that will automatically update when the
 // record is created and updated
 @readonly @date('created_at') createdAt
 @readonly @date('updated_at') updatedAt

 // Actions are functions that you can call on the database object
 // These can be something like calculating a new field, but in this
 // case we're using them to modify the database object directly.
 @action async rename(newName) {
   await this.update(t => {
     t.description = newName
   })
 }

 @action async delete() {
   await this.markAsDeleted()
 }
}

Enter fullscreen mode Exit fullscreen mode

Following that, we need to register the model with the database. In your index.js file, add these lines of code just before the register component line.

import Task from './src/model/Task'
import { mySchema } from './src/db/schema';
import SQLiteAdapter from '@nozbe/watermelondb/adapters/sqlite'
import { Database } from '@nozbe/watermelondb';

const adapter = new SQLiteAdapter({
 dbName: 'BadTodo',
 schema: mySchema,
})

const database = new Database({
 adapter,
 modelClasses: [Task],
 actionsEnabled: true,
})
Enter fullscreen mode Exit fullscreen mode

Now we run the android app again. npx react-native run-android. If you’ve set up everything correctly, you will see the database being set up in the metro window.

Creating an Android App with React Native, NativeBase and WatermelonDB

❗ Note that each time you make any changes to the database schema, you will need to uninstall the app or clear the app cache. You can do this by long-pressing the app and clicking on app info, then uninstalling it. After that, all you need to do is to run the run-android command again to reinstall the application.

🧡 Bonus: If you want to know how to do relations and indices, check out github where I added the code for that.

We’re almost there!


Section 5: Hooking everything up

💡 Code changes for this section 💡

Now comes the best part, hooking everything up so that we have a working todo app.

In your App.js file, add this code just above the render() method.

constructor(props) {
   super(props)

   // A better way to do this would be to use observables.
   // You can find out more about this in the watermelondb docs.
   this.updatePage()

   // We're just using the react basic state.
   // For a real app, it's recommended to use something like redux
   this.state = {
     taskCount: 0,
     tasks: [],
     newTaskDescription: ''
   }
 }

 // Refreshes the page
 async updatePage() {
   // Note this is the table name 'tasks' and not the model name 'Task'
   const tasksCollection = this.props.database.get('tasks')
   // query() without any parameters will get all the records in the table
   const taskCount = await tasksCollection.query().fetchCount()
   const tasks = await tasksCollection.query()
   console.log("Tasks count", taskCount)
   this.setState({taskCount: taskCount, tasks: tasks})
 }


 // Adds a new task from the newTaskDescription varaible in the component state
 async add() {
   const tasksCollection = this.props.database.get('tasks')
   if (this.state.newTaskDescription == '') {
     alert("Cannot add a blank task")
     return false;
   }
   await this.props.database.action(async () => {
     const newTask = await tasksCollection.create(task => {
       task.description = this.state.newTaskDescription;
       task.isComplete = false
     })
     console.log("Adding new task")
   })
   this.setState({newTaskDescription: ''})
   console.log("Added new task")
   this.updatePage()
 }

 // This should actually not delete the task, but instead set the
 // is_complete variable to true. But this is just a proof of concept.
 async setChecked(task) {
   console.log("Setting checked", task.id)
   await task.delete()
   this.updatePage()
 }

 // Renames a task. It's currently hardcoded to Renamed!
 async rename(task) {
   console.log("Renaming", task.id)
   await task.rename("Renamed!")
   this.updatePage()
 }

 // This is called when the text is changed.
 async onChangeText(text) {
   this.setState({newTaskDescription: text})
   console.log(text)
 }
Enter fullscreen mode Exit fullscreen mode

Notice that the code assumes there is a database object passed in from the props? Well, let’s pass the database object into the props then. In index.js, replace the line:

AppRegistry.registerComponent(appName, () => App);
Enter fullscreen mode Exit fullscreen mode

with

import React from 'react';
// ...
const AppWrapper = (props) => {
 return <App {...{
   ...props,
   database: database,
 }}/>;
};

AppRegistry.registerComponent(appName, () => AppWrapper);
Enter fullscreen mode Exit fullscreen mode

This creates a wrapper where we get to pass in the database object.

Now we need to make sure our render method actually makes use of the component state. Replace the whole render method with the code below:

render() {
   return (
     <Container>
       <Header>
         <Body>
           <Title>Really Bad Todo</Title>
         </Body>
         <Right />
       </Header>
       <Content padder>
         <Text>
           There are { this.state.taskCount } tasks
         </Text>
         {
           this.state.tasks.map((m, i) => {
             return (
               <ListItem key={m.id}>
                 <Grid>
                   <Row>
                     <CheckBox checked={m.isComplete} onPress={() => this.setChecked(m) }/>
                     <Body>
                       <Text onPress={() => this.rename(m) }>{m.description}</Text>
                     </Body>
                   </Row>
                 </Grid>
               </ListItem>
             )
           })
         }
         <Form>
           <Item floatingLabel>
             <Label>New Task Name</Label>
             <Input value={this.state.newTaskDescription} onChangeText={text => this.onChangeText(text)}/>
           </Item>
         </Form>
       </Content>
       <Footer>
         <FooterTab>
           <Button full onPress={() => this.add()}>
             <Text>Add Task</Text>
           </Button>
         </FooterTab>
       </Footer>
     </Container>
   );
 }
Enter fullscreen mode Exit fullscreen mode

Once done, you should have a fully* functional working todo app!

<!--kg-card-begin: html-->* definition of fully may vary.<!--kg-card-end: html-->

If you have issues, check the troubleshooting section.


The Result

We now have a functional CRUD app. 🎉🎉🎉

Creation

To add a task, type a description in the text field, and click on Add Task at the bottom

Retrieval

Well, the task list itself is an example of retrieval

Update

Tapping on the task name will rename it to Renamed!

Delete

Checking the task will delete it.

Creating an Android App with React Native, NativeBase and WatermelonDB

Further Improvements

This app is nowhere near what you would consider complete or even production-ready, but I hope it helped you to understand the basics of an app with a database.

For further reading, please take a look at the NativeBase and WatermelonDB documentation, they are pretty detailed.

These are things that should have been done but aren’t.

Using a router for page navigation

An app does not exist as a single page. We definitely need more than one page. In this case, we need a routing library. One that I see come up pretty often is React Navigation.

Using a library for state management

As apps get much bigger, allowing your state to be modified willy nilly by anyone is a recipe for many many bugs. A state management library like Redux will help you keep your app state sane.

Using observables for the page updates

Needing to call the refresh method to update your page is fine, but that sometimes leads to unnecessary updates. You should instead use the observable paradigm which is described in this section of the watermelon docs. Observables mean that a list or text field will automatically update when the underlying database updates. That’s the whole point of using WatermelonDB as opposed to other React Native databases.

Setting this up for iOS as well

I don’t like Apple so I didn’t bother (yes I know I’m using a Mac, it was required for my job okay?), but the whole point of React Native and NativeBase is to allow deploying to multiple devices with a single code base.

It’s probably better to start your project off testing with both iOS and Android, instead of trying to get it running on iOS once your app is complete. You might end up finding out that some things you did would not work on iOS and you might spend way more time than necessary trying to “port” it over to iOS.


As you can see, creating an app is pretty easy for a web developer these days. With so many libraries out there you don't even need to learn a new language to code a simple app. If you do it right, you can even use the same codebase for your website since WatermelonDB supports web apps as well.


Troubleshooting

Troubleshooting Section 3: Installing WatermelonDB

Creating an Android App with React Native, NativeBase and WatermelonDB

I ran into some issues when trying to run the android app. The error message is pretty misleading. It tells you that you should check if your Android development environment is set up, but we already know that is working since we’ve been able to run the app before. The real error is above. In this case it was simply that my android virtual device had run out of storage space.

 > Could not resolve project :watermelondb.
     Required by:
         project :app
      > Unable to find a matching configuration of project :watermelondb:
          - None of the consumable configurations have attributes.

Enter fullscreen mode Exit fullscreen mode

In this case, this means that the WatermelonDB installation likely doesn’t exist for some reason. If you take a look at the settings.gradle you will see that we are sourcing the watermelon db installation from this folder: ../node_modules/@nozbe/watermelondb/native/android

So you should check if that folder exists. In my case it did not and I could not seem to install it. So I deleted the node_modules folder, updated my node to a newer version and tried to reinstall WatermelonDB again. Then it worked.

Troubleshooting Section 5: Hooking everything up

WARN Possible Unhandled Promise Rejection (id: 0):
TypeError: undefined is not an object (evaluating 'columnSchema.type')
Enter fullscreen mode Exit fullscreen mode

This means a column is named wrongly, check the column names in the Task.js file and in the schema.js file


Discussion (0)

pic
Editor guide