View Changes not properly - android

Ok from the Start, I have a RecyclerView where I want to change the color of the entries, if they are clicked.
The Recyclerview gets a list from a database, where I added a bool value, if the View is selected.
In my holder I check if the view is selected and change the color with a selector.
So far so good, but now there is the problem.
The Activity Starts, the old selected Entry is colored, but when I click on an other Entry the color of the old entry disappears and on the one tapped nothing happens. I have to reclick on the old entry again and from this point the color changing works fine.
method called in viewHolder
itemView.view_foreground_nav.isSelected = (table.isSelected)
selector
<selector
xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:drawable="#android:color/darker_gray"
android:state_pressed="true"/>
<item
android:drawable="#android:color/darker_gray"
android:state_selected="true"/>
<item
android:drawable="#android:color/darker_gray"
android:state_focused="true"/>
<item android:drawable="#android:color/white"/>
</selector>
RelativeLayout from viewholder where color should changed
android:id="#+id/view_foreground_nav"
android:layout_width="match_parent"
android:layout_height="#dimen/ic_delete"
android:background="#drawable/recycler_nav_item_background">
variable in the Activity which is set in onCreate
private var selectedTable: Table? = null
onClick Method
if (selectedTable != null) {
var oldSelectedTable = selectedTable
oldSelectedTable?.isSelected = false
productViewModel.updateTable(oldSelectedTable!!)
data.isSelected = true
productViewModel.updateTable(data)
selectedTable = data
} else {
data.isSelected = true
productViewModel.updateTable(data)
selectedTable = data
}
button_add_note.isEnabled = true
observerProduct(selectedTable)
Diffutil
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int):Boolean{
return oldListTable[oldItemPosition].idTable == newListTable[newItemPosition].idTable
}
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
return oldListTable[oldItemPosition].title == newListTable[newItemPosition].title&&
oldListTable[oldItemPosition].isSelected == newListTable[newItemPosition].isSelected
}
The ProductViewModel is a lateinit in the MainActivity
private lateinit var productViewModel: ProductViewModel
In OnCreate I set the ViewModel
productViewModel =
ViewModelProviders.of(this).get(ProductViewModel::class.java)
GenericAdapter Call
private fun observerTable() {
productViewModel.getAllTables().observe(this, Observer<List<Table>> { it ->
adapterNav.setItems(it as MutableList<Any>)
})
}
Table Dao
#Query("Select * from main_table")
fun getAllTables(): LiveData<MutableList<Table>>
ProductRepro
private var tableDao: TableDao
private var allTables: LiveData<MutableList<Table>>
init {
var database: ProductDatabase? = ListMasterApplication.database
this.tableDao = database!!.tableDao()
this.allTables = tableDao.getAllTables()
}
var database: ProductDatabase? = ListMasterApplication.database
fun getAllTable(): LiveData<MutableList<Table>> {
return allTables
}
ProductVieModel
private var productRepository: ProductRepository = ProductRepository()
private var liveDataTable: LiveData<MutableList<Table>>
init {
this.liveDataTable = productRepository.getAllTable()
}
fun getAllTables():LiveData<MutableList<Table>>{
return liveDataTable
}
Observer in MainActivity
private fun observerTable() {
productViewModel.getAllTables().observe(this, Observer<MutableList<Table>> { it ->
adapterNav.setItems(it as MutableList<Any>)
})
}
In GenericAdapter set Items
fun setItems(items: MutableList<T>) {
val diffCallback = DiffCallback(listItems ,items)
val diffResult = DiffUtil.calculateDiff(diffCallback)
listItems.clear()
listItems.addAll(items)
diffResult.dispatchUpdatesTo(this)
}
I logged my Diffutil and on each click on different items the Log was
always correct. I logged the selected table variable an it is always
correct.
I don't know where to go from this point. It only happens in the start
of the activity and then it works fine. Do you have some hints or
something, which i could try. If you need some more insight of the
code, let me know and I publish more.

Related

Kotlin recycler view cannot change background when later calling a function

