I have an Android application, that reads Bluetooth values from a device. I present these values in a recyclerView. I would like to find a way to update this list and i feel abit lost since i tried diffrent solutions that i found on google.
I really dont know if it's my recyclerView thats the issue or my livedata Observer that is not getting triggerd. When i try to read the values again the list disappears, but i can see in my logs that there is a new list of values comming from the viewmodel
The app runs as i intended the first iteration.
I can provide more code if needed
Sharing some code for extra clarity
Here i read the values where i retrive a byteArray.
ReadLineNodeValues().iRObjectTemperature -> {
bleListViewModel.addBleToList(characteristic.value)
}
Which i send to my ViewModel here
class BleValueViewModel: ViewModel() {
fun addBleToList(bleValue: ByteArray) {
blueToothLEvalue.add(bleValue)
mutableLiveDataBluetooth.postValue(blueToothLEvalue)
}
fun getList(): MutableLiveData<ArrayList<ByteArray>> {
return mutableLiveDataBluetooth
}
Here i trying to retrive the list where i add it to my recyclverView
private fun showItems() {
val bleValueViewModel = ViewModelProvider(requireActivity()).get(BleValueViewModel::class.java)
bleValueViewModel.getList().observe(viewLifecycleOwner) {
if (it.size == 26) {
showlist(it)
}
}
}
RecyclerView
class LineNodeValueDataAdapter :
RecyclerView.Adapter<LineNodeBigViewHolder>() {
private val differCallback = object : DiffUtil.ItemCallback<LinenNodeValueData>() {
override fun areItemsTheSame(
oldItem: LinenNodeValueData,
newItem: LinenNodeValueData
): Boolean {
return oldItem.valueOne == newItem.valueOne && oldItem.valueTwo == newItem.valueTwo && oldItem.valueThree == newItem.valueThree && oldItem.valueFour == newItem.valueFour
}
override fun areContentsTheSame(
oldItem: LinenNodeValueData,
newItem: LinenNodeValueData
): Boolean {
return oldItem == newItem
}
}
val differ = AsyncListDiffer(this, differCallback)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): LineNodeBigViewHolder {
val binding = CardviewListlayoutValueBinding
.inflate(LayoutInflater.from(parent.context), parent, false)
return LineNodeBigViewHolder(binding)
}
override fun onBindViewHolder(holder: LineNodeBigViewHolder, position: Int) {
val place = differ.currentList[position]
holder.bind(place)
}
override fun getItemCount(): Int = differ.currentList.size
}
I convert the values since they all come as ByteArrays
fun showlist(listValues: MutableList<ByteArray>) {
//Systemnode
val systemNodeMCUTemp = String(listValues[0])
val systemNodeVoltages = String(listValues[1])
val systemNodeWeatherSensor = String(listValues[2])
val systemNodeOpenThreadCfg = String(listValues[3])
//Linenode
val currentLineNode = String(listValues[4])
val acceleroMeterX = ByteBuffer.wrap(listValues[5]).order(ByteOrder.LITTLE_ENDIAN).float
val acceleroMeterY = ByteBuffer.wrap(listValues[6]).order(ByteOrder.LITTLE_ENDIAN).float
val acceleroMeterZ = ByteBuffer.wrap(listValues[7]).order(ByteOrder.LITTLE_ENDIAN).float
val iRObjectTemperature =
ByteBuffer.wrap(listValues[8]).order(ByteOrder.LITTLE_ENDIAN).float
val contactSensorTemperature =
ByteBuffer.wrap(listValues[9]).order(ByteOrder.LITTLE_ENDIAN).float
val magneticField = ByteBuffer.wrap(listValues[10]).order(ByteOrder.LITTLE_ENDIAN).float
val internalVoltage1V8 =
ByteBuffer.wrap(listValues[11]).order(ByteOrder.LITTLE_ENDIAN).float
val internalVoltagevBusRail =
ByteBuffer.wrap(listValues[12]).order(ByteOrder.LITTLE_ENDIAN).float
val internalVoltageVDD =
ByteBuffer.wrap(listValues[13]).order(ByteOrder.LITTLE_ENDIAN).float
val internalVoltageVDDH =
ByteBuffer.wrap(listValues[14]).order(ByteOrder.LITTLE_ENDIAN).float
val powerHarvestingVoltageSol1 =
ByteBuffer.wrap(listValues[15]).order(ByteOrder.LITTLE_ENDIAN).float
val powerHarvestingVoltageSol2 =
ByteBuffer.wrap(listValues[16]).order(ByteOrder.LITTLE_ENDIAN).float
val powerHarvestingVoltageEmppt =
ByteBuffer.wrap(listValues[17]).order(ByteOrder.LITTLE_ENDIAN).float
val powerHarvestingVoltageHmppt =
ByteBuffer.wrap(listValues[18]).order(ByteOrder.LITTLE_ENDIAN).float
val eHFieldHField =
ByteBuffer.wrap(listValues[19]).order(ByteOrder.LITTLE_ENDIAN).float
val eHFieldEField =
ByteBuffer.wrap(listValues[20]).order(ByteOrder.LITTLE_ENDIAN).float
val ambientTemperaturesAcc =
ByteBuffer.wrap(listValues[21]).order(ByteOrder.LITTLE_ENDIAN).float
val ambientTemperaturesIR =
ByteBuffer.wrap(listValues[22]).order(ByteOrder.LITTLE_ENDIAN).float
val ambientTemperaturesMag =
ByteBuffer.wrap(listValues[23]).order(ByteOrder.LITTLE_ENDIAN).float
val ambientTemperaturesMCU =
ByteBuffer.wrap(listValues[24]).order(ByteOrder.LITTLE_ENDIAN).float
val openThreadCFG =
ByteBuffer.wrap(listValues[25]).order(ByteOrder.LITTLE_ENDIAN).float
val systemNodeList = ArrayList<SystemValue>()
systemNodeList.add(SystemValue("Systemode MguTemp", systemNodeMCUTemp))
systemNodeList.add(SystemValue("Systemnode Voltages", systemNodeVoltages))
systemNodeList.add(SystemValue("Systemnode WeatherSensor", systemNodeWeatherSensor))
systemNodeList.add(SystemValue("Systemnode OpenThreadCfg", systemNodeOpenThreadCfg))
val lineNodevaluelist = ArrayList<LinenNodeValueData>()
val lineNodeListBle = ArrayList<SystemValue>()
lineNodeListBle.add(SystemValue("LineNode Service", currentLineNode))
lineNodeListBle.add(SystemValue("Current LineNode", acceleroMeterX.toString()))
//lineNodeListBle.add(SystemValue("Acclero meter", lineNodeValueSeven.toString()))
Timber.i("Acclerometer2 :: ${acceleroMeterY}}")
lineNodeListBle.add(
SystemValue(
"Accelero Meter",
"X: $acceleroMeterX " + "Y: $acceleroMeterY"
+ " Z: $acceleroMeterZ"
)
)
lineNodeListBle.add(
SystemValue(
"iRObject Temperature",
iRObjectTemperature.toString()
)
)
lineNodeListBle.add(
SystemValue(
"ContactSensor Temperature",
contactSensorTemperature.toString()
)
)
lineNodeListBle.add(SystemValue("Magnetic Field", magneticField.toString()))
lineNodevaluelist.add(
LinenNodeValueData(
"Internal Voltage",
"1V8: $internalVoltage1V8",
"Vbus rail: $internalVoltagevBusRail",
"VDD: $internalVoltageVDD",
"VDDH: $internalVoltageVDDH"
)
)
lineNodevaluelist.add(
LinenNodeValueData(
"Power Harvesting Voltage",
"Sol1: $powerHarvestingVoltageSol1",
"Sol2: $powerHarvestingVoltageSol2",
"E-Mppt: $powerHarvestingVoltageEmppt", "H-Mppt: $powerHarvestingVoltageHmppt"
)
)
lineNodeListBle.add(
SystemValue(
"eHField",
"H-field: $eHFieldHField E-field: $eHFieldEField"
)
)
lineNodevaluelist.add(
LinenNodeValueData(
"Ambient Temperatures",
"Acc: $ambientTemperaturesAcc",
"IR: $ambientTemperaturesIR",
"Mag: $ambientTemperaturesMag",
"Mcu: $ambientTemperaturesMCU"
)
)
lineNodeListBle.add(SystemValue("openThreadCFG", openThreadCFG.toString()))
setupRecyclerViewSystemNode(binding.systemNodeRecyclerView, systemNodeList)
Timber.i("blueviewRecycler :: ${systemNodeList.size}")
setupRecylerViewLineNodeOne(binding.lineNodeRecyclerView, lineNodeListBle, lineNodevaluelist)
Timber.i("blueviewRecycler1 :: ${lineNodeListBle.size}+${lineNodevaluelist.size}")
}
Have a nice day everyone
Kind regards
Droid
Here you have a condition.
if (it.size == 26) {
showlist(it)
}
If you add new values to the list, list.size changed and condition is false. So, just remove a condition:
bleValueViewModel.getList().observe(viewLifecycleOwner) {
showlist(it)
}
also, i hope you call showItems only once.
Regarding your diff utils implementation, from the doc:
areContentsTheSame(int oldItemPosition, int newItemPosition)
Called by the DiffUtil when it wants to check whether two items have
the same data.
areItemsTheSame(int oldItemPosition, int newItemPosition)
Called by the DiffUtil to decide whether two object represent the same
Item.
Also it looks like you should reimplement your DIfUtils Callback:
private val differCallback = object : DiffUtil.ItemCallback<LinenNodeValueData>() {
override fun areItemsTheSame(
oldItem: LinenNodeValueData,
newItem: LinenNodeValueData
): Boolean {
return //return true if i's the same item. I suppose in your case you should check that it's the same ble characteristic.
}
override fun areContentsTheSame(
oldItem: LinenNodeValueData,
newItem: LinenNodeValueData
): Boolean {
return // return true if it has the same content. If you return true it means that content(values) for the object has not change. In other case if values are different, you should return false, it means that values for characteristic changed and recyclerView should update respective view.
}
}
Related
We are making custom lint for the purpose of applying them to the Android project.
The problematic part is the lint that requires the use of NamedArgument in the Composable function.
#Composable
fun MyComposable(
a: String,
b: Int,
onClick: () -> Unit = {},
) {}
#Composable
fun success1() {
MyComposable(
a = "success",
b = 1,
) {
}
}
I was thinking about how to determine if MyComposable is #Composable when calling MyComposable in the success1 function in the following cases:
class NamedArgumentDetector : Detector(), SourceCodeScanner {
override fun getApplicableUastTypes() = listOf(
UExpression::class.java,
)
override fun createUastHandler(context: JavaContext) = object : UElementHandler() {
override fun visitExpression(node: UExpression) {
if (node !is KotlinUFunctionCallExpression || !node.isInvokedWithinComposable()) return
val firstMethodName = node.methodName?.first() ?: return
if (firstMethodName !in 'A'..'Z') return
val lastArgumentIndex = node.valueArguments.lastIndex
node.valueArguments.fastForEachIndexed { index, argument ->
if (index == lastArgumentIndex && argument is KotlinULambdaExpression) return
val expressionSourcePsi = argument.sourcePsi
val argumentParent = expressionSourcePsi?.node?.treeParent ?: return
val argumentFirstChildNode = argumentParent.firstChildNode
val argumentParentFirstChildNode = argumentParent.treeParent.firstChildNode
if (!(
argumentFirstChildNode.isValueArgumentName() ||
argumentParentFirstChildNode.isValueArgumentName()
)
) {
context.report(
issue = NamedArgumentIssue,
scope = expressionSourcePsi,
location = context.getLocation(expressionSourcePsi),
message = Explanation,
)
return
}
}
}
}
private fun ASTNode.isValueArgumentName() =
this.elementType == VALUE_ARGUMENT_NAME
}
Detector code. In the example above, we were able to check whether success1 invokes Composable, but I can't think of a way to check whether MyComposable called by success1 is a Composable function.
I would appreciate it if you let me know if there is a way to check.
I'm learning data structures and trying to implement a dynamic array from scratch in Kotlin using generics. I came up with the following implementation using a MutableList but that feels like cheating 😅. Am I doing this correctly or is there another/better way that allows me to learn by implementing the individual operations manually? What's the usual way others go about this?
class DynamicArray<T>(
private var values: MutableList<T>
) {
var length: Int = values.size
private set
var isEmpty: Boolean = length > 0
private set
fun getValues() = values
// O(1) time complexity
fun lookup(index: Int) = values[index]
// O(1) time complexity
fun push(item: T): MutableList<T> {
values.add(length, item)
length++
return values
}
// O(n) time complexity because we have to shift remaining items
fun remove(item: T): MutableList<T> {
values.remove(item)
length--
return values
}
}
Specifically you're saying that in order for your DynamicArray to exist, you need an already implemented mutable list (dynamic array) structure to rely upon.
So if you're trying to learn how these data structures are made, you should try to make one instead of using one. At the moment you're just delegating the difficult parts to someone else's work.
Try implementing this using only the Array type to make an ArrayList style implementation, or try not using an Array at all to implement a LinkedList type structure.
Here's a quick implementation that uses Array as the underlying DS.
It implements a MutableIterable which is basically an Iterable that also allows you to remove an element while iterating.
#Suppress("UNCHECKED_CAST")
class DynamicArray<T>(
private var size: Int,
private val expansionFactor: Float = 2f,
private val init: (Int) -> T) : MutableIterable<T> {
var capacity: Int = size
private set
private var arr: Array<Any?> = Array(size, init)
inner class DynamicArrayIterator: MutableIterator<T>{
val iterator = arr.iterator()
var index = -1
override fun hasNext(): Boolean {
return index<size-1
}
override fun next(): T {
if(iterator.hasNext()) index++
else throw NoSuchElementException()
return iterator.next() as T
}
override fun remove() {
removeAt(index--)
}
}
override fun iterator(): MutableIterator<T> = DynamicArrayIterator()
fun size(): Int = size
//O(1)
fun get(index: Int): T {
if(index > size-1) throw IndexOutOfBoundsException()
return arr[index] as T
}
//O(1)
fun set(index: Int, element: T) {
if(index > size-1) throw IndexOutOfBoundsException()
arr[index] = element
}
//O(1) amortized
fun add(element: T){
if(size == capacity){
capacity = (expansionFactor*size).toInt()
val newArr = Array<Any?>(capacity, init = init)
arr.forEachIndexed { index, any -> newArr[index] = any as T }
arr = newArr
}
arr[size] = element
size++
}
//O(n)
fun removeAt(index: Int): T{
if(index > size-1) throw IndexOutOfBoundsException()
val element = arr[index] as T
for( i in index until size){
arr[i] = arr[i+1]
}
size--
return element
}
//O(n)
fun remove(element: T): Boolean{
for(i in 0 until size){
if(arr[i] == element) {
removeAt(i)
return true
}
}
return false
}
//O(n)
override fun toString(): String {
val stringBuilder = StringBuilder()
stringBuilder.append("[")
var index = 0
do{
if (index in 1 until size)
stringBuilder.append(", ")
stringBuilder.append(arr[index])
index++
} while (index < size)
return stringBuilder.append("]").toString()
}
}
Problem:
I get 40 items at the beginning of the list, then it starts to count from 11, and after this, everything is good. So, 1...40,11,12,13,...,300.
And when I scroll a lot to the bottom and then scroll up to see first items, the items have been changed to 1,2,...,10,1,2,...,10,1,2,...,10,11,12,...,300.
But, when I pass false to enablePlaceholders in the PagingConfig, when I scroll to the bottom, I see the issue as I said above(1,2,..,40,11,...,300) and suddenly the 40 items vanish and I only see 1,2,...,10 + 11,12,...,300(the correct way); And it doesn't change or get worse again.
ProductsPagingSource:
#Singleton
class ProductsPagingSource #Inject constructor(
private val productsApi: ProductsApi
//private val query: String
) : RxPagingSource<Int, RecyclerItem>() {
override fun loadSingle(params: LoadParams<Int>): Single<LoadResult<Int, RecyclerItem>> {
val position = params.key ?: STARTING_PAGE_INDEX
//val apiQuery = query
return productsApi.getBeersList(position, params.loadSize)
.subscribeOn(Schedulers.io())
.map { listBeerResponse ->
listBeerResponse.map { beerResponse ->
beerResponse.toDomain()
}
}
.map { toLoadResult(it, position) }
.onErrorReturn { LoadResult.Error(it) }
}
private fun toLoadResult(
#NonNull response: List<RecyclerItem>,
position: Int
): LoadResult<Int, RecyclerItem> {
return LoadResult.Page(
data = response,
prevKey = if (position == STARTING_PAGE_INDEX) null else position - 1,
nextKey = if (response.isEmpty()) null else position + 1,
itemsBefore = LoadResult.Page.COUNT_UNDEFINED,
itemsAfter = LoadResult.Page.COUNT_UNDEFINED
)
}
}
ProductsListRepositoryImpl:
#Singleton
class ProductsListRepositoryImpl #Inject constructor(
private val pagingSource: ProductsPagingSource
) : ProductsListRepository {
override fun getBeers(ids: String): Flowable<PagingData<RecyclerItem>> = Pager(
config = PagingConfig(
pageSize = 10,
enablePlaceholders = true,
maxSize = 30,
prefetchDistance = 5,
initialLoadSize = 40
),
pagingSourceFactory = { pagingSource }
).flowable
}
ProductsListViewModel:
class ProductsListViewModel #ViewModelInject constructor(
private val getBeersUseCase: GetBeersUseCase
) : BaseViewModel() {
private val _ldProductsList: MutableLiveData<PagingData<RecyclerItem>> = MutableLiveData()
val ldProductsList: LiveData<PagingData<RecyclerItem>> = _ldProductsList
init {
loading(true)
getProducts("")
}
private fun getProducts(ids: String) {
loading(false)
getBeersUseCase(GetBeersParams(ids = ids))
.cachedIn(viewModelScope)
.observeOn(AndroidSchedulers.mainThread())
.subscribe {
_ldProductsList.value = it
}.addTo(compositeDisposable)
}
}
ProductsListFragment:
#AndroidEntryPoint
class ProductsListFragment : Fragment(R.layout.fragment_product_list) {
private val productsListViewModel: ProductsListViewModel by viewModels()
private val productsListAdapter: ProductsListAdapter by lazy {
ProductsListAdapter(::navigateToProductDetail)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupRecycler()
setupViewModel()
}
private fun setupRecycler() {
itemErrorContainer.gone()
productListRecyclerView.adapter = productsListAdapter
}
private fun setupViewModel() {
productsListViewModel.run {
observe(ldProductsList, ::addProductsList)
observe(ldLoading, ::loadingUI)
observe(ldFailure, ::handleFailure)
}
}
private fun addProductsList(productsList: PagingData<RecyclerItem>) {
loadingUI(false)
productListRecyclerView.visible()
productsListAdapter.submitData(lifecycle, productsList)
}
...
BASE_DIFF_CALLBACK:
val BASE_DIFF_CALLBACK = object : DiffUtil.ItemCallback<RecyclerItem>() {
override fun areItemsTheSame(oldItem: RecyclerItem, newItem: RecyclerItem): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: RecyclerItem, newItem: RecyclerItem): Boolean {
return oldItem == newItem
}
}
BasePagedListAdapter:
abstract class BasePagedListAdapter(
vararg types: Cell<RecyclerItem>,
private val onItemClick: (RecyclerItem, ImageView) -> Unit
) : PagingDataAdapter<RecyclerItem, RecyclerView.ViewHolder>(BASE_DIFF_CALLBACK) {
private val cellTypes: CellTypes<RecyclerItem> = CellTypes(*types)
override fun getItemViewType(position: Int): Int {
getItem(position).let {
return cellTypes.of(it).type()
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return cellTypes.of(viewType).holder(parent)
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
getItem(position).let {
cellTypes.of(it).bind(holder, it, onItemClick)
}
}
}
I had to set the same number for pageSize and initialLoadSize. Also, I had to set the enablePlaceholders to false.
config = PagingConfig(
pageSize = 10,
enablePlaceholders = false,
maxSize = 30,
prefetchDistance = 5,
initialLoadSize = 10
),
But still, I want to know if it's the normal way? If yes, I couldn't find anywhere point to this! If not, why's that? Why initialLoadSize can not have value more than the pageSize?!
As we can see, the default value for the initialLoadSize is:
internal const val DEFAULT_INITIAL_PAGE_MULTIPLIER = 3
val initialLoadSize: Int = pageSize * DEFAULT_INITIAL_PAGE_MULTIPLIER,
Your issue is most likely because at the repository layer your results page numbers are indexed with numbers derived from the total divided by your page size.
This page number scheme assumes that you will page through the items with pages of the same size. However, Paging wants to get an initial page that's three times bigger by default, and then each page should be the page size.
So, the indexes might be like 0 through totalElements/PageSize, with the number for page that includes a given position equaling itemsIndex/PageSize.
This part is especially relevant:
return LoadResult.Page(
data = response,
prevKey = if (position == STARTING_PAGE_INDEX) null else position - 1,
nextKey = if (response.isEmpty()) null else position + 1,
itemsBefore = LoadResult.Page.COUNT_UNDEFINED,
itemsAfter = LoadResult.Page.COUNT_UNDEFINED
)
Let's imagine a pagesize of 10, an initial load of 30, and 100 elements total. We start with page 0, and we request 30 items. Position is 0, and params.loadSize is 30.
productsApi.getBeersList(0, 30)
Then we return a page object with those 30 elements and a nextPage key of 1.
If we imagine all our objects as list, the first page would be the asterisks in this span: [***-------]
Let's get the next page:
productsApi.getBeersList(1, 10)
That returns elements that are this from our span: [-*--------]
And then you get: [--*-------]
So the 0th page is fine, but the 1th and 2nd page overlap with it. Then, pages 3 and onward contain new elements.
Because you're getting the keys for the next and previous pages by adding or subtracting to get the adjacent keys to the current page of the current length, the index can't scale with the page size. This isn't always easy without knowing what is inside the PagingConfig.
However you can make it work with dynamic page sizes if you can guarantee your initial load size is your regular page size times some integer and that you'll always get the requested number of items except for the last page, you could store an offset as your next/previous page keys, and turn that offset into the next page at load like:
/* Value for api pageNo */
val pageNo = (params.key/params.loadSize + STARTING_PAGE_INDEX) ?: STARTING_PAGE_INDEX
productsApi.getBeersList(pageNo, params.loadSize) // and so on
/* for offset keys in PageData */
nextKey = (pageNo + 1) * loadSize
prevKey = (pageNo - 1) * loadSize // Integer division rounds down so larger windows start where they should
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.
I am trying to return a list from inside firestore function based on if a condition is true.I want to return different lists when different categories are selected.
I tried:
putting the return statement out of firestore function which did not work and returned empty list due to firestore async behaviour.
creating my own callback to wait for Firestore to return the data using interface as I saw in some other questions but in that case how am i supposed to access it as my function has a Int value(i.e.private fun getRandomPeople(num: Int): List<String>)?
What could be the way of returning different lists for different categories based on firestore conditions?
My code(Non Activity class):
class Board// Create new game
(private val context: Context, private val board: GridLayout) {
fun newBoard(size: Int) {
val squares = size * size
val people = getRandomPeople(squares)
createBoard(context, board, size, people)
}
fun createBoard(context: Context, board: GridLayout, size: Int, people: List<String>) {
destroyBoard()
board.columnCount = size
board.rowCount = size
var iterator = 0
for(col in 1..size) {
for (row in 1..size) {
cell = RelativeLayout(context)
val cellSpec = { GridLayout.spec(GridLayout.UNDEFINED, GridLayout.FILL, 1f) }
val params = GridLayout.LayoutParams(cellSpec(), cellSpec())
params.width = 0
cell.layoutParams = params
cell.setBackgroundResource(R.drawable.bordered_rectangle)
cell.gravity = Gravity.CENTER
cell.setPadding(5, 0, 5, 0)
text = TextView(context)
text.text = people[iterator++]
words.add(text.text as String)
text.maxLines = 5
text.setSingleLine(false)
text.gravity = Gravity.CENTER
text.setTextColor(0xFF000000.toInt())
cell.addView(text)
board.addView(cell)
cells.add(GameCell(cell, text, false, row, col) { })
}
}
}
private fun getRandomPeople(num: Int): List<String> {
val mFirestore: FirebaseFirestore=FirebaseFirestore.getInstance()
val mAuth: FirebaseAuth=FirebaseAuth.getInstance()
val currentUser: FirebaseUser=mAuth.currentUser!!
var validIndexes :MutableList<Int>
var chosenIndexes = mutableListOf<Int>()
var randomPeople = mutableListOf<String>()
mFirestore.collection("Names").document(gName).get().addOnSuccessListener(OnSuccessListener<DocumentSnapshot>(){ queryDocumentSnapshot->
var categorySelected:String=""
if (queryDocumentSnapshot.exists()) {
categorySelected= queryDocumentSnapshot.getString("selectedCategory")!!
print("categoryselected is:$categorySelected")
Toast.makeText(context, "Got sel category from gameroom:$categorySelected", Toast.LENGTH_LONG).show()
when(categorySelected){
"CardWords"->{
for (i in 1..num) {
validIndexes=(0..CardWords.squares.lastIndex).toMutableList()
val validIndexIndex = (0..validIndexes.lastIndex).random()
val peopleIndex = validIndexes[validIndexIndex]
chosenIndexes.add(peopleIndex)
val person = CardWords.squares[peopleIndex]
randomPeople.add(person)
validIndexes.remove(peopleIndex)
peopleIndexes = chosenIndexes.toList()
}
}
else->{}
}
}
else {
Toast.makeText(context, "Sel category does not exist", Toast.LENGTH_LONG).show()
}
}).addOnFailureListener(OnFailureListener { e->
val error=e.message
Toast.makeText(context,"Error:"+error, Toast.LENGTH_LONG).show()
})
return randomPeople.toList()
}
}
Activity A:
Board(this, gridLay!!)