19

He visto la E/S charla RESTO Google I y leer las diapositivas: http://www.google.com/events/io/2010/sessions/developing-RESTful-android-apps.htmlCómo manejar la actualización REST del servidor remoto con SyncAdapter

Todavía estoy un poco confuso en cómo manejar muy bien, por ejemplo, una error de actualización lanzado por el servidor remoto. Implementé mi propio ContentProvider y SyncAdapter. Considere este escenario:

actualización de un usuario Detalles de contacto a través de llamadas REST:

  1. Solicitar una actualización mediante una ContentResolver.
  2. My ContentProvider inmediatamente actualiza la base de datos Sqlite local de la aplicación y solicita una sincronización (según las recomendaciones en la conversación de Google I/O).
  3. Se llama a My SyncAdapter.onPerformSync() y realiza una llamada REST para actualizar los datos remotos.
  4. El servidor remoto responde con "ERROR: número de teléfono no válido" (por ejemplo).

Mi pregunta es, ¿cuál es la mejor manera para el SyncAdapter para señalar a mi ContentProvider que este cambio debe ser respaldado de base de datos local de la aplicación, y para señalar también a mi actividad que la solicitud de actualización falló (y pasar los mensajes de error devueltos por el servidor)?

Mi actividad necesita mostrar un controlador de progreso mientras espera el resultado, y saber si la solicitud tuvo éxito o falló.


Para actualizar la base de datos aplicación local con el contenido del servidor, el patrón SyncAdapter tiene mucho sentido para mí, y tengo que trabajar bien. Pero para las actualizaciones desde la aplicación a el servidor, parece que no puedo encontrar una buena manera de manejar el escenario anterior.


Y otra cosa ...;)

Decir que llamo ContentResolver.notifyChange (URI, null, true); desde el método de actualización() de mi ContentProvider. true junto con android:supportsUploading="true" hará que se llame a my SyncAdapter's onPerformSync(). Genial, pero dentro dePerformSync(), ¿cómo puedo decir qué URI debo sincronizar? No quiero simplemente actualizar mi base de datos completa cada vez que recibo una solicitud de sincronización. Pero ni siquiera puede pasar un paquete al notifyChangeCall() para pasarlo a onPerformSync().

Todos los ejemplos que he visto de onPerformSync() han sido tan simples, y no se han utilizado los proveedores de contenido personalizados, ¿algún ejemplo del mundo real que hay? Y los documentos son un poco como un nido de pájaro. Virgil Dobjanschi, señor, me ha dejado en el arroyo sin un remo.

+0