I'm new to Kotlin and so if you do find rubbish code here and poor practices, do let me know. Otherwise, here is the issue I am having.
I am writing a tiny app that presents users with multiple questions from which they have to select the correct answer. If they select the correct answer, the option is supposed to be highlighted green for 250ms and then they move on to the next question. Otherwise, select the incorrect answer. The logic for moving onto the next question is defined in the main activity, and the background change logic is defined in the adapter class. Below is what the adapter class looks like at the moment (I've only included that which I think is relevant just to add too much faff):
class QuestionOptionAdapter(
private val items: ArrayList<String>,
private val correctAnswer: String,
) : RecyclerView.Adapter<QuestionOptionAdapter.ViewHolder>() {
var onSelectedAnswer: (String) -> Unit = {}
var onSelectedCorrectAnswer: () -> Unit = {}
var onSelectedIncorrectAnswer: () -> Unit = {}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = items[position]
holder.tvOption.text = item
holder.tvOption.setOnClickListener {
if (item == correctAnswer) {
runBlocking {
it.background =
ContextCompat.getDrawable(it.context, R.drawable.question_opt_correct)
delay(250)
}
onSelectedCorrectAnswer()
} else {
it.background =
ContextCompat.getDrawable(it.context, R.drawable.question_opt_incorrect)
onSelectedIncorrectAnswer()
}
}
}
}
I realised that although the code to changes the background is executed before onSelectedCorrectAnswer(), it won't change the background colour until the entire block has finished executing. Therefore, the user never sees the updated background.
Is there a way to show an update before the block finishes executing?
runBlocking doesn't work because it blocks. It will just wait for the whole time you delay and block the main thread so the device will be frozen and not show any visual changes until it returns.
You need to pass the Activity or Fragment's CoroutineScope into the adapter for the adapter to use. You can then launch a coroutine that won't block the main thread when you delay inside it.
Here I lifted the coroutine to encompass all your click listener logic. That will make it easier to modify the behavior later if you want.
class QuestionOptionAdapter(
private val scope: CoroutineScope,
private val items: ArrayList<String>,
private val correctAnswer: String,
) : RecyclerView.Adapter<QuestionOptionAdapter.ViewHolder>() {
var onSelectedAnswer: (String) -> Unit = {}
var onSelectedCorrectAnswer: () -> Unit = {}
var onSelectedIncorrectAnswer: () -> Unit = {}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = items[position]
holder.tvOption.text = item
holder.tvOption.setOnClickListener { view ->
scope.launch {
val isCorrect = item == correctAnswer
val colorDrawable =
if (isCorrect) R.drawable.question_opt_correct
else R.drawable.question_opt_incorrect
view.background = ContextCompat.getDrawable(view.context, colorDrawable)
if (isCorrect) {
delay(250)
onSelectedCorrectAnswer()
} else {
onSelectedIncorrectAnswer()
}
}
}
}
}
Actually, you probably want to also prevent the user from clicking other options during that 250ms delay, so you should set a Boolean that can disable further clicking of items during the delay:
class QuestionOptionAdapter(
private val scope: CoroutineScope,
private val items: ArrayList<String>,
private val correctAnswer: String,
) : RecyclerView.Adapter<QuestionOptionAdapter.ViewHolder>() {
var onSelectedAnswer: (String) -> Unit = {}
var onSelectedCorrectAnswer: () -> Unit = {}
var onSelectedIncorrectAnswer: () -> Unit = {}
private var isLockClickListeners = false
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = items[position]
holder.tvOption.text = item
holder.tvOption.setOnClickListener { view ->
if (isLockClickListeners) {
return#setOnClickListener
}
scope.launch {
val isCorrect = item == correctAnswer
val colorDrawable =
if (isCorrect) R.drawable.question_opt_correct
else R.drawable.question_opt_incorrect
view.background = ContextCompat.getDrawable(view.context, colorDrawable)
if (isCorrect) {
isLockClickListeners = true
delay(250)
onSelectedCorrectAnswer()
isLockClickListeners = false
} else {
onSelectedIncorrectAnswer()
}
}
}
}
}

Summary doesn't persists on dynamically load data in ListPreference

In my SettingActivity I'm loading some data to be shown in ListPreference from Room, once the items are selected all works correctly, the value is saved to `SharedPreferences and the summary is shown correctly, but once I return to SettingsActivity the summary value is reset to null.
Here is what is happening:
My code is pretty simple, onViewCreated() I start observing LiveData to be shown in ListPreference and then I set the values for entries and entryValues
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel.shops.observe(viewLifecycleOwner) {
setShopsPreferences(it)
}
}
private fun setShopsPreferences(shops: List<Shop>) {
val shopsPreference = preferenceManager.findPreference<ListPreference>("defaultShop")
if (shops.isEmpty()) {
shopsPreference?.isEnabled = false
return
} else {
shopsPreference?.isEnabled = true
}
val entries: ArrayList<String> = ArrayList()
val entryValues: ArrayList<String> = ArrayList()
shops.forEach {
entries.add(it.description)
entryValues.add(it.id)
}
shopsPreference?.entryValues = entryValues.toArray(arrayOfNulls<CharSequence>(entryValues.size))
shopsPreference?.entries = entries.toArray(arrayOfNulls<CharSequence>(entries.size))
}
ViewModel:
#HiltViewModel
class ShopsViewModel #Inject constructor(repository: ShopsRepository) : ViewModel() {
private val _shops = MutableLiveData<List<Shop>>()
val shops: LiveData<List<Shop>> = _shops
init {
repository.getAllShops().observeForever {
_shops.value = it
}
}
}
Repository:
fun getAllShops(): LiveData<List<Shop>> {
return shops.select()
}
DAO:
#Dao
interface ShopsDAO {
#Query("SELECT * FROM shops")
fun select(): LiveData<List<Shop>>
}
You are populating list entries dynamically after the preference hierarchy is already created by inflating the xml. But during that time there was no entry, hence the value was null. The data was then retrieved asynchronously but the change will not be reflected. So you have to set the summary manually.
Another approach I'm not sure about is to call recreate on the activity after populating the data inside the observer listener.
To resolve the issue with dynamic data in a ListPreference I've made some changes to my code, first of call in onCreatePreferences() I've added a preference listener to my ListPreference like this:
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.root_preferences, rootKey)
val shopsPreference = preferenceManager.findPreference<ListPreference>("defaultShop")
shopsPreference?.setOnPreferenceChangeListener { preference, newValue ->
val description = viewModel.shops.value?.find { it.id == newValue }
preference.summary = description?.description
true
}
}
So in that way on each new selection, I'm looking for the matching value for that preference in my ViewModel and getting the description for it which then is set as the summary.
While to set the summary every time the list has been changed or the SettingsActivity is opened I've added the following code to my setShopsPreferences() function:
private fun setShopsPreferences(shops: List<Shop>?) {
if (shops == null) {
return
}
val shopsPreference = preferenceManager.findPreference<ListPreference>("defaultShop")
// The preference is enabled only if there are shows inside the list
shopsPreference?.isEnabled = shops.isNotEmpty()
// Getting the defaultShop preference value, which will be used to find the actual shop description
val defaultShop = sharedPreferences.getString("defaultShop", "0")
defaultShop?.let { shopId ->
// Setting the summary based on defaultShop saved preference
shopsPreference?.summary = shops.find { it.id == shopId }?.description
}
val entries: ArrayList<String> = ArrayList()
val entryValues: ArrayList<String> = ArrayList()
shops.forEach {
entries.add(it.description)
entryValues.add(it.id)
}
shopsPreference?.entryValues =
entryValues.toArray(arrayOfNulls<CharSequence>(entryValues.size))
shopsPreference?.entries = entries.toArray(arrayOfNulls<CharSequence>(entries.size))
}

