I have this interface and 2 classes, I know it's not the best approach here, but that's not the point now.
interface FilterItem {
val id: String
val name: String
val isChecked: ObservableField<Boolean>
fun reset()
}
data class Technology(
override val id: String,
override val name: String,
override val isChecked: ObservableField<Boolean> = ObservableField(),
) : FilterItem {
override fun reset() {
isChecked.set(false)
}
}
data class Project(
override val id: String,
override val name: String,
override val isChecked: ObservableField<Boolean> = ObservableField(),
) : FilterItem {
override fun reset() {
isChecked.set(false)
}
}
And then I have a ViewModel
private val _filterList = MutableLiveData<List<FilterItem>>().apply {
viewModelScope.launch {
value = loadSkills()
}
}
val filterList: LiveData<List<FilterItem>>
get() = _filterList
private fun loadSkills(): MutableList<FilterItem> {
val technologiesList: MutableList<FilterItem> = mutableListOf()
technologiesList.add(Technology("1", "Android"))
technologiesList.add(Technology("2", "Kotlin"))
return technologiesList
}
private fun loadProjects(): MutableList<FilterItem> {
val projectsList: MutableList<FilterItem> = mutableListOf()
projectsList.add(Project("1", "Project 1"))
projectsList.add(Project("2", "Project 2"))
return projectsList
}
I want to use the filterList so that is holds a list of Technologies or a list of Projects, depending on what BottomSheetDialog I'm opening.
showSkillsButton.setOnClickListener {
showFilterModal(filterViewModel.filterList, SKILLS)
}
showProjectsButton.setOnClickListener {
showFilterModal(filterViewModel.projects, PROJECTS)
}
I want to somehow use filterList only(I had 2 lists for skills and projects, but it would be nice if I could replace them with only 1 filterList).
Is there any way to do this? Or what would be the best approach?
Related
I have a RecyclerView where an item can be edited via a DialogFragment, so when an item is clicked a Dialog is shown, then I can change some properties of that item, the issue is that RecyclerView is not updated with the updated properties and I have to force a notifyItemChanged when the Dialog is closed.
When an item in RecyclerView is clicked I set a MutableLiveData in my ViewModel so then it can be manipulated in the Dialog.
My ViewModel looks like this:
#HiltViewModel
class DocumentProductsViewModel #Inject constructor(private val repository: DocumentProductsRepository) :
ViewModel() {
val barcode = MutableLiveData<String>()
private val _selectedProduct = MutableLiveData<DocumentProduct>()
val selectedProduct: LiveData<DocumentProduct> = _selectedProduct
private val _selectedDocumentId = MutableLiveData<Long>()
val selectedDocumentId: LiveData<Long> = _selectedDocumentId
val products: LiveData<List<DocumentProduct>> = _selectedDocumentId.switchMap { documentId ->
repository.getDocumentProducts(documentId).asLiveData()
}
fun insert(documentProduct: DocumentProduct) = viewModelScope.launch {
repository.insert(documentProduct)
}
fun setProductQuantity(quantity: Float) {
_selectedProduct.value = _selectedProduct.value.also {
it?.timestamp = System.currentTimeMillis()
it?.quantity = quantity
}
update()
}
fun start(documentId: Long?) = viewModelScope.launch{
if (documentId == null) {
_selectedDocumentId.value = repository.getHeaderByType("Etichette")?.id
}
documentId?.let { documentId ->
_selectedDocumentId.value = documentId
}
}
fun select(product: DocumentProduct) {
_selectedProduct.value = product
}
fun delete() = viewModelScope.launch {
_selectedProduct.value?.let { repository.delete(it) }
}
private fun update() = viewModelScope.launch {
_selectedProduct.value?.let { repository.update(it) }
}
}
And in my fragment I'm subscribed to products as this:
private fun initRecyclerView() {
binding.rvProducts.adapter = adapter
viewModel.products.observe(viewLifecycleOwner) { products ->
val productsCount = products.count()
binding.tvProductsCount.text =
resources.getQuantityString(R.plurals.articoli, productsCount, productsCount)
// TODO: create amount string and set it with resources
binding.tvProductsAmount.text = productsCount.toEuro()
adapter.submitList(products)
binding.rvProducts.smoothScrollToPosition(adapter.itemCount - 1)
}
initSwipe(adapter)
}
When setProductQuantity is called the RecyclerView remains unchanged until notify is called while delete works fine without the necessity of calling any notify on RecyclerView.
UPDATE:
The item position is actually changed in RecyclerView as it's sorted by it's last changed timestamp BUT not the quantity field.
Here is my Adapter:
class DocumentProductsListAdapter : ListAdapter<DocumentProduct, DocumentProductsListAdapter.ViewHolder>(ProductDiffCallback) {
override fun onCreateViewHolder(viewGroup: ViewGroup, viewType: Int): ViewHolder {
val view: View = LayoutInflater.from(viewGroup.context)
.inflate(R.layout.layout_item, viewGroup, false)
return ViewHolder(view)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val product = getItem(position)
holder.bind(product)
}
class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
val barcode: TextView = itemView.findViewById(R.id.barcode)
val quantity: TextView = itemView.findViewById(R.id.quantity)
val description: TextView = itemView.findViewById(R.id.description)
val unitOfMeasure: TextView = itemView.findViewById(R.id.unitOfMeasure)
fun bind(product: DocumentProduct) {
barcode.text = product.barcode
quantity.text = product.quantity.formatForQta().replace(".", ",")
if (product.labelType != null && product.labelType != "") {
unitOfMeasure.text = product.labelType
} else {
unitOfMeasure.text = product.unitOfMeasure?.lowercase(Locale.ITALIAN)
}
description.text = product.description ?: "-"
}
}
}
object ProductDiffCallback : DiffUtil.ItemCallback<DocumentProduct>() {
override fun areItemsTheSame(oldItem: DocumentProduct, newItem: DocumentProduct): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: DocumentProduct, newItem: DocumentProduct): Boolean {
return oldItem == newItem
}
}
data class DocumentProduct(
#PrimaryKey(autoGenerate = true)
var id: Long,
var barcode: String,
#Json(name = "desc")
var description: String?,
#ColumnInfo(defaultValue = "PZ")
#Json(name = "um")
var unitOfMeasure: String?,
#Json(name = "qta")
var quantity: Float,
#Json(name = "id_testata")
var documentId: Long,
#Json(name = "tipo_frontalino")
var labelType: String?,
var timestamp: Long?
) {
constructor(barcode: String, documentId: Long, labelType: String?) : this(
0,
barcode,
null,
"PZ",
1f,
documentId,
labelType,
null
)
override fun equals(other: Any?): Boolean {
return super.equals(other)
}
override fun hashCode(): Int {
return super.hashCode()
}
}
You have the implementations of areContentsTheSame() and areItemsTheSame() swapped.
areContentsTheSame() is asking if everything in the two items being compared is the same. Therefore, if the class has a proper equals()/hashcode() for all properties used by the ViewHolder, you can use oldItem == newItem. If you use a data class with all relevant properties in the primary constructor, then you don't need to manually override equals()/hashcode().
areItemsTheSame() is asking if the two items represent the same conceptual row item, with possible differences in their details. So it should be oldItem.id == newItem.id.
The problem with your data class is that you are overriding equals()/hashcode() without providing any implementation at all. This is effectively disabling the proper implementations that are provided by the data modifier by calling through to the super implementation in the Any class. You should not override them at all when you use data class.
As a developer one needs to adapt to change, I read somewhere it says:
If you don’t choose the right architecture for your Android project, you will have a hard time maintaining it as your codebase grows and your team expands.
I wanted to implement Clean Architecture with MVVM
My app data flow will look like this:
Model class
data class Note(
val title: String? = null,
val timestamp: String? = null
)
Dtos
data class NoteRequest(
val title: String? = null,
val timestamp: String? = null
)
and
data class NoteResponse(
val id: String? = null,
val title: String? = null,
val timestamp: String? = null
)
Repository layer is
interface INoteRepository {
fun getNoteListSuccessListener(success: (List<NoteResponse>) -> Unit)
fun deleteNoteSuccessListener(success: (List<NoteResponse>) -> Unit)
fun getNoteList()
fun deleteNoteById(noteId: String)
}
NoteRepositoryImpl is:
class NoteRepositoryImpl: INoteRepository {
private val mFirebaseFirestore = Firebase.firestore
private val mNotesCollectionReference = mFirebaseFirestore.collection(COLLECTION_NOTES)
private val noteList = mutableListOf<NoteResponse>()
private var getNoteListSuccessListener: ((List<NoteResponse>) -> Unit)? = null
private var deleteNoteSuccessListener: ((List<NoteResponse>) -> Unit)? = null
override fun getNoteListSuccessListener(success: (List<NoteResponse>) -> Unit) {
getNoteListSuccessListener = success
}
override fun deleteNoteSuccessListener(success: (List<NoteResponse>) -> Unit) {
deleteNoteSuccessListener = success
}
override fun getNoteList() {
mNotesCollectionReference
.addSnapshotListener { value, _ ->
noteList.clear()
if (value != null) {
for (item in value) {
noteList
.add(item.toNoteResponse())
}
getNoteListSuccessListener?.invoke(noteList)
}
Log.e("NOTE_REPO", "$noteList")
}
}
override fun deleteNoteById(noteId: String) {
mNotesCollectionReference.document(noteId)
.delete()
.addOnSuccessListener {
deleteNoteSuccessListener?.invoke(noteList)
}
}
}
ViewModel layer is:
interface INoteViewModel {
val noteListStateFlow: StateFlow<List<NoteResponse>>
val noteDeletedStateFlow: StateFlow<List<NoteResponse>>
fun getNoteList()
fun deleteNoteById(noteId: String)
}
NoteViewModelImpl is:
class NoteViewModelImpl: ViewModel(), INoteViewModel {
private val mNoteRepository: INoteRepository = NoteRepositoryImpl()
private val _noteListStateFlow = MutableStateFlow<List<NoteResponse>>(mutableListOf())
override val noteListStateFlow: StateFlow<List<NoteResponse>>
get() = _noteListStateFlow.asStateFlow()
private val _noteDeletedStateFlow = MutableStateFlow<List<NoteResponse>>(mutableListOf())
override val noteDeletedStateFlow: StateFlow<List<NoteResponse>>
get() = _noteDeletedStateFlow.asStateFlow()
init {
// getNoteListSuccessListener
mNoteRepository
.getNoteListSuccessListener {
viewModelScope
.launch {
_noteListStateFlow.emit(it)
Log.e("NOTE_G_VM", "$it")
}
}
// deleteNoteSuccessListener
mNoteRepository
.deleteNoteSuccessListener {
viewModelScope
.launch {
_noteDeletedStateFlow.emit(it)
Log.e("NOTE_D_VM", "$it")
}
}
}
override fun getNoteList() {
// Get all notes
mNoteRepository.getNoteList()
}
override fun deleteNoteById(noteId: String) {
mNoteRepository.deleteNoteById(noteId = noteId)
}
}
and last but not least Fragment is:
class HomeFragment : Fragment() {
private lateinit var binding: FragmentHomeBinding
private val viewModel: INoteViewModel by viewModels<NoteViewModelImpl>()
private lateinit var adapter: NoteAdapter
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = FragmentHomeBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val recyclerView = binding.recyclerViewNotes
recyclerView.addOnScrollListener(
ExFABScrollListener(binding.fab)
)
adapter = NoteAdapter{itemView, noteId ->
if (noteId != null) {
showMenu(itemView, noteId)
}
}
recyclerView.adapter = adapter
// initView()
fetchFirestoreData()
binding.fab.setOnClickListener {
val action = HomeFragmentDirections.actionFirstFragmentToSecondFragment()
findNavController().navigate(action)
}
}
private fun fetchFirestoreData() {
// Get note list
viewModel
.getNoteList()
// Create list object
val noteList:MutableList<NoteResponse> = mutableListOf()
// Impose StateFlow
viewModel
.noteListStateFlow
.onEach { data ->
data.forEach {noteResponse ->
noteList.add(noteResponse)
adapter.submitList(noteList)
Log.e("NOTE_H_FRAG", "$noteResponse")
}
}.launchIn(viewLifecycleOwner.lifecycleScope)
}
//In the showMenu function from the previous example:
#SuppressLint("RestrictedApi")
private fun showMenu(v: View, noteId: String) {
val menuBuilder = MenuBuilder(requireContext())
SupportMenuInflater(requireContext()).inflate(R.menu.menu_note_options, menuBuilder)
menuBuilder.setCallback(object : MenuBuilder.Callback {
override fun onMenuItemSelected(menu: MenuBuilder, item: MenuItem): Boolean {
return when(item.itemId){
R.id.option_edit -> {
val action = HomeFragmentDirections.actionFirstFragmentToSecondFragment(noteId = noteId)
findNavController().navigate(action)
true
}
R.id.option_delete -> {
viewModel
.deleteNoteById(noteId = noteId)
// Create list object
val noteList:MutableList<NoteResponse> = mutableListOf()
viewModel
.noteDeletedStateFlow
.onEach {data ->
data.forEach {noteResponse ->
noteList.add(noteResponse)
adapter.submitList(noteList)
Log.e("NOTE_H_FRAG", "$noteResponse")
}
}.launchIn(viewLifecycleOwner.lifecycleScope)
true
} else -> false
}
}
override fun onMenuModeChange(menu: MenuBuilder) {}
})
val menuHelper = MenuPopupHelper(requireContext(), menuBuilder, v)
menuHelper.setForceShowIcon(true) // show icons!!!!!!!!
menuHelper.show()
}
}
With all the above logic I'm facing TWO issues
issue - 1
As mentioned here, I have added SnapshotListener on collection as:
override fun getNoteList() {
mNotesCollectionReference
.addSnapshotListener { value, _ ->
noteList.clear()
if (value != null) {
for (item in value) {
noteList
.add(item.toNoteResponse())
}
getNoteListSuccessListener?.invoke(noteList)
}
Log.e("NOTE_REPO", "$noteList")
}
}
with it if I change values of a document from Firebase Console, I get updated values in Repository and ViewModel, but list of notes is not being updated which is passed to adapter, so all the items are same.
issue - 2
If I delete any item from list/recyclerview using:
R.id.option_delete -> {
viewModel
.deleteNoteById(noteId = noteId)
// Create list object
val noteList:MutableList<NoteResponse> = mutableListOf()
viewModel
.noteDeletedStateFlow
.onEach {data ->
data.forEach {noteResponse ->
noteList.add(noteResponse)
adapter.submitList(noteList)
Log.e("NOTE_H_FRAG", "$noteResponse")
}
}.launchIn(viewLifecycleOwner.lifecycleScope)
still I get updated list(i.e new list of notes excluding deleted note) in Repository and ViewModel, but list of notes is not being updated which is passed to adapter, so all the items are same, no and exclusion of deleted item.
Question Where exactly I'm making mistake to initialize/update adapter? because ViewModel and Repository are working fine.
Make following changes:
In init{} block of NoteViewModelImpl :
// getNoteListSuccessListener
mNoteRepository
.getNoteListSuccessListener{noteResponseList ->
viewModelScope.launch{
_noteListStateFlow.emit(it.toList())
}
}
you must add .toList() if you want to emit list in StateFlow to get notified about updates, and in HomeFragment
private fun fetchFirestoreData() {
// Get note list
viewModel
.getNoteList()
// Impose StateFlow
lifecycleScope.launch {
viewModel.noteListStateFlow.collect { list ->
adapter.submitList(list.toMutableList())
}
}
}
That's it, I hope it works fine.
Try to remove additional lists of items in the fetchFirestoreData() and showMenu() (for item R.id.option_delete) methods of the HomeFragment fragment and see if it works:
// remove `val noteList:MutableList<NoteResponse>` in `fetchFirestoreData()` method
private fun fetchFirestoreData() {
...
// remove this line
val noteList:MutableList<NoteResponse> = mutableListOf()
// Impose StateFlow
viewModel
.noteListStateFlow
.onEach { data ->
adapter.submitList(data)
}.launchIn(viewLifecycleOwner.lifecycleScope)
}
And the same for the delete menu item (R.id.option_delete).
My application uses Google Places API which data I later use to get weather from openweather.
I have a SearchFragment with RecyclerView where this happens.
Inside SearchFragment I observe the list I'm getting:
viewModel.predictions.observe(viewLifecycleOwner) {
citiesAdapter.submitList(it)
}
<...>
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.menu_fragment_weather, menu)
<...>
searchView.onQueryTextChanged {
viewModel.searchQuery.value = it
}
}
My viewModel:
class SearchViewModel #Inject constructor(
private val repository: AutocompleteRepository,
private val weatherRepository: WeatherRepository
) : ViewModel() {
fun provideClient(client: PlacesClient) {
repository.provideClient(client)
}
val searchQuery = MutableStateFlow("")
private val autocompleteFlow = searchQuery.flatMapLatest {
repository.getPredictions(it)
}
val predictions = autocompleteFlow.asLiveData()
fun onAddPlace(place: PlacesPrediction, added: Boolean) {
viewModelScope.launch {
repository.update(place, added)
if (added) weatherRepository.addWeather(place)
else weatherRepository.delete(place)
}
}
fun onDestroy() = viewModelScope.launch {repository.clearDb()}
}
Inside adapter I bind my items like this:
inner class CityViewHolder(private val binding: ItemCityToAddBinding) : RecyclerView.ViewHolder(binding.root) {
init {
binding.apply {
btnAdd.setOnClickListener {
val position = adapterPosition
if (position != RecyclerView.NO_POSITION) {
val place = getItem(position)
btnAdd.animate().rotation(if (place.isAdded) 45f else 0f).start()
println("Current item state (isAdded): ${place.isAdded}")
listener.onAddClick(place, !place.isAdded)
}
}
}
}
fun bind(prediction : PlacesPrediction) {
binding.apply {
val cityName = prediction.fullText.split(", ")[0]
locationName.text = cityName
fullName.text = prediction.fullText
btnAdd.animate().rotation(if (prediction.isAdded) 45f else 0f).start()
}
}
}
Where listener is passed to my adapter as a parameter from my fragment:
override fun onAddClick(place: PlacesPrediction, isAdded: Boolean) {
viewModel.onAddPlace(place, isAdded)
println("Parameter passed to onClick: $isAdded, placeId = ${place.placeId}")
}
<...>
val citiesAdapter = CitiesAdapter(this)
My repository's update() method looks like this:
suspend fun update(place: PlacesPrediction, added: Boolean) =
database.dao().update(place.copy(isAdded = added))
And finally, my dao's update:
#Update(onConflict = OnConflictStrategy.REPLACE)
suspend fun update(prediction: PlacesPrediction)
This is all tied up on PlacesPrediction class, an here it is:
#Entity(tableName = "autocomplete_table")
data class PlacesPrediction(
val fullText: String,
val latitude: Double,
val longitude: Double,
val placeId: String,
val isAdded: Boolean = false
) {
#PrimaryKey(autoGenerate = true) var id: Int = 0
}
So, my problem is that PlacesPredictions entries in my database are not getting updated. Actually, the only field I want to update with the code provided above is isAdded, but it stays the same after I press btnAdd of my list item. I used Android Studio's Database Inspector to verify that.
I tried using #Insert instead like so:
#Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(prediction: PlacesPrediction)
suspend fun update(place: PlacesPrediction, added: Boolean) =
database.dao().insert(place.copy(isAdded = added))
But strangely it only inserts a copy of place, the original item I clicked on stays the same.
Workaround
I get the desired behavior only if I hack my way to it:
#Entity(tableName = "autocomplete_table")
data class PlacesPrediction(
val fullText: String,
val latitude: Double,
val longitude: Double,
val placeId: String,
var isAdded: Boolean = false,
#PrimaryKey(autoGenerate = true) var id: Int = 0
)
suspend fun update(place: PlacesPrediction, added: Boolean) =
database.dao().insert(place.copy(isAdded = added, id = place.id))
And I don't like this soution at all. So my question is: how do I make #Update work?
As you probably already understood, the generated copy method of data classes ignores all members declared outside the constructor. So place.copy(isAdded = added) will generate a copy of all constructor parameters, but leave the id as the default 0, meaning a new element should be inserted, instead of updating an existing one.
Now this is my personal opinion:
Having the id as constructor parameter is the most elegant solution, as updates will work out of the box.
However if you dislike it that much, maybe an extension function might help you:
inline fun PlacesPrediction.preserveId(copyBuilder: PlacesPrediction.() -> PlacesPrediction): PlacesPrediction{
val copy = copyBuilder(this)
copy.id = this.id
return copy
}
//usage
suspend fun update(place: PlacesPrediction, added: Boolean) =
database.dao().insert(place.preserveId { copy(isAdded = added) })
I have a model
data class RegisterPostDataWithPwdCheck(
var phone_number: String?,
var name: String?,
var password: String?,
var secondPassword: String?)
And a ViewModel
class SignUpViewModel(application: Application) : BaseViewModel(application){
val registerPostData = MutableLiveData<RegisterPostDataWithPwdCheck>...
fun checkPassword(){}...}
I also have a View that has this code inside
viewModel.registerPostData.observe(viewLifecycleOwner, Observer {
viewModel.checkPassword()
})
In the XML there are two fields of interest
<EditText
android:id="#+id/edittext_sign_up_password"
android:text="#={view_model.registerPostData.password}" />
<EditText
android:id="#+id/edittext_sign_up_second_pw"
android:text="#={view_model.registerPostData.secondPassword}" />
What I understood so far is that the .observe will be called only when the entire RegisterPostDataWithPwdCheck object changes and I don't want that. I want it to be triggered when any of the parameters changes so I can call the fun checkPassword(){} in order to see if the two fields match. Is this possible?
Using #mahdi-shahbazi comment I've managed to work this out in Kotlin. My Model is now:
data class RegisterPostDataWithPwdCheck(
#SerializedName(value = "phone_number")
private var phoneNumber: String?,
private var name: String?,
private var password: String?,
private var secondPassword: String?
) : BaseObservable() {
#Bindable
fun getPhoneNumber(): String? {
return phoneNumber
}
fun setPhoneNumber(value: String) {
if (value != phoneNumber) {
phoneNumber = value
notifyPropertyChanged(BR.phoneNumber)
}
}
#Bindable
fun getName(): String? {
return name
}
fun setName(value: String?) {
if (value != name) {
name = value
notifyPropertyChanged(BR.name)
}
}
#Bindable
fun getPassword(): String? {
return password
}
fun setPassword(value: String?) {
if (value != password) {
password = value
notifyPropertyChanged(BR.password)
}
}
#Bindable
fun getSecondPassword(): String? {
return secondPassword
}
fun setSecondPassword(value: String?) {
if (value != secondPassword) {
secondPassword = value
notifyPropertyChanged(BR.secondPassword)
}
}
}
And creating custom LiveData class:
class PropertyAwareMutableLiveData<T : BaseObservable> : MutableLiveData<T>()
{
private val callback = object : Observable.OnPropertyChangedCallback() {
override fun onPropertyChanged(sender: Observable?, propertyId: Int) {
value = value
}
}
override fun setValue(value: T?) {
super.setValue(value)
value?.addOnPropertyChangedCallback(callback)
}
}
What I still don't know if there is a way to automate this #Binding process which is terribly slow and boring and also forces some changes (turning parameters to private).
I want to have generic RecyclerView to be able to reuse it. In my case I have 2 models: CategoryImages and Category. While trying to add constructor() it brings the following errors. I know the second one is because it understands like both primary and secondary constructor are same.
Is it possible to do such kind of thing? If yes, then how? if no - thank you.
Here is CategoryImage:
class CategoryImage {
#SerializedName("url")
private var url: String? = null
fun getUrl(): String? {
return url
}
}
And here is Category:
class Category {
#SerializedName("_id")
var id: String? = null
#SerializedName("name")
var name: String? = null
#SerializedName("__v")
var v: Int? = null
#SerializedName("thumbnail")
var thumbnail: String? = null
}
Here is the part of RecyclerViewAdapter's constructor:
class RecyclerViewAdapter(var arrayList: ArrayList<CategoryImage>?, var fragment: Int): RecyclerView.Adapter<RecyclerViewAdapter.ViewHolder>() {
constructor(arrayList: ArrayList<Category>, fragment: Int): this(arrayList, fragment)
}
I want to have generic RecyclerView to be able to reuse it.
That's nice intention, then why you haven't made your adapter generic?
I think you can adopt the approach outlined by Arman Chatikyan in this blog post. After applying some Kotlin magic you'll only need following lines of code in order to setup your RecyclerView:
recyclerView.setUp(users, R.layout.item_layout, {
nameText.text = it.name
surNameText.text = it.surname
})
And if you need to handle clicks on RecyclerView items:
recyclerView.setUp(users, R.layout.item_layout, {
nameText.text = it.name
surNameText.text = it.surname
}, {
toast("Clicked $name")
})
Now the adapter of the RecyclerView is generic and you are able to pass list of any models inside setup() method's first argument.
In this section I will copy-paste sources from the blog post, in order to be evade from external sources deprecation.
fun <ITEM> RecyclerView.setUp(items: List<ITEM>,
layoutResId: Int,
bindHolder: View.(ITEM) -> Unit,
itemClick: ITEM.() -> Unit = {},
manager: RecyclerView.LayoutManager = LinearLayoutManager(this.context)): Kadapter<ITEM> {
return Kadapter(items, layoutResId, {
bindHolder(it)
}, {
itemClick()
}).apply {
layoutManager = manager
adapter = this
}
}
class Kadapter<ITEM>(items: List<ITEM>,
layoutResId: Int,
private val bindHolder: View.(ITEM) -> Unit)
: AbstractAdapter<ITEM>(items, layoutResId) {
private var itemClick: ITEM.() -> Unit = {}
constructor(items: List<ITEM>,
layoutResId: Int,
bindHolder: View.(ITEM) -> Unit,
itemClick: ITEM.() -> Unit = {}) : this(items, layoutResId, bindHolder) {
this.itemClick = itemClick
}
override fun onBindViewHolder(holder: Holder, position: Int) {
holder.itemView.bindHolder(itemList[position])
}
override fun onItemClick(itemView: View, position: Int) {
itemList[position].itemClick()
}
}
abstract class AbstractAdapter<ITEM> constructor(
protected var itemList: List<ITEM>,
private val layoutResId: Int)
: RecyclerView.Adapter<AbstractAdapter.Holder>() {
override fun getItemCount() = itemList.size
override fun onCreateViewHolder(parent: ViewGroup,
viewType: Int): Holder {
val view = LayoutInflater.from(parent.context).inflate(layoutResId, parent, false)
return Holder(view)
}
override fun onBindViewHolder(holder: Holder, position: Int) {
val item = itemList[position]
holder.itemView.bind(item)
}
protected abstract fun onItemClick(itemView: View, position: Int)
protected open fun View.bind(item: ITEM) {
}
class Holder(itemView: View) : RecyclerView.ViewHolder(itemView)
}
Assuming CategoryImage means a Category with image.
You can express this relationship with inheritance:
open class Category(
val name: String
)
class CategoryImage(
name: String,
val image: String
) : Category(name)
class RecyclerViewAdapter(
val arr: List<Category>,
val fragment: Int
) {
fun bind(i: Int) {
val item = arr[i]
val name: String = item.name
val image: String? = (item as? CategoryImage)?.image
}
}
Another options it to have a common interface (which removes that ugly cast):
interface CategoryLike {
val name: String
val image: String?
}
class Category(
override val name: String
) : CategoryLike {
override val image: String? = null
}
class CategoryImage(
override val name: String,
override val image: String
) : CategoryLike
class RecyclerViewAdapter(private var arr: List<CategoryLike>, var fragment: Int) {
fun bind(i: Int) {
val item = arr[i]
val name: String = item.name
val image: String? = item.image
}
}
In both cases the following works (just to see that it can be compiled):
fun testCreation() {
val cats: List<Category> = listOf()
val catImages: List<CategoryImage> = listOf()
RecyclerViewAdapter(cats, 0)
RecyclerViewAdapter(catImages, 0)
}
Tip: don't use ArrayList, List (listOf(...)) or MutableList (mutableListOf(...)) should be enough for all your needs.
Tip: try to use val as much as you can, it helps prevent mistakes.
Wish: Next time please also include some relevant parts of your code in a copy-able form (not screenshot), so we don't have to re-type it and have more context. See https://stackoverflow.com/help/mcve
One "terrible" way of doing it is to simply have 1 constructor taking an ArrayList of Objects and perform an instanceof on the objects.
Both methods have the same signature, because type parameters are not considered as different types (for Java Virtual Machine both are just ArrayLists). You also need to be aware of type erasure.
Check this repository https://github.com/shashank1800/RecyclerGenericAdapter
lateinit var adapter: RecyclerGenericAdapter<AdapterItemBinding, TestModel>
...
val clickListener = ArrayList<CallBackModel<AdapterItemBinding, TestModel>>()
clickListener.add(CallBackModel(R.id.show) { model, position, binding ->
Toast.makeText(context, "Show button clicked at $position", Toast.LENGTH_SHORT)
.show()
})
adapter = RecyclerGenericAdapter(
R.layout.adapter_item, // layout for adapter
BR.testModel, // model variable name which is in xml
clickListener // adding click listeners is optional
)
binding.recyclerView.adapter = adapter
binding.recyclerView.layoutManager = LinearLayoutManager(this)
adapter.submitList(viewModel.testModelList)
Recycler adapter item R.layout.adapter_item XML.
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable
name="testModel"
type="com.packagename.model.TestModel" />
</data>
...
VERY IMPORTANT NOTE: I'm using same layout for all my screens.
//********Adapter*********
// include a template parameter T which allows Any datatype
class MainAdapter<T : Any>(var data: List<T>) : RecyclerView.Adapter<MainViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MainViewHolder {
val view = parent.inflateLayout()
return MainViewHolder(view)
}
override fun onBindViewHolder(holder: MainViewHolder, position: Int) {
val item = data[position]
holder.bind(item)
}
override fun getItemCount(): Int = data.size
class MainViewHolder(private val binding: MainItemsListBinding) :
RecyclerView.ViewHolder(binding.root) {
// do the same for for bind function on Viewholder
fun <T : Any> bind(item: T) {
// Extension function see code below
binding.appInfo.mySpannedString(item)
}
}
}
//Cast Item to type
fun <T : Any> TextView.mySpannedString(item: T) {
when (item.javaClass.simpleName) {
"AaProgram" -> {
item as AaProgram
this.text = buildSpannedString {
appInfo(item.numero, item.principio)
}
}
"AppContent" -> {
item as AppContent
this.text = buildSpannedString {
appInfo(item.title, item.coment, item.footnote)
}
}
"AutoDiagnostic" -> {
item as AppContent
this.text = buildSpannedString {
appInfo(item.title, item.coment, item.footnote)
}
}
"GroupDirectory" -> {}
"ReflexionsBook" -> {}
"County" -> {}
"States" -> {}
"Towns" -> {}
}
}