Moim problemem jest, że mój shopViewModel, który posiada instancję paging-flow jest w jakiś sposób przeciekający. Próbowałem rozwiązać ten problem, konwertując flow do livedata, ale nic nie zmieniło.

ViewModel

class ShopViewModel @ViewModelInject constructor(
    private val shopPagingSource: ShopPagingSource,
) : ViewModel() {
    val SHOP_PAGE_CONFIG: PagingConfig = PagingConfig(pageSize = 20, enablePlaceholders = false)

    // As LiveData
    val shopFlow = Pager(SHOP_PAGE_CONFIG) { shopPagingSource }.flow.cachedIn(viewModelScope).asLiveData()

   // Before
    val shopFlow = Pager(SHOP_PAGE_CONFIG) { shopPagingSource }.flow.cachedIn(viewModelScope)
}

Fragment

@AndroidEntryPoint
class ShopFragment(private val shopListAdapter: ShopAdapter) : Fragment(R.layout.fragment_shop), ShopAdapter.OnItemClickListener {
    private val shopViewModel: ShopViewModel by viewModels()
    private val shopBinding: FragmentShopBinding by viewBinding()

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        shopBinding.adapter = shopListAdapter.withLoadStateFooter(ShopLoadAdapter(shopListAdapter::retry))
        shopListAdapter.clickHandler(this)
        collectShopList()
    }
    
    override fun forwardClick(product: @NotNull Product) {
        val action = ShopFragmentDirections.actionShopFragmentToShopItemFragment(product)
        findNavController().navigate(action)
    }
   
    private fun collectShopListWithLiveData() = lifecycleScope.launch {
        shopViewModel.shopFlow.observe(viewLifecycleOwner) {
            lifecycleScope.launch {
                shopListAdapter.submitData(it)
            }
        }
    }

    // Before converting to livedata
    private fun collectShopListWithFlow() = lifecycleScope.launch {
        shopViewModel.shopFlow.collectLatest {
             shopListAdapter.submitData(it)
        }
    }


    // To avoid memory leak from injected adapter
    override fun onDestroyView() {
        requireView().findViewById<RecyclerView>(R.id.rv_shop).adapter = null
        super.onDestroyView()
    }
}

Adapter

class ShopAdapter @Inject constructor() : PagingDataAdapter<Product, ShopAdapter.ShopViewHolder>(Companion) {

    private lateinit var clickListener: OnItemClickListener

    companion object: DiffUtil.ItemCallback<Product>() {
        override fun areItemsTheSame(oldItem: Product, newItem: Product): Boolean = oldItem.articelNumber == newItem.articelNumber
        override fun areContentsTheSame(oldItem: Product, newItem: Product): Boolean = oldItem == newItem
    }

    inner class ShopViewHolder(val binding: ShopListItemBinding) : RecyclerView.ViewHolder(binding.root)

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ShopAdapter.ShopViewHolder {
        val layoutInflater = LayoutInflater.from(parent.context)
        val binding = ShopListItemBinding.inflate(layoutInflater, parent, false)
        return ShopViewHolder(binding).also {
            binding.mcvProductItem.setOnClickListener { clickListener.forwardClick(binding.product!!) }
        }
    }

    override fun onBindViewHolder(holder: ShopAdapter.ShopViewHolder, position: Int) {
        holder.binding.product = getItem(position) ?: return
        holder.binding.executePendingBindings()
    }

    fun clickHandler(clickEventHandler: OnItemClickListener) {
        clickListener = clickEventHandler
    }

    interface OnItemClickListener {
        fun forwardClick(product: @NotNull Product)
    }
}

Mainfragmentfactory.

