簡體   English   中英

Android RecyclerView 項目列表在更改設置並再次導航到片段后未正確更改

[英]Android RecyclerView items list doesn't change appropriately after chang settings and navigate again to fragment

我有一個活動 android 應用程序,一個具有任務管理的服務應用程序。 對於導航,我使用帶有底部導航的導航組件。 我還使用數據綁定和 Dagger2 DI,如果它對問題調查很重要的話。

用戶成功登錄后,會出現帶有隊列列表(水平回收視圖)的主屏幕。

主頁片段:-

每個隊列(recyclerview 項目)都有適當的任務列表可供用戶執行。

您可以 select 任何活動的隊列項目(其中至少包含一個任務)。 所選隊列的任務顯示在 recyclerview 下方,就像另一個垂直 recyclerview。 還有一個摘要隊列項(圖片上最左邊的項)計算並顯示所有可用隊列中的所有任務。

此摘要隊列項目的外觀取決於配置文件屏幕上的開關,表示為配置文件片段

簡介片段:-

設想:

  1. 默認情況下在主屏幕上顯示摘要隊列項目;
  2. 我導航到配置文件屏幕並關閉切換器。 在 ProfileFragment 中,我在視圖 model 中調用方法 updateGeneralQueueState,它保存在房間 db 參數 isShouldBeShown(在本例中為 false);
  3. 我導航回主屏幕。 在這里,我通過在視圖 model 中調用適當的方法來檢索我的 Home Fragment 中的 isShouldBeShown 參數,該方法從房間數據庫返回先前保存的參數。

問題:我希望看到摘要隊列項目不在隊列列表中,而且大多數情況下它在隊列列表中,但有時當我重復這個場景時它不在。 如果不是我 go 到配置文件片段或任何其他屏幕,然后 go 再次回到主屏幕,然后摘要隊列項目不在預期的列表中。

可能存在一些架構錯誤,這就是為什么我尋求真正的幫助並解釋問題發生的原因,因為我不僅希望解決它,而且還希望了解這種奇怪的行為。 我將在下面附上所有相關代碼! 提前謝謝了!

HomeFragment.kt

class HomeFragment : BaseFragment<HomeFragmentBinding>(), MenuItem.OnActionExpandListener {

    @Inject lateinit var factory: HomeViewModelFactory
    @Inject lateinit var viewModel: HomeViewModel
    private lateinit var ticketsListAdapter: TicketsListAdapter
    private lateinit var queuesListAdapter: QueuesListAdapter
    private var searchView: SearchView? = null
    private var pageLimit: Long = 10
    private var offset: Long = 0L
    private var selectedQueueId: Long = 0L
    private var selectedQueueIndex: Int = 0
    private var prevTicketsThreshold: Int = 0 // new
    private var ticketsThreshold: Int = 0
    private var lockId: Int = 1
    private var allQueueIds: List<Long> = listOf()
    private var isGeneralShoudlBeShown: Boolean = false
    private var favoriteMode: Boolean = false
    private lateinit var prefs: Prefs
    private var selectedQueue: Queue? = null

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        ComponentsHolder.getComponent().inject(this)
        super.onViewCreated(view, savedInstanceState)

        prefs = Prefs(requireContext())

        (activity as MainActivity).showBottomNavigation()
        (activity as MainActivity).getUnreadNotificationsCount()

        val toolbar = view.findViewById(R.id.tickets_search_toolbar) as Toolbar
        (activity as MainActivity).setSupportActionBar(toolbar)
        toolbar.title = "Главная"
        setHasOptionsMenu(true)

        viewModel = ViewModelProvider(this, factory)[HomeViewModel::class.java]
        binding.model = viewModel
        binding.lifecycleOwner = this

