I worked on a project recently and I had tons of spinners to be created, with data populated from a network resource. I first of all began with extending the ArrayAdapter class for each type I had
class Hotel(val name: String, val address: String, val hasPool: Boolean, val hasWifi: Boolean){ | |
override fun toString(): String { | |
return "Hotel(name='$name', address='$address', hasPool=$hasPool, hasWifi=$hasWifi)" | |
} | |
} | |
class HotelAdapter(context: Context, @LayoutRes private val layoutResource: Int, private val hotels: List<Hotel>): | |
ArrayAdapter<Hotel>(context, layoutResource, hotels) { | |
override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View { | |
return createViewFromResource(position, convertView, parent) | |
} | |
override fun getDropDownView(position: Int, convertView: View?, parent: ViewGroup?): View { | |
return createViewFromResource(position, convertView, parent) | |
} | |
private fun createViewFromResource(position: Int, convertView: View?, parent: ViewGroup?): View{ | |
val view: TextView = convertView as TextView? ?: LayoutInflater.from(context).inflate(layoutResource, parent, false) as TextView | |
view.text = hotels[position].name | |
return view | |
} | |
} |
After writing basically the same classes for 2 times, I figured that it didn’t make sense and there must be a better way to do it, after some googling, I discovered I could do without extending ArrayAdapter only problem was that I’d have to override the toString method of the classes with the desired display name, since the default array adapter implementation uses that 😞.
While the awesome Kingsley Adio was reviewing my code, he was quick to point out that I shouldn’t use the toString implementation for UI elements. This lead me to investigate why, I discovered a good answer and that reignited a reason for me to start reading Joshua Bloch’s Effective Java. I’ve had the book for years and heard good things about it but never really got to read it 😁(I’m in Item 27 now! 💪 I also hear the 3rd edition is ready, sigh 😩). The general contract for toString says that the returned string should be “a concise but informative representation that is easy for a person to read”
When practical, the toString method should return all of the interesting information contained in the object — Effective Java
Returning just the desired name doesn’t follow this advice, also as the toString method is very useful in debugging it’s wrong to just twist it to suit your selfish needs.
Another thing to consider is accidental success. Say you forgot to override toString appropriately, you’ll end up seeing stuff like co.package.name.Model@12f114 on the UI, or the bogus toString that comes by default if you use data classes
After some back and forth with him 😍, he suggested that I create an interface with a display name variable which classes that implement will override to set the UI name, then use my custom array adapter repeatedly
interface ModelDisplayName { | |
val displayName: String | |
} |
class CustomArrayAdapter(context: Context, | |
@LayoutRes private val layoutResource: Int, | |
@IdRes private val textViewResourceId: Int = 0, | |
private val values: List<ModelDisplayName>) : ArrayAdapter<ModelDisplayName>(context, layoutResource, values) { | |
override fun getItem(position: Int): ModelDisplayName = values[position] | |
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { | |
val view = createViewFromResource(convertView, parent, layoutResource) | |
return bindData(getItem(position), view) | |
} | |
override fun getDropDownView(position: Int, convertView: View?, parent: ViewGroup): View { | |
val view = createViewFromResource(convertView, parent, android.R.layout.simple_spinner_dropdown_item) | |
return bindData(getItem(position), view) | |
} | |
private fun createViewFromResource(convertView: View?, parent: ViewGroup, layoutResource: Int): TextView { | |
val context = parent.context | |
val view = convertView ?: LayoutInflater.from(context).inflate(layoutResource, parent, false) | |
return try { | |
if (textViewResourceId == 0) view as TextView | |
else { | |
view.findViewById(textViewResourceId) ?: | |
throw RuntimeException("Failed to find view with ID " + | |
"${context.resources.getResourceName(textViewResourceId)} in item layout") | |
} | |
} catch (ex: ClassCastException){ | |
Log.e("CustomArrayAdapter", "You must supply a resource ID for a TextView") | |
throw IllegalStateException( | |
"ArrayAdapter requires the resource ID to be a TextView", ex) | |
} | |
} | |
private fun bindData(value: ModelDisplayName, view: TextView): TextView { | |
view.text = value.displayName | |
return view | |
} | |
} |
Things look a whole lot cleaner now!
Learnt something new from this post? Share! and star the repo.
You’ve been in a similar situation? Let me know how you went about it, send me a DM on Twitter!
Reference:
https://www.javaworld.com/article/2073619/core-java/java-tostring---considerations.html
Top comments (0)