Estoy luchando con una [configuración similar] (http://stackoverflow.com/questions/11906172/synchronize-android-client-and-rest-server). Quizás puedas ayudar. – JJD

Respuesta

3

Respuesta breve, si su objetivo es ~ API nivel 7, es "Do not". La situación puede haber mejorado en posteriores API, pero tal como estaba ...Recomiendo encarecidamente evitar SyncAdapter por completo; está documentado muy mal y la gestión "automática" de la cuenta/autenticación tiene un alto precio, ya que la API también es intrincada y está poco documentada. Esta parte de la API no ha sido pensada más allá de los casos de uso más triviales.

Así que aquí está el patrón Terminé yendo con. Dentro de mis actividades que tenía un controlador con una simple adición de una superclase controlador personalizado (podría comprobar si hay un bool m_bStopped):

private ResponseHandler mHandler = new ResponseHandler(); 

class ResponseHandler extends StopableHandler { 

    @Override 
    public void handleMessage(Message msg) { 
     if (isStopped()) { 
      return; 
     } 
     if (msg.what == WebAPIClient.GET_PLANS_RESPONSE) { 
      ... 
     } 
     ... 
    } 
} 

La actividad invocaría las solicitudes REST como se muestra a continuación. Tenga en cuenta que el controlador se transfiere a la clase WebClient (una clase auxiliar para crear/realizar solicitudes HTTP, etc.). WebClient utiliza este controlador cuando recibe la respuesta HTTP para enviar un mensaje a la actividad y le informa que los datos se han recibido y, en mi caso, se han almacenado en una base de datos SQLite (que recomendaría). En la mayoría de las actividades, que yo llamaría mHandler.stopHandler(); en onPause() y mHandler.startHandler(); en onResume() para evitar la respuesta HTTP que se marcó de nuevo a una actividad inactiva etc. Esto resultó ser un enfoque bastante robusto.

final Bundle bundle = new Bundle(); 
bundle.putBoolean(WebAPIRequestHelper.REQUEST_CREATESIMKITORDER, true); 
bundle.putString(WebAPIRequestHelper.REQUEST_PARAM_KIT_TYPE, sCVN);  
final Runnable runnable = new Runnable() { public void run() { 
    VendApplication.getWebClient().processRequest(null, bundle, null, null, null, 
        mHandler, NewAccountActivity.this); 
    }}; 
mRequestThread = Utils.performOnBackgroundThread(runnable); 

Handler.handleMessage() se invoca en el hilo principal. De modo que puede detener sus diálogos de progreso aquí y realizar otras actividades de forma segura.

I declaró un ContentProvider:

<provider android:name="au.com.myproj.android.app.webapi.WebAPIProvider" 
      android:authorities="au.com.myproj.android.app.provider.webapiprovider" 
      android:syncable="true" /> 

y puesto en práctica para crear y gestionar el acceso a la base de datos SQLite:

public class WebAPIProvider extends ContentProvider 

modo que lo pueda conseguir cursores sobre la base de datos en sus actividades como esta :

mCursor = this.getContentResolver().query (
      WebAPIProvider.PRODUCTS_URI, null, 
      Utils.getProductsWhereClause(this), null, 
      Utils.getProductsOrderClause(this)); 
startManagingCursor(mCursor); 

Encontré la clase org.apache.commons.lang3.text.StrSubstitutor para ser imme nly útil en la construcción de las torpes solicitudes XML requeridas por la API REST que tuve que integrar con, por ejemplo, en WebAPIRequestHelper tuve métodos auxiliares como:

public static String makeAuthenticateQueryString(Bundle params) 
{ 
    Map<String, String> valuesMap = new HashMap<String, String>(); 
    checkRequiredParam("makeAuthenticateQueryString()", params, REQUEST_PARAM_ACCOUNTNUMBER); 
    checkRequiredParam("makeAuthenticateQueryString()", params, REQUEST_PARAM_ACCOUNTPASSWORD); 

    valuesMap.put(REQUEST_PARAM_APIUSERNAME, API_USERNAME); 
    valuesMap.put(REQUEST_PARAM_ACCOUNTNUMBER, params.getString(REQUEST_PARAM_ACCOUNTNUMBER)); 
    valuesMap.put(REQUEST_PARAM_ACCOUNTPASSWORD, params.getString(REQUEST_PARAM_ACCOUNTPASSWORD)); 

    String xmlTemplate = VendApplication.getContext().getString(R.string.XMLREQUEST_AUTHENTICATE_ACCOUNT); 
    StrSubstitutor sub = new StrSubstitutor(valuesMap); 
    return sub.replace(xmlTemplate); 
} 

Qué me anexar a la URL de punto final apropiado.

Aquí hay más detalles sobre cómo la clase WebClient hace las peticiones HTTP. Este es el método processRequest() llamado anteriormente en Runnable. Observe el parámetro handler que se utiliza para enviar los resultados al ResponseHandler que describí anteriormente. El syncResult es en el parámetro fuera utilizado por el SyncAdapter hacer retardo exponencial, etc. Yo lo uso en el executeRequest(), incrementando sus varias cuentas de errores, etc. Una vez más, muy poco documentadas y una pita para conseguir trabajo. parseXML() aprovecha el excelente Simple XML lib.

public synchronized void processRequest(Account account, Bundle extras, String authority, ContentProviderClient provider, SyncResult syncResult, Handler handler, Context context) 
{ 
    // Helper to construct the query string from the query params passed in the extras Bundle. 
    HttpUriRequest request = createHTTPRequest(extras); 
    // Helper to perform the HTTP request using org.apache.http.impl.client.DefaultHttpClient. 
    InputStream instream = executeRequest(request, syncResult); 

    /* 
    * Process the result. 
    */ 
    if(extras.containsKey(WebAPIRequestHelper.REQUEST_GETBALANCE)) 
    { 
     GetServiceBalanceResponse xmlDoc = parseXML(GetServiceBalanceResponse.class, instream, syncResult); 
     Assert.assertNotNull(handler); 
     Message m = handler.obtainMessage(WebAPIClient.GET_BALANCE_RESPONSE, xmlDoc); 
     m.sendToTarget(); 
    } 
    else if(extras.containsKey(WebAPIRequestHelper.REQUEST_GETACCOUNTINFO)) 
    { 
     ... 
    } 
    ... 

} 

Usted debe poner un poco de los tiempos de espera en las peticiones HTTP por lo que la aplicación no espera siempre si los datos móviles se retira, o se cambia de Wifi a 3G. Esto provocará una excepción si se produce el tiempo de espera.

// Set the timeout in milliseconds until a connection is established. 
    int timeoutConnection = 30000; 
    HttpConnectionParams.setConnectionTimeout(httpParameters, timeoutConnection); 
    // Set the default socket timeout (SO_TIMEOUT) in milliseconds which is the timeout for waiting for data. 
    int timeoutSocket = 30000; 
    HttpConnectionParams.setSoTimeout(httpParameters, timeoutSocket); 
    HttpClient client = new DefaultHttpClient(httpParameters);   

Así que en general, el SyncAdapter y cuentas material era un dolor total y me costó mucho tiempo sin ganancia. ContentProvider fue bastante útil, principalmente para el soporte de cursor y transacción. La base de datos SQLite fue realmente buena. Y la clase Handler es increíble.Usaría la clase AsyncTask ahora en lugar de crear tus propios subprocesos como lo hice arriba para generar las solicitudes HTTP.

Espero que esta explicación divagante ayude a alguien un poco.

+0

Aprecio tus explicaciones. ¿Podría agregar más detalles de su interfaz * WebClient *? – JJD

+0

Actualizado con más detalles sobre el WebClient. –

+0

Gran respuesta, muy informativo. ¿Crees que las cosas han cambiado desde agosto de 2013? SyncAdapter se ajusta conceptualmente a mis necesidades muy bien. –

1

¿Qué hay del patrón de diseño Observer? ¿Puede su actividad ser un observador de SyncAdapter o de la base de datos? De esta forma, cuando falla una actualización, el adaptador notificará a sus observadores y podrá actuar sobre los datos que hayan cambiado. Hay un montón de clases observables en el SDK, vea cuál funciona mejor en su situación. http://developer.android.com/search.html#q=Observer&t=0

+0

Las clases de Android Observer parecen incompletas o parciales. Por ejemplo, DataSetObserver y ContentObserver reciben llamadas 'onChanged (boolean)', sin posibilidad de pasar información adicional sobre el cambio. Todo lo que puede hacer es llamar a ContentResolver.registerContentObserver() para un URI dado, que es a) demasiado grosero para mis propósitos yb) aplicable solo al ContentProvider, no al SyncAdapter (que no tiene tales métodos). Creo que implementaré mi propia solución en base a principios similares, pero permitiendo que se pasen params adicionales. –

Cuestiones relacionadas