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

发布于 2024-12-13 17:59:04 字数 1392 浏览 0 评论 0原文

我观看了 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 发出信号表明此更改需要从应用程序的本地数据库中撤回,并向我的 Activity 发出信号表明更新请求失败(并传递从服务器返回的错误消息)?

我的活动需要在等待结果时显示进度微调器,并了解请求是成功还是失败。


为了使用服务器中的内容更新本地应用程序数据库,SyncAdapter 模式对我来说完全有意义,而且我工作得很好。但是对于应用程序服务器的更新,我似乎找不到处理上述情况的好方法。


还有一件事...;)

假设我调用 ContentResolver.notifyChange(uri, null, true);从我的 ContentProvider 的 update() 方法中。 trueandroid:supportsUploading="true" 将导致我的 SyncAdapter 的 onPerformSync() 被调用。很好,但是在 onPerformSync() 内部,我如何知道应该同步哪个 URI?我不想每次收到同步请求时都简单地刷新整个数据库。但您甚至无法将 Bundle 传递到 notificationChangeCall() 中以传递给 onPerformSync()。

我见过的 onPerformSync() 的所有示例都非常简单,并且没有使用自定义 ContentProvider,有任何现实世界的示例吗?这些文档有点像鸟巢。 Virgil Doobjanschi,先生,您让我在没有桨的情况下留在小溪里。

I've watched the Google I/O REST talk and read the slides: http://www.google.com/events/io/2010/sessions/developing-RESTful-android-apps.html

I'm still a bit unclear on how to nicely handle, say, an update error thrown by the remote server. I have implemented my own ContentProvider and SyncAdapter. Consider this scenario:

Update a user's Contact Details via REST call:

  1. Request an update using a ContentResolver.
  2. My ContentProvider immediately updates the app's local Sqlite database and requests a Sync (as per recommendations in the Google I/O talk).
  3. My SyncAdapter.onPerformSync() is called and does a REST call to update the remote data.
  4. Remote server responds with "ERROR: Invalid Phone Number" (for instance).

My question is, what is the best way for the SyncAdapter to signal to my ContentProvider that this change needs to be backed out of the app's local database, and to also signal to my Activity that the update request failed (and pass the error messages returned from the Server)?

My activity needs to display a progress spinner while waiting for the result, and know whether the request succeeded or failed.


For updating the local app database with content from the Server, the SyncAdapter pattern makes complete sense to me, and I have that working fine. But for updates from the app to the server, I can't seem to find a nice way to handle the above scenario.


And another thing... ;)

Say I call ContentResolver.notifyChange(uri, null, true); from within my ContentProvider's update() method. true along with android:supportsUploading="true" will cause my SyncAdapter's onPerformSync() to be called. Great, but inside onPerformSync(), how do I tell what URI I should sync? I don't want to simply refresh my entire DB every time I get a Sync request. But you can't even pass a Bundle into the notifyChangeCall() to be passed on to onPerformSync().

All the examples I've seen of onPerformSync() have been so simple, and not using a custom ContentProvider, any real world examples out there? And the docs are a bit of a bird's nest. Virgil Dobjanschi, Sir, you've left me up the creek without a paddle.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

呢古 2024-12-20 17:59:04

如果您的目标是 API 级别 7,简短的回答是“不要”。这种情况在后来的 API 中可能有所改善,但事实上……我强烈建议完全避免使用 SyncAdapter;它的记录非常糟糕,并且“自动”帐户/身份验证管理的代价很高,因为它的 API 也很复杂且记录不足。除了最琐碎的用例之外,API 的这一部分还没有经过深思熟虑。

这就是我最终采用的模式。在我的活动中,我有一个处理程序,其中包含来自自定义处理程序超类的简单添加(可以检查 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 数据库中(我建议这样做)。在大多数 Activity 中,我会在 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() 在主线程上调用。因此,您可以在此处停止进度对话框并安全地执行其他活动操作。

我声明了一个 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 用来执行指数退避等的 out 参数。我在 executeRequest() 中使用它,增加它的各种错误计数等。同样,非常糟糕记录并获得 PITA 才能开始工作。 parseXML() 利用了出色的简单 XML 库

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 请求。

我希望这个漫无目的的解释对某人有一点帮助。

Short answer, if you're targeting ~API level 7, is "Don't". The situation may have improved in later APIs but as it was... I would strongly recommend avoiding SyncAdapter completely; it's documented very poorly and the "automatic" account/authentication management comes at a high price as the API for it is also convoluted and under-documented. This part of the API has not been thought through beyond the most trivial use cases.

So here's the pattern I ended up going with. Inside my activities I had a handler with a simple addition from a custom Handler superclass (could check for a 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) {
            ...
        } 
        ...
    }
}

The activity would invoke the REST requests as shown below. Notice, the handler is passed through to the WebClient class (a helper class for building/making the HTTP requests etc.). The WebClient uses this handler when it receives the HTTP response to message back to the activity and let it know the data has been received and, in my case, stored in an SQLite database (which I would recommend). In most Activities, I would call mHandler.stopHandler(); in onPause() and mHandler.startHandler(); in onResume() to avoid the HTTP response being signalled back to an inactive Activity etc. This turned out to be quite a robust approach.

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() is invoked on the main thread. So you can stop your progress dialogs here and do other Activity stuff safely.

I declared a ContentProvider:

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

And implemented it to create and manage access to the SQLite db:

public class WebAPIProvider extends ContentProvider

So you can then get cursors over the database in your Activities like this:

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

I found the org.apache.commons.lang3.text.StrSubstitutor class to be immensely helpful in constructing the clumsy XML requests required by the REST API I had to integrate with e.g. in WebAPIRequestHelper I had helper methods like:

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

Which I would append to the appropriate endpoint URL.

Here's some more details on how the WebClient class does the HTTP requests. This is the processRequest() method called earlier in the Runnable. Notice the handler parameter which is used to message the results back to the ResponseHandler I described above. The syncResult is in out parameter used by the SyncAdapter to do exponential backoff etc. I use it in the executeRequest(), incrementing it's various error counts etc. Again, very poorly documented and a PITA to get working. parseXML() leverages the superb 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))
    {
      ...
    }
    ...

}

You should put some timeouts on the HTTP requests so the app doesn't wait forever if the mobile data drops out, or it switches from Wifi to 3G. This will cause an exception to be thrown if the timeout occurs.

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

So overall, the SyncAdapter and Accounts stuff was a total pain and cost me a lot of time for no gain. The ContentProvider was fairly useful, mainly for the cursor and transaction support. The SQLite database was really good. And the Handler class is awesome. I would use the AsyncTask class now instead of creating your own Threads like I did above to spawn the HTTP requests.

I hope this rambling explanation helps someone a bit.

把昨日还给我 2024-12-20 17:59:04

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

What about the Observer design pattern? Can your activity be an observer of the SyncAdapter or database? That way when an update fails, the adapter will notify its observers and can then act on the data that changed. There are a bunch of Observable classes in the SDK, see which one works best in your situation. http://developer.android.com/search.html#q=Observer&t=0

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文