        with(viewModel) {
            (activity as MainActivity).getPushToken { t ->
                registerPushToken(t)
                getUserSettings()
                getUnreadNotificationsCount()
            }

            notificationscount.observe(viewLifecycleOwner) {
                it?.let {
                    if (it.unreadCount > 0) {
                        (activity as MainActivity).setUnreadNotificationsCount(it.unreadCount)
                            .also { (activity as MainActivity).getUnreadNotificationsCount() }
                    }
                }
            }

            checkUserSettings.observe(viewLifecycleOwner) {
                isGeneralShoudlBeShown = it.isGeneralChecked
                favoriteMode = it.isFavoritesChecked!!
                getQueues(isGeneralShoudlBeShown, favoriteMode, selectedQueueIndex)
            }

            queueIds.observe(viewLifecycleOwner) {
                it?.let {
                    allQueueIds = it
                }
            }

            queues.observe(viewLifecycleOwner) {
                it?.let {
                    when (it.responseCode) {
                        200 -> {
                            queuesListAdapter.submitList(it.queues)
                            queuesListAdapter.notifyDataSetChanged()
                            retrieveSelectedQueue(it.queues)
                            getTickets(
                                if (selectedQueueId == 0L) 0 else selectedQueueId,
                                if (selectedQueueId == 0L) allQueueIds else emptyList(),
                                lockId,
                                pageLimit,
                                offset
                            )
                        }
                    }
                }
            }

            tickets.observe(viewLifecycleOwner) {
                it?.let {
                    binding.refreshDate.text = getLastRefreshDateTime()
                    Log.i("hmfrgmnt", it.toString())
                    when (it.responseCode) {
                        401 -> {
                            binding.bottomProgress.visibility = View.GONE
                            if (mayNavigate()) {
                                findNavController().navigate(
                                    HomeFragmentDirections
                                        .actionHomeFragmentToSplashFragment()
                                )
                            }
                        }
                        200 -> {
                            binding.bottomProgress.visibility = View.GONE
                            ticketsListAdapter.submitList(null)
                            ticketsListAdapter.notifyDataSetChanged()
                        }
                        else -> (activity as MainActivity).showErrorDialog(
                            it.responseMessage!!,
                            null
                        )
                    }
                }
            }

            navigateToTicketDetails.observe(viewLifecycleOwner) { ticketId ->
                ticketId?.let {
                    if (mayNavigate()) {
                        findNavController().navigate(
                            HomeFragmentDirections
                                .actionHomeFragmentToTicketDetailsFragment(ticketId)
                        )
                    }
                    viewModel.onTicketDetailsNavigated()
                }
            }
        }

