-
Notifications
You must be signed in to change notification settings - Fork 0
More realistic example
In this example we will create an unit that executes HTTP request against a server, processes the result and present it to the user.
The URL of the server endpoint will be http://forge-samples.bolyartech.com/realistic.html. It is just plain text file that contains JSON encoded simple object with two fields example_int
and example_string
.
We will have a button 'Retrieve data' that will initiate the transfer, while the transfer is in progress we will show indeterminate progress dialog to indicate that we are busy. When there is a result from the request we will either show the data or show an error dialog if there was an error.
❗ In this example for the sake of simplicity we will not support rotation or other configuration changes.
For base project we will use Simplest forge app and we will build upon it.
Clone it with:
git clone [email protected]:ogrebgr/forge-android-examples-basic.git
and import it in Android Studio.
If you don't want to type the code go to the project dir and you can obtain the end result with:
git checkout more_realistic
The server endpoint http://forge-samples.bolyartech.com/realistic.html is returning
{"example_int":1440,"example_string":"Hello world"}
which is JSON encoded object. We will need a java class in order to transform and hold that information:
class ExampleData {
private final int mExampleInt;
private final String mExampleString;
public ExampleData(int exampleInt, String exampleString) {
this.mExampleInt = exampleInt;
this.mExampleString = exampleString;
}
static ExampleData fromJson(JSONObject json) throws JSONException {
return new ExampleData(
json.getInt("example_int"),
json.getString("example_string")
);
}
public int getExampleInt() {
return mExampleInt;
}
public String getExampleString() {
return mExampleString;
}
}
It is a plain java class, the only thing that is more interesting is the static factory method fromJson
which takes JSONObject as a parameter and returns new instance of ExampleData
.
Our unit will reside in units/realistic
. Create the dir.
We will have an activity with a button 'Retrieve data' which when clicked will tell the resident component to start a HTTP exchange so we will need a method called retrieveExampleData()
. Also we will need a method to get the example data when it is available so we will also have a method getExampleData()
.
As we mentioned earlier it is good practice to define the contract of the resident in a separate interface like this.
interface ResRealistic extends ResidentComponent {
void retrieveExampleData();
ExampleData getExampleData();
}
Having separate interface makes unit testing easier because it is easier to create mocks/fakes.
Full source is here. First we need to implement the interface:
class ResRealisticImpl extends ResidentComponentAdapter implements ResRealistic {
@Override
public void retrieveExampleData() {
}
@Override
public ExampleData getExampleData() {
}
}
In order to execute HTTP requests we will need a HTTP client and we will use the Forge's standard OkHttp (it is include in Forge-base so you don't have to touch your gradle):
class ResRealisticImpl extends ResidentComponentAdapter implements ResRealistic {
private final OkHttpClient mOkHttpClient = new OkHttpClient();
We will also need a field where we will keep the ExampleData
when it is available:
private ExampleData mExampleData;
Now we can implement the retrieveExampleData()
:
@Override
public void retrieveExampleData() {
Thread t = new Thread(new Runnable() { // (1)
@Override
public void run() {
mExampleData = null;
Request request = new Request.Builder() // (2)
.url("http://forge-samples.bolyartech.com/realistic.html")
.build();
try {
Response response = mOkHttpClient.newCall(request).execute(); // (3)
JSONObject json = new JSONObject(response.body().string()); // (4)
mExampleData = ExampleData.fromJson(json); // (5)
onData(); // (6)
} catch (JSONException | IOException e) {
onError(); // (7)
}
}
});
t.start(); // (8)
}
private synchronized void onError() {
}
private synchronized void onData() {
}
- (1) We create new thread because we don't want network operations to happen on the UI thread.
- (2) Building the HTTP request
- (3) Executing the request and getting response
- (4) Converting the raw response into JSONObject
- (5) Converting the JSONObject into our ExampleData object
- (6) If everything is OK we call
onData()
- (7) If there is an error we call
onError()
- (8) starting the thread
We have to implement also getExampleData()
:
@Override
public ExampleData getExampleData() {
return mExampleData;
}
Activity's full source is here. Layout XML is here
We will need 3 utility methods that will show/hide dialogs (bodies are skipped for brevity).
public void showCommWaitDialog() {}
public void hideCommWaitDialog() {}
public void showCommProblemDialog() {}
First two will be used to show/hide indeterminate progress dialog which will be shown while HTTP exchange is in progress in order to indicate to the user that we are busy.
The third will show a dialog with error message and it will be used if there is an error (e.g. network communication problem).
We will initialize our view the usual way:
private void initViews(View view) {
ViewUtils.initButton(view, R.id.btn_retrieve, new DebouncedOnClickListener() { // (1)
@Override
public void onDebouncedClick(View v) {
showCommWaitDialog(); // (2)
getRes().retrieveExampleData(); // (3)
}
});
mTvInt = ViewUtils.findTextViewX(view, R.id.tv_int);
mTvString = ViewUtils.findTextViewX(view, R.id.tv_string);
}
- (1) We initiate the button using
ViewUtils
which provides convenience methods for initializing view. You can achieve the same withfindViewById
+setOnClickListener
. We are usingDebouncedOnClickListener
which prevents unintentional double clicks - (2) When clicked we first show CommWait indeterminate progress dialog
- (3) and then tell the resident to retrieve the data.
getRes()
is shorthand alias ofgetResident()
We will need to define 2 additional methods that will be used by the resident to notify the activity for the result of the operation. First the "success" method which will be called if everything is OK:
void onExampleData() {
runOnUiThread(new Runnable() { // (1)
@Override
public void run() {
hideCommWaitDialog(); // (2)
ExampleData data = getRes().getExampleData(); // (3)
mTvInt.setText(getString(R.string.act__realistic__tv_int, data.getExampleInt())); // (4)
mTvString.setText(getString(R.string.act__realistic__tv_string, data.getExampleString()));
}
});
}
- (1) We run it on the UI thread because we will be manipulating views.
- (2) Hiding the CommWait dialog
- (3) Getting the data from the resident
- (4) Showing the data
The "error" method which will be called in case of some error:
void onError() {
runOnUiThread(new Runnable() { // (1)
@Override
public void run() {
hideCommWaitDialog(); // (2)
showCommProblemDialog(); // (3)
}
});
}
- (1) Again, we run on UI thread
- (2) Hiding the CommWait dialog
- (3) Showing dialog with error message
So far we have the resident and the activity but how we will notify the activity when the operation is complete?
First we need to define a field that will hold a reference to our activity:
private ActRealistic mActivity;
Then we will override Resident's lifecycle methods like this:
@Override
public synchronized void onActivityResumed(UnitActivity activity) {
super.onActivityResumed(activity);
mActivity = (ActRealistic) activity;
}
@Override
public synchronized void onActivityPaused() {
super.onActivityPaused();
mActivity = null;
}
What we do here is to record a reference to the activity when it resumed and to nullify it when it is paused. Nullifying the reference in onActivityPaused()
is very important because that way we prevent memory leaks.
Having the reference to the activity we may modify our onError()
and onData()
methods:
private synchronized void onError() {
if (mActivity != null) {
mActivity.onError();
}
}
private synchronized void onData() {
if (mActivity != null) {
mActivity.onExampleData();
}
}
Please note the synchronized
keyword used in the above 4 methods. It is needed because we are accessing mActivity
from different threads and we need it in order to have atomicity and visibility.
There are two pieces missing - dialog fragment classes. You can find them here: DfCommProblem, DfCommWait
You will have to edit the AndroidManifest.xml
and set ActRealistic
to be the launcher activity.
For the full source of the project please go to Prerequisites
Although called 'More realistic' please note that some of the approaches in this tutorial are not meant to be used in "real" applications. In real Forge applications:
- you will not create and start threads directly as in
retrieveExampleData()
but useTaskExecutor
orForgeExchangeManager
for executing HTTP exchanges. That way most of the complexity and the messiness of the thread will be handled for you by the framework. - in case of HTTP requests you will not use
OkHttpClient
directly but troughForgeExchangeManager
. That way you will: a) use the reliable and convenient infrastructure of Forge framework; b) in the unit tests will be able to substitute the actual HTTP client with mocked one and test your unit in isolation from the network/servers/etc. - you will not store reference to the whole Activity directly but to an interface that the Activity implements.
- you will use "standard"
AbstractOperationResidentComponent
,AbstractMultiOperationResidentComponent
andSideEffectOperationResidentComponent
classes that provide ready-to-use functionality for single operation, multiple operations or operations with side effects. For example ourResRealisticImpl
will be better implemented usingAbstractOperationResidentComponent
because it provides simplified and standard way for doing the work. - the global and common dialogs like
DfCommProblem
andDfCommWait
will reside indialogs/
directory not in the unit directory. - you will not use hardcoded full URL in the request
Next tutorial is Creating side effect resident component