├── .gitignore ├── .idea ├── .gitignore ├── compiler.xml ├── gradle.xml ├── misc.xml └── vcs.xml ├── README.md ├── README.org ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── example │ │ └── concatadaptergrid │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── example │ │ │ └── concatadaptergrid │ │ │ ├── ConcatenableAdapter.kt │ │ │ ├── MainActivity.kt │ │ │ ├── MultiAdapterFragment.kt │ │ │ ├── PagingFragment.kt │ │ │ ├── SampleApp.kt │ │ │ ├── Tags.kt │ │ │ ├── adapters │ │ │ ├── PagingProductAdapter.kt │ │ │ ├── ProductAdapterBasic.kt │ │ │ ├── ProductAdapterSingleHeader.kt │ │ │ ├── ProductItemCard.kt │ │ │ ├── ProductItemFactory.kt │ │ │ ├── ProductItemHeader.kt │ │ │ ├── ProductItemVM.kt │ │ │ └── ProductVH.kt │ │ │ ├── entities │ │ │ └── Product.kt │ │ │ ├── modules │ │ │ └── RepositoryModule.kt │ │ │ ├── paging │ │ │ └── ProductPagingSource.kt │ │ │ ├── repositories │ │ │ └── ProductRepository.kt │ │ │ └── viewmodels │ │ │ ├── ProductPagingViewModel.kt │ │ │ └── ProductSimpleViewModel.kt │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ └── ic_launcher_background.xml │ │ ├── layout │ │ ├── activity_main.xml │ │ ├── content_main.xml │ │ ├── fragment_multiadapter.xml │ │ ├── fragment_paging.xml │ │ ├── fragment_second.xml │ │ ├── item_product_card.xml │ │ └── item_product_header.xml │ │ ├── menu │ │ ├── bottom_navigation_menu.xml │ │ └── menu_main.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── navigation │ │ └── nav_graph.xml │ │ ├── values-land │ │ └── dimens.xml │ │ ├── values-night │ │ └── themes.xml │ │ ├── values-w1240dp │ │ └── dimens.xml │ │ ├── values-w600dp │ │ └── dimens.xml │ │ └── values │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── strings.xml │ │ └── themes.xml │ └── test │ └── java │ └── com │ └── example │ └── concatadaptergrid │ ├── ConcatenableAdapterConcateViewItemTypeTest.kt │ ├── ConcatenableAdapterHasGlobalItemViewTypeTest.kt │ └── ConcatenableAdapterResolveViewItemTypeTest.kt ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | .DS_Store 11 | /build 12 | /captures 13 | .externalNativeBuild 14 | .cxx 15 | local.properties 16 | .java-version 17 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 20 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 18 | 19 | 20 | 21 | 22 | 23 | 25 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [Sample project for blog post](https://u3x.medium.com/challenges-of-composing-recyclerview-with-concatadapter-in-a-grid-9bcf0d0c435a) 2 | 3 | Project explores how to build dynamic lists using `RecyclerView` and compose its items using different types of adapters for easier maintenance on complex marketing apps. 4 | 5 | - ConcatAdapter 6 | - GridLayoutManager 7 | - Page3 8 | -------------------------------------------------------------------------------- /README.org: -------------------------------------------------------------------------------- 1 | #+TITLE: Sample App for ConcatAdapter + GridLayoutManager 2 | 3 | [[https://u3x.medium.com/challenges-of-composing-recyclerview-with-concatadapter-in-a-grid-9bcf0d0c435a][Sample project for blog post]] 4 | 5 | Project explores how to build dynamic lists using =RecyclerView= and compose its items using different types of adapters for easier maintenance on complex marketing apps. 6 | 7 | - ConcatAdapter 8 | - GridLayoutManager 9 | - Page3 10 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.application' 3 | id 'kotlin-android' 4 | id 'kotlin-kapt' 5 | id 'dagger.hilt.android.plugin' 6 | id 'androidx.navigation.safeargs.kotlin' 7 | } 8 | 9 | android { 10 | compileSdk 31 11 | 12 | defaultConfig { 13 | applicationId "com.example.concatadaptergrid" 14 | minSdk 21 15 | targetSdk 31 16 | versionCode 1 17 | versionName "1.0" 18 | 19 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 20 | } 21 | 22 | buildTypes { 23 | release { 24 | minifyEnabled false 25 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 26 | } 27 | } 28 | compileOptions { 29 | sourceCompatibility JavaVersion.VERSION_1_8 30 | targetCompatibility JavaVersion.VERSION_1_8 31 | } 32 | kotlinOptions { 33 | jvmTarget = '1.8' 34 | } 35 | buildFeatures { 36 | viewBinding true 37 | } 38 | } 39 | 40 | dependencies { 41 | 42 | implementation 'androidx.core:core-ktx:1.6.0' 43 | implementation 'androidx.appcompat:appcompat:1.3.1' 44 | implementation 'com.google.android.material:material:1.4.0' 45 | implementation 'androidx.constraintlayout:constraintlayout:2.1.0' 46 | implementation 'androidx.navigation:navigation-fragment-ktx:2.3.5' 47 | implementation 'androidx.navigation:navigation-ui-ktx:2.3.5' 48 | implementation "androidx.recyclerview:recyclerview:1.2.0" 49 | implementation "androidx.paging:paging-runtime-ktx:3.0.0" 50 | implementation "androidx.fragment:fragment-ktx:1.3.6" 51 | implementation "com.jakewharton.timber:timber:4.7.1" 52 | 53 | implementation "com.google.dagger:hilt-android:2.38.1" 54 | kapt "com.google.dagger:hilt-compiler:2.38.1" 55 | 56 | testImplementation 'junit:junit:4.12' 57 | testImplementation "org.mockito:mockito-core:3.9.0" 58 | testImplementation "com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0" 59 | testImplementation "org.assertj:assertj-core:3.19.0" 60 | testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.4.2" 61 | 62 | androidTestImplementation 'androidx.test.ext:junit:1.1.3' 63 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' 64 | } -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /app/src/androidTest/java/com/example/concatadaptergrid/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.example.concatadaptergrid 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("com.example.concatadaptergrid", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 13 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/concatadaptergrid/ConcatenableAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.example.concatadaptergrid 2 | 3 | /** 4 | * Identifies adapter items that it will be used in [ConcatAdapter] 5 | * This class functionality creates adapter itemViewType unique for all adapters 6 | * This is useful when used in [ConcatAdapter] + [ConcatAdapter.setIsolateViewTypes(false)] 7 | * Be sure to provide [globalViewItemType] when identifying an item in child adapter, and 8 | * restore itemViewType back when used internally 9 | */ 10 | interface ConcatenableAdapter { 11 | val concatAdapterIndex: Int 12 | 13 | /** 14 | * Returns span size when used in Grid 15 | * Span size is resolved in numbers. 16 | * - Grid spanCount=2 17 | * - When takes 1 column out of 2, spanSize = 1 18 | * - When takes 2 column out of 2, spanSize = 1 19 | * - When takes both columns 2, spanSize = 2 20 | * By default this does not change span size 21 | * @param globalItemViewType global item view type (calculated with [resolveGlobalViewItemType]) 22 | * @return span size 23 | */ 24 | fun spanSizeByType(globalItemViewType: Int): Int = 1 25 | 26 | /** 27 | * @return true if item type belongs to adapter 28 | */ 29 | fun hasGlobalViewItemType(globalItemViewType: Int): Boolean { 30 | val minItemIndex = VIEW_ITEM_TYPE_MULTIPLIER * (concatAdapterIndex + 1) 31 | val maxItemIndex = VIEW_ITEM_TYPE_MULTIPLIER * (concatAdapterIndex + 2) 32 | return globalItemViewType >= minItemIndex && 33 | globalItemViewType < maxItemIndex 34 | } 35 | 36 | /** 37 | * @return [RecyclerView.Adapter.getItemViewType(position: Int)] when used in 38 | * [ConcatAdapter] to provide a unique item type 39 | */ 40 | fun globalViewItemType(localItemViewType: Int = 0): Int { 41 | return VIEW_ITEM_TYPE_MULTIPLIER * (concatAdapterIndex + 1) + localItemViewType 42 | } 43 | 44 | /** 45 | * Returns the original view item type for internal use 46 | * @param globalItemViewType is calculated type with [globalViewItemType] 47 | * @return resolved local itemViewType 48 | */ 49 | fun resolveGlobalViewItemType(globalItemViewType: Int): Int { 50 | return globalItemViewType - (VIEW_ITEM_TYPE_MULTIPLIER * (concatAdapterIndex + 1)) 51 | } 52 | 53 | companion object { 54 | private const val VIEW_ITEM_TYPE_MULTIPLIER = 100 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/concatadaptergrid/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.example.concatadaptergrid 2 | 3 | import android.os.Bundle 4 | import androidx.appcompat.app.AppCompatActivity 5 | import androidx.navigation.NavDirections 6 | import androidx.navigation.findNavController 7 | import androidx.navigation.ui.AppBarConfiguration 8 | import androidx.navigation.ui.navigateUp 9 | import androidx.navigation.ui.setupActionBarWithNavController 10 | import com.example.concatadaptergrid.databinding.ActivityMainBinding 11 | import dagger.hilt.android.AndroidEntryPoint 12 | 13 | @AndroidEntryPoint 14 | class MainActivity : AppCompatActivity() { 15 | 16 | private lateinit var binding: ActivityMainBinding 17 | 18 | override fun onCreate(savedInstanceState: Bundle?) { 19 | super.onCreate(savedInstanceState) 20 | binding = ActivityMainBinding.inflate(layoutInflater) 21 | setContentView(binding.root) 22 | setSupportActionBar(binding.toolbar) 23 | val navController = findNavController(R.id.nav_host_fragment_content_main) 24 | binding.bottomNavigation.setOnNavigationItemSelectedListener { menuItem -> 25 | val currentDestination = navController.currentDestination 26 | when (menuItem.itemId) { 27 | R.id.page_1 -> { 28 | if (currentDestination?.id != R.id.PagingFragment) { 29 | navController.navigate( 30 | MultiAdapterFragmentDirections 31 | .actionMultiAdapterFragmentToPagingFragment(), 32 | ) 33 | true 34 | } else { 35 | false 36 | } 37 | } 38 | R.id.page_2 -> { 39 | if (currentDestination?.id != R.id.MultiAdapterFragment) { 40 | navController.navigate( 41 | PagingFragmentDirections 42 | .actionPagingFragmentToMultiAdapterFragment() 43 | ) 44 | true 45 | } else { 46 | false 47 | } 48 | } 49 | else -> false 50 | } 51 | } 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/concatadaptergrid/MultiAdapterFragment.kt: -------------------------------------------------------------------------------- 1 | package com.example.concatadaptergrid 2 | 3 | import android.os.Bundle 4 | import android.view.LayoutInflater 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import androidx.fragment.app.Fragment 8 | import androidx.fragment.app.viewModels 9 | import androidx.recyclerview.widget.ConcatAdapter 10 | import androidx.recyclerview.widget.GridLayoutManager 11 | import com.example.concatadaptergrid.adapters.ProductAdapterBasic 12 | import com.example.concatadaptergrid.adapters.ProductAdapterSingleHeader 13 | import com.example.concatadaptergrid.adapters.ProductItemFactory 14 | import com.example.concatadaptergrid.databinding.FragmentMultiadapterBinding 15 | import com.example.concatadaptergrid.viewmodels.ProductSimpleUIStateError 16 | import com.example.concatadaptergrid.viewmodels.ProductSimpleUIStateLoading 17 | import com.example.concatadaptergrid.viewmodels.ProductSimpleUIStateSuccess 18 | import com.example.concatadaptergrid.viewmodels.ProductSimpleViewModel 19 | import com.google.android.material.snackbar.Snackbar 20 | import dagger.hilt.android.AndroidEntryPoint 21 | 22 | /** 23 | * Fragment with limited data 24 | * Using ConcatAdapter + GridLayoutManager 25 | */ 26 | @AndroidEntryPoint 27 | class MultiAdapterFragment : Fragment() { 28 | 29 | private val vmProduct: ProductSimpleViewModel by viewModels() 30 | 31 | private var _binding: FragmentMultiadapterBinding? = null 32 | private val binding 33 | get() = _binding!! 34 | 35 | private lateinit var adapterProducts1: ProductAdapterBasic 36 | private lateinit var adapterProducts2: ProductAdapterBasic 37 | 38 | override fun onCreateView( 39 | inflater: LayoutInflater, 40 | container: ViewGroup?, 41 | savedInstanceState: Bundle? 42 | ): View { 43 | _binding = FragmentMultiadapterBinding.inflate(inflater, container, false) 44 | return binding.root 45 | } 46 | 47 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 48 | super.onViewCreated(view, savedInstanceState) 49 | val context = requireContext() 50 | val adapterSingleHeader = ProductAdapterSingleHeader( 51 | context = context, 52 | concatAdapterIndex = 0, 53 | gridSpanSize = PagingFragment.GRID_SPAN_SIZE, 54 | ) 55 | adapterProducts1 = ProductAdapterBasic( 56 | context = context, 57 | concatAdapterIndex = 1, 58 | ) 59 | val adapterSingleHeader2 = ProductAdapterSingleHeader( 60 | context = context, 61 | concatAdapterIndex = 2, 62 | gridSpanSize = PagingFragment.GRID_SPAN_SIZE, 63 | ) 64 | adapterProducts2 = ProductAdapterBasic( 65 | context = context, 66 | concatAdapterIndex = 3, 67 | ) 68 | val layoutManager = GridLayoutManager(context, PagingFragment.GRID_SPAN_SIZE) 69 | val concatAdapterConfig = ConcatAdapter.Config.Builder() 70 | .setIsolateViewTypes(false) 71 | .build() 72 | val concatAdapter = ConcatAdapter( 73 | concatAdapterConfig, 74 | adapterSingleHeader, 75 | adapterProducts1, 76 | adapterSingleHeader2, 77 | adapterProducts2 78 | ) 79 | binding.productRecycler.layoutManager = layoutManager 80 | binding.productRecycler.adapter = concatAdapter 81 | layoutManager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() { 82 | override fun getSpanSize(position: Int): Int { 83 | val globalItemViewType = concatAdapter.getItemViewType(position) 84 | val spanSize: Int = concatAdapter 85 | .adapters 86 | .filterIsInstance() 87 | .first { it.hasGlobalViewItemType(globalItemViewType) } 88 | .spanSizeByType(globalItemViewType) 89 | return spanSize 90 | } 91 | } 92 | adapterSingleHeader.bindHeaderSimple("Items1") 93 | adapterSingleHeader2.bindHeaderSimple("Items2") 94 | observeStateChanges() 95 | vmProduct.fetchProducts() 96 | } 97 | 98 | private fun observeStateChanges() { 99 | vmProduct.ldState.observe(this, { 100 | when (it) { 101 | is ProductSimpleUIStateError -> { 102 | Snackbar 103 | .make(binding.root, "Error loading products", Snackbar.LENGTH_SHORT) 104 | .show() 105 | } 106 | ProductSimpleUIStateLoading -> { 107 | Snackbar 108 | .make(binding.root, "Loading products", Snackbar.LENGTH_SHORT) 109 | .show() 110 | } 111 | is ProductSimpleUIStateSuccess -> { 112 | Snackbar 113 | .make(binding.root, "Showing products", Snackbar.LENGTH_SHORT) 114 | .show() 115 | adapterProducts1.bindProducts( 116 | ProductItemFactory.createFromProducts(it.products1) 117 | ) 118 | adapterProducts2.bindProducts( 119 | ProductItemFactory.createFromProducts(it.products2) 120 | ) 121 | } 122 | }.javaClass 123 | }) 124 | } 125 | 126 | override fun onDestroyView() { 127 | super.onDestroyView() 128 | _binding = null 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/concatadaptergrid/PagingFragment.kt: -------------------------------------------------------------------------------- 1 | package com.example.concatadaptergrid 2 | 3 | import android.os.Bundle 4 | import android.view.LayoutInflater 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import androidx.fragment.app.Fragment 8 | import androidx.fragment.app.viewModels 9 | import androidx.lifecycle.lifecycleScope 10 | import androidx.recyclerview.widget.ConcatAdapter 11 | import androidx.recyclerview.widget.GridLayoutManager 12 | import androidx.recyclerview.widget.RecyclerView 13 | import com.example.concatadaptergrid.adapters.PagingProductAdapter 14 | import com.example.concatadaptergrid.adapters.ProductAdapterSingleHeader 15 | import com.example.concatadaptergrid.databinding.FragmentPagingBinding 16 | import com.example.concatadaptergrid.viewmodels.PPUiState 17 | import com.example.concatadaptergrid.viewmodels.ProductPagingUiAction 18 | import com.example.concatadaptergrid.viewmodels.ProductPagingViewModel 19 | import dagger.hilt.android.AndroidEntryPoint 20 | import kotlinx.coroutines.flow.StateFlow 21 | import kotlinx.coroutines.flow.collectLatest 22 | import kotlinx.coroutines.flow.distinctUntilChanged 23 | import kotlinx.coroutines.flow.map 24 | import kotlinx.coroutines.launch 25 | 26 | /** 27 | * Fragment with data loading in pages 28 | * Using ConcatAdapter + GridLayoutManager + Page3 framework 29 | * Source: https://developer.android.com/topic/libraries/architecture/paging/v3-overview 30 | */ 31 | @AndroidEntryPoint 32 | class PagingFragment : Fragment() { 33 | 34 | private val vmProduct: ProductPagingViewModel by viewModels() 35 | 36 | private var _binding: FragmentPagingBinding? = null 37 | private val binding 38 | get() = _binding!! 39 | 40 | override fun onCreateView( 41 | inflater: LayoutInflater, 42 | container: ViewGroup?, 43 | savedInstanceState: Bundle? 44 | ): View { 45 | _binding = FragmentPagingBinding.inflate(inflater, container, false) 46 | return binding.root 47 | } 48 | 49 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 50 | super.onViewCreated(view, savedInstanceState) 51 | val context = requireContext() 52 | val adapterSingleHeader = ProductAdapterSingleHeader( 53 | context = context, 54 | concatAdapterIndex = 0, 55 | gridSpanSize = GRID_SPAN_SIZE, 56 | ) 57 | val adapterSingleHeader2 = ProductAdapterSingleHeader( 58 | context = context, 59 | concatAdapterIndex = 1, 60 | gridSpanSize = GRID_SPAN_SIZE, 61 | ) 62 | val adapterPagingProducts = PagingProductAdapter( 63 | context = context, 64 | concatAdapterIndex = 2 65 | ) 66 | val layoutManager = GridLayoutManager(context, GRID_SPAN_SIZE) 67 | val concatAdapterConfig = ConcatAdapter.Config.Builder() 68 | .setIsolateViewTypes(false) 69 | .build() 70 | val concatAdapter = ConcatAdapter( 71 | concatAdapterConfig, 72 | adapterSingleHeader, 73 | adapterSingleHeader2, 74 | adapterPagingProducts 75 | ) 76 | binding.productRecycler.layoutManager = layoutManager 77 | binding.productRecycler.adapter = concatAdapter 78 | layoutManager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() { 79 | override fun getSpanSize(position: Int): Int { 80 | val globalItemViewType = concatAdapter.getItemViewType(position) 81 | val spanSize: Int = concatAdapter 82 | .adapters 83 | .filterIsInstance() 84 | .first { it.hasGlobalViewItemType(globalItemViewType) } 85 | .spanSizeByType(globalItemViewType) 86 | return spanSize 87 | } 88 | } 89 | adapterSingleHeader.bindHeaderSimple("Single adapter title 1") 90 | adapterSingleHeader2.bindHeaderSimple("Single adapter title 2") 91 | binding.bindList( 92 | adapter = adapterPagingProducts, 93 | uiState = vmProduct.state, 94 | onScrollChanged = vmProduct.accept, 95 | ) 96 | } 97 | 98 | private fun FragmentPagingBinding.bindList( 99 | adapter: PagingProductAdapter, 100 | uiState: StateFlow, 101 | onScrollChanged: (ProductPagingUiAction.Scroll) -> Unit 102 | ) { 103 | productRecycler.addOnScrollListener( 104 | object : RecyclerView.OnScrollListener() { 105 | override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { 106 | if (dy != 0) { 107 | onScrollChanged(ProductPagingUiAction.Scroll) 108 | } 109 | } 110 | } 111 | ) 112 | val pagingData = uiState 113 | .map { it.pagingData } 114 | .distinctUntilChanged() 115 | lifecycleScope.launch { 116 | pagingData 117 | .collectLatest { pagingData -> 118 | adapter.submitData(pagingData) 119 | } 120 | } 121 | } 122 | 123 | override fun onDestroyView() { 124 | super.onDestroyView() 125 | _binding = null 126 | } 127 | 128 | companion object { 129 | const val GRID_SPAN_SIZE = 2 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/concatadaptergrid/SampleApp.kt: -------------------------------------------------------------------------------- 1 | package com.example.concatadaptergrid 2 | 3 | import android.app.Application 4 | import dagger.hilt.android.HiltAndroidApp 5 | import timber.log.Timber 6 | 7 | @HiltAndroidApp 8 | class SampleApp : Application() { 9 | override fun onCreate() { 10 | super.onCreate() 11 | Timber.plant(Timber.DebugTree()) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/concatadaptergrid/Tags.kt: -------------------------------------------------------------------------------- 1 | package com.example.concatadaptergrid 2 | 3 | object Tags { 4 | const val INTERNAL = "ConcatGrid" 5 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/concatadaptergrid/adapters/PagingProductAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.example.concatadaptergrid.adapters 2 | 3 | import android.content.Context 4 | import android.view.LayoutInflater 5 | import android.view.ViewGroup 6 | import androidx.paging.PagingDataAdapter 7 | import androidx.recyclerview.widget.DiffUtil 8 | import com.example.concatadaptergrid.ConcatenableAdapter 9 | import com.example.concatadaptergrid.databinding.ItemProductCardBinding 10 | import com.example.concatadaptergrid.databinding.ItemProductHeaderBinding 11 | 12 | class PagingProductAdapter( 13 | context: Context, 14 | override val concatAdapterIndex: Int, 15 | ) : PagingDataAdapter(REPO_COMPARATOR), 16 | ConcatenableAdapter { 17 | 18 | private val inflater = LayoutInflater.from(context) 19 | 20 | override fun onCreateViewHolder(viewGroup: ViewGroup, viewType: Int): ProductVH { 21 | val localViewType = resolveGlobalViewItemType(viewType) 22 | return when (ProductViewItemType.fromItemTypeValue(localViewType)) { 23 | ProductViewItemType.HEADER -> ProductVHHeader( 24 | binding = ItemProductHeaderBinding.inflate( 25 | inflater, 26 | viewGroup, 27 | false 28 | ) 29 | ) 30 | ProductViewItemType.CARD -> ProductVHCard( 31 | binding = ItemProductCardBinding.inflate( 32 | inflater, 33 | viewGroup, 34 | false 35 | ) 36 | ) 37 | } 38 | } 39 | 40 | override fun onBindViewHolder(viewHolder: ProductVH, position: Int) { 41 | val item = getItem(position) 42 | when (viewHolder) { 43 | is ProductVHHeader -> viewHolder.bind(item as ProductItemHeader) 44 | is ProductVHCard -> viewHolder.bind(item as ProductItemCard) 45 | }.javaClass 46 | } 47 | 48 | override fun getItemViewType(position: Int): Int { 49 | val itemViewType = ProductViewItemType 50 | .CARD 51 | .itemTypeValue 52 | return globalViewItemType(itemViewType) 53 | } 54 | 55 | companion object { 56 | private val REPO_COMPARATOR = object : DiffUtil.ItemCallback() { 57 | override fun areItemsTheSame( 58 | oldItem: ProductItemCard, 59 | newItem: ProductItemCard 60 | ): Boolean { 61 | return oldItem.id == newItem.id 62 | } 63 | 64 | override fun areContentsTheSame( 65 | oldItem: ProductItemCard, 66 | newItem: ProductItemCard 67 | ): Boolean = 68 | oldItem == newItem 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/concatadaptergrid/adapters/ProductAdapterBasic.kt: -------------------------------------------------------------------------------- 1 | package com.example.concatadaptergrid.adapters 2 | 3 | import android.content.Context 4 | import android.view.LayoutInflater 5 | import android.view.ViewGroup 6 | import androidx.recyclerview.widget.RecyclerView 7 | import com.example.concatadaptergrid.ConcatenableAdapter 8 | import com.example.concatadaptergrid.databinding.ItemProductCardBinding 9 | import com.example.concatadaptergrid.databinding.ItemProductHeaderBinding 10 | 11 | class ProductAdapterBasic( 12 | private val context: Context, 13 | override val concatAdapterIndex: Int, 14 | ) : RecyclerView.Adapter(), ConcatenableAdapter { 15 | 16 | private val items = mutableListOf() 17 | private val inflater = LayoutInflater.from(context) 18 | 19 | override fun onCreateViewHolder(viewGroup: ViewGroup, viewType: Int): ProductVH { 20 | val localViewType = resolveGlobalViewItemType(viewType) 21 | return when (ProductViewItemType.fromItemTypeValue(localViewType)) { 22 | ProductViewItemType.HEADER -> ProductVHHeader( 23 | binding = ItemProductHeaderBinding.inflate( 24 | inflater, 25 | viewGroup, 26 | false 27 | ) 28 | ) 29 | ProductViewItemType.CARD -> ProductVHCard( 30 | binding = ItemProductCardBinding.inflate( 31 | inflater, 32 | viewGroup, 33 | false 34 | ) 35 | ) 36 | } 37 | } 38 | 39 | override fun getItemViewType(position: Int): Int { 40 | val itemViewType = ProductViewItemType 41 | .toItemType(productItem = items[position]) 42 | .itemTypeValue 43 | return globalViewItemType(itemViewType) 44 | } 45 | 46 | override fun onBindViewHolder(viewHolder: ProductVH, position: Int) { 47 | val item = items[position] 48 | when (viewHolder) { 49 | is ProductVHHeader -> viewHolder.bind(item as ProductItemHeader) 50 | is ProductVHCard -> viewHolder.bind(item as ProductItemCard) 51 | }.javaClass 52 | } 53 | 54 | override fun getItemCount(): Int = items.size 55 | 56 | fun bindProducts( 57 | newProducts: List 58 | ) { 59 | this.items.clear() 60 | this.items.addAll(newProducts) 61 | notifyDataSetChanged() 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/concatadaptergrid/adapters/ProductAdapterSingleHeader.kt: -------------------------------------------------------------------------------- 1 | package com.example.concatadaptergrid.adapters 2 | 3 | import android.content.Context 4 | import android.view.LayoutInflater 5 | import android.view.ViewGroup 6 | import androidx.recyclerview.widget.RecyclerView 7 | import com.example.concatadaptergrid.ConcatenableAdapter 8 | import com.example.concatadaptergrid.databinding.ItemProductHeaderBinding 9 | 10 | /** 11 | * Single item adapter to compose more varied variation of items 12 | */ 13 | class ProductAdapterSingleHeader( 14 | private val context: Context, 15 | override val concatAdapterIndex: Int, 16 | private val gridSpanSize: Int, 17 | ) : RecyclerView.Adapter(), ConcatenableAdapter { 18 | 19 | private val inflater = LayoutInflater.from(context) 20 | private var productItemHeader: ProductItemHeader = ProductItemHeader.asEmpty() 21 | 22 | override fun onCreateViewHolder(viewGroup: ViewGroup, viewType: Int): ViewHolder { 23 | return ViewHolder( 24 | binding = ItemProductHeaderBinding.inflate( 25 | inflater, 26 | viewGroup, 27 | false 28 | ) 29 | ) 30 | } 31 | 32 | override fun onBindViewHolder(viewHolder: ViewHolder, i: Int) { 33 | viewHolder.binding.productItemTitle.text = productItemHeader.title 34 | } 35 | 36 | override fun getItemCount(): Int = 1 37 | 38 | fun bindHeader(productItemHeader: ProductItemHeader) { 39 | this.productItemHeader = productItemHeader 40 | notifyDataSetChanged() 41 | } 42 | 43 | fun bindHeaderSimple(title: String) { 44 | this.productItemHeader = ProductItemHeader(title = title) 45 | notifyDataSetChanged() 46 | } 47 | 48 | override fun getItemViewType(position: Int): Int { 49 | return globalViewItemType() 50 | } 51 | 52 | override fun spanSizeByType(globalItemViewType: Int): Int { 53 | return gridSpanSize 54 | } 55 | 56 | class ViewHolder( 57 | val binding: ItemProductHeaderBinding 58 | ) : RecyclerView.ViewHolder(binding.root) 59 | 60 | } 61 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/concatadaptergrid/adapters/ProductItemCard.kt: -------------------------------------------------------------------------------- 1 | package com.example.concatadaptergrid.adapters 2 | 3 | import com.example.concatadaptergrid.entities.Product 4 | 5 | /** 6 | * View model class to display product in a card 7 | */ 8 | data class ProductItemCard private constructor( 9 | val id: String, 10 | val title: String, 11 | val subtitle: String, 12 | val price: String, 13 | ) : ProductItemVM { 14 | 15 | companion object { 16 | fun from(product: Product): ProductItemCard { 17 | return ProductItemCard( 18 | id = product.id, 19 | title = product.title, 20 | subtitle = "Page(${product.page}) / index(${product.pageItemIndex})", 21 | price = "${product.price} $", 22 | ) 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/concatadaptergrid/adapters/ProductItemFactory.kt: -------------------------------------------------------------------------------- 1 | package com.example.concatadaptergrid.adapters 2 | 3 | import com.example.concatadaptergrid.entities.Product 4 | 5 | object ProductItemFactory { 6 | fun createFromProducts( 7 | products: List 8 | ): List { 9 | return products 10 | .map { ProductItemCard.from(it) } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/concatadaptergrid/adapters/ProductItemHeader.kt: -------------------------------------------------------------------------------- 1 | package com.example.concatadaptergrid.adapters 2 | 3 | /** 4 | * View model class to display product in a header 5 | */ 6 | data class ProductItemHeader( 7 | val title: String, 8 | ) : ProductItemVM { 9 | companion object { 10 | fun asEmpty(): ProductItemHeader { 11 | return ProductItemHeader( 12 | title = "", 13 | ) 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/concatadaptergrid/adapters/ProductItemVM.kt: -------------------------------------------------------------------------------- 1 | package com.example.concatadaptergrid.adapters 2 | 3 | /** 4 | * Represents view model class to display items 5 | */ 6 | interface ProductItemVM 7 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/concatadaptergrid/adapters/ProductVH.kt: -------------------------------------------------------------------------------- 1 | package com.example.concatadaptergrid.adapters 2 | 3 | import android.view.View 4 | import androidx.recyclerview.widget.RecyclerView 5 | import com.example.concatadaptergrid.databinding.ItemProductCardBinding 6 | import com.example.concatadaptergrid.databinding.ItemProductHeaderBinding 7 | import java.lang.IllegalArgumentException 8 | 9 | enum class ProductViewItemType(val itemTypeValue: Int) { 10 | HEADER(0), 11 | CARD(1), 12 | ; 13 | 14 | companion object { 15 | fun toItemType(productItem: ProductItemVM): ProductViewItemType { 16 | return when (productItem) { 17 | is ProductItemHeader -> HEADER 18 | is ProductItemCard -> CARD 19 | else -> throw IllegalArgumentException("Item card is not supported by the adapter") 20 | } 21 | } 22 | 23 | fun fromItemTypeValue(itemTypeValue: Int): ProductViewItemType { 24 | return values().first { it.itemTypeValue == itemTypeValue } 25 | } 26 | } 27 | } 28 | 29 | sealed class ProductVH( 30 | itemView: View, 31 | ) : RecyclerView.ViewHolder(itemView) 32 | 33 | class ProductVHHeader( 34 | val binding: ItemProductHeaderBinding 35 | ) : ProductVH(binding.root) { 36 | fun bind(productItem: ProductItemHeader) { 37 | binding.productItemTitle.text = productItem.title 38 | } 39 | } 40 | 41 | class ProductVHCard( 42 | val binding: ItemProductCardBinding 43 | ) : ProductVH(binding.root) { 44 | fun bind(productItem: ProductItemCard) { 45 | binding.productItemTitle.text = productItem.title 46 | binding.productItemSubtitle.text = productItem.subtitle 47 | binding.productItemPrice.text = productItem.price 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/concatadaptergrid/entities/Product.kt: -------------------------------------------------------------------------------- 1 | package com.example.concatadaptergrid.entities 2 | 3 | /** 4 | * Mock up 'Product' item 5 | */ 6 | data class Product( 7 | val id: String, 8 | val title: String, 9 | val page: Int, 10 | val pageItemIndex: Int, 11 | val price: Double, 12 | ) 13 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/concatadaptergrid/modules/RepositoryModule.kt: -------------------------------------------------------------------------------- 1 | package com.example.concatadaptergrid.modules 2 | 3 | import com.example.concatadaptergrid.repositories.ProductRepository 4 | import dagger.Module 5 | import dagger.Provides 6 | import dagger.hilt.InstallIn 7 | import dagger.hilt.components.SingletonComponent 8 | import javax.inject.Singleton 9 | 10 | @Module() 11 | @InstallIn(SingletonComponent::class) 12 | class RepositoryModule { 13 | 14 | @Provides 15 | @Singleton 16 | fun provideProductRepository(): ProductRepository { 17 | return ProductRepository() 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/concatadaptergrid/paging/ProductPagingSource.kt: -------------------------------------------------------------------------------- 1 | package com.example.concatadaptergrid.paging 2 | 3 | import androidx.paging.PagingSource 4 | import androidx.paging.PagingState 5 | import com.example.concatadaptergrid.Tags 6 | import com.example.concatadaptergrid.entities.Product 7 | import com.example.concatadaptergrid.repositories.ProductRepository 8 | import timber.log.Timber 9 | import java.io.IOException 10 | 11 | class ProductPagingSource( 12 | private val productRepository: ProductRepository, 13 | ) : PagingSource() { 14 | 15 | override suspend fun load(params: LoadParams): LoadResult { 16 | val page = params.key ?: START_PAGE 17 | return try { 18 | Timber.tag(Tags.INTERNAL).d("Fetching products: page($page) / limit(${params.loadSize})") 19 | val products: List = productRepository.fetchProducts( 20 | page = page, 21 | limit = params.loadSize, 22 | ) 23 | Timber.tag(Tags.INTERNAL).d("Products: $products") 24 | LoadResult.Page( 25 | data = products, 26 | prevKey = prevKey(currentPage = page), 27 | nextKey = nextKey( 28 | currentPage = page, 29 | currentLoadSize = products.size, 30 | requestedItemsToLoad = params.loadSize 31 | ) 32 | ) 33 | } catch (exception: IOException) { 34 | return LoadResult.Error(exception) 35 | } 36 | } 37 | 38 | /** 39 | * The refresh key is used for subsequent refresh calls to PagingSource.load after the initial load 40 | * We need to get the previous key (or next key if previous is null) of the page 41 | * that was closest to the most recently accessed index. 42 | * Anchor position is the most recently accessed index 43 | */ 44 | override fun getRefreshKey(state: PagingState): Int? { 45 | return state.anchorPosition?.let { anchorPosition -> 46 | state.closestPageToPosition(anchorPosition)?.prevKey?.plus(1) 47 | ?: state.closestPageToPosition(anchorPosition)?.nextKey?.minus(1) 48 | } 49 | } 50 | 51 | companion object { 52 | const val START_PAGE = 1 53 | const val PAGE_SIZE = 20 54 | 55 | fun nextKey( 56 | currentPage: Int, 57 | currentLoadSize: Int, 58 | requestedItemsToLoad: Int 59 | ): Int? { 60 | return if (currentLoadSize == 0) { 61 | null 62 | } else { 63 | // initial load size = 3 * NETWORK_PAGE_SIZE 64 | // ensure we're not requesting duplicating items, at the 2nd request 65 | currentPage + (requestedItemsToLoad / PAGE_SIZE) 66 | } 67 | } 68 | 69 | fun prevKey( 70 | currentPage: Int 71 | ): Int? { 72 | return if (currentPage == START_PAGE) { 73 | null 74 | } else { 75 | currentPage - 1 76 | } 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/concatadaptergrid/repositories/ProductRepository.kt: -------------------------------------------------------------------------------- 1 | package com.example.concatadaptergrid.repositories 2 | 3 | import androidx.paging.Pager 4 | import androidx.paging.PagingConfig 5 | import androidx.paging.PagingData 6 | import com.example.concatadaptergrid.entities.Product 7 | import com.example.concatadaptergrid.paging.ProductPagingSource 8 | import kotlinx.coroutines.flow.Flow 9 | import kotlin.random.Random 10 | 11 | class ProductRepository { 12 | suspend fun fetchProducts( 13 | limit: Int, 14 | page: Int 15 | ): List { 16 | return (page..(page + limit)).map { index -> 17 | Product( 18 | id = "${page}_$index", 19 | title = "Product", 20 | page = page, 21 | pageItemIndex = index, 22 | price = Random.nextInt(0, 1000).toDouble() 23 | ) 24 | } 25 | } 26 | 27 | fun pagingProductStream(): Flow> { 28 | return Pager( 29 | config = PagingConfig( 30 | pageSize = ProductPagingSource.PAGE_SIZE, 31 | enablePlaceholders = false 32 | ), 33 | pagingSourceFactory = { 34 | ProductPagingSource(this) 35 | } 36 | ).flow 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/concatadaptergrid/viewmodels/ProductPagingViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.example.concatadaptergrid.viewmodels 2 | 3 | import androidx.lifecycle.LiveData 4 | import androidx.lifecycle.MutableLiveData 5 | import androidx.lifecycle.ViewModel 6 | import androidx.lifecycle.viewModelScope 7 | import androidx.paging.PagingData 8 | import androidx.paging.cachedIn 9 | import androidx.paging.map 10 | import com.example.concatadaptergrid.adapters.ProductItemCard 11 | import com.example.concatadaptergrid.entities.Product 12 | import com.example.concatadaptergrid.repositories.ProductRepository 13 | import dagger.hilt.android.lifecycle.HiltViewModel 14 | import kotlinx.coroutines.flow.* 15 | import kotlinx.coroutines.launch 16 | import javax.inject.Inject 17 | 18 | @HiltViewModel 19 | class ProductPagingViewModel @Inject constructor( 20 | private val productRepository: ProductRepository, 21 | ) : ViewModel() { 22 | 23 | private val _ldState = MutableLiveData() 24 | val ldState: LiveData 25 | get() = _ldState 26 | 27 | val accept: (ProductPagingUiAction) -> Unit 28 | val state: StateFlow 29 | 30 | init { 31 | val actionStateFlow = MutableSharedFlow() 32 | accept = { action -> 33 | viewModelScope.launch { actionStateFlow.emit(action) } 34 | } 35 | val searches = actionStateFlow 36 | .filterIsInstance() 37 | .distinctUntilChanged() 38 | .onStart { emit(ProductPagingUiAction.Search) } 39 | val queriesScrolled = actionStateFlow 40 | .filterIsInstance() 41 | .distinctUntilChanged() 42 | // This is shared to keep the flow "hot" while caching the last query scrolled, 43 | // otherwise each flatMapLatest invocation would lose the last query scrolled, 44 | .shareIn( 45 | scope = viewModelScope, 46 | started = SharingStarted.WhileSubscribed(stopTimeoutMillis = 5000), 47 | replay = 1 48 | ) 49 | .onStart { emit(ProductPagingUiAction.Scroll) } 50 | state = searches 51 | .flatMapLatest { search -> 52 | combine( 53 | queriesScrolled, 54 | searchProducts(), 55 | ::Pair 56 | ) 57 | }.map { (scroll, pagingData) -> 58 | val pagingProductItems = pagingData 59 | .map { ProductItemCard.from(it) } 60 | PPUIStateSuccess(_pagingData = pagingProductItems) 61 | }.stateIn( 62 | scope = viewModelScope, 63 | started = SharingStarted.WhileSubscribed(stopTimeoutMillis = 5000), 64 | initialValue = PPUIStateLoading 65 | ) 66 | } 67 | 68 | private fun searchProducts(): Flow> = 69 | productRepository.pagingProductStream() 70 | .cachedIn(viewModelScope) 71 | } 72 | 73 | sealed class PPUiState( 74 | val pagingData: PagingData = PagingData.empty() 75 | ) 76 | object PPUIStateLoading : PPUiState() 77 | data class PPUIStateSuccess( 78 | private val _pagingData: PagingData 79 | ) : PPUiState(_pagingData) 80 | 81 | data class PPUIStateError(val error: String) : PPUiState() 82 | 83 | sealed class ProductPagingUiAction { 84 | object Search : ProductPagingUiAction() 85 | object Scroll : ProductPagingUiAction() 86 | } 87 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/concatadaptergrid/viewmodels/ProductSimpleViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.example.concatadaptergrid.viewmodels 2 | 3 | import androidx.lifecycle.LiveData 4 | import androidx.lifecycle.MutableLiveData 5 | import androidx.lifecycle.ViewModel 6 | import androidx.lifecycle.viewModelScope 7 | import com.example.concatadaptergrid.entities.Product 8 | import com.example.concatadaptergrid.repositories.ProductRepository 9 | import dagger.hilt.android.lifecycle.HiltViewModel 10 | import kotlinx.coroutines.launch 11 | import javax.inject.Inject 12 | 13 | @HiltViewModel 14 | class ProductSimpleViewModel @Inject constructor( 15 | val productRepository: ProductRepository 16 | ) : ViewModel() { 17 | 18 | private val _ldState = MutableLiveData() 19 | val ldState: LiveData 20 | get() = _ldState 21 | 22 | fun fetchProducts() { 23 | viewModelScope.launch { 24 | _ldState.value = ProductSimpleUIStateLoading 25 | try { 26 | val products1 = productRepository.fetchProducts( 27 | limit = 20, 28 | page = 1 29 | ) 30 | val products2 = productRepository.fetchProducts( 31 | limit = 30, 32 | page = 2 33 | ) 34 | _ldState.value = ProductSimpleUIStateSuccess(products1, products2) 35 | } catch (e: Exception) { 36 | _ldState.value = ProductSimpleUIStateError(e.message ?: "error") 37 | } 38 | } 39 | } 40 | } 41 | 42 | sealed class ProductSimpleUIState 43 | object ProductSimpleUIStateLoading : ProductSimpleUIState() 44 | data class ProductSimpleUIStateSuccess( 45 | val products1: List, 46 | val products2: List, 47 | ) : ProductSimpleUIState() 48 | data class ProductSimpleUIStateError(val error: String) : ProductSimpleUIState() 49 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 13 | 14 | 20 | 21 | 22 | 23 | 26 | 27 | 33 | 34 | -------------------------------------------------------------------------------- /app/src/main/res/layout/content_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 19 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_multiadapter.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_paging.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_second.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 17 | 18 |