Как обработать RESTful-обновление удаленного сервера с помощью SyncAdapter - PullRequest
20 голосов
/ 04 ноября 2011

Я смотрел выступление Google I / O REST и читал слайды: http://www.google.com/events/io/2010/sessions/developing-RESTful-android-apps.html

Мне все еще немного неясно, как правильно обрабатывать, скажем, ошибку обновления, выданную пультом дистанционного управления.сервер.Я реализовал свой собственный ContentProvider и SyncAdapter.Рассмотрим следующий сценарий:

Обновление контактной информации пользователя с помощью вызова REST:

  1. Запрос обновления с использованием ContentResolver.
  2. Мой ContentProvider немедленно обновляет локальную базу данных Sqlite приложения и запрашивает синхронизацию (в соответствии с рекомендациями в докладе Google I / O).
  3. Мой SyncAdapter.onPerformSync ()вызывается и выполняет вызов REST для обновления удаленных данных.
  4. Удаленный сервер отвечает «ОШИБКА: неверный номер телефона» (например).

Мой вопросчто является лучшим способом для SyncAdapter сигнализировать моему ContentProvider, что это изменение необходимо сохранить из локальной базы данных приложения, а также сигнализировать моей активности, что запрос на обновление не выполнен (и передавать сообщения об ошибках, возвращаемые с сервера)?

Моя активность должна отображать счетчик хода выполнения при ожидании результата и знать, был ли запрос успешным или неудачным.


Для обновления локальной базы данных приложения с помощьюсодержимое с сервера, шаблон SyncAdapter имеет для меня полный смысл, и у меня это работает нормально.Но для обновлений с приложения на сервера, я не могу найти хороший способ справиться с вышеописанным сценарием.


И еще одна вещь...;)

Скажем, я вызываю ContentResolver.notifyChange (uri, null, true);изнутри метода update () моего ContentProvider.true вместе с android:supportsUploading="true" вызовет onPerformSync () моего SyncAdapter.Отлично, но внутри onPerformSync (), как мне узнать, какой URI мне следует синхронизировать?Я не хочу просто обновлять всю мою БД каждый раз, когда я получаю запрос синхронизации.Но вы даже не можете передать Bundle в notifyChangeCall () для передачи в onPerformSync ().

Все примеры, которые я видел в onPerformSync (), были настолько просты и не использовали пользовательскийContentProvider, есть ли примеры из реальной жизни?И документы немного птичьего гнезда.Вирджил Добжански, сэр, вы оставили меня в ручье без весла.

Ответы [ 2 ]

3 голосов
/ 18 августа 2012

Короткий ответ, если вы нацеливаетесь на уровень API 7, «Не надо».Ситуация, возможно, улучшилась в более поздних API, но как это было ... Я настоятельно рекомендовал бы полностью избегать SyncAdapter;он очень плохо документирован, и «автоматическое» управление учетными записями / аутентификацией обходится дорого, поскольку API для него также сложен и недостаточно документирован.Эта часть API не была продумана за пределами самых тривиальных вариантов использования.

Итак, вот схема, с которой я закончил.Внутри моей деятельности у меня был обработчик с простым добавлением из пользовательского суперкласса Handler (можно было проверить m_bStopped bool):

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) {
            ...
        } 
        ...
    }
}

Эта операция будет вызывать запросы REST, как показано ниже.Обратите внимание, что обработчик передается в класс WebClient (вспомогательный класс для создания / выполнения запросов HTTP и т. Д.).WebClient использует этот обработчик, когда он получает HTTP-ответ на сообщение обратно к действию и сообщает ему, что данные были получены и, в моем случае, сохранены в базе данных SQLite (что я бы порекомендовал).В большинстве видов деятельности я бы вызывал mHandler.stopHandler(); в onPause() и mHandler.startHandler(); в onResume(), чтобы избежать возврата HTTP-ответа на неактивное действие и т. Д. Это оказалось довольно надежным подходом.

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() вызывается в главном потоке.Таким образом, вы можете остановить свои диалоговые окна прогресса и безопасно выполнять другие действия.

Я объявил ContentProvider:

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

И реализовал его для создания и управления доступом к базе данных SQLite:

public class WebAPIProvider extends ContentProvider

Таким образом, вы можете получить курсоры над базой данных в своей деятельности следующим образом:

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

Я обнаружил, что класс org.apache.commons.lang3.text.StrSubstitutor очень полезен при построении неуклюжих запросов XML, требуемыхAPI REST, с которым мне пришлось интегрироваться, например, в WebAPIRequestHelper у меня были вспомогательные методы, такие как:

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);
}

, которые я бы добавил к соответствующему URL-адресу конечной точки.

Вот еще несколько подробностей о том, каккласс WebClient выполняет HTTP-запросы.Это метод processRequest(), вызываемый ранее в Runnable.Обратите внимание на параметр handler, который используется для отправки результатов обратно в ResponseHandler I, описанный выше.syncResult - это параметр out, используемый SyncAdapter для экспоненциального отката и т. Д. Я использую его в executeRequest(), увеличивая количество ошибок и т. Д. Опять же, очень плохо документировано и PITA работает.parseXML() использует превосходный 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))
    {
      ...
    }
    ...

}

Вы должны установить некоторые тайм-ауты для HTTP-запросов, чтобы приложение не ожидало вечно, если мобильные данные выпадают, илион переключается с Wi-Fi на 3G.Это вызовет исключение, если произойдет тайм-аут.

    // 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);          

Таким образом, в целом, вещи SyncAdapter и Accounts были полной болью и стоили мне много времени без всякой выгоды.ContentProvider был довольно полезен, главным образом для поддержки курсора и транзакции.База данных SQLite была действительно хороша.И класс Handler просто потрясающий.Я бы использовал класс AsyncTask сейчас вместо того, чтобы создавать свои собственные потоки, как я делал выше, для порождения HTTP-запросов.

Надеюсь, это бессвязное объяснение кому-нибудь немного поможет.

1 голос
/ 04 ноября 2011

А как насчет шаблона проектирования Observer?Может ли ваша деятельность быть наблюдателем SyncAdapter или базы данных?Таким образом, когда обновление завершится неудачно, адаптер уведомит своих наблюдателей, а затем сможет воздействовать на измененные данные.В SDK есть несколько классов Observable, посмотрите, какой из них лучше всего подходит для вашей ситуации.http://developer.android.com/search.html#q=Observer&t=0

...