I think my question is pretty simple, but I just started my road in android, so it's a big problem right now. Anyway, I want to read text from url, convert it to array and display as ListView.
Here's part of my code:
private fun getSongList() {
Executors.newSingleThreadExecutor().execute() {
val songList =
URL("https://vivalaresistance.ru/radio/stuff/vlrradiobot.php?type=getPlaylist").readText(
Charset.forName("windows-1251")
)
.replace("й", "й")
.replace("Й", "Й")
.split("\n").toTypedArray()
}
val list = view?.findViewById<ListView>(R.id.list)
val arrayAdapter =
ArrayAdapter(requireActivity(), android.R.layout.simple_list_item_1, songList)
list?.adapter = arrayAdapter
}
The problem is in declaring values, I guess.
What should I change to make this whole thing work?
Several things are wrong with your code :
You don't actually call the url, you are just defining it without opening a connection with a HttpUrlConnection
You are updating UI code in Executors.newSingleThreadExecutor().execute() which is a background thread, you should always update UI on UI thread
It's just about optimisation but you better use RecylerView instead of ListViewand dataBinding instead of view reference and you shouldn't have UI code and background code in same class
With that said a correct way to implement your function using anonymous asyncTask would be :
private fun getSongList() {
val task = object : AsyncTask<Void, Void, Array<String>>() {
override fun doInBackground(vararg params: Void): Array<String> {
val url = URL("https://vivalaresistance.ru/radio/stuff/vlrradiobot.php?type=getPlaylist")
val connection = url.openConnection() as HttpURLConnection
connection.requestMethod = "GET"
connection.connect()
val songList = BufferedReader(InputStreamReader(connection.inputStream))
connection.disconnect()
return songList
.readText()
.replace("й", "й")
.replace("Й", "Й")
.split("\n").toTypedArray()
}
override fun onPostExecute(result: Array<String>) {
val list = view?.findViewById<ListView>(R.id.list)
val arrayAdapter = ArrayAdapter(requireActivity(), android.R.layout.simple_list_item_1,result)
list?.adapter = arrayAdapter
}
}
task.execute()
}
Assuming that you want to perform split/replace operations on the result and not the url itself
Related
How do I have multiple callback function back to the Activity/Fragment of a RecyclerView?
I have multiple options for each item in the RecyclerView (Edit, Delete, CheckedAsComplete, View) and I would like to have callback function for each of them in the Activity/Fragment of the RecyclerView.
Here is a link of how I got one callback in the Adapter: https://www.geeksforgeeks.org/kotlin-lambda-functions-for-recyclerview-adapter-callbacks-in-android/
I just need to know if it is possible to have multiple callbacks in the adapter and if so, how do I implement it?
My Activity's Adapter Code:
val adapter = ProductAdapter(this) {
deleteProduct(it),
editProduct(it),
viewProduct(it),
checkAsComplete(it)
}
Here is my Adapter's Constructor:
class ProductAdapter(
private var context: Context,
private val deleteProduct: (ItemTable) -> Unit,
private val editProduct: (ItemTable) -> Unit,
private val viewProduct: (ItemTable) -> Unit,
private val checkedAsComplete: (ItemTable) -> Unit
): RecyclerView.Adapter<ProductAdapter.ItemProductViewHolder>() {
// Rest of RecyclerView Adapter Code
}
I'm pretty new to kotlin so I would really appreciate your help!
You can use curly braces out only for the last callback of the list.
Assuming you declared the following methods in your activity :
fun deleteProduct(itemTable: ItemTable)
fun editProduct(itemTable: ItemTable)
fun checkAsComplete(itemTable: ItemTable)
fun viewProduct(itemTable: ItemTable)
You can use named parameters and you have two choices
With method reference
val adapter = ProductAdapter(
context = this,
deleteProduct = ::deleteProduct,
editProduct = ::editProduct,
viewProduct = ::viewProduct,
checkAsComplete = ::checkAsComplete
)
With lambda
val adapter = ProductAdapter(
context = this,
deleteProduct = { deleteProduct(it) },
editProduct = { editProduct(it) },
viewProduct = { viewProduct(it) },
checkAsComplete = { checkAsComplete(it) }
)
You can use different approach. This does not depend on how many events you have. For example with enum class you can use single callback with many options
class ProductAdapter(private val clickEvent: (ClickEvent, ItemTable) -> Unit):
RecyclerView.Adapter<ProductAdapter.ItemProductViewHolder>() {
enum class ClickEvent {
DELETE,
EDIT,
VIEW,
COMPLETE
}
}
Usage:
val adapter = ProductAdapter{ event, item ->
when(event){
DELETE -> deleteProduct(item)
....//All other enum values
}
}
I have some recycler view code in a function that gets called several times as bluetooth devices are scanned. My code is working but I am wondering what unseen effects are occurring from having my recycler view initialization code in a function that gets repeated a lot? I eventually want to update the list rather than replace it via notifyDataSetChanged() but I am unsure how to do that with my current code structure. Any help would be appreciated!
#SuppressLint("MissingPermission", "NotifyDataSetChanged")
fun displayDevices(
scannedDevicesStrings: TreeSet<String>,
deviceMap: HashMap<String, String>
) {
val sortedDeviceMap = deviceMap.toSortedMap()
Log.d(TAG, "displayDevices: ${sortedDeviceMap.entries}")
// Set linear layout manager for the widget.
val linearLayoutManager = LinearLayoutManager(applicationContext)
binding.recyclerviewDevices.layoutManager = linearLayoutManager
// Specify an adapter.
listAdapter = CustomAdapter(scannedDevicesStrings.toList(), sortedDeviceMap, bluetoothManager)
binding.recyclerviewDevices.adapter = listAdapter
listAdapter.notifyDataSetChanged()
// Notify the view to update when data is changed.
if ( binding.recyclerviewDevices.isAttachedToWindow) {
binding.progressBarCyclic.visibility = GONE
}
}
This code calls my CustomAdapter() class which looks like this:
class CustomAdapter(
private val treeSet: List<String>,
private var hashMap: SortedMap<String, String>,
private val bluetoothManager: BluetoothManager
) : RecyclerView.Adapter<CustomAdapter.ViewHolder>() {
class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
val textView: TextView = view.findViewById(R.id.textview_list_item)
val listLayout: FrameLayout = view.findViewById(R.id.item_layout)
val context: Context = view.context
val textView2: TextView = view.findViewById(R.id.textview_list_item_address)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.text_device_row_item, parent, false)
return ViewHolder(view)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val deviceList = hashMap.keys.toList()
val macAddressList = hashMap.values.toList()
holder.textView.text = deviceList.elementAt(position)
holder.textView2.text = macAddressList.elementAt(position)
val selectedDeviceString = deviceList.elementAt(position).toString()
val selectedDevice = bluetoothManager.adapter.getRemoteDevice(hashMap[selectedDeviceString])
val sharedPreferences = holder.context.getSharedPreferences("mSharedPrefs", Context.MODE_PRIVATE) ?: return
with(sharedPreferences.edit()) {
putString("selectedDeviceString", selectedDevice.toString())
apply()
}
holder.listLayout.setOnClickListener {
val intent = Intent(holder.context, DeviceActivity::class.java)
intent.putExtra("btDevice", selectedDevice)
intent.putExtra("btDeviceName", selectedDeviceString)
sharedPreferences.edit().putString("loadedFrom", "loadedFromCustomAdapter").apply()
sharedPreferences.edit().putString("selectedDeviceName", selectedDeviceString).apply()
sharedPreferences.edit().putString("selectedDeviceString", selectedDevice.toString()).apply()
holder.context.startActivity(intent)
}
}
override fun getItemCount() = treeSet.size
}
Setting a new adapter makes the RecyclerView reinitialise itself, and it'll create all the ViewHolders again, etc. You'd want to avoid that really. This is generally how you'd make it update:
class CustomAdapter(
private var data: List<Thing>
...
) {
fun setData(data: List<Thing>) {
// store the data and do any other setup work
this.data = data
// make sure the RecyclerView updates to show the new data
notifyDataSetChanged()
}
}
Then you just need to keep a reference to the adapter when you first create it in onCreate or whatever, and call theAdapter.setData(newData) whenever you get the new stuff. You're just setting up by creating an adapter to handle displaying your data in the list, and then you hand it data whenever it needs to update.
The actual "how things update" logic is in setData - it's the adapter's business how the adapter works internally, y'know? Right now it's the most basic notifyDataSetChanged() call, i.e. "refresh everything", but you could change that later - the outside world doesn't need to care about that though.
I noticed in onBindViewHolder you're doing this:
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val deviceList = hashMap.keys.toList()
val macAddressList = hashMap.values.toList()
That function runs every time a ViewHolder needs to display some new information (just before it scrolls onto the screen, basically) so you're creating a lot of lists whenever you scroll. Really you should do that once, when the data is set - derive your lists from the source and keep them around:
class CustomAdapter(
initialData: List<Thing> // not a var now - see init block
...
) {
// these might need to be lateinit since you're init-ing through a function
private var data: List<Thing>
private var deviceList: List<String>
private var macAddressList: List<String>
init {
setData(initialData)
}
fun setData(data: List<Thing>) {
// Now you're storing the data and deriving the other lists
// You might not even need to store the 'data' object if you're not using it?
this.data = data
deviceList = data.keys.toList()
macAddressList = data.values.toList()
// make sure the RecyclerView updates to show the new data
notifyDataSetChanged()
}
}
So now setData takes some source data, derives the lists you need to use and stores those, and calls the update function. Because that setup has to be done, you need to call this function every time the source data is set - including when you first create the adapter. That's why the data parameter in the constructor isn't a var, it's just used in the initialisation, passed to the setData call.
Or alternatively, don't pass any data in the constructor at all - just create the adapter, and immediately call setData on it. Initialise the variables to emptyList() etc, and then you don't need to handle the "setup at construction time" case at all!
Just another couple of tips - I don't know what treeSet is for, but you're using its size in getItemCount. You shouldn't do that, it should usually reflect the size of the data set you're actually displaying, which is the contents of hashSet - when you get a position in onBindViewHolder, you're looking up an element in hashSet, not treeSet, so that should be your source for the number of items
The other thing is, all that stuff in onBindViewHolder... you're doing a lot of setup work that should really only happen when the item is actually clicked. Usually you'd set up the click listener once, in onCreateViewHolder, and when binding you'd set a field on the viewholder telling it which position it's currently displaying. If the click listener fires, then you can look up the current position in the data, create Intents, etc
Even if you don't move that into the VH, at least move the setup code into the onClickListener so it doesn't run every time a new item scrolls into view. That sharedPreferences bit is especially a problem - that gets overwritten every time a new item is bound (and they can be bound when they're still off-screen) so it probably isn't set to what you expect
Setting the adapter multiple times should be avoided. Doing so causes its scroll position to be lost and reset to the top, and causes it to have to reinflate all of its views and ViewHolders. Instead, you should update the model your adapter points at and notifyDataSetChanged() on it (or better yet, use DiffUtil to update individual items).
I finished updating my code and it works great! The data no longer jumps to the top when new data is added. Thought I would post the code for anyone who is interested.
Here is my adapter:
class CustomAdapter(
private val bluetoothManager: BluetoothManager
) : RecyclerView.Adapter<CustomAdapter.ViewHolder>() {
private var sortedMap = emptyMap<String, String>()
private var deviceList = emptyList<String>()
private var macAddressList = emptyList<String>()
class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
val textView: TextView = view.findViewById(R.id.textview_list_item)
val listLayout: FrameLayout = view.findViewById(R.id.item_layout)
val context: Context = view.context
val textView2: TextView = view.findViewById(R.id.textview_list_item_address)
}
#SuppressLint("NotifyDataSetChanged")
fun setData(sortedMap: SortedMap<String, String>) {
this.sortedMap = sortedMap
deviceList = sortedMap.keys.toList()
macAddressList = sortedMap.values.toList()
notifyDataSetChanged()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.text_device_row_item, parent, false)
return ViewHolder(view)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.textView.text = deviceList.elementAt(position)
holder.textView2.text = macAddressList.elementAt(position)
holder.listLayout.setOnClickListener {
val selectedDeviceString = deviceList.elementAt(position).toString()
val selectedDevice = bluetoothManager.adapter.getRemoteDevice(sortedMap[selectedDeviceString])
val sharedPreferences = holder.context.getSharedPreferences("mSharedPrefs", Context.MODE_PRIVATE) ?: return#setOnClickListener
with(sharedPreferences.edit()) {
putString("selectedDeviceString", selectedDevice.toString())
apply()
}
val intent = Intent(holder.context, DeviceActivity::class.java)
intent.putExtra("btDevice", selectedDevice)
intent.putExtra("btDeviceName", selectedDeviceString)
sharedPreferences.edit().putString("loadedFrom", "loadedFromCustomAdapter").apply()
sharedPreferences.edit().putString("selectedDeviceName", selectedDeviceString).apply()
sharedPreferences.edit().putString("selectedDeviceString", selectedDevice.toString()).apply()
holder.context.startActivity(intent)
}
}
override fun getItemCount() = sortedMap.size
}
And the activity onCreate() code:
// Specify an adapter.
listAdapter = CustomAdapter(bluetoothManager)
binding.recyclerviewDevices.adapter = listAdapter
And my function that updates the data:
#SuppressLint("MissingPermission", "NotifyDataSetChanged")
fun displayDevices(
deviceMap: HashMap<String, String>
) {
val sortedDeviceMap = deviceMap.toSortedMap()
listAdapter.setData(sortedDeviceMap)
Log.d(TAG, "displayDevices: ${sortedDeviceMap.entries}")
// Notify the view to update when data is changed.
if ( binding.recyclerviewDevices.isAttachedToWindow) {
binding.progressBarCyclic.visibility = GONE
}
}
I want to update at any time some values in my RecyclerView.
Here is my data class ParameterText:
data class ParameterText(
var parameterName: String?,
var parameterValue: String?
)
Here is my ViewHolder class ParameterTextViewHolder:
class ParameterTextViewHolder(itemView: View) : ViewHolder(itemView) {
val parameterName: TextView = itemView.findViewById(R.id.parameterName)
val parameterText: TextView = itemView.findViewById(R.id.parameterValue)
}
Here is my Adapter (in my Activity):
// Adapter
private val parametersTextFoundList = emptyDataSourceTyped<ParameterText>()
And here is my RecyclerView setup (also in my Activity):
rv_parameters_text.setup {
withDataSource(parametersTextFoundList)
withItem<ParameterText, ParameterTextViewHolder>(R.layout.parameter_text) {
onBind(::ParameterTextViewHolder) { _, item ->
parameterName.text = item.parameterName
parameterText.text = item.parameterValue
}
}
}
I tried this:
private fun updateValue(index: Int, value: String) {
parametersTextFoundList[index].parameterValue = value
}
But it doesn't work. I read that I should also use the notifyDataSetChanged() method but I don't know where to use it. Can you help me?
There is an entire suite of notify API's, including notifyItemInserted(), notifyItemRemoved(), notifyItemChanged(), which are designed to more efficiently update a RecyclerView.
when changing the contents of one existing row in your RecyclerView, its more efficient to use adapter.notifyItemChanged(row), as notifyDataSetChanged() will reload the entire RecyclerView. I recommend:
private fun updateValue(index: Int, value: String)
{
parametersTextFoundList[index].parameterValue = value
rv_parameters_text.adapter?.notifyItemChanged(index)
}
You need to use notifyDataSetChanged() method with the update like this
rv_parameters_text.adapter?.notifyDataSetChanged()
I am in a process of converting my app from Java to Kotlin and I am getting a ClassCastException while executing the code below. Thoughts?
private fun getImageFragments(imagesList: MutableList<Restaurant?>?) {
if (imagesList != null) {
val restaurant: Restaurant? = imagesList[0]
/*exception: java.lang.ClassCastException:
java.lang.String cannot be cast to com.example.misc.Restaurant */
}
}
Not sure if the implementation of the Restaurant Class is of any importance in this case? https://pastebin.com/q5z76exh
While inspecting the contents of imagesList[0] with debugger I am seeing this:
ArrayList size = 1
https://domainName.com/files/app/banners/1550449003515.png
count=116
hash:0
shadow$_klass_=class java.lang.String
shadow$_monitor_
Moreover, when I try to rewrite the problematic line to
val restaurant: String? = imagesList[0]
I am getting:
Type mismatch: inferred type is Restaurant? but String? was expected
EDIT:
So as per requests I am posting some more code, be warned though, it's not going to be pretty. Trust me, I would gladly show you the whole thing if it wasn't closed but the main reason is I don't want you to suffer. Ask if you want more. Here is the code of the function that is calling getImageFragments:
getDrawerBanners(App.preference.getString("language", "ENG"),
object : OnAPIRequestListResultListener<Restaurant?> {
override fun onListResult(list: MutableList<Restaurant?>?) {
bannerViewPager.adapter = fragmentManager?.
let{ DrawerBannersAdapter(it, getImageFragments(list)) }
}
})
And here is OnAPIRequestListResultListener interface, I have a strong feeling that error might be hidden smowewhere here, if you think otherwise read on further.
interface OnAPIRequestListResultListener<T> {
fun onListResult(list: MutableList<T>?)
}
Original signature + body of getDrawerBanners function
// Downloading banners from server
#JvmStatic
fun getDrawerBanners(languageCode: String?, listener:
OnAPIRequestListResultListener<Restaurant?>) {
val async = GetMenuBannersAsync(listener)
async.execute(languageCode)
}
GetMenuBannersAsync
private class GetMenuBannersAsync(var listener:
OnAPIRequestListResultListener<Restaurant?>) : AsyncTask<String?, Void?, Void?>() {
var bannersUrls: MutableList<Restaurant?> = ArrayList()
override fun doInBackground(vararg params: String?): Void? {
val url = "https://api.smartapp.com/App/Language/" + params[0]
val client: HttpClient = DefaultHttpClient()
val httpGet = HttpGet(url)
httpGet.addHeader(AUTHORIZATION, AUTHORIZATION_STRING)
var httpResponse: HttpResponse? = null
var httpEntity: HttpEntity? = null
httpResponse = client.execute(httpGet)
httpEntity = httpResponse.entity
val response = EntityUtils.toString(httpEntity)
val json = JSONObject(response)
// Getting drawer banner
val bannersArray = json.getJSONArray("mainMenuBanners")
bannersUrls = ArrayList()
for (i in 0 until bannersArray.length()) {
val bannerObj = bannersArray.getJSONObject(i)
val imageUrl = bannerObj.getString("image")
(bannersUrls as ArrayList<String>).add(imageUrl)
}
return null
}
override fun onPostExecute(result: Void?) {
listener.onListResult(bannersUrls)
super.onPostExecute(result)
}
}
The problem lies in your GetMenuBannersAsync function. There you create the bannersUrls list with type MutableList<Restaurant?> but the you put values into the list with (bannersUrls as ArrayList<String>).add(imageUrl). The cast expression tells the compiler that the conversion you do is save, which it is not in your case.
So always remember that you are on your own if your use a cast and the type system will no longer help you to detect problems. You need to make sure that the conversion is safe do do.
To solve the actual problem you need to create the Restaurant objects and put them into the list instead of urls.
I need to run 6 API calls Simultaneously and need to update the UI for each when corresponding request finishes
Currently I am using kotlin coroutines parallel execution using the following code
suspend fun getAllData() : List<String>{
return withContext(Dispatchers.IO) {
lateinit var getObject1Task: Deferred<Response<String>>
lateinit var getObject2Task: Deferred<Response<String>>
lateinit var getObject3Task: Deferred<Response<String>>
lateinit var getObject4Task: Deferred<Response<String>>
lateinit var getObject5Task: Deferred<Response<String>>
lateinit var getObjec6Task: Deferred<Response<String>>
launch {
getObject1Task = dataApiService.getData()
getObject2Task = dataApiService.getData()
getObject3Task = dataApiService.getData()
getObject4Task = dataApiService.getData()
getObject5Task = dataApiService.getData()
getObject6Task = dataApiService.getData()
}
var stringList = ArrayList<String >()
stringList.add(getObject1Task.await().body()!!) /// add All to the list
stringList
}
}
I am unable to find a way a way to get data for each string as soon as that API finishes.
I also tried LiveData but some how that was making any sense.
Each String has no link with the other so it not essential to add all strings in a list
Using coroutines, there are multiple ways to achieve this. Here are 2 examples:
Use launch without returning a value and directly update UI from
within the coroutine, once the string is ready.
Similar to your approach, you can also use async, wait for the
future response return value and then update UI.
Example 1: Fire and forget, update UI directly when each element is ready
When updating UI elements from within a coroutine, you should use Dispatchers.Main as coroutine context.
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
repeat(6){index ->
val id = resources.getIdentifier("tv${index+1}", "id", packageName)
val textView = findViewById<TextView>(id)
textViews.add(textView)
}
repeat(6){ index ->
GlobalScope.launch(Dispatchers.Main) { // launch coroutine in the main thread
val apiResponseTime = Random.nextInt(1000, 10000)
delay(apiResponseTime.toLong())
textViews[index].text = apiResponseTime.toString()
}
}
}
Note:
Here, every TextView gets updated as soon as the string is ready, without blocking the main thread.
I used 6 example TextViews in a LinearLayout with IDs "tv1", "tv2"...
Example 2: Using parallel async + await(), update UI when all jobs are finished (similar to yours)
Here we are launching 6 async in parallel and add the results to the list as soon as they are ready. When the last result is added, we return the list and update the TextViews in a loop.
val textViews = mutableListOf<TextView>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
repeat(6){index ->
val id = resources.getIdentifier("tv${index+1}", "id", packageName)
val textView = findViewById<TextView>(id)
textViews.add(textView)
}
// note: again we use Dispatchers.Main context to update UI
GlobalScope.launch(Dispatchers.Main) {
val strings = updateUIElementAfterThisFinishes()
repeat(6){index ->
textViews[index].text = strings[index]
}
}
}
// for API calls we use Dispatchers.IO context, this function will finish at 10 seconds or less
suspend fun updateUIElementAfterThisFinishes(): List<String> = withContext(Dispatchers.IO){
val strings = mutableListOf<String>()
val jobs = Array(6){GlobalScope.async {
val apiResponseTime = Random.nextInt(1000, 10000)
delay(apiResponseTime.toLong())
apiResponseTime.toString()
}}
jobs.forEach {
strings.add(it.await())
}
return#withContext strings
}
If APIs are not related to each other, then you launch a new coroutine for every API. Coroutines are lite weight, so you can launch as many as you want.
Since you have 6 APIs, launch 6 coroutines at once and handle their responses in there corresponding coroutines.
and you also want to follow MVVM, so you can use LiveData to pass data from viewModel to your fragment or activity.
In ViewModel, it will look something like this
// liveDatas for passing data to your fragment or activity
// each liveData should be observed and update their respective views
val object1LiveData = MutableLiveData<String>()
val object2LiveData = MutableLiveData<String>()
val object3LiveData = MutableLiveData<String>()
val object4LiveData = MutableLiveData<String>()
val object5LiveData = MutableLiveData<String>()
val object6LiveData = MutableLiveData<String>()
private val listOfLiveData = listOf(
object1LiveData,
object2LiveData,
object3LiveData,
object4LiveData,
object5LiveData,
object6LiveData
)
fun fetchData(){
listOfLiveData.forEach { liveData->
// launching a new coroutine for every liveData
// for parellel execution
viewModelScope.launch{
callApiAndUpdateLiveData(liveData)
}
}
}
private suspend fun callApiAndUpdateLiveData(liveData: MutableLiveData<String>){
val response = dataApiService.getData().await()
liveData.value = response.body()!!
}