質問

Some preliminary information:

I have to create an API for an Android Library. This API has to deal with basic CRUD (find, find all, insert, update, delete) operations of generic data. Then other apps that use this library can use this API with any data/object they want. However, this data is stored in a backend server, where it's stored and used by all users that have the same app in different devices (basically a BaaS). The apps should be able to work without an internet connection. So checked out a video Google I/O made about how to design REST clients in android and followed it.

Thus now:

  • When inserting/updating/deleting data, the API uses a local Content Provider and sets specific flags to the data.
  • I have a SyncAdapter running, which checks all data in the sqlite in each run.
  • It checks the flag the data has (if it has been inserted, updated or deleted), and calls a REST API to sincronize it with the server

However, I want the apps to be able to pass a callback to this sincronization. Basically, what I want is, when the app calls the "insert" method from my API, it should pass it a callback. This callback should do 2 things:

  • Define what to do in case the insert failed
  • Define what to do in case the insert succeeded.

This is an example of what I'd want to do:

GenericData data = new GenericData();
//Initialize data
API.insert(GenericData.class, data, new CallBack<GenericData>(){

    @Override
    public void onSuccess(GenericData insertedData){
        //Data inserted and syncronized successfully
    }

    @Override
    public void onFailure(){
        //Data failed to be inserted an/or sincronized
    }
});

This callback, as an annonymous class, would be called from an AsyncTask in the app. What I want to do, is call callBack.onSuccess(data) from the SyncAdapter if the sync is done with success, or call callBack.onFailure() from the SyncAdapter if the sync failed.

The Callback class would thus look something like this:

//Concrete empty methods are set so it's not necessary to implement all these methods
public abstract class Callback<T>{
    public void onSuccess(T insertedData){}
    public void onFailure(){}
}

What I had in mind was this:

Create a BroadcastReceiver that will handle the "insert" callbacks. This receiver will be listening for specific Intents.

When the SyncAdapter finished syncing a row of inserted data, it will create a specific Intent and broadcast it. This Intent would have, as additional data, the id of the row processed, and the status of the sync (if it succeeded or if it failed). When the receiver is called, it gets the id of the row from the intent, gets the specific callback for that id, and depending on the status of the sync calls callBack.onSuccess(data) or callBack.onFailure() .

My problem comes from determining which callback to call. Each "API.insert(..)" method is passed a Callback subclass, which is an annonymous class. I don't know how to serialize it, or "store" it with the specific id of the row. If it could be serialized somehow, then the BroadcastReceiver would just do Callback callback = lookup(id) and get the callback associated to that id. However I don't know if this is possible, because I tried serializing the callback and it didn't work. I think the problem is that the Activity itself (where the insert method is called from) is not serializable itself, so the callback can't be serialized either.

I thought about maybe not using an annonymous class, but use a named Callback class instead. But then again, if I allow to pass a Callback object in the method, then you can still pass an annonymous class to it. Thus any app that does exactly that will have errors. So the callbacks should work even if they are passed as annonymous classes, unless there is an alternative for that situation above.

Also, I'd like it if it was an annonymous class inside the Activity, so the callback can use variables defined in the activity. For example, to insert 2 different objects in a row (the callback would use a OtherData data object defined in the activity). If it can somehow be done, then if the Activity is still up and running, the callback should use that same Activity object. But if the Activity is closed, somehow serialize it, so when the callback is called later it uses the variables/data from the Activity right before it was cloaed/destroyed

Any ideas?

P.S: Also, like I said the callbacks and the data should be generic, so the method for calling the callback should support any possible type. I guess this won't be a problem anyways, for example using a wildcard Callback<?> callback = lookupCallback(id).


Update:

Okay, I think I have a solution, perhaps.

Then main problem I had, was that I needed the Callback to be an annonymous class and serializable at the same time, and that is not possible. If it is serializable, but not an annoynmous class, I can't use variables/attributes from the Activity that calls it (which is necessary to process those callbacks). But if it is an annonymous class, but not serializable, then I can't serialize the callback for it to be deserialized and called when the SyncAdapter finishes syncing.

So I thought maybe I could do it like this, to include the best of both worlds:

This would be the CallBack class:

//Concrete empty methods are set so it's not necessary to implement all these methods
public abstract class Callback<T> implements Serializable{
    public void onSuccess(T insertedData){}
    public void onFailure(){}
}

Every callback should be a Named class (external class, or static inner class, etc), and in its creator pass an activity. Then you can have any field you want taken from that activity that you want.

I think this would be better shown with examples, so I'll try including one: "I create a User and an Address that has that user. I want to insert the User, then insert the address when I know that user was inserted".

In this case, I think I'd have a callback like this:

public class UserInsertedCallback extends Callback<User> implements Serializable{
    //Here goes serialId
    private Address address;