        with(binding) {
            tabs.selectTab(tabs.getTabAt((lockId - 1)), true)
            (queueList.layoutManager as LinearLayoutManager).scrollToPositionWithOffset(selectedQueueIndex, queueList.top)

            ticketsListAdapter = TicketsListAdapter(TicketsListListener { ticketId ->
                viewModel.onTicketDetailsClicked(ticketId)
            })

            queuesListAdapter = QueuesListAdapter(
                QueuesListListener { queue ->
                    setActiveQueueData(queue)
                    tabs.selectTab(tabs.getTabAt((lockId - 1)), true)

                    viewModel.onQueueClicked(if (queue.queueId == 0L) 0 else selectedQueueId, if (queue.queueId == 0L) allQueueIds else emptyList(), lockId, pageLimit, offset)
//                    ticketsListAdapter.notifyDataSetChanged()
                }
            )

            ticketsList.adapter = ticketsListAdapter
            queueList.adapter = queuesListAdapter

            tabs.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
                override fun onTabSelected(tab: TabLayout.Tab?) {
                    when (tab?.position) {
                        1 -> {
                            offset = 0
                            lockId = 2
                            viewModel.onQueueClicked(if (selectedQueueId == 0L) 0 else selectedQueueId, if (selectedQueueId == 0L) allQueueIds else emptyList(), lockId, pageLimit, offset)
                        }
                        else -> {
                            offset = 0
                            lockId = 1
                            viewModel.onQueueClicked(if (selectedQueueId == 0L) 0 else selectedQueueId, if (selectedQueueId == 0L) allQueueIds else emptyList(), lockId, pageLimit, offset)
                        }
                    }
                }
                override fun onTabUnselected(tab: TabLayout.Tab?) {}
                override fun onTabReselected(tab: TabLayout.Tab?) {}
            })

            nestedScroll.setOnScrollChangeListener { v, _, scrollY, _, _ ->
                if ((scrollY > (v as NestedScrollView).getChildAt(0).measuredHeight - v.measuredHeight - homeMainLayout.paddingBottom) && viewModel.status.value != ApiStatus.LOADING) {
                    if (ticketsThreshold > prevTicketsThreshold) {
                        if (ticketsThreshold < pageLimit || ticketsThreshold == 0) {
                            moreButton.visibility = View.GONE
                            endOfListView.visibility = View.VISIBLE
                        } else {
                            moreButton.visibility = View.VISIBLE
                            endOfListView.visibility = View.GONE
                        }

                    } else if (ticketsThreshold == prevTicketsThreshold) {
                        moreButton.visibility = View.GONE
                        endOfListView.visibility = View.VISIBLE
                    } else {
                        moreButton.visibility = View.VISIBLE
                        endOfListView.visibility = View.GONE
                    }
                }
            }

            refreshButton.setOnClickListener {
                offset = 0
                viewModel.refresh(isGeneralShoudlBeShown, favoriteMode, selectedQueueIndex, selectedQueueId, allQueueIds, lockId, pageLimit, offset)
                (queueList.layoutManager as LinearLayoutManager).scrollToPositionWithOffset(selectedQueueIndex, queueList.top)
                tabs.selectTab(tabs.getTabAt((lockId - 1)), true)
                queuesListAdapter.notifyDataSetChanged()
            }

            moreButton.setOnClickListener {
                prevTicketsThreshold = ticketsThreshold
                offset += pageLimit
                viewModel.getTickets(
                    if (selectedQueueId == 0L) 0 else selectedQueueId,
                    if (selectedQueueId == 0L) allQueueIds else emptyList(),
                    lockId,
                    pageLimit,
                    offset
                )
            }
        }
    }

    override fun getFragmentBinding(
        inflater: LayoutInflater,
        container: ViewGroup?
    ) = HomeFragmentBinding.inflate(inflater, container, false)

    private fun setActiveQueueData(queue: Queue) {
        offset = 0
        selectedQueue = queue
        prefs.queueObject = queue
        binding.selectedQueueTitle.text = queue.title
        selectedQueueIndex = queuesListAdapter.currentList.getQueuePosition(selectedQueue as Queue) ?: 0
        queuesListAdapter.currentList.forEach { i -> i.isSelected = false }
        queuesListAdapter.notifyDataSetChanged()
        queuesListAdapter.selectItem(selectedQueueIndex)
        (binding.queueList.layoutManager as LinearLayoutManager).scrollToPositionWithOffset(selectedQueueIndex, binding.queueList.top)
    }

    private fun saveSelectedQueueBeforeNavigating(selectedQueue: Queue) {
        prefs.queueObject = selectedQueue
    }

    override fun onDestroyView() {
        super.onDestroyView()
        Log.i("profileSaveQueue", "i will save queue: $selectedQueue")
        saveSelectedQueueBeforeNavigating(selectedQueue!!)
    }
}

HomeViewModel.kt

