How to handle RESTful update of remote server with SyncAdapter

后端 未结 2 1498
花落未央
花落未央 2021-01-31 10:29

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 o

相关标签:
2条回答
  • 2021-01-31 10:51

    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

    0 讨论(0)
  • 2021-01-31 11:13

    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.

    0 讨论(0)
提交回复
热议问题