ActionChain
A substitute for AsyncTask
. Also a "variant" of JDeferred
where you could easily specify the thread to run actions on.
For type-safe ActionChains, please refer to TActionChain class, and TActionChainTest
Sample Code :)
Example 0:
The original AsyncTask version of code was created by Graham Smith on Stack Overflow.
public class AsyncTaskActivity extends Activity implements OnClickListener {
Button btn;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
btn = (Button) findViewById(R.id.button1);
btn.setOnClickListener(new View.OnClickListener()
{
@Override
public void onClick(View view)
{
new LongOperation().execute("");
}
});
}
private class LongOperation extends AsyncTask<String, Void, String> {
@Override
protected String doInBackground(String... params) {
// TODO: replace this with real network, blocking actions
for (int i = 0; i < 5; i++) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.interrupted();
}
}
return "Executed";
}
@Override
protected void onPostExecute(String result) {
TextView txt = (TextView) findViewById(R.id.output);
txt.setText("Executed"); // txt.setText(result);
// might want to change "executed" for the returned string passed
// into onPostExecute() but that is upto you
}
@Override
protected void onPreExecute() {}
@Override
protected void onProgressUpdate(Void... values) {}
}
}
The basic idea is to avoid instantiating abstract classes, which are too lengthy code. And the tool to partially accomplish this goal is the new feature brought in Java 8 -- lambda. (In Java 7, we could get lambda support using RetroLambda.)
Basically as long as the abstract class has only one abstract method it could be shortened like this:
The original code:
btn = (Button) findViewById(R.id.button1);
btn.setOnClickListener(new View.OnClickListener()
{
@Override
public void onClick(View view)
{
new LongOperation().execute("");
}
});
The simplified code using Java 8 or Retrolambda:
btn = (Button) findViewById(R.id.button1);
btn.setOnClickListener(view -> new LongOperation().execute(""));
This convertion is available for .setOnClickListener
because the abstract class only needs one function. However AsyncTask could not be directly shortened as a simple Lambda because it needs 4 functions, all of which are important and cannot be ignored, too.
Therefore we need a new interface similar to AsyncTask
that helps building something similar while enabling developers to use lambdas. And the new interface, ActionChain
looks like this:
public class AsyncTaskActivity extends Activity implements OnClickListener {
Button btn;
// create a ActionChain factory using Android API (runOnUiThread) and 2 extra worker threads.
ActionChainFactory chainFactory = new ActionChainFactory(uiCode -> runOnUiThread(uiCode), Executors.newFixedThreadPool(2));
TextView txt;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
txt = (TextView) findViewById(R.id.output);
btn = (Button) findViewById(R.id.button1);
btn.setOnClickListener(view -> onBtnClick());
}
void onBtnClick() {
// The following code is using ActionChain
// The ").xxx" style may look strange but that's the only way to obey most IDEs' indentation rule.
chainFactory.get(
).uiConsume(obj -> { // equivalent to onPreExecute, could be deleted
}).netThen(obj -> { // equivalent to doInBackground
// TODO: replace this with real network, blocking actions
for(int i = 0; i < 5; i++) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.interrupted();
}
}
return "Executed";
}).uiConsume((String result) -> txt.setText(result) // equivalent to onPostExecute
).start();
}
}
The different between "xxConsume" and "xxThen" is that "Consume" will not return new values to the chain and set the chain's inner value to "null" but "Then" will return a value and pass that value to inner chain value, so the next "Then" or "Consume" will receive that value as input parameter.
Example 1:
Realm
database caused some trouble for us because it's required to be run on the same thread and defaults to run on UI thread, which might affect user experience. But ActionChain
enables you to restrict Realm
to run only on worker threads, in a simple yet safe way:
Realm
Encapsulation of /**
* Created on 3/14/16
*
* Note: PureAction<I,O> is interface for a function that takes an argument of type I and returns an object of type O
* ReadOnlyChain is the object representing a launched ActionChain.
* (ActionChain is not launched until calling ".start()")
*/
public class RealmAccess implements PureAction<PureAction<Realm, ?>, ReadOnlyChain> {
@Inject
ActionChainFactory chainFactory;
@Inject
Context context;
@Override
public ReadOnlyChain process(PureAction<Realm, ?> editor) throws Exception {
return chainFactory.get(fail -> fail.getCause().printStackTrace()
).netThen(() -> {
Realm realm = Realm.getInstance(new RealmConfiguration.Builder(context)
.name(context.getString(R.string.realm_filename))
.deleteRealmIfMigrationNeeded()
.build());
Object result = editor.process(realm);
realm.close();
return result;
}).start();
}
}
ReamAccess
Usage of Example 1.1:
// Retrieve current user
chainFactory.get(fail -> fail.getCause().printStackTrace()
).netThen(() -> realmAccess.process(realm -> {
return realm.where(Person.class)
.equalTo("personId", userStore.getMostRecentUserId())
.findFirst().getName();
})).uiConsume((String name) -> {
mUserName.setText(name);
}).start();
Example 1.2:
chain.netThen((String userName) -> {
Response<ResponseBody> response = service.getCurrentPerson().execute();
Person person = new Person();
if (response.code() != 200)
throw new IOException(response.message());
ResponseBody responseBody = response.body();
JSONObject jsonObject = new JSONObject(responseBody.string());
String ourUserID = jsonObject.getString("_id");
// Set user ID in preferences
userStore.setUserId(ourUserID);
Photo photo = new Photo();
photo.setPhotoUrl(jsonObject.getString("avatarUrl"));
photo.setType(Photo.TYPE_AVATAR);
person.setName(userName);
person.setAvatar(photo);
person.setFacebookId(jsonObject.getString("facebookId"));
person.setCreatedAt(DateTimeConverter.toDate(jsonObject.getString("createdAt")));
person.setPersonId(ourUserID);
Log.d("UpUserInfo", "A" + person);
return person;
}).netThen((Person newPerson) -> realmAccess.process(realm -> {
try {
Log.d("UpUserInfo", newPerson == null ? "null" : newPerson.toString());
// Set user details in database
realm.beginTransaction();
Person result = realm.where(Person.class)
.equalTo("personId", newPerson.getPersonId())
.findFirst();
if (result != null)
result.removeFromRealm();
realm.copyToRealm(newPerson);
realm.commitTransaction();
// MAYBE NOT NEEDED: bus.post(new UserInfoUpdatedEvent(result));
return newPerson;
} catch (Exception err) {
// Maybe errorHolder.retry() ?
if (realm.isInTransaction())
realm.cancelTransaction();
err.printStackTrace();
throw err;
}
}));
Example 2
Android application, used with retrolambda
Untangled version:
- Althought the event bus version also eliminates callback hell,
AsyncTask
forces developers to think in a un-natural way, where:- UI code and other operations cannot be mixed together. (Otherwise the code will contain lots of small pieces of
AsyncTask
s) - It's not straightforward to understand the sequence of the Tasks
- UI code and other operations cannot be mixed together. (Otherwise the code will contain lots of small pieces of
- ActionChain Bonus:
- Once-for-all exception handling for all the threads, as long as they are in the same task.
Diagram illustration
We turn this tangled thought: #### into this linear, straightforward one:
Documentations
Please visit this github.io website.
It's recommended to begin by reading the documentations about ChainStyle
and ErrorHolder
.
Get Started
Thanks to JitPack, we could all import this library using standard syntax!
For gradle:
1. Add the JitPack repository (please edit /build.gradle)
allprojects {
repositories {
...
maven { url "https://jitpack.io" }
}
}
2. Add the dependency (please edit /<module name>/build.gradle)
dependencies {
compile 'com.github.C4Phone:SmartActionChain:v0.3'
}
For Maven:
1. Add the JitPack repository
<repositories>
<repository>
<id>jitpack.io</id>
<url>https://jitpack.io</url>
</repository>
</repositories>
2. Add the dependency
<dependency>
<groupId>com.github.C4Phone</groupId>
<artifactId>SmartActionChain</artifactId>
<version>v0.3</version>
</dependency>