Button onClick keep replacing values in mutableStateList

I'm trying to display a 4x4 grid with values that change depending on user input. To achieve that, I created mutableStateListOf that I use in a ViewModel to survive configuration changes. However, when I try to replace a value in that particular list using button onClick, it keeps doing that until app crashes. I can't understand why is onReplaceGridContent looping after clicking the button once. Currently, my code looks like this:
ViewModel:
class GameViewModel : ViewModel(){
var gameGridContent = mutableStateListOf<Int>()
private set // Restrict writes to this state object to private setter only inside view model
fun replaceGridContent(int: Int, index: Int){
gameGridContent[index] = int
}
fun removeGridContent(index: Int){
gameGridContent[index] = -1
}
fun initialize(){
for(i in 0..15){
gameGridContent.add(-1)
}
val firstEmptyGridTile = GameUtils.getRandomTilePosition(gameGridContent)
val firstGridNumber = GameUtils.getRandomTileNumber()
gameGridContent[firstEmptyGridTile] = firstGridNumber
}
}
Button:
Button(
onClick = {
onReplaceGridContent(GameUtils.getRandomTileNumber(),GameUtils.getRandomTilePosition(gameGridContent))},
colors = Color.DarkGray
){
Text(text = "Add number to tile")
}
Activity Composable:
#Composable
fun gameScreen(gameViewModel: GameViewModel){
gameViewModel.initialize()
MainStage(
gameGridContent = gameViewModel.gameGridContent,
onReplaceGridContent = gameViewModel::replaceGridContent,
onRemoveGridContent = gameViewModel::removeGridContent
)
}
Your initialize will actually run on every recomposition of gameScreen:
You click on a tile - state changes causing recomposition.
initializa is called and changes the state again causing recomposition.
Step 2 happens again and again.
You should initialize your view model in its constructor instead (or use boolean flag to force one tim initialization) to make it inly once.
Simply change it to constructor:
class GameViewModel : ViewModel(){
var gameGridContent = mutableStateListOf<Int>()
private set // Restrict writes to this state object to private setter only inside view model
fun replaceGridContent(int: Int, index: Int){
gameGridContent[index] = int
}
fun removeGridContent(index: Int){
gameGridContent[index] = -1
}
init {
for(i in 0..15){
gameGridContent.add(-1)
}
val firstEmptyGridTile = GameUtils.getRandomTilePosition(gameGridContent)
val firstGridNumber = GameUtils.getRandomTileNumber()
gameGridContent[firstEmptyGridTile] = firstGridNumber
}
}
Now you don't need to call initialize in the composable:
#Composable
fun gameScreen(gameViewModel: GameViewModel){
MainStage(
gameGridContent = gameViewModel.gameGridContent,
onReplaceGridContent = gameViewModel::replaceGridContent,
onRemoveGridContent = gameViewModel::removeGridContent
)
}

Android: Enable a Button inside RecyclerView.ViewHolder

