Skip to content

More realistic example

Ognyan Bankov edited this page Oct 25, 2016 · 11 revisions

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.

Prerequisites

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

Creating the data class

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.

Creating the forge unit

Our unit will reside in units/realistic. Create the dir.

Creating the resident

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().

Creating the interface

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.

Creating the resident component implementation

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

Creating the activity

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 with findViewById + setOnClickListener. We are using DebouncedOnClickListener 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 of getResident()

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

Wiring resident to the activity

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.

What is left?

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

Conclusion

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 use TaskExecutor or ForgeExchangeManager 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 trough ForgeExchangeManager. 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 and SideEffectOperationResidentComponent classes that provide ready-to-use functionality for single operation, multiple operations or operations with side effects. For example our ResRealisticImpl will be better implemented using AbstractOperationResidentComponent because it provides simplified and standard way for doing the work.
  • the global and common dialogs like DfCommProblem and DfCommWait will reside in dialogs/ directory not in the unit directory.
  • you will not use hardcoded full URL in the request

What's next?

Next tutorial is Creating side effect resident component