I comes from Java background and working first time on Kotlin. For most of the people it will be basic question, but it may help people who start working first time on Kotlin and comes from Java background
So, let say I have listadapter and I want to set list of item in that. I have two options now.
1) create a private property which stores list of items and then create a setter for it, which set the list and call notifydatasetChanged()
2) create a property with set property function and then access like instance.property
Which will be better option in Kotlin out of above two options.
A property in Kotlin is nothing else then a getter and setter for a value. If you don't want to provide a getter, you have to use a fun setData(data: List).
Otherwise it's also possible to handle everything with the property
var data = listOf()
set(data: List) {
field = data
notifydatasetChanged()
}
But eventually it's even better to use an implementation with DiffUtil.
Related
I already have a list adapter that works properly. But I want to divide the object in the list into sections according to the date they were created. Something like this:
I found something called "sectioned recycler view" but couldn't find any documentation on that. I read all the related questions, but they all are either outdated or use a third-party library. What's the native way of implementing this feature?
There are a couple of approaches you could use. First the easy one:
make the header part of your item layout, but with GONE visibility by default
in onBindViewHolder, decide whether the header should be VISIBLE or GONE
The logic there depends on what you want, but it could be as simple as
val visible = position == 0 || items[position].date != items[position - 1].date
Basically you just need to work out what the condition is that would cause an item to be in a different "group" than the previous item, and then if it's met, show the header over that item.
The approach MarkL is talking about is more complex, but it's also more robust - by having separate Item and Header elements, you can treat them differently, and even do stuff like having the header collapse/show its children, select them all etc. You can do that with the other approach, but it requires more code since you're not really treating things as groups, it's more of a trick when it comes to displaying stuff.
Basically, ignoring the how for now, you need a list of headers and items. A sealed class is a good way to represent that:
sealed class ListElement {
data class Header(val date: Date) : ListElement()
data class Item(val itemData: YourItem) : ListElement()
}
I've made Item a wrapper class that holds your actual data object inside, since that's probably coming from elsewhere and you can't define it as part of this sealed class hierarchy - so sticking it inside one of the subclasses like this allows you to do that.
So now you can have a List<ListElement> containing Headers and Items in display order. Since you've mentioned creating the ViewHolders in a comment I won't explain all that, but basically when you're getting the item type for a position, you just need to check is Header or is Item and then handle it from there.
As for creating that list, there are lots of ways to do it - you could use groupBy to generate a Map of dates to lists of items, map each of those entries to a list of Header, Item, Item..., and flatten the whole thing into a single list:
items.map { Item(it) }
.groupBy { it.itemData.date }
.entries
.flatMap { (date, items) -> listOf(Header(date)) + items }
The advantage with a map like this is it's an actual grouped structure, so you can keep it around to generate flat lists for display - e.g. hiding a group's contents by only including the header in the list.
Or you could just build the list yourself, similar to the logic I mentioned in the first example - if the date has changed from the previous item, insert a Header first:
val list = mutableListOf<ListElement>().apply {
for (item in items) {
// add a header if the date changed - this handles the first header
// in an empty list too (where lastOrNull returns null, so the date is null)
val previousItemDate = (lastOrNull() as? Item)?.itemData?.date
if (previousItemDate != item.date) add(Header(item.date))
add(Item(item))
}
}
Or you could use fold. Don't forget to sort stuff!
You could create 2 types of view holders:
header which holds the date
data container which holds the other information.
And then create a list of objects which contain something like this:
listToBind = (header, data, data, header, data, data)
For your case, where header & info is the same object, you can do something like this:
take your object you receive from backend (example)
YourObject(val header: String, val info:InfoObject)
create 2 display objects, both inheriting from a type that your adapter accepts (say - AdapterDisplayEntity)
HeaderDisplayEntity(val header: String): AdapterDisplayEntity
InfoDisplayEntity(val info: InfoObject): AdapterDisplayEntity
now you can use your list that contains these items and submit to your adapter.
Use nested recycler view for this instead. You can check the example here.
Best solution for this scenario so far.
If you are using Jetpack Compose you can use the stickyHeader() as documented in the documentation
I'm new to all of this and was making good progress but I have hit a brick wall with working out how to do the next thing. I am using Kotlin and have a Fragment with an associated Recyclerview Adapter. I would like to set OnClick (On or Off) against items in a row depending upon a value in the Fragment which could change at any time.
My adapter works fine to show and update the array of data and also to implement OnClick.
I have tried sending a data element via a constructor which changed in the fragment but always showed as the initial setting in the adapter. The same with trying to call a method.
Many other questions touch on the issue but only show snippets of code, and it seems that I'm not advanced enough to get them working successfully in my code.
Could anyone please provide a pointer to a working set of Kotlin code that includes parsing a variable from fragment to adapter - perhaps in Git or a tutorial. I'm sure that if I can study a working program I can move forward. Thank you.
It would have been better if you had included your Adapter and Fragment code in the question, that would have helped us in understanding how you have setup everything and what data model are you passing to adapter.
But looking at your question, one solution that comes to my mind is to add an enabled boolean in your data model that is displayed in the ViewHolder. Using this you can set view.clickable = model.enabled. Now whenever your "value in the Fragment" changes you can update this list and let the adapter rebind items.
Note that the above solution is when you want to selectively enable/disable clicks on individual items. If you want to do this for all items at once, it's better to create a variable in adapter that you can change from the Fragment, and inside the clickListener you can check the value of that adapter variable. If it's false, just return out of the click listener. Something like,
view.setOnClickListener {
if(adapterValue) {
// handle Click
}
}
If this approach doesn't help, I would ask you to add more context in your question and show what you have done so far.
Hello i want to know why is my program changing selectedDataEdited List when i only changing editTransactionList ?
var editTransactionList: MutableList<Transaction>? = mutableListOf()
var selectedDataEdited: List<Transaction>? = listOf()
editTransactionList = listTest as MutableList<Transaction>
selectedDataEdited = listTest
var position = 0
println("edit $editTransactionList")
println("select $selectedDataEdited")
editTransactionList.get(position).apply {
amount = 2000
name = "K"
}
println("edit $editTransactionList")
println("select $selectedDataEdited")
editTransactionList.get(position).apply {
amount = 3000
name = "Z"
}
println("edit $editTransactionList")
println("select $selectedDataEdited")
the output is
edit [Transaction(amount=1000, name=T, test=1)]
select [Transaction(amount=1000, name=T, test=1)]
edit [Transaction(amount=2000, name=K, test=1)]
select [Transaction(amount=2000, name=K, test=1)]
edit [Transaction(amount=3000, name=Z, test=1)]
select [Transaction(amount=3000, name=Z, test=1)]
Variables are basically references. When you store an object in a variable you actually say "when using this variable please refer to this object". So if you "store" the same object into 2 different variables, each of them still refers to that same object. Getting the object using the first variable, making changes to it, and then accessing the second variable, will still get you that changed object.
You will need to copy the list to prevent the unwanted behavior. Keep in mind though that you would probably need a deep copy. Simply calling toList() on it for example only makes a shallow copy, which means that even though it will be a different list, the objects inside it will still refer to the original.
It's hard to tell what would work without knowing what Transaction looks like. If Transaction is a data class then selectedDataEdited = listTest.map { it.copy() } might work. See this example https://pl.kotl.in/Q_o8pYXVs
KOTLIN Why when i changed a List it is also accidentally changing another list
Because you don't have "another" list. You only have one list.
When you do selectedDataEdited = listTest, you assign a second reference to the same list. If you want two separate lists, you must create them, possibly by cloning the original list.
Instead of using as MutableList use toMutableList:
editTransactionList = listTest.toMutableList()
It will make a copy of your list instead of passing a reference to the same list.
I'm using SelectionTracker form the support-library-v28
It works great, just as expected.
Only thing I need, is to enable Select All feature (using the ToolBar)
Looking at the API, I see that there is one way to select all, but that one requires creating Iterable<Long> with all the values, meaning, create an array which hold Long values from 1 to datasource.size()
Is there any simpler way to select all the items in my datasource?
Seeing as I've recently had to set up the same sort of functionality, I thought I'd share my approach. Using the setItemsSelected(Iterable<K> keys, boolean selected) method really isn't as complex as it seems.
Yes, you will need to pass in an iterable. What I did was, loop through my data and store the index of each item as a 'long' inside of an arrayList().
EX:
yourData.forEachWithIndex { i, item ->
//Be sure to start at one, just plus one
someOtherTempArray.add(i.toLong() + 1)
}
Then I created a method that makes it easy to 'trigger' the select all functionality:
EX:
private fun startHandler(isChecked: Boolean){
val handler = android.os.Handler(Looper.getMainLooper())
val runnable = Runnable {
kotlin.run {
mTracker!!.setItemsSelected(someOtherTempArray.asIterable(),
isChecked)
}
}
handler.post(runnable)
}
The 'isChecked' parameter, which will essentially determine if we want to select all or deselect all.
Note that I am simply using my arrayList of 'keys' (for me this is just the index of the item. However, this may differ depending on how you have your ItemDetails Builder set up) and calling the Kotlin .asIterable() function to turn it into an iterable.
I have also placed the call to setItemsSelected() inside of a handler to force the selection to take place on the UI thread.
Now whenever you want to select/deselect all, you can call the startHandler(true)!
I'm trying to bind a MvxBindableListView in TwoWay mode, for it to update in the View when I Set it's value in the ViewModel (through a Buttons's Click command).
Currently it only updates when the layout is fully loaded at start/tabchange...
The ViewModel is:
public List<MyType> TestList
{
get { return _testList; }
set
{
_testList = value;
FirePropertyChanged("TestList");
}
}
The .axml in the View is:
<Mvx.MvxBindableListView
android:layout_width="fill_parent"
android:layout_height="fill_parent"
local:MvxBind="{'ItemsSource':{'Path':'TestList','Mode':'TwoWay'}}"
local:MvxItemTemplate="#layout/my_item_layout" />
The way data-binding works is through an interface called INotifyPropertyChanged
What happens in this interface is that the ViewModel sends the View a message whenever a property changes - e.g.
FirePropertyChanged("TestList");
With a list, this doesn't help if the contents of the list itself change - e.g. when the list has an item added or removed.
To solve this, the .Net Mvvm implementation includes another interface INotifyCollectionChanged.
A collection - such as a list - can implement INotifyCollectionChanged in order to let the View know when the contents of the collection change.
For example, the collection might fire events containing hints such as:
everything has changed - NotifyCollectionChangedAction.Reset
an item has been added - NotifyCollectionChangedAction.Add
an item has been removed - NotifyCollectionChangedAction.Remove
...
There's a short introduction into this interface about 12:30 into the MvvmCross Xaminar http://www.youtube.com/watch?v=jdiu_dH3z5k
To use this interface for a small in-memory list - e.g. less than 1000 'small' objects - all you have to do is to change your List<T> for an ObservableCollection<T> - the ObservableCollection is a class from the core .Net libraries (from Microsoft or Mono) and it will fire the correct events when you Add/Remove list items.
You can see the source for the Mono ObservableCollection implementation in: https://github.com/mosa/Mono-Class-Libraries/blob/master/mcs/class/System/System.Collections.ObjectModel/ObservableCollection.cs - it is worth taking some time to look at this implementation so that you can understand a bit more about how Mvvm works with INotifyCollectionChanged.
If you use the ObservableCollection class, then your code will become:
private ObservableCollection<MyType> _testList;
public ObservableCollection<MyType> TestList
{
get { return _testList; }
set
{
_testList = value;
FirePropertyChanged("TestList");
// in vNext use RaisePropertyChanged(() => TestList);
}
}
with:
<Mvx.MvxBindableListView
android:layout_width="fill_parent"
android:layout_height="fill_parent"
local:MvxBind="{'ItemsSource':{'Path':'TestList'}}"
local:MvxItemTemplate="#layout/my_item_layout" />
Note:
that the binding is OneWay - this means that binding is still only going from ViewModel to View - there are no updates going from View to ViewModel.
that ObservableCollection is designed to be single-threaded - so make sure all changes to the collection are done on the UI thread - not on a worker thread. If you need to, you can marshall work back onto the UI thread using InvokeOnMainThread(() => { /* do work here */ }) in a ViewModel.
that in Android, the way lists work (through the base AdapterView) means that every time you call any update on the ObservableCollection then the UI List will ignore the action hint (Add, Remove, etc) - it will treat every change as a Reset and this will cause the entire list to redraw.
For larger collections - where you don't want all the items in memory at the same time - you may need to implement some data-store backed list yourself.
There is a brief example of one simple sqlite data-backed store in https://github.com/slodge/MvvmCross/blob/vnext/Sample%20-%20SimpleDialogBinding/SimpleDroidSql.Core/DatabaseBackedObservableCollection.cs
This virtualizing of collection data is common in WP and WPF apps - e.g. see questions and answers like Is listbox virtualized by default in WP7 Mango?
We just found a workaround for this that works for us!!
NOTE: Adding and removing from the list updates the view with the new/removed item. However any changes to the state of the existing items were not reflected.
SOLUTION: We cleared our list and re-added the items to the ViewModel property with the updated state. Calling raisepropertychanged then mimics a two-way bind behavior. Essentially it was removing all values and re-adding all values.