進行状況の追跡を始めよう
Trailhead のホーム
Trailhead のホーム

forcedroid ネイティブアプリケーションの変更

学習の目的

この単元を完了すると、次のことができるようになります。

  • Android ネイティブテンプレートアプリケーションを変更して、リスト画面をカスタマイズする。
  • REST 応答を処理する。
  • REST 要求を使用して Salesforce レコードを削除する。

リスト画面のカスタマイズ

すでに確認したように、Android ネイティブテンプレートアプリケーションには、ユーザの Salesforce 組織から取引先責任者または取引先のリストが表示されます。現在は、Mobile SDK REST クラスにより処理される単純な SOQL SELECT クエリから作成された、参照のみのリストです。長押し操作に削除アクションを添付して、編集機能を強化しましょう。ユーザがリストビューで取引先責任者名をタップしたまま押さえると、新しい削除アクションが、関連付けられた Salesforce レコードの削除を試みます。試行が成功すると、アプリケーションは取引先責任者リストビューから該当する行を完全に削除します。要求が失敗すると、アプリケーションはユーザに失敗の理由を表示し、リストビューの行を復元します。

ロングクリックリスナーについて

ロングクリックリスナーは、簡単に実装できます。ただし、ロングクリックリスナークラスを作成する方法を決めるときは少し注意が必要です。さまざまなコーディングオプションを検討すると、まず、リストビューレベルではクリックをリスンしないことが明らかになります。代わりに、リスト項目レベルでリスンします。Android OnItemLongClickListener インターフェースのおかげで、リスト項目のリスナーを実装するのは面倒な作業ではありません。このインターフェースでは、リストビューに添付されてリストの項目の長押しに反応する 1 つのリスナーを定義します。このクラスのインスタンスを作成するには、ビュークラス内に公開インターフェースを実装します。

次の課題は、ロングクリックリスナーを実装するビュークラスを決めることです。ListView オブジェクトを使用して、リストに表示される情報を提供するデータオブジェクトを指定します。Mobile SDK テンプレートアプリケーションは、この目的のために ArrayAdapter<String> オブジェクトを作成し、ListView に添付します。

listAdapter = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, new 
ArrayList<String>());
((ListView) findViewById(R.id.contacts_list)).setAdapter(listAdapter);

ただし、ArrayAdapter<String> は、データオブジェクトであってビューではありません。それ以上の機能があります。取引先責任者リストビューの 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() メソッドを見てみましょう。
  1. Android Studio で、MainActivity.java ファイルを開きます。
  2. onResume() メソッドを見つけます。
    @Override 
    public void onResume() {
        // Hide everything until we are logged in
        findViewById(R.id.root).setVisibility(View.INVISIBLE);
    
        // Create list adapter
        listAdapter = new ArrayAdapter<String>(this, 
            android.R.layout.simple_list_item_1, new ArrayList<String>());
        ((ListView) findViewById(R.id.contacts_list)).setAdapter(listAdapter);			
        // ADD CODE HERE!
        super.onResume();
    }
    マークに従ってコーディングを開始します。ListView 用の listAdapter がセットされた後で、super.onResume() コールの前です。
  3. 取引先責任者リストビューを参照する便利な ListView 変数を宣言して割り当てます。Activity.findViewById() メソッドを使用してリストビューリソースを検索します。
    ((ListView) findViewById(R.id.contacts_list)).setAdapter(listAdapter);	
    ListView lv = ((ListView) findViewById(R.id.contacts_list));
    super.onResume();
  4. lv 変数を使用して、AdapterView.setOnItemLongClickListener() をコールしてロングクリックイベントのリスナーを設定します。リスナーのパラメータには、AdapterView.OnItemLongClickListener インターフェースのインラインスタブをインスタンス化します。
    ListView lv = ((ListView) findViewById(R.id.contacts_list));
    lv.setOnItemLongClickListener(
        new AdapterView.OnItemLongClickListener() {
            @Override
    	 public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) {
                return false;
            }
        });
    
    Android Studio は単一の仮想インターフェースメソッドもスタブしています。便利ですね。
  5. (省略可能) インポートの欠落に関するエラーが発生する場合は、クラスインポートに import android.widget.AdapterView を追加します。
  6. AdapterView.OnItemLongClickListener ボディ内で、定型の return ステートメントを確認トーストメッセージを表すコードに置き換えます。
    ListView lv = ((ListView) findViewById(R.id.contacts_list));
    lv.setOnItemLongClickListener (new AdapterView.OnItemLongClickListener() {
        @Override
        public boolean onItemLongClick(AdapterView<?> parent, View view,
            int position, long id) {
            Toast.makeText(getApplicationContext(),
                "Long press received", Toast.LENGTH_SHORT).show();
            return true;
            }
    });
  7. アプリケーションをビルドし、実行します。
  8. アプリケーションにログインしたら、[Fetch Contacts (取引先責任者を取得)] をクリックします。
  9. 取引先責任者リスト内のエントリをタップし、数秒間押したままにします。すべてが正しく機能すると、トーストメッセージが表示されます。

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 アプリケーションのリソースで定義: getString(R.string.api_version)
objectType “Contact” (ハードコード化)
objectId ??
objectId パラメータは少々複雑です。そのままの forcedroid アプリケーションが認識しない Salesforce 値を必要とします。なぜ所有していないのでしょうか? そしてどうすれば取得できるのでしょうか? 答えは簡単です。
  • ID を所有していないのは、元の REST 要求 (リストを入力する要求) がそれを求めないためです。
  • REST 要求を変更することによって ID を取得できます。

