How to change a function argument value [duplicate] - kotlin

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

Kotlin on Android: How to use LiveData from a database in a fragment?

I use MVVM and have a list of data elements in a database that is mapped through a DAO and repository to ViewModel functions.
Now, my problem is rather banal; I just want to use the data in fragment variables, but I get a type mismatch.
The MVVM introduces a bit of code, and for completeness of context I'll run through it, but I'll strip it to the essentials:
The data elements are of a data class, "Objects":
#Entity(tableName = "objects")
data class Objects(
#ColumnInfo(name = "object_name")
var objectName: String
) {
#PrimaryKey(autoGenerate = true)
var id: Int? = null
}
In ObjectsDao.kt:
#Dao
interface ObjectsDao {
#Query("SELECT * FROM objects")
fun getObjects(): LiveData<List<Objects>>
}
My database:
#Database(
entities = [Objects::class],
version = 1
)
abstract class ObjectsDatabase: RoomDatabase() {
abstract fun getObjectsDao(): ObjectsDao
companion object {
// create database
}
}
In ObjectsRepository.kt:
class ObjectsRepository (private val db: ObjectsDatabase) {
fun getObjects() = db.getObjectsDao().getObjects()
}
In ObjectsViewModel.kt:
class ObjectsViewModel(private val repository: ObjectsRepository): ViewModel() {
fun getObjects() = repository.getObjects()
}
In ObjectsFragment.kt:
class ObjectsFragment : Fragment(), KodeinAware {
private lateinit var viewModel: ObjectsViewModel
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel = ViewModelProvider(this, factory).get(ObjectsViewModel::class.java)
// I use the objects in a recyclerview; rvObjectList
rvObjectList.layoutManager = GridLayoutManager(context, gridColumns)
val adapter = ObjectsAdapter(listOf(), viewModel)
// And I use an observer to keep the recyclerview updated
viewModel.getObjects.observe(viewLifecycleOwner, {
adapter.objects = it
adapter.notifyDataSetChanged()
})
}
}
The adapter:
class ObjectsAdapter(var objects: List<Objects>,
private val viewModel: ObjectsViewModel):
RecyclerView.Adapter<ObjectsAdapter.ObjectsViewHolder>() {
// Just a recyclerview adapter
}
Now, all the above works fine - but my problem is that I don't want to use the observer to populate the recyclerview; in the database I store some objects, but there are more objects that I don't want to store.
So, I try to do this instead (in the ObjectsFragment):
var otherObjects: List<Objects>
// ...
if (condition) {
adapter.objects = viewModel.getObjects()
} else {
adapter.objects = otherObjects
}
adapter.notifyDataSetChanged()
And, finally, my problem; I get type mismatch for the true condition assignment:
Type mismatch: inferred type is LiveData<List> but List was expected
I am unable to get my head around this. Isn't this pretty much what is happening in the observer? I know about backing properties, such as explained here, but I don't know how to do that when my data is not defined in the ViewModel.
We need something to switch data source. We pass switching data source event to viewModel.
mySwitch.setOnCheckedChangeListener { _, isChecked ->
viewModel.switchDataSource(isChecked)
}
In viewModel we handle switching data source
(To use switchMap include implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.4.0")
class ObjectsViewModel(private val repository: ObjectsRepository) : ViewModel() {
// Best practice is to keep your data in viewModel. And it is useful for us in this case too.
private val otherObjects = listOf<Objects>()
private val _loadDataFromDataBase = MutableLiveData<Boolean>()
// In case your repository returns liveData of favorite list
// from dataBase replace MutableLiveData(otherObjects) with repository.getFavorite()
fun getObjects() = _loadDataFromDataBase.switchMap {
if (it) repository.getObjects() else MutableLiveData(otherObjects)
}
fun switchDataSource(fromDataBase: Boolean) {
_loadDataFromDataBase.value = fromDataBase
}
}
In activity/fragment observe getObjects()
viewModel.getObjects.observe(viewLifecycleOwner, {
adapter.objects = it
adapter.notifyDataSetChanged()
})
You can do something like this:
var displayDataFromDatabase = true // Choose whatever default fits your use-case
var databaseList = emptyList<Objects>() // List we get from database
val otherList = // The other list that you want to show
toggleSwitch.setOnCheckedChangeListener { _, isChecked ->
displayDataFromDatabase = isChecked // Or the negation of this
// Update adapter to use databaseList or otherList depending upon "isChecked"
}
viewModel.getObjects.observe(viewLifecycleOwner) { list ->
databaseList = list
if(displayDataFromDatabase)
// Update adapter to use this databaseList
}

Spinner Listener LiveData Issue

In my Fragment, I have two Spinners.
Both are populated by LiveData which is observed in the ViewModel as below:
// Observe Filtered ProductGroups and populate Spinner
businessViewModel.filteredAppDataProductGroups.observe(viewLifecycleOwner, { productGroupArrayList ->
if (!productGroupArrayList.isNullOrEmpty()){
val adapter = ArrayAdapter(requireContext(), android.R.layout.simple_spinner_dropdown_item, productGroupArrayList)
binding.inventoryAddEditProductGroupSpinner.adapter = adapter
}
})
// Observe Filtered ProductTypes and populate Spinner
businessViewModel.filteredAppDataProductTypes.observe(viewLifecycleOwner, { productTypeArrayList ->
if (productTypeArrayList != null){
val adapter = ArrayAdapter(requireContext(), android.R.layout.simple_spinner_dropdown_item, productTypeArrayList)
binding.inventoryAddEditProductTypeSpinner.adapter = adapter
}
})
This works fine, but I am trying to filter the data in the second Spinner based on the current selection of the first, by setting a listener and updating ViewModel data as below:
binding.inventoryAddEditProductGroupSpinner.onItemSelectedListener = object: AdapterView.OnItemSelectedListener{
override fun onItemSelected(parent:AdapterView<*>, view: View, position: Int, id: Long){
val productGroupObject = parent.selectedItem as ProductGroupObject
if (productGroupObject.productGroupID.isNotEmpty()){
businessViewModel.updateCurrentProductGroupVMLiveData(productGroupObject.productGroupID)
}
}
override fun onNothingSelected(parent: AdapterView<*>){
}
}
Which updates the filtered list in the ViewModel as below:
fun updateCurrentProductGroupVMLiveData (currentProductGroupId: String) {
val newProductGroup = allAppDataProductGroups.value?.find { productGroup -> productGroup.productGroupID == currentProductGroupId }
_currentProductGroup.value = newProductGroup
if(newProductGroup?.productGroup != null) {
val filteredProductsList = allAppDataProductTypes.value?.filter { productTypeObject -> productTypeObject.productGroup == newProductGroup.productGroup} as ArrayList<ProductTypeObject>
_filteredAppDataProductTypes.value = filteredProductsList
}
Log.d(TAG, "updateCurrentProductGroupVMLiveData(): '_currentProductGroupId.value' updated ($currentProductGroupId)")
}
However, if I add the code shown into onItemSelected, the app crashes and I see the following error:
--------- beginning of crash
2021-10-06 23:06:34.740 17510-17510/com.xxxxx.acorn E/AndroidRuntime: FATAL EXCEPTION: main
Process: com.locators.acorn, PID: 17510
java.lang.NullPointerException: Parameter specified as non-null is null: method kotlin.jvm.internal.Intrinsics.checkNotNullParameter, parameter view
at com.xxxxx.acorn.business.inventory.InventoryAddEditFragment$setSpinnerListeners$2.onItemSelected(InventoryAddEditFragment.kt)
at android.widget.AdapterView.fireOnSelected(AdapterView.java:931)
at android.widget.AdapterView.dispatchOnItemSelected(AdapterView.java:920)
at android.widget.AdapterView.-wrap1(AdapterView.java)
at android.widget.AdapterView$SelectionNotifier.run(AdapterView.java:890)
at android.os.Handler.handleCallback(Handler.java:751)
at android.os.Handler.dispatchMessage(Handler.java:95)
at android.os.Looper.loop(Looper.java:154)
at android.app.ActivityThread.main(ActivityThread.java:6077)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:866)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:756)
I'm seeing that the ViewModel list is changing, so I'm at a loss to why this is causing a NullPointerException.
Seems the error log pointing to view (at 3rd line)
java.lang.NullPointerException: Parameter specified as non-null is null: method kotlin.jvm.internal.Intrinsics.checkNotNullParameter, parameter view
I think those parent: AdapterView<*> and view: View are nullable type.
Therefore need to add null safe call operator for that type
binding.inventoryAddEditProductGroupSpinner.onItemSelectedListener = object: AdapterView.OnItemSelectedListener{
override fun onItemSelected(parent:AdapterView<*>?, view: View?, position: Int, id: Long){
...
}
override fun onNothingSelected(parent: AdapterView<*>?){
}
}
You can simplify and clean your code.
Just pass the selected position to your viewmodel
override fun onItemSelected(parent:AdapterView<*>, view: View, position: Int, id: Long){
businessViewModel.updateCurrentProductGroup(position)
}
And inside your viewmodel class write the main business logic. like the following
fun updateCurrentProductGroup(position: Int){
// here you can do what you want
// example
val productGroupObject = filteredAppDataProductGroups.value[position]
if (productGroupObject.productGroupID.isNotEmpty()){
businessViewModel.updateCurrentProductGroupVMLiveData(productGroupObject.productGroupID)
}
}

How to express in Kotlin "assign value exactly once on the first call"?

Looking for a natural Kotlin way to let startTime be initialized only in a particular place and exactly once.
The following naive implementation have two problems:
it is not thread safe
it does not express the fact "the variable was or will be assigned exactly once in the lifetime of an Item instance"
class Item {
var startTime: Instant?
fun start(){
if (startTime == null){
startTime = Instant.now()
}
// do stuff
}
}
I believe some kind of a delegate could be applicable here. In other words this code needs something similar to a lazy variable, but without initialization on first read, instead it happens only after explicit call of "touching" method. Maybe the Wrap calls could give an idea of possible implementation.
class Wrap<T>(
supp: () -> T
){
private var value: T? = null
private val lock = ReentrantLock()
fun get(){
return value
}
fun touch(){
lock.lock()
try{
if (value == null){
value = supp()
} else {
throw IllegalStateExecption("Duplicate init")
}
} finally{
lock.unlock()
}
}
}
How about combining AtomicReference.compareAndSet with a custom backing field?
You can use a private setter and make sure that the only place the class sets the value is from the start() method.
class Item(val value: Int) {
private val _startTime = AtomicReference(Instant.EPOCH)
var startTime: Instant?
get() = _startTime.get().takeIf { it != Instant.EPOCH }
private set(value) = check(_startTime.compareAndSet(Instant.EPOCH, value)) { "Duplicate set" }
fun start() {
startTime = Instant.now()
}
override fun toString() = "$value: $startTime"
}
fun main() = runBlocking {
val item1 = Item(1)
val item2 = Item(2)
println(Instant.now())
launch { println(item1); item1.start(); println(item1) }
launch { println(item1) }
delay(1000)
println(item2)
item2.start()
println(item2)
println(item2)
item2.start()
}
Example output:
2021-07-14T08:20:27.546821Z
1: null
1: 2021-07-14T08:20:27.607365Z
1: 2021-07-14T08:20:27.607365Z
2: null
2: 2021-07-14T08:20:28.584114Z
2: 2021-07-14T08:20:28.584114Z
Exception in thread "main" java.lang.IllegalStateException: Duplicate set
I think your Wrap class is a good starting point to implement this. I would definitely make it a property delegate and touch() could be much simplified:
fun touch() {
synchronized(this) {
check(value == null) { "Duplicate init" }
value = supp()
}
}
Then you can remove lock. But generally, this is a good approach.
If you would like to reuse lazy util from stdlib then you can do this by wrapping it with another object which does not read its value until asked:
class ManualLazy<T : Any>(private val lazy: Lazy<T>) {
operator fun getValue(thisRef: Any?, property: KProperty<*>): T? {
return if (lazy.isInitialized()) lazy.value else null
}
fun touch() {
lazy.value
}
}
class Item {
private val _startTime = ManualLazy(lazy { Instant.now() })
val startTime: Instant? by _startTime
fun start(){
_startTime.touch()
}
}
Of course, depending on your needs you can implement it in a much different way, using a similar technique.
This may be considered exploiting or hacking lazy util. I agree and I think Wrap approach is a better one.

Why can't I use this as paramter of observe event when I use LiveData with Kotlin?

I use LiveData in my layout file, and add observe event for some LiveData variable , you can see Code C.
1: Why can I use assign either this.viewLifecycleOwner or this to binding.lifecycleOwner in Code A?
2: I think mHomeViewModel.listVoiceBySort.observe(this) {... } in Code B can work well, but in fact, it failed, why?
Code A
binding.lifecycleOwner = this.viewLifecycleOwner //It can work well
binding.lifecycleOwner = this //It can work well
Code B
mHomeViewModel.listVoiceBySort.observe(viewLifecycleOwner) { //It can work well
mHomeViewModel.listVoiceBySort.observe(this) { //It cannot work
Code C
class FragmentHome : Fragment() {
private lateinit var binding: LayoutHomeBinding
private val mHomeViewModel by lazy {
getViewModel {
HomeViewModel(mActivity.application, provideRepository(mContext))
}
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
binding = DataBindingUtil.inflate(
inflater, R.layout.layout_home, container, false
)
binding.lifecycleOwner = this.viewLifecycleOwner
//binding.lifecycleOwner = this //It can work well
binding.aHomeViewModel = mHomeViewModel
mHomeViewModel.listVoiceBySort.observe(viewLifecycleOwner) {
//mHomeViewModel.listVoiceBySort.observe(this) { //It cannot work
myAdapter.submitList(it)
}
...
return binding.root
}
private fun showActionMenu() {
val view = mActivity.findViewById<View>(R.id.menuMoreAction) ?: return
PopupMenu(mContext, view).run {
menuInflater.inflate(R.menu.menu_option_action, menu)
for (item in menu){
if (item.itemId == R.id.menuMoreActionShowCheckBox){
mHomeViewModel.displayCheckBox.observe(this#FragmentHome){
//mHomeViewModel.displayCheckBox.observe(this#FragmentHome.viewLifecycleOwner){ //It can work well
if (it){
item.title =mContext.getString(R.string.menuMoreActionHideCheckBox)
}else{
item.title =mContext.getString(R.string.menuMoreActionShowCheckBox)
}
}
}
}
...
}
}
...
}
viewLifecycleOwner is available only after view of the Fragment is being inflated. Referencing the Fragment this for lifecycleOwner will use the Fragment lifecycle.
You can use this (referencing the Fragment) before view of the fragment is inflated, in onAttach() and onCreateView(). While viewLifecycle can be used in and after any point after view is completely created, after onViewCreated().
1: Why can I use assign either this.viewLifecycleOwner or this to binding.lifecycleOwner in Code A?
Because they are both LifecycleOwner. Although you generally want to use viewLifecycleOwner from onViewCreated, otherwise you can get bugs on fragments that are in "limbo state" (replace().addToBackStack()'d).
2.) private fun showActionMenu() { ... observe(
You generally shouldn't "start observing" things that are triggered by side-effects, otherwise you will end up with observers that don't unregister even after your PopupMenu is dismissed, for example.

Recycler View recycle issue

I have a recyclerView. When I do the pull to refresh, if the new data is just one list item, then the recycler view loads the item perfectly. But if the updated data contains 2 or more, then I think the view is not recycled properly. In the actionContainer, there should only one item to be added for each of the updated list item. But during pull to refresh, ONLY WHEN there are 2 or more list items to be updated, the actionContainer shows 2 data where it should be only one. Can someone help me to fix this?
override fun onBindViewHolder(holder: HistoryListAdapter.ViewHolder?, position: Int) {
info("onBindViewHolder =>"+listAssets.size)
info("onBindViewHolder itemCount =>"+itemCount)
info("onBindViewHolder position =>"+position)
val notesButton = holder?.notesButton
val notesView = holder?.notesTextView
val dateTime = listAssets[position].date
val location = listAssets[position].location
val sessionId = listAssets[position].id
holder?.sessionID = sessionId
holder?.portraitImageView?.setImageDrawable(listAssets[position].image)
holder?.titleTextView?.text = DateTimeFormatter.getFormattedDate(context, dateTime)
val timeString = DateTimeFormatter.getFormattedTime(context, dateTime)
if (location.length != 0) {
holder?.subtitleTextView?.text = "$timeString # $location"
} else {
holder?.subtitleTextView?.text = "$timeString"
}
val data = listAssets[position].data
for (actionData in data) {
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
val parent = inflater.inflate(R.layout.history_card_action, null)
val icon = parent?.findViewById(R.id.historyActionIcon) as ImageView
val title = parent?.findViewById(R.id.historyActionTitle) as TextView
val subtitle = parent?.findViewById(R.id.historyActionSubtitle) as TextView
var iconDrawable: Drawable? = null
when(actionData.type) {
ActionType.HEART -> {
iconDrawable = ContextCompat.getDrawable(context, R.drawable.heart)
}
ActionType.LUNGS -> {
iconDrawable = ContextCompat.getDrawable(context, R.drawable.lungs)
}
ActionType.TEMPERATURE -> {
iconDrawable = ContextCompat.getDrawable(context, R.drawable.temperature)
}
}
icon.setImageDrawable(iconDrawable)
val titleString = actionData.title
titleString?.let {
title.text = titleString
}
val subtitleString = actionData.subtitle
subtitleString?.let {
subtitle.text = subtitleString
}
holder?.actionContainer?.addView(parent)
}
val notes = listAssets[position].notes
notesView?.text = notes
if (notes.length == 0) {
notesButton?.layoutParams?.width = 0
} else {
notesButton?.layoutParams?.width = toggleButtonWidth
}
if (expandedNotes.contains(sessionId)) {
notesView?.expandWithoutAnimation()
} else {
notesView?.collapseWithoutAnimation()
}
notesButton?.onClick {
notesView?.toggleExpansion()
}
}
data class ListAssets(val id: String,
val date: Date,
val location: String,
val notes: String,
val image: Drawable,
val data: ArrayList<ListData>)
data class ListData(val type: ActionType,
val title: String?,
val subtitle: String?)
override fun onViewRecycled(holder: HistoryListAdapter.ViewHolder?) {
super.onViewRecycled(holder)
if (holder != null) {
holder.actionContainer.removeAllViewsInLayout()
holder.actionContainer.removeAllViews()
val notesTextView = holder.notesTextView
if (notesTextView != null) {
if (notesTextView.expandedState) {
val sessionID = holder.sessionID
sessionID?.let {
val sessionSearch = expandedNotes.firstOrNull {
it.contentEquals(sessionID)
}
if (sessionSearch == null) {
expandedNotes.add(sessionID)
}
}
} else {
val sessionID = holder.sessionID
sessionID?.let {
val sessionSearch = expandedNotes.firstOrNull {
it.contentEquals(sessionID)
}
if (sessionSearch != null) {
expandedNotes.remove(sessionSearch)
}
}
}
}
}
}
First, you should probably not override onViewRecycled() unless you have to perform some very particular resources cleanup.
The place where you want to setup your views before display is onBindViewHolder().
Second, you don't need not add or remove views dynamically in a RecyclerView item, it's simpler and more efficient to only switch the visibility of the view between VISIBLE and GONE. In cases where this is not enough because views are too different, you should declare different view types, which will create separate ViewHolders for each view type.
You should not remove or add any view while overriding onBindViewHoder() method of RecyclerView Adapter because next time when a recycled layout is used, the removed views will not be found. Instead of this you can use show/hide on a view.
If you add any view to the layout dynamically, later on when this layout is recycled, it also contains the extra view which you have added before.
Similarly, if you remove any view from the layout dynamically, later on when this layout is recycled, it does not contain the view which you have removed earlier.
I have implemented a RecyclerView and Retrofit,it has the SwipeView layout (Pull to Refresh).Here is the link to the repisitory.
https://github.com/frankodoom/Retrofit-RecyclerVew