├── .gitignore ├── .idea ├── codeStyles │ ├── Project.xml │ └── codeStyleConfig.xml ├── misc.xml └── runConfigurations.xml ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── example │ │ └── mvrxpaged │ │ ├── Sample.kt │ │ ├── SampleApplication.kt │ │ ├── data │ │ └── MainRepositoryImpl.kt │ │ ├── di │ │ └── ActivityScope.kt │ │ ├── domain │ │ ├── entity │ │ │ ├── BannerData.kt │ │ │ ├── CategoryData.kt │ │ │ ├── DealData.kt │ │ │ └── MainViewType.kt │ │ ├── interactor │ │ │ ├── GetBanner.kt │ │ │ ├── GetCategory.kt │ │ │ ├── GetDeal.kt │ │ │ └── GetMainLayout.kt │ │ └── repository │ │ │ └── MainRepository.kt │ │ └── ui │ │ ├── Extension.kt │ │ ├── Holder.kt │ │ ├── OnClick.kt │ │ ├── main │ │ ├── GetMainModelPagedListStream.kt │ │ ├── ItemViewModel.kt │ │ ├── MainActivity.kt │ │ ├── MainActivityModule.kt │ │ ├── MainAdapter.kt │ │ ├── MainArgs.kt │ │ ├── MainEpoxyModelDataSource.kt │ │ ├── MainFragment.kt │ │ ├── MainFragmentModule.kt │ │ ├── MainViewModel.kt │ │ └── view │ │ │ ├── BannerView.kt │ │ │ ├── CategoryView.kt │ │ │ ├── DealView.kt │ │ │ ├── LoadingView.kt │ │ │ └── SeparatorView.kt │ │ └── select │ │ └── SelectActivity.kt │ └── res │ ├── drawable-v24 │ └── ic_launcher_foreground.xml │ ├── drawable │ └── ic_launcher_background.xml │ ├── layout │ ├── banner_view.xml │ ├── category_view.xml │ ├── deal_view.xml │ ├── loading_view.xml │ ├── main_activity.xml │ ├── main_fragment.xml │ ├── select_activity.xml │ └── separator_view.xml │ ├── mipmap-anydpi-v26 │ ├── ic_launcher.xml │ └── ic_launcher_round.xml │ ├── mipmap-hdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-mdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xxhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xxxhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ └── values │ ├── colors.xml │ ├── strings.xml │ └── styles.xml ├── 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 | 16 | # Built application files 17 | *.apk 18 | *.ap_ 19 | *.aab 20 | 21 | # Files for the ART/Dalvik VM 22 | *.dex 23 | 24 | # Java class files 25 | *.class 26 | 27 | # Generated files 28 | bin/ 29 | gen/ 30 | out/ 31 | release/ 32 | 33 | # Gradle files 34 | .gradle/ 35 | build/ 36 | 37 | # Local configuration file (sdk path, etc) 38 | local.properties 39 | 40 | # Proguard folder generated by Eclipse 41 | proguard/ 42 | 43 | # Log Files 44 | *.log 45 | 46 | # Android Studio Navigation editor temp files 47 | .navigation/ 48 | 49 | # Android Studio captures folder 50 | captures/ 51 | 52 | # IntelliJ 53 | *.iml 54 | .idea/workspace.xml 55 | .idea/tasks.xml 56 | .idea/gradle.xml 57 | .idea/assetWizardSettings.xml 58 | .idea/dictionaries 59 | .idea/libraries 60 | # Android Studio 3 in .gitignore file. 61 | .idea/caches 62 | .idea/modules.xml 63 | # Comment next line if keeping position of elements in Navigation Editor is relevant for you 64 | .idea/navEditor.xml 65 | 66 | # Keystore files 67 | # Uncomment the following lines if you do not want to check your keystore files in. 68 | #*.jks 69 | #*.keystore 70 | 71 | # External native build folder generated in Android Studio 2.2 and later 72 | .externalNativeBuild 73 | 74 | # Google Services (e.g. APIs or Firebase) 75 | # google-services.json 76 | 77 | # Freeline 78 | freeline.py 79 | freeline/ 80 | freeline_project_description.json 81 | 82 | # fastlane 83 | fastlane/report.xml 84 | fastlane/Preview.html 85 | fastlane/screenshots 86 | fastlane/test_output 87 | fastlane/readme.md 88 | 89 | # Version control 90 | vcs.xml 91 | 92 | # lint 93 | lint/intermediates/ 94 | lint/generated/ 95 | lint/outputs/ 96 | lint/tmp/ 97 | # lint/reports/ -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 9 | 10 | -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 9 | -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | apply plugin: 'kotlin-android' 4 | 5 | apply plugin: 'kotlin-android-extensions' 6 | apply plugin: 'kotlin-kapt' 7 | apply plugin: 'com.jakewharton.butterknife' 8 | android { 9 | compileSdkVersion 28 10 | defaultConfig { 11 | applicationId "com.example.mvrxpaged" 12 | minSdkVersion 21 13 | targetSdkVersion 28 14 | versionCode 1 15 | versionName "1.0" 16 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 17 | } 18 | buildTypes { 19 | release { 20 | minifyEnabled false 21 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 22 | } 23 | } 24 | } 25 | 26 | dependencies { 27 | implementation fileTree(dir: 'libs', include: ['*.jar']) 28 | implementation"org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 29 | implementation 'androidx.appcompat:appcompat:1.0.2' 30 | implementation 'androidx.core:core-ktx:1.0.1' 31 | implementation 'androidx.constraintlayout:constraintlayout:1.1.3' 32 | testImplementation 'junit:junit:4.12' 33 | androidTestImplementation 'androidx.test.ext:junit:1.1.0' 34 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' 35 | 36 | 37 | kapt 'com.google.dagger:dagger-compiler:2.23.1' 38 | implementation 'com.google.dagger:dagger-android-support:2.23.1' 39 | kapt 'com.google.dagger:dagger-android-processor:2.23.1' 40 | implementation 'com.airbnb.android:mvrx:1.0.1' 41 | implementation 'com.airbnb.android:epoxy:3.5.0' 42 | implementation 'com.airbnb.android:epoxy-paging:3.5.0' 43 | kapt 'com.airbnb.android:epoxy-processor:3.5.0' 44 | implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0-alpha01' 45 | implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.2.0-alpha01' 46 | implementation 'androidx.paging:paging-runtime-ktx:2.1.0' 47 | implementation 'androidx.paging:paging-rxjava2-ktx:2.1.0' 48 | implementation 'androidx.fragment:fragment-ktx:1.1.0-alpha09' 49 | 50 | } 51 | -------------------------------------------------------------------------------- /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 22 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/mvrxpaged/Sample.kt: -------------------------------------------------------------------------------- 1 | package com.example.mvrxpaged 2 | 3 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/mvrxpaged/SampleApplication.kt: -------------------------------------------------------------------------------- 1 | package com.example.mvrxpaged 2 | 3 | import com.example.mvrxpaged.data.MainRepositoryImpl 4 | import com.example.mvrxpaged.di.ApplicationScope 5 | import com.example.mvrxpaged.domain.repository.MainRepository 6 | import com.example.mvrxpaged.ui.main.MainActivityModule 7 | import dagger.Binds 8 | import dagger.Module 9 | import dagger.android.AndroidInjector 10 | import dagger.android.support.AndroidSupportInjectionModule 11 | import dagger.android.support.DaggerApplication 12 | 13 | class SampleApplication : DaggerApplication() { 14 | override fun applicationInjector(): AndroidInjector { 15 | return DaggerSampleApplication_Component.create() 16 | } 17 | 18 | 19 | @dagger.Component( 20 | modules = [ 21 | AndroidSupportInjectionModule::class, 22 | MainActivityModule::class, 23 | Component.Binding::class 24 | ] 25 | ) 26 | @ApplicationScope 27 | interface Component : AndroidInjector { 28 | 29 | @Module 30 | interface Binding { 31 | @Binds 32 | fun mainRepo(impl: MainRepositoryImpl): MainRepository 33 | } 34 | } 35 | 36 | 37 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/mvrxpaged/data/MainRepositoryImpl.kt: -------------------------------------------------------------------------------- 1 | package com.example.mvrxpaged.data 2 | 3 | import com.example.mvrxpaged.di.ApplicationScope 4 | import com.example.mvrxpaged.domain.entity.BannerData 5 | import com.example.mvrxpaged.domain.entity.CategoryData 6 | import com.example.mvrxpaged.domain.entity.DealData 7 | import com.example.mvrxpaged.domain.entity.MainViewType 8 | import com.example.mvrxpaged.domain.repository.MainRepository 9 | import javax.inject.Inject 10 | import kotlin.random.Random 11 | 12 | @ApplicationScope 13 | class MainRepositoryImpl @Inject constructor() : MainRepository { 14 | private val layerA = listOf( 15 | MainViewType.Banner("A"), 16 | 17 | MainViewType.Deal("1"), 18 | MainViewType.Deal("2"), 19 | 20 | MainViewType.Category("1"), 21 | MainViewType.Category("2"), 22 | MainViewType.Category("3") 23 | ) 24 | 25 | // change view content + layout 26 | private val layerB = listOf( 27 | MainViewType.Banner("B"), 28 | MainViewType.Deal("1"), 29 | 30 | MainViewType.Category("1"), 31 | MainViewType.Deal("3"), 32 | 33 | MainViewType.Category("5"), 34 | MainViewType.Category("3") 35 | ) 36 | 37 | // obtain the layer from A/B testing 38 | override fun getMainScreenLayer(): List { 39 | return if (Random.nextBoolean()) layerA else layerB 40 | } 41 | 42 | override fun getBanner(name: String): BannerData { 43 | Thread.sleep(1000) 44 | return BannerData("banner name: $name") 45 | } 46 | 47 | override fun getCategory(code: String): CategoryData { 48 | Thread.sleep(1000) 49 | return CategoryData("category $code") 50 | } 51 | 52 | override fun getDeal(code: String): DealData { 53 | Thread.sleep(1000) 54 | return DealData("deal $code") 55 | } 56 | 57 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/mvrxpaged/di/ActivityScope.kt: -------------------------------------------------------------------------------- 1 | package com.example.mvrxpaged.di 2 | 3 | import javax.inject.Qualifier 4 | import javax.inject.Scope 5 | 6 | @Scope 7 | annotation class ActivityScope 8 | 9 | @Scope 10 | annotation class FragmentScope 11 | 12 | 13 | @Scope 14 | annotation class ApplicationScope 15 | 16 | 17 | @Qualifier 18 | annotation class FormViewModel -------------------------------------------------------------------------------- /app/src/main/java/com/example/mvrxpaged/domain/entity/BannerData.kt: -------------------------------------------------------------------------------- 1 | package com.example.mvrxpaged.domain.entity 2 | 3 | data class BannerData(val value: String) -------------------------------------------------------------------------------- /app/src/main/java/com/example/mvrxpaged/domain/entity/CategoryData.kt: -------------------------------------------------------------------------------- 1 | package com.example.mvrxpaged.domain.entity 2 | 3 | data class CategoryData(val value: String) -------------------------------------------------------------------------------- /app/src/main/java/com/example/mvrxpaged/domain/entity/DealData.kt: -------------------------------------------------------------------------------- 1 | package com.example.mvrxpaged.domain.entity 2 | 3 | data class DealData(val value: String) -------------------------------------------------------------------------------- /app/src/main/java/com/example/mvrxpaged/domain/entity/MainViewType.kt: -------------------------------------------------------------------------------- 1 | package com.example.mvrxpaged.domain.entity 2 | 3 | sealed class MainViewType { 4 | data class Banner(val name: String) : MainViewType() 5 | data class Deal(val code: String) : MainViewType() 6 | data class Category(val code: String) : MainViewType() 7 | } 8 | 9 | 10 | /** 11 | Banner, 12 | Deal, 13 | DealHeader, 14 | Category, 15 | CategoryFooter, 16 | CategoryHeader, 17 | Separator,**/ -------------------------------------------------------------------------------- /app/src/main/java/com/example/mvrxpaged/domain/interactor/GetBanner.kt: -------------------------------------------------------------------------------- 1 | package com.example.mvrxpaged.domain.interactor 2 | 3 | import com.example.mvrxpaged.di.ApplicationScope 4 | import com.example.mvrxpaged.domain.entity.BannerData 5 | import com.example.mvrxpaged.domain.repository.MainRepository 6 | import javax.inject.Inject 7 | 8 | @ApplicationScope 9 | class GetBanner @Inject constructor( 10 | private val mainRepository: MainRepository 11 | ) { 12 | 13 | operator fun invoke(name: String): BannerData { 14 | return mainRepository.getBanner(name) 15 | } 16 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/mvrxpaged/domain/interactor/GetCategory.kt: -------------------------------------------------------------------------------- 1 | package com.example.mvrxpaged.domain.interactor 2 | 3 | import com.example.mvrxpaged.di.ApplicationScope 4 | import com.example.mvrxpaged.domain.entity.CategoryData 5 | import com.example.mvrxpaged.domain.repository.MainRepository 6 | import javax.inject.Inject 7 | 8 | @ApplicationScope 9 | class GetCategory @Inject constructor( 10 | private val mainRepository: MainRepository 11 | ) { 12 | 13 | 14 | operator fun invoke(code: String): CategoryData { 15 | return mainRepository.getCategory(code) 16 | } 17 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/mvrxpaged/domain/interactor/GetDeal.kt: -------------------------------------------------------------------------------- 1 | package com.example.mvrxpaged.domain.interactor 2 | 3 | import com.example.mvrxpaged.di.ApplicationScope 4 | import com.example.mvrxpaged.domain.entity.DealData 5 | import com.example.mvrxpaged.domain.repository.MainRepository 6 | import javax.inject.Inject 7 | 8 | @ApplicationScope 9 | class GetDeal @Inject constructor( 10 | private val mainRepository: MainRepository 11 | ) { 12 | 13 | operator fun invoke(code: String): DealData { 14 | return mainRepository.getDeal(code) 15 | } 16 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/mvrxpaged/domain/interactor/GetMainLayout.kt: -------------------------------------------------------------------------------- 1 | package com.example.mvrxpaged.domain.interactor 2 | 3 | import com.example.mvrxpaged.di.ApplicationScope 4 | import com.example.mvrxpaged.domain.entity.MainViewType 5 | import com.example.mvrxpaged.domain.repository.MainRepository 6 | import javax.inject.Inject 7 | 8 | @ApplicationScope 9 | class GetMainLayout @Inject constructor( 10 | private val mainRepository: MainRepository 11 | ) { 12 | 13 | operator fun invoke(): List { 14 | return mainRepository.getMainScreenLayer() 15 | } 16 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/mvrxpaged/domain/repository/MainRepository.kt: -------------------------------------------------------------------------------- 1 | package com.example.mvrxpaged.domain.repository 2 | 3 | import com.example.mvrxpaged.domain.entity.BannerData 4 | import com.example.mvrxpaged.domain.entity.CategoryData 5 | import com.example.mvrxpaged.domain.entity.DealData 6 | import com.example.mvrxpaged.domain.entity.MainViewType 7 | 8 | interface MainRepository { 9 | 10 | fun getCategory(code: String): CategoryData 11 | fun getDeal(code: String): DealData 12 | fun getBanner(name: String): BannerData 13 | 14 | // from remote config for a/b testing purpose 15 | // this example assume that you use Firebase Remote Config, its api is synchronous 16 | fun getMainScreenLayer(): List 17 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/mvrxpaged/ui/Extension.kt: -------------------------------------------------------------------------------- 1 | package com.example.mvrxpaged.ui 2 | 3 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/mvrxpaged/ui/Holder.kt: -------------------------------------------------------------------------------- 1 | package com.example.mvrxpaged.ui 2 | 3 | import javax.inject.Provider 4 | 5 | class Holder(private val func: Function0) : Provider { 6 | override fun get(): T { 7 | return func() 8 | } 9 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/mvrxpaged/ui/OnClick.kt: -------------------------------------------------------------------------------- 1 | package com.example.mvrxpaged.ui 2 | 3 | import android.view.View 4 | 5 | class OnClick(block: Function0) : View.OnClickListener, Function0 by block { 6 | override fun onClick(v: View?) { 7 | invoke() 8 | } 9 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/mvrxpaged/ui/main/GetMainModelPagedListStream.kt: -------------------------------------------------------------------------------- 1 | package com.example.mvrxpaged.ui.main 2 | 3 | import androidx.lifecycle.LiveData 4 | import androidx.paging.Config 5 | import androidx.paging.PagedList 6 | import androidx.paging.toLiveData 7 | import javax.inject.Inject 8 | 9 | class GetMainModelPagedListStream @Inject constructor( 10 | private val dataSourceFactory: MainEpoxyModelDataSource.Factory 11 | ) { 12 | operator fun invoke(placeholderEnabled: Boolean): LiveData> { 13 | return dataSourceFactory.toLiveData( 14 | config = Config( 15 | pageSize = 10, 16 | enablePlaceholders = placeholderEnabled, 17 | prefetchDistance = 10 18 | ) 19 | ) 20 | } 21 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/mvrxpaged/ui/main/ItemViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.example.mvrxpaged.ui.main 2 | 3 | interface ItemViewModel { 4 | val id: String 5 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/mvrxpaged/ui/main/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.example.mvrxpaged.ui.main 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import android.os.Bundle 6 | import com.example.mvrxpaged.R 7 | import dagger.android.support.DaggerAppCompatActivity 8 | 9 | class MainActivity : DaggerAppCompatActivity() { 10 | 11 | override fun onCreate(savedInstanceState: Bundle?) { 12 | super.onCreate(savedInstanceState) 13 | setContentView(R.layout.main_activity) 14 | 15 | if (savedInstanceState == null) { 16 | supportFragmentManager.beginTransaction() 17 | .replace(R.id.container, MainFragment().apply { 18 | arguments = intent?.extras?.getBundle("args") 19 | }, "123") 20 | .commit() 21 | } 22 | } 23 | 24 | companion object { 25 | fun starterIntent(context: Context, infinity: Boolean, placeholder: Boolean): Intent { 26 | return Intent(context, MainActivity::class.java) 27 | .putExtra( 28 | "args", 29 | MainArgs( 30 | infinity = infinity, 31 | placeHolderEnabled = placeholder 32 | ).toBundle() 33 | ) 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/mvrxpaged/ui/main/MainActivityModule.kt: -------------------------------------------------------------------------------- 1 | package com.example.mvrxpaged.ui.main 2 | 3 | import com.example.mvrxpaged.di.ActivityScope 4 | import dagger.Module 5 | import dagger.android.ContributesAndroidInjector 6 | 7 | @Module 8 | interface MainActivityModule { 9 | 10 | @ContributesAndroidInjector(modules = [MainFragmentModule::class]) 11 | @ActivityScope 12 | fun activity(): MainActivity 13 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/mvrxpaged/ui/main/MainAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.example.mvrxpaged.ui.main 2 | 3 | import android.view.LayoutInflater 4 | import android.view.View 5 | import android.view.ViewGroup 6 | import androidx.paging.PagedListAdapter 7 | import androidx.recyclerview.widget.DiffUtil 8 | import androidx.recyclerview.widget.RecyclerView 9 | import com.example.mvrxpaged.R 10 | import com.example.mvrxpaged.ui.OnClick 11 | import com.example.mvrxpaged.ui.main.view.* 12 | 13 | 14 | class MainAdapter : PagedListAdapter(ItemCallback) { 15 | 16 | private object ItemCallback : DiffUtil.ItemCallback() { 17 | override fun areItemsTheSame(oldItem: ItemViewModel, newItem: ItemViewModel): Boolean { 18 | return when { 19 | oldItem is BannerView.Model && newItem is BannerView.Model -> oldItem.id == newItem.id 20 | oldItem is DealView.Model && newItem is DealView.Model -> oldItem.id == newItem.id 21 | oldItem is CategoryView.Model && newItem is CategoryView.Model -> oldItem.id == newItem.id 22 | oldItem is SeparatorView.Model && newItem is SeparatorView.Model -> oldItem.id == newItem.id 23 | else -> false 24 | } 25 | } 26 | 27 | override fun areContentsTheSame(oldItem: ItemViewModel, newItem: ItemViewModel): Boolean { 28 | return when { 29 | oldItem is BannerView.Model && newItem is BannerView.Model -> oldItem.content == newItem.content 30 | oldItem is DealView.Model && newItem is DealView.Model -> oldItem.content == newItem.content 31 | oldItem is CategoryView.Model && newItem is CategoryView.Model -> oldItem.content == newItem.content 32 | oldItem is SeparatorView.Model && newItem is SeparatorView.Model -> true 33 | else -> false 34 | } 35 | } 36 | } 37 | 38 | override fun getItemViewType(position: Int): Int { 39 | return when (getItem(position)) { 40 | is BannerView.Model -> 1 41 | is CategoryView.Model -> 2 42 | is DealView.Model -> 3 43 | is SeparatorView.Model -> 4 44 | else -> 0 // loading model 45 | } 46 | } 47 | 48 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemViewHolder { 49 | val inflater = LayoutInflater.from(parent.context) 50 | val layoutRes = when (viewType) { 51 | 1 -> R.layout.banner_view 52 | 2 -> R.layout.category_view 53 | 3 -> R.layout.deal_view 54 | 4 -> R.layout.separator_view 55 | else -> R.layout.loading_view 56 | } 57 | val view = inflater.inflate(layoutRes, parent, false) 58 | return ItemViewHolder(view = view) 59 | } 60 | 61 | override fun onBindViewHolder(holder: ItemViewHolder, position: Int) { 62 | val model = getItem(position) ?: LoadingView.Model( 63 | id = "loading $position" 64 | ) 65 | 66 | when (model) { 67 | is BannerView.Model -> (holder.itemView as BannerView).apply { 68 | setContent(model.content) 69 | setOnClick(model.onClick) 70 | } 71 | is DealView.Model -> (holder.itemView as DealView).apply { 72 | setHeader(model.content) 73 | setContent(model.content) 74 | setOnClick(model.onClick) 75 | } 76 | is CategoryView.Model -> (holder.itemView as CategoryView).apply { 77 | setHeader(model.content) 78 | setContent(model.content) 79 | setFooter(model.content) 80 | setOnClick(model.onClick) 81 | } 82 | } 83 | } 84 | 85 | 86 | class ItemViewHolder(view: View) : RecyclerView.ViewHolder(view) 87 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/mvrxpaged/ui/main/MainArgs.kt: -------------------------------------------------------------------------------- 1 | package com.example.mvrxpaged.ui.main 2 | 3 | import android.os.Bundle 4 | 5 | data class MainArgs( 6 | val infinity: Boolean, 7 | val placeHolderEnabled: Boolean 8 | ) { 9 | fun toBundle() = Bundle().apply { 10 | putBoolean("infinity", infinity) 11 | putBoolean("placeHolderEnabled", placeHolderEnabled) 12 | } 13 | 14 | companion object { 15 | fun fromBundle(bundle: Bundle) = MainArgs( 16 | infinity = bundle.getBoolean("infinity"), 17 | placeHolderEnabled = bundle.getBoolean("placeHolderEnabled") 18 | ) 19 | } 20 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/mvrxpaged/ui/main/MainEpoxyModelDataSource.kt: -------------------------------------------------------------------------------- 1 | package com.example.mvrxpaged.ui.main 2 | 3 | import androidx.lifecycle.LifecycleOwner 4 | import androidx.paging.DataSource 5 | import androidx.paging.PageKeyedDataSource 6 | import com.example.mvrxpaged.di.FragmentScope 7 | import com.example.mvrxpaged.domain.entity.MainViewType 8 | import com.example.mvrxpaged.domain.entity.MainViewType.* 9 | import com.example.mvrxpaged.domain.interactor.GetBanner 10 | import com.example.mvrxpaged.domain.interactor.GetCategory 11 | import com.example.mvrxpaged.domain.interactor.GetDeal 12 | import com.example.mvrxpaged.domain.interactor.GetMainLayout 13 | import com.example.mvrxpaged.ui.OnClick 14 | import com.example.mvrxpaged.ui.main.view.* 15 | import javax.inject.Inject 16 | import javax.inject.Provider 17 | import kotlin.random.Random 18 | 19 | @FragmentScope 20 | class MainEpoxyModelDataSource @Inject constructor( 21 | private val viewModelProvider: Provider, 22 | private val args: MainArgs, 23 | private val getBanner: GetBanner, 24 | private val getCategory: GetCategory, 25 | private val getDeal: GetDeal, 26 | private val getMainLayout: GetMainLayout 27 | ) : PageKeyedDataSource() { 28 | private lateinit var layout: List 29 | private val totalItemCount: Int by lazy { 30 | layout.map { viewType -> 31 | when (viewType) { 32 | is Banner -> 1 + 1 // banner + separator 33 | is Deal -> 1 + 1 // header + separator 34 | is Category -> 1 + 1 // header + separator 35 | } 36 | }.sum() 37 | } 38 | 39 | private val totalPage: Int by lazy { layout.size } 40 | 41 | private val viewModel: MainViewModel by lazy { viewModelProvider.get() } 42 | 43 | @FragmentScope 44 | class Factory @Inject constructor( 45 | private val provider: Provider 46 | ) : DataSource.Factory() { 47 | override fun create(): DataSource { 48 | return provider.get() 49 | } 50 | } 51 | 52 | override fun loadInitial(params: LoadInitialParams, callback: LoadInitialCallback) { 53 | layout = getMainLayout() 54 | load(page = 0, loadInitialCallback = callback) 55 | } 56 | 57 | override fun loadAfter(params: LoadParams, callback: LoadCallback) { 58 | load(page = params.key, loadCallback = callback) 59 | } 60 | 61 | override fun loadBefore(params: LoadParams, callback: LoadCallback) { 62 | // data source does not change 63 | TODO("not implemented") //To change body of created functions use File | Settings | File Templates. 64 | } 65 | 66 | private fun load( 67 | page: Int, 68 | loadInitialCallback: LoadInitialCallback? = null, 69 | loadCallback: LoadCallback? = null 70 | ) { 71 | 72 | val nextPage = page + 1 73 | // index goes out of bound 74 | if (page > totalPage - 1) { 75 | // for infinity list -> load more random category block 76 | if (args.infinity) { 77 | val code = Random.nextInt() % 1000 78 | val models = loadCategory(code.toString()) 79 | loadCallback?.onResult(models, nextPage) 80 | } else { 81 | loadCallback?.onResult(emptyList(), null) 82 | loadInitialCallback?.onResult(emptyList(), 0, 0, null, null) 83 | } 84 | } else { 85 | val models = when (val viewType = layout[page]) { 86 | is Banner -> loadBanner(viewType.name) 87 | is Deal -> loadDeal(viewType.code) 88 | is Category -> loadCategory(viewType.code) 89 | } 90 | 91 | 92 | if (args.infinity) { 93 | loadInitialCallback?.onResult(models, null, nextPage) 94 | } else { 95 | loadInitialCallback?.onResult(models, 0, totalItemCount, null, nextPage) 96 | 97 | } 98 | loadCallback?.onResult(models, nextPage) 99 | } 100 | } 101 | 102 | private fun loadBanner(name: String): List { 103 | return getBanner(name).let { bannerData -> 104 | listOf( 105 | BannerView.Model( 106 | id = "banner ${bannerData.value}", 107 | content = bannerData.value, 108 | onClick = OnClick { 109 | viewModel.onBannerClick(bannerData) 110 | }), 111 | SeparatorView.Model( 112 | id = "separator ${bannerData.value}" 113 | ) 114 | ) 115 | } 116 | } 117 | 118 | private fun loadDeal(code: String): List { 119 | return getDeal(code).let { dealData -> 120 | listOf( 121 | DealView.Model( 122 | id = "header ${dealData.value}", 123 | content = dealData.value, 124 | onClick = OnClick { 125 | viewModel.onDealClick(dealData) 126 | } 127 | ), 128 | SeparatorView.Model( 129 | id = "Seperator ${dealData.value}" 130 | ) 131 | ) 132 | } 133 | } 134 | 135 | private fun loadCategory(code: String): List { 136 | return getCategory(code).let { categoryData -> 137 | listOf( 138 | CategoryView.Model( 139 | id = "category ${categoryData.value}", 140 | content = categoryData.value, 141 | onClick = OnClick { 142 | viewModel.onCategoryClick(categoryData) 143 | }), 144 | SeparatorView.Model( 145 | id = "separator ${categoryData.value}" 146 | ) 147 | ) 148 | } 149 | } 150 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/mvrxpaged/ui/main/MainFragment.kt: -------------------------------------------------------------------------------- 1 | package com.example.mvrxpaged.ui.main 2 | 3 | import android.os.Bundle 4 | import android.view.LayoutInflater 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import android.widget.Toast 8 | import androidx.fragment.app.viewModels 9 | import androidx.lifecycle.observe 10 | import androidx.recyclerview.widget.LinearLayoutManager 11 | import androidx.recyclerview.widget.RecyclerView 12 | import com.example.mvrxpaged.R 13 | import dagger.android.support.DaggerFragment 14 | import javax.inject.Inject 15 | 16 | class MainFragment : DaggerFragment() { 17 | 18 | @Inject 19 | lateinit var viewModelFactory: MainViewModel.Factory 20 | 21 | val viewModel: MainViewModel by viewModels(factoryProducer = { viewModelFactory }) 22 | 23 | private var toast: Toast? = null 24 | 25 | private lateinit var recyclerViewAdapter: MainAdapter 26 | 27 | override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { 28 | viewLifecycleOwner 29 | viewModel.run { 30 | messageEvent.observe(viewLifecycleOwner) { 31 | toast?.cancel() 32 | toast = Toast.makeText(requireContext(), it, Toast.LENGTH_LONG) 33 | toast?.show() 34 | } 35 | models.observe(viewLifecycleOwner) { 36 | recyclerViewAdapter.submitList(it) 37 | } 38 | } 39 | 40 | return inflater.inflate(R.layout.main_fragment, container, false).apply { 41 | findViewById(R.id.content_recycler_view).apply { 42 | recyclerViewAdapter = MainAdapter() 43 | adapter = recyclerViewAdapter 44 | layoutManager = LinearLayoutManager(requireContext()) 45 | } 46 | } 47 | } 48 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/mvrxpaged/ui/main/MainFragmentModule.kt: -------------------------------------------------------------------------------- 1 | package com.example.mvrxpaged.ui.main 2 | 3 | import android.os.Bundle 4 | import androidx.lifecycle.LifecycleOwner 5 | import com.example.mvrxpaged.di.FormViewModel 6 | import com.example.mvrxpaged.di.FragmentScope 7 | import dagger.Module 8 | import dagger.Provides 9 | import dagger.android.ContributesAndroidInjector 10 | 11 | @Module 12 | interface MainFragmentModule { 13 | 14 | @ContributesAndroidInjector(modules = [Provision::class]) 15 | @FragmentScope 16 | fun fragment(): MainFragment 17 | 18 | @Module 19 | object Provision { 20 | @Provides 21 | @FormViewModel 22 | @JvmStatic 23 | fun viewModelHolder(fragment: MainFragment): MainViewModel = fragment.viewModel 24 | 25 | @Provides 26 | @FragmentScope 27 | @JvmStatic 28 | fun args(fragment: MainFragment): MainArgs = MainArgs.fromBundle(fragment.arguments ?: Bundle.EMPTY) 29 | } 30 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/mvrxpaged/ui/main/MainViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.example.mvrxpaged.ui.main 2 | 3 | import androidx.lifecycle.MutableLiveData 4 | import androidx.lifecycle.ViewModel 5 | import androidx.lifecycle.ViewModelProvider 6 | import com.example.mvrxpaged.domain.entity.BannerData 7 | import com.example.mvrxpaged.domain.entity.CategoryData 8 | import com.example.mvrxpaged.domain.entity.DealData 9 | import javax.inject.Inject 10 | import javax.inject.Provider 11 | 12 | class MainViewModel @Inject constructor( 13 | args: MainArgs, 14 | getMainModelPagedListStream: GetMainModelPagedListStream 15 | ) : ViewModel() { 16 | 17 | class Factory @Inject constructor( 18 | private val provider: Provider 19 | ) : ViewModelProvider.Factory { 20 | @Suppress("UNCHECKED_CAST") 21 | override fun create(modelClass: Class) = provider.get() as T 22 | } 23 | 24 | // don't do this, use Event pattern 25 | val messageEvent = MutableLiveData() 26 | 27 | val models = getMainModelPagedListStream(args.placeHolderEnabled) 28 | 29 | fun onDealClick(dealData: DealData) { 30 | println("open deal screen for: ${dealData.value}") 31 | } 32 | 33 | fun onCategoryClick(data: CategoryData) { 34 | messageEvent.value = "onCategoryClick $data" 35 | } 36 | 37 | fun onBannerClick(data: BannerData) { 38 | messageEvent.value = "onBannerClick $data" 39 | } 40 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/mvrxpaged/ui/main/view/BannerView.kt: -------------------------------------------------------------------------------- 1 | package com.example.mvrxpaged.ui.main.view 2 | 3 | import android.content.Context 4 | import android.util.AttributeSet 5 | import android.widget.LinearLayout 6 | import android.widget.TextView 7 | import com.airbnb.epoxy.ModelView 8 | import com.example.mvrxpaged.R 9 | import com.example.mvrxpaged.R2 10 | import com.example.mvrxpaged.ui.OnClick 11 | import com.example.mvrxpaged.ui.main.ItemViewModel 12 | 13 | class BannerView @JvmOverloads constructor( 14 | context: Context, 15 | attributeSet: AttributeSet? = null 16 | ) : LinearLayout(context, attributeSet) { 17 | 18 | data class Model(override val id: String, val content: String, val onClick: OnClick) : ItemViewModel 19 | 20 | private val contentTextView: TextView by lazy { 21 | findViewById(R.id.content) 22 | } 23 | 24 | fun setContent(content: String) { 25 | println("SimpleTextView setContent: $content") 26 | contentTextView.text = content 27 | } 28 | 29 | fun setOnClick(onClick: OnClick? = null) { 30 | contentTextView.setOnClickListener(onClick) 31 | } 32 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/mvrxpaged/ui/main/view/CategoryView.kt: -------------------------------------------------------------------------------- 1 | package com.example.mvrxpaged.ui.main.view 2 | 3 | import android.content.Context 4 | import android.util.AttributeSet 5 | import android.widget.LinearLayout 6 | import android.widget.TextView 7 | import com.airbnb.epoxy.ModelView 8 | import com.example.mvrxpaged.R 9 | import com.example.mvrxpaged.R2 10 | import com.example.mvrxpaged.ui.OnClick 11 | import com.example.mvrxpaged.ui.main.ItemViewModel 12 | 13 | class CategoryView @JvmOverloads constructor( 14 | context: Context, 15 | attributeSet: AttributeSet? = null 16 | ) : LinearLayout(context, attributeSet) { 17 | 18 | data class Model(override val id: String, val content: String, val onClick: OnClick) : ItemViewModel 19 | 20 | private val contentTextView: TextView by lazy { findViewById(R.id.content) } 21 | private val headerTextView: TextView by lazy { findViewById(R.id.header) } 22 | private val footerTextView: TextView by lazy { findViewById(R.id.footer) } 23 | 24 | fun setContent(value: String) { 25 | contentTextView.text = value 26 | } 27 | 28 | fun setHeader(value: String) { 29 | headerTextView.text = "-- CATEGORY HEADER OF $value ---".toUpperCase() 30 | } 31 | 32 | fun setFooter(value: String) { 33 | footerTextView.text = "-- CATEGORY FOOTER OF $value ---".toUpperCase() 34 | } 35 | 36 | fun setOnClick(onClick: OnClick? = null) { 37 | contentTextView.setOnClickListener(onClick) 38 | } 39 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/mvrxpaged/ui/main/view/DealView.kt: -------------------------------------------------------------------------------- 1 | package com.example.mvrxpaged.ui.main.view 2 | 3 | import android.content.Context 4 | import android.util.AttributeSet 5 | import android.widget.LinearLayout 6 | import android.widget.TextView 7 | import com.airbnb.epoxy.ModelView 8 | import com.example.mvrxpaged.R 9 | import com.example.mvrxpaged.R2 10 | import com.example.mvrxpaged.ui.OnClick 11 | import com.example.mvrxpaged.ui.main.ItemViewModel 12 | 13 | class DealView @JvmOverloads constructor( 14 | context: Context, 15 | attributeSet: AttributeSet? = null 16 | ) : LinearLayout(context, attributeSet) { 17 | 18 | data class Model(override val id: String, val content: String, val onClick: OnClick) : ItemViewModel 19 | 20 | private val contentTextView: TextView by lazy { findViewById(R.id.content) } 21 | private val headerTextView: TextView by lazy { findViewById(R.id.header) } 22 | 23 | fun setContent(value: String) { 24 | contentTextView.text = value 25 | } 26 | 27 | fun setHeader(value: String) { 28 | headerTextView.text = "-- DEAL HEADER OF $value ---".toUpperCase() 29 | } 30 | 31 | fun setOnClick(onClick: OnClick? = null) { 32 | contentTextView.setOnClickListener(onClick) 33 | } 34 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/mvrxpaged/ui/main/view/LoadingView.kt: -------------------------------------------------------------------------------- 1 | package com.example.mvrxpaged.ui.main.view 2 | 3 | import android.content.Context 4 | import android.util.AttributeSet 5 | import android.widget.LinearLayout 6 | import android.widget.TextView 7 | import com.airbnb.epoxy.ModelView 8 | import com.example.mvrxpaged.R 9 | import com.example.mvrxpaged.R2 10 | import com.example.mvrxpaged.ui.OnClick 11 | import com.example.mvrxpaged.ui.main.ItemViewModel 12 | 13 | class LoadingView @JvmOverloads constructor( 14 | context: Context, 15 | attributeSet: AttributeSet? = null 16 | ) : LinearLayout(context, attributeSet) { 17 | 18 | data class Model(override val id: String) : ItemViewModel 19 | 20 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/mvrxpaged/ui/main/view/SeparatorView.kt: -------------------------------------------------------------------------------- 1 | package com.example.mvrxpaged.ui.main.view 2 | 3 | import android.content.Context 4 | import android.util.AttributeSet 5 | import android.widget.LinearLayout 6 | import com.airbnb.epoxy.ModelView 7 | import com.example.mvrxpaged.R2 8 | import com.example.mvrxpaged.ui.main.ItemViewModel 9 | 10 | class SeparatorView @JvmOverloads constructor( 11 | context: Context, 12 | attributeSet: AttributeSet? = null 13 | ) : LinearLayout(context, attributeSet) { 14 | 15 | class Model(override val id: String) : ItemViewModel 16 | 17 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/mvrxpaged/ui/select/SelectActivity.kt: -------------------------------------------------------------------------------- 1 | package com.example.mvrxpaged.ui.select 2 | 3 | import android.os.Bundle 4 | import androidx.appcompat.app.AppCompatActivity 5 | import com.example.mvrxpaged.R 6 | import com.example.mvrxpaged.ui.main.MainActivity 7 | import kotlinx.android.synthetic.main.select_activity.* 8 | 9 | class SelectActivity : AppCompatActivity() { 10 | 11 | override fun onCreate(savedInstanceState: Bundle?) { 12 | super.onCreate(savedInstanceState) 13 | setContentView(R.layout.select_activity) 14 | go_button.setOnClickListener { 15 | startActivity( 16 | MainActivity.starterIntent( 17 | this, 18 | infinity = infinity_check_box.isChecked, 19 | placeholder = placeholder_checkbox.isChecked 20 | ) 21 | ) 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 13 | 19 | 22 | 25 | 26 | 27 | 28 | 34 | 35 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 10 | 12 | 14 | 16 | 18 | 20 | 22 | 24 | 26 | 28 | 30 | 32 | 34 | 36 | 38 | 40 | 42 | 44 | 46 | 48 | 50 | 52 | 54 | 56 | 58 | 60 | 62 | 64 | 66 | 68 | 70 | 72 | 74 | 75 | -------------------------------------------------------------------------------- /app/src/main/res/layout/banner_view.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/layout/category_view.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 12 | 17 | 22 | -------------------------------------------------------------------------------- /app/src/main/res/layout/deal_view.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 13 | 17 | -------------------------------------------------------------------------------- /app/src/main/res/layout/loading_view.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/layout/main_activity.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/layout/main_fragment.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/layout/select_activity.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 15 | 22 |