テンプレートアプリケーションの SOQL 要求の調整

MainActivity クラスは 2 つの REST 要求を発行します。1 つは取引先責任者用で、もう 1 つは取引先用です。各要求には SOQL ステートメントが含まれています。取引先は使用しないため、取引先責任者レコードリクエストが ID 値を返すように変更しましょう。

  1. Android Studio で、MainActivity.java ファイルを開きます。
  2. onFetchContactsClick() メソッドを見つけます。
    public void onFetchContactsClick(View v) throws UnsupportedEncodingException
    {
       sendRequest("SELECT Name FROM Contact");
    }
    
  3. SOQL クエリを変更して、Name 項目と Id 項目を選択するようにします。ID 項目名のキャメルケースのスペルに注意してください。項目名では大文字と小文字が区別されます。
    public void onFetchContactsClick(View v) throws UnsupportedEncodingException
    {
       sendRequest("SELECT Name, Id FROM Contact");
    }

これで、REST 応答の ID 値を受け取れるようになりましたが、ID 値をどこに保存するのがよいでしょうか。テンプレートアプリケーションは、各レコードの名前値をリストビューの行にコピーするだけで、ID 値をキャッシュしません。長押しハンドラで参照できるようにするには、クラスの範囲内に ID を保存する必要があります。

テンプレートアプリケーションの sendRequest() メソッドの適合

sendRequest(String soql) メソッドまでスクロールダウンします。ここに取得応答が返されます。このメソッドを見ると、Mobile SDK REST メカニズムの動作がよくわかります。さっそく見てみましょう。初めに、メソッドは、指定された SOQL クエリの REST 要求を定義する RestRequest ファクトリメソッドをコールします。
RestRequest restRequest = RestRequest.getRequestForQuery(
    ApiVersionStrings.getVersionNumber(this), soql);

次に、client.sendAsync() コール内で、この新しい RestRequest オブジェクトを Salesforce に送信します。

client.sendAsync(restRequest, new AsyncRequestCallback() {
    @Override
    public void onSuccess(RestRequest request, final RestResponse result) {
        result.consumeQuietly(); // consume before going back to main thread
        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                try {
                    listAdapter.clear();
                    JSONArray records = result.asJSONObject().getJSONArray("records");
                    for (int i = 0; i < records.length(); i++) {
                        listAdapter.add(records.getJSONObject(i).getString("Name"));
                    }
                } catch (Exception e) {
                    onError(e);
                }
            }
        });
    }
 
    @Override
    public void onError(final Exception exception) {
        runOnUiThread(new Runnable() {
        @Override
            public void run() {
                Toast.makeText(MainActivity.this,
                    MainActivity.this.getString(
                        R.string.sf__generic_error, 
                        exception.toString()),
                    Toast.LENGTH_LONG).show();
            }
        });
    }
});

sendAsync() コールには、RestRequest オブジェクトの他に、Mobile SDK AsyncRequestCallback インターフェースの実装も必要です。このインターフェースの onSuccess() メソッドは、コールバックを介して REST 応答を非同期に受信します。デフォルトの AsyncRequestCallback 実装は、forcedroid アプリケーションで定義される SOQL クエリのみを処理します。

