React Nativeアプリケーション開発にNoSQL組み込みデータベースCouchbase Liteを使ってみる

## はじめに

先日、下記の記事を発表しました。

https://qiita.com/yoshiyuki_kono/items/bffda8fba33839a3e9aa#couchbase-lite

そこで、Couchbase Liteについて、下記のように書きました。

>...チュートリアルが公開されています。提供されているReact NativeのUI実装（スタータープロジェクト）に対して、Couchbase Liteを利用するためのコードを追加していく形になっています。2018年時点のものにて、提供されているReact Nativeのソースコードは、すでに時代遅れなものとなっていますが、提供されているUI実装は無視しても（React Nativeを昔から使っている人は、提供物をベースに最新化してもよいかもしれませんが ）、チュートリアルの内容を参考にして実装を行うことができるでしょう（将来別の記事で発表したいと思っています）。

本稿では、「Couchbase Liteを利用するためのコードを追加していく」部分について、見ていきたいと思います。

ここでは、提供されているスタータープロジェクトおよび実装済みのUIは重視せず、React Nativeアプリケーション開発に精通している読者に対して、Couchbase Lite独自の部分を解説していきます。

## チュートリアルについて

Android用とiOS用の2つのチュートリアルがあります。各チュートリアルでは、React Nativeアプリ内からCouchbase Lite2.xとインターフェイスするReact Nativeモジュールを構築する方法の手順を説明しています。

## アプリケーションについて

「Hotel-Finder」と呼ばれるアプリを構築します。このアプリは、ユーザーが以下の操作をすることを可能にします

- ホテルの情報を検索する（Couchbase Liteデータベースに予め格納されているホテル情報を使用）
- 検索されたホテルをブックマークする（ブックマーク情報を、Couchbase Liteデータベースに保存）


アプリケーションには2つの画面が含まれています。


- ホテル検索画面
- ブックマーク済みホテル一覧表示画面

## Androidアプリケーション

### 前提条件

- Android Studio 3.0以降

- Android SDK 19以降

- Couchbase Lite 2.6.0



### ネイティブモジュールセットアップ

ネイティブコードとJavaScript間の通信を確立するためのネイティブモジュールインターフェイスを構築します。

Native Modules APIを使用してJavaでメソッドを実装し、JavaScriptコードからメソッドを呼び出します。


### Javaセットアップ

`ReactContextBaseJavaModule`を継承して、JavaScriptに対してエクスポートされるメソッドのネイティブ実装が含まれるクラス（ここでは、`HotelFinderNative`）を実装します。

```JAVA:HotelFinderNative.java
public class HotelFinderNative extends ReactContextBaseJavaModule {

    HotelFinderNative(ReactApplicationContext reactContext) {
        super(reactContext);
    }

    @Override
    public String getName() {
        return "HotelFinderNative";
    }

}
```

`ReactPackage`を継承したクラス（ここでは、`HotelFinderPackage`）を実装します。

```JAVA:HotelFinderPackage.java
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;
    }
}
```

パッケージには複数のモジュールを含めることができます。これは、ネイティブロジックを個別のモジュールに分割する場合に活用できます（この例では、登録するモジュールは`HotelFinderNative`1つだけです）。

次に、`MainApplication`の`getPackages`メソッドにパッケージを登録します。`getPackages`メソッドが返すリストに追加するパッケージ(`HotelFinderPackage`)を加えます。以下は実装のイメージです（`ReactPackages`リストに`HotelFinderPackage`を加える方法は、下記と異なっていても構いません）。

```JAVA:MainApplication.java
protected List<ReactPackage> getPackages() {
  return Arrays.<ReactPackage>asList(
    ...,
    new HotelFinderPackage()
  );
}
```

これで、Javaで機能を実装する準備が整いました。次のステップは、Couchbase Liteをプロジェクトにインポートすることです。

### Couchbase Liteセットアップ
アプリケーションのbuild.gradle（appフォルダーにあるもの）の`dependencies`セクションに以下を追加します。

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

### データベースのセットアップ


ここでは、データベース操作を担当する`DatabaseManager`クラスを定義します。

この例では、最初に、アプリケーションが参照する「travel-sample」という名前のデータベースが存在するかどうかを確認します。存在しない場合、バンドルされたデータベースファイルを、デフォルトのCouchbase Liteディレクトリにコピーします。

次にデータベースをオープンします。

ここでは、データベース初期化に合わせ、全文検索インデックスを作成しています。

```JAVA:DatabaseManager.java
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;
    }

}
```

`HotelFinderNative`クラスに`Database`インスタンスを追加します。

```JAVA:HotelFinderNative.java
private static String DOC_TYPE = "bookmarkedhotels";
private Database database;
```