class HomeViewModel @Inject constructor(
    private val userRepository: UserRepository,
    private val ticketsRepository: TicketsRepository,
    private val queuesRepository: QueuesRepository,
    private val notificationsRepository: NotificationsRepository,
    private val pushRepository: PushRepository
) : BaseViewModel() {

    private var ticketsList: MutableList<Ticket> = mutableListOf()
    private var summaryTicketsCount: Int? = 0

    private val _status = MutableLiveData<ApiStatus>()
    val status: LiveData<ApiStatus>
        get() = _status

    private val _notificationsCount = MutableLiveData<NoticeCountResponse?>()
    val notificationscount: LiveData<NoticeCountResponse?>
        get() = _notificationsCount

    private val _tickets = MutableLiveData<TicketsResponse?>()
    val tickets: LiveData<TicketsResponse?>
        get() = _tickets

    private val _navigateToTicketDetails = MutableLiveData<Long?>()
    val navigateToTicketDetails
        get() = _navigateToTicketDetails

    private val _queues = MutableLiveData<QueuesResponse?>()
    val queues: LiveData<QueuesResponse?>
        get() = _queues

    private val _queueIds = MutableLiveData<List<Long>?>()
    val queueIds: LiveData<List<Long>?>
        get() = _queueIds

    private val _checkUserSettings = MutableLiveData<User>()
    val checkUserSettings: LiveData<User>
        get() = _checkUserSettings

    fun refresh(showGeneral: Boolean, favoriteOnly: Boolean, selectedQueueIndex: Int, queueId: Long, queueIds: List<Long>?, lockId: Int?, limit: Long?, offset: Long?) {
        ticketsList = mutableListOf()
        getQueues(showGeneral, favoriteOnly, selectedQueueIndex)
    }

    fun getUserSettings() {
        viewModelScope.launch {
            _checkUserSettings.value = retrieveUserSettings()
        }
    }

    private suspend fun retrieveUserSettings(): User? {
        return withContext(Dispatchers.IO) {
            userRepository.getUserInfo()
        }
    }

    fun getUnreadNotificationsCount() {
        _status.value = ApiStatus.LOADING
        viewModelScope.launch {
            kotlin.runCatching { notificationsRepository.getUnreadNotificationsCount("Bearer ${getToken()}") }
                .onSuccess {
                    _notificationsCount.value = it
                    _status.value = ApiStatus.DONE
                }
                .onFailure {
                    _status.value = ApiStatus.DONE
                }
        }
    }

    fun registerPushToken(token: String) {
        viewModelScope.launch {
            pushRepository.registerToken("Bearer ${getToken()}", TokenRegisterBody(token, 1))
        }
    }

    fun getQueues(showGeneral: Boolean, favoriteOnly: Boolean, selectedQueueIndex: Int) {
        _status.value = ApiStatus.LOADING
        viewModelScope.launch {
            kotlin.runCatching { queuesRepository.getQueuesListWithTicketsCount("Bearer ${getToken()}", favoriteOnly) }
                .onSuccess { value ->
                    summaryTicketsCount = value.queues?.mapNotNull { q -> q.ticketsCount }?.sum()
                    val queuesList: List<Queue> = sortQueues(value.queues, selectedQueueIndex, showGeneral)
                    _queueIds.value = value.queues?.map { item -> item.queueId }
                    _queues.value = QueuesResponse(queuesList, value.responseCode, value.responseMessage)
                    _status.value = ApiStatus.DONE
                }
                .onFailure {
                    if (it is HttpException) {
                        _queues.value = QueuesResponse(null, it.code(), getResponseMessage(it))
                        _status.value = ApiStatus.DONE
                    }
                    else {
                        _queues.value = QueuesResponse(null, -1, "Что-то пошло не так")
                        _status.value = ApiStatus.DONE
                    }
                }
        }
    }

    fun getTickets(queueId: Long?, queueIds: List<Long>?, lockId: Int?, limit: Long?, offset: Long?) {
        _status.value = ApiStatus.LOADING
        val body = TicketsListBody(queueId = queueId, queueIds = queueIds, lockId = lockId, limit = limit, offset = offset)
        viewModelScope.launch {
            kotlin.runCatching { ticketsRepository.getTickets("Bearer ${getToken()}", body) }
                .onSuccess {
                    it.tickets?.forEach { ticket -> if (ticket !in ticketsList) { ticketsList.add(ticket) } }
                    _tickets.value = TicketsResponse(ticketsList, it.responseCode, it.responseMessage)
                    _status.value = ApiStatus.DONE
                }
                .onFailure {
                    if (it is HttpException) {
                        _tickets.value = TicketsResponse(null, it.code(), getResponseMessage(it))
                        _status.value = ApiStatus.DONE
                    }
                    else {
                        _tickets.value = TicketsResponse(null, -1, "Что-то пошло не так")
                        _status.value = ApiStatus.DONE
                    }
                }
        }
    }

    private fun sortQueues(queues: List<Queue>?, selectedQueueIndex: Int, showGeneral: Boolean): List<Queue> {
        val favoriteQueuesList: List<Queue>? = queues?.toMutableList()
            ?.filter { a -> a.isInFavoritesList }
            ?.sortedByDescending { b -> b.ticketsCount }

        val restQueuesList: List<Queue>? = queues?.toMutableList()
            ?.filter { a -> !a.isInFavoritesList }
            ?.sortedByDescending { b -> b.ticketsCount }

        val queuesList: List<Queue> = mutableListOf<Queue>()
            .also { items ->
                if (showGeneral) {
                    items.add(0, Queue(0, null, summaryTicketsCount, true,false))
                }
                favoriteQueuesList?.forEach { a -> items.add(a) }
                restQueuesList?.forEach { a -> items.add(a) }
                items[selectedQueueIndex].isSelected = true
            }
        return queuesList
    }

    fun onTicketDetailsClicked(id: Long) { _navigateToTicketDetails.value = id }
    fun onTicketDetailsNavigated() { _navigateToTicketDetails.value = null }

    fun onQueueClicked(id: Long, ids: List<Long>?, lockId: Int?, limit: Long?, offset: Long) {
        ticketsList = mutableListOf()
        getTickets(id, ids, lockId, limit, offset)
    }

    private suspend fun getToken(): String? {
        return withContext(Dispatchers.IO) {
            userRepository.getUserInfo()?.sessionValue
        }
    }

    fun logout() {
        viewModelScope.launch {
            withContext(Dispatchers.IO) {
                userRepository.clean()
            }
        }
    }

    override fun onCleared() {
        super.onCleared()
        ticketsList = mutableListOf()
    }
}

