Search code examples
androidmultithreadingkotlinhttpurl

Simply retrieve Json publically posted on HTTPS in Android app in Kotlin


I need to serialize a bunch of info stored under json format on a public url.

I know I can use java.net.URL() to retrieve the Json, and it works. But I can only use URL() on a non-main thread or else Android throws NetworkOnMainThreadException.

I can't manage to understand how to extract the result of URL() from inside a thread. Here is a mock-up of where I ended up right now:

fun getJsonFromUrl(myUrl: String): String {
    var myJson: String = ""
    thread { 
        myJson: URL = URL(myUrl).readText()
    }.start()
    return myJson
}

This always returns an empty string, because the return statement is done before the thread actually finishes (or even starts maybe). How do I force this function to return the value when thread is done? Or if this is a bad idea (I know threads aren't supposed to put app on wait), how to do it differently with URL() still?

I know there are already a lot of SO questions about this, but I didn't find anything to properly answer this question. I'm also aware there are a lot of packages to manage HTTP GET (Volley, OkHttp, ...) but I will not be needing anything more than this simple connection, and I don't think those packages will help me understand how to work in threads. I tried a lot of things, mostly SO posts, that didn't help me at all.


Solution

  • You didn't specify how you want to handle the result, but from what I can tell you probably just need to wrap your head around Unidirectional data flow (UDF).

    When the UI initiates the read operation (readData), the view model starts the network access in a coroutine, and only when the result is ready it updates its state that is exposed to the UI (myObject):

    class MyViewModel : ViewModel() {
        private val _myObject = MutableStateFlow<MyDeserializedObject?>(null)
        val myObject: StateFlow<MyDeserializedObject?> = _myObject.asStateFlow()
    
        fun readData(url: String) {
            viewModelScope.launch {
                val json = getJsonFromUrl(url)
                _myObject.value = deserializeData(json)
            }
        }
    }
    

    It basically works the same with LiveData if you don't use flows in your view model (which you should use, though).

    The above code will still produce the same error, but since you are now in the world of coroutines you can simply switch to another thread in getJsonFromUrl like this:

    suspend fun getJsonFromUrl(myUrl: String): String = withContext(Dispatchers.IO) {
        URL(myUrl).readText()
    }
    

    suspend indicates that this function can only be called when already inside a coroutine (which you are because of viewModelScope.launch). Then you can switch to an appropriate dispatcher with withContext. There are three dispatchers:

    • Main: This dispather only has the main thread which is used by the UI and must be very responsive. Never execute anything on that dispatcher that can take some time to finish (like the network request from URL), otherwise your UI will freeze.
    • IO: This dispatcher has lots of different threads which are assumed to be mostly idle - as it is with IO operations like network requests, database access or file system access.
    • Default: This dispatcher has a very limited amount of threads (usually equal to the number of processor cores) which can be used to perform long-running operations that consume a lot of cpu power.

    In your case the IO dispatcher is the appropriate one. The current coroutine then switches to one of the available threads and is thereby moved off the main thread, fixing the error you got.

    Don't create threads yourself, let the coroutine framework handle that for you.