AleksejB
AleksejB

Reputation: 332

How to make LiveData<MutableList<T>> update when I change a property of T?

I'm making an app which gets the (pseudo) latency values by making a request to some urls and recording how long that will take.

First, I use retrofit to get a JSON response from a web server. This response contains: the name of the host (e.g. Ebay UK), the url of the host (e.g. www.ebay.co.uk), and an image url. I map this response onto my data class which looks like the following:

data class(
    val name: String,
    var url: String,
    val icon: String,
    var averagePing: Long = -1
)

url is a var property as before making the calls to get the latency values, I need to add https:// in order to make the request.

I'm doing all of this like so:

fun getHostsLiveData() {
    viewModelScope.launch(Dispatchers.IO) {
        val hostList = repo.getHosts()
        for (host in hostList) {
            host.url = "https://" + host.url
            host.averagePing = -1
        }
        hostListLiveData.postValue(hostList)//updated the recyclerview with initial values
        //with default (-1) value of averagePing

        for (host in hostList) {
            async { pingHostAndUpdate(host.url, hostList) }
        }
    }
}

The first for loop prepares my data. The line after the for loop submits the data to the recycler adapter, in order to show the host name, url and icon straight away (this all works i.e. I have a working observer for the LiveData), while I'm waiting for the latency values.

The second for loop calls the function to calculate the latency values for each host and the updateHostList() function updates the LiveData.

This is how the functions look:

suspend fun pingHostAndUpdate(url: String, hostList: MutableList<Host>) {
    try {
        val before = Calendar.getInstance().timeInMillis
        val connection = URL(url).openConnection() as HttpURLConnection //Need error handling
        connection.connectTimeout = 5*1000
        connection.connect()
        val after = Calendar.getInstance().timeInMillis
        connection.disconnect()
        val diff = after - before
        updateHostList(url, diff, hostList)
    } catch (e: MalformedURLException) {
        Log.e("MalformedURLExceptionTAG", "MalformedURLException")
    } catch (e: IOException) {
        Log.e("IOExceptionTAG", "IOException")
    }
}

fun updateHostList(url: String, pingResult: Long, hostList: MutableList<Host>) {
    //All this on mainThread
    var foundHost: Host? = null
    var index = 0
    for (host in hostListLiveData.value!!) { 
        if (host.url == url) {
            foundHost = host
            break
        }
        index++
    } 
    if (foundHost != null) {
        viewModelScope.launch(Dispatchers.Main) {
            val host =  Host(foundHost.name, foundHost.url, foundHost.icon, pingResult)
            Log.d("TAAAG", "$host") 
            hostList[index] = host
            hostListLiveData.value = hostList
        }
    }
}

All of this happens in the viewModel. Currently I'm updating my list by submitting the entire list again when I change one property of one element of the list, which seems horrible to me.

My question is: How can I update only one property of host and have it refresh the UI automatically?

Thanks in advance

Edit: My observer looks like this:

viewModel.hostListLiveData.observe(this, Observer { adapter.updateData(it) })

And updateData() looks like this:

fun updateData(freshHostList: List<Host>) {
    hostList.clear()
    hostList.addAll(freshHostList)
    notifyDataSetChanged()
}

@ArpitShukla, do you suggest I would have 2 update functions? one for showing the initial list and another to update on item of the list? Or would I just put both notifyDataSetChanged() and notifyItemChanged() in updateData()?

Edit2: changed my function call to make it async.

Upvotes: 0

Views: 695

Answers (1)

Putra Nugraha
Putra Nugraha

Reputation: 614

You can consider to update items observed from hostListLiveData using notifyItemChanged(position) instead notifyDataSetChanged() in your adapter.

notifyItemChanged(position) is an item change event, which update only the content of the item.

EDIT:
You're using notifyDataSetChanged() on updating the content of data which causing to relayout and rebind the RecyclerView which you're not expecting. Therefore you should update the content of your data using notifyItemChanged(position).

I think you may create a new function for updating your RecyclerView in the adapter e.g.

fun updateHostAndPing(updatedHost: Host, position: Int) {
    hostList[position].apply {
        url = updatedHost.url
        averagePing = updatedHost.averagePing
    }
    notifyItemChanged(position)
}

and in your observer, you may need to check whether it is fresh list or and updated list

viewModel.hostListLiveData.observe(this, Observer { 
    if (adapter.itemCount == ZERO) {
        adapter.updateData(it) 
    } else {
        it.forEachIndexed { index, host ->
            adapter.updateHostAndPing(host, index) 
        }
    }
})

Upvotes: 1

Related Questions