隊列列表適配器.kt

class QueuesListAdapter (val clickListener : QueuesListListener):
    ListAdapter<Queue, QueuesListAdapter.ViewHolder>(DIFF_CALLBACK) {

    companion object {
        private val DIFF_CALLBACK = object : DiffUtil.ItemCallback<Queue>() {

            override fun areItemsTheSame(oldItem: Queue, newItem: Queue): Boolean {
                return oldItem.queueId == newItem.queueId
            }

            override fun areContentsTheSame(oldItem: Queue, newItem: Queue): Boolean {
                return oldItem == newItem
            }
        }
        private var statesMap = HashMap<Int,Boolean>()
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        val item = getItem(position)
        setItemView(item, holder.binding)
        holder.bind(item, clickListener)
        item.isSelected = statesMap[position] != null
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        return ViewHolder.from(parent)
    }

    fun selectItem(position: Int) {
        val item = getItem(position)
        item.isSelected = true
        statesMap.clear()
        statesMap[position] = item.isSelected
        notifyItemChanged(position)
    }

    private fun setItemView(item: Queue, binding: ItemQueueBinding) {
        when (item.isSelected) {
            true -> {
                item.isSelected = false
                binding.queueContent.setBackgroundResource(R.drawable.item_selected_queue_background)
                binding.queueContent.alpha = 1F
            }
            false -> {
                binding.queueContent.setBackgroundResource(R.drawable.item_queue_background)
                if (item.ticketsCount == 0) {
                    binding.queueContent.isEnabled = false
                    binding.queueContent.isFavoriteIcon.isEnabled = false
                    binding.queueContent.alpha = 0.3F
                } else {
                    binding.queueContent.isEnabled = true
                    binding.queueContent.isFavoriteIcon.isEnabled = true
                    binding.queueContent.alpha = 1F
                }
            }
        }
    }

    class ViewHolder private constructor(val binding: ItemQueueBinding): RecyclerView.ViewHolder(
        binding.root
    ) {

        fun bind(item: Queue,  clickListener: QueuesListListener) {
            binding.queues = item
            binding.clickListener = clickListener
        }

        companion object {
            fun from(parent: ViewGroup): ViewHolder {
                val layoutInflater = LayoutInflater.from(parent.context)
                val binding = ItemQueueBinding.inflate(layoutInflater, parent, false)
                return ViewHolder(binding)
            }
        }
    }
}

class QueuesListListener(val clickListener: (queue: Queue) -> Unit) {
    fun onClick(queue: Queue) {
        clickListener(queue)
    }
}

ProfileFragment.kt

class ProfileFragment : BaseFragment<ProfileFragmentBinding>() {

    @Inject lateinit var factory: ProfileViewModelFactory
    @Inject lateinit var viewModel: ProfileViewModel
    private lateinit var profileQueuesListAdapter: ProfileQueuesListAdapter
    private var initialQueuesList = mutableListOf<Queue>()
    private var favorites = mutableMapOf<Long,Boolean>()

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        ComponentsHolder.getComponent().inject(this)
        super.onViewCreated(view, savedInstanceState)

