Two Spinners Populated By Same ArrayList - kotlin

So I am trying to populate two Spinners in the same Fragment, both using the same list, but to display different items.
I have the following data class:
data class ProductTypeObject (
//ProductType fields (2 fields)
var productType: String = "",
var productGroup: String = "",
#ServerTimestamp
var dateEditedTimestamp: Date? = null,
#Exclude #set:Exclude #get:Exclude
var productTypeID: String = ""
) : Serializable {
override fun toString(): String {
return productType
}
}
The Spinner is populated in the Fragment when the list is observed from the ViewModel as below:
// Observe ProductTypes and populate Spinner
businessViewModel.allAppDataProductTypes.observe(viewLifecycleOwner, Observer { productTypeArrayList ->
if (!productTypeArrayList.isNullOrEmpty()){
val adapter = ArrayAdapter(requireContext(), android.R.layout.simple_spinner_item, productTypeArrayList)
binding.inventoryAddEditProductGroupSpinner.adapter = adapter
}
})
This shows a list of product types as I have specified this in the toString()of the object, but is there a way to direct a second Spinner to show a list ofproduct group?

If you don't need to retrieve the values from the spinners, it's easiest to map the values to a new list:
businessViewModel.allAppDataProductTypes.observe(viewLifecycleOwner, Observer { productTypeArrayList ->
if (!productTypeArrayList.isNullOrEmpty()){
//...
val adapter2 = ArrayAdapter(requireContext(), android.R.layout.simple_spinner_item,
productTypeArrayList.map(ProductTypeObject::productGroup)
//...
}
})
If you need both Spinners to be able to retrieve the original item type, then you can't use the ArrayAdapter class as is, since it relies purely on the toString() of your class. You can subclass it like this for a more flexible version that lets you pass property or lambda that is used instead of toString(). I didn't test it, but I think it will do what you want. If you use this class, you don't need to override toString() in your original data class.
class CustomArrayAdapter<T : Any>(
context: Context,
items: List<T>,
val itemToCharSequence: T.() -> CharSequence = Any::toString
) : ArrayAdapter<T>(context, 0, items) {
private val inflater = LayoutInflater.from(context)
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
return (convertView ?: inflater.inflate(android.R.layout.simple_spinner_item, parent, false))
.apply {
val item = getItem(position)!! // will never be null inside getView()
(this as TextView).text = itemToCharSequence(item)
}
}
}
Usage:
val typeAdapter = CustomArrayAdapter(requireContext(), productTypeArrayList, ProductTypeObject::productType)
val groupAdapter = CustomArrayAdapter(requireContext(), productTypeArrayList, ProductTypeObject::productGroup)

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
}

Filter Observed ViewModel data for Spinner based on another Spinners selection (LiveData)