I have a button (id: readyButtonIntro) inside a layout (introscreen.xml) that i need to enable. To do that, i have another button inside the RecyclerView.ViewHolder.
This is my Layout to need access
introscreen.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#color/colorWhite"
tools:context=".IntroScreenVC">
<LinearLayout
android:id="#+id/indicatorContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="40dp"
android:gravity="center"
android:orientation="horizontal"
app:layout_constraintBottom_toBottomOf="parent" />
<Button
android:id="#+id/readyButtonIntro"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:layout_marginBottom="16dp"
android:background="#color/colorWhite"
android:textColor="#color/colorTerciary"
android:alpha="0"
android:enabled="false"
android:text="Ready"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
And the another button is inside into the ViewHolder
slide_item_container.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="15dp"
>
<Button
android:id="#+id/addData"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:text="Agregar Datos"
android:background="#drawable/button_rounded2"
/>
</LinearLayout>
How can I enable from inside the class that listener the button?
class IntroSlideViewHolder(view: View) : RecyclerView.ViewHolder(view) {
init {
addData.setOnClickListener(View.OnClickListener {
//NEED TO ENABLE THE BUTTON
// val introScreen = IntroScreenVC()
// introScreen.readyButton()
}
}
I have a fun into IntroScreenVC but always have a error that its null, if a pass the context or view, do nothing.
fun readyButton(){
readyButtonIntro.isEnabled = true
}
Could you help me with this? I would really appreciate it.
Thank you very much!
Regards.
Edit:
I put the adapter and the ViewHolder for more information.
I ignored that because I didn't want to create confusion. Sorry for that..
Class Constructor
data class IntroSlide(val title: String, val description: String, val icon: Int, val firstButton: Boolean, val secondButton: Boolean, val thirdButton: Boolean)
IntroScreenVC.kt
class IntroScreenVC: AppCompatActivity() {
private val introSliderAdapter = IntroScreenAdapter(
listOf(
IntroSlide(
"title1",
"description1",
R.drawable.logo,
false,
false,
false
),
IntroSlide(
"title2",
"description2",
R.drawable.doggrooming,
true,
false,
false
),
IntroSlide(
"title3",
"description3",
R.drawable.introscreen3,
false,
true,
false
)
)
)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.introscreen)
introSliderViewPager.adapter = introSliderAdapter
}
}
IntroScreenAdapter.kt
class IntroScreenAdapter(private val introSlides: List<IntroSlide>) : RecyclerView.Adapter<IntroSlideViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): IntroSlideViewHolder {
val layoutInflater = LayoutInflater.from(parent?.context)
val cellForRow = layoutInflater.inflate(R.layout.slide_item_container,parent,false)
return IntroSlideViewHolder(cellForRow)
}
override fun getItemCount(): Int {
return introSlides.size
}
override fun onBindViewHolder(holder: IntroSlideViewHolder, position: Int) {
holder.bind(introSlides[position])
}
}
class IntroSlideViewHolder(view: View) : RecyclerView.ViewHolder(view) {
private val textTitle = view.findViewById<TextView>(R.id.textTitle)
private val textDescription = view.findViewById<TextView>(R.id.textDescription)
private val imageIcon = view.findViewById<ImageView>(R.id.imageSlideIcon)
private val addData = view.findViewById<Button>(R.id.addData)
private val addPet = view.findViewById<Button>(R.id.agregarMascota)
val contexto = itemView.context;
fun bind(introSlide: IntroSlide) {
textTitle.text = introSlide.title
textDescription.text = introSlide.description
imageIcon.setImageResource(introSlide.icon)
addData.isEnabled = introSlide.firstButton
addPet.isEnabled = introSlide.thirdButton
}
}
init {
addData.setOnClickListener(View.OnClickListener {
//ADD A ALERTDIALOG AND WHEN PRESS OK NEED TO ENABLE THAT BUTTON
val mDialogView = LayoutInflater.from(contexto).inflate(R.layout.alertdialog_add_data,null)
val builder = AlertDialog.Builder(contexto)
builder.setView(mDialogView)
val dialog: AlertDialog = builder.create()
dialog.show()
dialog.getWindow()?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT));
mDialogView.agregarDatosOK.setOnClickListener {
//HERE I NEED TO ENABLE THE BUTTON
//readyButtonIntro(introscreen.xml)
}
}
}
Edit2:
This is what I do with sharedPreferences.
IntroScreenAdapter.kt
class IntroSlideViewHolder(view: View) : RecyclerView.ViewHolder(view) {
private val textTitle = view.findViewById<TextView>(R.id.textTitle)
private val textDescription = view.findViewById<TextView>(R.id.textDescription)
private val imageIcon = view.findViewById<ImageView>(R.id.imageSlideIcon)
private val addData = view.findViewById<Button>(R.id.addData)
private val addPet = view.findViewById<Button>(R.id.agregarMascota)
//INIT sharedPreferences
private val prefs: SharedPreferences = view.context.getSharedPreferences(getString(R.string.prefs_file), Context.MODE_PRIVATE)
val contexto = itemView.context;
fun bind(introSlide: IntroSlide) {
textTitle.text = introSlide.title
textDescription.text = introSlide.description
imageIcon.setImageResource(introSlide.icon)
addData.isEnabled = introSlide.firstButton
addPet.isEnabled = introSlide.thirdButton
}
}
init {
addData.setOnClickListener(View.OnClickListener {
val mDialogView = LayoutInflater.from(contexto).inflate(R.layout.alertdialog_add_data,null)
val builder = AlertDialog.Builder(contexto)
builder.setView(mDialogView)
val dialog: AlertDialog = builder.create()
dialog.show()
dialog.getWindow()?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT));
mDialogView.agregarDatosOK.setOnClickListener {
//HERE EDIT THE sharedPreferences
with (prefs.edit()) {
putBoolean("ready_button_enabled", true)
apply()
}
dialog.dismiss()
}
}
}
IntroScreenVC.kt
class IntroScreenVC: AppCompatActivity() {
private val introSliderAdapter = IntroScreenAdapter(
listOf(
IntroSlide(
"title1",
"description1",
R.drawable.logo,
false,
false,
false
),
IntroSlide(
"title2",
"description2",
R.drawable.doggrooming,
true,
false,
false
),
IntroSlide(
"title3",
"description3",
R.drawable.introscreen3,
false,
true,
false
)
)
)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.introscreen)
introSliderViewPager.adapter = introSliderAdapter
}
//HERE PUT THE RESUME TO EXPECT THE SHOW AND ENABLE THE BUTTON
override fun onResume() {
super.onResume()
val prefs = getSharedPreferences(getString(R.string.prefs_file), Context.MODE_PRIVATE)
val buttonEnabled = prefs.getBoolean("ready_button_enabled", false)
readyButtonIntro.isEnabled = buttonEnabled
if (buttonEnabled) {
readyButtonIntro.alpha = 1f
}else {
readyButtonIntro.alpha = 0f
}
}
}
SOLUTION:
Into the Activity (IntroScreenVC)
class IntroScreenVC: AppCompatActivity(), IntroScreenAdapter.AdapterOnClick {
private val introSliderAdapter =
listOf(
...
)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.introscreen)
introSliderViewPager.adapter = IntroScreenAdapter(introSliderAdapter, this)
}
...
override fun onClick() {
//HERE ENABLE AND SHOW THE BUTTON
readyButtonIntro.isEnabled = true
readyButtonIntro.alpha = 1f
}
And the into the Adapter and RecyclerView
class IntroScreenAdapter(private val introSlides: List<IntroSlide>, val adapterOnClick: AdapterOnClick) : RecyclerView.Adapter<IntroScreenAdapter.IntroSliderViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): IntroScreenAdapter.IntroSliderViewHolder {
val layoutInflater = LayoutInflater.from(parent?.context)
val cellForRow = layoutInflater.inflate(R.layout.slide_item_container,parent,false)
return IntroSliderViewHolder(cellForRow)
}
...
inner class IntroSliderViewHolder(view: View) : RecyclerView.ViewHolder(view) {
...
init {
addData.setOnClickListener(View.OnClickListener {
val mDialogView = LayoutInflater.from(contexto).inflate(R.layout.alertdialog_add_data,null)
val builder = AlertDialog.Builder(contexto)
builder.setView(mDialogView)
val dialog: AlertDialog = builder.create()
dialog.show()
dialog.getWindow()?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT));
mDialogView.agregarDatosOK.setOnClickListener {
//FINALLY HERE CHANGE THE BUTTON TO ENABLE :)
adapterOnClick.onClick()
}
}
}
}
As I understand your problem you have a class A that is trying to communicate (change something) in class B.
There are several options for solving this kind of problem, depending on your exact needs.
From the code you have provided the relation of your Layouts and classes is not clear enough to me to give a more precise answer.
First of all, I understand you are using a recycler view.
A recycler view can have many items, and I assume you want to be able to enable that button from each item.
In order to let your IntroScreen class communicate with your viewholder, you have to pass a reference to the ViewHolder constructor.
For this purpose you could implement a simple "callback pattern".
Here is an example for defining an interface (e.g. for a function that enables the button) and implementing the callback.
Have a read here to see a well-explained example in Java. In Kotlin you could do it the same way.
Here a summary of the implementation steps:
define interface EnableButtonCallback that implements an abstract method enableButton
let your InfoScreen class implement that interface (in which you enable the button)
pass your InfoScreen class to your RecyclerView adapter and then from your adapter to your ViewHolder
in your ViewHolder onClickListener call the interface method enableButton
Update 2020/08/11
I try to give suggestions based on your updated code.
In the intro screen you set your viewPager adapter, but it is still not clear where this property is coming from and where exactly it is displayed. I guess maybe you just cut out the parameter definition. However, I just assume you have your views set up properly and this is not a problem here. For using recycler view with viewPager I found some related information here.
I can not yet see your use case clearly yet. Are you adding data persistently? Then should your button in the IntroScreen be permanently enabled?
In this case probably SharedPreferences are a good choice for persisting this kind of information. Even when it doesn't need to be persisted. Reading one shared preference file is lightweight and quick enough to be done on the main thread.
I will give you an example implementation here:
Get a shared preferences object
val sharedPref = activity?.getSharedPreferences(
"intro_button_settings_file", Context.MODE_PRIVATE) // String with the key should be in your string resource file
Pass your sharedPref to your adapter and your viewHolder and write to it:
with (sharedPref.edit()) {
putBoolean("ready_button_enabled", true) // String with the key should be in your string resource file
commit()
}
in your IntroScreen check the setting
val readyButtonShouldBeEnabled = sharedPref.getBoolean("ready_button_enabled",
false) // defaults to false
If, after clicking your enable button (that sets the setting to true), you need to return to your IntroScreen activity: then you could enable your button in your activities onResume method
A different solution would be:
You check the setting in your IntroScreen onClick method.
Then you don't need to disable the button.
You just set:
// in your IntroScreen readyButtonIntro onClick method
val buttonEnabled = sharedPref.getBoolean("ready_button_enabled",
false)
if (!buttonEnabled) {
// optional: write a Toast to notify the user why the button is doing nothing (yet)
Toast.makeText(yourIntroScreenContext, "First agregar datos", Toast.LENGTH_SHORT).show()
return // onClick returns, so nothing else will happen when clicked
}
... // your code when the button **should** be enabled
If your button should be disabled again, simply save false to the setting.
Since I do not know more about your use case, this seems like an easy and quick solution to me. This way you do not need to bother with implementing an interface. Anyways, when clicking your button in your viewHolder there is no immediate action taking place in your IntroScreen activity. You still want the user to return to the IntroScreen and click the enabled button.
Then checking if your button was enabled just when clicking on it appears sufficient to me.

