简体   繁体   中英

How to return response to calling function? (Android-App calling Java REST-Server via JSON)

I'm rewriting some pieces of an Android App to call a REST-Server I've written in Java, using the Spring-Boot Framework.

To summarize, let's say in the app, function A calls a function B in my MainController (which is supposed to manage the Client-Server Communication). Function B calls (with a few extra steps) calls my Server. When function B makes the call, it uses an anonymous inner class overriding a method to define how to handle the response. I struggle to get that response from the anonymous inner class to function B so it can be returned to function A. Sadly, some asynchronity is involved, just to make things that exta bit more interesting.

My first attempt was to use a Map that stores the call result with a unique ID to make it accessible outside the inner class. Works fairly well in theory, but crumbles under asyncronity

Secondly I bluntly added a loop that is supposed to wait for the result to be put in the Map before continuing, making sure there is something to return.

Somehow, the loop isn't executed properly (I believe), and when I use the loop, the call to my server never finishes (which it does when there is no loop)

See the following code

Client: MainController

class MainController
{
    private final AtomicLong counter;
    private final HashMap<Long, Object> callResults;

    public MainController(){
        counter = new AtomicLong();
        callResults  = new HashMap<>();
    }

    public Login getLoginByUsername(String username)
    {
        long callID = counter.incrementAndGet();
        System.out.println("Start call");
        ServerConnection.get("myURL",null, new JsonHttpResponseHandler(){
            @Override
            public void onSuccess(int statusCode, Header[] headers, JSONObject response){
                try {
                    System.out.println("Call success, enter result:");
                    callResults.put(callID, new CallResult(Login.parseFromJson(response)));
                    System.out.println(callResults.get(callID));

                } catch (JSONException e) {
                    e.printStackTrace();
                }
            }
        });

        System.out.println("start waiting");
       while(callResults.get(callID) == null){
            System.out.print(".");
        }
        System.out.println("waiting over, return result:");
        System.out.println(callResults.get(callID));
        //return (Login) callResults.remove(callID).content;
        return null;
    }
}

Client: ServerConnection

import com.loopj.android.http.*;
import cz.msebera.android.httpclient.HttpEntity;

public class ServerConnection {

    private static final String BASE_URL = "http://1.3.3.7:8080/";

    private static AsyncHttpClient client = new AsyncHttpClient();

    public static void get(String url, RequestParams params, JsonHttpResponseHandler responseHandler) {
        client.get(BASE_URL+url, params, responseHandler);
    }
}

Here is what I know:

  1. the intended response will come eventually. When I don't use the loop to wait and just return null, the console will display the messages "Call success, enter result:" and an Object-ID

  2. When using the while-loop towards the end, it never prints "." - it just does nothing. I know the application is still running though, because AndroidStudio keeps updating me on space allocation

  3. Trying to apply an observer-pattern might is completely out of scope. I would have to dig really deep into client-sided code that I'd rather not touch. The code I was provided isn't exactly easy to work with to be honest

This is, what I assume is the problem: The loop prevents the asynchronous REST-call to be made, thus making it impossible to terminate.

My question: A) How can I return the Login-Object I received from the server to the function that called the MainController.getLoginByUsername-function in the first place?

B) What's going on with the while-loop I attempted to use for waiting? Why won't the body ( print("."); ) be executed, and more importantly, why does it stop the REST-call form being executed / finished?

(also feel free to leave feedback on my posting style, this is my first time asking a question here)

Edit: added question B, might provide an easier way to find a quick solution

Edit to UPDATE: Sorry for not making this clear enough: any suggestion that needs me to adapt the calling function as well (eg Observer-Patterns like Livedata or Rxjava or the otherwise really good suggestions provided by Leo Pelozo) are not feasible. I tested at least Leo Pelozos suggestion, and the underlying code simply doesn't allow that. I lose access to parameters and can't utilize function returns any more. I know, that reworking the underlying system would be the best course of action, but right now that's simply out of scope...

Have You used GSON by google, it converts JSON object to java object, its syntax is simple and you will love it, I use it all the time. And I use volley for HTTP request response. Just use GSON like

GSON gson = new GSON();
LoginObject login = new LoginObject();
login=gson.fromJson(responce,LoginObject.class);

and voila you are done.

If you don't want to use Livedata or Rxjava you could refactor that method to accept an JsonHttpResponseHandler or a custom callback, it's not pretty but it works:

1.

public void getLoginByUsername(String username, JsonHttpResponseHandler callback)
{
    long callID = counter.incrementAndGet();
    System.out.println("Start call");
    ServerConnection.get("myURL",null, callback);

}