        (activity as MainActivity).showBottomNavigation()
        (activity as MainActivity).getUnreadNotificationsCount()

        viewModel = ViewModelProvider(this, factory)[ProfileViewModel::class.java]
        binding.model = viewModel
        binding.lifecycleOwner = this

        with(viewModel) {
            getUserSettings()

            checkUserSettings.observe(viewLifecycleOwner) {
                it?.let {
                    favoritesSwitchItem.isChecked = it.isFavoritesChecked!!
                    generalQueueSwitchItem.isChecked = it.isGeneralChecked
                }
            }

            loggedOut.observe(viewLifecycleOwner) {
                it?.let {
                    if (mayNavigate()) {
                        findNavController().navigate(
                            ProfileFragmentDirections
                                .actionProfileFragmentToLoginFragment()
                        )
                    }
                }
            }
        }

        with(binding) {
            profileAppBar.toolbar.title = "Профиль"
            logoutButton.setOnClickListener { viewModel.logout() }
            appVersionDescription.text = requireContext().packageManager.getPackageInfo(requireContext().packageName, 0).versionName

            generalQueueSwitchItem.setOnCheckedChangeListener { _, _ ->
                if (generalQueueSwitchItem.isChecked) {
                    viewModel.updateGeneralQueueState(true)
                } else {
                    viewModel.updateGeneralQueueState(false)
                }
            }

            favoritesSwitchItem.setOnCheckedChangeListener { _, _ ->
                if (favoritesSwitchItem.isChecked) {
                    viewModel.updateFavoritesState(true)
                } else {
                    viewModel.updateFavoritesState(false)
                }
            }
        }
    }

    override fun getFragmentBinding(
        inflater: LayoutInflater,
        container: ViewGroup?
    ) = ProfileFragmentBinding.inflate(inflater, container, false)

}

ProfileViewModel.kt

class ProfileViewModel @Inject constructor(
    private val userRepository: UserRepository,
    private val queuesRepository: QueuesRepository
) : BaseViewModel() {

    private var summaryTicketsCount: Int? = 0
    private var addToFavoritesList = mutableListOf<Long>()
    private var removeFromFavoritesList = mutableListOf<Long>()

    private val _status = MutableLiveData<ApiStatus>()
    val status: LiveData<ApiStatus>
        get() = _status

    private val _queues = MutableLiveData<QueuesResponse?>()
    val queues: LiveData<QueuesResponse?>
        get() = _queues

    private val _loggedOut = MutableLiveData<Boolean>()
    val loggedOut : LiveData<Boolean>
        get() = _loggedOut

    private val _checkUserSettings = MutableLiveData<User>()
    val checkUserSettings: LiveData<User>
        get() = _checkUserSettings

    init { }

    fun logout() {
        coroutineScope.launch {
            clean().also { _loggedOut.value = true }
        }
    }

    fun getUserSettings() {
        coroutineScope.launch {
            _checkUserSettings.postValue(retrieveUserSettings())
        }
    }

    private suspend fun retrieveUserSettings(): User? {
        return withContext(Dispatchers.IO) {
            userRepository.getUserInfo()
        }
    }

    fun updateGeneralQueueState(isShouldBeShown: Boolean) {
        _status.value = ApiStatus.LOADING
        coroutineScope.launch {
            updateGeneralQueue(isShouldBeShown)
            _status.value = ApiStatus.DONE
        }
    }

    fun updateFavoritesState(isFavoritesActive: Boolean) {
        _status.value = ApiStatus.LOADING
        coroutineScope.launch {
            updateFavorites(isFavoritesActive)
            _status.value = ApiStatus.DONE
        }
    }

    private suspend fun updateGeneralQueue(isShouldBeShown: Boolean) {
        withContext(Dispatchers.IO) {
            userRepository.updateGeneralQueueState(isShouldBeShown)
        }
    }

    private suspend fun updateFavorites(isFavoritesActive: Boolean) {
        withContext(Dispatchers.IO) {
            userRepository.updateFavoritesState(isFavoritesActive)
        }
    }

    private suspend fun getToken(): String? {
        return withContext(Dispatchers.IO) {
            userRepository.getUserInfo()?.sessionValue
        }
    }

    private suspend fun clean() {
        withContext(Dispatchers.IO) {
            userRepository.clean()
        }
    }
}

