2

I am trying to pass a string from AsyncTask back to my Activity. Browsing through other similar questions (eg. here), I decided to use an interface listener.

Interface:

package com.example.myapplication;

public interface GetListener {
    void passJSONGet(String s);
}

AsyncTask class:

package com.example.myapplication;

import android.content.Context;
import android.os.AsyncTask;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLEncoder;


public class DataGetter extends AsyncTask<String, Void, String> {

    Context context;
    private GetListener GetListener;
    public String jsonString;

    DataGetter(Context ctx, GetListener listener) {
        this.context = ctx;
        this.GetListener = listener;
    }

    @Override
    protected String doInBackground(String... params) {
        String ip = params[0];
        String scriptname = params[1];
        String db = params[2];
        String urladress = "http://" + ip + "/"+ scriptname +".php";

        try {
            URL url = new URL(urladress);
            HttpURLConnection connection = (HttpURLConnection) url.openConnection();
            connection.setDoInput(true);
            connection.setDoOutput(true);

            OutputStream os = connection.getOutputStream();
            OutputStreamWriter writer = new OutputStreamWriter(os, "UTF-8");

            String data = URLEncoder.encode("database", "UTF-8") + "=" + URLEncoder.encode(db, "UTF-8");

            writer.write(data);
            writer.flush();
            writer.close();

            BufferedReader reader = new BufferedReader(new
                    InputStreamReader(connection.getInputStream()));
            StringBuilder sb = new StringBuilder();

            String line = null;
            while ((line = reader.readLine()) != null) {
                sb.append(line);
                break;
            }
            return sb.toString();
        } catch (Exception e) {
            return e.getMessage();
        }
    }

    @Override
    protected void onPostExecute(String result) {
        GetListener.passJSONGet(result);
    }
}

My Activity:

import android.os.Bundle;
import android.widget.TextView;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
import org.json.JSONArray;
import org.json.JSONException;


public class WeatherDisplay extends AppCompatActivity implements GetListener {

    private JSONArray jsonArray;
    private String jsonString;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        
       // other code
      
        DataGetter dataGetter = new DataGetter(this, WeatherDisplay.this);
        dataGetter.execute(ip, "readLatestOutside", "weather");
        Toast.makeText(this, this.jsonString, Toast.LENGTH_LONG).show();
        this.jsonArray = new JSONArray(this.jsonString);

       // furter code
}

    @Override
    public void passJSONGet(String jsonstring) {
        this.jsonString = jsonstring;
    }

The JSON is properly get from server and is seen in onPostExecute normally, however, it isn't visible later on in WeatherDisplay (the Toast is displayed empty, variable is still a nullpointer).

How do I resolve this issue? I am inexperienced and might've missed some trivial stuff.

Filip Frey
  • 57
  • 6
  • 3
    Your ``onCreate`` code creates the ``AsyncTask``, sets it running in the background, and immediately jumps to the next line and displays the ``Toast`` - which you're not ready for yet! Later the task finishes, runs ``onPostExecute`` which basically ends up setting your variable in the ``Activity``, and that's all it does. You should create the toast in ``passJSONGet`` since that's where you actually receive the info you want to display – cactustictacs Aug 23 '20 at 22:30
  • Just in case you're not aware, code usually goes line by line, and if you went fetching some data you'd have to wait for the result to come back before you could move on (it would **block** the thread). That's called _synchronous execution_, it's all happening together. The opposite is _asynchronous execution_ which usually gets called _async_, which is basically "stuff can run and finish at a different time" so you can move on and do other things. ``AsyncTask``s are a way to do some stuff in the background like that - you set them off and later they come back with the results and do whatever – cactustictacs Aug 23 '20 at 22:36
  • @cactustictacs I appended `passJSONGet` to set true a bool flag in `WeatherDisplay` ctivity, yeat a 'doing-nothing' while loop on that flag crashes te app. How could I propely wait for the `AsyncTask` to return the string so that I could convert it into a `JSON array` (as per code, ignoring the `Toast` for now)? – Filip Frey Aug 24 '20 at 16:32
  • Ok I added an answer with a bunch of background to help you understand what's going on in general, but you shouldn't be waiting for a result at all - your code is meant to react to the ``AsyncTask`` completing, by running whatever is in ``onPostExecute``. That's where you do whatever you need to do with the result. Have a look, hope it helps! – cactustictacs Aug 24 '20 at 20:03

2 Answers2

1

I think a little explanation about how Android runs your code might help.

So in the background, Android is running some code in an endless loop. Part of that loop is to check a message queue, which is basically where tasks get delivered - code it needs to execute. Ideally it will get all the work required in each loop finished quickly enough that it can manage 60 loops per second. If it has too much to do, that's where it starts to slow down and feel janky.

Some of the tasks that might get executed are things like an Activity being created, so it might want the system to run its onCreate code - which happens once for each Activity. You've probably noticed it only happens once, and that's where you've put your endless loop, right? To kind of trap the execution in there, until the AsyncTask delivers its result?

The problem is you're stopping the main loop from working - it can't move on and do anything until it's finished that task, and you're blocking it on purpose. That's very bad in general, and it's why you're encouraged not to do slow operations on that main thread (the one that runs the main looper) - it creates work that takes too long, and makes the whole UI less responsive (UI handling is also tasks that need to run)

So that's bad in general, but the way AsyncTasks actually work is they run on another thread (so they're not blocking the main one, it's like having another independent person working on stuff) - but they deliver the result on the main thread. They post a message to the main thread's message queue, telling it to run the onPostExecute code.

But the system can't get to that message until it's handled the earlier tasks in the queue. And if it's busy running an endless loop waiting for the result of the AsyncTask, it will never get to that message, the variable will never be updated, and the loop will never see the change it's waiting for. It's like someone holding up a line, refusing to move until they see something that someone further down the line is trying to deliver


That's a bunch of background, but the point is you shouldn't ever block a thread like that, unless it's a special thread you created so you know it's ok and it's not interfering with anything else. Think of Android more as an event-driven system - you write code that should be executed when something happens, like when an Activity gets created (ònCreate) or when a button is pressed(onClick) or when an AsyncTask completes (onPostExecute). There's a reason all these methods start with "on"! "When a thing happens, do this..."

So when your task completes, it runs onPostExecute and that's where all your code to handle receiving the result should go. It doesn't literally need to be inside the onPostExecute method - like you've put yours inside a passJSONGet method to keep things organised, but that gets called from onPostExecute, that's the event that triggers the code being run.

So whatever you need to do with the result, call it from onPostExecute. When that happens, it will do the stuff you've told it to do. Update a variable, make a toast, populate views in your layout with some new data, whatever!

cactustictacs
  • 17,935
  • 2
  • 14
  • 25
1

Change the code to :

  @Override
protected void onCreate(Bundle savedInstanceState) {
    
   // other code
  
    DataGetter dataGetter = new DataGetter(this, WeatherDisplay.this);
    dataGetter.execute(ip, "readLatestOutside", "weather");
    

   // furter code
}
@Override
public void passJSONGet(String jsonstring) {
    this.jsonString = jsonstring;
    this.jsonArray = new JSONArray(this.jsonString);
    Toast.makeText(this, this.jsonString, Toast.LENGTH_LONG).show();
    
}

Your flow is wrong. All UI work should be done after onPostExecute

Lokesh Deshmukh
  • 728
  • 5
  • 13