I have two Spinners, each populated by ArrayList, which is observed in the ViewModel from the Fragment as below:
InventoryAddEdit Fragment
// Observe ProductGroups and populate Spinner
businessViewModel.allAppDataProductGroups.observe(viewLifecycleOwner, { productGroupArrayList -> // ArrayList<ProductGroupObject>
if (!productGroupArrayList.isNullOrEmpty()){
val adapter = ArrayAdapter(requireContext(), android.R.layout.simple_spinner_dropdown_item, productGroupArrayList)
binding.inventoryAddEditProductGroupSpinner.adapter = adapter
}
})
// Observe ProductTypes and populate Spinner
businessViewModel.allAppDataProductTypes.observe(viewLifecycleOwner, { productTypeArrayList -> // ArrayList<ProductTypeObject>
if (!productTypeArrayList.isNullOrEmpty()){
val adapter = ArrayAdapter(requireContext(), android.R.layout.simple_spinner_dropdown_item, productTypeArrayList)
binding.inventoryAddEditProductTypeSpinner.adapter = adapter
binding.inventoryAddEditProductTypeSpinner.setSelection(17) // Sets default value
}
})
I am trying to avoid keeping data in the Fragment (MVVM), so I am wondering how best to filter the second ArrayList based on the selection of the first.
I thought I could use the onItemSelected method on the first spinner to cancel the observer and reattach, but then filter the newly observed ArrayList by a selection of the first spinner. However, this seems a bit clunky. Another idea was to create another filtered list in the ViewModel, but that will mean more data in the ViewModel.
Is there another option I am missing, please?
For info, the ProductGroupObject and ProductTypeObject look like this:
ProductGroupObject
#IgnoreExtraProperties
data class ProductGroupObject (
//ProductGroup fields (1 fields)
var productGroup: String = "",
#ServerTimestamp
var dateEditedTimestamp: Date? = null,
#Exclude #set:Exclude #get:Exclude
var productGroupID: String = ""
) : Serializable {
override fun toString(): String {
return productGroup
}
}
ProductTypeObject
#IgnoreExtraProperties
data class ProductTypeObject (
//ProductType fields (2 fields)
var productType: String = "",
var productGroup: String = "",
#ServerTimestamp
var dateEditedTimestamp: Date? = null,
#Exclude #set:Exclude #get:Exclude
var productTypeID: String = ""
) : Serializable {
override fun toString(): String {
return productType
}
fun detailsText(): String {
val detailsString = StringBuilder()
if(productTypeID.isNotEmpty()) detailsString.append("$productTypeID\n")
if(productType.isNotEmpty()) detailsString.append("$productType\n")
if(productGroup.isNotEmpty()) detailsString.append("$productGroup\n")
return detailsString.toString()
}
}
So the best solution I came up with was to create a 'full list' and 'filtered list' for each Spinner data set in the ViewModel + 'current selection' object for each Spinner (also kept in the ViewModel).
The 'full list' is populated by the Cloud Database on startup, the 'filtered lists' are filtered depending on the Spinner selection by way of the following code:
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)
}
}
This updates the 'filtered lists' (filtering the 'full list') which hold data to any of the linked Spinners 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
}
// UPDATE OTHER SPINNERS HERE
Log.d(TAG, "updateCurrentProductGroupVMLiveData(): '_currentProductGroupId.value' updated ($currentProductGroupId)")
}
One issue I faced was that the View was nullable because of how LiveData works, which was solved in the following post: Spinner Listener LiveData Issue

Map Key Values to Dataclass in Kotlin

how can I set properties of a dataclass by its name. For example, I have a raw HTTP GET response
propA=valueA
propB=valueB
and a data class in Kotlin
data class Test(var propA: String = "", var propB: String = ""){}
in my code i have an function that splits the response to a key value array
val test: Test = Test()
rawResp?.split('\n')?.forEach { item: String ->
run {
val keyValue = item.split('=')
TODO
}
}
In JavaScript I can do the following
response.split('\n').forEach(item => {
let keyValue = item.split('=');
this.test[keyValue[0]] = keyValue[1];
});
Is there a similar way in Kotlin?
You cannot readily do this in Kotlin the same way you would in JavaScript (unless you are prepared to handle reflection yourself), but there is a possibility of using a Kotlin feature called Delegated Properties (particularly, a use case Storing Properties in a Map of that feature).
Here is an example specific to code in your original question:
class Test(private val map: Map<String, String>) {
val propA: String by map
val propB: String by map
override fun toString() = "${javaClass.simpleName}(propA=$propA,propB=$propB)"
}
fun main() {
val rawResp: String? = """
propA=valueA
propB=valueB
""".trimIndent()
val props = rawResp?.split('\n')?.map { item ->
val (key, value) = item.split('=')
key to value
}?.toMap() ?: emptyMap()
val test = Test(props)
println("Property 'propA' of test is: ${test.propA}")
println("Or using toString: $test")
}
This outputs:
Property 'propA' of test is: valueA
Or using toString: Test(propA=valueA,propB=valueB)
Unfortunately, you cannot use data classes with property delegation the way you would expect, so you have to 'pay the price' and define the overridden methods (toString, equals, hashCode) on your own if you need them.
By the question, it was not clear for me if each line represents a Test instance or not. So
If not.
fun parse(rawResp: String): Test = rawResp.split("\n").flatMap { it.split("=") }.let { Test(it[0], it[1]) }
If yes.
fun parse(rawResp: String): List<Test> = rawResp.split("\n").map { it.split("=") }.map { Test(it[0], it[1]) }
For null safe alternative you can use nullableString.orEmpty()...

Bind dirty properties of different view-models