我不知道到底發生了什么,因為這是很多代碼並且很難逐行跟蹤; 在 SO 上閱讀代碼時很容易遺漏一些東西,但我確實看到了一些可以改進架構的東西。

我要開始的地方是查看您的體系結構中具有“代碼味道”的部分(注意:我將編寫的大部分代碼都是偽代碼,而且我沒有使用數據綁定的經驗(僅ViewBinding),因為我不喜歡它的功能,而且我一直選擇不使用它,所以如果數據綁定導致問題,我不確定。

建築學

在利用協同程序的強大功能時,您希望受益於使用掛起函數的能力,以及 LiveData(或 Flow)的反應性質,以便在您的 UI 中觀察和做出反應。 我不會 go 對每個主題進行過多的詳細介紹,但是當我看到潛在的可測試性問題時,我會提到它們,因為您需要對您的業務邏輯進行單元測試,為此,您應該考慮一些事情。

一般來說,你會想要遵循 Jetpack 架構的想法(除非你為 Square 工作,在這種情況下一切都必須不同,因為谷歌是錯誤的); 考慮到這一點,我將在適用的情況下遵循Google 推薦的做法,因為如果您不喜歡它,您可以找到自己的替代方案;)

碎片

我在碎片中看到了很多state。 許多布爾值、整數、列表等。這通常是一個危險信號。 你有一個 ViewModel,那是你的 state 應該來自的地方,片段很少有理由在本地“存儲”這個 state。

視圖模型

我覺得您使用了很多LiveData,這很好,但我相信您可以通過將其中的大部分替換為組合流來進一步受益。 你的每個內部狀態都是一個流,如果你想拆分你的反應代碼的一部分,你可以暴露給片段一個(組合)或一對。 通過在您的 VM 中使用combine(flow1, flow2, etc...) function,您可以生成一個更具凝聚力的 state,甚至將其公開為StateFlow以提高效率,因為您隨后會觀察來自你的片段使用類似的東西:

viewLifecycleOwner.lifecycleScope.launchWhenStarted { 
    viewModel.yourFlow.collect {...} //or collectLatest, depending on your usecase
}

這是可選的,但它比有這么多 liveDatas 浮動更好。

片段 - 適配器

我看到您有兩個或三個 ListView 適配器(很好),但它們實際上不需要是 lateinit。 你並沒有真正添加太多,在初始化時創建它們:

private val adapter1 = SomeAdapter()
private val adapter2 = AnotherAdater()

由於它們是 ListAdapter,一旦您通過 (livedata/flow) 接收數據,您應該做的就是adapter1.submitList(...) ,因為它們永遠不可能是 null。 通過使用 lateinit (在你知道你無論如何都會需要的東西中)你並沒有真正獲得任何東西,並且正在引入復雜性。 你可以做比lateinit更好的優化。

最后,您的片段應盡可能虛擬 您在顯示它時“加載它”,遵守它瘋狂的生命周期,然后將所有東西連接起來,以便它可以觀察實時數據/流並使用傳入的 state 更新其 UI,這就是它應該做的。 當然還有導航,但主要是因為它是您應該在 android 框架中執行的必需管道的一部分。

如果你添加更多的邏輯/東西/狀態,你就會把自己置於一個測試角落和一個更復雜的場景來管理,因為片段被銷毀、分離、重新添加等。

列表適配器

使用列表適配器做得很好,但您的適配器有一些問題。

  • 不要調用notifyDataSetChanged ,它會破壞目的。 提交列表應該可以解決問題,這就是您擁有 DiffUtil 的原因。 如果這不起作用,那么您可能需要注意 ListAdapter 的其他問題,但是一旦您解決了這些問題,您應該可以使用 go。

以您的代碼片段為例:

    fun selectItem(position: Int) {
        val item = getItem(position)
        item.isSelected = true
        statesMap.clear()
        statesMap[position] = item.isSelected
        notifyItemChanged(position)
    }

