Modificar la aplicación forcedroid nativa
Objetivos de aprendizaje
Después de completar esta unidad, podrá:
- Modificar la aplicación de plantilla Android nativa para personalizar la pantalla de lista.
- Controlar una respuesta REST.
- Eliminar un registro de Salesforce mediante una solicitud REST.
Personalización de la pantalla de lista
Como ha visto anteriormente, la aplicación de plantilla Android nativa muestra una lista de contactos o cuentas de la organización de Salesforce del usuario. En la actualidad, es una lista de solo lectura creada a partir de una consulta SOQL SELECT sencilla que se procesa mediante clases de REST de Mobile SDK.
Vamos a ampliar la capacidad de modificación mediante la adición de una acción de eliminación al gesto de pulsación prolongada. Cuando el usuario toca de forma continua un nombre de contacto en la vista de lista, la nueva acción de eliminación intenta eliminar el registro de Salesforce asociado. Si el intento tiene éxito, la aplicación elimina permanentemente la fila de la vista de lista de contactos en la aplicación. Si la solicitud genera un error, la aplicación indica al usuario el motivo del error y restablece la fila de la vista de lista.
Acerca del proceso de escucha de clic prolongado
La implementación del proceso de escucha de clic prolongado es sencilla. Sin embargo, decidir cómo crear la clase de proceso de escucha de clic prolongado, es algo un poco más difícil. Si analiza las distintas opciones de codificación, lo primero que descubre es que no se escuchan los clics en el nivel de vista de lista. En su lugar, se escuchan en el nivel de elemento de lista.
Por suerte, la implementación de procesos de escucha no es una tarea compleja gracias a la interfaz OnItemLongClickListener de Android. Esta interfaz define un único proceso de escucha asociado a la vista de lista y que responde a las pulsaciones prolongadas de cualquier elemento de la lista. Cree una instancia de esta clase mediante la implementación de una interfaz pública en su clase de vista.
El siguiente reto es determinar qué clase de vista implementa el proceso de escucha de clic prolongado. En el caso de los objetos ListView, especifique un objeto de datos que proporcione la información mostrada en la lista. La aplicación de plantilla de Mobile SDK crea un objeto ArrayAdapter<String> con este fin y, a continuación, lo asocia a ListView.
listAdapter = ArrayAdapter(this, android.R.layout.simple_list_item_1, ArrayList()) findViewById<ListView>(R.id.contacts_list).adapter = listAdapter
Sin embargo, ArrayAdapter es un objeto de datos y no una vista, ¿verdad? Sí, y mucho más. ArrayAdapter crea en la vista de lista de contactos un objeto AdapterView para cada elemento del conjunto de datos de la lista. Dado que estas vistas de adaptador representan los objetos de interés, use la clase AdapterView para implementar OnItemLongClickListener.
A continuación, asocie el objeto de proceso de escucha con el objeto ListView, el cual recibe todas las notificaciones de sus elementos secundarios. Esta asociación limita OnItemLongClickListener a la interacción solo con los elementos de la vista de lista de la aplicación de plantilla. Por último, implemente su comportamiento de eliminación en el método de interfaz única.
Un detalle final que solucionar: ¿Dónde se incluye este código de proceso de escucha de clic prolongado? Mobile SDK proporciona los siguientes métodos de devolución de llamada de punto de entrada:
public abstract void onResume(RestClient client); @Override protected void onCreate(Bundle savedInstanceState); @Override public void onResume();
Podemos eliminar uno de ellos: onCreate(Bundle savedInstanceState). Este método configura la actividad y controla los flujos de autenticación antes de que se creen instancias de las vistas. Las vistas entran en escena en el método onResume(). Por lo tanto, este método parecer ser la opción más probable. La llamada al método onResume(RestClient client) se realiza mediante la superclase en el inicio de sesión para capturar el objeto RestClient autenticado, que vamos a dejar como está. Por lo tanto, ya tenemos los resultados. Agregue el código de proceso de escucha de clic prolongado a onResume().
Implementar un proceso de escucha de clic prolongado
En este caso, vamos a empezar por el código. En Android Studio, abra la clase MainActivity y examine el método onResume().
- En Android Studio, abra el archivo MainActivity.kt.
- Encuentre el método onResume().
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()
}Va a empezar con la codificación según el marcado; es decir, después establecer listAdapter para ListView, pero antes de la llamada super.onResume().
- Declare y asigne una variable ListView adecuada que haga referencia a la vista de lista de contactos. Use el método Activity.findViewById() para buscar el recurso de la vista de lista.
findViewById<ListView>(R.id.contacts_list).adapter = listAdapter
val listView = findViewById<ListView>(R.id.contacts_list) super.onResume()
- Con la variable listView, llame a la función lambda AdapterView.setOnItemLongClickListener() para configurar un proceso de escucha para los eventos de clic prolongado. En el caso del parámetro del proceso de escucha, cree una instancia de código auxiliar en línea de la interfaz AdapterView.OnItemLongClickListener.
val listView = findViewById<ListView>(R.id.contacts_list)
listView.setOnItemLongClickListener { parent, view, position, id -> false }- (Opcional) Si obtiene un error porque falta una importación, agregue import android.widget.AdapterView a sus importaciones de clases.
- Dentro del cuerpo de AdapterView.OnItemLongClickListener, reemplace la declaración return reutilizable por código que muestre un mensaje emergente de confirmación.
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
}- Cree la aplicación y ejecútela.
- Cuando inicie sesión en la aplicación, haga clic en Fetch Contacts (Obtener contactos).
- Toque una entrada de la lista de contactos y mantenga la pulsación durante un par de segundos. Si todo es correcto, se muestra el mensaje emergente.
Agregar solicitudes REST de Mobile SDK
Ya está casi todo listo para agregar elementos de Mobile SDK. En el método onItemLongClick(), cree una solicitud REST para eliminar el registro de Salesforce asociado a la fila tocada. A continuación, envíe la solicitud a Salesforce. Antes de profundizar en el código, revise estos aspectos importantes.
Obtener una instancia de RestClient
Recuerde que los objetos RestClient nunca se deben crear directamente. Mobile SDK crea uno y lo devuelve a MainActivity mediante el método onResume(RestClient client). Esta instancia de RestClient se autentica con el token de acceso del usuario actual. Para su uso, el método onResume(RestClient client) asigna esta instancia a la variable de clase client.
Crear una solicitud REST
A fin de crear la solicitud REST para este ejercicio, llame al método de generador RestRequest para la eliminación de un registro:
public static RestRequest getRequestForDelete(String apiVersion, String objectType, String objectId);
¿Dónde se obtienen los valores de argumento? Consulte esta tabla.
Parámetro | Valor |
|---|---|
apiVersion | ApiVersionStrings.getVersionNumber(Context context) |
objectType |
|
objectId | ¿? ¿? |
El parámetro objectId es un elemento complejo. Requiere un valor de Salesforce que la aplicación forcedroid sin procesar no reconoce. ¿Por qué no lo tiene y qué puede hacer para obtenerlo? Las respuestas son sencillas:
- No tiene estos Id. porque las solicitudes REST originales (que completan las listas) no los solicitan.
- Para obtener el Id. cambie la solicitudes REST.
Ajustar la solicitud SOQL de la aplicación de plantilla
La clase MainActivity genera dos solicitudes REST: una para contactos y otra para cuentas. Cada solicitud contiene una declaración SOQL. Dado que no vamos a usar cuentas, vamos a actualizar la solicitud de registros de contacto para devolver valores de Id.
- En Android Studio, abra el archivo MainActivity.kt.
- Busque el método onFetchContactsClick().
fun onFetchContactsClick(v: View) {
sendRequest("SELECT Name FROM Contact")
}- Cambie la consulta SOQL para seleccionar los campos Name (Nombre) e Id. Asegúrese de usar la ortografía con distinción entre mayúsculas y minúsculas del nombre de campo de Id. Los nombres de campo distinguen entre mayúsculas y minúsculas.
fun onFetchContactsClick(v: View) {
sendRequest("SELECT Name, Id FROM Contact")
}Ahora que está preparado para recibir los valores de Id. en la respuesta REST, ¿cuál es la mejor ubicación para almacenarlos? La aplicación de plantilla solo copia el valor de nombre para cada registro en una fila de la vista de lista, pero no almacena en caché los valores de Id. Para activar las búsquedas en el controlador de pulsación prolongada, debe almacenar los Id. en el ámbito de clase.
Adaptar el método sendRequest() de la aplicación de plantilla
Desplácese al método sendRequest(String soql), que es a donde llega la respuesta obtenida. Este método es una demostración clara de cómo funciona el mecanismo REST de Mobile SDK. Veamos rápidamente cómo funciona. Primero, el método llama a un método de generador RestRequest que define una solicitud REST para una consulta SOQL determinada:
val restRequest = RestRequest.getRequestForQuery(ApiVersionStrings.getVersionNumber(this), soql)
A continuación, envía el nuevo objeto RestRequest a Salesforce en la llamada client.sendAsync().
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()
}
}
})Además del objeto RestRequest, la llamada sendAsync() requiere una implementación de la interfaz AsyncRequestCallback de Mobile SDK. El método onSuccess() de esta interfaz recibe la respuesta REST de forma asíncrona mediante una devolución de llamada. La implementación AsyncRequestCallback predeterminada controla solo las consultas SOQL definidas en la aplicación forcedroid.
Este método onSuccess() ya hace lo que necesitamos. Contiene el código que extrae los registros devueltos por la respuesta REST y los asigna a la variable records local. Vamos a mover esta variable al ámbito de clase. Para ello, se vuelve a declarar fuera del cuerpo del método.
- Junto a la parte superior de la definición de clase MainActivity, declare JSONArray records como una variable privada con las siguientes declaraciones de variable de clase existentes:
class MainActivity : SalesforceActivity() {
private var client: RestClient? = null
private var listAdapter: ArrayAdapter<String>? = null
private lateinit var records: JSONArray
…- En el método onSuccess(), elimine la declaración de tipo para la variable 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)
}
}
}Finalizar el método onItemLongClick()
Ya cuenta con la preparación para finalizar el método onItemLongClick(). El algoritmo básico es el siguiente:
- Obtenga un objeto de “solicitud de eliminación” mediante la llamada a un método de generador RestRequest de Mobile SDK apropiado. Dado que todos los métodos RestRequest devuelven excepciones, asegúrese de incluir la llamada en un bloque try...catch.
- Envíe el objeto de “solicitud de eliminación” a Salesforce mediante el objeto RestClient generado.
- Controle el resultado de REST en métodos de devolución de llamada.
Obtener un objeto RestRequest
- Desplácese de nuevo al método setOnItemLongClickListener en el método onResume().
- Después de la llamada Toast.makeText(), declare un objeto local RestRequest llamado restRequest.
var restRequest: RestRequest
- Agregue una construcción try...catch vacía.
try {
} catch (e: Exception) {
}- En el bloque try, llame a un método de generador que obtenga un objeto de solicitud REST para una operación de eliminación. SUGERENCIA: Use el método estático RestRequest.getRequestForDelete().
try {
restRequest = RestRequest.getRequestForDelete(
// arguments?
)
} catch (e: Exception) {
}- Para el primer parámetro, recupere la versión de la API de Salesforce especificada en los recursos del Mobile SDK de su proyecto.
try {
restRequest = RestRequest.getRequestForDelete(
/* apiVersion = */ ApiVersionStrings.getVersionNumber(this),
// ..
)
} catch (e: Exception) {
}- Para el parámetro objectType, especifique
"Contact"(Contacto).
try {
restRequest = RestRequest.getRequestForDelete(
/* apiVersion = */ ApiVersionStrings.getVersionNumber(this),
/* objectType = */ "Contact",
// ..
)
} catch (e: Exception) {
}- Pase a RestRequest.getRequestForDelete() el Id. de la entrada de la matriz de registros que coincide con la posición de vista de lista actual. Así es como puede recuperar el Id.
try {
restRequest = RestRequest.getRequestForDelete(
/* apiVersion = */ ApiVersionStrings.getVersionNumber(this),
/* objectType = */ "Contact",
/* objectId = */ records.getJSONObject(position).getString("Id"),
)
} catch (e: Exception) {
}- En el bloque catch, llame a printStackTrace() en el argumento Exception.
try {
restRequest = RestRequest.getRequestForDelete(
/* apiVersion = */ ApiVersionStrings.getVersionNumber(this),
/* objectType = */ "Contact",
/* objectId = */ records.getJSONObject(position).getString("Id"),
)
// Send the request
// ...
} catch (e: Exception) {
e.printStackTrace()
}Una vez que obtenga el objeto de “solicitud de eliminación”, envíelo a Salesforce y controle el resultado en métodos de devolución de llamada.
Agregar el método RestClient.sendAsync()
¡Ya casi ha terminado! La última pieza del rompecabezas es el envío de la solicitud mediante el método RestClient.sendAsync(). Este método requiere que implemente la interfaz AsyncRequestCallback. Como sabe, Mobile SDK envía las respuestas REST a los métodos AsyncRequestCallback.
- En onItemLongClick(), después de la llamada getRequestForDelete(), copie y pegue el código RestClient.sendAsync() del método sendRequest().
- Elimine el código interno de la rama try del método onSuccess(). No elimine la rama catch, ya que solo llama a un controlador de errores.
- Mantenga la implementación de sustitución onError(), ya que es lo bastante genérica para funcionar con cualquier respuesta de Salesforce.
Esta es la llamada de código auxiliar a 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()
}
}
})Implementar el método de devolución de llamada onSuccess()
En el método onSuccess() de AsyncRequestCallback(), haga lo siguiente:
- Asegúrese de que la operación de eliminación es correcta mediante la prueba del estado HTTP. Esta comprobación es necesaria, ya que el componente de red subyacente solo indica errores de la capa de transporte y no errores de solicitud.
- Si la operación es correcta, elimine el elemento de la posición correspondiente de la vista de lista.
- Publique un mensaje de resultado correcto.
Use la variable de clase listAdapter para eliminar la fila. Puede llamar a ArrayAdapter.remove(T object) con el valor de posición pasado al método onItemLongClick() para obtener el objeto. Por ejemplo:
listAdapter?.run {
remove(getItem(position))
}- Después de eliminar un elemento de lista, llame a sendRequest(request) para actualizar la lista reordenada y mantenerla en sincronía con su matriz records local:
if (result.isSuccess) {
try {
listAdapter?.run {
remove(getItem(position))
}
sendRequest("SELECT Name, Id FROM Contact")
} catch (e: Exception) {
onError(e)
}
}- Por último, publique un cuadro de alerta para mostrar un mensaje de resultado correcto. En caso contrario, indique el mensaje de error correspondiente.
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()
}Aquí está la función setOnItemLongClickListener() completada.
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()
}Limpieza final y tiempo de ejecución
Parte de la limpieza final consiste en eliminar el botón Fetch Accounts (Obtener cuentas). Dado que la vista de lista se comparte entre Fetch Contacts (Obtener contactos) y Fetch Accounts (Obtener cuentas), el controlador de pulsación prolongada se aplica a ambos por igual. No obstante, este controlador no es válido para las cuentas, ya que el Id. usado para eliminar el registro solo se aplica a un contacto. De forma alternativa y como “ventaja adicional”, puede aplicar lo que ha aprendido y adaptar el controlador de pulsación prolongada para eliminar contactos y cuentas. Sin embargo, en este tutorial, vamos a eliminar el código relacionado con las cuentas.
Elimine los siguientes elementos de los archivos indicados.
Archivo | Acción |
|---|---|
MainActivity.kt | Elimine la función onFetchAccountsClick(v: View). |
res/layout/main.xml | Realice una de las siguientes acciones:
|
res/values/strings.xml | Realice una de las siguientes acciones:
|
Al fin, su aplicación está terminada y lista para su ejecución.
- En Android Studio, haga clic en Run (Ejecutar) | Run ‘app’ (Ejecutar “aplicación”).
- Seleccione un emulador o dispositivo conectado compatible con Mobile SDK.
- Cuando la aplicación se esté ejecutando, inicie sesión en su organización de Salesforce y, a continuación, haga clic en Fetch Contacts (Obtener contactos) para ver la lista. Toque cualquier elemento de la lista y mantenga la pulsación hasta que se muestre un mensaje emergente para confirmar la pulsación prolongada.
Observe que obtiene una respuesta de error al intentar eliminar cualquier contacto predeterminado de la base de datos de Developer Edition. Estos errores se producen porque cada contacto que viene empaquetado previamente de una organización de Developer Edition es el elemento principal de otros registros. Para prepararse para las pruebas, inicie sesión en su organización de Developer Edition y cree uno o varios contactos de prueba que no tengan otros registros en propiedad. Si la eliminación es correcta, se muestra un mensaje para indicar que el registro se ha eliminado.
Recursos
- Guía de desarrolladores de Salesforce: Using REST APIs (Usar API de REST)
- Salesforce Developers: RestClient Class
- Salesforce Developers: RestRequest Class