I have a tornadoFX application following the MVVM pattern with the model:
data class Person (
val name: String,
val cars: List<Car>
)
data class Car (
val brand: String,
val model: String
)
The application defines the following view:
There is a list-view that lists all persons. Besides the listView is a details-view with a text-field for the person´s name and a table-view for the person´s cars.
A double click on a car entry in the table opens a dialog, in which one can edit the car´s properties.
I want, that if I open the car-details and edit an entry, the changes will be reflected in the table-view. Since i can´t alter the Car-model (which is an immutable type) by adding fx-properties, i came up with the following view-model:
class PersonViewModel(): ItemViewModel<Person> {
val name = bind(Person::name)
val cars = bind { SimpleListProperty<CarViewModel>(item?.cars?.map{CarViewModel(it)}?.observable()) }
override fun onCommit {
// create new person based on ViewModel and store it
}
}
class CarViewModel(item: Car): ItemViewModel<Car> {
val brand = bind(Car::name)
val model = bind(Car::model)
init {
this.item = item
}
}
This way, if double-click on a car-entry in the table-view and open the car-detail-view, an update on the car will be directly reflected in the table-view.
My Problem here is, that I can´t find a way to bind the dirty properties of all my CarViewModels in the table to the PersonViewModel. So if I change a car, the PersonViewModel is not marked as dirty.
Is there a way to bind the dirty-properties of PersonViewModel and CarViewModel? (And also rebind them, if another person is selected).
Or is there even a better way to define my view-models?
I've made a change to the framework to allow ViewModel bindings towards lists to observe ListChange events. This enables you to trigger the dirty state of a list property by altering the list somehow. Merely changing a property inside an item in the list will not trigger it, so in the following example I just get the index of the Car before committing, and reassigning the Car to the same index. This will trigger a ListChange event, which the framework now listens for.
The important action happens in the Car dialog save function:
button("Save").action {
val index = person.cars.indexOf(car.item)
car.commit {
person.cars[index] = car.item
close()
}
}
The index of the car is recorded before the values are committed (to make sure that equals/hashCode matches the same entry), then the newly committed item is inserted in the same index, thus triggering a change event on the list.
Here is a complete example, using mutable JavaFX properties, since they are the idiomatic JavaFX way. You can pretty easily adapt it to using immutable items, or use wrappers.
class Person(name: String, cars: List<Car>) {
val nameProperty = SimpleStringProperty(name)
var name by nameProperty
val carsProperty = SimpleListProperty<Car>(FXCollections.observableArrayList(cars))
var cars by carsProperty
}
class PersonModel : ItemViewModel<Person>() {
val name = bind(Person::nameProperty)
val cars: SimpleListProperty<Car> = bind(Person::carsProperty)
}
class Car(brand: String, model: String) {
val brandProperty = SimpleStringProperty(brand)
var brand by brandProperty
val modelProperty = SimpleStringProperty(model)
var model by modelProperty
}
class CarModel(car: Car? = null) : ItemViewModel<Car>(car) {
val brand = bind(Car::brandProperty)
val model = bind(Car::modelProperty)
}
class DataController : Controller() {
val people = FXCollections.observableArrayList<Person>()
init {
people.add(
Person("Person 1", listOf(Car("BMW", "M3"), Car("Ford", "Fiesta")))
)
}
}
class PersonMainView : View() {
val data: DataController by inject()
val selectedPerson: PersonModel by inject()
override val root = borderpane {
center {
tableview(data.people) {
column("Name", Person::nameProperty)
bindSelected(selectedPerson)
}
}
right(PersonEditor::class)
}
}
class PersonEditor : View() {
val person: PersonModel by inject()
val selectedCar : CarModel by inject()
override val root = form {
fieldset {
field("Name") {
textfield(person.name).required()
}
field("Cars") {
tableview(person.cars) {
column("Brand", Car::brandProperty)
column("Model", Car::modelProperty)
bindSelected(selectedCar)
onUserSelect(2) {
find<CarEditor>().openModal()
}
}
}
button("Save") {
enableWhen(person.dirty)
action {
person.commit()
}
}
}
}
}
class CarEditor : View() {
val car: CarModel by inject()
val person: PersonModel by inject()
override val root = form {
fieldset {
field("Brand") {
textfield(car.brand).required()
}
field("Model") {
textfield(car.model).required()
}
button("Save").action {
val index = person.cars.indexOf(car.item)
car.commit {
person.cars[index] = car.item
close()
}
}
}
}
}
The feature is available in TornadoFX 1.7.17-SNAPSHOT.

Union types / extension interfaces