    public UserInsertedCallback(UserActivity activity){
        address = activity.getAddress();
    }

    @Override
    public void onSuccess(User insertedUser){
        //This is another callback we may want to use
        Callback<Address> callback = createCallback();
        //I create Foreign Key in Address referencing the user
        address.setUserId(insertedUser.getId());
        API.insert(Address.class, address, callback); 
    }
}

Now this would be the activity:

public class UserActivity extends Activity{
    private Address address;
    ....
    public Address getAddress(){return address;}

    private class TestTask extends AsyncTask<Void,Void,Void>{
       @Override
       protected Void doInBackground(Void... void){
            User user = ... //create user
            address = ... //create address
            API.insert(User.class, user, new UserInsertedCallback(UserActivity.this));
        }
    }
}

My API method would serialize UserInsertedCallback and insert it into a database table that can be easily retrieved. There is no problem here, assuming Address is serializable. Even if it's not, the developer would just include serializable objects/primitives in UserInsertedCallback and could create the Address object again in onSuccess(). With the callback now serialized, whenever the SyncAdapter finishes inserting the User successfully, it can obtain the serialized callback from the database, deserialize it, and call "onSuccess(insertedUser)" from it.

If the Address object is the only thing needed, then the constructor of UserInsertedCallback could have taken it instead. But perhaps the developer would need other stuff from the Activity as well, so they have the ability to pass the activity as well.

Granted, I'm sure it's not as easy as that, but I will soon try to see if this works. I still don't know how to prevent people from passing the callback as an annonymous class though

役に立ちましたか?

解決

So I was finally able to get this going on.
It worked like I put it in my Edit, but I'll give some more explanations:

Basically, the Callback class is similar to how I defined it above, except I changed the methods a little bit:

public abstract class Callback<T> implements Serializable{
    private static final long serialVersionID = ...; //version Id goes here
    public void onSuccess(T insertedData){}
    public void onFailure(T failedData, CustomException ex){}
}

I pass more info to the onFailure event, such as the data that failed the synchronization, and a custom exception thrown by the synchronization (since the synchronization can fail for a lot of reasons).

Then each app can create a named class that extends Callback<T>, and can have any field it wants (as long as it's serializable), just like I mentioned in the Edit. My API call takes this callback as a parameter, serializes it, and stores it in the database alongside with the _ID of the data to synchronize.
Later on, my SyncAdapter takes that data and tries to synchronize it with the server. If a fatal error occurs (one it can't recover from and try again later for instance), it reverts the data back to its original state and sends a broadcast message passing the serialized data, a serialized exception (both via JSON), the _ID field, and passing a parameter stating the synchronization failed.
Then I set up a BroadcastReceiver who listens for this broadcast, gets all this information and starts a Service with the same extras.
This new service gets the _ID field, looks up the serialized callback from the database, deserializes it, deserializes the data and the custom exception, and calls callback.onFailure(data, ex), and it works pretty well!
Once the callback is finished being called, it is deleted from the database. All of this (except defining the extended callback class and calling the API) is done in the Android library itself.

P.S: Actually, the data is in JSON format and de/serialized with GSON. To do this I added a Class<T> data_class; field to Callback<T>, and a onFailureFromJson(String json, CustomException ex) final method which deserializes the JSON into an object of type T, and calls onFailure(entity,ex) with it. Thus the service calls this onFailureFromJson method instead.
Here's the final Callback<T> class:

public abstract class Callback<T> implements Serializable{
    private static final long serialVersionID = ...; //version Id goes here
    private Class<T> data_class;

    public Callback(Class<T> data_class){this.data_class = data_class;}
    public abstract void onSuccess(T insertedData){}
    public abstract void onFailure(T failedData, CustomException ex){}

    public final void onSuccessFromJson(String json){
        Gson gson = new GsonBuilder().create();
        T entity = gson.fromJson(json,data_class);
        onSuccess(entity);
    }
    public final void onFailureFromJson(String json, CustonException ex){
        Gson gson = new GsonBuilder().create();
        T entity = gson.fromJson(json,data_class);
        onFailure(entity,ex);    
    }    
}

他のヒント

although there might be better ways i'm not aware of, a working technique

public void onPerformSync(Account account, Bundle extras, String authority, ContentProviderClient provider, SyncResult syncResult) {

   //do your CURD actions
   boolean isSuccess = false; //according to your operation action set this to true or false

   if(isSuccess){
        new ExtendedCallback.onSuccess(); 
        //ExtendedCallback is the extension of Callback class to suit your requirement
        //of course yu would have to define this new class class and send it to you SyncAdapter class before using
   }
}
ライセンス: CC-BY-SA帰属
所属していません StackOverflow
scroll top