forcedroid ネイティブアプリケーションの変更
学習の目的
この単元を完了すると、次のことができるようになります。
- Android ネイティブテンプレートアプリケーションを変更して、リスト画面をカスタマイズする。
- REST 応答を処理する。
- REST 要求を使用して Salesforce レコードを削除する。
リスト画面のカスタマイズ
すでに確認したように、Android ネイティブテンプレートアプリケーションには、ユーザーの Salesforce 組織から取引先責任者または取引先のリストが表示されます。現在は、Mobile SDK REST クラスにより処理される単純な SOQL SELECT クエリから作成された、参照のみのリストです。
長押し操作に削除アクションを添付して、編集機能を強化しましょう。ユーザーがリストビューで取引先責任者名をタップしたまま押さえると、新しい削除アクションが、関連付けられた Salesforce レコードの削除を試みます。試行が成功すると、アプリケーションは取引先責任者リストビューから該当する行を完全に削除します。要求が失敗すると、アプリケーションはユーザーに失敗の理由を表示し、リストビューの行を復元します。
ロングクリックリスナーについて
ロングクリックリスナーは、簡単に実装できます。ただし、ロングクリックリスナークラスを作成する方法を決めるときは少し注意が必要です。さまざまなコーディングオプションを検討すると、まず、リストビューレベルではクリックをリスンしないことが明らかになります。代わりに、リスト項目レベルでリスンします。
Android OnItemLongClickListener インターフェースのおかげで、リスト項目のリスナーを実装するのは面倒な作業ではありません。このインターフェースでは、リストビューに添付されてリストの項目の長押しに反応する 1 つのリスナーを定義します。このクラスのインスタンスを作成するには、ビュークラス内に公開インターフェースを実装します。
次の課題は、ロングクリックリスナーを実装するビュークラスを決めることです。ListView オブジェクトを使用して、リストに表示される情報を提供するデータオブジェクトを指定します。Mobile SDK テンプレートアプリケーションは、この目的のために ArrayAdapter<String> オブジェクトを作成し、ListView に添付します。
listAdapter = ArrayAdapter(this, android.R.layout.simple_list_item_1, ArrayList()) findViewById<ListView>(R.id.contacts_list).adapter = listAdapter
ただし、ArrayAdapter は、データオブジェクトであってビューではありません。それ以上の機能があります。取引先責任者リストビューの ArrayAdapter は、リストのデータセットの項目ごとに AdapterView オブジェクトを作成します。これらのアダプタービューは、対象オブジェクトを表すため、OnItemLongClickListener の実装には AdapterView クラスを使用します。
次に、リスナーオブジェクトを、子に代わってすべての通知を受信する ListView オブジェクトに関連付けます。この関連付けにより、OnItemLongClickListener が操作するのはテンプレートアプリケーションのリストビュー内の項目のみに制限されます。さらに、削除動作を単独のインターフェースメソッドに実装します。
最後に細かい点ですが、このロングクリックリスナーのコードはどこに配置するのでしょうか。Mobile SDK には、次のエントリポイントコールバックメソッドがあります。
public abstract void onResume(RestClient client); @Override protected void onCreate(Bundle savedInstanceState); @Override public void onResume();
onCreate(Bundle savedInstanceState) は除外できます。このメソッドは、ビューがインスタンス化される前にアクティビティを設定して、認証フローを処理します。ビューが登場するのは、onResume() メソッドです。したがって、このメソッドが最も可能性の高い候補です。onResume(RestClient client) メソッドは、ログイン時に認証された RestClient オブジェクトを取得するためにスーパークラスによってコールされます。これはそのままにしておきましょう。これで結果が出ました。onResume() にロングクリックリスナーコードを配置しましょう。
基本的なロングクリックリスナーの実装
では、コーディングを開始しましょう。Android Studio で、MainActivity クラスを開き、onResume() メソッドを見てみましょう。
- Android Studio で、MainActivity.kt ファイルを開きます。
- onResume() [method (メソッド)] を見つけます。
override fun onResume() {
// Hide everything until we are logged in
findViewById<ViewGroup>(root).visibility = View.INVISIBLE
// Create list adapter
listAdapter = ArrayAdapter(this, android.R.layout.simple_list_item_1, ArrayList())
findViewById<ListView>(R.id.contacts_list).adapter = listAdapter
// ADD CODE HERE!
super.onResume()
}マークに従ってコーディングを開始します。ListView 用の listAdapter がセットされた後で、super.onResume() コールの前です。
- 取引先責任者リストビューを参照する便利な ListView 変数を宣言して割り当てます。Activity.findViewById() メソッドを使用してリストビューリソースを検索します。
findViewById<ListView>(R.id.contacts_list).adapter = listAdapter
val listView = findViewById<ListView>(R.id.contacts_list) super.onResume()
- listView 変数を使用して、AdapterView.setOnItemLongClickListener() Lambda 関数をコールしてロングクリックイベントのリスナーを設定します。リスナーのパラメーターには、AdapterView.OnItemLongClickListener インターフェースのインラインスタブをインスタンス化します。
val listView = findViewById<ListView>(R.id.contacts_list)
listView.setOnItemLongClickListener { parent, view, position, id -> false }- (省略可能) インポートの欠落に関するエラーが発生する場合は、クラスインポートに import android.widget.AdapterView を追加します。
- AdapterView.OnItemLongClickListener の本文内で、定型の return 文を、確認用のトーストメッセージを表示するコードに置き換えます。
val listView = findViewById<ListView>(R.id.contacts_list)
listView.setOnItemLongClickListener { parent, view, position, id ->
Toast.makeText(
applicationContext,
"Long press received",
Toast.LENGTH_SHORT,
).show()
true
}- アプリケーションをビルドし、実行します。
- アプリケーションにログインしたら、[Fetch Contacts (取引先責任者を取得)] をクリックします。
- 取引先責任者リスト内のエントリをタップし、数秒間押したままにします。すべてが正しく機能すると、トーストメッセージが表示されます。
Mobile SDK REST 要求を追加する
Mobile SDK 要素を追加する準備がほぼ整いました。onItemLongClick() メソッドで、タップされた行に関連付けられている Salesforce レコードを削除する REST 要求を作成します。次に、その要求を Salesforce に送信します。コードを詳しく見る前に、次の重要事項を確認します。
RestClient インスタンスを取得する
RestClient オブジェクトは直接作成できません。Mobile SDK によって作成され、onResume(RestClient client) メソッドを介して MainActivity に返されます。この RestClient インスタンスは、現在のユーザーのアクセストークンを使用して認証されています。onResume(RestClient client) メソッドはこのインスタンスを client クラス変数に割り当てて、使用できるようにします。
REST 要求を作成する
この練習問題の REST 要求を作成するには、レコードを削除する RestRequest ファクトリメソッドをコールします。
public static RestRequest getRequestForDelete(String apiVersion, String objectType, String objectId);
引数の値はどこで取得するのでしょうか? 次の表を参照してください。
パラメーター | 値 |
|---|---|
apiVersion | ApiVersionStrings.getVersionNumber(Context context) |
objectType |
|
objectId | ? ? |
objectId パラメーターは少々複雑です。そのままの forcedroid アプリケーションが認識しない Salesforce 値を必要とします。なぜ所有していないのでしょうか? そしてどうすれば取得できるのでしょうか? 答えは簡単です。
- ID を所有していないのは、元の REST 要求 (リストを入力する要求) がそれを求めないためです。
- REST 要求を変更することによって ID を取得できます。
テンプレートアプリケーションの SOQL 要求を調整する
MainActivity クラスは 2 つの REST 要求を発行します。1 つは取引先責任者用で、もう 1 つは取引先用です。各要求には SOQL ステートメントが含まれています。取引先は使用しないため、取引先責任者レコードリクエストが ID 値を返すように変更しましょう。
- Android Studio で、MainActivity.kt ファイルを開きます。
- onFetchContactsClick() メソッドを見つけます。
fun onFetchContactsClick(v: View) {
sendRequest("SELECT Name FROM Contact")
}- SOQL クエリを変更して、Name 項目と Id 項目を選択するようにします。ID 項目名のキャメルケースのスペルに注意してください。項目名では大文字と小文字が区別されます。
fun onFetchContactsClick(v: View) {
sendRequest("SELECT Name, Id FROM Contact")
}これで、REST 応答の ID 値を受け取れるようになりましたが、ID 値をどこに保存するのがよいでしょうか。テンプレートアプリケーションは、各レコードの名前値をリストビューの行にコピーするだけで、ID 値をキャッシュしません。長押しハンドラーで参照できるようにするには、クラスの範囲内に ID を保存する必要があります。
テンプレートアプリケーションの sendRequest() メソッドを適合させる
sendRequest(String soql) メソッドまでスクロールダウンします。ここに取得応答が返されます。このメソッドを見ると、Mobile SDK REST メカニズムの動作がよくわかります。さっそく見てみましょう。初めに、メソッドは、指定された SOQL クエリの REST 要求を定義する RestRequest ファクトリメソッドをコールします。
val restRequest = RestRequest.getRequestForQuery(ApiVersionStrings.getVersionNumber(this), soql)
次に、client.sendAsync() コール内で、この新しい RestRequest オブジェクトを Salesforce に送信します。
client!!.sendAsync(restRequest, object : AsyncRequestCallback {
override fun onSuccess(request: RestRequest, result: RestResponse) {
result.consumeQuietly() // consume before going back to main thread
runOnUiThread {
try {
listAdapter!!.clear()
val records = result.asJSONObject().getJSONArray("records")
for (i in 0..<records.length()) {
listAdapter!!.add(records.getJSONObject(i).getString("Name"))
}
} catch (e: Exception) {
onError(e)
}
}
}
override fun onError(exception: Exception) {
runOnUiThread {
Toast.makeText(this@MainActivity,
this@MainActivity.getString(R.string.sf__generic_error),
Toast.LENGTH_LONG).show()
}
}
})sendAsync() コールには、RestRequest オブジェクトの他に、Mobile SDK AsyncRequestCallback インターフェースの実装も必要です。このインターフェースの onSuccess() メソッドは、コールバックを介して REST 応答を非同期に受信します。デフォルトの AsyncRequestCallback 実装は、forcedroid アプリケーションで定義される SOQL クエリのみを処理します。
この onSuccess() メソッドがすでに目的の機能を果たしています。このメソッドには、REST 応答から返されたレコードを抽出し、ローカル records 変数に割り当てるコードが含まれています。この変数をメソッド本文の外で再宣言することによって、この変数をクラス範囲に移動しましょう。
- MainActivity クラス宣言の上部付近で、既存のクラス変数宣言を使用して、JSONArray records を非公開変数として宣言します。
class MainActivity : SalesforceActivity() {
private var client: RestClient? = null
private var listAdapter: ArrayAdapter<String>? = null
private lateinit var records: JSONArray
…- onSuccess() メソッドで、records 変数の型宣言を削除します:
override fun onSuccess(request: RestRequest, result: RestResponse) {
result.consumeQuietly() // consume before going back to main thread
runOnUiThread {
try {
listAdapter!!.clear()
records = result.asJSONObject().getJSONArray("records")
for (i in 0..<records.length()) {
listAdapter!!.add(records.getJSONObject(i).getString("Name"))
}
} catch (e: Exception) {
onError(e)
}
}
}onItemLongClick() メソッドを終了する
onItemLongClick() メソッドを終了する準備ができました。基本アルゴリズムは次のとおりです。
- 適切な Mobile SDK RestRequest ファクトリメソッドをコールして、「削除要求」オブジェクトを取得します。すべての RestRequest メソッドが例外を発生させるため、コールを try...catch ブロックで囲む必要があります。
- 生成された RestClient オブジェクトを使用して「削除要求」オブジェクトを Salesforce に送信します。
- コールバックメソッドで REST の結果を処理します。
RestRequest オブジェクトを取得する
- onResume() メソッド内の setOnItemLongClickListener メソッドまでスクロールして戻ります。
- Toast.makeText() コールの後で、restRequest というローカル RestRequest オブジェクトを宣言します。
var restRequest: RestRequest
- 空の try...catch 構造を追加します。
try {
} catch (e: Exception) {
}- try ブロックで、削除操作のための REST 要求オブジェクトを取得するファクトリメソッドをコールします。ヒント: 静的 RestRequest.getRequestForDelete() メソッドを使用します。
try {
restRequest = RestRequest.getRequestForDelete(
// arguments?
)
} catch (e: Exception) {
}- 最初のパラメーターに、プロジェクトの Mobile SDK リソースで指定されている Salesforce API バージョンを取得します。
try {
restRequest = RestRequest.getRequestForDelete(
/* apiVersion = */ ApiVersionStrings.getVersionNumber(this),
// ..
)
} catch (e: Exception) {
}- objectType パラメーターに
“Contact”を指定します。
try {
restRequest = RestRequest.getRequestForDelete(
/* apiVersion = */ ApiVersionStrings.getVersionNumber(this),
/* objectType = */ "Contact",
// ..
)
} catch (e: Exception) {
}- RestRequest.getRequestForDelete() に、レコード配列内で現在のリストビュー位置に一致するエントリの ID を渡します。以下に、ID を取得する方法を示します。
try {
restRequest = RestRequest.getRequestForDelete(
/* apiVersion = */ ApiVersionStrings.getVersionNumber(this),
/* objectType = */ "Contact",
/* objectId = */ records.getJSONObject(position).getString("Id"),
)
} catch (e: Exception) {
}- catch ブロックで、Exception 引数に対して printStackTrace() をコールします。
try {
restRequest = RestRequest.getRequestForDelete(
/* apiVersion = */ ApiVersionStrings.getVersionNumber(this),
/* objectType = */ "Contact",
/* objectId = */ records.getJSONObject(position).getString("Id"),
)
// Send the request
// ...
} catch (e: Exception) {
e.printStackTrace()
}削除要求オブジェクトを取得したら、それを Salesforce に送信し、コールバックメソッドで結果を処理します。
RestClient.sendAsync() メソッドを追加する
もう少しで作業は完了です。パズルの最後のピースは、RestClient.sendAsync() メソッドを使用して要求を送信することです。このメソッドを使用するには、AsyncRequestCallback インターフェースを実装する必要があります。ご存知のとおり、Mobile SDK は REST 応答を AsyncRequestCallback メソッドに送信します。
- onItemLongClick() で、getRequestForDelete() コールの後に、sendRequest() メソッドから RestClient.sendAsync() コードをコピーして貼り付けます。
- onSuccess() メソッドの try ブランチの内部コードを削除します。catch ブランチは、エラーハンドラーへのコールアウトを行うだけなので、削除しません。
- onError() 上書き実装は汎用で、どの Salesforce 応答でも動作するため、そのままにします。
不要な部分を削除した RestClient.sendAsync() へのコールは次のようになります。
restRequest = RestRequest.getRequestForDelete(
/* apiVersion = */ ApiVersionStrings.getVersionNumber(this),
/* objectType = */ "Contact",
/* objectId = */ records.getJSONObject(position).getString("Id"),
)
client!!.sendAsync(restRequest, object : AsyncRequestCallback {
override fun onSuccess(request: RestRequest?, result: RestResponse) {
result.consumeQuietly()
runOnUiThread {
// The network component doesn’t report app layer status.
// Use Mobile SDK RestResponse.isSuccess() method to check
// whether the REST request itself succeeded.
if (result.isSuccess) {
try {
} catch (e: Exception) {
onError(e)
}
}
}
}
override fun onError(exception: Exception) {
runOnUiThread {
Toast.makeText(this@MainActivity,
getString(R.string.sf__generic_error),
Toast.LENGTH_LONG,
).show()
}
}
})onSuccess() コールバックメソッドを実装する
AsyncRequestCallback() の onSuccess() メソッド内では次のことを行います。
- HTTP 状況をテストして削除操作が成功したことを確認する。基礎部分のネットワークコンポーネントでは、トランスポートレイヤーのエラーのみがレポートされ、REST 要求のエラーはレポートされないため、このチェックは必須です。
- 操作が成功した場合は、リストビューの指定された位置の項目を削除する。
- 成功メッセージを作成する。
行を削除するには listAdapter クラス変数を使用します。オブジェクトを取得するには、onItemLongClick() メソッドに渡された位置の値を使用して、ArrayAdapter.remove(T object) をコールします。次に例を示します。
listAdapter?.run {
remove(getItem(position))
}- リスト項目を削除した後、sendRequest(request) をコールして並び替えたリストを更新し、ローカルの records 配列と同期した状態にします。
if (result.isSuccess) {
try {
listAdapter?.run {
remove(getItem(position))
}
sendRequest("SELECT Name, Id FROM Contact")
} catch (e: Exception) {
onError(e)
}
}- 次に、成功メッセージを表示するアラートボックスを作成します。そうでない場合は、エラーメッセージを報告します。
if (result.isSuccess) {
try {
listAdapter?.run {
remove(getItem(position))
}
sendRequest("SELECT Name, Id FROM Contact")
AlertDialog.Builder(this@MainActivity)
.setMessage("Record successfully deleted!")
.setCancelable(true)
.create()
.show()
} catch (e: Exception) {
onError(e)
}
} else {
Toast.makeText(this@MainActivity,
getString(R.string.sf__generic_error),
Toast.LENGTH_LONG,
).show()
}完成した setOnItemLongClickListener() 関数を以下に示します。
override fun onResume() {
// Hide everything until we are logged in
findViewById<ViewGroup>(root).visibility = View.INVISIBLE
// Create list adapter
listAdapter = ArrayAdapter(this, android.R.layout.simple_list_item_1, ArrayList())
findViewById<ListView>(R.id.contacts_list).adapter = listAdapter
val listView = findViewById<ListView>(R.id.contacts_list)
listView.setOnItemLongClickListener { parent, view, position, id ->
Toast.makeText(
applicationContext,
"Long press received",
Toast.LENGTH_SHORT,
).show()
var restRequest: RestRequest
try {
restRequest = RestRequest.getRequestForDelete(
/* apiVersion = */ ApiVersionStrings.getVersionNumber(this),
/* objectType = */ "Contact",
/* objectId = */ records.getJSONObject(position).getString("Id"),
)
client!!.sendAsync(restRequest, object : AsyncRequestCallback {
override fun onSuccess(request: RestRequest?, result: RestResponse) {
result.consumeQuietly()
runOnUiThread {
// The network component doesn’t report app layer status.
// Use Mobile SDK RestResponse.isSuccess() method to check
// whether the REST request itself succeeded.
if (result.isSuccess) {
try {
listAdapter?.run {
remove(getItem(position))
}
sendRequest("SELECT Name, Id FROM Contact")
AlertDialog.Builder(this@MainActivity)
.setMessage("Record successfully deleted!")
.create()
.show()
} catch (e: Exception) {
onError(e)
}
} else {
Toast.makeText(this@MainActivity,
getString(R.string.sf__generic_error),
Toast.LENGTH_LONG,
).show()
}
}
}
override fun onError(exception: java.lang.Exception) {
runOnUiThread {
Toast.makeText(
this@MainActivity,
this@MainActivity.getString(R.string.sf__generic_error),
Toast.LENGTH_LONG
).show()
}
}
})
} catch (e: Exception) {
e.printStackTrace()
}
true
}
super.onResume()
}最終クリーンアップと実行
クリーンアップの最後に [Fetch Accounts (取引先を取得)] ボタンを削除します。リストビューは [Fetch Contacts (取引先責任者を取得)] と [Fetch Account (取引先を取得)] で共有されているため、長押しハンドラーは両方に等しく適用されます。ただし、レコードの削除に使用する ID は取引先責任者にのみ適用されるため、このハンドラーは取引先には役に立ちません。または、この単元の範囲ではありませんが、ここで学習した内容を応用し、長押しハンドラーを取引先責任者と取引先の両方を削除するように調整することもできます。このチュートリアルでは、取引先関連のコードは削除します。
指示されたファイルから次の項目を削除します。
File | アクション |
|---|---|
MainActivity.kt | onFetchAccountsClick(v: View) 関数を削除します。 |
res/layout/main.xml | 次のいずれかの操作を実行します。
|
res/values/strings.xml | 次のいずれかの操作を実行します。
|
ついにアプリケーションが完成し、実行できる状態になりました。
- Android Studio で、[Run (実行)] | [Run ‘app’ (「app」を実行)] をクリックします。
- Mobile SDK 互換のエミュレーターまたは接続デバイスを選択します。
- アプリケーションが実行されている間に、Salesforce 組織にログインし、[Fetch Contacts (取引先責任者を取得)] をクリックしてリストを表示します。長押しを確認するトーストメッセージが表示されるまで、リストの任意の項目をタップしたまま押さえます。
Developer Edition データベース内のデフォルトの取引先責任者を削除しようとすると、エラー応答が返されます。これらのエラーは、Developer Edition 組織にあらかじめパッケージされた各取引先責任者が他のレコードの親であるために発生します。テストの準備をするには、Developer Edition 組織にログインし、他のレコードを所有していないテスト取引先責任者を 1 件以上作成します。削除が成功すると、レコードが削除されたことを示すメッセージが表示されます。
リソース