Android как взаимодействовать с вложенным реселлером Посмотреть из фрагмента - PullRequest
0 голосов
/ 03 июля 2019

Как правильно отправить данные ребенку adapter в fragment? Я в основном пытаюсь реализовать Instagram, например, раздел комментариев, например куча комментариев, каждый из которых может иметь больше комментариев (ответов).

Для этого я использую один main recyclerView + main adapter, экземпляры которого сохраняются в моем фрагменте, а в главном адаптере я связываю комментарии детей (recyclerView + adapter).

Добавление комментариев к основному адаптеру легко, поскольку объект всегда доступен во фрагменте, поэтому я просто вызываю 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)
}

Но как назвать addComments одного конкретного nested-rV? Я прочитал, я не должен сохранять экземпляры адаптера и использовать только позиции. Я пытаюсь сделать это в своем фрагменте следующим образом:

val item = rVComments.findViewHolderForItemId(mAdapter.itemId)!!.itemView
val adapt = item.rVReplies.adapter as ChildCommentsAdapter
adapt.addComment(it.data.comment)

Но это не очень хорошо работает: поскольку у нас есть только RecyclerViews, этот конкретный ViewHolder часто уже перерабатывается, если пользователь прокручивает после публикации или выборки элементов, что приводит к исключению NullPointerException. Отсюда возникает первый вопрос: как правильно взаимодействовать с вложенными просмотрщиками и их адаптерами? Если ответ через интерфейс, приведите пример, поскольку я пытался безуспешно, так как не должен сохранять объекты адаптера .

Ответы [ 2 ]

1 голос
/ 04 июля 2019

Этого можно добиться с помощью одного адаптера типа multi-view, разместив комментарии как часть родительского элемента, вы добавляете дочерние элементы под родительский элемент и вызываете notifyItemRangeInserted.

Таким образом, вам не придется иметь дело с большинством проблем утилизации.

Когда вы хотите обновить комментарий, вы просто обновляете комментарий внутри родительского элемента и вызываете notifyItemChanged.

Если вы хотите, я создал библиотеку , которая может генерировать этот код для вас во время компиляции. Он поддерживает именно тот случай, который вы хотели, и многое другое.

0 голосов
/ 04 июля 2019

Используя предложение @Gil Goldzweig, вот что я сделал: в случае системы комментариев, подобной Instagram, с ответами я использовал вложенную систему recyclerView.Это просто облегчает добавление и удаление элементов.Однако, что касается вопроса Как правильно отправлять данные дочернему адаптеру во фрагменте? Нет.Это становится супер грязным.Из моего фрагмента я отправил данные в свой mainAdapter, который, в свою очередь, отправил данные в соответствующий childAdapter.Ключом к его плавности является использование notifyItemRangeInserted при добавлении комментария в mainAdapter, а затем notifyItemChanged при добавлении ответов на комментарий.Второе событие позволит отправить данные на дочерний адаптер с помощью payload.Вот код на случай, если другие люди заинтересуются:

Фрагмент

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)
}



}

ChildAdapterочень простой и имеет почти 0 логики.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...