5

So I'm trying to write a very simple Android app that fetches a response from a URL when a button gets pressed. The kotlin Android extensions have been advertised as a drop-in replacement for the boilerplate necessary in Java, so I tried my hand. Here's what I tried so far:

package com.example.susemihl.myapplication

import android.os.Bundle
import android.support.v7.app.AppCompatActivity
import android.widget.TextView
import kotlinx.android.synthetic.main.activity_main.*
import kotlinx.coroutines.experimental.CommonPool
import kotlinx.coroutines.experimental.async
import kotlinx.coroutines.experimental.runBlocking
import java.net.URL

suspend fun fetch_url(url: String): String {
    return URL(url).readText()
}

fun fetch_async(url: String, view: TextView) = runBlocking {
    val result = async(CommonPool) { fetch_url(url) }
    view.setText(result.await())
}

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        mainTextView.setText("Hello there.")
        mainButton.setOnClickListener {
            mainButton.setText("Check again.")
            fetch_async("https://random-app.appspot.com/", 
                        mainTextView)
        }

    }
}

This worked intermittently, but is now completely broken. There is no response to the button click. Print-debugging shows me that the thread gets executed, but seems to hang on the readText() call. Anything stupid I'm doing wrong here?

glee8e
  • 6,180
  • 4
  • 31
  • 51
Alex S
  • 1,027
  • 1
  • 10
  • 17

3 Answers3

1

You have to switch to main thread in order to update the UI from a suspend function. I would do the networking logic in a ViewModel and expose the result as LiveData to your Activity:

class MainViewModel : ViewModel() {
  val urlLiveData = MutableLiveData<String>()

  fun fetchUrl(url: String): String {
    viewModelScope.launch {
      // Dispatchers.IO (main-safety block)
      withContext(Dispatchers.IO) {
        fetchAsync(url)
      }
    }
  }

  private suspend fun fetchAsync(url: String) {
    urlLiveData.postValue(URL(url).readText())  
  }

}

class MainActivity : AppCompatActivity() {
  private val mainViewModel by viewModels<MainViewModel>()

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    mainViewModel.urlLiveData.observe(
        viewLifecycleOwner,
        Observer { urlText ->
          mainTextView.setText(urlText)
        }
     )
   }

   mainViewModel.fetchUrl(""https://random-app.appspot.com/")
}
IgorGanapolsky
  • 26,189
  • 23
  • 116
  • 147
0

I know your case, it's because of you are using runBlocking, although await isn't block the thread, but it will suspend the coroutine, and because of the current coroutine did not complete yet, the runBlocking thread will be blocked waiting for it.

So just using launc(UI) instead of runBlocking to solve this problem:

package com.example.susemihl.myapplication

import android.os.Bundle
import android.support.v7.app.AppCompatActivity
import android.widget.TextView
import kotlinx.android.synthetic.main.activity_main.*
import kotlinx.coroutines.experimental.CommonPool
import kotlinx.coroutines.experimental.android.UI
import kotlinx.coroutines.experimental.async
import kotlinx.coroutines.experimental.launch
import java.net.URL

fun fetch_url(url: String): String {
    return URL(url).readText()
}

fun fetch_async(url: String, view: TextView) = launch(UI) {
    val result = async(CommonPool) { fetch_url(url) }
    view.text = result.await()
}

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        mainTextView.text = "Hello there."
        mainButton.setOnClickListener {
            mainButton.text = "Check again."
            fetch_async("https://jacksgong.com", mainTextView)
        }

    }
}
Jacks Gong
  • 694
  • 8
  • 18
0

Here is a async sample that can be used with kotlin which is working perfect for me

val result = URL("<api call>").readText()

try {
        URL url = new URL("<api call>");
            urlConnection = (HttpURLConnection) url.openConnection();
        urlConnection.setRequestMethod("GET");
        urlConnection.connect();
        InputStream inputStream = urlConnection.getInputStream();
            StringBuffer buffer = new StringBuffer();
        if (inputStream == null) {
            // Nothing to do.
            return null;
        }
        reader = new BufferedReader(new InputStreamReader(inputStream));
        String line;
        while ((line = reader.readLine()) != null) {
            buffer.append(line + "\n");
        }
        if (buffer.length() == 0) {
            return null;
        }
        result = buffer.toString();
    } catch (IOException e) {
        Log.e("Request", "Error ", e);
        return null;
    } finally{
        if (urlConnection != null) {
            urlConnection.disconnect();
        }
        if (reader != null) {
            try {
                reader.close();
            } catch (final IOException e) {
                Log.e("Request", "Error closing stream", e);
            }
        }
    }

@Override protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    task = new AsyncTask<Void, Void, String>() {
        @Override protected String doInBackground(Void... params) {
            return requestFromServer("<api call>");
        }

        @Override protected void onPostExecute(String s) {
            if (!isFinishing() && !isCancelled()) {
                Log.d("Request", s);
                Toast.makeText(ExampleActivity.this, "Request performed", Toast.LENGTH_LONG).show();
            }
        }
    };
}

@Override protected void onDestroy() {
    super.onDestroy();

    if (task != null) {
        task.cancel(true);
        task = null;
    }
}

Referenced from - antonioleiva

Gopi.cs
  • 985
  • 1
  • 16
  • 41