この onSuccess() メソッドがすでに目的の機能を果たしています。このメソッドには、REST 応答から返されたレコードを抽出し、ローカル records 変数に割り当てるコードが含まれています。この変数をメソッド本文の外で再宣言することによって、この変数をクラス範囲に移動しましょう。
  1. MainActivity クラス宣言の上部付近で、既存のクラス変数宣言を使用して、JSONArray records を非公開変数として宣言します。
    public class MainActivity extends SalesforceActivity {
        private RestClient client;
        private ArrayAdapter<String> listAdapter;
        private JSONArray records;
       ….
  2. onSuccess() メソッドで、records 変数の型宣言を削除します。
    public void onSuccess(RestRequest request, final RestResponse result) {
        result.consumeQuietly(); // consume before going back to main thread
        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                try {
                    listAdapter.clear();
                    records = result.asJSONObject().getJSONArray("records");
                    for (int i = 0; i < records.length(); i++) {
                        listAdapter.add(records.getJSONObject(i).getString("Name"));
                    }
                } catch (Exception e) {
                    onError(e);
                }
            }
        });
    }

onItemLongClick() メソッドの終了

onItemLongClick() メソッドを終了する準備ができました。基本アルゴリズムは次のとおりです。

  1. 適切な Mobile SDK RestRequest ファクトリメソッドをコールして、「削除要求」オブジェクトを取得します。すべての RestRequest メソッドが例外を発生させるため、コールを try...catch ブロックで囲む必要があります。
  2. 生成された RestClient オブジェクトを使用して「削除要求」オブジェクトを Salesforce に送信します。
  3. コールバックメソッドで REST の結果を処理します。

RestRequest オブジェクトを取得する

  1. onResume() メソッド内の onItemLongClick() メソッドまでスクロールして戻ります。
  2. Toast.makeText() コールの後で、restRequest というローカル RestRequest オブジェクトを宣言し、null に初期化します。
    RestRequest restRequest = null;
  3. 空の try...catch 構造を追加します。
    RestRequest restRequest = null;
    try {
    
    } catch (Exception e) {
     
    }
  4. try ブロックで、削除操作のための REST 要求オブジェクトを取得するファクトリメソッドをコールします。ヒント: 静的 RestRequest.getRequestForDelete() メソッドを使用します。
    RestRequest restRequest = null;
    try {
        restRequest = RestRequest.getRequestForDelete(
                   // arguments?
                );
    } catch (Exception e) {
     
    }
  5. 最初のパラメータに、プロジェクトの Mobile SDK リソースで指定されている Salesforce API バージョンを取得します。
    RestRequest restRequest = null;
    try {
         restRequest = RestRequest.getRequestForDelete(
            getString(R.string.api_version), //...);
    } catch (Exception e) {
    
    }
  6. objectType パラメータに “Contact” を指定します。
    RestRequest restRequest = null;
    try {
        restRequest = RestRequest.getRequestForDelete(
            getString(R.string.api_version), "Contact", //...);
    } catch (Exception e) {
    
    }
  7. RestRequest.getRequestForDelete() に、レコード配列内で現在のリストビュー位置に一致するエントリの ID を渡します。以下に、ID を取得する方法を示します。
    RestRequest restRequest = null;
    try {
        restRequest = RestRequest.getRequestForDelete(
            getString(R.string.api_version), "Contact",
            records.getJSONObject(position).getString("Id"));
        // Send the request
        // ...
    } catch (Exception e) {
    
    }
  8. catch ブロックで、Exception 引数に対して printStackTrace() をコールします。
    RestRequest restRequest = null;
    try {
        restRequest = RestRequest.getRequestForDelete(
            getString(R.string.api_version), "Contact",
            records.getJSONObject(position).getString("Id"));
        // Send the request
        // ...
    } catch (Exception e) {
        e.printStackTrace();
    }

削除要求オブジェクトを取得したら、それを Salesforce に送信し、コールバックメソッドで結果を処理します。

RestClient.sendAsync() メソッドを追加する

もう少しで作業は完了です。パズルの最後のピースは、RestClient.sendAsync() メソッドを使用して要求を送信することです。このメソッドを使用するには、AsyncRequestCallback インターフェースを実装する必要があります。ご存知のとおり、Mobile SDK は REST 応答を AsyncRequestCallback メソッドに送信します。

  1. onItemLongClick() で、getRequestForDelete() コールの後に、sendRequest() メソッドから RestClient.sendAsync() コードをコピーして貼り付けます。
  2. onSuccess() メソッドの try ブランチの内部コードを削除します。catch ブランチは、エラーハンドラへのコールアウトを行うだけなので、削除しません。
  3. onError() 上書き実装は汎用で、どの Salesforce 応答でも動作するため、そのままにします。

不要な部分を削除した RestClient.sendAsync() へのコールは次のようになります。

restRequest = RestRequest.getRequestForDelete(
        getString(R.string.api_version), "Contact",
        records.getJSONObject(position).getString("Id"));
client.sendAsync(restRequest, new AsyncRequestCallback() {
    @Override
    public void onSuccess(RestRequest request, final RestResponse result) {
        result.consumeQuietly();
        runOnUiThread(new Runnable() { 
            @Override
            public void run() {
                // 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 (Exception e) {
                        onError(e);
                    }
                }
            }
        });
    }

    @Override
    public void onError(final Exception exception) {
        runOnUiThread(new Runnable() {
        @Override
            public void run() {
                Toast.makeText(MainActivity.this,
                        MainActivity.this.getString(R.string.sf__generic_error, exception.toString()),
                        Toast.LENGTH_LONG).show();
            }
        });
    }
});

