How do I have multiple callback function back to the Activity/Fragment of a RecyclerView?
I have multiple options for each item in the RecyclerView (Edit, Delete, CheckedAsComplete, View) and I would like to have callback function for each of them in the Activity/Fragment of the RecyclerView.
Here is a link of how I got one callback in the Adapter: https://www.geeksforgeeks.org/kotlin-lambda-functions-for-recyclerview-adapter-callbacks-in-android/
I just need to know if it is possible to have multiple callbacks in the adapter and if so, how do I implement it?
My Activity's Adapter Code:
val adapter = ProductAdapter(this) {
deleteProduct(it),
editProduct(it),
viewProduct(it),
checkAsComplete(it)
}
Here is my Adapter's Constructor:
class ProductAdapter(
private var context: Context,
private val deleteProduct: (ItemTable) -> Unit,
private val editProduct: (ItemTable) -> Unit,
private val viewProduct: (ItemTable) -> Unit,
private val checkedAsComplete: (ItemTable) -> Unit
): RecyclerView.Adapter<ProductAdapter.ItemProductViewHolder>() {
// Rest of RecyclerView Adapter Code
}
I'm pretty new to kotlin so I would really appreciate your help!
You can use curly braces out only for the last callback of the list.
Assuming you declared the following methods in your activity :
fun deleteProduct(itemTable: ItemTable)
fun editProduct(itemTable: ItemTable)
fun checkAsComplete(itemTable: ItemTable)
fun viewProduct(itemTable: ItemTable)
You can use named parameters and you have two choices
With method reference
val adapter = ProductAdapter(
context = this,
deleteProduct = ::deleteProduct,
editProduct = ::editProduct,
viewProduct = ::viewProduct,
checkAsComplete = ::checkAsComplete
)
With lambda
val adapter = ProductAdapter(
context = this,
deleteProduct = { deleteProduct(it) },
editProduct = { editProduct(it) },
viewProduct = { viewProduct(it) },
checkAsComplete = { checkAsComplete(it) }
)
You can use different approach. This does not depend on how many events you have. For example with enum class you can use single callback with many options
class ProductAdapter(private val clickEvent: (ClickEvent, ItemTable) -> Unit):
RecyclerView.Adapter<ProductAdapter.ItemProductViewHolder>() {
enum class ClickEvent {
DELETE,
EDIT,
VIEW,
COMPLETE
}
}
Usage:
val adapter = ProductAdapter{ event, item ->
when(event){
DELETE -> deleteProduct(item)
....//All other enum values
}
}
Related
I'm fairly new to Jetpack Compose. Currently, I have a ViewModel making 1 network call.
class PlatformViewModel #Inject constructor(
private val getProductListUseCase: GetListUseCase
) : ViewModel()
I had 3 states.
sealed class PlatformState {
object Loading : PlatformState()
data class Success(val listOfProducts: List<Product>) : PlatformState()
object Error : PlatformState()
}
In the UI, it Was easy to handle observing 1 live data.
val state = viewModel.platformState.observeAsState(PlatformState.Loading)
when (state) {
is PlatformState.Success -> SuccessView(listOfProducts = state.listOfProducts)
is PlatformState.Loading -> LoadingView()
is PlatformState.Error -> ErrorView()
}
now, I need to add 1 more network call in viewModel for the same screen
class PlatformViewModel #Inject constructor(
private val getProductListUseCase: GetListUseCase,
private val getHeaderUseCase: GetHeaderUseCase,
) : ViewModel()
-Should I add 3 more states and 1 more live data to observe for the UI, what is the best way to handle this?
Note: both network calls are unrelated but their result populates the same composable.
fun bodyContent(listOfProducts:List<Products>,headerDetails:HeaderDetails){
LazyColumn{
item{ HeaderDetails(details=headerDetails)}
items(listOfProducts.size){
ProductItem()
}
Since the composable function, bodyContent requires both parameters listOfProducts:List<Products>, headerDetails:HeaderDetails.
I would create a data class that holds those values and that class should be sent to the composable function.
data class BodyContentUIData(listOfProducts:List<Products>, headerDetails:HeaderDetails)
The composable should be
fun BodyContent(bodyContentUIData: BodyContentUIData) {
LazyColumn {
item { HeaderDetails(details = bodyContentUIData.headerDetails) }
items(bodyContentUIData.listOfProducts.size) {
ProductItem()
}
}
}
//BTW, composable functions should start with a capital case.
At the view model, You should have a function called getBodyContentData that first calls getHeaderUseCase and if it's a success then call getProductListUseCase after the success you'll be having the listOfProducts and the headerDetails now you create the data class and send it to the composable function.
The view model could look like this:
class PlatformViewModel #Inject constructor(
private val getProductListUseCase: GetListUseCase,
private val getHeaderUseCase: GetHeaderUseCase,
) : ViewModel() {
fun getBodyContentData() {
getHeaderUseCase().onSuccess { headerDetails ->
getProductListUseCase().onSuccess { listOfProducts ->
_bodyContentLiveData.value = SuccessView(BodyContent(headerDetails, listOfProducts))
}
}
}
}
This way you have just 1 live data and 3 states for the composable.
In my task I need to fetch list of bikes.
I made BikesApi and put some bikes into a list.
In MovieRepository implementation I overrided function getBikes that returns now Flow<List>>.
val BikeZ : Flow<List<Bike>> = flow{
while(true){
val lastBikeX = bikeApi.getBikes()
emit(lastBikeX)
kotlinx.coroutines.delay(1000)
}
}
return BikeZ
In BikeViewModel I implemented it like this, It gets MovieRepository (Koin):
public fun getPopularBikes() = flow<Bike> {
val bikesList = repository.getPopularBikes().collect()
return#flow bikesList
}
And when I try in Compose function this:
val viewModel = viewModel<BikeViewModel>()
I got this error:
Cannot create an instance of class BikeViewModel.
java.lang.RuntimeException:
Cannot create an instance of class BikeViewModel
I'm using Koin.
I need list of Movies to implement them into UI.
EDIT:
In app module i did this:
single<BikeApi>{
BikeApiImpl()
}
single<BikeRepository>{
BikeRepositoryImpl(get())
}
viewModel { BikeViewModel(get()) }
Bike api:
class BikeApiImpl : BikeApi{
override suspend fun getPopularBikes(): List<Bike> {
return listOf(
Bike(
id = 1,
name = "Nakamura",
isCheckedOff = true,
bikeType = "MTB",
overview = "None",
picture = R.drawable.nakamura_1,
userScore = 72.0
)
I't wont return list of Bikes.
Initialize your viewModel like this if you are using Koin di:
val viewModel: BikeViewModel by viewModel()
also import
import org.koin.androidx.viewmodel.ext.android.viewModel
I want to update at any time some values in my RecyclerView.
Here is my data class ParameterText:
data class ParameterText(
var parameterName: String?,
var parameterValue: String?
)
Here is my ViewHolder class ParameterTextViewHolder:
class ParameterTextViewHolder(itemView: View) : ViewHolder(itemView) {
val parameterName: TextView = itemView.findViewById(R.id.parameterName)
val parameterText: TextView = itemView.findViewById(R.id.parameterValue)
}
Here is my Adapter (in my Activity):
// Adapter
private val parametersTextFoundList = emptyDataSourceTyped<ParameterText>()
And here is my RecyclerView setup (also in my Activity):
rv_parameters_text.setup {
withDataSource(parametersTextFoundList)
withItem<ParameterText, ParameterTextViewHolder>(R.layout.parameter_text) {
onBind(::ParameterTextViewHolder) { _, item ->
parameterName.text = item.parameterName
parameterText.text = item.parameterValue
}
}
}
I tried this:
private fun updateValue(index: Int, value: String) {
parametersTextFoundList[index].parameterValue = value
}
But it doesn't work. I read that I should also use the notifyDataSetChanged() method but I don't know where to use it. Can you help me?
There is an entire suite of notify API's, including notifyItemInserted(), notifyItemRemoved(), notifyItemChanged(), which are designed to more efficiently update a RecyclerView.
when changing the contents of one existing row in your RecyclerView, its more efficient to use adapter.notifyItemChanged(row), as notifyDataSetChanged() will reload the entire RecyclerView. I recommend:
private fun updateValue(index: Int, value: String)
{
parametersTextFoundList[index].parameterValue = value
rv_parameters_text.adapter?.notifyItemChanged(index)
}
You need to use notifyDataSetChanged() method with the update like this
rv_parameters_text.adapter?.notifyDataSetChanged()
I'm trying to combine three different flows in my ViewModel to make a list of items that will then be displayed on a RecyclerView in a fragment. I found out that when navigating to the screen, when there is no data in the table yet, the flow for testData1 doesn't emit the data in the table. Happens probably 1/5 of the time. I assume it's a timing issue because it only happens so often, but I don't quite understand why it happens. Also, this only happens when I'm combining flows so maybe I can only have so many flows in one ViewModel?
I added some code to check to see if the data was in the table during setListData() and it's definitely there. I can also see the emit happening but, there is no data coming from room. Any guidance would be greatly appreciated!
Versions I'm using:
Kotlin: 1.4.20-RC
Room: 2.3.0-alpha03
Here is my ViewModel
class DemoViewModel #Inject constructor(
demoService: DemoService,
private val demoRepository: DemoRepository
) : ViewModel() {
private val _testData1 = demoRepository.getData1AsFlow()
private val _testData2 = demoRepository.getData2AsFlow()
private val _testData3 = demoRepository.getData3AsFlow()
override val mainList = combine(_testData1, _testData2, _testData3) { testData1, testData2, testData3 ->
setListData(testData1, testData2, testData3)
}.flowOn(Dispatchers.Default)
.asLiveData()
init {
viewModelScope.launch(Dispatchers.IO) {
demoService.getData()
}
}
private suspend fun setListData(testData1: List<DemoData1>, testData2: List<DemoData2>, testData3: List<DemoData3>): List<CombinedData> {
// package the three data elements up to one list of rows
...
}
}
And here is my Repository/DAO layer (repeats for each type of data)
#Query("SELECT * FROM demo_data_1_table")
abstract fun getData1AsFlow() : Flow<List<DemoData1>>
I was able to get around this issue by removing flowOn in the combine function. After removing that call, I no longer had the issue.
I still wanted to run the setListData function on the default dispatcher, so I just changed the context in the setListData instead.
class DemoViewModel #Inject constructor(
demoService: DemoService,
private val demoRepository: DemoRepository
) : ViewModel() {
private val _testData1 = demoRepository.getData1AsFlow()
private val _testData2 = demoRepository.getData2AsFlow()
private val _testData3 = demoRepository.getData3AsFlow()
override val mainList = combine(_testData1, _testData2, _testData3) { testData1, testData2, testData3 ->
setListData(testData1, testData2, testData3)
}.asLiveData()
init {
viewModelScope.launch(Dispatchers.IO) {
demoService.getData()
}
}
private suspend fun setListData(testData1: List<DemoData1>, testData2: List<DemoData2>, testData3: List<DemoData3>): List<CombinedData> = withContext(Dispatchers.Default) {
// package the three data elements up to one list of rows
...
}
}
I want to filter List <Notification> which is inside Observable collection by the specified event.
Here is a Retrofit call:
#GET("/notifications")
Observable<NotificationCollection> getNotifications(#Query("page") Integer page);
NotificationCollection model:
class NotificationCollection {
var items: List<Notification>? = null
var pagination: Pagination? = null
}
Notification model:
class Notification {
var event: String? = null
var id: Long? = null
var data: NotificationData? = null
}
In my helper class, I return Observable to interactor:
override fun getNotifications(page: Int): Observable<NotificationCollection> {
return service.getNotifications(page)
}
I tried a couple of approaches:
override fun getNotifications(page: Int): Observable<NotificationCollection> {
return service.getNotificationsTimeline(page)
.filter { it.items?.get(0)?.event == "STORY"}
}
Here I want to apply a predicate to all list items and not only to the ones I define by an index. Is there a way to make something like .filter { it.items.event = "STORY"} ?
Another approach I tried using flatMap here which makes more sense to me but I don't see the way how to map my filtered result to the original response type of Observable<NotificationCollection> and not to the Observable<Notification> like here :
return service.getNotifications(page)
.flatMap { Observable.fromIterable(it.items) }
.filter {it.event == "STORY"}
The easiest way would be to apply filter function in my presenter class with flatMap but I want to generalize my solution because the method in the helper class is called in different places. So I want to make the list filtered here.
So, is there a way to filter the list inside Observable collection and return the original response type of Observable<NotificationCollection>?
You should use 'filter' and 'any' together.
class Test {
init {
val testList = listOf(
TestData("1","1", listOf("1a")),
TestData("2","2", listOf("2b")),
TestData("3","3", listOf("3c")))
val result = testList.filter { item ->
item.c.any { it == "STORY" }
}
}
}
data class TestData(val a: String, val b: String, val c: List<String>)