and then calling it like this:

getLoginByUsername("myUsername", new JsonHttpResponseHandler(){
        @Override
        public void onSuccess(int statusCode, Header[] headers, JSONObject response){
            try {
                Login myLogin = Login.parseFromJson(response);
                //do stuff

            } catch (JSONException e) {
                e.printStackTrace();
            }
        }
    });

or

2.

class MainController {
    private final AtomicLong counter;
    private final HashMap<Long, Object> callResults;

    interface OnLogin{
        void onSuccessLogin(Login login);
    }

    public MainController() {
        counter = new AtomicLong();
        callResults = new HashMap<>();
    }

    public void getLoginByUsername(String username, OnLogin callback)
    {

        ServerConnection.get("myURL",null, new JsonHttpResponseHandler(){
            @Override
            public void onSuccess(int statusCode, Header[] headers, JSONObject response){
                try {
                    Login loginObject = Login.parseFromJson(response);
                    callback.onSuccessLogin(loginObject);

                } catch (JSONException e) {
                    e.printStackTrace();
                }
            }
        });

    }


}

and you call it like this:

    MainController controller = new MainController();
    controller.getLoginByUsername("myUsername", new MainController.OnLogin() {
        @Override
        public void onSuccessLogin(Login login) {
            //do whatever here
        }
    });

You can easily achieve this by using a CompletableFuture<?> . It works similarly to a Promise in JavaScript. If you require compatibility before API Level 24, check this answer here .

So your getLoginByUsername would look something like this:

public CompletableFuture<Login> loginByUsername(String username) {
    CompletableFuture<Login> promise = new CompletableFuture<Login>();

    System.out.println("Start call");
    ServerConnection.get("myURL",null, new JsonHttpResponseHandler(){
        @Override
        public void onSuccess(int statusCode, Header[] headers, JSONObject response){
            try {
                System.out.println("Call success, enter result:");
                promise.complete(Login.parseFromJson(response));

            } catch (JSONException e) {
                promise.completeExceptionally(e);
            }
        }
    });

    return promise;
}

And your call site something like this:

Login login = loginByUsername("user").get();
// Or wait a maximum time
Login login = loginByUsername("user").get(30, TimeUnit.SECONDS);

Never call get() on the UIThread. This will block the UI and provide a bad experience for the user

Thanks for all your help! I found a solution that works for my case.

First, as this is just a prototype, I don't really need my requests to be async, so I changed ServerConnection to use SyncHttpClient instead:

public class ServerConnection {

    private static final String BASE_URL = "http://10.203.58.11:8080/";

    private static SyncHttpClient client = new SyncHttpClient();

    public static void get(String url, RequestParams params, JsonHttpResponseHandler responseHandler) {
        client.get(getAbsoluteUrl(url), params, responseHandler);
    }
}

Then, Android won't allow me to perform requests on the main thread. This wasn't an issue with AsyncHttpClient , as the async-part is handled in a thread of its own. So I had to add threads to my requests. Thus, my MainController now looks something like this:

public class MainController
{
    private final AtomicLong counter;
    private final HashMap<Long, CallResult> callResults;

    public MainController(){
        counter = new AtomicLong();
        callResults  = new HashMap<>();
    }

public Login getLoginByUsername(String username)
    {
        long callID = counter.incrementAndGet();
        new Thread(new Runnable() {
            @Override
            public void run() {
                ServerConnection.get("person-login-relations?byUsername="+username,null, new JsonHttpResponseHandler(){
                    @Override
                    public void onSuccess(int statusCode, Header[] headers, JSONObject response){
                        try {
                            callResults.put(callID, new CallResult(Login.parseFromJson(response)));
                            System.out.println(callResults.get(callID));

                        } catch (JSONException e) {
                            e.printStackTrace();
                        }
                    }
                });


            }
        })   .start();

        while(callResults.get(callID) == null){
            //wait
        }
        return (Login) callResults.remove(callID).content;
    }
}

I guess the while-loop I use to wait now works because it's not blocking the thread that's responsible for performing and handling the request. Not entirely sure on that though.

Admittedly, this may not be the ideal solution, but it's the most feasible given my specific context.

I a few months, I will be tasked to rework the application I'm now extending to connect to a client. At that point I will make sure to implement most of your suggestions! So to anyone reading this solution: this right here is a workaround because I don't require asynchronous requests and I'm not supposed to change the functions calling the methods you see here! I suggest you still read the other replies as they are qualitatively superior!

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM