Build a React Native Module with Couchbase Lite on Android

In this tutorial, you will learn how to use Couchbase Lite in a React Native project.

The sample project is an application that allows users to search and bookmark hotels from a Couchbase Lite database. The application contains 2 screens:

  • Bookmarks Screen: to list the bookmarked hotels. You can unbookmark a previously bookmarked hotel from this screen

  • Search Screen: to search for hotels by providing a location and/or full-text search query. You can bookmark (or unbookmark) a hotel from this screen.

android flow

Pre-requisites

This tutorial requires the following components and versions to run successfully.

  • Android Studio 3.0 or above

  • Android SDK 19 or above

  • Couchbase Lite 2.6.0

The tutorial also assumes that the reader has a basic understanding of developing apps with React Native and Java.

Getting Started

The User Interface has already been implemented in the starter project. You will add the code to persist and query data.

  1. Download the starter project.

  2. Unzip starter-project.zip.

  3. Open the starter-project/HotelFinder/ directory in the JavaScript editor of your choice (for example, Visual Studio Code or WebStorm).

  4. The User Interface code is located in ui/Bookmarks.js and ui/Search.js.

  5. Run the following commands in your Terminal.

    cd HotelFinder
    npm install -g react-native-cli
    npm install
    react-native link

    The react-native link command bundles native dependencies in your Android project. It is required in the react-native-elements installation process.

  6. Start the React Native development server.

    npm run start

    The npm run start command starts a web server that bundles and serves the JavaScript code to the application. You should see the following in the output.

    Metro Bundler ready.
    
    Loading dependency graph...
  7. Open the Android Studio project at HotelFinder/android/build.gradle.

  8. Build and run.

    1. Note Depending on the version of Android Studio, you may be promoted to upgrade the version of gradle from the version specified in gradle-wrapper.properties. Go ahead and do that

    2. Note If you run the app on SDK version 23 or above, you will be prompted to allow the "Display over other apps" permission the first time the app is installed. In some cases, you may see the app crash on initial launch. Relaunch the app if it does not relaunch automatically. That will display the permissions screen. Be sure to allow this permission and run the app again from Android Studio. This permission allows React Native to overlay the status when downloading a new bundle from the dev server.This permission is only required during development so app users wouldn’t perform these steps in a production build of the application.

      overlay permissions
  9. You can click on the Hotels button to run a search query. The result of the query will be empty.

In the next section, you will setup the Native Module interface which is the first step for establishing communication between native code and JavaScript.

Native Modules Setup

With Native Modules, you can write native code and have access to it from JavaScript. It is helpful when an app needs access to native APIs, and React Native doesn’t have a corresponding module yet. In this tutorial, you will use the Native Modules API to implement methods in Java and call them from the JavaScript code. These methods will do the following:

  • Full Text Search for terms in a Couchbase Lite database.

  • Query documents in a Couchbase Lite database.

  • Create and Update documents in a Couchbase Lite database.

Java Setup

  1. Create a new file named HotelFinderNative.java in app/src/main/java/com/hotelfinder/ .This file will contain the native implementation of the methods that will be exported to JS. Insert the following class definition.

    public class HotelFinderNative extends ReactContextBaseJavaModule {
    
        HotelFinderNative(ReactApplicationContext reactContext) {
            super(reactContext);
        }
    
        @Override
        public String getName() {
            return "HotelFinderNative";
        }
    
    }
  2. Create a new file named HotelFinderPackage.java in app/src/main/java/com/hotelfinder/ with the following.

    public class HotelFinderPackage implements ReactPackage {
        @Override
        public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
            return Collections.emptyList();
        }
    
        @Override
        public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
            List<NativeModule> modules = new ArrayList<>();
            modules.add(new HotelFinderNative(reactContext));
            return modules;
        }
    }

    This code subclasses the ReactPackage class and is required by React Native. Packages can contain multiple modules which is useful if you wish to separate the native logic into separate modules. In this example, there is only one module to register, HotelFinderNative.

  3. Next, you must register the package in React Native’s getPackages method. Open MainApplication.java and update the getPackages method to also include HotelFinderPackage in the list that is returned.

    protected List<ReactPackage> getPackages() {
      return Arrays.<ReactPackage>asList(
        new VectorIconsPackage(),
        new HotelFinderPackage() // <-- Add this line with your package name.
      );
    }