onSuccess() コールバックメソッドを実装する

AsyncRequestCallback()onSuccess() メソッド内では次のことを行います。
  1. HTTP 状況をテストして削除操作が成功したことを確認する。基礎部分のネットワークコンポーネントでは、トランスポートレイヤのエラーのみがレポートされ、REST 要求のエラーはレポートされないため、このチェックは必須です。
  2. 操作が成功した場合は、リストビューの指定された位置の項目を削除する。
  3. 成功メッセージを作成する。
行を削除するには listAdapter クラス変数を使用します。オブジェクトを取得するには、onItemLongClick() メソッドに渡された位置の値を使用して、ArrayAdapter.remove(T object) をコールします。以下に例を示します。
listAdapter.remove(listAdapter.getItem(position));
このコードを追加すると、範囲の問題が発生します。インターフェース実装コンテキストで作業しているため、onItemLongClick() コンテキストからのローカル position 変数は使用できません。代わりに、クラス変数を追加し、位置変数をクラス変数に割り当てられます。
  1. クラスの上部で、int 型の pos という private クラス変数を宣言し、初期化します。
    public class MainActivity extends SalesforceActivity {
    
        private RestClient client;
        private ArrayAdapter<String> listAdapter;
        private JSONArray records;
        private int pos = -1;
  2. onItemLongClick() メソッドの第 1 行で、position 値を取得します。
    public boolean onItemLongClick(AdapterView<?> parent, View view,
    	int position, long id) {
    	pos = position;
    	...
  3. AsyncRequestCallback 実装の onSuccess() メソッドで、数行下のスタブインした if ブロックまでスクロールダウンします。
    if (result.isSuccess()) {
        try {
    
        } catch (Exception e) {
            onError(e);
        }
    }
  4. result.isSuccess() が true の場合は、listAdapter.remove() メソッドをコールして行を削除します。行を削除するには、position ではなく pos を使用します。
    if (result.isSuccess()) {
        listAdapter.remove(listAdapter.getItem(pos));
    
    }
  5. リスト項目を削除した後、sendRequest(request) をコールして並び替えたリストを更新し、ローカルのl records 配列と同期した状態にします。
    if (result.isSuccess()) {
        listAdapter.remove(listAdapter.getItem(pos));
        sendRequest(”SELECT Name, Id FROM Contact”);
    
    }
  6. 最後に、成功メッセージを表示するアラートボックスを作成します。そうでない場合は、エラーメッセージを報告します。
    if (result.isSuccess()) {
        listAdapter.remove(listAdapter.getItem(pos));
        sendRequest(”SELECT Name, Id FROM Contact”);
        AlertDialog.Builder b = new AlertDialog.Builder(findViewById(R.id.contacts_list).getContext());
        b.setMessage("Record successfully deleted!");
        b.setCancelable(true);
        AlertDialog a = b.create();
        a.show();
    } else {
       Toast.makeText(MainActivity.this,
             MainActivity.this.getString(R.string.sf__generic_error, result.toString()),
             Toast.LENGTH_LONG).show();
    }

以下は、完成した onItemLongClick() メソッドです。

@Override
public boolean onItemLongClick(AdapterView<?> parent, 
        View view, int position, long id) {
    pos = position;

    Toast.makeText(getApplicationContext(),
        "Long press detected", Toast.LENGTH_SHORT).show();
    RestRequest restRequest = null;
    try {
       RestRequest request = RestRequest.getRequestForDelete(
           getString(R.string.api_version), "Contact", 
               records.getJSONObject(position).getString("Id"));
       client.sendAsync(request, new AsyncRequestCallback() {
            @Override
            public void onSuccess(RestRequest request, final RestResponse result) {
                result.consumeQuietly();
                runOnUiThread(new Runnable() { 
                    @Override
                    public void run() {
                        try {
                            // 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()) {                                        
                                listAdapter.remove(listAdapter.getItem(pos));
                                sendRequest(”SELECT Name, Id FROM Contact”);
                                AlertDialog.Builder b = 
                                    new AlertDialog.Builder(findViewById
                                        (R.id.contacts_list).getContext());
                                b.setMessage("Record successfully deleted!");
                                b.setCancelable(true);
                                AlertDialog a = b.create();
                                a.show();
                            } else {
                                Toast.makeText(MainActivity.this,
                                    MainActivity.this.getString(
                                        R.string.sf__generic_error, 
                                        result.toString()),
                                    Toast.LENGTH_LONG).show(); 
                            }   
                        } catch (Exception e) {
                            onError(e);
                        }
                    }});
                }

                @Override
                public void onError(final Exception exception) {
                    runOnUiThread(new Runnable() {
                        @Override
                        public void run() {		          
                            Toast.makeText(MainActivity.this,
                                MainActivity.this.getString(
                                    R.string.sf__generic_error, 
                                    exception.toString()),
                                Toast.LENGTH_LONG).show();
                        }
                    });
                }
            });
        } catch (Exception e) {
            e.printStackTrace();
        }
    return true;
}

最終クリーンアップと実行

クリーンアップの最後に [取引先を取得] ボタンを削除します。リストビューは [取引先責任者を取得][取引先を取得] で共有されているため、長押しハンドラは両方に等しく適用されます。ただし、レコードの削除に使用する ID は取引先責任者にのみ適用されるため、このハンドラは取引先には役に立ちません。または、この単元の範囲ではありませんが、ここで学習した内容を応用し、長押しハンドラを取引先責任者と取引先の両方を削除するように調整することもできます。このチュートリアルでは、取引先関連のコードは削除します。

指示されたファイルから次の項目を削除します。

ファイル アクション
MainActivity.java onFetchAccountsClick(View v) メソッドを削除します。
res/layout/Main.xml 次のいずれかの操作を実行します。
  • グラフィカルレイアウトで、[Fetch Accounts (取引先の取得)] ボタンを削除します。
  • XML ビューで、ID が「@+id/fetch_accounts」の <Button> ノードを削除します。
res/values/strings.xml 次のいずれかの操作を実行します。
  • [Resources (リソース)] タブで、「fetch_accounts_button (String)」を選択して [Remove (削除)] をクリックします。
  • XML ビューで、「fetch_accounts_button」という名前の <string> ノードを削除します。

ついにアプリケーションが完成し、実行できる状態になりました。

  1. Android Studio で、[Run (実行)] | [Run ‘app’ (「app」を実行)] をクリックします。
  2. Mobile SDK 互換のエミュレータまたは接続デバイスを選択します。
  3. アプリケーションが実行されている間に、Salesforce 組織にログインし、[Fetch Contacts (取引先責任者を取得)] をクリックしてリストを表示します。長押しを確認するトーストメッセージが表示されるまで、リストの任意の項目をタップしたまま押さえます。

Developer Edition データベース内のデフォルトの取引先責任者を削除しようとすると、エラー応答が返されます。これらのエラーは、Developer Edition 組織にあらかじめパッケージされた各取引先責任者が他のレコードの親であるために発生します。テストの準備をするには、Developer Edition 組織にログインし、他のレコードを所有していないテスト取引先責任者を 1 件以上作成します。削除が成功すると、レコードが削除されたことを示すメッセージが表示されます。