class MainFragmentFactory @Inject constructor(
    // .. other dependencies
    private val shopAdapter: ShopAdapter,
) : FragmentFactory() {
    override fun instantiate(classLoader: ClassLoader, className: String): Fragment = when(className) {
        // ... other fragments
        ShopFragment::class.java.name -> ShopFragment(shopAdapter)
        else -> super.instantiate(classLoader, className)
}

Pagingsource.

class ShopPagingSource @Inject constructor(
    private val shopRepository: ShopFirebaseRepository,
) : PagingSource<QuerySnapshot, Product>() {

    override suspend fun load(params: LoadParams<QuerySnapshot>): LoadResult<QuerySnapshot, Product> = try {
            withTimeout(SHOP_MAX_LOADING_TIME) {
                val currentPage = params.key ?: shopRepository.getCurrentPage()

                val lastDocumentSnapShot = currentPage.documents[currentPage.size() - 1]

                val nextPage = shopRepository.getNextPage(lastDocumentSnapShot)

                LoadResult.Page(
                    data = currentPage.toObjects(),
                    prevKey = null,
                    nextKey = nextPage
                )
            }
        } catch (e: TimeoutCancellationException) {
            Timber.d("Mediator failed, No Internet Connection")
            LoadResult.Error(e)
        } catch (e: ArrayIndexOutOfBoundsException) {
            Timber.d("Mediator failed, ArrayIndexOutOfBounds")
            LoadResult.Error(e)
        } catch (e: Exception) {
            Timber.d("Mediator failed, Unknown Error: ${e.message.toString()}")
            LoadResult.Error(e)
        }
    }

Szczelna

D/LeakCanary: ====================================
    HEAP ANALYSIS RESULT
    ====================================
    1 APPLICATION LEAKS
    
    References underlined with "~~~" are likely causes.
    Learn more at https://squ.re/leaks.
    
    2618 bytes retained by leaking objects
    Signature: 944313b4ecbdb77c99682dc8c1646e12e4f37d8
    ┬───
    │ GC Root: Local variable in native code
    │
    ├─ dalvik.system.PathClassLoader instance
    │    Leaking: NO (InternalLeakCanary↓ is not leaking and A ClassLoader is never leaking)
    │    ↓ PathClassLoader.runtimeInternalObjects
    ├─ java.lang.Object[] array
    │    Leaking: NO (InternalLeakCanary↓ is not leaking)
    │    ↓ Object[].[2142]
    ├─ leakcanary.internal.InternalLeakCanary class
    │    Leaking: NO (MainActivity↓ is not leaking and a class is never leaking)
    │    ↓ static InternalLeakCanary.resumedActivity
    ├─ com.example.app.framework.ui.view.MainActivity instance
    │    Leaking: NO (MainNavHostFragment↓ is not leaking and Activity#mDestroyed is false)
    │    ↓ MainActivity.navController$delegate
    ├─ kotlin.SynchronizedLazyImpl instance
    │    Leaking: NO (MainNavHostFragment↓ is not leaking)
    │    ↓ SynchronizedLazyImpl._value
    ├─ androidx.navigation.NavHostController instance
    │    Leaking: NO (MainNavHostFragment↓ is not leaking)
    │    ↓ NavHostController.mLifecycleOwner
    ├─ com.example.app.framework.ui.view.utils.MainNavHostFragment instance
    │    Leaking: NO (Fragment#mFragmentManager is not null)
    │    ↓ MainNavHostFragment.mainFragmentFactory
    │                          ~~~~~~~~~~~~~~~~~~~
    ├─ com.example.app.framework.ui.view.utils.MainFragmentFactory instance
    │    Leaking: UNKNOWN
    │    ↓ MainFragmentFactory.shopAdapter
    │                          ~~~~~~~~~~~
    ├─ com.example.app.framework.ui.adapter.recyclerview.ShopAdapter instance
    │    Leaking: UNKNOWN
    │    ↓ ShopAdapter.differ
    │                  ~~~~~~
    ├─ androidx.paging.AsyncPagingDataDiffer instance
    │    Leaking: UNKNOWN
    │    ↓ AsyncPagingDataDiffer.differBase
    │                            ~~~~~~~~~~
    ├─ androidx.paging.AsyncPagingDataDiffer$differBase$1 instance
    │    Leaking: UNKNOWN
    │    Anonymous subclass of androidx.paging.PagingDataDiffer
    │    ↓ AsyncPagingDataDiffer$differBase$1.receiver
    │                                         ~~~~~~~~
    ├─ androidx.paging.PageFetcher$PagerUiReceiver instance
    │    Leaking: UNKNOWN
    │    ↓ PageFetcher$PagerUiReceiver.this$0
    │                                  ~~~~~~
    ├─ androidx.paging.PageFetcher instance
    │    Leaking: UNKNOWN
    │    ↓ PageFetcher.pagingSourceFactory
    │                  ~~~~~~~~~~~~~~~~~~~
    ├─ com.example.app.framework.ui.viewmodel.ShopViewModel$shopFlow$1 instance
    │    Leaking: UNKNOWN
    │    Anonymous subclass of kotlin.jvm.internal.Lambda
    │    ↓ ShopViewModel$shopFlow$1.this$0
    │                               ~~~~~~
    ╰→ com.example.app.framework.ui.viewmodel.ShopViewModel instance
    ​     Leaking: YES (ObjectWatcher was watching this because com.example.app.framework.ui.viewmodel.ShopViewModel received ViewModel#onCleared() callback)
    ​     key = 0e65fcab-e6dd-475a-83d4-87b2050d797b
    ​     watchDurationMillis = 7771
    ​     retainedDurationMillis = 2769
    ====================================

EDYTOWAĆ

Podczas scoping ShopAdapter za pomocą @FragmentScoped, otrzymuję następujący wyciek:

    ┬───
    │ GC Root: Local variable in native code
    │
    ├─ dalvik.system.PathClassLoader instance
    │    Leaking: NO (InternalLeakCanary↓ is not leaking and A ClassLoader is never leaking)
    │    ↓ PathClassLoader.runtimeInternalObjects
    ├─ java.lang.Object[] array
    │    Leaking: NO (InternalLeakCanary↓ is not leaking)
    │    ↓ Object[].[409]
    ├─ leakcanary.internal.InternalLeakCanary class
    │    Leaking: NO (MainActivity↓ is not leaking and a class is never leaking)
    │    ↓ static InternalLeakCanary.resumedActivity
    ├─ com.example.app.framework.ui.view.MainActivity instance
    │    Leaking: NO (MainNavHostFragment↓ is not leaking and Activity#mDestroyed is false)
    │    mApplication instance of com.example.app.App
    │    mBase instance of androidx.appcompat.view.ContextThemeWrapper, not wrapping known Android context
    │    ↓ MainActivity.navController$delegate
    ├─ kotlin.SynchronizedLazyImpl instance
    │    Leaking: NO (MainNavHostFragment↓ is not leaking)
    │    ↓ SynchronizedLazyImpl._value
    ├─ androidx.navigation.NavHostController instance
    │    Leaking: NO (MainNavHostFragment↓ is not leaking)
    │    mActivity instance of com.example.app.framework.ui.view.MainActivity with mDestroyed = false
    │    mContext instance of dagger.hilt.android.internal.managers.ViewComponentManager$FragmentContextWrapper, wrapping
    │    activity com.example.app.framework.ui.view.MainActivity with mDestroyed = false
    │    ↓ NavHostController.mLifecycleOwner
    ├─ com.example.app.framework.ui.view.utils.MainNavHostFragment instance
    │    Leaking: NO (Fragment#mFragmentManager is not null)
    │    componentContext instance of dagger.hilt.android.internal.managers.ViewComponentManager$FragmentContextWrapper,
    │    wrapping activity com.example.app.framework.ui.view.MainActivity with mDestroyed = false
    │    ↓ MainNavHostFragment.mainFragmentFactory
    │                          ~~~~~~~~~~~~~~~~~~~
D/LeakCanary: ├─ com.example.app.framework.ui.view.utils.MainFragmentFactory instance
    │    Leaking: UNKNOWN
    │    Retaining 212 bytes in 7 objects
    │    ↓ MainFragmentFactory.shopAdapter
    │                          ~~~~~~~~~~~
    ├─ com.example.app.framework.ui.adapter.recyclerview.ShopAdapter instance
    │    Leaking: UNKNOWN
    │    Retaining 14461 bytes in 546 objects
    │    ↓ ShopAdapter.clickListener
    │                  ~~~~~~~~~~~~~
    ╰→ com.example.app.framework.ui.view.fragments.shop.ShopFragment instance
    ​     Leaking: YES (ObjectWatcher was watching this because com.example.app.framework.ui.view.fragments.shop.
    ​     ShopFragment received Fragment#onDestroy() callback and Fragment#mFragmentManager is null)
    ​     Retaining 2121 bytes in 79 objects
    ​     key = 71ec5094-8509-47a5-9e0a-070fe642ca8a
    ​     watchDurationMillis = 18366
    ​     retainedDurationMillis = 13365
    ​     componentContext instance of dagger.hilt.android.internal.managers.ViewComponentManager$FragmentContextWrapper,
    ​     wrapping activity com.example.app.framework.ui.view.MainActivity with mDestroyed = false
0
Andrew 14 październik 2020, 23:14

2 odpowiedzi

Najlepsza odpowiedź

Dobra, udało mi się rozwiązać ten wyciek. Przeciek został spowodowany, ponieważ wstrzyknąłem mojego ShopAdapter przez iniekcję konstruktora do mojego {x1}}. Podczas wstrzykiwania coś w fragment przez wtrysku konstruktora , musisz przejść zależność od MainFragmentFactory. Ale z tego powodu, MainFragmentFactory będzie zawsze posiadają odniesienie do adaptera, nawet gdy fragment zostanie zniszczony, a fragment nie jest już potrzebny (dlatego requireView().findViewById<RecyclerView>(R.id.rv_shop).adapter = null nie "Nawet wprowadź tutaj).

Aby rozwiązać ten problem, nie wstrzykiwać adapter za pomocą wstrzyknięcia konstruktora i raczej wstrzyknąć go za pomocą wtrysku pola.

0
Andrew 16 październik 2020, 15:38

Wiem, że spóźniłem się na imprezę, ale widzę, że używasz lifecycleScope.launch dużo, a wewnątrz dzwonisz do adaptera do Summdata. Oznacza to, że ten adapter nie będzie mógł być zebrany prawidłowo śmieci. Jest to prawdopodobnie źródło wycieku pamięci. Spróbuj zamiast tego użyć viewLifecycleOwner.lifecycleScope.launch.

Jest to znany błąd: https://proandroiddev.com/5-common-mistakes- Korzystanie z architektury-Components-403E9899F4CB

0
Warcello 4 marzec 2021, 12:51