I have several data class with fields, which are used in forms and need them to have a method return true if any of the fields has been filled.
I don't want to rewrite this for all the classes, so I'm doing it like this at the moment:
data class Order(var consumer: String, var pdfs: List<URI>): Form {
override val isEmpty(): Boolean
get() = checkEmpty(consumer, pdfs)
}
data class SomethingElse(var str: String, var set: Set<String>): Form {
override val isEmpty(): Boolean
get() = checkEmpty(str, set)
}
interface Form {
val isEmpty: Boolean
fun <T> checkEmpty(vararg fields: T): Boolean {
for (f in fields) {
when (f) {
is Collection<*> -> if (!f.isEmpty()) return false
is CharSequence -> if (!f.isBlank()) return false
}
}
return true;
}
}
This is obviously not very pretty nor type-safe.
What's a more idiomatic way of doing this, without abstracting every property into some kind of Field-type?
Clarification: What I'm looking for is a way to get exhaustive when, for example by providing all the allowed types (String, Int, List, Set) and a function for each to tell if they're empty. Like an "extension-interface" with a method isEmptyFormField.
It's kinda hacky but should work.
Every data class creates set of method per each constructor parameters. They're called componentN() (where N is number starting from 1 indicating constructor parameter).
You can put such methods in your interface and make data class implicitly implement them. See example below:
data class Order(var consumer: String, var pdfs: List) : Form
data class SomethingElse(var str: String, var set: Set) : Form
interface Form {
val isEmpty: Boolean
get() = checkEmpty(component1(), component2())
fun checkEmpty(vararg fields: T): Boolean {
for (f in fields) {
when (f) {
is Collection -> if (!f.isEmpty()) return false
is CharSequence -> if (!f.isBlank()) return false
}
}
return true;
}
fun component1(): Any? = null
fun component2(): Any? = null
}
You can also add fun component3(): Any? = null etc... to handle cases with more that 2 fields in data class (e.g. NullObject pattern or handling nulls directly in your checkEmpty() method.
As I said, it's kinda hacky but maybe will work for you.
If all you are doing is checking for isEmpty/isBlank/isZero/etc. then you probably don't need a generic checkEmpty function, etc.:
data class Order(var consumer: String, var pdfs: List<URI>) : Form {
override val isEmpty: Boolean
get() = consumer.isEmpty() && pdfs.isEmpty()
}
data class SomethingElse(var str: String, var set: Set<String>) : Form {
override val isEmpty: Boolean
get() = str.isEmpty() && set.isEmpty()
}
interface Form {
val isEmpty: Boolean
}
However, if you are actually do something a bit more complex then based on your added clarification I believe that "abstracting every property into some kind of Field-type" is exactly what you want just don't make the Field instances part of each data class but instead create a list of them when needed:
data class Order(var consumer: String, var pdfs: List<URI>) : Form {
override val fields: List<Field<*>>
get() = listOf(consumer.toField(), pdfs.toField())
}
data class SomethingElse(var str: String, var set: Set<String>) : Form {
override val fields: List<Field<*>>
get() = listOf(str.toField(), set.toField())
}
interface Form {
val isEmpty: Boolean
get() = fields.all(Field<*>::isEmpty)
val fields: List<Field<*>>
}
fun String.toField(): Field<String> = StringField(this)
fun <C : Collection<*>> C.toField(): Field<C> = CollectionField(this)
interface Field<out T> {
val value: T
val isEmpty: Boolean
}
data class StringField(override val value: String) : Field<String> {
override val isEmpty: Boolean
get() = value.isEmpty()
}
data class CollectionField<out C : Collection<*>>(override val value: C) : Field<C> {
override val isEmpty: Boolean
get() = value.isEmpty()
}
This gives you type-safety without changing your data class components, etc. and allows you to "get exhaustive when".
You can use null to mean "unspecified":
data class Order(var consumer: String?, var pdfs: List<URI>?) : Form {
override val isEmpty: Boolean
get() = checkEmpty(consumer, pdfs)
}
data class SomethingElse(var str: String?, var set: Set<String>?) : Form {
override val isEmpty: Boolean
get() = checkEmpty(str, set)
}
interface Form {
val isEmpty: Boolean
fun <T> checkEmpty(vararg fields: T): Boolean = fields.all { field -> field == null }
}
The idea here is the same as that of an Optional<T> in Java but without the extra object, etc.
You now have to worry about null safety but if your fields are meant to have a concept of absent/empty then this seems appropriate (UsingAndAvoidingNullExplained · google/guava Wiki).