Android how to interact with nested recyclerView from fragment

How does one properly send data to child adapter in a fragment?
I'm basically trying to implement an Instagram like comments-section, e.g. a bunch of comments that can each have more comments (replies).
To do that, I use one main recyclerView + main adapter, which instances are retained in my fragment, and within the main adapter I bind the children comments (recyclerView + adapter).
Adding comments to the main adapter is easy since the object is always available in the fragment, so I just call mainAdapter.addComments(newComments):
MainAdapter
fun addComments(newComments: List<Comment>){
comments.addAll( 0, newComments) //loading comments or previous comments go to the beginning
notifyItemRangeInserted(0, newComments.size)
}
But how to call addComments of one particular nested-rV? I read I should not save the adapter instances and only use positions.
I'm trying to do that in my Fragment as follows:
val item = rVComments.findViewHolderForItemId(mAdapter.itemId)!!.itemView
val adapt = item.rVReplies.adapter as ChildCommentsAdapter
adapt.addComment(it.data.comment)
But that doesn't work very well: since we have only RecyclerViews, that particular ViewHolder is often already recycled if the user scrolled after posting or fetching items, which leads to a NullPointerException.
Hence the initial question: how does one properly interact with nested recyclerviews and their adapter? If the answer is via Interface, please provide an example as I've tried it without success since I shouldn't save adapter objects.
You can achieve that using a single multi-view type adapter by placing the comments
as part of the parent item, with that, you add the child items below the parent item and call notifyItemRangeInserted.
That way you don't have to deal with most of the recycling issues.
When you want to update a comment you just update the comment inside the parent item and call notifyItemChanged.
If you want I created a library that can generate that code for you in compile time.
It supports the exact case you wanted and much more.
Using #Gil Goldzweig's suggestion, here is what I did: in case of an Instagram like comments' system with replies, I did use a nested recyclerView system. It just makes it easier to add and remove items. However, as for the question How does one properly send data to child adapter in a fragment? You don't. It gets super messy. From my fragment, I sent the data to my mainAdapter, which in turn sent the data to the relevant childAdapter. The key to make it smooth is using notifyItemRangeInserted when adding a comment to the mainAdapter and then notifyItemChanged when adding replies to a comment. The second event will allow sending data to the child adapter using the payload. Here's the code in case other people are interested:
Fragment
class CommentsFragment : androidx.fragment.app.Fragment(), Injectable,
SendCommentButton.OnSendClickListener, CommentsAdapter.Listener {
#Inject
lateinit var viewModelFactory: ViewModelProvider.Factory
private val viewModel by lazy {
ViewModelProviders.of(requireActivity(), viewModelFactory).get(CommentsViewModel::class.java)
}
private val searchViewModel by lazy {
ViewModelProviders.of(requireActivity(), viewModelFactory).get(SearchViewModel::class.java)
}
private val mAdapter = CommentsAdapter(this)
private var contentid: Int = 0 //store the contentid to process further posts or requests for more comments
private var isLoadingMoreComments: Boolean = false //used to check if we should fetch more comments
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_comments, container, false)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
//hide the action bar
activity?.findViewById<BottomNavigationView>(R.id.bottomNavView)?.visibility = View.GONE
contentid = arguments!!.getInt("contentid") //argument is mandatory, since comment is only available on content
ivBackArrow.setOnClickListener{ activity!!.onBackPressed() }
viewModel.initComments(contentid) //fetch comments
val layoutManager = LinearLayoutManager(this.context)
layoutManager.stackFromEnd = true
rVComments.layoutManager = layoutManager
mAdapter.setHasStableIds(true)
rVComments.adapter = mAdapter
setupObserver() //observe initial comments response
setupSendCommentButton()
post_comment_text.setSearchViewModel(searchViewModel)
setupScrollListener(layoutManager) //scroll listener to load more comments
iVCancelReplyTo.setOnClickListener{
//reset ReplyTo function
resetReplyLayout()
}
}
private fun loadMoreComments(){
viewModel.fetchMoreComments(contentid, mAdapter.itemCount)
setupObserver()
}
/*
1.check if not already loading
2.check scroll position 0
3.check total visible items != total recycle items
4.check itemcount to make sure we can still make request
*/
private fun setupScrollListener(layoutManager: LinearLayoutManager){
rVComments.addOnScrollListener(object: RecyclerView.OnScrollListener(){
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
val visibleItemCount = rVComments.childCount
val totalItemCount = layoutManager.itemCount
val pos = layoutManager.findFirstCompletelyVisibleItemPosition()
if(!isLoadingMoreComments && pos==0 && visibleItemCount!=totalItemCount && mAdapter.itemCount%10==0){
//fetch more comments
isLoadingMoreComments = true
loadMoreComments()
}
}
})
}
private fun setupSendCommentButton() {
btnSendComment.setOnSendClickListener(this)
}
override fun onSendClickListener(v: View?) {
if(isInputValid(post_comment_text.text.toString())) {
val isReply = mAdapter.commentid!=null
viewModel.postComment(post_comment_text.text.toString(), mAdapter.commentid?: contentid, isReply) //get reply ID, otherwise contentID
observePost()
post_comment_text.setText("")
btnSendComment.setCurrentState(SendCommentButton.STATE_DONE)
}
}
override fun postCommentAsReply(username: String) {
//main adapter method to post a reply
val replyText = "${getString(R.string.replyingTo)} $username"
tVReplyTo.text = replyText
layoutReplyTo.visibility=View.VISIBLE
post_comment_text.requestFocus()
}
override fun fetchReplies(commentid: Int, commentsCount: Int) {
//main adapter method to fetch replies
if(!isLoadingMoreComments){ //load one series at a time
isLoadingMoreComments = true
viewModel.fetchReplies(commentid, commentsCount)
viewModel.replies.observe(this, Observer<Resource<List<Comment>>> {
if (it?.data != null) when (it.status) {
Resource.Status.LOADING -> {
//showProgressBar(true)
}
Resource.Status.ERROR -> {
//showProgressBar(false)
isLoadingMoreComments = false
}
Resource.Status.SUCCESS -> {
isLoadingMoreComments = false
mAdapter.addReplies(mAdapter.replyCommentPosition!!, it.data)
rVComments.scrollToPosition(mAdapter.replyCommentPosition!!)
}
}
})
}
}
private fun isInputValid(text: String): Boolean = text.isNotEmpty()
private fun observePost(){
viewModel.postComment.observe(this, Observer<Resource<PostCommentResponse>> {
if (it?.data != null) when (it.status) {
Resource.Status.LOADING -> {
//showProgressBar(true)
}
Resource.Status.ERROR -> {
//showProgressBar(false)
}
Resource.Status.SUCCESS -> {
if(it.data.asReply){
//dispatch comment to child adapter via main adapter
mAdapter.addReply(mAdapter.replyCommentPosition!!, it.data.comment)
rVComments.scrollToPosition(mAdapter.replyCommentPosition!!)
}else{
mAdapter.addComment(it.data.comment)
}
resetReplyLayout()
//showProgressBar(false)
}
}
})
}
private fun setupObserver(){
viewModel.comments.observe(this, Observer<Resource<List<Comment>>> {
if (it?.data != null) when (it.status) {
Resource.Status.LOADING -> {
//showProgressBar(true)
}
Resource.Status.ERROR -> {
isLoadingMoreComments = false
//showProgressBar(false)
}
Resource.Status.SUCCESS -> {
mAdapter.addComments(it.data)
isLoadingMoreComments = false
//showProgressBar(false)
}
}
})
}
private fun resetReplyLayout(){
layoutReplyTo.visibility=View.GONE
mAdapter.replyCommentPosition = null
mAdapter.commentid = null
}
override fun onStop() {
super.onStop()
activity?.findViewById<BottomNavigationView>(R.id.bottomNavView)?.visibility = View.VISIBLE
}
}
MainAdapter
class CommentsAdapter(private val listener: Listener) : RecyclerView.Adapter<CommentsAdapter.ViewHolder>(), ChildCommentsAdapter.ChildListener {
//method from child adapter
override fun postChildReply(replyid: Int, username: String, position: Int) {
commentid = replyid
replyCommentPosition = position
listener.postCommentAsReply(username)
}
interface Listener {
fun postCommentAsReply(username: String)
fun fetchReplies(commentid: Int, commentsCount: Int=0)
}
class ViewHolder(val view: View) : RecyclerView.ViewHolder(view)
private var comments = mutableListOf<Comment>()
private var repliesVisibility = mutableListOf<Boolean>() //used to store visibility state for replies
var replyCommentPosition: Int? = null //store the main comment's position
var commentid: Int? = null //used to indicate which comment is replied to
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_comment, parent, false)
return ViewHolder(view)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val comment = comments[position]
with(holder.view) {
//reset visibilities (rebinding purpose)
rVReplies.visibility = View.GONE
iVMoreReplies.visibility = View.GONE
tVReplies.visibility = View.GONE
content.loadUserPhoto(comment.avatarThumbnailURL)
text.setCaptionText(comment.username!!, comment.comment)
tvTimestamp.setTimeStamp(comment.timestamp!!)
val child = ChildCommentsAdapter(
//we pass parent commentid and position to child to be able to pass it again on click
this#CommentsAdapter, comments[holder.adapterPosition].id!!, holder.adapterPosition
)
val layoutManager = LinearLayoutManager(this.context)
rVReplies.layoutManager = layoutManager
rVReplies.adapter = child
//initial visibility block when binding the viewHolder
val txtMore = this.resources.getString(R.string.show_more_replies)
if(comment.repliesCount>0) {
tVReplies.visibility = View.VISIBLE
if (repliesVisibility[position]) {
//replies are to be shown directly
rVReplies.visibility = View.VISIBLE
child.addComments(comment.replies!!)
tVReplies.text = resources.getString(R.string.hide_replies)
if (comment.repliesCount > comment.replies!!.size) {
//show the load more replies arrow if we can fetch more replies
iVMoreReplies.visibility = View.VISIBLE
}
} else {
//replies all hidden
val txt = txtMore + " (${comment.repliesCount})"
tVReplies.text = txt
}
}
//second visibility block when toggling with the show more/hide textView
tVReplies.setOnClickListener{
//toggle child recyclerView visibility and change textView text
if(holder.view.rVReplies.visibility == View.GONE){
//show stuff
if(comment.replies!!.isEmpty()){
Timber.d(holder.adapterPosition.toString())
//fetch replies if none were fetched yet
replyCommentPosition = holder.adapterPosition
listener.fetchReplies(comments[holder.adapterPosition].id!!)
}else{
//load comments into adapter if not already
if(comment.replies!!.size>child.comments.size){child.addComments(comment.replies!!)}
}
repliesVisibility[position] = true
holder.view.rVReplies.visibility = View.VISIBLE
holder.view.tVReplies.text = holder.view.resources.getString(R.string.hide_replies)
if (comment.repliesCount > comment.replies!!.size && comment.replies!!.isNotEmpty()) {
//show the load more replies arrow if we can fetch more replies
iVMoreReplies.visibility = View.VISIBLE
}
}else{
//hide replies and change text
repliesVisibility[position] = false
holder.view.rVReplies.visibility = View.GONE
holder.view.iVMoreReplies.visibility = View.GONE
val txt = txtMore + " (${comment.repliesCount})"
holder.view.tVReplies.text = txt
}
}
tvReply.setOnClickListener{
replyCommentPosition = holder.adapterPosition
commentid = comments[holder.adapterPosition].id!!
listener.postCommentAsReply(comments[holder.adapterPosition].username!!)
}
iVMoreReplies.setOnClickListener{
replyCommentPosition = holder.adapterPosition
listener.fetchReplies(comments[holder.adapterPosition].id!!, layoutManager.itemCount) //pass amount of replies too
}
}
}
#Suppress("UNCHECKED_CAST")
override fun onBindViewHolder(holder: ViewHolder, position: Int, payloads: MutableList<Any>) {
if(payloads.isNotEmpty()){
//add reply to child adapter
with(holder.view){
Timber.d(payloads.toString())
val adapter = rVReplies.adapter as ChildCommentsAdapter
if(payloads[0] is Comment){
adapter.addComment(payloads[0] as Comment)
}else{
//will be of type List<Comment>
adapter.addComments(payloads[0] as List<Comment>)
val comment = comments[position]
if (comment.repliesCount > comment.replies!!.size) {
//show the load more replies arrow if we can fetch more replies
iVMoreReplies.visibility = View.VISIBLE
}else{
iVMoreReplies.visibility = View.GONE
}
}
}
}else{
super.onBindViewHolder(holder,position, payloads) //delegate to normal binding process
}
}
override fun getItemCount(): Int = comments.size
//add multiple replies to child adapter at pos 0
fun addReplies(position: Int, newComments: List<Comment>){
comments[position].replies!!.addAll(0, newComments)
notifyItemChanged(position, newComments)
}
//add a single reply to child adapter at last position
fun addReply(position: Int, newComment: Comment){
comments[position].replies!!.add(newComment)
comments[position].repliesCount += 1 //update replies count in case viewHolder gets rebinded
notifyItemChanged(position, newComment)
}
//add a new comment to main adapter at last position
fun addComment(comment: Comment){
comments.add(comment) //new comment just made goes to the end
repliesVisibility.add(false)
notifyItemInserted(itemCount-1)
}
//add multiple new comments to main adapter at pos 0
fun addComments(newComments: List<Comment>){
comments.addAll( 0, newComments) //loading comments or previous comments go to the beginning
repliesVisibility.addAll(0, List(newComments.size) { false })
notifyItemRangeInserted(0, newComments.size)
}
}
The childAdapter is very basic and has nearly 0 logic.

Categories

Resources