You are now ready to implement functionalities in Java. The next step is to import the Couchbase Lite framework in your project.

Couchbase Lite Setup

  1. Add the following in the dependencies section of the application’s build.gradle (the one in the app folder).

    implementation 'com.couchbase.lite:couchbase-lite-android:2.6.0'

Database Setup

In our example, we will start with a pre-built Couchbase Lite database that contains a bunch of hotel documents. We will make our queries against the documents in this database. Note that in a real world application, the data could be synced down from other Couchbase Lite clients or from Sync Gateway in the cloud.

The pre-built database needs to be added to the Android Studio project.

  1. Download travel-sample.cblite2.zip and drag it over to android/app/src/main/assets/.

  2. You will use the singleton pattern to setup the database instance. Create a new file named DatabaseManager.java and insert the following.

    In this code, you first check if a database named "travel-sample" exists. If it doesn’t exist, the bundled database file is copied to the default Couchbase Lite directory. The database is then opened and the instance is set. The createIndex method creates the Full-Text Search index on the description property.

    public class DatabaseManager {
    
        private static String DB_NAME = "travel-sample";
    
        private static Database database;
        private static DatabaseManager instance = null;
    
        private DatabaseManager(Context context) {
            if (!Database.exists("travel-sample", context.getFilesDir())) {
                String assetFile = String.format("%s.cblite2.zip", DB_NAME);
                Utils.installPrebuiltDatabase(context, assetFile);
            }
            DatabaseConfiguration configuration = new DatabaseConfiguration();
            try {
                database = new Database(DB_NAME, configuration);
            } catch (CouchbaseLiteException e) {
                e.printStackTrace();
            }
            this.createIndexes();
        }
    
        private void createIndexes() {
            try {
                FullTextIndexItem item = FullTextIndexItem.property("description");
                FullTextIndex index = IndexBuilder.fullTextIndex(item);
                database.createIndex("descFTSIndex", index);
            } catch (CouchbaseLiteException e) {
                e.printStackTrace();
            }
        }
    
        public static DatabaseManager getSharedInstance(Context context) {
            if (instance == null) {
                CouchbaseLite.init(context);
                instance = new DatabaseManager(context);
            }
            return instance;
        }
    
        public static Database getDatabase() {
            if (instance == null) {
                try {
                    throw new Exception("Must call getSharedInstance first");
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
            return database;
        }
    
    }
  3. Next, add the following properties in HotelFinderNative.java.

    private static String DOC_TYPE = "bookmarkedhotels";
    private Database database;
  4. Finally, update the HotelFinderNative class initializer to include code to initialize the database.

    HotelFinderNative(ReactApplicationContext reactContext) {
      super(reactContext);
      DatabaseManager.getSharedInstance(reactContext);
      this.database = DatabaseManager.getDatabase();
    }

    This code adds the database as an instance property on the HotelFinderNative class.

  5. Build the project. Confirm that it builds successfully.

In the next sections, you will use this instance variable to perform various operations.

Search Hotels

In this section, you will add the functionality to search for hotels.

  1. First, we import the appropriate ReactNative module. For this, add the following to the top of HotelFinder/ui/Search.js.

    import { NativeModules } from 'react-native';
    let HotelFinderNative = NativeModules.HotelFinderNative;

    The HotelFinderNative constant corresponds to the native module that was created in the Java Setup section.

  2. Next, you must implement a method in the HotelFinderNative module before it can be accessed in JavaScript. Implement this method in HotelFinderNative.java.

    This code creates the Full-Text search query using the match() operator.

    In this particular example, the match expression looks for the descriptionText value in the description property. This match expression is logically ANDed with an equalTo comparison expression which looks for the locationText value in the country, city, state or address properties.This expression is then used in the where clause of the query in the usual way.

    The ReactMethod annotation is used to mark methods that are exposed to React Native. Note that on iOS, we use a header file for method signatures. To do the same on Android, you could declare an interface to list all signatures and have the HotelFinderNative class implement methods from the interface. In this tutorial however, you will implement the methods directly in HotelFinderNative.java.

    @ReactMethod
    private void search(String description, String location, Callback errorCallback, Callback successCallback) {
        Expression locationExp = Expression.property("country")
            .like(Expression.string("%" + location + "%"))
            .or(Expression.property("city").like(Expression.string("%" + location + "%")))
            .or(Expression.property("state").like(Expression.string("%" + location + "%")))
            .or(Expression.property("address").like(Expression.string("%" + location + "%")));
    
        Expression queryExpression = null;
        if (description == null) {
            queryExpression = locationExp;
        } else {
            Expression descExp = FullTextExpression.index("descFTSIndex").match(description);
            queryExpression = descExp.and(locationExp);
        }
    
        Query query = QueryBuilder
            .select(
                SelectResult.expression(Meta.id),
                SelectResult.property("name"),
                SelectResult.property("address"),
                SelectResult.property("phone")
            )
            .from(DataSource.database(database))
            .where(
                Expression.property("type").equalTo(Expression.string("hotel"))
                    .and(queryExpression)
            );
    
        ResultSet resultSet = null;
        try {
            resultSet = query.execute();
        } catch (CouchbaseLiteException e) {
            e.printStackTrace();
            errorCallback.invoke();
        }
        WritableArray writableArray = Arguments.createArray();
        assert resultSet != null;
        for (Result result : resultSet) {
            WritableMap writableMap = Arguments.makeNativeMap(result.toMap());
            writableArray.pushMap(writableMap);
        }
        successCallback.invoke(writableArray);
    }
  3. You can call the search swift method from Search.js. For this, add the following text to the onChangeText method in Search.js.

    HotelFinderNative.search(descriptionText, locationText, err => {
      console.log(err);
    }, hotels => {
      this.setState({hotels: hotels});
    });
  4. Build & run. You may see a number of "Cannot find symbol class" error. Be sure to import the relevant packages.

  5. Tap on "Hotels" button to get to the "Search" screen.

  6. In search screen. enter "UK" in the Country input field and press the Lookup button. You should now see a list of hotels in the search result.

    android search

Bookmark Hotel

  1. Bookmarked hotel IDs are persisted in a separate document of type bookmarkedhotels.

    The first time a hotel is bookmarked, the bookmarkedhotels document is created. Subsequently, every time a new hotel is bookmarked, the hotel ID is appended to the hotels array of the existing document. You will add a method to find or create the document of type bookmarkedhotels. Add the following findOrCreateBookmarkDocument method in HotelFinderNative.java.

    private MutableDocument findOrCreateBookmarkDocument() {
        Query query = QueryBuilder
            .select(
                SelectResult.expression(Meta.id)
            )
            .from(DataSource.database(database))
            .where(
                Expression.property("type")
                .equalTo(Expression.string(DOC_TYPE))
            );
    
        try {
            ResultSet resultSet = query.execute();
            List<Result> list = resultSet.allResults();
    
            if (list.isEmpty()) {
                MutableDocument mutableDocument = new MutableDocument()
                    .setString("type", DOC_TYPE)
                    .setArray("hotels", new MutableArray());
                database.save(mutableDocument);
                return mutableDocument.toMutable();
            } else {
                Result result = list.get(0);
                String documentId = result.getString("id");
                return database.getDocument(documentId).toMutable();
            }
        } catch (CouchbaseLiteException e) {
            e.printStackTrace();
            return null;
        }
    }
  2. You will now add the method to update the bookmarkedhotels document every time a hotel is bookmarked.

    Implement the bookmark method in HotelFinderNative.java. Every time a new hotel is bookmarked, the hotel ID is appended to the hotels array and the update is saved to the database.

    @ReactMethod
    private void bookmark(String hotelId, Callback errorCallback, Callback successCallback) {
        MutableDocument mutableDocument = findOrCreateBookmarkDocument();
        assert mutableDocument != null;
        MutableArray mutableArray = mutableDocument.getArray("hotels");
        mutableArray.addString(hotelId);
        mutableDocument.setArray("hotels", mutableArray);
        try {
            database.save(mutableDocument);
            successCallback.invoke(Arguments.fromList(mutableArray.toList()));
        } catch (CouchbaseLiteException e) {
            e.printStackTrace();
            errorCallback.invoke();
        }
    }
  3. You can now call it from Search.js. Add the following to the bookmark method in Search.js.

    HotelFinderNative.bookmark(hotelId, err => {
      console.log(err);
    }, bookmarkIds => {
      this.setState({bookmarkIds: bookmarkIds});
    });
  4. While searching for hotels, the app should also display an icon on hotels that are previously bookmarked . To do so, you will add a new method to query hotel Ids. Implement the corresponding method natively in HotelFinderNative.java.

    @ReactMethod
    private void queryBookmarkIds(Callback errorCallback, Callback successCallback) {
        MutableDocument mutableDocument = findOrCreateBookmarkDocument();
        assert mutableDocument != null;
        MutableArray mutableArray = mutableDocument.getArray("hotels");
        successCallback.invoke(Arguments.fromList(mutableArray.toList()));
    }
  5. . You can now call queryBookmarkIds java method from Search.js. For that, add the following to the componentWillMount method in Search.js

    HotelFinderNative.queryBookmarkIds(err => {
      console.log(err);
    }, hotels => {
      this.setState({bookmarkIds: hotels});
    });
  6. Build & run.

  7. Click Hotels and search for a hotel (type "United States" in the country field for example).

  8. You can now swipe a table view row to bookmark a hotel. The bookmark icon is displayed.

    android swipe

In the next section, you will query the bookmarked hotels to display them on the Bookmarks screen.

List Bookmarks

  1. Insert a new method called queryBookmarkDocuments in HotelFinderNative.java.

    @ReactMethod
    private void queryBookmarkDocuments(Callback errorCallback, Callback successCallback) {
        DataSource bookmarkDS = DataSource.database(database).as("bookmarkDS");
        DataSource hotelsDS = DataSource.database(database).as("hotelsDS");
    
        Expression hotelsExpr = Expression.property("hotels").from("bookmarkDS");
        Expression hotelIdExpr = Meta.id.from("hotelsDS");
    
        Expression joinExpr = ArrayFunction.contains(hotelsExpr, hotelIdExpr);
        Join join = Join.join(hotelsDS).on(joinExpr);
    
        Expression typeExpr = Expression.property("type").from("bookmarkDS");
    
        SelectResult bookmarkAllColumns = SelectResult.all().from("bookmarkDS");
        SelectResult hotelsAllColumns = SelectResult.all().from("hotelsDS");
    
        Query query = QueryBuilder
            .select(bookmarkAllColumns, hotelsAllColumns)
            .from(bookmarkDS)
            .join(join)
            .where(typeExpr.equalTo(Expression.string(DOC_TYPE)));
    
        ResultSet resultSet = null;
        try {
            resultSet = query.execute();
        } catch (CouchbaseLiteException e) {
            e.printStackTrace();
            errorCallback.invoke(e.toString());
        }
    
        WritableArray writableArray = Arguments.createArray();
        assert resultSet != null;
        for (Result result : resultSet) {
            Dictionary dictionary = result.getDictionary("hotelsDS");
            WritableMap writableMap = Arguments.makeNativeMap(dictionary.toMap());
            writableArray.pushMap(writableMap);
        }
    
        successCallback.invoke(writableArray);
    }

    To query bookmark documents, you will write a JOIN query between the document of type bookmarkedhotels which contains hotel Ids and documents of type hotels which contain all the other fields (name, address, phone etc.)

  2. On the JavaScript side, you must first import the HotelFinderNative ReactNative module. Add the following to the top of HotelFinder/ui/Bookmarks.js.

    import {NativeModules} from 'react-native';
    let HotelFinderNative = NativeModules.HotelFinderNative;
  3. You can now call the queryBookmarkDocuments native method from Bookmarks.js. Add the following text to the queryBookmarkDocuments method in Bookmarks.js.

    HotelFinderNative.queryBookmarkDocuments(err => {
      console.log(err);
    }, bookmarks => {
      this.setState({bookmarkDocuments: bookmarks});
    });
  4. Build and run.

  5. You should now see the hotel that was bookmarked in the Bookmark Hotel section listed in the bookmarks screen

    android home screen

By now, the pattern should seem very familiar and essentially consists of the following steps:

  • Implement the method natively in HotelFinderNative.java. This layer will interact with the native Android Java implementation of Couchbase Lite for data persistence functions.

  • Invoke the exported method from JavaScript (you will have to import the React Native module the very first time).

Conclusion

Well done! You have learned how to import Couchbase Lite in a React Native project, and how to add search and persistence functionalities to your application!

As an exercise, you can follow the same procedure to implement the functionality to:

  • Unbookmark a hotel on the Bookmarks screen.

  • Unbookmark a hotel on the Search screen.

You can find a working copy of the completed project in the final project zip file. To build and run the final project, follow the instructions outlined in the Getting Started section. The final project also implements the missing functionalities mentioned above.