In the following viewModel code I am generating a list of items from graphQl server
private val _balloonsStatus =
MutableStateFlow<Status<List<BalloonsQuery.Edge>?>>(Status.Loading())
val balloonsStatus get() = _balloonsStatus
private val _endCursor = MutableStateFlow<String?>(null)
val endCursor get() = _endCursor
init {
loadBalloons(null)
}
fun loadBalloons(cursor: String?) {
viewModelScope.launch {
val data = repo.getBalloonsFromServer(cursor)
if (data.errors == null) {
_balloonsStatus.value = Status.Success(data.data?.balloons?.edges)
_endCursor.value = data.data?.balloons?.pageInfo?.endCursor
} else {
_balloonsStatus.value = Status.Error(data.errors!![0].message)
_endCursor.value = null
}
}
}
and in the composable function I am getting this data by following this code:
#Composable
fun BalloonsScreen(
navHostController: NavHostController? = null,
viewModel: SharedBalloonViewModel
) {
val endCursor by viewModel.endCursor.collectAsState()
val balloons by viewModel.balloonsStatus.collectAsState()
AssignmentTheme {
Column(modifier = Modifier.fillMaxSize()) {
when (balloons) {
is Status.Error -> {
Log.i("Reyjohn", balloons.message!!)
}
is Status.Loading -> {
Log.i("Reyjohn", "loading..")
}
is Status.Success -> {
BalloonList(edgeList = balloons.data!!, navHostController = navHostController)
}
}
Spacer(modifier = Modifier.weight(1f))
Button(onClick = { viewModel.loadBalloons(endCursor) }) {
Text(text = "Load More")
}
}
}
}
#Composable
fun BalloonList(
edgeList: List<BalloonsQuery.Edge>,
navHostController: NavHostController? = null,
) {
LazyColumn {
items(items = edgeList) { edge ->
UserRow(edge.node, navHostController)
}
}
}
but the problem is every time I click on Load More button it regenerates the view and displays a new set of list, but I want to append the list at the end of the previous list. As far I understand that the list is regenerated as the flow I am listening to is doing the work behind this, but I am stuck here to get a workaround about how to achieve my target here, a kind hearted help would be much appreciated!
You can create a private list in ViewModel that adds List<BalloonsQuery.Edge>?>
and instead of
_balloonsStatus.value = Status.Success(data.data?.balloons?.edges)
you can do something like
_balloonsStatus.value = Status.Success(myLiast.addAll(
data.data?.balloons?.edges))
should update Compose with the latest data appended to existing one
I have LazyColumn with swipe-to-delete. When I swipe an item, it is deleted by viewModel. The problem is that if I swipe the item away, the LazyColumn doesn't update the position of other items (as shown in GIF).
Here's my code implementation:
#ExperimentalMaterialApi
#Composable
fun Screen() {
val livedata = viewModel.itemsLiveData.observeAsState()
val stateList = remember { mutableStateListOf<Data>() }
stateList.addAll(livedata.value!!)
SwipableLazyColumn(stateList)
}
#ExperimentalMaterialApi
#Composable
fun SwipableLazyColumn(
stateList: SnapshotStateList<Data>
) {
LazyColumn {
items(items = stateList) { item ->
val dismissState = rememberDismissState()
if (dismissState.isDismissed(EndToStart) || dismissState.isDismissed(StartToEnd)) {
viewModel.swipeToDelete(item)
}
SwipeToDismiss(
state = dismissState,
directions = setOf(StartToEnd, EndToStart),
dismissThresholds = {
FractionalThreshold(0.25f)
},
background = {},
dismissContent = {
MyData(item)
}
)
}
}
}
I use SnapshotStateList as it's suggested here. Although I don't use swapList because it clears out all items
ViewModel:
class MyViewModel #Inject internal constructor(
private val itemRepository: ItemRepository
) : BaseViewModel(), LifecycleObserver {
private val itemsList = mutableListOf<MyData>()
private val _itemsLiveData = MutableLiveData<List<MyData>>()
val itemsLiveData: LiveData<List<MyData>> = _itemsLiveData
init {
loadItems()
}
private fun loadItems() {
viewModelScope.launch {
itemRepository.getItems().collect {
when (it) {
is Result.Success -> onItemsLoaded(it.data)
is Result.Error -> {
onItemsLoaded(emptyList())
}
}
}
}
}
private fun onItemsLoaded(itemsList: List<MyData>) {
itemsList.clear()
itemsList.addAll(notifications)
_itemsLiveData.value = if (itemsList.isNotEmpty()) {
itemsList
} else {
null
}
}
fun swipeToDelete(item: MyData) {
if (itemsList.size == 0) return
viewModelScope.launch {
when (
val result =
itemRepository.deletelItem(item)
) {
is Result.Success -> {
onItemDeleted(item)
}
is Result.Error -> {
showSnackBar(
"error"
)
}
}
}
}
private fun onItemDeleted(item: MyData) {
itemsList.remove(item)
_itemsLiveData.value = itemsList
}
}
You should refresh your list inside viemodel(delete item) and return modified list right here
var tempList = itemList
ItemList.clear()
ItemList.addAll(tempList)
You need to provide key for the LazyColumn's items.
By default, each item's state is keyed against the position of the
item in the list. However, this can cause issues if the data set
changes, since items which change position effectively lose any
remembered state.
Example
LazyColumn {
items(
items = stateList,
key = { _, listItem ->
listItem.hashCode()
},
) { item ->
// As it is ...
}
}
Reference
inside an activity I have an observer I am calling a function to setup adapter. After that I am setting list to my adapter. And doing How to know when the RecyclerView has finished laying down the items?. And setting layout manager on that. But it's not working.
activity.kt
class activity : BaseActivity() {
private var cAdapter: cdAdapter? = null
fun setupObserver(){
viewModel.list.observe(this, { value ->
if (cAdapter == null) {
setupAdapter()
}
submitList(value)
binding.cRecyclerView.afterMeasured {
layoutManager = LinearLayoutManager(this.context).apply {
direction(this)
}
}
})
}
private fun setupAdapter() {
cAdapter = cdAdapter()
binding.cRecyclerView.adapter = cAdapter
}
private fun direction(linearLayoutManager: LinearLayoutManager) {
linearLayoutManager.apply {
// if (binding.cRecyclerView.canScrollVertically(1)) {
// logE("Yes can scroll")
//} else {
// logE("No scroll")
// }
if (viewModel.itemsAreMorethanTwo()) {
stackFromEnd = true
reverseLayout = true
val index = viewModel.findLastBiggerValue()
if (index != null && index >= 0) {
scrollToPosition(index)
}
} else {
stackFromEnd = false
reverseLayout = true
}
}
}
inline fun <T : View> T.afterMeasured(crossinline f: T.() -> Unit) {
viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener {
override fun onGlobalLayout() {
if (measuredWidth > 0 && measuredHeight > 0) {
viewTreeObserver.removeOnGlobalLayoutListener(this)
f()
}
}
})
}
}
Basically inside direction function I am checking that if item has more than two value, i'm using reverse layout as well as stackFromEnd as true. As you see inside direction function some lines are commented and I need to check that my items are less than the whole height, i.e. I don't want to scroll up or down and the whole items consist in a single screen
For the commented section, I want to know if the view is scrollable or not.
the code below is working and above code is not.
If I move this code from observer
binding.cRecyclerView.afterMeasured {
layoutManager = LinearLayoutManager(this.context).apply {
direction(this)
}
}
inside like this
private fun setupAdapter() {
cAdapter = cdAdapter()
binding.cRecyclerView.apply {
adapter = cAdapter
layoutManager = LinearLayoutManager(this.context).apply {
setLayoutDirection(this)
}
}
}
When I move this piece of code in setupAdapter the commented section does not work though it sets the layout manager correctly.
What I am doing wrong in above code? Please suggest me something.
I have an app that launches the majority of the time, but every 7 or so launches it crashes with the error:
kotlin.UninitializedPropertyAccessException: lateinit property weekdayList has not been initialized
This is a clear error, I'm just not sure how to ensure the variable is initialized early enough in the context of my app.
Things I have tried
I tried moving variables around, making "inner" and "outer"
variables, one within onCreate and an underscore led variable as
the class variable.
Changing the viewmodel so that it will wait until the call to the db has finished (I
couldn't make this work, but mostly because I wasn't sure how to do it).
I think the problem is in the onCreate function, and that the weekday observe isn't setting the value of the variable faster than the task observe (where the weekdayList variable is needed) is called?
Edit 1
I referenced this but I end up with a similar error
java.lang.IndexOutOfBoundsException: Empty list doesn't contain element at index 1.
Edit 2
I understand how lateinit variables and nullables work at this point, I want to try and clarify this a little better.
The variable weekdayList needs to be initialized to the correct list before I hit the observe for the taskList, otherwise the app will crash.
I have tried setting the variable to be nullable, and Iend up with:
skipping parts of the program when it's null (not an option)
crashing with a null pointer exception (if set to non-nullable)
no tasks get assigned to any day, which means no recyclerviews get updated, thus making the app appear to contain no tasks when it does.
weekday buttons that don't work because there is no weekdayList for them to compare against to launch the next activity
My problem doesn't stand in figuring out if it's null or not, it's trying to guarantee that it won't be null.
Sorry for the confusion
Main Activity
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private val plannerViewModel: PlannerViewModel by viewModels {
PlannerViewModelFactory((application as PlannerApplication).repository)
}
private var weekdayList: List<Weekday> = listOf()
private var taskList: List<Task> = listOf()
private var taskDayList = mutableListOf<Task>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
val clearButtonText = binding.clearCardText
val sundayButtonText = binding.sundayCardText
val mondayButtonText = binding.mondayCardText
val tuesdayButtonText = binding.tuesdayCardText
val wednesdayButtonText = binding.wednesdayCardText
val thursdayButtonText = binding.thursdayCardText
val fridayButtonText = binding.fridayCardText
val saturdayButtonText = binding.saturdayCardText
val sundayRv: RecyclerView = binding.sundayRv
val sundayAdapter = TaskRvAdapter(null)
sundayRv.adapter = sundayAdapter
sundayRv.layoutManager = LinearLayoutManager(this)
val mondayRv: RecyclerView = binding.mondayRv
val mondayAdapter = TaskRvAdapter(null)
mondayRv.adapter = mondayAdapter
mondayRv.layoutManager = LinearLayoutManager(this)
val tuesdayRv: RecyclerView = binding.tuesdayRv
val tuesdayAdapter = TaskRvAdapter(null)
tuesdayRv.adapter = tuesdayAdapter
tuesdayRv.layoutManager = LinearLayoutManager(this)
val wednesdayRv: RecyclerView = binding.wednesdayRv
val wednesdayAdapter = TaskRvAdapter(null)
wednesdayRv.adapter = wednesdayAdapter
wednesdayRv.layoutManager = LinearLayoutManager(this)
val thursdayRv: RecyclerView = binding.thursdayRv
val thursdayAdapter = TaskRvAdapter(null)
thursdayRv.adapter = thursdayAdapter
thursdayRv.layoutManager = LinearLayoutManager(this)
val fridayRv: RecyclerView = binding.fridayRv
val fridayAdapter = TaskRvAdapter(null)
fridayRv.adapter = fridayAdapter
fridayRv.layoutManager = LinearLayoutManager(this)
val saturdayRv: RecyclerView = binding.saturdayRv
val saturdayAdapter = TaskRvAdapter(null)
saturdayRv.adapter = saturdayAdapter
saturdayRv.layoutManager = LinearLayoutManager(this)
// Setting day card names
clearButtonText.text = "Clear"
sundayButtonText.text = "Sun"
mondayButtonText.text = "Mon"
tuesdayButtonText.text = "Tue"
wednesdayButtonText.text = "Wed"
thursdayButtonText.text = "Thu"
fridayButtonText.text = "Fri"
saturdayButtonText.text = "Sat"
sundayButtonText.text = "Sun"
plannerViewModel.allWeekdays.observe(this, {
weekdayList = it
})
plannerViewModel.allTasks.observe(this, { tasks ->
taskList = tasks
taskDayList = mutableListOf()
for (i in 1..7) {
taskDayList = sortTasks(weekdayList[i], taskList)
when (i) {
1 -> {
sundayAdapter.submitList(taskDayList)
toggleVisibility(taskDayList, binding.sundayInner,
binding.sundayCardText, sundayRv, binding.sundayNoTasks)
}
2 -> {
mondayAdapter.submitList(taskDayList)
toggleVisibility(taskDayList, binding.mondayInner,
binding.mondayCardText, mondayRv, binding.mondayNoTasks)
}
3 -> {
tuesdayAdapter.submitList(taskDayList)
toggleVisibility(taskDayList, binding.tuesdayInner,
binding.tuesdayCardText, tuesdayRv, binding.tuesdayNoTasks)
}
4 -> {
wednesdayAdapter.submitList(taskDayList)
toggleVisibility(taskDayList, binding.wednesdayInner,
binding.wednesdayCardText, wednesdayRv, binding.wednesdayNoTasks)
}
5 -> {
thursdayAdapter.submitList(taskDayList)
toggleVisibility(taskDayList, binding.thursdayInner,
binding.thursdayCardText, thursdayRv, binding.thursdayNoTasks)
}
6 -> {
fridayAdapter.submitList(taskDayList)
toggleVisibility(taskDayList, binding.fridayInner,
binding.fridayCardText, fridayRv, binding.fridayNoTasks)
}
7 -> {
saturdayAdapter.submitList(taskDayList)
toggleVisibility(taskDayList, binding.saturdayInner,
binding.saturdayCardText, saturdayRv, binding.saturdayNoTasks)
}
}
}
})
}
private fun toggleVisibility(taskDayList: List<Task>, inner: ConstraintLayout,
cardText: View, rv: RecyclerView, noTask: View) {
if (taskDayList.count() == 0 ) {
val newConstraintSet = ConstraintSet()
newConstraintSet.clone(inner)
newConstraintSet.connect(noTask.id, ConstraintSet.TOP,
cardText.id, ConstraintSet.BOTTOM)
newConstraintSet.applyTo(inner)
newConstraintSet.connect(cardText.id, ConstraintSet.BOTTOM,
noTask.id, ConstraintSet.TOP)
newConstraintSet.applyTo(inner)
rv.visibility = View.GONE
noTask.visibility = View.VISIBLE
Log.i("this", "ran zero")
} else {
val newConstraintSet = ConstraintSet()
newConstraintSet.clone(inner)
newConstraintSet.connect(rv.id, ConstraintSet.TOP,
cardText.id, ConstraintSet.BOTTOM)
newConstraintSet.applyTo(inner)
newConstraintSet.connect(cardText.id, ConstraintSet.BOTTOM,
rv.id, ConstraintSet.TOP)
newConstraintSet.applyTo(inner)
rv.visibility = View.VISIBLE
noTask.visibility = View.GONE
Log.i("this", "ran else")
}
}
private fun sortTasks(day: Weekday, tasks: List<Task>): MutableList<Task> {
val newAdapterList = mutableListOf<Task>()
tasks.forEach {
if (it.weekdayId == day.id) {
newAdapterList.add(it)
}
}
return newAdapterList
}
private fun startWeekdayActivity(day: Weekday) {
val intent = Intent(this, WeekdayActivity::class.java)
intent.putExtra("dayId", day.id)
this.startActivity(intent)
}
private fun clearDb(taskList: List<Task>) {
val alertDialog: AlertDialog = this.let { outerIt ->
val builder = AlertDialog.Builder(outerIt)
builder.apply {
setPositiveButton("Clear",
DialogInterface.OnClickListener { dialog, id ->
if (taskList.count() == 0) {
Toast.makeText(context, "No tasks to clear", Toast.LENGTH_SHORT).show()
} else {
plannerViewModel.deleteAllTasks()
Toast.makeText(context, "Tasks cleared", Toast.LENGTH_SHORT).show()
}
})
setNegativeButton("Cancel",
DialogInterface.OnClickListener { dialog, id ->
// User cancelled the dialog
})
}
.setTitle("Clear tasks?")
.setMessage("Are you sure you want to clear the weeks tasks?")
builder.create()
}
alertDialog.show()
}
private fun checkDay(dayIn: String, weekdayList: List<Weekday>) {
weekdayList.forEach {
if (dayIn == "clear_card" && it.day == "Clear") {
clearDb(taskList)
} else {
val dayInAbr = dayIn.substring(0, 3).toLowerCase(Locale.ROOT)
val dayOutAbr = it.day.substring(0, 3).toLowerCase(Locale.ROOT)
if (dayInAbr == dayOutAbr) {
startWeekdayActivity(it)
}
}
}
}
fun buttonClick(view: View) {
when (view.id) {
R.id.clear_card -> checkDay(view.context.resources.getResourceEntryName(R.id.clear_card).toString(), weekdayList)
R.id.sunday_card -> checkDay(view.context.resources.getResourceEntryName(R.id.sunday_card).toString(), weekdayList)
R.id.monday_card -> checkDay(view.context.resources.getResourceEntryName(R.id.monday_card).toString(), weekdayList)
R.id.tuesday_card -> checkDay(view.context.resources.getResourceEntryName(R.id.tuesday_card).toString(), weekdayList)
R.id.wednesday_card -> checkDay(view.context.resources.getResourceEntryName(R.id.wednesday_card).toString(), weekdayList)
R.id.thursday_card -> checkDay(view.context.resources.getResourceEntryName(R.id.thursday_card).toString(), weekdayList)
R.id.friday_card -> checkDay(view.context.resources.getResourceEntryName(R.id.friday_card).toString(), weekdayList)
R.id.saturday_card -> checkDay(view.context.resources.getResourceEntryName(R.id.saturday_card).toString(), weekdayList)
}
}
}
Viewmodel
class PlannerViewModel(private val repository: DbRepository) : ViewModel() {
val allWeekdays: LiveData<List<Weekday>> = repository.allWeekdays.asLiveData()
val allTasks: LiveData<List<Task>> = repository.allTasks.asLiveData()
fun insertWeekday(weekday: Weekday) = viewModelScope.launch {
repository.insertWeekday(weekday)
}
fun insertTask(task: Task) = viewModelScope.launch {
repository.insertTask(task)
}
fun deleteTask(task: Task) = viewModelScope.launch {
repository.deleteTask(task)
}
fun deleteAllTasks() = viewModelScope.launch {
repository.deleteAllTasks()
}
}
class PlannerViewModelFactory(private val repository: DbRepository) : ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(PlannerViewModel::class.java)) {
#Suppress("UNCHECKED_CAST")
return PlannerViewModel(repository) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
}
A variable declared as lateinit just means that you are sure that when the object is dereferenced it will not be null. In your case you are calling a method from weekdayList object before it is assigned a value. It is important to understand the concept clearly and why your code works.
Happy Coding!
You can use the "isInitialized" method, for checking "lateinit" variable is initialized or not.
Please refer the below article for the this-
https://blog.mindorks.com/how-to-check-if-a-lateinit-variable-has-been-initialized
lateinit is a way for you to have a var without an initial value when you declare it. It's a nice way to avoid taking something that will never be null, and making it nullable (and having to null check it forever) just so you can temporarily set it to null as a placeholder that nothing will ever see.
What you're doing is promising the compiler "ok, I'm not going to provide a value when the class is constructed, but I promise I'll set it to something before anything tries to read it". You're telling the compiler to trust you, that you know how your code works, and you can guarantee it'll all be ok.
Your problem is that it seems you can't guarantee that things won't try to read that property before you write to it. Your state can either be "has a value", or "doesn't have a value", and the rest of your code could encounter either state.
The "no value" state is basically null, so you should probably make the variable nullable instead, and initialise it as null. Kotlin has all that nice null-safety stuff to help your code handle it, until you do get a value. lateinit seems like the wrong tool for the job, even if you check ::isInitialized it's just making your life a lot harder when the null-checking stuff is right there!
Use lazy properties , refer to this doc for more informations:
assuming weekDayList is the property you want to successfully initialize ->
private var weekDayList: List<WeekDay> by lazy {
//return your first value
listOf<WeekDay>()
}
Here is a useful link about LifeCycleAware Lazy properties: blog Although, it is not required.
A solution with help from cactustictacs in the comments.
I moved a lot of the list dependency to a new function called setAdapterList. this allows both observes to run the function, and only the one with both lists initialized will run the code contained. I kept the variables lateinit and it seems to be working so far!
The Main Change in Main Activity
...
private fun setAdapterLists(adapterList: List<TaskRvAdapter>, rvList: List<RecyclerView>) {
if (this::weekdayList.isInitialized && this::taskList.isInitialized) {
adapterList.forEach {
taskDayList = mutableListOf()
val i = adapterList.indexOf(it)
taskDayList = sortTasks(weekdayList[i + 1], taskList)
Log.i("rvli", rvList[i].toString())
when (i) {
0 -> {
adapterList[i].submitList(taskDayList)
toggleVisibility(taskDayList, binding.sundayInner,
binding.sundayCardText, rvList[i], binding.sundayNoTasks)
}
1 -> {
adapterList[i].submitList(taskDayList)
toggleVisibility(taskDayList, binding.mondayInner,
binding.mondayCardText, rvList[i], binding.mondayNoTasks)
}
2 -> {
adapterList[i].submitList(taskDayList)
toggleVisibility(taskDayList, binding.tuesdayInner,
binding.tuesdayCardText, rvList[i], binding.tuesdayNoTasks)
}
3 -> {
adapterList[i].submitList(taskDayList)
toggleVisibility(taskDayList, binding.wednesdayInner,
binding.wednesdayCardText, rvList[i], binding.wednesdayNoTasks)
}
4 -> {
adapterList[i].submitList(taskDayList)
toggleVisibility(taskDayList, binding.thursdayInner,
binding.thursdayCardText, rvList[i], binding.thursdayNoTasks)
}
5 -> {
adapterList[i].submitList(taskDayList)
toggleVisibility(taskDayList, binding.fridayInner,
binding.fridayCardText, rvList[i], binding.fridayNoTasks)
}
6 -> {
adapterList[i].submitList(taskDayList)
toggleVisibility(taskDayList, binding.saturdayInner,
binding.saturdayCardText, rvList[i], binding.saturdayNoTasks)
}
}
}
}
}
...
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.