I'm implementing a SpinnerAdapter in Android project. So I have to override getView(i: Int, convertView: View, parent: ViewGroup) method. So convertView is here in order to reuse existing view and reduce memory usage and GC occurrences. So if it is null I have to create view and use already created otherwise.
So in fact I have to write something like this (officially recomended by google):
if (view == null) {
view = View.inflate(context, R.layout.item_spinner, parent)
view.tag(Holder(view))
} else {
(view.tag as Holder).title.text = getItem(i)
}
But Kotlin does not allow to write to param.
What I found on the internet is an official blog post that says that it is not possible since Feb, 2013.
So I'm wondering if there is any workaround ?
There are two issues here.
First, you are mistakenly assuming that modifying view in Java does anything outside of the current function scope. It does not. You setting that parameter to a new value affects nothing outside of the local function scope.
View getView(int i, View view, ViewGroup parent) {
// modify view here does nothing to the original caller reference to view
// but returning a view does do something
}
Next, in Kotlin all parameters are final (JVM modifier, also same as final modifier in Java). The Kotlin if statement version of this code would be:
fun getView(i: Int, view: View?, parent: ViewGroup): View {
return if (view == null) {
val tempView = View.inflate(context, R.layout.item_spinner, parent)
tempView.tag(Holder(tempView))
tempView
} else {
(view.tag as Holder).title.text = getItem(i)
view
}
}
or avoiding the new local variable:
fun getView(i: Int, view: View?, parent: ViewGroup): View {
return if (view == null) {
View.inflate(context, R.layout.item_spinner, parent).apply {
tag(Holder(this)) // this is now the new view
}
} else {
view.apply { (tag as Holder).title.text = getItem(i) }
}
}
or
fun getView(i: Int, view: View?, parent: ViewGroup): View {
if (view == null) {
val tempView = View.inflate(context, R.layout.item_spinner, parent)
tempView.tag(Holder(tempView))
return tempView
}
(view.tag as Holder).title.text = getItem(i)
return view
}
or using the ?. and ?: null operators combined with apply():
fun getView(i: Int, view: View?, parent: ViewGroup): View {
return view?.apply {
(tag as Holder).title.text = getItem(i)
} ?: View.inflate(context, R.layout.item_spinner, parent).apply {
tag(Holder(this))
}
}
And there are another 10 variations, but you can experiment to see what you like.
It is considered less-than-a-good practice (but allowed) to shadow variables by using the same name, that is why it is a compiler warning. And why you see a change in the variable name above from view to tempView
Mutable parameters are not supported in Kotlin.
I would like to refer you to this discussion in kotlinlang.org
There's a dirty but useful way to achieve that.
fun a(b: Int) {
var b = b
b++ // this compiles
}
Officially speaking, you are not allowed to override a method param. The best you can do is "shadow" the param variable.
So you can do it similar to (not sure why you would want to shadow though but you can)
getView(i: Int, view: View?, parent: ViewGroup) {
val view = view ?: View.inflate(context, R.layout.item_spinner, parent)
.apply { tag(Holder(view)) }
(view.tag as Holder).title.text = getItem(i)
}
Related
I have Spinner with ArrayAdapter. ArrayAdapter code show like:
class HouseholdsArrayAdapter(
context: Context,
resource: Int,
) : ArrayAdapter<Household>(context, resource, arrayListOf()) {
fun submitList(list: List<Household>) {
this.clear()
this.addAll(list)
}
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
return super.getView(position, convertView, parent).also {
(it as TextView).text = getItem(position)!!.fullAddress
}
}
override fun getDropDownView(position: Int, convertView: View?, parent: ViewGroup): View {
return super.getDropDownView(position, convertView, parent).also {
(it as TextView).text = getItem(position)!!.fullAddress
}
}
Also i have households list. It is LiveData in my ViewModel. I observe it in onViewCreated() method:
viewModel.householdList().observe(viewLifecycleOwner){
householdAdapter.submitList(it)
}
My issue:
Spinner DOES NOT save selected position after rotating screen.
What is more: Spinner save position after rotating if i submit households list immediately. But it does not work with LiveData mechanism.
This is very strange because how i understand Spinner must save state with the help onSaveInstanceState() and onRestoreInstanceState().
I can't find a solution to my problem : I want a hint text displayed on my Spinner but the adapter I set only accepts enum type (IdentityType enum)so I cannot add a String to it (for the hint)
Do you have any solution still using the enum in the adapter?
private fun initDriverIdentityTypeSpinner() {
driverIdentityTypeSpinner.adapter = object : ArrayAdapter<IdentityType>(context!!, android.R.layout.simple_spinner_item,IdentityType.values()) {
override fun getDropDownView(position: Int, convertView: View?, parent: ViewGroup): View =
(super.getDropDownView(position, convertView, parent) as CheckedTextView).also{
it.setText(getItem(position)!!.stringRes())
}
override fun getView(position: Int, convertView: View?, parent: ViewGroup) =
(super.getView(position, convertView, parent) as TextView).also {
it.setText(getItem(position)!!.stringRes())
}
override fun isEnabled(position: Int): Boolean = position != 0
}.also {
it.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
}
}
//IdentityType Extension
#StringRes
fun IdentityType.stringRes(): Int {
return when(this) {
IdentityType.DRIVING_LICENSE -> R.string.driving_license
IdentityType.ID_CARD -> R.string.id_card
IdentityType.PASSPORT -> R.string.passport
}
}
In Kotlin there is possibility to put properties inside enum (here it calls enum class). You can define it in constructor like following:
enum class IdentityType(val stringResId: Int) {
DRIVING_LICENSE(R.string.driving_license),
ID_CARD(R.string.id_card),
PASSPORT(R.string.passport)
}
Then you can use it like it is a common property of a class.
val type: IdentityType = ...
val string = getString(type.stringResId)
I've been trying to implement recycling in my PagerAdapter, which I could do successfully thanks to this question however I'm running into a problem to cache the views with data binding.
I have tried like this, keeping a Stack<View>:
class CustomAdapter : PagerAdapter() {
private var recycledViews: Stack<View> = Stack()
var items: List<Item> = ArrayList()
set(value) {
field = value
notifyDataSetChanged()
}
override fun instantiateItem(container: ViewGroup, position: Int): Any {
val binding = inflateOrRecycle(container)
binding.item = items[position]
binding.handler = this
container.addView(binding.root)
return binding.root
}
private fun inflateOrRecycle(container: ViewGroup): CustomBinding {
val inflater = LayoutInflater.from(container.context)
return if (recycledViews.isEmpty()) {
CustomBinding.inflate(inflater, container, false)
} else {
val view = recycledViews.pop()
CustomBinding.bind(view)
}
}
override fun destroyItem(container: ViewGroup, position: Int, `object`: Any) {
val view = `object` as View
container.removeView(view)
recycledViews.add(view)
}
}
However, whenever it tries to use a recycled view for the first time and calls CustomBinding.bind(view) it crashes because the view must have a tag. I've searched this, but none of the answers I've found have quite fixed my problem.
I've also tried keeping a Stack<CustomBinding>, but the problem is I'm not sure how to handle the destroyItem method. Because if I do:
override fun destroyItem(container: ViewGroup, position: Int, `object`: Any) {
val view = `object` as View
container.removeView(view)
recycledViews.add(CustomBinding.bind(view))
}
I'll still get the same error. How can I "recycle" data binding objects like this? Or, if I recycle the views themselves, how do I convert them back to binding objects?
You have done a simple mistake, I guess.
You can correct me if I am wrong, I tried with this adapter and it worked.
val demoAdapter = object : PagerAdapter() {
private var recycledViews: Stack<View> = Stack()
var items: List<String> = ArrayList()
set(value) {
field = value
notifyDataSetChanged()
}
override fun instantiateItem(container: ViewGroup, position: Int): Any {
val binding = inflateOrRecycle(container)
container.addView(binding.root)
return binding.root
}
private fun inflateOrRecycle(container: ViewGroup): DemoItemBinding {
val inflater = LayoutInflater.from(container.context)
return if (recycledViews.isEmpty()) {
DemoItemBinding.inflate(inflater, container, false)
} else {
val view = recycledViews.pop()
val custBinding = DataBindingUtil.getBinding<DemoItemBinding>(view)
if(custBinding == null)
DemoItemBinding.bind(view)
else
custBinding
}
}
override fun destroyItem(container: ViewGroup, position: Int, `object`: Any) {
val view = `object` as View
container.removeView(view)
recycledViews.add(view)
}
override fun isViewFromObject(view: View, `object`: Any): Boolean {
return `object` is View && view.equals(`object`)
}
override fun getCount(): Int {
return 4
}
}
The portion I changed from your code was this
return if (recycledViews.isEmpty()) {
CustomBinding.inflate(inflater, container, false)
} else {
val view = recycledViews.pop()
CustomBinding.bind(view)
}
to
return if (recycledViews.isEmpty()) {
DemoItemBinding.inflate(inflater, container, false)
} else {
val view = recycledViews.pop()
val custBinding = DataBindingUtil.getBinding<DemoItemBinding>(view)
if(custBinding == null)
DemoItemBinding.bind(view)
else
custBinding
}
I think, you were trying to bind to a view which already has a Binding attached to it. Thus it was giving you an error. What I have done is check for any previous binding, if it's there return the associated binding.
java.lang.IllegalArgumentException: Parameter specified as non-null is null: method kotlin.jvm.internal.Intrinsics.checkParameterIsNotNull, parameter convertView
Adapter.getView
at android.widget.AbsListView.obtainView(AbsListView.java:2346)
at android.widget.ListView.makeAndAddView(ListView.java:1876)
at android.widget.ListView.fillDown(ListView.java:702)
at android.widget.ListView.fillFromTop(ListView.java:763)
at android.widget.ListView.layoutChildren(ListView.java:1671)
at android.widget.AbsListView.onLayout(AbsListView.java:2148)
This is logcat of android.
I tried with java it's working fine base adapter something wrong in Adapter or other.
I tried with the public constructor and also array list count 3 found i checked it. Alway it's crash at getView
MyAdapter Code::
inner class MyAppAdapter constructor(private val parkingList: ArrayList<App>, private val mContext: Context) : BaseAdapter() {
override fun getCount(): Int {
return this.parkingList.size
}
override fun getItem(position: Int): Any {
return position
}
override fun getItemId(position: Int): Long {
return position.toLong()
}
override fun getView(position: Int, convertView: View, parent: ViewGroup): View? {
val viewHolder: ViewHolder
var rowView: View? = convertView
if (rowView == null) {
rowView = LayoutInflater.from(mContext).inflate(R.layout.item_more_apps, parent, false)
viewHolder = ViewHolder()
viewHolder.appIcon = rowView.findViewById(R.id.appIcon)
viewHolder.appName = rowView.findViewById(R.id.appName)
viewHolder.appDescription = rowView.findViewById(R.id.appDescription)
rowView.tag = viewHolder
} else {
viewHolder = convertView.tag as ViewHolder
}
viewHolder.appName!!.text = String.format("%s", this.parkingList[position].name)
viewHolder.appDescription!!.text = String.format("%s", this.parkingList[position].description)
Glide.with(applicationContext).load(this.parkingList[position].icon).into(viewHolder.appIcon!!)
rowView?.setOnClickListener {
try {
startActivity(Intent("android.intent.action.VIEW", Uri.parse("market://details?id=" + this#MyAppAdapter.parkingList[position].link)))
} catch (e: ActivityNotFoundException) {
startActivity(Intent("android.intent.action.VIEW", Uri.parse("http://play.google.com/store/apps/details?id=" + this#MyAppAdapter.parkingList[position].link)))
}
}
return rowView
}
inner class ViewHolder {
var appDescription: TextView? = null
var appIcon: ImageView? = null
var appName: TextView? = null
}
}
Used at AsyncTask -> onPostExecute
myAppAdapter = MyAppAdapter(appArrayList, applicationContext)
lvPoses!!.adapter = myAppAdapter
Variable Decleared like this
lateinit var myAppAdapter: MyAppAdapter
private val appArrayList = ArrayList<App>()
private var lvPoses: ListView? = null
convertView can be null if no view has been created yet. Fix parametr declaration:
override fun getView(position: Int, convertView: View?, parent:
ViewGroup): View? {
...
}
I had similar issues with Asynctasks, not sure if this will help you, but if you just execute the asynctask (without the .get() ). Your code after calling asynctask will run, but the element is still NULL as the Asynctask didnt execute yet, and when it did it was already too late. Try adding .get() after execute (it will freeze the UI, but the postExecute method will be called in time). Or another approach is to set a boolean variable false and make it true when post executed was executed then check every second or so (Timer, Thread) if the variable is true and then execute the rest of the code. For better UI experience add progressdialog between preExecute and postExecute.
Or you can also set the view to be NULL but I dont think that fixes your problem as you dont want it to be NULL.
I am using kotlin to create my adapter which extends BaseAdapter in android. below is the code inside getView method
override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View? {
var binding : ImageBinding
var conView = convertView
if(conView == null){
binding = DataBindingUtil.inflate(LayoutInflater.from(parent?.context),
R.layout.image, parent, false)
conView = binding.root;
conView.tag = binding
}else {
binding = conView.getTag() as ImageBinding
}
return conView;
}
conView.tag = binding and binding = conView.getTag() is highlighted in a pink-like color.When i hover over conView.tag = binding with my mouse, a popup appears with a message Smart cast to android.view.View!. And when i hover over binding = conView.tag,a popup appears with a message Smart cast to android.view.View. Note the difference in the two messages where the latter is missing the platform type sign (!)
How can i implement the two suggested options?
Because convertView is a nullable type variable(View?), var conView = convertView assigment creates a copy of a nullable conView variable. So you should handle conView variable as nullable.
Try the following way:
override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View? {
var binding : ImageBinding
convertView?.let {
binding = it.tag as ImageBinding
return it
}
binding = DataBindingUtil.inflate(LayoutInflater.from(parent?.context), R.layout.image, parent, false)
val conView = binding.root
conView.tag = binding
return conView
}
Note: Semicolons in Kotlin are not required, be aware of that