`HotelFinderNative`クラスのコンストラクターで、データベースを初期化します。

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


### データベース利用（ホテル検索）機能追加

このセクションでは、データベースを利用（ここでは、ホテル情報を検索）する機能を追加します。

#### インポート
まず、必要なReact Nativeモジュールをインポートします。Search.jsの先頭に以下を追加します。

```JAVASCRIPT:Search.js
import { NativeModules } from 'react-native';
let HotelFinderNative = NativeModules.HotelFinderNative;
```

`HotelFinderNative`定数は、Javaのセットアップセクションで作成されたネイティブモジュールに対応します。

#### ネイティブ処理実装

JavaScriptでアクセスする前に、ネイティブモジュールにメソッドを実装する必要があります。このメソッドをHotelFinderNative.javaに実装します。

iOSでは、メソッドシグネチャーにヘッダーファイルを使用しますが、Androidで同じことを行うには、公開されるシグネチャーを備えたインターフェースを宣言し、そのインターフェースのメソッドを実装するクラスを用いることができます。ただし、このチュートリアルでは、`HotelFinderNative`に直接メソッドを実装しています。

`ReactMethod`アノテーションは、公開されるメソッドをマークするために使用されます。

```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);
}
```

これでJavaScriptからメソッドを呼び出すことができます。このために、Search.jsの`onChangeText`メソッドに次のコードを追加します。

```JAVASCRIPT:Search.js
HotelFinderNative.search(descriptionText, locationText, err => {
  console.log(err);
}, hotels => {
  this.setState({hotels: hotels});
});
```

### 実装パターン

チュートリアルでは、検索以外の処理の実装についても解説されています。ネイティブモジュールを実装するパターンは同一となるためここでは解説を割愛します。

パターンは、次の手順で構成されます。

- HotelFinderNative.javaでネイティブにメソッドを実装します。このレイヤーは、データ永続化機能のためにCouchbase LiteのネイティブAndroid Java実装と相互作用します。

- JavaScriptからエクスポートされたメソッドを呼び出します（最初にReact Nativeモジュールをインポートする必要があります）。

## iOSアプリケーション

### Swift/Objective-Cセットアップ

HotelFinder-RCTBridge.mという名前の新しいファイルを作成します。このファイルは、JavaScriptレイヤーにエクスポートするメソッドを定義します。

HotelFinder-RCTBridge.mに以下を挿入します。

```obj-c:HotelFinder-RCTBridge.m
#import <Foundation/Foundation.h>
#import "React/RCTBridgeModule.h"

@interface RCT_EXTERN_MODULE(HotelFinderNative, NSObject)

/* code will be added here later. */

+ (BOOL)requiresMainQueueSetup
{
  return YES;
}
@end
```

HotelFinderNative.swiftという名前の新しいファイルを作成します。このファイルには、JavaScriptレイヤーにエクスポートされるAPIのネイティブ実装が含まれます。

HotelFinderNative.swiftに以下を挿入します。

```swift:HotelFinderNative.swift
import Foundation

@objc (HotelFinderNative)
class HotelFinderNative: NSObject {

	/* code will be added here later. */

}
```

これで、Swiftで機能を実装する準備が整いました。次のステップは、CouchbaseLiteフレームワークをプロジェクトにインポートすることです。

### Couchbase Liteセットアップ

下記記事を参照ください。

https://qiita.com/yoshiyuki_kono/items/9d9988826014bfc331d3


### インポート

HotelFinderNative.swiftにSwiftフレームワークをインポートします。

```swift:HotelFinderNative.swift
import CouchbaseLiteSwift
```

### データベースのセットアップ

この例では、ホテルのドキュメントを含む、事前に構築されたCouchbase Liteデータベースを用います。このデータベース内のドキュメントに対してクエリを実行します。
事前に構築されたデータベースをXcodeプロジェクトに追加する必要があります。

travel-sample.cblite2.zipをダウンロードし、Xcodeプロジェクトナビゲーターにドラッグします。[必要に応じてアイテムをコピーする]チェックボックスを必ず選択してください。

データベースインスタンスをセットアップします。DatabaseManager.swiftという名前の新しいファイルを作成し、以下を挿入します。

このコードでは、最初に「travel-sample」という名前のデータベースが存在するかどうかを確認します。存在しない場合は、バンドルされたデータベースファイルがデフォルトのCouchbase Liteディレクトリにコピーされます。次にデータベースが開かれ、インスタンスが設定されます。このcreateIndexメソッドは、descriptionプロパティに全文検索インデックスを作成します。

```swift:DatabaseManager.swift
import CouchbaseLiteSwift

class DatabaseManager {

  private static var privateSharedInstance: DatabaseManager?
  
  var database: Database
  
  let DB_NAME = "travel-sample"
  
  class func sharedInstance() -> DatabaseManager   {
    guard let privateInstance = DatabaseManager.privateSharedInstance else {
      DatabaseManager.privateSharedInstance = DatabaseManager()
      return DatabaseManager.privateSharedInstance!
    }
    return privateInstance
  }
  
  private init() {
    let path = Bundle.main.path(forResource: self.DB_NAME, ofType: "cblite2")!
    if !Database.exists(withName: self.DB_NAME) {
      do {
        try Database.copy(fromPath: path, toDatabase: self.DB_NAME, withConfig: nil)
      } catch {
        fatalError("Could not copy database")
      }
    }
    do {
      self.database = try Database(name: "travel-sample")
      self.createIndex(database)
    } catch {
      fatalError("Could not copy database")
    }
  }
  
  func createIndex(_ database: Database) {
    do {
      try database.createIndex(IndexBuilder.fullTextIndex(items: FullTextIndexItem.property("description")).ignoreAccents(false), withName: "descFTSIndex")
    } catch {
      print(error)
    }
  }
  
}
```

次に、HotelFinderNative.swiftに次のプロパティを追加します。

```swift:HotelFinderNative.swift
let database = DatabaseManager.sharedInstance().database
let DOC_TYPE = "bookmarkedhotels"
```

このコードは、データベースを`HotelFinderNative`クラスのプロパティとして追加します。


### データベース利用（ホテル検索）機能追加
このセクションでは、ホテルを検索する機能を追加します。

まず、適切なReactNativeモジュールをインポートします。Search.jsの上部に以下を追加します。

```JAVASCRIPT:Search.js
import { NativeModules } from 'react-native';
let HotelFinderNative = NativeModules.HotelFinderNative;
```


JavaScriptでアクセスする前に、モジュールにメソッドを実装する必要があります。HotelFinder-RCTBridge.mに新しいメソッドシグネチャを挿入します。

```obj-c:HotelFinder-RCTBridge.m
RCT_EXTERN_METHOD(search :(NSString *)description :(NSString *)location :(RCTResponseSenderBlock)errorCallback :(RCTResponseSenderBlock)successCallback)
```

`RCT_EXTERN_METHOD()`は、 このメソッドをJavaScriptにエクスポートする必要があることを指定するReactNativeマクロです。

以下のメソッドをHotelFinderNative.swiftに実装します。


```swift:HotelFinderNative.swift
@objc func search(_ description: String?, _ location: String = "", _ errorCallback: @escaping () -> Void, _ successCallback: @escaping ([[[AnyHashable : Any]]]) -> Void) {
  
  let locationExpression = 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)")))
  
  var searchExpression: ExpressionProtocol = locationExpression
  if let text = description {
    let descriptionFTSExpression = FullTextExpression.index("descFTSIndex").match(text)
    searchExpression = descriptionFTSExpression.and(locationExpression)
  }
  
  let query = QueryBuilder
    .select(
      SelectResult.expression(Meta.id),
      SelectResult.expression(Expression.property("name")),
      SelectResult.expression(Expression.property("address")),
      SelectResult.expression(Expression.property("phone"))
    )
    .from(DataSource.database(self.database))
    .where(
      Expression.property("type").equalTo(Expression.string("hotel"))
        .and(searchExpression)
  )
  
  do {
    let resultSet = try query.execute()
    var array: [[AnyHashable : Any]] = []
    for result in resultSet {
      let map = result.toDictionary()
      array.append(map)
    }
    successCallback([array])
  } catch {
    print(error)
    errorCallback();
  }
}
```

これで、Search.jsからswiftメソッドを呼び出すことができます。Search.jsに次のコードを追加します。

```JAVASCRIPT:Search.js
HotelFinderNative.search(descriptionText, locationText, err => {
  console.log(err);
}, hotels => {
  this.setState({hotels: hotels});
});
```

### 実装パターン

チュートリアルでは、検索以外の処理の実装についても解説されています。ネイティブモジュールを実装するパターンは同一となるためここでは解説を割愛します。

パターンは、次の手順で構成されます。

- HotelFinder-RCTBridge.mでエクスポートするメソッドを宣言します
- HotelFinderNative.swiftでネイティブにメソッドを実装します。このレイヤーは、データ永続化機能のためにCouchbaseLiteのネイティブiOS実装と相互作用します。
- JavaScriptからエクスポートされたメソッドを呼び出します（最初にReact Nativeモジュールをインポートする必要があります）。



## 関連情報

https://blog.couchbase.com/couchbase-lite-react-native/

https://docs.couchbase.com/tutorials/hotel-finder/ios.html

https://docs.couchbase.com/tutorials/hotel-finder/android.html