3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

React NativeAdvent Calendar 2021

Day 23

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

Last updated at Posted at 2021-12-22

はじめに

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

そこで、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)を実装します。

HotelFinderNative.java
public class HotelFinderNative extends ReactContextBaseJavaModule {

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

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

}

ReactPackageを継承したクラス(ここでは、HotelFinderPackage)を実装します。

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

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

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

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

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

Couchbase Liteセットアップ

アプリケーションのbuild.gradle(appフォルダーにあるもの)のdependenciesセクションに以下を追加します。

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

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

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

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

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

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

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インスタンスを追加します。

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

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

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

データベース利用(ホテル検索)機能追加

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

インポート

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

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

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

ネイティブ処理実装

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

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

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

@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メソッドに次のコードを追加します。

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に以下を挿入します。

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に以下を挿入します。

HotelFinderNative.swift
import Foundation

@objc (HotelFinderNative)
class HotelFinderNative: NSObject {

	/* code will be added here later. */

}

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

Couchbase Liteセットアップ

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

インポート

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

HotelFinderNative.swift
import CouchbaseLiteSwift

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

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

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

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

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

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に次のプロパティを追加します。

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

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

データベース利用(ホテル検索)機能追加

このセクションでは、ホテルを検索する機能を追加します。

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

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

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

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

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

以下のメソッドをHotelFinderNative.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に次のコードを追加します。

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モジュールをインポートする必要があります)。

関連情報

3
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?