├── .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 |
5 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
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 |
27 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/item_product_card.xml:
--------------------------------------------------------------------------------
1 |
2 |
12 |
13 |
16 |
17 |
28 |
29 |
38 |
39 |
49 |
50 |
61 |
62 |
73 |
74 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/item_product_header.xml:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/app/src/main/res/menu/bottom_navigation_menu.xml:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/src/main/res/menu/menu_main.xml:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marius-m/android-concat-grid/2c26bcccff5f1930680a1042b3ae3581d2eb2c0a/app/src/main/res/mipmap-hdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marius-m/android-concat-grid/2c26bcccff5f1930680a1042b3ae3581d2eb2c0a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marius-m/android-concat-grid/2c26bcccff5f1930680a1042b3ae3581d2eb2c0a/app/src/main/res/mipmap-mdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marius-m/android-concat-grid/2c26bcccff5f1930680a1042b3ae3581d2eb2c0a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marius-m/android-concat-grid/2c26bcccff5f1930680a1042b3ae3581d2eb2c0a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marius-m/android-concat-grid/2c26bcccff5f1930680a1042b3ae3581d2eb2c0a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marius-m/android-concat-grid/2c26bcccff5f1930680a1042b3ae3581d2eb2c0a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marius-m/android-concat-grid/2c26bcccff5f1930680a1042b3ae3581d2eb2c0a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marius-m/android-concat-grid/2c26bcccff5f1930680a1042b3ae3581d2eb2c0a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marius-m/android-concat-grid/2c26bcccff5f1930680a1042b3ae3581d2eb2c0a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/navigation/nav_graph.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
13 |
19 |
20 |
25 |
31 |
32 |
--------------------------------------------------------------------------------
/app/src/main/res/values-land/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 | 48dp
3 |
--------------------------------------------------------------------------------
/app/src/main/res/values-night/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
16 |
--------------------------------------------------------------------------------
/app/src/main/res/values-w1240dp/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 | 200dp
3 |
--------------------------------------------------------------------------------
/app/src/main/res/values-w600dp/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 | 48dp
3 |
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #FFBB86FC
4 | #FF6200EE
5 | #FF3700B3
6 | #FF03DAC5
7 | #FF018786
8 | #FF000000
9 | #FFFFFFFF
10 |
--------------------------------------------------------------------------------
/app/src/main/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 | 16dp
3 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | ConcatAdapterGrid
3 | Settings
4 |
5 | First Fragment
6 | Second Fragment
7 | Next
8 | Previous
9 |
10 | Hello first fragment
11 | Hello second fragment. Arg: %1$s
12 |
13 | Paging
14 | Multi-adapter
15 |
--------------------------------------------------------------------------------
/app/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
16 |
17 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/app/src/test/java/com/example/concatadaptergrid/ConcatenableAdapterConcateViewItemTypeTest.kt:
--------------------------------------------------------------------------------
1 | package com.example.concatadaptergrid
2 |
3 | import org.assertj.core.api.Assertions
4 | import org.junit.Test
5 |
6 | class ConcatenableAdapterConcateViewItemTypeTest {
7 |
8 | @Test
9 | fun adapter0_item0() {
10 | // Assemble
11 | val adapter = MockAdapter1(concatAdapterIndex = 0)
12 |
13 | // Act
14 | val result = adapter.globalViewItemType(localItemViewType = 0)
15 |
16 | // Assert
17 | Assertions.assertThat(result).isEqualTo(100)
18 | }
19 |
20 | @Test
21 | fun adapter0_item1() {
22 | // Assemble
23 | val adapter = MockAdapter1(concatAdapterIndex = 0)
24 |
25 | // Act
26 | val result = adapter.globalViewItemType(localItemViewType = 1)
27 |
28 | // Assert
29 | Assertions.assertThat(result).isEqualTo(101)
30 | }
31 |
32 | @Test
33 | fun adapter1_item0() {
34 | // Assemble
35 | val adapter = MockAdapter1(concatAdapterIndex = 1)
36 |
37 | // Act
38 | val result = adapter.globalViewItemType(localItemViewType = 0)
39 |
40 | // Assert
41 | Assertions.assertThat(result).isEqualTo(200)
42 | }
43 |
44 | @Test
45 | fun adapter1_item1() {
46 | // Assemble
47 | val adapter = MockAdapter1(concatAdapterIndex = 1)
48 |
49 | // Act
50 | val result = adapter.globalViewItemType(localItemViewType = 1)
51 |
52 | // Assert
53 | Assertions.assertThat(result).isEqualTo(201)
54 | }
55 |
56 | @Test
57 | fun adapter2_item0() {
58 | // Assemble
59 | val adapter = MockAdapter1(concatAdapterIndex = 2)
60 |
61 | // Act
62 | val result = adapter.globalViewItemType(localItemViewType = 0)
63 |
64 | // Assert
65 | Assertions.assertThat(result).isEqualTo(300)
66 | }
67 |
68 | @Test
69 | fun adapter2_item1() {
70 | // Assemble
71 | val adapter = MockAdapter1(concatAdapterIndex = 2)
72 |
73 | // Act
74 | val result = adapter.globalViewItemType(localItemViewType = 1)
75 |
76 | // Assert
77 | Assertions.assertThat(result).isEqualTo(301)
78 | }
79 |
80 | class MockAdapter1(
81 | override val concatAdapterIndex: Int
82 | ) : ConcatenableAdapter
83 | }
84 |
--------------------------------------------------------------------------------
/app/src/test/java/com/example/concatadaptergrid/ConcatenableAdapterHasGlobalItemViewTypeTest.kt:
--------------------------------------------------------------------------------
1 | package com.example.concatadaptergrid
2 |
3 | import org.assertj.core.api.Assertions
4 | import org.junit.Test
5 |
6 | class ConcatenableAdapterHasGlobalItemViewTypeTest {
7 |
8 | @Test
9 | fun adapter0_firstItem() {
10 | // Assemble
11 | val adapter = MockAdapter1(concatAdapterIndex = 0)
12 |
13 | // Act
14 | val result = adapter.hasGlobalViewItemType(globalItemViewType = 100)
15 |
16 | // Assert
17 | Assertions.assertThat(result).isTrue()
18 | }
19 |
20 | @Test
21 | fun adapter0_item1() {
22 | // Assemble
23 | val adapter = MockAdapter1(concatAdapterIndex = 0)
24 |
25 | // Act
26 | val result = adapter.hasGlobalViewItemType(globalItemViewType = 101)
27 |
28 | // Assert
29 | Assertions.assertThat(result).isTrue()
30 | }
31 |
32 | @Test
33 | fun adapter0_lastItem() {
34 | // Assemble
35 | val adapter = MockAdapter1(concatAdapterIndex = 0)
36 |
37 | // Act
38 | val result = adapter.hasGlobalViewItemType(globalItemViewType = 199)
39 |
40 | // Assert
41 | Assertions.assertThat(result).isTrue()
42 | }
43 |
44 | @Test
45 | fun incorrectItemType_adapter0() {
46 | // Assemble
47 | val adapter = MockAdapter1(concatAdapterIndex = 0)
48 |
49 | // Act
50 | val result = adapter.hasGlobalViewItemType(globalItemViewType = 200)
51 |
52 | // Assert
53 | Assertions.assertThat(result).isFalse()
54 | }
55 |
56 | @Test
57 | fun adapter1_firstItem() {
58 | // Assemble
59 | val adapter = MockAdapter1(concatAdapterIndex = 1)
60 |
61 | // Act
62 | val result = adapter.hasGlobalViewItemType(globalItemViewType = 200)
63 |
64 | // Assert
65 | Assertions.assertThat(result).isTrue()
66 | }
67 |
68 | @Test
69 | fun adapter1_item1() {
70 | // Assemble
71 | val adapter = MockAdapter1(concatAdapterIndex = 1)
72 |
73 | // Act
74 | val result = adapter.hasGlobalViewItemType(globalItemViewType = 201)
75 |
76 | // Assert
77 | Assertions.assertThat(result).isTrue()
78 | }
79 |
80 | @Test
81 | fun adapter1_lastItem() {
82 | // Assemble
83 | val adapter = MockAdapter1(concatAdapterIndex = 1)
84 |
85 | // Act
86 | val result = adapter.hasGlobalViewItemType(globalItemViewType = 299)
87 |
88 | // Assert
89 | Assertions.assertThat(result).isTrue()
90 | }
91 |
92 | @Test
93 | fun incorrectItemType_adapter1() {
94 | // Assemble
95 | val adapter = MockAdapter1(concatAdapterIndex = 1)
96 |
97 | // Act
98 | val result = adapter.hasGlobalViewItemType(globalItemViewType = 300)
99 |
100 | // Assert
101 | Assertions.assertThat(result).isFalse()
102 | }
103 |
104 | class MockAdapter1(
105 | override val concatAdapterIndex: Int
106 | ) : ConcatenableAdapter
107 | }
108 |
--------------------------------------------------------------------------------
/app/src/test/java/com/example/concatadaptergrid/ConcatenableAdapterResolveViewItemTypeTest.kt:
--------------------------------------------------------------------------------
1 | package com.example.concatadaptergrid
2 |
3 | import org.assertj.core.api.Assertions
4 | import org.junit.Test
5 |
6 | class ConcatenableAdapterResolveViewItemTypeTest {
7 |
8 | @Test
9 | fun adapter0_item0() {
10 | // Assemble
11 | val adapter = MockAdapter1(concatAdapterIndex = 0)
12 |
13 | // Act
14 | val result = adapter.resolveGlobalViewItemType(globalItemViewType = 100)
15 |
16 | // Assert
17 | Assertions.assertThat(result).isEqualTo(0)
18 | }
19 |
20 | @Test
21 | fun adapter0_item1() {
22 | // Assemble
23 | val adapter = MockAdapter1(concatAdapterIndex = 0)
24 |
25 | // Act
26 | val result = adapter.resolveGlobalViewItemType(globalItemViewType = 101)
27 |
28 | // Assert
29 | Assertions.assertThat(result).isEqualTo(1)
30 | }
31 |
32 | @Test
33 | fun adapter1_item0() {
34 | // Assemble
35 | val adapter = MockAdapter1(concatAdapterIndex = 1)
36 |
37 | // Act
38 | val result = adapter.resolveGlobalViewItemType(globalItemViewType = 200)
39 |
40 | // Assert
41 | Assertions.assertThat(result).isEqualTo(0)
42 | }
43 |
44 | @Test
45 | fun adapter1_item1() {
46 | // Assemble
47 | val adapter = MockAdapter1(concatAdapterIndex = 1)
48 |
49 | // Act
50 | val result = adapter.resolveGlobalViewItemType(globalItemViewType = 201)
51 |
52 | // Assert
53 | Assertions.assertThat(result).isEqualTo(1)
54 | }
55 |
56 | @Test
57 | fun adapter2_item0() {
58 | // Assemble
59 | val adapter = MockAdapter1(concatAdapterIndex = 2)
60 |
61 | // Act
62 | val result = adapter.resolveGlobalViewItemType(globalItemViewType = 300)
63 |
64 | // Assert
65 | Assertions.assertThat(result).isEqualTo(0)
66 | }
67 |
68 | @Test
69 | fun adapter2_item1() {
70 | // Assemble
71 | val adapter = MockAdapter1(concatAdapterIndex = 2)
72 |
73 | // Act
74 | val result = adapter.resolveGlobalViewItemType(globalItemViewType = 301)
75 |
76 | // Assert
77 | Assertions.assertThat(result).isEqualTo(1)
78 | }
79 |
80 | class MockAdapter1(
81 | override val concatAdapterIndex: Int
82 | ) : ConcatenableAdapter
83 | }
84 |
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 | buildscript {
3 | repositories {
4 | google()
5 | mavenCentral()
6 | }
7 | dependencies {
8 | classpath "com.android.tools.build:gradle:7.0.0"
9 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.21"
10 | classpath 'com.google.dagger:hilt-android-gradle-plugin:2.38.1'
11 | classpath "androidx.navigation:navigation-safe-args-gradle-plugin:2.3.5"
12 |
13 | // NOTE: Do not place your application dependencies here; they belong
14 | // in the individual module build.gradle files
15 | }
16 | }
17 |
18 | task clean(type: Delete) {
19 | delete rootProject.buildDir
20 | }
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app"s APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Automatically convert third-party libraries to use AndroidX
19 | android.enableJetifier=true
20 | # Kotlin code style for this project: "official" or "obsolete":
21 | kotlin.code.style=official
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marius-m/android-concat-grid/2c26bcccff5f1930680a1042b3ae3581d2eb2c0a/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Mon Sep 20 11:30:39 EEST 2021
2 | distributionBase=GRADLE_USER_HOME
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-bin.zip
4 | distributionPath=wrapper/dists
5 | zipStorePath=wrapper/dists
6 | zipStoreBase=GRADLE_USER_HOME
7 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | #
4 | # Copyright 2015 the original author or authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | ##
21 | ## Gradle start up script for UN*X
22 | ##
23 | ##############################################################################
24 |
25 | # Attempt to set APP_HOME
26 | # Resolve links: $0 may be a link
27 | PRG="$0"
28 | # Need this for relative symlinks.
29 | while [ -h "$PRG" ] ; do
30 | ls=`ls -ld "$PRG"`
31 | link=`expr "$ls" : '.*-> \(.*\)$'`
32 | if expr "$link" : '/.*' > /dev/null; then
33 | PRG="$link"
34 | else
35 | PRG=`dirname "$PRG"`"/$link"
36 | fi
37 | done
38 | SAVED="`pwd`"
39 | cd "`dirname \"$PRG\"`/" >/dev/null
40 | APP_HOME="`pwd -P`"
41 | cd "$SAVED" >/dev/null
42 |
43 | APP_NAME="Gradle"
44 | APP_BASE_NAME=`basename "$0"`
45 |
46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
48 |
49 | # Use the maximum available, or set MAX_FD != -1 to use that value.
50 | MAX_FD="maximum"
51 |
52 | warn () {
53 | echo "$*"
54 | }
55 |
56 | die () {
57 | echo
58 | echo "$*"
59 | echo
60 | exit 1
61 | }
62 |
63 | # OS specific support (must be 'true' or 'false').
64 | cygwin=false
65 | msys=false
66 | darwin=false
67 | nonstop=false
68 | case "`uname`" in
69 | CYGWIN* )
70 | cygwin=true
71 | ;;
72 | Darwin* )
73 | darwin=true
74 | ;;
75 | MINGW* )
76 | msys=true
77 | ;;
78 | NONSTOP* )
79 | nonstop=true
80 | ;;
81 | esac
82 |
83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
84 |
85 |
86 | # Determine the Java command to use to start the JVM.
87 | if [ -n "$JAVA_HOME" ] ; then
88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
89 | # IBM's JDK on AIX uses strange locations for the executables
90 | JAVACMD="$JAVA_HOME/jre/sh/java"
91 | else
92 | JAVACMD="$JAVA_HOME/bin/java"
93 | fi
94 | if [ ! -x "$JAVACMD" ] ; then
95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
96 |
97 | Please set the JAVA_HOME variable in your environment to match the
98 | location of your Java installation."
99 | fi
100 | else
101 | JAVACMD="java"
102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
103 |
104 | Please set the JAVA_HOME variable in your environment to match the
105 | location of your Java installation."
106 | fi
107 |
108 | # Increase the maximum file descriptors if we can.
109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
110 | MAX_FD_LIMIT=`ulimit -H -n`
111 | if [ $? -eq 0 ] ; then
112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
113 | MAX_FD="$MAX_FD_LIMIT"
114 | fi
115 | ulimit -n $MAX_FD
116 | if [ $? -ne 0 ] ; then
117 | warn "Could not set maximum file descriptor limit: $MAX_FD"
118 | fi
119 | else
120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
121 | fi
122 | fi
123 |
124 | # For Darwin, add options to specify how the application appears in the dock
125 | if $darwin; then
126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
127 | fi
128 |
129 | # For Cygwin or MSYS, switch paths to Windows format before running java
130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
133 |
134 | JAVACMD=`cygpath --unix "$JAVACMD"`
135 |
136 | # We build the pattern for arguments to be converted via cygpath
137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
138 | SEP=""
139 | for dir in $ROOTDIRSRAW ; do
140 | ROOTDIRS="$ROOTDIRS$SEP$dir"
141 | SEP="|"
142 | done
143 | OURCYGPATTERN="(^($ROOTDIRS))"
144 | # Add a user-defined pattern to the cygpath arguments
145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
147 | fi
148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
149 | i=0
150 | for arg in "$@" ; do
151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
153 |
154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
156 | else
157 | eval `echo args$i`="\"$arg\""
158 | fi
159 | i=`expr $i + 1`
160 | done
161 | case $i in
162 | 0) set -- ;;
163 | 1) set -- "$args0" ;;
164 | 2) set -- "$args0" "$args1" ;;
165 | 3) set -- "$args0" "$args1" "$args2" ;;
166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
172 | esac
173 | fi
174 |
175 | # Escape application args
176 | save () {
177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
178 | echo " "
179 | }
180 | APP_ARGS=`save "$@"`
181 |
182 | # Collect all arguments for the java command, following the shell quoting and substitution rules
183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
184 |
185 | exec "$JAVACMD" "$@"
186 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if "%ERRORLEVEL%"=="0" goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
84 | exit /b 1
85 |
86 | :mainEnd
87 | if "%OS%"=="Windows_NT" endlocal
88 |
89 | :omega
90 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | dependencyResolutionManagement {
2 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
3 | repositories {
4 | google()
5 | mavenCentral()
6 | jcenter() // Warning: this repository is going to shut down soon
7 | }
8 | }
9 | rootProject.name = "ConcatAdapterGrid"
10 | include ':app'
11 |
--------------------------------------------------------------------------------