為什么會有一個static hashMap來表示選擇呢?

Adapter 在幕后已經有很多工作要做,它不應該承擔這個責任。

選擇某些內容后,您將對其進行一些處理(將一些 boolean 設置為 true,例如yourItem.isSelected = true ),然后生成一個新列表,該列表將提交給適配器,diffutil 將選擇更改。

(這只是改變您的列表的操作示例,它可能是其他操作,但原則是,不要將 state 保留在它不屬於的地方,而是對通過預期渠道收到的更改做出反應)。

ViewHolders/適配器

這看起來不錯,但我覺得你沒有正確委派你的職責。 你的適配器不應該有很多 if 語句。 如果選擇了一個項目,ViewHolder 應該接收此信息並采取相應行動,而不是 Adapter。 因此,我會將 boolean 與 ViewHolder 設置其正確外觀所需的所有信息一起傳遞給您的fun bind 無論如何,這都是只讀信息。

協程范圍/調度程序

小心在整個地方硬編碼 Dispatchers.IO,這使得無法正確測試,因為您無法輕易覆蓋調度程序。 相反,注入它,因為您已經在使用 Dagger。 這樣您的測試將能夠覆蓋它們。

在 viewModel 中總是這樣做

viewModelScope.launch(injected_dispatcher) {
   //call your suspend functions and such
   val result = someRepo.getSomething()
   someFlow.emit(result)
}

(只是一個例子)。

當您測試您的 VM 時,您將提供一個測試調度程序。

結論

總的來說,在架構上做得很好,這比完成所有工作的大型活動要好;)

我覺得您可以稍微簡化您的代碼,這反過來將極大地幫助您找到未按預期運行的部分。

記住。 Activity/Fragment 觀察 ViewModel,並處理 Android 框架事物(Google 稱它們為“策略委托”),如導航、意圖等。它們對接收到的數據做出反應並將其傳遞(例如,傳遞給適配器)。 viewModel 位於您的真實來源(存儲庫、數據層)和您的業務邏輯(拆分為用例/交互器或您所說的任何東西)之間。 ViewModel 可以為脆弱的 Fragments/Activities 提供更穩定、壽命更長的組件,將數據與 UI 粘合在一起。

回購協議/等。 都是suspend函數,從源頭返回你需要的數據,需要的時候再更新。 (例如,與 Room DB 或 API 或兩者交談。)它們僅返回數據供 VM 和更高級別使用。

用例/交互器只是“重用”視圖模型和存儲庫等(或某些特定邏輯)之間的通信的抽象。 他們可以在他們認為合適的時候對您的數據應用轉換,從而將虛擬機從這種責任中解放出來。

例如,如果您有一個 GetSomethingUseCase,它可能會在幕后與存儲庫對話,等待(暫停),然后將 API/DB 響應轉換為 UI 所需的內容,所有這些都在沒有 VM(或 UI)的情況下完成知道發生了什么。

最后,使您的適配器盡可能小。 請記住,他們已經承擔了很多責任。

我看到這個工作的方式是。

片段 1 開始,VM 正在初始化。 片段在啟動時通過一些實時數據/流觀察其 state。 片段改變其視圖以匹配收到的 state。

用戶轉到片段 2 並更改某些內容,此“內容”更新數據庫或內存中的數據結構。

用戶回到Fragment 1,又是init還是restored,不過這次又觀察了liveData/Flow,數據又回來了(現在被fragment2修改了)。 Fragment 更新其 UI 而無需考慮太多,因為這不是它的責任。

我希望這個冗長的答案能為您指明正確的方向。

除此之外,我建議您將問題分解為一個更小的問題,以嘗試找出沒有做應做的事情。 隨機位置(適配器、片段等)中的“狀態”越少,出現奇怪問題的可能性就越小。

不要與框架作斗爭;)

最后,如果你做到了這一點,如果你向適配器提交了一個列表但它沒有更新, 請查看這個 SO 問題/答案,因為 ListAdapter 有一個“根據谷歌,這不是錯誤,但文檔沒有”不夠清楚”的情況。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM