はじめに
先日、下記の記事を発表しました。
そこで、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
)を実装します。
public class HotelFinderNative extends ReactContextBaseJavaModule {
HotelFinderNative(ReactApplicationContext reactContext) {
super(reactContext);
}
@Override
public String getName() {
return "HotelFinderNative";
}
}
ReactPackage
を継承したクラス(ここでは、HotelFinderPackage
)を実装します。
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
を加える方法は、下記と異なっていても構いません)。
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ディレクトリにコピーします。
次にデータベースをオープンします。
ここでは、データベース初期化に合わせ、全文検索インデックスを作成しています。
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
インスタンスを追加します。
private static String DOC_TYPE = "bookmarkedhotels";
private Database database;
HotelFinderNative
クラスのコンストラクターで、データベースを初期化します。
HotelFinderNative(ReactApplicationContext reactContext) {
super(reactContext);
DatabaseManager.getSharedInstance(reactContext);
this.database = DatabaseManager.getDatabase();
}
データベース利用(ホテル検索)機能追加
このセクションでは、データベースを利用(ここでは、ホテル情報を検索)する機能を追加します。
インポート
まず、必要なReact Nativeモジュールをインポートします。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
メソッドに次のコードを追加します。
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に以下を挿入します。
#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に以下を挿入します。
import Foundation
@objc (HotelFinderNative)
class HotelFinderNative: NSObject {
/* code will be added here later. */
}
これで、Swiftで機能を実装する準備が整いました。次のステップは、CouchbaseLiteフレームワークをプロジェクトにインポートすることです。
Couchbase Liteセットアップ
下記記事を参照ください。
インポート
HotelFinderNative.swiftにSwiftフレームワークをインポートします。
import CouchbaseLiteSwift
データベースのセットアップ
この例では、ホテルのドキュメントを含む、事前に構築されたCouchbase Liteデータベースを用います。このデータベース内のドキュメントに対してクエリを実行します。
事前に構築されたデータベースをXcodeプロジェクトに追加する必要があります。
travel-sample.cblite2.zipをダウンロードし、Xcodeプロジェクトナビゲーターにドラッグします。[必要に応じてアイテムをコピーする]チェックボックスを必ず選択してください。
データベースインスタンスをセットアップします。DatabaseManager.swiftという名前の新しいファイルを作成し、以下を挿入します。
このコードでは、最初に「travel-sample」という名前のデータベースが存在するかどうかを確認します。存在しない場合は、バンドルされたデータベースファイルがデフォルトのCouchbase Liteディレクトリにコピーされます。次にデータベースが開かれ、インスタンスが設定されます。このcreateIndexメソッドは、descriptionプロパティに全文検索インデックスを作成します。
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に次のプロパティを追加します。
let database = DatabaseManager.sharedInstance().database
let DOC_TYPE = "bookmarkedhotels"
このコードは、データベースをHotelFinderNative
クラスのプロパティとして追加します。
データベース利用(ホテル検索)機能追加
このセクションでは、ホテルを検索する機能を追加します。
まず、適切なReactNativeモジュールをインポートします。Search.jsの上部に以下を追加します。
import { NativeModules } from 'react-native';
let HotelFinderNative = NativeModules.HotelFinderNative;
JavaScriptでアクセスする前に、モジュールにメソッドを実装する必要があります。HotelFinder-RCTBridge.mに新しいメソッドシグネチャを挿入します。
RCT_EXTERN_METHOD(search :(NSString *)description :(NSString *)location :(RCTResponseSenderBlock)errorCallback :(RCTResponseSenderBlock)successCallback)
RCT_EXTERN_METHOD()
は、 このメソッドをJavaScriptにエクスポートする必要があることを指定するReactNativeマクロです。
以下のメソッドを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に次のコードを追加します。
HotelFinderNative.search(descriptionText, locationText, err => {
console.log(err);
}, hotels => {
this.setState({hotels: hotels});
});
実装パターン
チュートリアルでは、検索以外の処理の実装についても解説されています。ネイティブモジュールを実装するパターンは同一となるためここでは解説を割愛します。
パターンは、次の手順で構成されます。
- HotelFinder-RCTBridge.mでエクスポートするメソッドを宣言します
- HotelFinderNative.swiftでネイティブにメソッドを実装します。このレイヤーは、データ永続化機能のためにCouchbaseLiteのネイティブiOS実装と相互作用します。
- JavaScriptからエクスポートされたメソッドを呼び出します(最初にReact Nativeモジュールをインポートする必要があります)。
関連情報