Using the android library androidx.recyclerview.selection, I have tried to implement multiple selection in RecyclerView following the tutorials here and here.
But, I want my key to be a String, instead of Long, but I am facing two errors as follows:
tracker = SelectionTracker.Builder<String>(
"selection_id",
recyclerView,
StableIdKeyProvider(recyclerView), // this line shows error
MyItemDetailsLookup(recyclerView),
StorageStrategy.createStringStorage() // this line shows error
).withSelectionPredicate(
SelectionPredicates.createSelectAnything()
).build()
I want some details about how is ItemKeyProvider implemented for a String, and secondly,
StorageStrategy.createStringStorage() // this shows error
StorageStrategy.createLongStorage() // this doesn't show error
why is that happening, when everywhere I have replaced the generic type from Long to String?
According to the docs, StorageStrategy is used for storing keys in saved state,
/* for Long keys */ StorageStrategy.createLongStorage()
/* for String keys */ StorageStrategy.createStringStorage()
/* for Parcelable keys */ StorageStrategy.createParcelableStorage(Class)
Also, according to the docs, StableIdKeyProvider provides for keys of type Long. That is why, your StorageStrategy is showing error because it is expecting Long keys.
To provide String keys, you have to create your own ItemKeyProvider class. For more details on ItemKeyProvider, you can refer the docs here.
This is how you can implement ItemKeyProvider class for String keys:
class MyItemKeyProvider(private val rvAdapter: MyAdapter): ItemKeyProvider<String>(SCOPE_CACHED) {
override fun getKey(position: Int): String = rvAdapter.getItem(position).myKey
override fun getPosition(key: String): Int = rvAdapter.getPosition(key)
}
and in MyAdapter:
class MyAdapter(private val myList: ArrayList<MyModel>): RecyclerView.Adapter<MyAdapter.MyViewHolder>() {
// functions used in MyItemKeyProvider
fun getItem(position: Int) = myList[position]
fun getPosition(key: String) = myList.indexOfFirst { it.myKey == key }
// other functions
}
where MyModel is something like this:
data class MyModel (
val myKey: String,
// other data
)
Now, you can simply build your SelectionTracker like this:
myTracker = SelectionTracker.Builder(
"my_selection_id",
recyclerView,
MyItemKeyProvider(rvAdapter),
MyItemDetailsLookup(recyclerView),
StorageStrategy.createStringStorage()
).withSelectionPredicate(
SelectionPredicates.createSelectAnything()
).build()
Note that you should not write the following code in your Adapter if you are not using StableIdKeyProvider:
init { setHasStableIds(true) }
otherwise it will show this error:
Attempt to invoke virtual method 'boolean androidx.recyclerview.widget.RecyclerView$ViewHolder.shouldIgnore()' on a null object reference
This tutorial shows how to implement recyclerview-selection with Long keys, also showing how to implement your own ItemKeyProvider class for Long keys.
To implement recyclerview-selection with Parcelable keys, I have found a sample code here.
Related
I have the following in my viewmodel :
// hold the list of comments of a Post
private val _commentsOfPost = MutableLiveData<PagedList<Comment>>()
val commentsOfPost : LiveData<PagedList<Comment>> = _commentsOfPost
fun getCommentsOfPost(postId: Long){
_commentsOfPost.value = commentRepository.getCommentsOfPost(postId) // <--- TYPE MISMATCH
}
So, what happens is that whenever the getCommentsOfPost() is called by the Fragment, it retrieves a PagedList of Comment instances belonging to a Post specified by its ID.
But Android tells me about a Type mismatch ( see the arrow in the codesnippet above):
Required: PagedList<Comment>?
Found: LiveData<PagedList<Comment>>
For the sake of completeness, this is the getCommentsOfPost() interface:
fun getCommentsOfPost(postId: Long) : LiveData<PagedList<Comment>>
How can I change it so that this error disappears ?
You should return to PagedList,
sample:
fun getCommentsOfPost(postId: Long) : PagedList<Comment> {
// your code
// return PagedList<Comment>
}
I am practicing my android skills (beginner) by coding a grocery list app. I have two tables in my db, a shopping_item table (The items I want to buy) and a reference_item table (The items I know the category and the unit price). Each time I add a shopping item, there is an refId field referencing to the reference item id corresponding. It is a default value to a default reference item if the shopping item is not referenced yet.
I use a MVVM model. I then have a DAO, a repository, a viewModel and my fragments that display data.
When I add a new shopping item, I want to know if there is a corresponding reference item. I want to do the following Query:
#Query(value = "SELECT refId FROM reference_items WHERE reference_item_name = :refName")
suspend fun getRefItem(refName : String) : Int
It returns the id of the reference item corresponding as an Int or is null if it is not referenced yet. In my repository, I have a function like that:
suspend fun getRefItem(refName : String) = db.getShoppingDao().getRefItem(refName)
For now, I think I am doing alright. No mistake in sight I guess.
The problem begin when I try to implement my viewModel. What should I do? What about my fragment?
I have a addNewItem(name: String, amount: Int) function in my fragment to add the new item. I can find the reference item corresponding with the name provided.
I tried multiple things, using LiveData, suspend functions, mutableLiveData/LiveData, but I am getting lost right now. Every tutorials or examples use LiveData or Query all data from the db. I just want one Integer, one Time, no need of LiveData I think.
here is the complete solution. Hope this is useful for you.
DAO
#Query(value = "SELECT refId FROM reference_items WHERE reference_item_name = :refName")
suspend fun getRefItem(refName : String) : Int
Repository
// Specify return datatype as Int
suspend fun getRefItem(refName : String): Int = db.getShoppingDao().getRefItem(refName)
ViewModel
fun getRefItem(name: String): LiveData<Int> {
val result : MutableLiveData<Int>() <-- setup livedata to return as value
viewModelScope.lanuch {
result.postValue(repository.getRefItem(name))
}
return result <-- return livedata
}
Fragment
fun addNewItem(name: String, amount: Int) {
// setup viewModel observer
viewModel.getRefItem(name).observer { viewLifecycleOwner, { value ->
// GET YOUR INT VALUE HERE
Log.i("VALUE", value)
}
}
}
Ok, so I have three interface types.
Movement<T : Animal>
Animal with subinterfaces Cat, Dog, Horse
AnimalMovement
Movement Interface
interface Movement<T : Animal> {
fun moveAnimal(type:T)
}
Animal Interfaces
interface Animal {
fun takeSteps()
fun jump()
fun hide()
}
interface Cat : Animal
interface Dog : Animal
AnimalMovement
interface CatMovement : Movement<Cat>
I then implement the CatMovement interface
class CatMovementImpl : CatMovement {
override fun moveAnimal(type: Cat) {
TODO("not implemented")
}
}
Problem
fun TestGenerics() {
var catMovement : Movement<Cat> = CatMovementImpl() // this works
var catMovement : Movement<Animal> = CatMovementImpl() // this doesn't?
}
I am sure in Java both lines would have worked fine. However in Kotlin the second line fails to execute. Why would that be? An animal is the base type for cat, so this should have worked right?
I am not an expert of Kotlin, but that seems perfectly normal:
When declared like this:
var animalMovement : Movement<Animal>
You can write code:
animalMovement.moveAnimal(dog)
But if assigning it like this:
var animalMovement : Movement<Animal> = CatMovementImpl()
is allowed, it means that your CatMovementImpl should be able to move a dog ?
What moskito said in the comments is correct.
I am pretty sure that doesn't work in Java either. A Movement<Cat> is
NOT a subtype of Movement<Animal>, the same way a List is NOT
a subtype of List<Object>. You might want to read this.
But in Kotlin this is possible using type variance.
fun TestGenerics() {
var catMovement1: Movement<Cat> = CatMovementImpl()
var catMovement2: Movement<out Animal> = CatMovementImpl() // works
}
You basically tell the compiler "accept all implementations of Movement<Animal> or implementations Movement<S> for which S has Animal as upper bound".
But, then a problem arises. You cannot invoke
val cat: Cat = /* ... */
catMovement2.moveAnimal(cat) // error
giving you the error
Out-projected type Movement<out Animal> prohibits the use of [...].
because T can only be used as producer (out position) and not as consumer (in position) like this (function made up to demonstrate the point):
val c: Cat = catMovement2.getAnimal() // works
This problem becomes clear right away when you use out at the Movement declaration like this:
interface Movement<out T : Animal> {
fun moveAnimal(type: T) // error
}
It depends on your use case but maybe you should just let Kotlin infer the type, which would be CatMovementImpl.
var catMovement = CatMovementImpl()
Credit goes to EpicPandaForce for already suggesting using out in the comments.
I'm struggling to understand and/or make Kotlin generics & polymorphism work for me. Consider this code:
class Item<T: BaseAttributes> {
var id: Long = -1L
lateinit var type: String
lateinit var attributes: T
}
open class BaseAttributes {
lateinit var createdAt: String
lateinit var updatedAt: String
}
open class BaseResponseList<T : BaseAttributes> {
lateinit var items: List<Item<T>> // the collection of items fetched from an API
}
class FruitAttributes(val id: Long, val color: String /* ... */) : BaseAttributes()
class FruitResponseList: BaseResponseList<FruitAttributes>()
// base service for all types of items
interface ApiService {
fun getItems(): BaseResponseList<BaseAttributes>
// fun getItemById(itemId: Long): BaseResponse<BaseAttributes>
/* other CRUD functions here ... */
}
// service for fruits
interface FruitService: ApiService {
override fun getItems(): FruitResponseList // get fruit items
}
I'm stumped by this compiler error that suggests FruitResponseList is not a subtype of the parametrized base class (BaseResponseList<FruitAttributes>):
Return type of 'getItems' is not a subtype of the return type of the overridden member 'public abstract fun getItems(): BaseResponseList<BaseAttributes> defined in ApiService'
I try to use declaration-site covariance in BaseAttributes to tell the compiler my intention that a FruitResponseList is a subclass of the base response list like this:
open class BaseResponseList<out T : BaseAttributes> {
lateinit var items: List<Item<T>> // the collection of items fetched from an API
}
leads to this error:
Type parameter T is declared as 'out' but occurs in 'invariant' position in type List<Item<T>>
How can I achieve the type-subtype relation between Fruit & Base response lists?
The Context
I'm implementing the networking code to perform CRUD operations against an API that's based on the JSON API spec format, thus I created the attributes and data (Item) classes to represent the json response objects.
My goal is to reduce the amount of duplicated code so that I only have to write the API service declarations once for every entity in my application (fruits, vendors, buyers, etc). I also want to avoid writing duplicated/boilerplate implementations of the data repository layers for every entity in my app (in the context of clean architecture). I should be able to just specify the business entity-specific types (models/entities) and let the one generic implementation do the work of fetching network data.
I thought it would make sense to use generics and inheritance to achieve this. In this particular example, the idea is that the fruit-specific GET would return a fruit response list, which is a subtype of the base response list. Will greatly appreciate any guidance on this, or alternative approaches to this problem
I'm stumped by this compiler error that suggests FruitResponseList is not a subtype of the parametrized base class (BaseResponseList<FruitAttributes>):
It is a subtype of BaseResponseList<FruitAttributes>, which isn't a subtype of BaseResponseList<BaseAttributes>.
I try to use declaration-site covariance in BaseAttributes to tell the compiler my intention that a FruitResponseList is a subclass of the base response list like this:...
This could be a correct approach, but the problem is that Item is not covariant (and it can't be because attributes is a var and its setter takes a T parameter). If Item can be modified to avoid this, good.
Another approach would be to add a type parameter to ApiService:
// base service for all types of items
interface ApiService<T: BaseAttributes> {
fun getItems(): BaseResponseList<T>
// fun getItemById(itemId: Long): BaseResponse<T>
/* other CRUD functions here ... */
}
// service for fruits
interface FruitService: ApiService<FruitAttributes> {
override fun getItems(): FruitResponseList // get fruit items
}
Why not something like this :
// base service for all types of items
interface ApiService<T> {
fun getItems(): T
// fun getItemById(itemId: Long): BaseResponse<BaseAttributes>
/* other CRUD functions here ... */
}
// service for fruits
interface FruitService: ApiService<FruitResponseList> {
override fun getItems(): FruitResponseList
}
or this :
// base service for all types of items
interface ApiService<T : BaseAttributes> {
fun getItems() : BaseResponseList<T>
// fun getItemById(itemId: Long): BaseResponse<BaseAttributes>
/* other CRUD functions here ... */
}
// service for fruits
interface FruitService: ApiService<FruitAttributes> {
override fun getItems(): FruitResponseList
}
You will be bounded to BaseResponseList<BaseAttributes> in your current implementation.
The answers provided by Mark and Alexey were very helpful. Particularly, parametrizing the ApiService on the BaseAttributes was the more flexible option since it allowed functions to take or return both BaseResponseList and BaseResponse [sub]types.
However, for what it's worth, it turns out that the Retrofit library does not allow its service declarations to extend other interfaces or even to be parametrized, supposedly in favor of composition over inheritance. There's plenty of debate about their decision on this issue here.
So I ended up creating separate interfaces for every one of my models :/
I'm learning the sample code about Anko at Kotlin for Android Developers (the book) https://github.com/antoniolg/Kotlin-for-Android-Developers
The Method 1 is from sample code and override parseList ,but it's hard to understand.
So I try to use the Method 2 instead of the Method 1, the Method 2 use original parseList function, but I get blank record when I use the Method 2, what error do I made in the Method 2
class DayForecast(var map: MutableMap<String, Any?>) {
var _id: Long by map
var date: Long by map
var description: String by map
var high: Int by map
var low: Int by map
var iconUrl: String by map
var cityId: Long by map
constructor(date: Long, description: String, high: Int, low: Int,
iconUrl: String, cityId: Long) : this(HashMap()) {
this.date = date
this.description = description
this.high = high
this.low = low
this.iconUrl = iconUrl
this.cityId = cityId
}
}
Method 1
override fun requestForecastByZipCode(zipCode: Long, date: Long) =
forecastDbHelper.use {
val dailyRequest = "${DayForecastTable.CITY_ID} = ? AND ${DayForecastTable.DATE} >= ?"
val dailyForecast = select(DayForecastTable.NAME)
.whereSimple(dailyRequest, zipCode.toString(), date.toString())
.parseList { DayForecast(HashMap(it)) }
/* common code block */
}
fun <T : Any> SelectQueryBuilder.parseList(parser: (Map<String, Any?>) -> T):
List<T> = parseList(object : MapRowParser<T> {
override fun parseRow(columns: Map<String, Any?>): T = parser(columns)
})
Method 2
override fun requestForecastByZipCode(zipCode: Long, date: Long) =
forecastDbHelper.use {
val dailyRequest = "${DayForecastTable.CITY_ID} = ? AND ${DayForecastTable.DATE} >= ?"
val dailyForecast = select(DayForecastTable.NAME)
.whereSimple(dailyRequest, zipCode.toString(), date.toString())
.exec { parseList(classParser<DayForecast>()) }
/* common code block */
}
I really do think you should stick to using the 'method 1' approach, it's a lot easier once you realise what Kotlin is letting you do. As I don't know how much you know about Kotlin, I'll try to cover this completely.
The existing class SelectQueryBuilder has (I presume) a function called parseList, that existing function takes a MapRowParser<T>. The MapRowParser<T> has a function parseRow that takes a Map<String, Any?> and returns a T.
In the old Java way of working, you would derive from MapRowParser<T> and would override parseRow so that it does the conversion you want; converting the Map<String, Any?> into a DayForecast (the generic T would now have a type). An instance of this derived class is passed into the existing parseList function. Your derived class would look something like
class MapToDayForecastRowParser extends MapRowParser<DayForecast> {
#Override public DayForecast parseRow(Map<String, Object> map) {
// Note that Java's "Object" is more or less Kotlin's "Any?"
return new DayForecast(map); // Might need to convert the map type btw
}
}
The extension method is making it really easy to wrap/hide/abstract that creation of the derived class. The extension method take a lambda, that is, you have to parse into the new parseList method a block of code that takes a Map<String, Any?> and returns T (this is what DayForecast(HashMap(it)) is doing, the it is an automatically named variable that is the Map. The extension method then calls the existing parseList method, parsing in an anonymous class that it creates itself. That means that ever use of this extension method creates a new anonymous class, but the Kotlin compiler handles this very well.
One part that did confuse me at first is the way Kotlin handles the anonymous class.
// Java
new MapRowParser<T>() {
#Override public T parseRow(Map<String, Object>) {
/* Map to T logic */
}
}
// Kotlin
object : MapRowParser<T> {
override fun parseRow(columns: Map<String, Any?>): T = parser(columns)
}
Kotlin also makes it very easy to handle the 'lambda'. It is parsed into the extension method as parser and then set as the implementation of our anonymous class parseRow function. You can also reuse them if you so wished, your so if you need to do the same sort of parsing in lots of places, you can use a named function instead.
The great advantage to this new Kotlin way is that it's very easy focus on what you want to do. With that extension method in place, it's very quick to re-use it so that in another query you can do parseList{ it.getOrDefault("name", "unkown_user") }. You can now easily work on just thinking "If each row is a map, how do I convert that down to a value I want?".