如何使用SyncAdapter处理远程服务器的RESTful更新?

21
我观看了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.远程服务器返回“ERROR:Invalid Phone Number”(例如)。
我的问题是,SyncAdapter向我的ContentProvider发出信号,表示此更改需要从应用程序的本地数据库中撤消,并向我的Activity发出信号,表示更新请求失败(并传递从服务器返回的错误消息)最佳方法是什么? 我的活动需要在等待结果时显示进度微调器,并知道请求成功还是失败。
对于从服务器更新本地应用程序数据库中的内容,SyncAdapter模式对我来说完全有意义,并且我已经很好地工作。 但是对于从应用程序到服务器的更新,我似乎找不到优雅处理上述情况的方法。
还有一件事...... ;)假设我从ContentProvider的update()方法中调用ContentResolver.notifyChange(uri, null, true)。使用android:supportsUploading="true",将使得SyncAdapter的onPerformSync()被调用。很好,但在onPerformSync()内部,我该如何知道应该同步哪个URI?我不想每次收到同步请求时都刷新整个数据库。但你甚至不能将Bundle传递给notifyChangeCall(),以便传递给onPerformSync()。
我看到的所有onPerformSync()的示例都非常简单,并且没有使用自定义ContentProvider,有现实世界的示例吗?文档有些混乱。Virgil Dobjanschi先生,您让我不知所措。

我正在与一个类似的设置(https://dev59.com/kWfWa4cB1Zd3GeqPeCWa)苦苦挣扎。也许你可以帮忙。 - JJD
2个回答

3
简短的回答是,如果你正在针对 ~API 级别 7,那么请不要使用 SyncAdapter。尽管后续 API 的情况可能已经得到改善,但我强烈建议完全避免使用 SyncAdapter;它的文档非常差,而且“自动”帐户/身份验证管理的代价很高,因为它的 API 也很复杂,并且缺乏充分的文档支持。这部分 API 没有超出最简单的使用案例进行思考。
因此,这里是我最终采用的模式。在我的活动中,我使用一个处理程序和一个简单的添加操作,来自于一个自定义的处理程序超类(可以检查一个名为 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) {
            ...
        } 
        ...
    }
}

活动将调用如下所示的REST请求。请注意,处理程序通过WebClient类传递(WebClient是一个帮助构建/制作HTTP请求等的辅助类)。当WebClient接收到HTTP响应以向活动发送消息并让其知道数据已被接收并在我的情况下存储在SQLite数据库中(我建议这样做)时,WebClient使用此处理程序。在大多数活动中,我会在onPause()中调用mHandler.stopHandler(),并在onResume()中调用mHandler.startHandler(),以避免将HTTP响应返回给非活动状态的Activity等。这证明是一种相当健壮的方法。
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()被调用在主线程上。因此您可以在这里停止进度对话框,安全地执行其他Activity操作。

我声明了一个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类能够非常有用的帮助构建REST API所需的笨拙XML请求,例如在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请求的更多详细信息。这是在Runnable中先前调用的processRequest()方法。请注意handler参数,它用于将结果消息传递回上面描述的ResponseHandlersyncResult是一个输出参数,由SyncAdapter用于指数退避等操作。我在executeRequest()中使用它,增加各种错误计数等。再次强调,文档非常差,让人感到很麻烦。 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请求中设置一些超时时间,以防移动数据掉线或从Wifi切换到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请求。

希望这篇冗长的解释能对某些人有所帮助。


我很感谢您的解释。您能否进一步添加有关WebClient接口的详细信息? - JJD
更新了WebClient的更多细节。 - Jarrod Smith
非常好的答案,非常有信息量。您认为自2013年8月以来情况是否发生了变化?SyncAdapter在概念上非常适合我的需求。 - Michael Kariv
抱歉,我不知道,很遗憾我没有重新访问SyncAdapter的原因。 - Jarrod Smith
请注意,如果天真地使用AsyncTask,它会有自己的问题:在不同的操作系统级别上执行操作大小不同,引用丢失等。 - android.weasel

1
观察者设计模式怎么样?你的活动可以成为SyncAdapter或数据库的观察者吗?这样,当更新失败时,适配器将通知其观察者,然后可以对更改的数据进行操作。SDK中有许多可观察类,请查看哪个在您的情况下最有效。http://developer.android.com/search.html#q=Observer&t=0

Android的Observer类似乎都不完整或部分。例如,DataSetObserver和ContentObserver都接收onChanged(boolean)调用,没有任何传递有关更改的其他信息的设施。你所能做的就是为给定的URI调用ContentResolver.registerContentObserver(),这对我的目的来说太粗糙了,并且仅适用于ContentProvider,而不适用于SyncAdapter(其没有此类方法)。我认为我会基于类似原则的自己的解决方案,但允许传递附加参数。 - Jarrod Smith

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接