├── docs
├── wip.md
├── material.md
├── guided-step.md
└── object-adapter.md
├── fixture
├── src
│ └── main
│ │ ├── AndroidManifest.xml
│ │ ├── res
│ │ ├── values
│ │ │ └── strings.xml
│ │ └── layout
│ │ │ └── layout_test_guided_action_custom.xml
│ │ └── java
│ │ └── jp
│ │ └── co
│ │ └── cyberagent
│ │ └── fixture
│ │ ├── ObjectAdapterExt.kt
│ │ ├── KotestProjectConfig.kt
│ │ ├── Memo.kt
│ │ └── TestLifecycleOwner.kt
└── build.gradle.kts
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── sample
├── src
│ └── main
│ │ ├── res
│ │ ├── values
│ │ │ ├── strings.xml
│ │ │ ├── colors.xml
│ │ │ └── styles.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
│ │ ├── layout
│ │ │ ├── layout_divider.xml
│ │ │ ├── activity_main.xml
│ │ │ ├── model_info.xml
│ │ │ └── model_entry.xml
│ │ ├── mipmap-anydpi-v26
│ │ │ ├── ic_launcher.xml
│ │ │ └── ic_launcher_round.xml
│ │ ├── drawable-v24
│ │ │ └── ic_launcher_foreground.xml
│ │ ├── navigation
│ │ │ └── nav_main.xml
│ │ └── drawable
│ │ │ ├── ic_tv.xml
│ │ │ └── ic_launcher_background.xml
│ │ ├── java
│ │ └── jp
│ │ │ └── co
│ │ │ └── cyberagent
│ │ │ └── lounge
│ │ │ └── sample
│ │ │ ├── binding
│ │ │ └── ViewBindingAdapters.kt
│ │ │ ├── utils
│ │ │ ├── RandomColor.kt
│ │ │ └── PagedListCreator.kt
│ │ │ ├── model
│ │ │ ├── EntryModel.kt
│ │ │ ├── InfoModel.kt
│ │ │ └── TextModel.kt
│ │ │ └── ui
│ │ │ ├── MainActivity.kt
│ │ │ ├── HomeFragment.kt
│ │ │ ├── VerticalGridExampleFragment.kt
│ │ │ ├── GuidedStepExampleFragment.kt
│ │ │ ├── PagedListStressTestFragment.kt
│ │ │ └── RowsExampleFragment.kt
│ │ └── AndroidManifest.xml
├── proguard-rules.pro
└── build.gradle.kts
├── lounge
├── src
│ ├── test
│ │ └── java
│ │ │ └── jp
│ │ │ └── co
│ │ │ └── cyberagent
│ │ │ └── lounge
│ │ │ ├── util
│ │ │ ├── ValueHolder.kt
│ │ │ └── TestModel.kt
│ │ │ ├── AdapterDslTest.kt
│ │ │ ├── LambdaLoungeControllerTest.kt
│ │ │ ├── LoungeGuidedActionsStylistTest.kt
│ │ │ ├── LoungeControllerPropertyTest.kt
│ │ │ ├── LoungeGuidedActionsBuilderTest.kt
│ │ │ └── ListRowModelTest.kt
│ └── main
│ │ ├── AndroidManifest.xml
│ │ └── java
│ │ └── jp
│ │ └── co
│ │ └── cyberagent
│ │ └── lounge
│ │ ├── HeaderData.kt
│ │ ├── Guidance.kt
│ │ ├── LoungeModel.kt
│ │ ├── DeferredLoungeModel.kt
│ │ ├── LambdaLoungeController.kt
│ │ ├── MemorizedController.kt
│ │ ├── LoungeGuidedActionsStylist.kt
│ │ ├── LoungeBuildModelScope.kt
│ │ ├── AdapterDsl.kt
│ │ ├── internal
│ │ ├── Logger.kt
│ │ └── LoungeAdapter.kt
│ │ ├── LoungeControllerInterceptor.kt
│ │ ├── LoungeGuidedStepSupportFragment.kt
│ │ ├── SimpleLoungeModelAwaitInterceptor.kt
│ │ ├── TypedPresenter.kt
│ │ ├── KeyUtils.kt
│ │ ├── LoungeControllerProperty.kt
│ │ ├── LoungeGuidedActionsBuilder.kt
│ │ ├── LoungeGuidedActionBuilder.kt
│ │ ├── TypedRowPresenter.kt
│ │ └── LoungeGuidedAction.kt
├── gradle.properties
└── build.gradle.kts
├── lounge-paging
├── src
│ ├── main
│ │ ├── AndroidManifest.xml
│ │ └── java
│ │ │ └── jp
│ │ │ └── co
│ │ │ └── cyberagent
│ │ │ └── lounge
│ │ │ └── paging
│ │ │ ├── internal
│ │ │ ├── ChannelExt.kt
│ │ │ ├── CacheOp.kt
│ │ │ └── PagedListModelCache.kt
│ │ │ ├── PagedListLoungeBuildModelScope.kt
│ │ │ ├── LambdaPagedListLoungeController.kt
│ │ │ ├── PagedListLoungeController.kt
│ │ │ └── PagedListRowModel.kt
│ └── test
│ │ └── java
│ │ └── jp
│ │ └── co
│ │ └── cyberagent
│ │ └── lounge
│ │ └── paging
│ │ ├── util
│ │ ├── TestModel.kt
│ │ ├── EmptyPresenter.kt
│ │ ├── Item.kt
│ │ ├── PagedListUtil.kt
│ │ └── ListDataSource.kt
│ │ ├── PagedListLoungeControllerTest.kt
│ │ └── PagedListRowModelTest.kt
├── gradle.properties
└── build.gradle.kts
├── lounge-material
├── src
│ └── main
│ │ ├── AndroidManifest.xml
│ │ └── res
│ │ └── values
│ │ └── themes.xml
├── gradle.properties
└── build.gradle.kts
├── lounge-databinding
├── src
│ └── main
│ │ ├── AndroidManifest.xml
│ │ └── java
│ │ └── jp
│ │ └── co
│ │ └── cyberagent
│ │ └── lounge
│ │ └── databinding
│ │ ├── SimpleDataBindingRowPresenter.kt
│ │ ├── SimpleDataBindingPresenter.kt
│ │ ├── DataBindingRowPresenter.kt
│ │ └── DataBindingPresenter.kt
├── gradle.properties
└── build.gradle.kts
├── lounge-navigation
├── src
│ └── main
│ │ ├── AndroidManifest.xml
│ │ ├── res
│ │ └── values
│ │ │ └── values.xml
│ │ └── java
│ │ └── jp
│ │ └── co
│ │ └── cyberagent
│ │ └── lounge
│ │ └── navigation
│ │ └── BrowseSupportFragmentBackPressedCallback.kt
├── gradle.properties
└── build.gradle.kts
├── .idea
├── codeStyles
│ ├── codeStyleConfig.xml
│ └── Project.xml
└── detekt.xml
├── .editorconfig
├── tools
├── prepare_docs.sh
└── deploy_snapshot.sh
├── .github
├── ISSUE_TEMPLATE
│ ├── feature_request.md
│ └── bug_report.md
├── workflows
│ ├── deploy-docs.yml
│ └── ci.yml
└── PULL_REQUEST_TEMPLATE
├── CHANGELOG.md
├── gradle.properties
├── RELEASING.md
├── LICENSE
├── README.md
├── settings.gradle.kts
├── mkdocs.yml
├── gradlew.bat
├── config
└── detekt
│ └── detekt.yml
├── .gitignore
└── gradlew
/docs/wip.md:
--------------------------------------------------------------------------------
1 | # WIP
2 |
--------------------------------------------------------------------------------
/fixture/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ca-love/lounge/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/sample/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Lounge Sample
3 |
4 |
--------------------------------------------------------------------------------
/sample/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ca-love/lounge/HEAD/sample/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/sample/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ca-love/lounge/HEAD/sample/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/sample/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ca-love/lounge/HEAD/sample/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/sample/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ca-love/lounge/HEAD/sample/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/lounge/src/test/java/jp/co/cyberagent/lounge/util/ValueHolder.kt:
--------------------------------------------------------------------------------
1 | package jp.co.cyberagent.lounge.util
2 |
3 | data class ValueHolder(val v: T)
4 |
--------------------------------------------------------------------------------
/sample/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ca-love/lounge/HEAD/sample/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/lounge/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/sample/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ca-love/lounge/HEAD/sample/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/sample/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ca-love/lounge/HEAD/sample/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/sample/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ca-love/lounge/HEAD/sample/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/sample/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ca-love/lounge/HEAD/sample/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/sample/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ca-love/lounge/HEAD/sample/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/lounge-paging/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/lounge-material/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/fixture/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Test Guided Action
4 |
5 |
--------------------------------------------------------------------------------
/lounge-databinding/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/lounge-navigation/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/lounge/gradle.properties:
--------------------------------------------------------------------------------
1 | POM_ARTIFACT_ID=lounge
2 | POM_NAME=Lounge
3 | POM_DESCRIPTION=Leanback enhancement library for accelerating Android TV development.
4 | POM_PACKAGING=aar
5 |
--------------------------------------------------------------------------------
/lounge-material/gradle.properties:
--------------------------------------------------------------------------------
1 | POM_ARTIFACT_ID=lounge-material
2 | POM_NAME=Lounge Material
3 | POM_DESCRIPTION=Building a Leanback app with Material Design Components.
4 | POM_PACKAGING=aar
5 |
--------------------------------------------------------------------------------
/lounge-navigation/gradle.properties:
--------------------------------------------------------------------------------
1 | POM_ARTIFACT_ID=lounge-navigation
2 | POM_NAME=Lounge Navigation
3 | POM_DESCRIPTION=Building a Leanback app with Navigation Components.
4 | POM_PACKAGING=aar
5 |
--------------------------------------------------------------------------------
/.idea/codeStyles/codeStyleConfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | charset = utf-8
5 | end_of_line = lf
6 | indent_size = 2
7 | indent_style = space
8 | insert_final_newline = true
9 | max_line_length = off
10 | tab_width = 2
11 |
--------------------------------------------------------------------------------
/lounge-paging/gradle.properties:
--------------------------------------------------------------------------------
1 | POM_ARTIFACT_ID=lounge-paging
2 | POM_NAME=Lounge Paging
3 | POM_DESCRIPTION=Leanback enhancement library for accelerating Android TV development. Additional Paging support.
4 | POM_PACKAGING=aar
5 |
--------------------------------------------------------------------------------
/tools/prepare_docs.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | set -e
3 |
4 | # Generate API docs
5 | ./gradlew dokkaHtmlMultiModule
6 |
7 | # Copy *.md files into docs directory
8 | cp README.md docs/index.md
9 | cp CHANGELOG.md docs/changelog.md
10 |
--------------------------------------------------------------------------------
/fixture/src/main/java/jp/co/cyberagent/fixture/ObjectAdapterExt.kt:
--------------------------------------------------------------------------------
1 | package jp.co.cyberagent.fixture
2 |
3 | import androidx.leanback.widget.ObjectAdapter
4 |
5 | val ObjectAdapter.items: List
6 | get() = (0 until size()).map { get(it) }
7 |
--------------------------------------------------------------------------------
/lounge-databinding/gradle.properties:
--------------------------------------------------------------------------------
1 | POM_ARTIFACT_ID=lounge-databinding
2 | POM_NAME=Lounge DataBinding
3 | POM_DESCRIPTION=Leanback enhancement library for accelerating Android TV development. Additional DataBinding support.
4 | POM_PACKAGING=aar
5 |
--------------------------------------------------------------------------------
/lounge-navigation/src/main/res/values/values.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 |
--------------------------------------------------------------------------------
/sample/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #6200EE
4 | #3700B3
5 | #03DAC5
6 |
7 |
--------------------------------------------------------------------------------
/sample/src/main/res/layout/layout_divider.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
--------------------------------------------------------------------------------
/sample/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/sample/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/lounge-material/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | `module-config`
3 | com.android.library
4 | `kotlin-android`
5 | id("com.vanniktech.maven.publish")
6 | }
7 |
8 | dependencies {
9 | implementation(Kotlin.stdlib.jdk8)
10 | implementation(AndroidX.appCompat)
11 |
12 | api(Google.android.material)
13 | api(AndroidX.leanback)
14 | }
15 |
--------------------------------------------------------------------------------
/sample/src/main/java/jp/co/cyberagent/lounge/sample/binding/ViewBindingAdapters.kt:
--------------------------------------------------------------------------------
1 | package jp.co.cyberagent.lounge.sample.binding
2 |
3 | import android.view.View
4 | import androidx.databinding.BindingAdapter
5 |
6 | @BindingAdapter("backgroundColorInt")
7 | fun View.bindColorBackground(colorInt: Int) {
8 | setBackgroundColor(colorInt)
9 | }
10 |
--------------------------------------------------------------------------------
/.idea/detekt.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | true
5 | true
6 | true
7 | config/detekt/detekt.yml
8 |
9 |
--------------------------------------------------------------------------------
/lounge-paging/src/main/java/jp/co/cyberagent/lounge/paging/internal/ChannelExt.kt:
--------------------------------------------------------------------------------
1 | package jp.co.cyberagent.lounge.paging.internal
2 |
3 | import kotlinx.coroutines.channels.SendChannel
4 |
5 | internal fun SendChannel.offerSafe(element: E) {
6 | @Suppress("EXPERIMENTAL_API_USAGE")
7 | if (!isClosedForSend) {
8 | offer(element)
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/lounge-paging/src/test/java/jp/co/cyberagent/lounge/paging/util/TestModel.kt:
--------------------------------------------------------------------------------
1 | package jp.co.cyberagent.lounge.paging.util
2 |
3 | import androidx.leanback.widget.Presenter
4 | import jp.co.cyberagent.lounge.LoungeModel
5 |
6 | data class TestModel(
7 | override val key: Long,
8 | ) : LoungeModel {
9 |
10 | override val presenter: Presenter
11 | get() = EmptyPresenter
12 | }
13 |
--------------------------------------------------------------------------------
/fixture/src/main/res/layout/layout_test_guided_action_custom.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
11 |
12 |
--------------------------------------------------------------------------------
/lounge-databinding/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | `module-config`
3 | com.android.library
4 | `kotlin-android`
5 | id("com.vanniktech.maven.publish")
6 | }
7 |
8 | android {
9 | buildFeatures {
10 | dataBinding = true
11 | }
12 | }
13 |
14 |
15 | dependencies {
16 | api(project(":lounge"))
17 | api(AndroidX.leanback)
18 |
19 | implementation(Kotlin.stdlib.jdk8)
20 | implementation(AndroidX.appCompat)
21 | implementation(AndroidX.core.ktx)
22 | }
23 |
--------------------------------------------------------------------------------
/lounge/src/main/java/jp/co/cyberagent/lounge/HeaderData.kt:
--------------------------------------------------------------------------------
1 | package jp.co.cyberagent.lounge
2 |
3 | import androidx.leanback.widget.HeaderItem
4 |
5 | /**
6 | * Represents the data of a [HeaderItem] in a data class.
7 | * So we can get proper implementation of [hashCode] and [equals].
8 | *
9 | * @see ListRowModel
10 | */
11 | data class HeaderData(
12 | val name: String,
13 | val description: String? = null,
14 | val contentDescription: String? = null,
15 | )
16 |
--------------------------------------------------------------------------------
/lounge-navigation/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | `module-config`
3 | com.android.library
4 | `kotlin-android`
5 | id("com.vanniktech.maven.publish")
6 | }
7 |
8 | dependencies {
9 | api(AndroidX.leanback)
10 |
11 | implementation(Kotlin.stdlib.jdk8)
12 | implementation(AndroidX.appCompat)
13 | implementation(AndroidX.core.ktx)
14 | implementation(AndroidX.fragmentKtx)
15 | implementation(AndroidX.activityKtx)
16 | implementation(AndroidX.navigation.fragmentKtx)
17 | }
18 |
--------------------------------------------------------------------------------
/fixture/src/main/java/jp/co/cyberagent/fixture/KotestProjectConfig.kt:
--------------------------------------------------------------------------------
1 | package jp.co.cyberagent.fixture
2 |
3 | import io.kotest.core.config.AbstractProjectConfig
4 | import io.kotest.core.spec.IsolationMode
5 | import io.kotest.extensions.robolectric.RobolectricExtension
6 |
7 | class KotestProjectConfig : AbstractProjectConfig() {
8 | override val isolationMode: IsolationMode = IsolationMode.InstancePerLeaf
9 |
10 | override fun extensions() = super.extensions() + RobolectricExtension()
11 | }
12 |
--------------------------------------------------------------------------------
/lounge-paging/src/main/java/jp/co/cyberagent/lounge/paging/PagedListLoungeBuildModelScope.kt:
--------------------------------------------------------------------------------
1 | package jp.co.cyberagent.lounge.paging
2 |
3 | import androidx.paging.PagedList
4 | import jp.co.cyberagent.lounge.LoungeBuildModelScope
5 | import jp.co.cyberagent.lounge.LoungeModel
6 |
7 | /**
8 | * A [LoungeBuildModelScope] that works with [PagedList].
9 | */
10 | interface PagedListLoungeBuildModelScope : LoungeBuildModelScope {
11 |
12 | /**
13 | * Gets built models from the [PagedList].
14 | */
15 | suspend fun getItemModels(): List
16 | }
17 |
--------------------------------------------------------------------------------
/lounge/src/main/java/jp/co/cyberagent/lounge/Guidance.kt:
--------------------------------------------------------------------------------
1 | package jp.co.cyberagent.lounge
2 |
3 | import android.graphics.drawable.Drawable
4 | import androidx.leanback.widget.GuidanceStylist
5 |
6 | /**
7 | * Constructs a [GuidanceStylist.Guidance].
8 | */
9 | @Suppress("FunctionName")
10 | fun Guidance(
11 | title: String? = null,
12 | description: String? = null,
13 | breadcrumb: String? = null,
14 | icon: Drawable? = null,
15 | ): GuidanceStylist.Guidance = GuidanceStylist.Guidance(
16 | title,
17 | description,
18 | breadcrumb,
19 | icon,
20 | )
21 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | ---
5 |
6 | ## ⚠️ Is your feature request related to a problem? Please describe
7 |
8 |
9 | ## 💡 Describe the solution you'd like
10 |
11 |
12 | ## 🤚 Do you want to develop this feature yourself?
13 |
14 | - [ ] Yes
15 | - [ ] No
16 |
--------------------------------------------------------------------------------
/lounge-paging/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | `module-config`
3 | com.android.library
4 | `kotlin-android`
5 | id("com.vanniktech.maven.publish")
6 | }
7 |
8 | dependencies {
9 | api(project(":lounge"))
10 | api(AndroidX.leanback)
11 | api(AndroidX.paging.runtimeKtx)
12 |
13 | implementation(Kotlin.stdlib.jdk8)
14 | implementation(KotlinX.coroutines.android)
15 |
16 | implementation(AndroidX.appCompat)
17 | implementation(AndroidX.core.ktx)
18 | implementation(AndroidX.lifecycle.runtimeKtx)
19 |
20 | testImplementation(project(":fixture"))
21 | }
22 |
--------------------------------------------------------------------------------
/lounge/src/main/java/jp/co/cyberagent/lounge/LoungeModel.kt:
--------------------------------------------------------------------------------
1 | package jp.co.cyberagent.lounge
2 |
3 | import androidx.leanback.widget.ObjectAdapter
4 | import androidx.leanback.widget.Presenter
5 |
6 | /**
7 | * Helper to connect data with [Presenter].
8 | */
9 | interface LoungeModel {
10 |
11 | /**
12 | * A key that can be used to uniquely identify this [LoungeModel] for use in [ObjectAdapter] with
13 | * stable ids.
14 | */
15 | val key: Long
16 |
17 | /**
18 | * A [Presenter] that can bind this model.
19 | */
20 | val presenter: Presenter
21 | }
22 |
--------------------------------------------------------------------------------
/lounge-paging/src/test/java/jp/co/cyberagent/lounge/paging/util/EmptyPresenter.kt:
--------------------------------------------------------------------------------
1 | package jp.co.cyberagent.lounge.paging.util
2 |
3 | import android.view.View
4 | import android.view.ViewGroup
5 | import androidx.leanback.widget.Presenter
6 |
7 | object EmptyPresenter : Presenter() {
8 | override fun onCreateViewHolder(parent: ViewGroup?): ViewHolder {
9 | return ViewHolder(View(parent?.context))
10 | }
11 |
12 | override fun onBindViewHolder(viewHolder: ViewHolder?, item: Any?) = Unit
13 |
14 | override fun onUnbindViewHolder(viewHolder: ViewHolder?) = Unit
15 | }
16 |
--------------------------------------------------------------------------------
/sample/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
10 |
11 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/sample/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
15 |
16 |
--------------------------------------------------------------------------------
/lounge/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | `module-config`
3 | com.android.library
4 | `kotlin-android`
5 | id("com.vanniktech.maven.publish")
6 | }
7 |
8 | dependencies {
9 | api(AndroidX.leanback)
10 |
11 | implementation(Kotlin.stdlib.jdk8)
12 | implementation(KotlinX.coroutines.android)
13 | implementation(AndroidX.appCompat)
14 | implementation(AndroidX.core.ktx)
15 | implementation(AndroidX.fragmentKtx)
16 | implementation(AndroidX.activityKtx)
17 | implementation(AndroidX.lifecycle.runtimeKtx)
18 |
19 | testImplementation(project(":fixture"))
20 | debugImplementation(AndroidX.fragmentTesting)
21 | }
22 |
--------------------------------------------------------------------------------
/lounge-paging/src/main/java/jp/co/cyberagent/lounge/paging/internal/CacheOp.kt:
--------------------------------------------------------------------------------
1 | package jp.co.cyberagent.lounge.paging.internal
2 |
3 | import jp.co.cyberagent.lounge.LoungeModel
4 | import kotlinx.coroutines.CompletableDeferred
5 |
6 | internal sealed class CacheOp {
7 | class Insert(val position: Int, val count: Int) : CacheOp()
8 | class Remove(val position: Int, val count: Int) : CacheOp()
9 | class Move(val fromPosition: Int, val toPosition: Int) : CacheOp()
10 | class Change(val position: Int, val count: Int) : CacheOp()
11 | class Get(val result: CompletableDeferred>) : CacheOp()
12 | object Clear : CacheOp()
13 | }
14 |
--------------------------------------------------------------------------------
/sample/src/main/java/jp/co/cyberagent/lounge/sample/utils/RandomColor.kt:
--------------------------------------------------------------------------------
1 | package jp.co.cyberagent.lounge.sample.utils
2 |
3 | private val recycle = listOf(
4 | -0xbbcca, -0x16e19d, -0x63d850, -0x98c549,
5 | -0xc0ae4b, -0xde690d, -0xfc560c, -0xff432c,
6 | -0xff6978, -0xb350b0, -0x743cb6, -0x3223c7,
7 | -0x14c5, -0x3ef9, -0x6800, -0xa8de,
8 | -0x86aab8, -0x616162, -0x9f8275, -0xcccccd
9 | )
10 |
11 | private val colors: MutableList = mutableListOf()
12 |
13 | val randomColor: Int
14 | get() {
15 | if (colors.isEmpty()) {
16 | colors += recycle
17 | colors.shuffle()
18 | }
19 | return colors.removeLast()
20 | }
21 |
--------------------------------------------------------------------------------
/fixture/src/main/java/jp/co/cyberagent/fixture/Memo.kt:
--------------------------------------------------------------------------------
1 | package jp.co.cyberagent.fixture
2 |
3 | import io.kotest.core.TestConfiguration
4 | import kotlin.properties.ReadOnlyProperty
5 |
6 | private val NOT_INITIALIZED = Any()
7 |
8 | @Suppress("UNCHECKED_CAST")
9 | fun TestConfiguration.memoized(
10 | destructor: (T) -> Unit = {},
11 | factory: () -> T,
12 | ): ReadOnlyProperty {
13 | var value: Any? = NOT_INITIALIZED
14 | beforeEach {
15 | value = factory()
16 | }
17 | afterEach {
18 | destructor(value as T)
19 | value = NOT_INITIALIZED
20 | }
21 | return ReadOnlyProperty { _, _ ->
22 | value as T
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | Changelog
2 | =========
3 | #### 1.2.0
4 |
5 | * Change: targetVersion/compileVersion update to 31
6 | * Change: AGP is updated to 7.1.1
7 | * Change: MDC is update to 1.5.0
8 |
9 | #### 1.1.2
10 |
11 | * Fix: Deadlock in `PagedListModelCache`
12 | * Change: Provide correct coroutine scope for `PagedListModelCache`
13 |
14 | #### 1.1.1
15 |
16 | * Fix: Fix `JobCancellationException` cased by sending element to closed channel
17 |
18 | #### 1.1.0
19 |
20 | * Change: Remove `workerDispatcher` from `PagedListLoungeController` constructor
21 | * Fix: Fix internal `IndexOutOfBoundsException` in `PagedListModelCache`
22 |
23 | #### 1.0.0
24 |
25 | * First release
26 |
--------------------------------------------------------------------------------
/fixture/src/main/java/jp/co/cyberagent/fixture/TestLifecycleOwner.kt:
--------------------------------------------------------------------------------
1 | package jp.co.cyberagent.fixture
2 |
3 | import androidx.lifecycle.Lifecycle
4 | import androidx.lifecycle.LifecycleOwner
5 | import androidx.lifecycle.LifecycleRegistry
6 |
7 | class TestLifecycleOwner : LifecycleOwner {
8 | private val lifecycleRegistry = LifecycleRegistry(this)
9 |
10 | override fun getLifecycle(): LifecycleRegistry = lifecycleRegistry
11 | }
12 |
13 | fun TestLifecycleOwner.withStartedThenCreated(
14 | block: () -> Unit,
15 | ) {
16 | lifecycle.currentState = Lifecycle.State.STARTED
17 | try {
18 | block()
19 | } finally {
20 | lifecycle.currentState = Lifecycle.State.CREATED
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/lounge/src/main/java/jp/co/cyberagent/lounge/DeferredLoungeModel.kt:
--------------------------------------------------------------------------------
1 | package jp.co.cyberagent.lounge
2 |
3 | /**
4 | * A [LoungeModel] whose content can be deferred.
5 | * This is useful if a [LoungeModel] need to load content asynchronously.
6 | * We can use a [LoungeControllerInterceptor] to control whether to display the model after its
7 | * content ready or display the model directly.
8 | *
9 | * @see ListRowModel
10 | * @see LoungeControllerInterceptor
11 | * @see SimpleLoungeModelAwaitInterceptor
12 | */
13 | interface DeferredLoungeModel : LoungeModel {
14 |
15 | /**
16 | * Awaits for completion of this model without blocking a thread.
17 | */
18 | suspend fun await()
19 | }
20 |
--------------------------------------------------------------------------------
/sample/src/main/java/jp/co/cyberagent/lounge/sample/model/EntryModel.kt:
--------------------------------------------------------------------------------
1 | package jp.co.cyberagent.lounge.sample.model
2 |
3 | import androidx.leanback.widget.Presenter
4 | import jp.co.cyberagent.lounge.LoungeModel
5 | import jp.co.cyberagent.lounge.databinding.SimpleDataBindingPresenter
6 | import jp.co.cyberagent.lounge.sample.BR
7 | import jp.co.cyberagent.lounge.sample.R
8 | import jp.co.cyberagent.lounge.toLoungeModelKey
9 |
10 | data class EntryModel(
11 | val name: String,
12 | val onClick: () -> Unit,
13 | ) : LoungeModel {
14 | override val key: Long = name.toLoungeModelKey()
15 |
16 | override val presenter: Presenter
17 | get() = SimpleDataBindingPresenter.get(R.layout.model_entry, BR.model)
18 | }
19 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | ---
5 |
6 | ## 🐛 Describe the bug
7 |
8 |
9 | ## ⚠️ Current behavior
10 |
11 |
12 | ## ✅ Expected behavior
13 |
14 |
15 | ## 💣 Steps to reproduce
16 |
17 |
18 | ## 📷 Screenshots
19 |
20 |
21 | ## 📱 Tech info
22 | - Device:
23 | - OS:
24 | - Library/App version:
25 |
--------------------------------------------------------------------------------
/lounge/src/main/java/jp/co/cyberagent/lounge/LambdaLoungeController.kt:
--------------------------------------------------------------------------------
1 | package jp.co.cyberagent.lounge
2 |
3 | import androidx.lifecycle.Lifecycle
4 | import kotlinx.coroutines.CoroutineDispatcher
5 | import kotlinx.coroutines.Dispatchers
6 |
7 | /**
8 | * A small wrapper around [LoungeController] that lets you implement [buildModels] by a lambda.
9 | */
10 | class LambdaLoungeController(
11 | lifecycle: Lifecycle,
12 | modelBuildingDispatcher: CoroutineDispatcher = Dispatchers.Main,
13 | ) : LoungeController(lifecycle, modelBuildingDispatcher) {
14 |
15 | /**
16 | * Forwards method [buildModels] to call this lambda.
17 | */
18 | var buildModels: suspend LoungeBuildModelScope.() -> Unit = {}
19 |
20 | override suspend fun buildModels() = buildModels(this)
21 | }
22 |
--------------------------------------------------------------------------------
/sample/src/main/java/jp/co/cyberagent/lounge/sample/model/InfoModel.kt:
--------------------------------------------------------------------------------
1 | package jp.co.cyberagent.lounge.sample.model
2 |
3 | import androidx.leanback.widget.Presenter
4 | import jp.co.cyberagent.lounge.LoungeModel
5 | import jp.co.cyberagent.lounge.databinding.SimpleDataBindingPresenter
6 | import jp.co.cyberagent.lounge.sample.BR
7 | import jp.co.cyberagent.lounge.sample.R
8 | import jp.co.cyberagent.lounge.sample.utils.randomColor
9 | import jp.co.cyberagent.lounge.toLoungeModelKey
10 |
11 | data class InfoModel(
12 | val title: String,
13 | val colorInt: Int = randomColor,
14 | ) : LoungeModel {
15 |
16 | override val key: Long = title.toLoungeModelKey()
17 |
18 | override val presenter: Presenter
19 | get() = SimpleDataBindingPresenter.get(R.layout.model_info, BR.model)
20 | }
21 |
--------------------------------------------------------------------------------
/fixture/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | `module-config`
3 | com.android.library
4 | `kotlin-android`
5 | }
6 |
7 | dependencies {
8 | implementation(Kotlin.stdlib.jdk8)
9 | implementation(KotlinX.coroutines.android)
10 | implementation(AndroidX.appCompat)
11 | implementation(AndroidX.core.ktx)
12 | implementation(AndroidX.fragmentKtx)
13 | implementation(AndroidX.activityKtx)
14 | implementation(AndroidX.lifecycle.runtimeKtx)
15 | implementation(AndroidX.leanback)
16 |
17 | api(KotlinX.coroutines.test)
18 | api(Testing.kotest.runner.junit5)
19 | api(Testing.kotest.assertions.core)
20 | api(Libs.Kotest.robolectric)
21 | api(Testing.roboElectric)
22 | api(AndroidX.test.coreKtx)
23 | api(AndroidX.test.espresso.core)
24 | api(AndroidX.test.espresso.contrib)
25 | api(AndroidX.test.rules)
26 | api(AndroidX.test.runner)
27 | }
28 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # Maven settings:
3 | VERSION_NAME=1.3.0-SNAPSHOT
4 | GROUP=jp.co.cyberagent.lounge
5 | POM_NAME=lounge
6 | POM_INCEPTION_YEAR=2020
7 | POM_URL=https://github.com/ca-love/lounge/
8 | POM_SCM_URL=https://github.com/ca-love/lounge/
9 | POM_SCM_CONNECTION=scm:git:git@github.com:ca-love/lounge.git
10 | POM_SCM_DEV_CONNECTION=scm:git:ssh://git@github.com/ca-love/lounge.git
11 | POM_LICENCE_NAME=The MIT License
12 | POM_LICENCE_URL=https://opensource.org/licenses/MIT
13 | POM_LICENCE_DIST=repo
14 | POM_DEVELOPER_ID=ca-love
15 | POM_DEVELOPER_NAME=CL
16 | POM_DEVELOPER_URL=https://www.cl-live.com
17 | RELEASE_SIGNING_ENABLED=true
18 | # Develop settings:
19 | org.gradle.jvmargs=-Xmx1536m
20 | org.gradle.vfs.watch=true
21 | android.useAndroidX=true
22 | android.enableJetifier=false
23 | kotlin.code.style=official
24 |
--------------------------------------------------------------------------------
/sample/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.kts.
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 |
--------------------------------------------------------------------------------
/lounge/src/main/java/jp/co/cyberagent/lounge/MemorizedController.kt:
--------------------------------------------------------------------------------
1 | package jp.co.cyberagent.lounge
2 |
3 | /**
4 | * Possess a [LoungeController] associated with the [key] in current [LoungeBuildModelScope].
5 | * Calling this method with the same key more than once during model building will throw [IllegalStateException].
6 | * If no [LoungeController] is associated with the [key], a new [LoungeController] will be created by the [factory].
7 | *
8 | * @see [LoungeController.possessTagDuringBuilding]
9 | */
10 | inline fun LoungeBuildModelScope.memorizedController(
11 | key: Any,
12 | noinline factory: () -> T,
13 | ): T {
14 | check(this is LoungeController) {
15 | "LoungeBuildModelScope must be a LoungeController to invoke memorizedController."
16 | }
17 | return possessTagDuringBuilding(key, T::class, factory)
18 | }
19 |
--------------------------------------------------------------------------------
/RELEASING.md:
--------------------------------------------------------------------------------
1 | # Releasing
2 |
3 | 1. Change the version in top-level `gradle.properties` to a non-SNAPSHOT version.
4 | 2. Update the `CHANGELOG.md` for the impending release.
5 | 3. Update the `README.md` with the new version.
6 | 4. `git commit -am "Prepare for release X.Y.Z."` (where X.Y.Z is the new version).
7 | 5. `./gradlew publish --no-daemon --no-parallel -PmavenCentralUsername="$NEXUS_USERNAME" -PmavenCentralPassword="$NEXUS_PASSWORD"`
8 | 6. Visit [SONATYPE](https://oss.sonatype.org/#stagingRepositories) and promote the artifact.
9 | 7. `git tag -a X.Y.X -m "X.Y.Z"` (where X.Y.Z is the new version)
10 | 8. Update the top-level `gradle.properties` to the next SNAPSHOT version.
11 | 9. `git commit -am "Prepare next development version."`
12 | 10. `git push && git push --tags`
13 |
14 | If step 5 or 6 fails, drop the Sonatype repo, fix the problem, commit, and start again at step 5.
15 |
--------------------------------------------------------------------------------
/lounge/src/test/java/jp/co/cyberagent/lounge/util/TestModel.kt:
--------------------------------------------------------------------------------
1 | package jp.co.cyberagent.lounge.util
2 |
3 | import android.view.View
4 | import android.view.ViewGroup
5 | import androidx.leanback.widget.Presenter
6 | import jp.co.cyberagent.lounge.LoungeBuildModelScope
7 | import jp.co.cyberagent.lounge.LoungeModel
8 |
9 | data class TestModel(
10 | override val key: Long,
11 | ) : LoungeModel {
12 |
13 | override val presenter: Presenter
14 | get() = Companion
15 |
16 | companion object : Presenter() {
17 | override fun onCreateViewHolder(parent: ViewGroup?): ViewHolder =
18 | ViewHolder(View(parent?.context))
19 |
20 | override fun onBindViewHolder(viewHolder: ViewHolder?, item: Any?) = Unit
21 |
22 | override fun onUnbindViewHolder(viewHolder: ViewHolder?) = Unit
23 | }
24 | }
25 |
26 | suspend fun LoungeBuildModelScope.testModel(key: Long) {
27 | +TestModel(key)
28 | }
29 |
--------------------------------------------------------------------------------
/lounge/src/main/java/jp/co/cyberagent/lounge/LoungeGuidedActionsStylist.kt:
--------------------------------------------------------------------------------
1 | package jp.co.cyberagent.lounge
2 |
3 | import androidx.leanback.widget.GuidedAction
4 | import androidx.leanback.widget.GuidedActionsStylist
5 |
6 | /**
7 | * A [GuidedActionsStylist] to use with [LoungeGuidedAction].
8 | */
9 | open class LoungeGuidedActionsStylist : GuidedActionsStylist() {
10 |
11 | override fun getItemViewType(action: GuidedAction?): Int {
12 | return if (action is LoungeGuidedAction &&
13 | action.layoutId != LoungeGuidedAction.DEFAULT_LAYOUT_ID
14 | ) {
15 | action.layoutId
16 | } else {
17 | super.getItemViewType(action)
18 | }
19 | }
20 |
21 | override fun onProvideItemLayoutId(viewType: Int): Int {
22 | return if (viewType != VIEW_TYPE_DEFAULT || viewType != VIEW_TYPE_DATE_PICKER) {
23 | viewType
24 | } else {
25 | super.onProvideItemLayoutId(viewType)
26 | }
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/.github/workflows/deploy-docs.yml:
--------------------------------------------------------------------------------
1 | name: Publish docs via GitHub Pages
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 |
8 | jobs:
9 | deploy:
10 | name: Generate API docs and deploy docs
11 | runs-on: ubuntu-latest
12 | steps:
13 | - uses: actions/checkout@v2
14 | - uses: gradle/wrapper-validation-action@v1
15 | - uses: actions/setup-java@v1
16 | with:
17 | java-version: 11
18 | - uses: actions/cache@v2
19 | with:
20 | path: ~/.gradle/caches
21 | key: gradle-${{ runner.os }}-${{ hashFiles('**/*.gradle.kts') }}-${{ hashFiles('**/gradle/wrapper/gradle-wrapper.properties') }}-${{ hashFiles('**/versions.properties') }}
22 |
23 | - name: Prepare Docs
24 | run: ./tools/prepare_docs.sh
25 |
26 | - name: Deploy docs
27 | uses: mhausenblas/mkdocs-deploy-gh-pages@master
28 | env:
29 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
30 |
--------------------------------------------------------------------------------
/tools/deploy_snapshot.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | REPOSITORY="ca-love/lounge"
4 | REF="refs/heads/master"
5 |
6 | set -e
7 |
8 | VERSION_NAME=$(grep VERSION_NAME gradle.properties)
9 |
10 | if [[ ! "$VERSION_NAME" =~ "SNAPSHOT" ]]; then
11 | echo "Skipping snapshot deployment: wrong version name '$VERSION_NAME'."
12 | elif [[ "$GITHUB_REPOSITORY" != "$REPOSITORY" ]]; then
13 | echo "Skipping snapshot deployment: wrong repository. Expected '$REPOSITORY' but was '$GITHUB_REPOSITORY'."
14 | elif [[ "$GITHUB_EVENT_NAME" != "push" ]]; then
15 | echo "Skipping snapshot deployment: was '$GITHUB_EVENT_NAME'."
16 | elif [[ "$GITHUB_REF" != "$REF" ]]; then
17 | echo "Skipping snapshot deployment: wrong ref. Expected '$REF' but was '$GITHUB_REF'."
18 | else
19 | echo "Deploying..."
20 | ./gradlew publish --no-daemon --no-parallel -PmavenCentralRepositoryUsername="$NEXUS_USERNAME" -PmavenCentralRepositoryPassword="$NEXUS_PASSWORD"
21 | echo "Deployed!"
22 | fi
23 |
--------------------------------------------------------------------------------
/lounge/src/main/java/jp/co/cyberagent/lounge/LoungeBuildModelScope.kt:
--------------------------------------------------------------------------------
1 | package jp.co.cyberagent.lounge
2 |
3 | import androidx.lifecycle.Lifecycle
4 | import kotlinx.coroutines.CoroutineDispatcher
5 |
6 | /**
7 | * A scope which can build [LoungeModel]s.
8 | */
9 | interface LoungeBuildModelScope {
10 |
11 | /**
12 | * The lifecycle of this scope's host.
13 | */
14 | val lifecycle: Lifecycle
15 |
16 | /**
17 | * The dispatcher for building models.
18 | */
19 | val modelBuildingDispatcher: CoroutineDispatcher
20 |
21 | /**
22 | * Adds a [LoungeModel] to this scope.
23 | */
24 | suspend operator fun LoungeModel.unaryPlus()
25 |
26 | /**
27 | * Adds a list of [LoungeModel]s to this scope.
28 | */
29 | suspend operator fun List.unaryPlus()
30 | }
31 |
32 | /**
33 | * Adds this model to the [LoungeBuildModelScope].
34 | */
35 | suspend fun LoungeModel.addTo(scope: LoungeBuildModelScope) = with(scope) { +this@addTo }
36 |
--------------------------------------------------------------------------------
/lounge/src/main/java/jp/co/cyberagent/lounge/AdapterDsl.kt:
--------------------------------------------------------------------------------
1 | package jp.co.cyberagent.lounge
2 |
3 | import androidx.leanback.widget.ObjectAdapter
4 | import androidx.lifecycle.Lifecycle
5 | import kotlinx.coroutines.CoroutineDispatcher
6 | import kotlinx.coroutines.Dispatchers
7 |
8 | /**
9 | * A simply function that let you construct an [ObjectAdapter] from some [LoungeModel]s directly.
10 | *
11 | * Example:
12 | *
13 | * ```
14 | * val adapter = objectAdapterWithLoungeModels(lifecycle) {
15 | * +MyModel(0)
16 | * +MyModel(1)
17 | * }
18 | * ```
19 | */
20 | fun objectAdapterWithLoungeModels(
21 | lifecycle: Lifecycle,
22 | modelBuildingDispatcher: CoroutineDispatcher = Dispatchers.Main,
23 | buildModels: suspend LoungeBuildModelScope.() -> Unit,
24 | ): ObjectAdapter {
25 | val controller = LambdaLoungeController(lifecycle, modelBuildingDispatcher)
26 | controller.buildModels = buildModels
27 | controller.requestModelBuild()
28 | return controller.adapter
29 | }
30 |
--------------------------------------------------------------------------------
/lounge-databinding/src/main/java/jp/co/cyberagent/lounge/databinding/SimpleDataBindingRowPresenter.kt:
--------------------------------------------------------------------------------
1 | package jp.co.cyberagent.lounge.databinding
2 |
3 | import androidx.annotation.LayoutRes
4 | import androidx.databinding.ViewDataBinding
5 |
6 | /**
7 | * A small wrapper around [DataBindingRowPresenter] which you can simply bind a item
8 | * to a [ViewDataBinding] via the BR id.
9 | *
10 | * @param T type of the item to be bind.
11 | * @param layoutId the DataBinding layout that the item will bind to.
12 | * @param modelVariableId the variable id ([BR](https://developer.android.com/topic/libraries/data-binding/generated-binding#dynamic_variables))
13 | * defined in the DataBinding layout.
14 | */
15 | open class SimpleDataBindingRowPresenter(
16 | @LayoutRes layoutId: Int,
17 | private val modelVariableId: Int,
18 | ) : DataBindingRowPresenter(layoutId) {
19 |
20 | override fun onBindRow(binding: ViewDataBinding, item: T) {
21 | binding.setVariable(modelVariableId, item)
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/lounge/src/main/java/jp/co/cyberagent/lounge/internal/Logger.kt:
--------------------------------------------------------------------------------
1 | package jp.co.cyberagent.lounge.internal
2 |
3 | import android.util.Log
4 | import androidx.annotation.VisibleForTesting
5 | import kotlin.system.measureNanoTime
6 |
7 | @Suppress("MagicNumber")
8 | internal inline fun logMeasureTimeMillis(
9 | enabled: Boolean,
10 | tag: String,
11 | blockName: () -> String,
12 | block: () -> Unit,
13 | ) {
14 | if (enabled) {
15 | val durationMs: Float = measureNanoTime(block).toFloat() / 1000000f
16 | log(tag, "${blockName()} in %.3f ms.".format(durationMs))
17 | } else {
18 | block()
19 | }
20 | }
21 |
22 | internal fun log(tag: String, message: String) = LoggerInstance.log(tag, message)
23 |
24 | @VisibleForTesting
25 | internal var LoggerInstance = AndroidLogger()
26 |
27 | internal interface Logger {
28 | fun log(tag: String, message: String)
29 | }
30 |
31 | internal class AndroidLogger : Logger {
32 | override fun log(tag: String, message: String) {
33 | Log.d(tag, message)
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/sample/src/main/java/jp/co/cyberagent/lounge/sample/ui/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package jp.co.cyberagent.lounge.sample.ui
2 |
3 | import android.os.Bundle
4 | import androidx.appcompat.app.AppCompatActivity
5 | import androidx.navigation.fragment.NavHostFragment
6 | import jp.co.cyberagent.lounge.LoungeController
7 | import jp.co.cyberagent.lounge.navigation.createGuidedStepFragmentNavigator
8 | import jp.co.cyberagent.lounge.sample.R
9 |
10 | class MainActivity : AppCompatActivity(R.layout.activity_main) {
11 |
12 | override fun onCreate(savedInstanceState: Bundle?) {
13 | super.onCreate(savedInstanceState)
14 | (supportFragmentManager.findFragmentById(R.id.nav_host) as? NavHostFragment)
15 | ?.navController
16 | ?.apply {
17 | navigatorProvider.addNavigator(
18 | createGuidedStepFragmentNavigator(R.id.nav_host)
19 | )
20 | setGraph(R.navigation.nav_main)
21 | }
22 | }
23 |
24 | companion object {
25 | init {
26 | LoungeController.GlobalDebugLogEnabled = true
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/docs/material.md:
--------------------------------------------------------------------------------
1 | # MaterialDesign Support
2 |
3 | Lounge provides `Theme.MaterialComponents.Leanback.Bridge`,
4 | a theme contains style attributes both required by Leanback and MaterialComponents.
5 |
6 | ## Usage
7 |
8 | ### Installation
9 |
10 | ```gradle
11 | dependencies {
12 | implementation 'jp.co.cyberagent.lounge:lounge-material:$latestVersion'
13 | }
14 | ```
15 |
16 | ### Ensure you are using AppCompatActivity
17 |
18 | ```kotlin
19 | class TvActivity : AppCompatActivity() {
20 | // Set up
21 | }
22 | ```
23 |
24 | ### Change your app theme
25 |
26 | In your theme xml files:
27 |
28 | ```xml
29 |
32 | ```
33 |
34 | In your `AndroidManifest.xml`:
35 |
36 | ```xml
37 |
41 |
44 |
45 | ```
46 |
47 | Now your can use MaterialComponents in your application.
48 |
--------------------------------------------------------------------------------
/lounge-paging/src/main/java/jp/co/cyberagent/lounge/paging/LambdaPagedListLoungeController.kt:
--------------------------------------------------------------------------------
1 | package jp.co.cyberagent.lounge.paging
2 |
3 | import androidx.lifecycle.Lifecycle
4 | import jp.co.cyberagent.lounge.LoungeModel
5 | import kotlinx.coroutines.CoroutineDispatcher
6 | import kotlinx.coroutines.Dispatchers
7 |
8 | /**
9 | * A small wrapper around [PagedListLoungeController] that lets you implement
10 | * [buildItemModel] and [buildModels] by lambdas.
11 | */
12 | class LambdaPagedListLoungeController(
13 | lifecycle: Lifecycle,
14 | modelBuildingDispatcher: CoroutineDispatcher = Dispatchers.Main,
15 | ) : PagedListLoungeController(
16 | lifecycle, modelBuildingDispatcher
17 | ) {
18 |
19 | lateinit var buildItemModel: (Int, T?) -> LoungeModel
20 |
21 | var buildModels: suspend PagedListLoungeBuildModelScope.(List) -> Unit = { +it }
22 |
23 | override fun buildItemModel(position: Int, item: T?): LoungeModel =
24 | buildItemModel.invoke(position, item)
25 |
26 | override suspend fun buildModels() = buildModels(getItemModels())
27 | }
28 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2020
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy
4 | of this software and associated documentation files (the "Software"), to deal
5 | in the Software without restriction, including without limitation the rights
6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | copies of the Software, and to permit persons to whom the Software is
8 | furnished to do so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in all
11 | copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19 | SOFTWARE.
--------------------------------------------------------------------------------
/sample/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | `module-config`
3 | com.android.application
4 | `kotlin-android`
5 | `kotlin-kapt`
6 | }
7 |
8 | android {
9 | defaultConfig {
10 | vectorDrawables.useSupportLibrary = true
11 | multiDexEnabled = true
12 | }
13 |
14 | buildFeatures {
15 | dataBinding = true
16 | }
17 |
18 | lintOptions {
19 | isWarningsAsErrors = false
20 | isAbortOnError = false
21 | }
22 | }
23 |
24 | dependencies {
25 | implementation(Kotlin.stdlib.jdk8)
26 |
27 | implementation(AndroidX.appCompat)
28 | implementation(AndroidX.constraintLayout)
29 | implementation(AndroidX.core.ktx)
30 | implementation(AndroidX.fragmentKtx)
31 | implementation(AndroidX.activityKtx)
32 | implementation(AndroidX.navigation.fragmentKtx)
33 | implementation(AndroidX.multidex)
34 | implementation(Google.android.material)
35 |
36 |
37 | implementation(project(":lounge"))
38 | implementation(project(":lounge-databinding"))
39 | implementation(project(":lounge-material"))
40 | implementation(project(":lounge-navigation"))
41 | implementation(project(":lounge-paging"))
42 | }
43 |
--------------------------------------------------------------------------------
/lounge/src/test/java/jp/co/cyberagent/lounge/AdapterDslTest.kt:
--------------------------------------------------------------------------------
1 | package jp.co.cyberagent.lounge
2 |
3 | import androidx.lifecycle.Lifecycle
4 | import io.kotest.core.spec.style.FunSpec
5 | import io.kotest.extensions.robolectric.RobolectricTest
6 | import io.kotest.matchers.shouldBe
7 | import jp.co.cyberagent.fixture.TestLifecycleOwner
8 | import jp.co.cyberagent.fixture.memoized
9 | import jp.co.cyberagent.lounge.util.TestModel
10 | import kotlinx.coroutines.ExperimentalCoroutinesApi
11 | import kotlinx.coroutines.test.TestCoroutineDispatcher
12 |
13 | @ExperimentalCoroutinesApi
14 | @RobolectricTest
15 | class AdapterDslTest : FunSpec({
16 | val owner by memoized { TestLifecycleOwner() }
17 | val dispatcher by memoized { TestCoroutineDispatcher() }
18 |
19 | test("objectAdapterWithLoungeModels") {
20 | val adapter = objectAdapterWithLoungeModels(owner.lifecycle, dispatcher) {
21 | +TestModel(1L)
22 | +TestModel(10L)
23 | }
24 |
25 | owner.lifecycle.currentState = Lifecycle.State.STARTED
26 | adapter.size() shouldBe 2
27 | adapter.get(0) shouldBe TestModel(1L)
28 | adapter.get(1) shouldBe TestModel(10L)
29 | }
30 | })
31 |
--------------------------------------------------------------------------------
/lounge-navigation/src/main/java/jp/co/cyberagent/lounge/navigation/BrowseSupportFragmentBackPressedCallback.kt:
--------------------------------------------------------------------------------
1 | package jp.co.cyberagent.lounge.navigation
2 |
3 | import androidx.activity.OnBackPressedCallback
4 | import androidx.activity.addCallback
5 | import androidx.leanback.app.BrowseSupportFragment
6 | import androidx.lifecycle.LifecycleOwner
7 |
8 | /**
9 | * A helper method that constructs a [OnBackPressedCallback] to simulate back press behavior when
10 | * [BrowseSupportFragment.isHeadersTransitionOnBackEnabled] equals to true.
11 | * This is useful when you use [BrowseSupportFragment] in a single activity architecture that
12 | * parent activity can has its own onBackPressed handling.
13 | */
14 | fun BrowseSupportFragment.addHeadersTransitionOnBackPressedCallback(
15 | owner: LifecycleOwner? = this,
16 | ): OnBackPressedCallback {
17 | val dispatcher = requireActivity().onBackPressedDispatcher
18 | return dispatcher.addCallback(owner) {
19 | if (isShowingHeaders) {
20 | isEnabled = false
21 | dispatcher.onBackPressed()
22 | isEnabled = true
23 | } else {
24 | startHeadersTransition(true)
25 | }
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/lounge/src/main/java/jp/co/cyberagent/lounge/LoungeControllerInterceptor.kt:
--------------------------------------------------------------------------------
1 | package jp.co.cyberagent.lounge
2 |
3 | /**
4 | * Intercept the lifecycle of the [LoungeController].
5 | * You can add a interceptor via [LoungeController.addInterceptor].
6 | */
7 | interface LoungeControllerInterceptor {
8 |
9 | /**
10 | * Callback before building models.
11 | *
12 | * @param controller the intercept target.
13 | */
14 | suspend fun beforeBuildModels(
15 | controller: LoungeController,
16 | ) = Unit
17 |
18 | /**
19 | * Callback after building models.
20 | * You can modify built [models].
21 | *
22 | * @param controller the intercept target.
23 | */
24 | suspend fun afterBuildModels(
25 | controller: LoungeController,
26 | models: MutableList,
27 | ) = Unit
28 |
29 | /**
30 | * Callback before add a [LoungeModel] to the controller.
31 | *
32 | * @param controller the intercept target.
33 | * @param addPosition the index of the model if added.
34 | * @param model the model before add.
35 | */
36 | suspend fun beforeAddModel(
37 | controller: LoungeController,
38 | addPosition: Int,
39 | model: LoungeModel,
40 | ) = Unit
41 | }
42 |
--------------------------------------------------------------------------------
/lounge/src/main/java/jp/co/cyberagent/lounge/internal/LoungeAdapter.kt:
--------------------------------------------------------------------------------
1 | package jp.co.cyberagent.lounge.internal
2 |
3 | import androidx.leanback.widget.ArrayObjectAdapter
4 | import androidx.leanback.widget.Presenter
5 | import androidx.leanback.widget.PresenterSelector
6 | import jp.co.cyberagent.lounge.LoungeModel
7 |
8 | internal class LoungeAdapter : ArrayObjectAdapter() {
9 |
10 | init {
11 | presenterSelector = LoungeModelPresenterSelector()
12 | setHasStableIds(true)
13 | }
14 |
15 | var listener: Listener? = null
16 |
17 | override fun get(position: Int): Any {
18 | listener?.onGetItemAt(position)
19 | return super.get(position)
20 | }
21 |
22 | override fun getId(position: Int): Long {
23 | return (super.get(position) as LoungeModel).key
24 | }
25 |
26 | fun interface Listener {
27 | fun onGetItemAt(position: Int)
28 | }
29 | }
30 |
31 | private class LoungeModelPresenterSelector : PresenterSelector() {
32 |
33 | override fun getPresenter(item: Any?): Presenter {
34 | require(item is LoungeModel) { "Require LoungeModel but get $item." }
35 | return item.presenter
36 | }
37 |
38 | override fun getPresenters(): Array = emptyArray()
39 | }
40 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Lounge
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | Lounge is an Android library for building Leanback user interface required by Android TV.
10 |
11 | ## Installation
12 |
13 | Add dependencies to module `build.gradle`:
14 |
15 | ```gradle
16 | dependencies {
17 | // Leanckback helper for ObjectAdapter, Presenter, GuidedAction and et al.
18 | implementation("jp.co.cyberagent.lounge:lounge:$latestVersion")
19 |
20 | // Paging Support:
21 | implementation("jp.co.cyberagent.lounge:lounge-paging:$latestVersion")
22 | // DataBinding Support:
23 | implementation("jp.co.cyberagent.lounge:lounge-databinding:$latestVersion")
24 | // Navigation Component Support:
25 | implementation("jp.co.cyberagent.lounge:lounge-navigation:$latestVersion")
26 | // Material Design Support:
27 | implementation("jp.co.cyberagent.lounge:lounge-material:$latestVersion")
28 | }
29 | ```
30 |
31 | ## Documentation
32 |
33 | Check out Lounge's [full documentation here](https://ca-love.github.io/lounge/).
34 |
35 | ## Contributing
36 |
37 | Feel free to open a issue or submit a pull request for any bugs/improvements.
38 |
--------------------------------------------------------------------------------
/sample/src/main/res/layout/model_info.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
11 |
12 |
20 |
21 |
26 |
27 |
38 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/sample/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 |
9 |
13 |
17 |
18 |
28 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE:
--------------------------------------------------------------------------------
1 |
2 |
3 | ## 🚀 Description
4 |
5 |
6 | ## 📄 Motivation and Context
7 |
8 |
9 |
10 | ## 🧪 How Has This Been Tested?
11 |
12 |
13 |
14 |
15 | ## 📷 Screenshots (if appropriate)
16 |
17 |
18 | ## 📦 Types of changes
19 |
20 | - [ ] Bug fix (non-breaking change which fixes an issue)
21 | - [ ] New feature (non-breaking change which adds functionality)
22 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
23 |
24 | ## ✅ Checklist
25 |
26 |
27 | - [ ] My code follows the code style of this project.
28 | - [ ] My change requires a change to the documentation.
29 | - [ ] I have updated the documentation accordingly.
--------------------------------------------------------------------------------
/lounge/src/test/java/jp/co/cyberagent/lounge/LambdaLoungeControllerTest.kt:
--------------------------------------------------------------------------------
1 | package jp.co.cyberagent.lounge
2 |
3 | import androidx.lifecycle.Lifecycle
4 | import io.kotest.core.spec.style.FunSpec
5 | import io.kotest.extensions.robolectric.RobolectricTest
6 | import io.kotest.matchers.shouldBe
7 | import jp.co.cyberagent.fixture.TestLifecycleOwner
8 | import jp.co.cyberagent.fixture.memoized
9 | import jp.co.cyberagent.lounge.util.TestModel
10 | import kotlinx.coroutines.Dispatchers
11 | import kotlinx.coroutines.ExperimentalCoroutinesApi
12 | import kotlinx.coroutines.test.TestCoroutineDispatcher
13 | import kotlinx.coroutines.test.setMain
14 |
15 | @ExperimentalCoroutinesApi
16 | @RobolectricTest
17 | class LambdaLoungeControllerTest : FunSpec({
18 |
19 | val owner by memoized { TestLifecycleOwner() }
20 | val dispatcher by memoized { TestCoroutineDispatcher() }
21 |
22 | beforeEach {
23 | Dispatchers.setMain(dispatcher)
24 | }
25 |
26 | test("Build models") {
27 | val models = List(10) { TestModel(it + 1L) }
28 | val controller = LambdaLoungeController(owner.lifecycle, dispatcher).apply {
29 | buildModels = { +models }
30 | }
31 |
32 | controller.requestModelBuild()
33 | owner.lifecycle.currentState = Lifecycle.State.STARTED
34 |
35 | models.forEachIndexed { index, testModel ->
36 | controller.adapter[index] shouldBe testModel
37 | }
38 | controller.adapter.size() shouldBe models.size
39 | }
40 | })
41 |
--------------------------------------------------------------------------------
/lounge/src/main/java/jp/co/cyberagent/lounge/LoungeGuidedStepSupportFragment.kt:
--------------------------------------------------------------------------------
1 | package jp.co.cyberagent.lounge
2 |
3 | import android.os.Bundle
4 | import android.view.View
5 | import androidx.leanback.app.GuidedStepSupportFragment
6 | import androidx.leanback.widget.GuidedAction
7 | import androidx.leanback.widget.GuidedActionsStylist
8 |
9 | /**
10 | * A subclass of [GuidedStepSupportFragment] with proper implementation for using [LoungeGuidedAction].
11 | *
12 | * @see createGuidedActions
13 | */
14 | open class LoungeGuidedStepSupportFragment : GuidedStepSupportFragment() {
15 |
16 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
17 | super.onViewCreated(view, savedInstanceState)
18 | setActionsDiffCallback(LoungeGuidedActionDiffCallback)
19 | }
20 |
21 | override fun onCreateActionsStylist(): GuidedActionsStylist {
22 | return LoungeGuidedActionsStylist()
23 | }
24 |
25 | override fun onGuidedActionClicked(action: GuidedAction?) {
26 | onLoungeGuidedActionClicked(action)
27 | }
28 |
29 | override fun onSubGuidedActionClicked(action: GuidedAction?): Boolean {
30 | return onSubLoungeGuidedActionClicked(action)
31 | }
32 |
33 | override fun onGuidedActionFocused(action: GuidedAction?) {
34 | onLoungeGuidedActionFocused(action)
35 | }
36 |
37 | override fun onGuidedActionEditedAndProceed(action: GuidedAction?): Long {
38 | return onLoungeGuidedActionEditedAndProceed(action)
39 | }
40 |
41 | override fun onGuidedActionEditCanceled(action: GuidedAction?) {
42 | onLoungeGuidedActionEditCanceled(action)
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/sample/src/main/java/jp/co/cyberagent/lounge/sample/model/TextModel.kt:
--------------------------------------------------------------------------------
1 | package jp.co.cyberagent.lounge.sample.model
2 |
3 | import android.view.ViewGroup
4 | import android.widget.TextView
5 | import androidx.core.view.setPadding
6 | import androidx.leanback.widget.Presenter
7 | import com.google.android.material.textview.MaterialTextView
8 | import jp.co.cyberagent.lounge.LoungeModel
9 | import jp.co.cyberagent.lounge.TypedPresenter
10 | import jp.co.cyberagent.lounge.sample.utils.randomColor
11 | import jp.co.cyberagent.lounge.toLoungeModelKey
12 |
13 | data class TextModel(
14 | val title: String,
15 | override val key: Long = title.toLoungeModelKey(),
16 | ) : LoungeModel {
17 |
18 | override val presenter: Presenter = TextModelPresenter
19 | }
20 |
21 | @Suppress("MagicNumber")
22 | private object TextModelPresenter : TypedPresenter() {
23 |
24 | override fun onCreate(parent: ViewGroup): ViewHolder {
25 | val textView = MaterialTextView(parent.context).apply {
26 | layoutParams = ViewGroup.LayoutParams(200, 120)
27 | isFocusable = true
28 | isFocusableInTouchMode = true
29 | setPadding(32)
30 | setBackgroundColor(randomColor)
31 | @Suppress("DEPRECATION")
32 | setTextAppearance(context, com.google.android.material.R.style.TextAppearance_AppCompat_Body1)
33 | }
34 | return ViewHolder(textView)
35 | }
36 |
37 | override fun onBind(vh: ViewHolder, item: TextModel) {
38 | vh.textView.text = item.title
39 | }
40 |
41 | class ViewHolder(val textView: TextView) : Presenter.ViewHolder(textView)
42 | }
43 |
--------------------------------------------------------------------------------
/sample/src/main/res/layout/model_entry.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
11 |
12 |
21 |
22 |
31 |
32 |
43 |
44 |
45 |
46 |
--------------------------------------------------------------------------------
/lounge/src/main/java/jp/co/cyberagent/lounge/SimpleLoungeModelAwaitInterceptor.kt:
--------------------------------------------------------------------------------
1 | package jp.co.cyberagent.lounge
2 |
3 | /**
4 | * An interceptor that can be used with [DeferredLoungeModel].
5 | * Await the completion of first few models before add them to the controller.
6 | *
7 | * Takes the [ListRowModel] as an example. The [ListRowModel] has its own [LoungeController] and
8 | * it may need some time to complete its first models building.
9 | * In the first rendering, its header data can already become visible before the completion of its row data.
10 | * And this is not our desired behavior.
11 | * Use this interceptor, we can ensure to display as soon as the first few visible rows complete,
12 | * and keep other invisible rows load content asynchronously.
13 | *
14 | * @param awaitNumOfModels the number of models to await. The default value will await all models
15 | * to be completion.
16 | */
17 | class SimpleLoungeModelAwaitInterceptor(
18 | private val awaitNumOfModels: Int = AWAIT_NUM_UNLIMITED,
19 | ) : LoungeControllerInterceptor {
20 |
21 | init {
22 | check(awaitNumOfModels >= AWAIT_NUM_UNLIMITED) {
23 | "LoungeModelAwaitInterceptor awaitNum must be at least -1, but $awaitNumOfModels was specified."
24 | }
25 | }
26 |
27 | override suspend fun beforeAddModel(
28 | controller: LoungeController,
29 | addPosition: Int,
30 | model: LoungeModel,
31 | ) {
32 | if (addPosition >= awaitNumOfModels && awaitNumOfModels != AWAIT_NUM_UNLIMITED) return
33 | if (model is DeferredLoungeModel) {
34 | model.await()
35 | }
36 | }
37 |
38 | companion object {
39 | private const val AWAIT_NUM_UNLIMITED = -1
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/sample/src/main/java/jp/co/cyberagent/lounge/sample/ui/HomeFragment.kt:
--------------------------------------------------------------------------------
1 | package jp.co.cyberagent.lounge.sample.ui
2 |
3 | import android.content.Context
4 | import android.os.Bundle
5 | import androidx.appcompat.view.ContextThemeWrapper
6 | import androidx.leanback.app.BrowseSupportFragment
7 | import androidx.navigation.fragment.findNavController
8 | import jp.co.cyberagent.lounge.listRowOf
9 | import jp.co.cyberagent.lounge.objectAdapterWithLoungeModels
10 | import jp.co.cyberagent.lounge.sample.R
11 | import jp.co.cyberagent.lounge.sample.model.EntryModel
12 |
13 | class HomeFragment : BrowseSupportFragment() {
14 |
15 | override fun onCreate(savedInstanceState: Bundle?) {
16 | super.onCreate(savedInstanceState)
17 | title = "Home"
18 | headersState = HEADERS_DISABLED
19 | isHeadersTransitionOnBackEnabled = false
20 | adapter = objectAdapterWithLoungeModels(lifecycle) {
21 | listRowOf(
22 | headerData = null,
23 | key = "entries"
24 | ) {
25 | +EntryModel("Rows Example") {
26 | findNavController().navigate(R.id.to_rows)
27 | }
28 |
29 | +EntryModel("Vertical Grid Example") {
30 | findNavController().navigate(R.id.to_vertical_grid)
31 | }
32 |
33 | +EntryModel("Guided Step Example") {
34 | findNavController().navigate(R.id.to_guided_step)
35 | }
36 |
37 | +EntryModel("PagedList Stress Test") {
38 | findNavController().navigate(R.id.fragment_paged_list_stress_test)
39 | }
40 | }
41 | }
42 | }
43 |
44 | override fun getContext(): Context? {
45 | return super.getContext()?.let { ContextThemeWrapper(it, R.style.AppTheme_Home) }
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/lounge-paging/src/test/java/jp/co/cyberagent/lounge/paging/util/Item.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018 The Android Open Source Project
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | *
16 | * For from airbnb/epoxy
17 | */
18 | package jp.co.cyberagent.lounge.paging.util
19 |
20 | import androidx.leanback.widget.Presenter
21 | import androidx.recyclerview.widget.DiffUtil
22 | import jp.co.cyberagent.lounge.LoungeModel
23 |
24 | /**
25 | * Dummy item for testing.
26 | */
27 | data class Item(val id: Int, val value: String) {
28 | companion object {
29 | val DIFF_CALLBACK = object : DiffUtil.ItemCallback- () {
30 | override fun areItemsTheSame(oldItem: Item, newItem: Item) = oldItem.id == newItem.id
31 |
32 | override fun areContentsTheSame(oldItem: Item, newItem: Item) = oldItem == newItem
33 | }
34 | }
35 | }
36 |
37 | class FakePlaceholderModel(val pos: Int) : LoungeModel {
38 | override val key = -pos.toLong()
39 | override val presenter: Presenter = EmptyPresenter
40 | }
41 |
42 | class FakeModel(val item: Item) : LoungeModel {
43 | override val key = item.id.toLong()
44 | override val presenter: Presenter = EmptyPresenter
45 | }
46 |
--------------------------------------------------------------------------------
/sample/src/main/java/jp/co/cyberagent/lounge/sample/utils/PagedListCreator.kt:
--------------------------------------------------------------------------------
1 | package jp.co.cyberagent.lounge.sample.utils
2 |
3 | import android.util.Log
4 | import androidx.paging.Config
5 | import androidx.paging.ItemKeyedDataSource
6 | import androidx.paging.PagedList
7 | import kotlinx.coroutines.Dispatchers
8 | import kotlinx.coroutines.GlobalScope
9 | import kotlinx.coroutines.asExecutor
10 | import kotlinx.coroutines.delay
11 | import kotlinx.coroutines.launch
12 | import kotlin.math.min
13 |
14 | fun List.asPagedList(pageSize: Int = 10, enablePlaceholders: Boolean): PagedList {
15 | val config = Config(pageSize, enablePlaceholders = enablePlaceholders)
16 | return PagedList(
17 | DataSource(this),
18 | config,
19 | Dispatchers.Main.asExecutor(),
20 | Dispatchers.IO.asExecutor(),
21 | )
22 | }
23 |
24 | private class DataSource(
25 | private val list: List,
26 | ) : ItemKeyedDataSource() {
27 |
28 | override fun loadInitial(params: LoadInitialParams, callback: LoadInitialCallback) {
29 | callback.onResult(list.take(params.requestedLoadSize), 0, list.size)
30 | }
31 |
32 | override fun loadAfter(params: LoadParams, callback: LoadCallback) {
33 | GlobalScope.launch {
34 | delay(1000)
35 | val start = params.key + 1
36 | val end = min(params.key + params.requestedLoadSize, list.lastIndex)
37 | Log.d("PagedList", "${list.size}, $start, $end, ${params.key}, ${params.requestedLoadSize}")
38 | callback.onResult(list.slice(start..end))
39 | }
40 | }
41 |
42 | override fun loadBefore(params: LoadParams, callback: LoadCallback) = Unit
43 |
44 | override fun getKey(item: T): Int = list.indexOf(item)
45 | }
46 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | paths-ignore:
8 | - "**.md"
9 | pull_request:
10 | paths-ignore:
11 | - "**.md"
12 |
13 | jobs:
14 | check:
15 | name: Check
16 | runs-on: ubuntu-latest
17 | timeout-minutes: 10
18 | steps:
19 | - uses: actions/checkout@v2
20 | - uses: gradle/wrapper-validation-action@v1
21 | - uses: actions/setup-java@v1
22 | with:
23 | java-version: 11
24 | - uses: actions/cache@v2
25 | with:
26 | path: ~/.gradle/caches
27 | key: gradle-${{ runner.os }}-${{ hashFiles('**/*.gradle.kts') }}-${{ hashFiles('**/gradle/wrapper/gradle-wrapper.properties') }}-${{ hashFiles('**/versions.properties') }}
28 |
29 | - name: Check
30 | run: ./gradlew lint detekt
31 |
32 | # Check if there has been a binary incompatible change to the API.
33 | # If this change is intentional, run `./gradlew apiDump` and commit the new API files.
34 | # - name: Check binary compatibility
35 | # run: ./gradlew apiCheck
36 |
37 | unit-tests:
38 | name: Unit tests
39 | runs-on: ubuntu-latest
40 | timeout-minutes: 10
41 | steps:
42 | - uses: actions/checkout@v2
43 | - uses: gradle/wrapper-validation-action@v1
44 | - uses: actions/setup-java@v1
45 | with:
46 | java-version: 11
47 | - uses: actions/cache@v2
48 | with:
49 | path: ~/.gradle/caches
50 | key: gradle-${{ runner.os }}-${{ hashFiles('**/*.gradle.kts') }}-${{ hashFiles('**/gradle/wrapper/gradle-wrapper.properties') }}-${{ hashFiles('**/versions.properties') }}
51 |
52 | - name: Unit tests
53 | run: ./gradlew testDebugUnitTest
54 |
--------------------------------------------------------------------------------
/sample/src/main/res/drawable-v24/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
8 |
9 |
10 |
17 |
21 |
25 |
26 |
27 |
28 |
35 |
36 |
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | import de.fayard.refreshVersions.bootstrapRefreshVersions
2 | import de.fayard.refreshVersions.migrateRefreshVersionsIfNeeded
3 |
4 | pluginManagement {
5 | repositories {
6 | gradlePluginPortal()
7 | google()
8 | mavenCentral()
9 | jcenter()
10 | }
11 | }
12 |
13 | buildscript {
14 | repositories { gradlePluginPortal() }
15 | dependencies.classpath("de.fayard.refreshVersions:refreshVersions:0.9.7")
16 | //// # available:0.10.0")
17 | //// # available:0.10.1")
18 | //// # available:0.11.0")
19 | //// # available:0.20.0")
20 | //// # available:0.21.0")
21 | //// # available:0.22.0")
22 | //// # available:0.23.0")
23 | //// # available:0.30.0")
24 | //// # available:0.30.1")
25 | //// # available:0.30.2")
26 | //// # available:0.40.0")
27 | //// # available:0.40.1")
28 | }
29 |
30 | migrateRefreshVersionsIfNeeded("0.9.7") // Will be automatically removed by refreshVersions when upgraded to the latest version.
31 |
32 | bootstrapRefreshVersions()
33 |
34 | rootProject.name = "lounge"
35 |
36 | include(
37 | "sample",
38 | "fixture",
39 | "lounge",
40 | "lounge-databinding",
41 | "lounge-material",
42 | "lounge-navigation",
43 | "lounge-paging"
44 | )
45 |
--------------------------------------------------------------------------------
/mkdocs.yml:
--------------------------------------------------------------------------------
1 | site_name: "Lounge"
2 | repo_name: ca-love/lounge
3 | repo_url: https://github.com/ca-love/lounge
4 | remote_branch: gh-pages
5 |
6 | theme:
7 | name: material
8 | language: "en"
9 | palette:
10 | primary: red
11 | accent: red
12 | font:
13 | text: Open Sans
14 | code: Roboto Mono
15 | features:
16 | - navigation.expand
17 |
18 | extra:
19 | manifest: manifest.webmanifest
20 |
21 | nav:
22 | - "Overview": index.md
23 | - "ObjectAdapter Support": object-adapter.md
24 | - "GuidedStep Support": guided-step.md
25 | - "Paging Support": wip.md
26 | - "MaterialDesign Support": material.md
27 | - "Navigation Support": wip.md
28 | - "Changelog": changelog.md
29 | - "API Docs":
30 | - "lounge": api/lounge/lounge
31 | - "lounge-databinding": api/lounge-databinding/lounge-databinding
32 | - "lounge-paging": api/lounge-paging/lounge-paging
33 | - "lounge-material": api/lounge-material/lounge-material
34 | - "lounge-navigation": api/lounge-navigation/lounge-navigation
35 |
36 | markdown_extensions:
37 | - admonition
38 | - smarty
39 | - codehilite:
40 | guess_lang: false
41 | linenums: True
42 | - footnotes
43 | - meta
44 | - toc:
45 | permalink: true
46 | - pymdownx.arithmatex
47 | - pymdownx.betterem:
48 | smart_enable: all
49 | - pymdownx.caret
50 | - pymdownx.critic
51 | - pymdownx.details
52 | - pymdownx.emoji:
53 | emoji_index: !!python/name:materialx.emoji.twemoji
54 | emoji_generator: !!python/name:materialx.emoji.to_svg
55 | - pymdownx.inlinehilite
56 | - pymdownx.magiclink
57 | - pymdownx.mark
58 | - pymdownx.smartsymbols
59 | - pymdownx.superfences
60 | - pymdownx.tasklist:
61 | custom_checkbox: true
62 | - pymdownx.tabbed
63 | - pymdownx.tilde
64 | - tables
65 |
66 | plugins:
67 | - search
68 | - minify:
69 | minify_html: true
70 |
--------------------------------------------------------------------------------
/sample/src/main/res/navigation/nav_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
12 |
16 |
20 |
24 |
25 |
26 |
30 |
31 |
35 |
36 |
40 |
44 |
50 |
51 |
52 |
56 |
57 |
58 |
--------------------------------------------------------------------------------
/lounge-databinding/src/main/java/jp/co/cyberagent/lounge/databinding/SimpleDataBindingPresenter.kt:
--------------------------------------------------------------------------------
1 | package jp.co.cyberagent.lounge.databinding
2 |
3 | import androidx.annotation.LayoutRes
4 | import androidx.databinding.ViewDataBinding
5 | import jp.co.cyberagent.lounge.LoungeModel
6 | import java.util.concurrent.ConcurrentHashMap
7 |
8 | /**
9 | * A small wrapper around [DataBindingPresenter] which you can simply bind a item
10 | * to a [ViewDataBinding] via the BR id.
11 | *
12 | * @param T type of the item to be bind.
13 | * @param layoutId the DataBinding layout that the item will bind to.
14 | * @param modelVariableId the variable id ([BR](https://developer.android.com/topic/libraries/data-binding/generated-binding#dynamic_variables))
15 | * defined in the DataBinding layout.
16 | */
17 | open class SimpleDataBindingPresenter(
18 | @LayoutRes layoutId: Int,
19 | private val modelVariableId: Int,
20 | ) : DataBindingPresenter(layoutId) {
21 |
22 | override fun onBind(binding: ViewDataBinding, item: T) {
23 | binding.setVariable(modelVariableId, item)
24 | }
25 |
26 | companion object {
27 |
28 | private val cache = ConcurrentHashMap>()
29 |
30 | /**
31 | * Returns the cached presenter for the key that corresponded to the given [layoutId]
32 | * and [modelVariableId]. If the presenter not presented in the cache yet, then a new
33 | * [SimpleDataBindingPresenter] will be created and puts it into the cache, then returns it.
34 | */
35 | fun get(
36 | layoutId: Int,
37 | modelVariableId: Int,
38 | ): SimpleDataBindingPresenter {
39 | val key = (layoutId.toLong() shl 32) or modelVariableId.toLong()
40 | return cache.getOrPut(key) {
41 | SimpleDataBindingPresenter(layoutId, modelVariableId)
42 | }
43 | }
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/lounge/src/test/java/jp/co/cyberagent/lounge/LoungeGuidedActionsStylistTest.kt:
--------------------------------------------------------------------------------
1 | package jp.co.cyberagent.lounge
2 |
3 | import android.content.Context
4 | import android.os.Bundle
5 | import androidx.fragment.app.testing.launchFragmentInContainer
6 | import androidx.leanback.app.GuidedStepSupportFragment
7 | import androidx.leanback.widget.GuidedAction
8 | import androidx.leanback.widget.GuidedActionsStylist
9 | import androidx.test.core.app.ApplicationProvider
10 | import androidx.test.espresso.Espresso.onView
11 | import androidx.test.espresso.assertion.ViewAssertions.matches
12 | import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
13 | import androidx.test.espresso.matcher.ViewMatchers.withText
14 | import io.kotest.core.spec.style.FunSpec
15 | import io.kotest.extensions.robolectric.RobolectricTest
16 | import jp.co.cyberagent.fixture.R
17 |
18 | @RobolectricTest
19 | class LoungeGuidedActionsStylistTest : FunSpec({
20 |
21 | test("LoungeGuidedActionStylist") {
22 | val testFragment = TestFragment()
23 | launchFragmentInContainer(themeResId = androidx.leanback.R.style.Theme_Leanback) { testFragment }
24 |
25 | val context = ApplicationProvider.getApplicationContext()
26 | onView(withText("Normal")).check(matches(isDisplayed()))
27 | onView(withText(context.getString(R.string.test_guided_action))).check(matches(isDisplayed()))
28 | }
29 | })
30 |
31 | class TestFragment : GuidedStepSupportFragment() {
32 | override fun onCreateActionsStylist(): GuidedActionsStylist {
33 | return LoungeGuidedActionsStylist()
34 | }
35 |
36 | override fun onCreateActions(actions: MutableList, savedInstanceState: Bundle?) {
37 | actions += createGuidedActions(requireContext()) {
38 | guidedAction {
39 | title("Normal")
40 | }
41 |
42 | guidedAction {
43 | layoutId(R.layout.layout_test_guided_action_custom)
44 | }
45 | }
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/lounge-paging/src/test/java/jp/co/cyberagent/lounge/paging/util/PagedListUtil.kt:
--------------------------------------------------------------------------------
1 | package jp.co.cyberagent.lounge.paging.util
2 |
3 | import androidx.paging.Config
4 | import androidx.paging.ItemKeyedDataSource
5 | import androidx.paging.PagedList
6 | import kotlin.math.max
7 | import kotlin.math.min
8 |
9 | const val DefaultInitialPageMultiplier = 2
10 |
11 | fun List.asPagedList(
12 | pageSize: Int = size,
13 | initialPosition: Int = 0,
14 | enablePlaceholders: Boolean = false,
15 | ): PagedList {
16 | val config = Config(
17 | pageSize = pageSize,
18 | initialLoadSizeHint = pageSize * DefaultInitialPageMultiplier,
19 | enablePlaceholders = enablePlaceholders,
20 | )
21 | return PagedList(
22 | dataSource = DataSource(this),
23 | config = config,
24 | notifyExecutor = { it.run() },
25 | fetchExecutor = { it.run() },
26 | initialKey = initialPosition,
27 | )
28 | }
29 |
30 | private class DataSource(
31 | private val list: List,
32 | ) : ItemKeyedDataSource() {
33 |
34 | override fun loadInitial(params: LoadInitialParams, callback: LoadInitialCallback) {
35 | val start = params.requestedInitialKey ?: 0
36 | val end = min(start + params.requestedLoadSize - 1, list.lastIndex)
37 | callback.onResult(
38 | list.slice(start..end),
39 | start,
40 | list.size,
41 | )
42 | }
43 |
44 | override fun loadAfter(params: LoadParams, callback: LoadCallback) {
45 | val start = params.key + 1
46 | val end = min(params.key + params.requestedLoadSize, list.lastIndex)
47 | callback.onResult(list.slice(start..end))
48 | }
49 |
50 | override fun loadBefore(params: LoadParams, callback: LoadCallback) {
51 | val end = params.key - 1
52 | val start = max(params.key - params.requestedLoadSize, 0)
53 | callback.onResult(list.slice(start..end))
54 | }
55 |
56 | override fun getKey(item: T): Int = list.indexOf(item)
57 | }
58 |
--------------------------------------------------------------------------------
/sample/src/main/java/jp/co/cyberagent/lounge/sample/ui/VerticalGridExampleFragment.kt:
--------------------------------------------------------------------------------
1 | package jp.co.cyberagent.lounge.sample.ui
2 |
3 | import android.os.Bundle
4 | import androidx.leanback.app.VerticalGridSupportFragment
5 | import androidx.leanback.widget.VerticalGridPresenter
6 | import androidx.lifecycle.coroutineScope
7 | import jp.co.cyberagent.lounge.LoungeController
8 | import jp.co.cyberagent.lounge.loungeProp
9 | import jp.co.cyberagent.lounge.sample.model.InfoModel
10 | import kotlinx.coroutines.delay
11 | import kotlin.random.Random
12 |
13 | @Suppress("MagicNumber")
14 | class VerticalGridExampleFragment : VerticalGridSupportFragment() {
15 |
16 | private val updateInterval = 3000L
17 | private val random = Random(System.currentTimeMillis())
18 | private val controller = Controller()
19 |
20 | override fun onCreate(savedInstanceState: Bundle?) {
21 | super.onCreate(savedInstanceState)
22 | title = "VerticalGrid Example"
23 | gridPresenter = VerticalGridPresenter().apply {
24 | numberOfColumns = 5
25 | }
26 | adapter = controller.adapter
27 | lifecycle.coroutineScope.launchWhenStarted {
28 | var cnt = 0
29 | while (true) {
30 | controller.infoList = List(random.nextInt(10, 1000)) {
31 | when {
32 | cnt % 15 == 0 -> "FooBar $it"
33 | cnt % 3 == 0 -> "Foo $it"
34 | cnt % 5 == 0 -> "Bar $it"
35 | else -> it.toString()
36 | }
37 | }
38 | title = "VerticalGrid Example $cnt"
39 | cnt++
40 | delay(updateInterval)
41 | }
42 | }
43 | }
44 |
45 | inner class Controller : LoungeController(lifecycle) {
46 |
47 | init {
48 | debugName = "VerticalGridExample"
49 | }
50 |
51 | var infoList: List by loungeProp(emptyList())
52 |
53 | override suspend fun buildModels() {
54 | infoList.forEach { title ->
55 | +InfoModel(title)
56 | }
57 | }
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/lounge/src/main/java/jp/co/cyberagent/lounge/TypedPresenter.kt:
--------------------------------------------------------------------------------
1 | package jp.co.cyberagent.lounge
2 |
3 | import android.view.ViewGroup
4 | import androidx.leanback.widget.Presenter
5 |
6 | /**
7 | * A wrapper around [Presenter] which provides generic typed APIs.
8 | */
9 | @Suppress("UNCHECKED_CAST")
10 | abstract class TypedPresenter : Presenter() {
11 |
12 | /**
13 | * Creates a new ViewHolder.
14 | */
15 | protected abstract fun onCreate(parent: ViewGroup): VH
16 |
17 | /**
18 | * Binds a ViewHolder to an item.
19 | */
20 | protected abstract fun onBind(vh: VH, item: T)
21 |
22 | /**
23 | * Binds a ViewHolder to an item with a list of payloads.
24 | * @param vh The ViewHolder which should be updated to represent the contents of
25 | * the item at the given position in the data set.
26 | * @param item The item which should be bound to view holder.
27 | * @param payloads A non-null list of merged payloads. Can be empty list if requires full update.
28 | */
29 | open fun onBind(vh: VH, item: T, payloads: List) {
30 | onBind(vh, item)
31 | }
32 |
33 | /**
34 | * Unbinds a ViewHolder from an item. Any expensive references may be
35 | * released here, and any fields that are not bound for every item should be
36 | * cleared here.
37 | */
38 | protected open fun onUnbind(vh: VH) = Unit
39 |
40 | // region ---- override parent presenter ----
41 |
42 | final override fun onCreateViewHolder(parent: ViewGroup): VH {
43 | return onCreate(parent)
44 | }
45 |
46 | final override fun onBindViewHolder(viewHolder: ViewHolder, item: Any?) {
47 | onBind(viewHolder as VH, item as T)
48 | }
49 |
50 | final override fun onBindViewHolder(
51 | viewHolder: ViewHolder,
52 | item: Any?,
53 | payloads: List,
54 | ) {
55 | onBind(viewHolder as VH, item as T, payloads)
56 | }
57 |
58 | final override fun onUnbindViewHolder(viewHolder: ViewHolder) {
59 | onUnbind(viewHolder as VH)
60 | }
61 |
62 | // endregion
63 | }
64 |
--------------------------------------------------------------------------------
/lounge/src/main/java/jp/co/cyberagent/lounge/KeyUtils.kt:
--------------------------------------------------------------------------------
1 | package jp.co.cyberagent.lounge
2 |
3 | /**
4 | * Hash a object into 64 bits lounge model key.
5 | *
6 | * @see hashLong64Bit
7 | * @see hashString64Bit
8 | */
9 | fun Any.toLoungeModelKey(): Long {
10 | return when (this) {
11 | is Long -> hashLong64Bit(this)
12 | is Int -> hashLong64Bit(toLong())
13 | else -> hashString64Bit(toString())
14 | }
15 | }
16 |
17 | /**
18 | * Hash a string into 64 bits instead of the normal 32. This allows us to better use strings as a
19 | * model id with less chance of collisions. This uses the FNV-1a algorithm for a good mix of speed
20 | * and distribution.
21 | * Performance comparisons found at http://stackoverflow.com/a/1660613
22 | * Hash implementation from http://www.isthe.com/chongo/tech/comp/fnv/index.html#FNV-1a
23 | *
24 | * Forked from [airbnb/epoxy](https://github.com/airbnb/epoxy/blob/3905f50321b98ad296b4d058b765ebf1fb5f4dea/epoxy-adapter/src/main/java/com/airbnb/epoxy/IdUtils.java#L36).
25 | */
26 | @Suppress("MagicNumber")
27 | fun hashString64Bit(v: CharSequence): Long {
28 | var result = -0x340d631b7bdddcdbL
29 | val len = v.length
30 | for (i in 0 until len) {
31 | result = result xor v[i].toLong()
32 | result *= 0x100000001b3L
33 | }
34 | return result
35 | }
36 |
37 | /**
38 | * Hash a long into 64 bits instead of the normal 32. This uses a xor shift implementation to
39 | * attempt psuedo randomness so object ids have an even spread for less chance of collisions.
40 | * From http://stackoverflow.com/a/11554034
41 | * http://www.javamex.com/tutorials/random_numbers/xorshift.shtml
42 | *
43 | * Forked from [airbnb/epoxy](https://github.com/airbnb/epoxy/blob/3905f50321b98ad296b4d058b765ebf1fb5f4dea/epoxy-adapter/src/main/java/com/airbnb/epoxy/IdUtils.java#L20)
44 | */
45 | @Suppress("MagicNumber")
46 | fun hashLong64Bit(v: Long): Long {
47 | var value = v
48 | value = value xor (value shl 21)
49 | value = value xor (value ushr 35)
50 | value = value xor (value shl 4)
51 | return value
52 | }
53 |
--------------------------------------------------------------------------------
/sample/src/main/res/drawable/ic_tv.xml:
--------------------------------------------------------------------------------
1 |
7 |
11 |
12 |
--------------------------------------------------------------------------------
/lounge-databinding/src/main/java/jp/co/cyberagent/lounge/databinding/DataBindingRowPresenter.kt:
--------------------------------------------------------------------------------
1 | package jp.co.cyberagent.lounge.databinding
2 |
3 | import android.view.LayoutInflater
4 | import android.view.ViewGroup
5 | import androidx.annotation.LayoutRes
6 | import androidx.databinding.DataBindingUtil
7 | import androidx.databinding.ViewDataBinding
8 | import androidx.leanback.widget.RowPresenter
9 | import jp.co.cyberagent.lounge.TypedRowPresenter
10 |
11 | /**
12 | * A wrapper around [TypedRowPresenter] which you can directly binding a item to a [ViewDataBinding]
13 | * instead of [RowPresenter.ViewHolder].
14 | *
15 | * @param T type of the item to be bind.
16 | * @param DB type of the [ViewDataBinding] that the item will bind to.
17 | * @param layoutId the layout file id that corresponded to [DB].
18 | */
19 | abstract class DataBindingRowPresenter(
20 | @LayoutRes val layoutId: Int,
21 | ) : TypedRowPresenter>() {
22 |
23 | /**
24 | * Binds the given row object to the given DataBinding.
25 | */
26 | abstract fun onBindRow(binding: DB, item: T)
27 |
28 | /**
29 | * Unbinds the given DataBinding.
30 | */
31 | open fun onUnbindRow(binding: DB) = Unit
32 |
33 | /**
34 | * A ViewHolder that cache its view's [ViewDataBinding].
35 | */
36 | class ViewHolder(
37 | val binding: DB,
38 | ) : RowPresenter.ViewHolder(binding.root)
39 |
40 | // region ---- override parent presenter ----
41 |
42 | override fun onCreateRow(parent: ViewGroup): ViewHolder {
43 | val layoutInflater = LayoutInflater.from(parent.context)
44 | val binding = DataBindingUtil.inflate(layoutInflater, layoutId, parent, false)
45 | return ViewHolder(binding)
46 | }
47 |
48 | final override fun onBindRow(vh: ViewHolder, item: T) {
49 | super.onBindRow(vh, item)
50 | onBindRow(vh.binding, item)
51 | vh.binding.executePendingBindings()
52 | }
53 |
54 | final override fun onUnbindRow(vh: ViewHolder) {
55 | super.onUnbindRow(vh)
56 | onUnbindRow(vh.binding)
57 | }
58 |
59 | // endregion
60 | }
61 |
--------------------------------------------------------------------------------
/lounge-paging/src/test/java/jp/co/cyberagent/lounge/paging/util/ListDataSource.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018 The Android Open Source Project
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | *
16 | * For from airbnb/epoxy
17 | */
18 | package jp.co.cyberagent.lounge.paging.util
19 |
20 | import androidx.paging.PositionalDataSource
21 |
22 | /**
23 | * Simple data source that works with a given list and its loading can be stopped / started.
24 | */
25 | class ListDataSource(
26 | private val data: List,
27 | ) : PositionalDataSource() {
28 | private var pendingActions = arrayListOf<() -> Unit>()
29 | private var running = true
30 |
31 | private fun compute(f: () -> Unit) {
32 | if (running) {
33 | f()
34 | } else {
35 | pendingActions.add(f)
36 | }
37 | }
38 |
39 | fun start() {
40 | running = true
41 | val pending = pendingActions
42 | pendingActions = arrayListOf()
43 | pending.forEach(this::compute)
44 | }
45 |
46 | fun stop() {
47 | running = false
48 | }
49 |
50 | override fun loadRange(params: LoadRangeParams, callback: LoadRangeCallback) {
51 | compute {
52 | callback.onResult(
53 | data.subList(
54 | params.startPosition,
55 | Math.min(data.size, params.startPosition + params.loadSize)
56 | )
57 | )
58 | }
59 | }
60 |
61 | override fun loadInitial(params: LoadInitialParams, callback: LoadInitialCallback) {
62 | val start = computeInitialLoadPosition(params, data.size)
63 | val itemCnt = computeInitialLoadSize(params, start, data.size)
64 | callback.onResult(
65 | data.subList(start, start + itemCnt),
66 | start,
67 | data.size
68 | )
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/lounge/src/test/java/jp/co/cyberagent/lounge/LoungeControllerPropertyTest.kt:
--------------------------------------------------------------------------------
1 | package jp.co.cyberagent.lounge
2 |
3 | import androidx.lifecycle.Lifecycle
4 | import io.kotest.core.spec.style.FunSpec
5 | import io.kotest.extensions.robolectric.RobolectricTest
6 | import io.kotest.matchers.shouldBe
7 | import jp.co.cyberagent.fixture.TestLifecycleOwner
8 | import jp.co.cyberagent.fixture.memoized
9 | import jp.co.cyberagent.lounge.LoungePropertyPredicate.Companion.referentialPredicate
10 | import jp.co.cyberagent.lounge.LoungePropertyPredicate.Companion.structuralPredicate
11 | import jp.co.cyberagent.lounge.util.ValueHolder
12 | import kotlinx.coroutines.ExperimentalCoroutinesApi
13 | import kotlinx.coroutines.test.TestCoroutineDispatcher
14 |
15 | @ExperimentalCoroutinesApi
16 | @RobolectricTest
17 | class LoungeControllerPropertyTest : FunSpec({
18 | val owner by memoized { TestLifecycleOwner() }
19 | val dispatcher by memoized { TestCoroutineDispatcher() }
20 |
21 | test("structuralPredicate") {
22 | var buildCount = 0
23 |
24 | val controller = object : LoungeController(owner.lifecycle, dispatcher) {
25 | var prop by loungeProp(ValueHolder(0), predicate = structuralPredicate())
26 |
27 | override suspend fun buildModels() {
28 | buildCount++
29 | }
30 | }
31 |
32 | owner.lifecycle.currentState = Lifecycle.State.STARTED
33 | buildCount shouldBe 0
34 | controller.requestModelBuild()
35 | buildCount shouldBe 1
36 | controller.prop = ValueHolder(0)
37 | buildCount shouldBe 1
38 | controller.prop = ValueHolder(1)
39 | buildCount shouldBe 2
40 | }
41 |
42 | test("referentialPredicate") {
43 | var buildCount = 0
44 |
45 | val controller = object : LoungeController(owner.lifecycle, dispatcher) {
46 | var prop by loungeProp(ValueHolder(0), predicate = referentialPredicate())
47 |
48 | override suspend fun buildModels() {
49 | buildCount++
50 | }
51 | }
52 |
53 | owner.lifecycle.currentState = Lifecycle.State.STARTED
54 | buildCount shouldBe 0
55 | controller.requestModelBuild()
56 | buildCount shouldBe 1
57 | controller.prop = ValueHolder(0)
58 | buildCount shouldBe 2
59 | controller.prop = ValueHolder(1)
60 | buildCount shouldBe 3
61 | }
62 | })
63 |
--------------------------------------------------------------------------------
/sample/src/main/java/jp/co/cyberagent/lounge/sample/ui/GuidedStepExampleFragment.kt:
--------------------------------------------------------------------------------
1 | package jp.co.cyberagent.lounge.sample.ui
2 |
3 | import android.os.Bundle
4 | import androidx.leanback.widget.GuidanceStylist
5 | import androidx.leanback.widget.GuidedAction
6 | import androidx.navigation.fragment.findNavController
7 | import jp.co.cyberagent.lounge.Guidance
8 | import jp.co.cyberagent.lounge.LoungeGuidedStepSupportFragment
9 | import jp.co.cyberagent.lounge.createGuidedActions
10 | import jp.co.cyberagent.lounge.sample.R
11 |
12 | class GuidedStepExampleFragment : LoungeGuidedStepSupportFragment() {
13 |
14 | override fun onCreateGuidance(savedInstanceState: Bundle?): GuidanceStylist.Guidance {
15 | return Guidance(
16 | title = "GuidedStep Sample: ${parentFragmentManager.backStackEntryCount}",
17 | description = "GuidedStep Description",
18 | breadcrumb = "GuidedStep BreadCrumb",
19 | )
20 | }
21 |
22 | override fun onCreateActions(actions: MutableList, savedInstanceState: Bundle?) {
23 | actions += createGuidedActions(requireContext()) {
24 | guidedAction {
25 | title("Next")
26 | description("Next Description")
27 | onClicked { findNavController().navigate(R.id.to_guided_step_self) }
28 | }
29 |
30 | guidedAction {
31 | title("Pop Back")
32 | description("Pop Back Description")
33 | onClicked { findNavController().popBackStack() }
34 | }
35 |
36 | guidedAction {
37 | title("Pop Back All")
38 | description("Pop Back All Description")
39 | subActions {
40 | guidedAction {
41 | title("Yes")
42 | onSubClicked {
43 | findNavController().popBackStack(R.id.fragment_home, false)
44 | true
45 | }
46 | }
47 |
48 | guidedAction {
49 | title("No")
50 | }
51 | }
52 | }
53 |
54 | guidedAction {
55 | infoOnly(true)
56 | focusable(false)
57 | layoutId(R.layout.layout_divider)
58 | }
59 |
60 | guidedAction {
61 | title("Home")
62 | description("Home Description")
63 | onClicked { findNavController().navigate(R.id.to_home) }
64 | }
65 | }
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/lounge-databinding/src/main/java/jp/co/cyberagent/lounge/databinding/DataBindingPresenter.kt:
--------------------------------------------------------------------------------
1 | package jp.co.cyberagent.lounge.databinding
2 |
3 | import android.view.LayoutInflater
4 | import android.view.ViewGroup
5 | import androidx.annotation.LayoutRes
6 | import androidx.databinding.DataBindingUtil
7 | import androidx.databinding.ViewDataBinding
8 | import androidx.leanback.widget.Presenter
9 | import jp.co.cyberagent.lounge.TypedPresenter
10 |
11 | /**
12 | * A wrapper around [TypedPresenter] which you can directly binding a item to a [ViewDataBinding]
13 | * instead of [Presenter.ViewHolder].
14 | *
15 | * @param T type of the item to be bind.
16 | * @param DB type of the [ViewDataBinding] that the item will bind to.
17 | * @param layoutId the layout file id that corresponded to [DB].
18 | */
19 | abstract class DataBindingPresenter(
20 | @LayoutRes val layoutId: Int,
21 | ) : TypedPresenter>() {
22 |
23 | /**
24 | * Binds a DataBinding to an item.
25 | */
26 | abstract fun onBind(binding: DB, item: T)
27 |
28 | /**
29 | * Binds a DataBinding to an item.
30 | */
31 | open fun onBind(binding: DB, item: T, payloads: List) {
32 | onBind(binding, item)
33 | }
34 |
35 | /**
36 | * Unbinds a DataBinding from an item. Any expensive references may be
37 | * released here, and any fields that are not bound for every item should be
38 | * cleared here.
39 | */
40 | open fun onUnbind(binding: DB) = Unit
41 |
42 | /**
43 | * A ViewHolder that cache its view's [ViewDataBinding].
44 | */
45 | class ViewHolder(
46 | val binding: DB,
47 | ) : Presenter.ViewHolder(binding.root)
48 |
49 | // region ---- override parent presenter ----
50 |
51 | override fun onCreate(parent: ViewGroup): ViewHolder {
52 | val layoutInflater = LayoutInflater.from(parent.context)
53 | val binding = DataBindingUtil.inflate(layoutInflater, layoutId, parent, false)
54 | return ViewHolder(binding)
55 | }
56 |
57 | final override fun onBind(vh: ViewHolder, item: T) {
58 | onBind(vh.binding, item)
59 | vh.binding.executePendingBindings()
60 | }
61 |
62 | final override fun onBind(vh: ViewHolder, item: T, payloads: List) {
63 | onBind(vh.binding, item, payloads)
64 | vh.binding.executePendingBindings()
65 | }
66 |
67 | final override fun onUnbind(vh: ViewHolder) {
68 | onUnbind(vh.binding)
69 | }
70 |
71 | // endregion
72 | }
73 |
--------------------------------------------------------------------------------
/lounge/src/main/java/jp/co/cyberagent/lounge/LoungeControllerProperty.kt:
--------------------------------------------------------------------------------
1 | package jp.co.cyberagent.lounge
2 |
3 | import jp.co.cyberagent.lounge.LoungePropertyPredicate.Companion.structuralPredicate
4 | import kotlin.properties.ReadWriteProperty
5 | import kotlin.reflect.KProperty
6 |
7 | /**
8 | * A delegation property that can be used in a [LoungeController].
9 | * If the delegated value changed then an update will be requested
10 | * by calling [LoungeController.requestModelBuild].
11 | */
12 | fun loungeProp(
13 | initialValue: T,
14 | predicate: LoungePropertyPredicate = structuralPredicate(),
15 | ): ReadWriteProperty =
16 | LoungeControllerProperty(initialValue, predicate)
17 |
18 | private class LoungeControllerProperty(
19 | private var value: T,
20 | private val predicate: LoungePropertyPredicate,
21 | ) : ReadWriteProperty {
22 |
23 | override fun getValue(thisRef: LoungeController, property: KProperty<*>): T = value
24 |
25 | override fun setValue(thisRef: LoungeController, property: KProperty<*>, value: T) {
26 | if (predicate.isChanged(this.value, value)) {
27 | this.value = value
28 | thisRef.requestModelBuild()
29 | }
30 | }
31 | }
32 |
33 | /**
34 | * A functional interface to determine whether the property value changed.
35 | */
36 | fun interface LoungePropertyPredicate {
37 |
38 | /**
39 | * Determine whether the property value changed. Returns true to request model build.
40 | */
41 | fun isChanged(oldValue: T, newValue: T): Boolean
42 |
43 | companion object {
44 | /**
45 | * A predicate to treat values of [loungeProp] changed if they are structurally unequal (!=).
46 | */
47 | @Suppress("UNCHECKED_CAST")
48 | fun structuralPredicate(): LoungePropertyPredicate =
49 | StructuralEqualityPredicate as LoungePropertyPredicate
50 |
51 | /**
52 | * A predictor to treat values of [loungeProp] changed if they are referentially unequal (!==).
53 | */
54 | @Suppress("UNCHECKED_CAST")
55 | fun referentialPredicate(): LoungePropertyPredicate =
56 | ReferentialEqualityPredicate as LoungePropertyPredicate
57 | }
58 | }
59 |
60 | private object StructuralEqualityPredicate : LoungePropertyPredicate {
61 | override fun isChanged(oldValue: Any?, newValue: Any?): Boolean =
62 | oldValue != newValue
63 | }
64 |
65 | private object ReferentialEqualityPredicate : LoungePropertyPredicate {
66 | override fun isChanged(oldValue: Any?, newValue: Any?): Boolean =
67 | oldValue !== newValue
68 | }
69 |
--------------------------------------------------------------------------------
/lounge/src/test/java/jp/co/cyberagent/lounge/LoungeGuidedActionsBuilderTest.kt:
--------------------------------------------------------------------------------
1 | package jp.co.cyberagent.lounge
2 |
3 | import android.content.Context
4 | import androidx.test.core.app.ApplicationProvider
5 | import io.kotest.core.spec.style.FunSpec
6 | import io.kotest.extensions.robolectric.RobolectricTest
7 | import io.kotest.matchers.shouldBe
8 | import jp.co.cyberagent.fixture.memoized
9 |
10 | @RobolectricTest
11 | class LoungeGuidedActionsBuilderTest : FunSpec({
12 |
13 | val context by memoized {
14 | ApplicationProvider.getApplicationContext()
15 | }
16 |
17 | test("Create actions") {
18 | val actions = createGuidedActions(context) {
19 | guidedAction {
20 | id(1L)
21 | title("Title1")
22 | }
23 |
24 | guidedAction {
25 | id(1000L)
26 | }
27 | }
28 |
29 | actions.size shouldBe 2
30 | actions[0].id shouldBe 1L
31 | actions[0].title shouldBe "Title1"
32 | actions[1].id shouldBe 1000L
33 | actions[1].title shouldBe null
34 | }
35 |
36 | test("Create actions with lounge property") {
37 | var invokeResult: String? = null
38 | val iid = 5L
39 | val layoutId = 100
40 | val action = createGuidedActions(context) {
41 | guidedAction {
42 | id(iid)
43 | layoutId(layoutId)
44 | onClicked {
45 | invokeResult = "click ${it.id}"
46 | }
47 |
48 | onSubClicked {
49 | invokeResult = "subclick ${it.id}"
50 | true
51 | }
52 |
53 | onFocused {
54 | invokeResult = "focus ${it.id}"
55 | }
56 |
57 | onEditedAndProceed {
58 | invokeResult = "edit ${it.id}"
59 | 1
60 | }
61 |
62 | onEditCanceled {
63 | invokeResult = "editcancel ${it.id}"
64 | }
65 |
66 | subActions {
67 | guidedAction {
68 | id(iid + 1)
69 | }
70 | }
71 | }
72 | }.first() as LoungeGuidedAction
73 |
74 | action.id shouldBe iid
75 | action.layoutId shouldBe layoutId
76 |
77 | invokeResult shouldBe null
78 | onLoungeGuidedActionClicked(action)
79 | invokeResult shouldBe "click $iid"
80 | onSubLoungeGuidedActionClicked(action) shouldBe true
81 | invokeResult shouldBe "subclick $iid"
82 | onLoungeGuidedActionFocused(action)
83 | invokeResult shouldBe "focus $iid"
84 | onLoungeGuidedActionEditedAndProceed(action)
85 | invokeResult shouldBe "edit $iid"
86 | onLoungeGuidedActionEditCanceled(action)
87 | invokeResult shouldBe "editcancel $iid"
88 |
89 | action.subActions.size shouldBe 1
90 | action.subActions.first().id shouldBe iid + 1
91 | }
92 | })
93 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/sample/src/main/java/jp/co/cyberagent/lounge/sample/ui/PagedListStressTestFragment.kt:
--------------------------------------------------------------------------------
1 | @file:Suppress("MagicNumber")
2 | package jp.co.cyberagent.lounge.sample.ui
3 |
4 | import android.os.Bundle
5 | import android.view.View
6 | import androidx.leanback.app.RowsSupportFragment
7 | import androidx.lifecycle.Lifecycle
8 | import androidx.lifecycle.lifecycleScope
9 | import androidx.paging.PagedList
10 | import jp.co.cyberagent.lounge.LoungeController
11 | import jp.co.cyberagent.lounge.paging.pagedListRowForIndexed
12 | import jp.co.cyberagent.lounge.sample.model.TextModel
13 | import jp.co.cyberagent.lounge.sample.utils.asPagedList
14 | import kotlinx.coroutines.Dispatchers
15 | import kotlinx.coroutines.delay
16 | import kotlinx.coroutines.isActive
17 | import kotlin.random.Random
18 |
19 | class PagedListStressTestFragment : RowsSupportFragment() {
20 |
21 | private val random = Random
22 | private val keyCandidates = List(50) { (it + 1).toString() }
23 | private val controller by lazy {
24 | TestController(lifecycle)
25 | }
26 |
27 | override fun onCreate(savedInstanceState: Bundle?) {
28 | super.onCreate(savedInstanceState)
29 | adapter = controller.adapter
30 | }
31 |
32 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
33 | super.onViewCreated(view, savedInstanceState)
34 | viewLifecycleOwner.lifecycleScope.launchWhenStarted {
35 | while (isActive) {
36 | val prevData = controller.pagedListRows.toMap()
37 | val prevKeys = prevData.keys.shuffled(random).toMutableList()
38 | val newData = mutableMapOf>()
39 | repeat(random.nextInt(20)) {
40 | val key = prevKeys.removeFirstOrNull()
41 | val list = when {
42 | key == null || random.nextBoolean() -> createPagedList(random)
43 | else -> prevData[key]!!
44 | }
45 | newData[key ?: keyCandidates.random()] = list
46 |
47 | if (random.nextBoolean()) {
48 | controller.pagedListRows = newData.toMap()
49 | controller.requestModelBuild()
50 | }
51 |
52 | delay(random.nextLong(50))
53 | }
54 |
55 | newData.values.forEach {
56 | it.loadAround(it.lastIndex)
57 | }
58 | }
59 | }
60 | }
61 | }
62 |
63 | private fun createPagedList(random: Random): PagedList- {
64 | val offset = random.nextLong(5)
65 | val items = List(random.nextInt(20, 50)) {
66 | Item(it + offset, it.toString())
67 | }
68 | return items.asPagedList(10, true)
69 | }
70 |
71 | private class TestController(lifecycle: Lifecycle) :
72 | LoungeController(lifecycle, Dispatchers.Default) {
73 |
74 | var pagedListRows: Map> = emptyMap()
75 |
76 | override suspend fun buildModels() {
77 | val data = pagedListRows.toList()
78 | data.forEach { (name, list) ->
79 | pagedListRowForIndexed(
80 | name = name,
81 | pagedList = list
82 | ) { index, item ->
83 | if (item == null) {
84 | TextModel("PlaceHolder", key = -index.toLong())
85 | } else {
86 | TextModel(item.value, key = item.id)
87 | }
88 | }
89 | }
90 | }
91 | }
92 |
93 | private data class Item(val id: Long, val value: String)
94 |
--------------------------------------------------------------------------------
/sample/src/main/java/jp/co/cyberagent/lounge/sample/ui/RowsExampleFragment.kt:
--------------------------------------------------------------------------------
1 | package jp.co.cyberagent.lounge.sample.ui
2 |
3 | import android.os.Bundle
4 | import android.view.View
5 | import androidx.leanback.app.BrowseSupportFragment
6 | import androidx.lifecycle.Lifecycle
7 | import androidx.lifecycle.lifecycleScope
8 | import androidx.paging.PagedList
9 | import jp.co.cyberagent.lounge.HeaderData
10 | import jp.co.cyberagent.lounge.LoungeController
11 | import jp.co.cyberagent.lounge.SimpleLoungeModelAwaitInterceptor
12 | import jp.co.cyberagent.lounge.listRowFor
13 | import jp.co.cyberagent.lounge.loungeProp
14 | import jp.co.cyberagent.lounge.navigation.addHeadersTransitionOnBackPressedCallback
15 | import jp.co.cyberagent.lounge.paging.pagedListRowFor
16 | import jp.co.cyberagent.lounge.paging.pagedListRowForIndexed
17 | import jp.co.cyberagent.lounge.sample.model.InfoModel
18 | import jp.co.cyberagent.lounge.sample.model.TextModel
19 | import jp.co.cyberagent.lounge.sample.utils.asPagedList
20 | import kotlinx.coroutines.delay
21 | import kotlin.random.Random
22 |
23 | @Suppress("MagicNumber")
24 | class RowsExampleFragment : BrowseSupportFragment() {
25 |
26 | private val random = Random(System.currentTimeMillis())
27 | private val controller = RowsController(lifecycle)
28 |
29 | override fun onCreate(savedInstanceState: Bundle?) {
30 | super.onCreate(savedInstanceState)
31 |
32 | title = "Rows Example"
33 | adapter = controller.adapter
34 | addHeadersTransitionOnBackPressedCallback(this)
35 | }
36 |
37 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
38 | super.onViewCreated(view, savedInstanceState)
39 |
40 | viewLifecycleOwner.lifecycleScope.launchWhenStarted {
41 | while (true) {
42 | controller.row1 = createList(10..20)
43 | controller.row2 = createList(10..20)
44 |
45 | controller.pagedRow1 = createList(50..200).asPagedList(10, true)
46 | controller.pagedRow2 = createList(50..200).asPagedList(10, false)
47 |
48 | delay(10000L)
49 | }
50 | }
51 | }
52 |
53 | private fun createList(sizeRange: IntRange): List {
54 | val bias = random.nextInt(5)
55 | return List(sizeRange.random(random)) {
56 | "Item ${it + bias}"
57 | }
58 | }
59 | }
60 |
61 | private class RowsController(lifecycle: Lifecycle) : LoungeController(lifecycle) {
62 |
63 | init {
64 | debugName = "RowsController"
65 | addInterceptor(SimpleLoungeModelAwaitInterceptor())
66 | }
67 |
68 | var row1: List? by loungeProp(null)
69 | var row2: List? by loungeProp(null)
70 |
71 | var pagedRow1: PagedList? by loungeProp(null)
72 | var pagedRow2: PagedList? by loungeProp(null)
73 |
74 | override suspend fun buildModels() {
75 | listRowFor("ListRow 1", row1.orEmpty()) { item ->
76 | InfoModel(item)
77 | }
78 |
79 | listRowFor("ListRow 2", row2.orEmpty()) { item ->
80 | TextModel(item)
81 | }
82 |
83 | pagedListRowForIndexed(
84 | headerData = HeaderData("PagedListRow 1", "Placeholder Enabled"),
85 | pagedList = pagedRow1
86 | ) { index, item ->
87 | InfoModel(item ?: "Placeholder $index")
88 | }
89 |
90 | pagedListRowFor(
91 | headerData = HeaderData("PagedListRow 2", "Placeholder Disabled"),
92 | pagedList = pagedRow2
93 | ) { item ->
94 | TextModel(item!!)
95 | }
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/lounge/src/main/java/jp/co/cyberagent/lounge/LoungeGuidedActionsBuilder.kt:
--------------------------------------------------------------------------------
1 | package jp.co.cyberagent.lounge
2 |
3 | import android.content.Context
4 | import androidx.leanback.app.GuidedStepSupportFragment
5 | import androidx.leanback.widget.GuidedAction
6 |
7 | /**
8 | * A DSL to create a list of [LoungeGuidedAction].
9 | *
10 | * For [LoungeGuidedAction] to function properly, you need to use other components together
11 | * in you [GuidedStepSupportFragment].
12 | * - Uses [LoungeGuidedActionsStylist] if you set [LoungeGuidedActionBuilder.layoutId]
13 | * - Uses [onLoungeGuidedActionClicked] if you set [LoungeGuidedActionBuilder.onClicked]
14 | * - Uses [onSubLoungeGuidedActionClicked] if you set [LoungeGuidedActionBuilder.onSubClicked]
15 | * - Uses [onLoungeGuidedActionFocused] if you set [LoungeGuidedActionBuilder.onFocused]
16 | * - Uses [onLoungeGuidedActionEditedAndProceed] if you set [LoungeGuidedActionBuilder.onEditedAndProceed]
17 | * - Uses [onLoungeGuidedActionEditCanceled] if you set [LoungeGuidedActionBuilder.onEditCanceled]
18 | *
19 | * Example usage:
20 | *
21 | * ```
22 | * class GuidedStepExampleFragment : GuidedStepSupportFragment() {
23 | *
24 | * override fun onCreateActionsStylist(): GuidedActionsStylist = LoungeGuidedActionsStylist()
25 | *
26 | * override fun onCreateActions(actions: MutableList, savedInstanceState: Bundle?) {
27 | * actions += createActions {
28 | * guidedAction {
29 | * id("id")
30 | * title("title")
31 | * description("description")
32 | * onClick { showToast("Clicked!") }
33 | * }
34 | *
35 | * guidedAction {
36 | * infoOnly(true)
37 | * focusable(false)
38 | * layoutId(R.layout.layout_divider)
39 | * }
40 | * }
41 | * }
42 | *
43 | * override fun onGuidedActionClicked(action: GuidedAction) = onLoungeGuidedActionClick(action)
44 | * }
45 | * ```
46 | *
47 | * Instead of inheriting [GuidedStepSupportFragment] and override with a bunch of boilerplate code,
48 | * you can choose to inherit [LoungeGuidedStepSupportFragment].
49 | * The [LoungeGuidedStepSupportFragment] has already override those methods that let
50 | * [LoungeGuidedAction] to function properly.
51 | *
52 | * Example usage:
53 | *
54 | * ```
55 | * class GuidedStepExampleFragment : LoungeGuidedStepSupportFragment() {
56 | *
57 | * override fun onCreateActions(actions: MutableList, savedInstanceState: Bundle?) {
58 | * actions += createActions {
59 | * guidedAction {
60 | * id("id")
61 | * title("title")
62 | * description("description")
63 | * onClick { showToast("Clicked!") }
64 | * }
65 | *
66 | * guidedAction {
67 | * infoOnly(true)
68 | * focusable(false)
69 | * layoutId(R.layout.layout_divider)
70 | * }
71 | * }
72 | * }
73 | * }
74 | * ```
75 | */
76 | fun createGuidedActions(
77 | context: Context,
78 | body: LoungeGuidedActionsBuilder.() -> Unit,
79 | ): List {
80 | val builder = LoungeGuidedActionsBuilder(context)
81 | return builder.apply(body).build()
82 | }
83 |
84 | /**
85 | * A builder to create a list of [GuidedAction].
86 | */
87 | class LoungeGuidedActionsBuilder internal constructor(
88 | private val context: Context,
89 | ) {
90 |
91 | private val actions: MutableList = mutableListOf()
92 |
93 | /**
94 | * Builds and add a new [GuidedAction].
95 | */
96 | fun guidedAction(
97 | body: LoungeGuidedActionBuilder.() -> Unit,
98 | ) {
99 | actions += LoungeGuidedActionBuilder(context).apply(body).build()
100 | }
101 |
102 | internal fun build(): List {
103 | return actions.toList()
104 | }
105 | }
106 |
--------------------------------------------------------------------------------
/lounge/src/main/java/jp/co/cyberagent/lounge/LoungeGuidedActionBuilder.kt:
--------------------------------------------------------------------------------
1 | package jp.co.cyberagent.lounge
2 |
3 | import android.content.Context
4 | import androidx.annotation.LayoutRes
5 | import androidx.leanback.widget.GuidedAction
6 | import jp.co.cyberagent.lounge.LoungeGuidedAction.OnGuidedActionClickedListener
7 | import jp.co.cyberagent.lounge.LoungeGuidedAction.OnGuidedActionEditCanceledListener
8 | import jp.co.cyberagent.lounge.LoungeGuidedAction.OnGuidedActionEditedAndProceedListener
9 | import jp.co.cyberagent.lounge.LoungeGuidedAction.OnGuidedActionFocusedListener
10 | import jp.co.cyberagent.lounge.LoungeGuidedAction.OnSubGuidedActionClickedListener
11 |
12 | /**
13 | * Builder class to build a [LoungeGuidedAction].
14 | */
15 | class LoungeGuidedActionBuilder(context: Context) :
16 | GuidedAction.BuilderBase(context) {
17 |
18 | private var _layoutId: Int = LoungeGuidedAction.DEFAULT_LAYOUT_ID
19 | private var _onClickedListener: OnGuidedActionClickedListener? = null
20 | private var _onSubClickedListener: OnSubGuidedActionClickedListener? = null
21 | private var _onFocusedListener: OnGuidedActionFocusedListener? = null
22 | private var _onEditedAndProceedListener: OnGuidedActionEditedAndProceedListener? = null
23 | private var _onEditCanceledListener: OnGuidedActionEditCanceledListener? = null
24 |
25 | /**
26 | * Sets the custom layout id for this action.
27 | *
28 | * Only takes effect when used with the [LoungeGuidedActionsStylist].
29 | */
30 | fun layoutId(@LayoutRes id: Int) = apply {
31 | _layoutId = id
32 | }
33 |
34 | /**
35 | * Sets the listener when clicked this action.
36 | *
37 | * Only takes effect when used with [onLoungeGuidedActionClicked].
38 | */
39 | fun onClicked(l: OnGuidedActionClickedListener?) = apply {
40 | _onClickedListener = l
41 | }
42 |
43 | /**
44 | * Sets the listener when clicked this action in sub actions.
45 | *
46 | * Only takes effect when used with [onSubLoungeGuidedActionClicked].
47 | */
48 | fun onSubClicked(l: OnSubGuidedActionClickedListener?) = apply {
49 | _onSubClickedListener = l
50 | }
51 |
52 | /**
53 | * Sets the listener when focused this action.
54 | *
55 | * Only takes effect when used with [onLoungeGuidedActionFocused].
56 | */
57 | fun onFocused(l: OnGuidedActionFocusedListener?) = apply {
58 | _onFocusedListener = l
59 | }
60 |
61 | /**
62 | * Sets the listener when edited and proceed this action, for example when user clicks confirm button
63 | * in IME window.
64 | *
65 | * Only takes effect when used with [onLoungeGuidedActionEditedAndProceed].
66 | */
67 | fun onEditedAndProceed(l: OnGuidedActionEditedAndProceedListener?) = apply {
68 | _onEditedAndProceedListener = l
69 | }
70 |
71 | /**
72 | * Sets the listener when canceled editing this action, for example when user closes
73 | * IME window by BACK key.
74 | *
75 | * Only takes effect when used with [onLoungeGuidedActionEditCanceled].
76 | */
77 | fun onEditCanceled(l: OnGuidedActionEditCanceledListener?) = apply {
78 | _onEditCanceledListener = l
79 | }
80 |
81 | /**
82 | * Set sub actions list via [LoungeGuidedActionBuilder].
83 | */
84 | fun subActions(
85 | body: LoungeGuidedActionsBuilder.() -> Unit,
86 | ) {
87 | subActions(LoungeGuidedActionsBuilder(context).apply(body).build())
88 | }
89 |
90 | /**
91 | * Builds the [LoungeGuidedAction] corresponding to this Builder.
92 | */
93 | fun build(): LoungeGuidedAction {
94 | val action = LoungeGuidedAction()
95 | applyValues(action)
96 | action.layoutId = _layoutId
97 | action.onClickedListener = _onClickedListener
98 | action.onSubClickedListener = _onSubClickedListener
99 | action.onFocusedListener = _onFocusedListener
100 | action.onEditedAndProceedListener = _onEditedAndProceedListener
101 | action.onEditCanceledListener = _onEditCanceledListener
102 | return action
103 | }
104 | }
105 |
--------------------------------------------------------------------------------
/docs/guided-step.md:
--------------------------------------------------------------------------------
1 | # GuidedStep Support
2 |
3 | `GuidedStepSupportFragment` is used to represent a single step in a series of steps.
4 | Lounge provides some helper methods/classes to simplify creating `GuidedStepSupportFragment`.
5 |
6 | ## Create guided actions
7 |
8 | We can override `onCreateActions()` to add user actions.
9 | Lounge provides a DSL `createGuidedActions(Context)` to build multiple actions.
10 |
11 | ```kotlin
12 | override fun onCreateActions(actions: MutableList, savedInstanceState: Bundle?) {
13 | actions += createGuidedActions(requireContext()) {
14 | guidedAction {
15 | // You can access all methods defined in `GuidedAction.Builder`
16 | title("Next")
17 | }
18 |
19 | guidedAction {
20 | title("Back")
21 | description("Really?")
22 | }
23 | }
24 | }
25 | ```
26 |
27 | ### Set Event Listener
28 |
29 | When using the `GuidedAction.Builder`, we cannot directly define the event listener, like click listener, for an action.
30 | We need to override `onGuidedActionClicked()` and process via identify the passed-in `GuidedAction`.
31 | Use `guidedAction {}` DSL and other helper methods like `onLoungeGuidedActionClicked` together,
32 | set event listener can become easier.
33 |
34 | ```kotlin
35 | override fun onCreateActions(actions: MutableList, savedInstanceState: Bundle?) {
36 | actions += createGuidedActions(requireContext()) {
37 | guidedAction {
38 | title("Hi")
39 | onClicked { showToast("Hello World!") }
40 | }
41 | }
42 | }
43 |
44 | override fun onGuidedActionClicked(action: GuidedAction?) {
45 | onLoungeGuidedActionClicked(action)
46 | }
47 | ```
48 |
49 | ### Add SubActions
50 |
51 | You can add sub actions via `subActions {}`.
52 |
53 | ```kotlin
54 | createGuidedActions(requireContext()) {
55 | guidedAction {
56 | title("Sign Out")
57 | subActions {
58 | guidedAction {
59 | title("Yes")
60 | }
61 |
62 | guidedAction {
63 | title("No")
64 | }
65 | }
66 | }
67 | }
68 | ```
69 |
70 | ### Custom Action Layout
71 |
72 | By override `onCreateActionsStylist()` and returns a `LoungeGuidedActionsStylist`,
73 | your layout file passed via `layoutId(Int)` can be correctly inflated.
74 |
75 | ```kotlin
76 | override fun onCreateActions(actions: MutableList, savedInstanceState: Bundle?) {
77 | actions += createGuidedActions(requireContext()) {
78 | guidedAction {
79 | infoOnly(true)
80 | focusable(false)
81 | layoutId(R.layout.layout_divider)
82 | }
83 | }
84 | }
85 |
86 | override fun onCreateActionsStylist(): GuidedActionsStylist {
87 | return LoungeGuidedActionsStylist()
88 | }
89 | ```
90 |
91 | ### LoungeGuidedStepSupportFragment
92 |
93 | If you want to reduce more boilerplate codes,
94 | you can extend the `LoungeGuidedStepSupportFragment`.
95 | `LoungeGuidedStepSupportFragment` already properly override methods that `createGuidedActions(Context)` DSL required.
96 |
97 | ```kotlin
98 | class GuidedStepExampleFragment : LoungeGuidedStepSupportFragment() {
99 | override fun onCreateActions(actions: MutableList, savedInstanceState: Bundle?) {
100 | actions += createGuidedActions(requireContext()) {
101 | }
102 | }
103 | ```
104 |
105 | ## Create Guidance
106 |
107 | We can override `onCreateGuidance()` and return a new `GuidanceStylist.Guidance` that contains context information, such as the step title, description, and icon.
108 | Lounge provides a top-level function `Guidance` for creating `GuidanceStylist.Guidance`.
109 | All parameters of `Guidance` are default null.
110 |
111 | ```kotlin
112 | override fun onCreateGuidance(savedInstanceState: Bundle?): GuidanceStylist.Guidance {
113 | return Guidance(
114 | title = "Title",
115 | description = "Description"
116 | )
117 | }
118 | ```
119 |
120 | ## Navigation
121 |
122 | When using `GuidedStepSupportFragment` with Navigation Component,
123 | we need to manually `setUiStyle` to get correct transition animation.
124 | Lounge provides a `GuidedStepFragmentNavigator` to automatically do `setUiStyle` based on the back stack.
125 |
126 | See TODO for more details.
127 |
--------------------------------------------------------------------------------
/lounge-material/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
67 |
68 |
--------------------------------------------------------------------------------
/lounge-paging/src/test/java/jp/co/cyberagent/lounge/paging/PagedListLoungeControllerTest.kt:
--------------------------------------------------------------------------------
1 | package jp.co.cyberagent.lounge.paging
2 |
3 | import androidx.lifecycle.Lifecycle
4 | import io.kotest.core.spec.style.FunSpec
5 | import io.kotest.extensions.robolectric.RobolectricTest
6 | import io.kotest.matchers.collections.shouldContainExactly
7 | import io.kotest.matchers.shouldBe
8 | import jp.co.cyberagent.fixture.TestLifecycleOwner
9 | import jp.co.cyberagent.fixture.items
10 | import jp.co.cyberagent.fixture.memoized
11 | import jp.co.cyberagent.fixture.withStartedThenCreated
12 | import jp.co.cyberagent.lounge.LoungeModel
13 | import jp.co.cyberagent.lounge.paging.util.TestModel
14 | import jp.co.cyberagent.lounge.paging.util.asPagedList
15 | import kotlinx.coroutines.ExperimentalCoroutinesApi
16 | import kotlinx.coroutines.test.TestCoroutineDispatcher
17 |
18 | @ExperimentalCoroutinesApi
19 | @RobolectricTest
20 | class PagedListLoungeControllerTest : FunSpec({
21 | val owner by memoized { TestLifecycleOwner() }
22 | val dispatcher by memoized { TestCoroutineDispatcher() }
23 |
24 | test("Build models") {
25 | val buildOrder = mutableListOf()
26 | val controller = object : PagedListLoungeController(
27 | owner.lifecycle,
28 | dispatcher,
29 | ) {
30 | override fun buildItemModel(position: Int, item: Int?): LoungeModel {
31 | buildOrder += item!!
32 | return TestModel(item!!.toLong())
33 | }
34 | }
35 |
36 | val list = List(10) { it }
37 | val pagedList = list.asPagedList(
38 | pageSize = 2,
39 | initialPosition = 3,
40 | enablePlaceholders = false,
41 | )
42 |
43 | owner.withStartedThenCreated {
44 | controller.adapter.size() shouldBe 0
45 | controller.submitList(pagedList)
46 | }
47 | controller.adapter.size() shouldBe 4
48 | controller.adapter.items.shouldContainExactly(
49 | TestModel(3),
50 | TestModel(4),
51 | TestModel(5),
52 | TestModel(6),
53 | )
54 |
55 | owner.withStartedThenCreated {
56 | controller.adapter.size() shouldBe 4
57 | controller.adapter.get(0)
58 | }
59 | controller.adapter.size() shouldBe 6
60 | controller.adapter.items.shouldContainExactly(
61 | TestModel(1),
62 | TestModel(2),
63 | TestModel(3),
64 | TestModel(4),
65 | TestModel(5),
66 | TestModel(6),
67 | )
68 |
69 | owner.withStartedThenCreated {
70 | controller.adapter.size() shouldBe 6
71 | controller.adapter.get(5)
72 | }
73 | controller.adapter.size() shouldBe 8
74 | controller.adapter.items.shouldContainExactly(
75 | TestModel(1),
76 | TestModel(2),
77 | TestModel(3),
78 | TestModel(4),
79 | TestModel(5),
80 | TestModel(6),
81 | TestModel(7),
82 | TestModel(8),
83 | )
84 |
85 | buildOrder.shouldContainExactly(
86 | 3, 4, 5, 6, 1, 2, 7, 8,
87 | )
88 | }
89 |
90 | test("Insert other model") {
91 | val controller = object : PagedListLoungeController(
92 | owner.lifecycle,
93 | dispatcher,
94 | ) {
95 | override fun buildItemModel(position: Int, item: Int?): LoungeModel {
96 | return TestModel(item!!.toLong())
97 | }
98 |
99 | override suspend fun buildModels() {
100 | +TestModel(-100L)
101 | +getItemModels()
102 | }
103 | }
104 |
105 | owner.lifecycle.currentState = Lifecycle.State.STARTED
106 | val pagedList = listOf(1, 2).asPagedList(2, 0, false)
107 | controller.submitList(pagedList)
108 | controller.adapter.size() shouldBe 3
109 | controller.adapter.items.shouldContainExactly(
110 | TestModel(-100L),
111 | TestModel(1L),
112 | TestModel(2L),
113 | )
114 | }
115 |
116 | test("Request force build") {
117 | val buildOrder = mutableListOf()
118 | val controller = object : PagedListLoungeController(
119 | owner.lifecycle,
120 | dispatcher,
121 | ) {
122 | override fun buildItemModel(position: Int, item: Int?): LoungeModel {
123 | buildOrder += item!!
124 | return TestModel(item!!.toLong())
125 | }
126 | }
127 |
128 | val pagedList = listOf(10).asPagedList(
129 | pageSize = 1,
130 | initialPosition = 0,
131 | enablePlaceholders = false,
132 | )
133 |
134 | owner.lifecycle.currentState = Lifecycle.State.STARTED
135 | controller.submitList(pagedList)
136 | buildOrder.shouldContainExactly(10)
137 | controller.requestModelBuild()
138 | buildOrder.shouldContainExactly(10)
139 | controller.requestForceModelBuild()
140 | buildOrder.shouldContainExactly(10, 10)
141 | }
142 | })
143 |
--------------------------------------------------------------------------------
/config/detekt/detekt.yml:
--------------------------------------------------------------------------------
1 | config:
2 | # is automatically ignored when custom-checks.jar is on the classpath
3 | # however other CI checks use the argsfile where our plugin is not applied
4 | # we need to care take of this by explicitly allowing this properties
5 | excludes: 'custom-checks.*'
6 |
7 | custom-checks:
8 | active: true
9 | SpekTestDiscovery:
10 | active: true
11 | includes: [ '**/test/**/*Spec.kt' ]
12 |
13 | comments:
14 | CommentOverPrivateProperty:
15 | active: false
16 | UndocumentedPublicClass:
17 | active: true
18 | excludes: [ '**/*.kt' ]
19 | includes: [ '**/detekt-api/src/main/**/api/*.kt' ]
20 | UndocumentedPublicFunction:
21 | active: true
22 | excludes: [ '**/*.kt' ]
23 | includes: [ '**/detekt-api/src/main/**/api/*.kt' ]
24 |
25 | complexity:
26 | StringLiteralDuplication:
27 | active: true
28 | excludes: [ '**/test/**', '**/*.Test.kt', '**/*.Spec.kt' ]
29 | threshold: 5
30 | ignoreAnnotation: true
31 | excludeStringsWithLessThan5Characters: true
32 | ignoreStringsRegex: '$^'
33 | ComplexInterface:
34 | active: true
35 | threshold: 10
36 | includeStaticDeclarations: false
37 | includePrivateDeclarations: false
38 | ComplexMethod:
39 | active: true
40 | ignoreSingleWhenExpression: true
41 | MethodOverloading:
42 | active: true
43 | LongParameterList:
44 | active: false
45 |
46 | exceptions:
47 | NotImplementedDeclaration:
48 | active: true
49 | InstanceOfCheckForException:
50 | active: true
51 | RethrowCaughtException:
52 | active: true
53 | ReturnFromFinally:
54 | active: true
55 | SwallowedException:
56 | active: false
57 | ThrowingExceptionFromFinally:
58 | active: true
59 | ThrowingExceptionsWithoutMessageOrCause:
60 | active: true
61 | ThrowingNewInstanceOfSameException:
62 | active: true
63 |
64 | formatting:
65 | active: true
66 | android: false
67 | autoCorrect: true
68 | Indentation:
69 | indentSize: 2
70 | continuationIndentSize: 2
71 | ParameterListWrapping:
72 | indentSize: 2
73 | MaximumLineLength:
74 | active: true
75 | ImportOrdering:
76 | active: true
77 |
78 | naming:
79 | MemberNameEqualsClassName:
80 | active: true
81 | VariableNaming:
82 | active: true
83 | variablePattern: '[a-z][A-Za-z0-9]*'
84 | privateVariablePattern: '(_)?[a-z][A-Za-z0-9]*'
85 | excludeClassPattern: '$^'
86 | TopLevelPropertyNaming:
87 | constantPattern: '[A-Z][_A-Za-z0-9]*'
88 |
89 | performance:
90 | ArrayPrimitive:
91 | active: true
92 |
93 | potential-bugs:
94 | EqualsAlwaysReturnsTrueOrFalse:
95 | active: true
96 | InvalidRange:
97 | active: true
98 | IteratorHasNextCallsNextMethod:
99 | active: true
100 | IteratorNotThrowingNoSuchElementException:
101 | active: true
102 | MissingWhenCase:
103 | active: true
104 | RedundantElseInWhen:
105 | active: true
106 | UnsafeCallOnNullableType:
107 | active: true
108 | UnsafeCast:
109 | active: true
110 | excludes: [ '**/test/**', '**/*.Test.kt', '**/*.Spec.kt' ]
111 | UselessPostfixExpression:
112 | active: true
113 | WrongEqualsTypeParameter:
114 | active: true
115 |
116 | style:
117 | ClassOrdering:
118 | active: true
119 | CollapsibleIfStatements:
120 | active: true
121 | EqualsNullCall:
122 | active: true
123 | ForbiddenComment:
124 | active: false
125 | values: [ 'TODO:', 'FIXME:', 'STOPSHIP:', '@author' ]
126 | FunctionOnlyReturningConstant:
127 | active: true
128 | LoopWithTooManyJumpStatements:
129 | active: true
130 | LibraryCodeMustSpecifyReturnType:
131 | active: true
132 | excludes: [ '**/*.kt' ]
133 | includes: [ '**/detekt-api/src/main/**/api/*.kt' ]
134 | MaxLineLength:
135 | excludes: [ '**/test/**', '**/*.Test.kt', '**/*.Spec.kt' ]
136 | excludeCommentStatements: true
137 | MagicNumber:
138 | ignoreHashCodeFunction: true
139 | ignorePropertyDeclaration: true
140 | ignoreAnnotation: true
141 | ignoreEnums: true
142 | ignoreNumbers: [ '-1', '0', '1', '2', '32', '100', '1000' ]
143 | MayBeConst:
144 | active: true
145 | NestedClassesVisibility:
146 | active: true
147 | ProtectedMemberInFinalClass:
148 | active: true
149 | RedundantVisibilityModifierRule:
150 | active: true
151 | ReturnCount:
152 | excludeGuardClauses: true
153 | SpacingBetweenPackageAndImports:
154 | active: true
155 | UnnecessaryAbstractClass:
156 | active: true
157 | UnnecessaryInheritance:
158 | active: true
159 | UnusedPrivateClass:
160 | active: true
161 | UnusedPrivateMember:
162 | active: true
163 | allowedNames: '(_|ignored|expected)'
164 | UseCheckOrError:
165 | active: true
166 | UselessCallOnNotNull:
167 | active: true
168 | UtilityClassWithPublicConstructor:
169 | active: true
170 |
--------------------------------------------------------------------------------
/lounge-paging/src/main/java/jp/co/cyberagent/lounge/paging/PagedListLoungeController.kt:
--------------------------------------------------------------------------------
1 | package jp.co.cyberagent.lounge.paging
2 |
3 | import android.annotation.SuppressLint
4 | import androidx.annotation.CallSuper
5 | import androidx.lifecycle.Lifecycle
6 | import androidx.lifecycle.coroutineScope
7 | import androidx.paging.PagedList
8 | import androidx.recyclerview.widget.DiffUtil
9 | import jp.co.cyberagent.lounge.LoungeController
10 | import jp.co.cyberagent.lounge.LoungeModel
11 | import jp.co.cyberagent.lounge.paging.internal.PagedListModelCache
12 | import kotlinx.coroutines.CoroutineDispatcher
13 | import kotlinx.coroutines.CoroutineScope
14 | import kotlinx.coroutines.Dispatchers
15 | import kotlinx.coroutines.SupervisorJob
16 | import kotlinx.coroutines.cancel
17 | import kotlinx.coroutines.job
18 |
19 | /**
20 | * A [LoungeController] that can work with a [PagedList].
21 | * The implementation is inspired by [epoxy/PagedListEpoxyController](https://github.com/airbnb/epoxy/blob/master/epoxy-paging/src/main/java/com/airbnb/epoxy/paging/PagedListEpoxyController.kt).
22 | *
23 | * Internally, this controller caches the model for each item in the [PagedList].
24 | * You should override [buildItemModel] method to build the model for the given item.
25 | * Since [PagedList] might include `null` items if placeholders are enabled,
26 | * this method needs to handle `null` values in the list.
27 | *
28 | * @param T The type of the items in the [PagedList].
29 | * @param lifecycle of [LoungeController]'s host.
30 | * @param modelBuildingDispatcher the dispatcher for building models.
31 | * @param itemDiffCallback detect changes between [PagedList]s.
32 | *
33 | * @see LambdaPagedListLoungeController
34 | */
35 | abstract class PagedListLoungeController(
36 | lifecycle: Lifecycle,
37 | modelBuildingDispatcher: CoroutineDispatcher = Dispatchers.Main,
38 | @Suppress("UNCHECKED_CAST")
39 | itemDiffCallback: DiffUtil.ItemCallback = DefaultPagedListItemDiffCallback as DiffUtil.ItemCallback,
40 | ) : LoungeController(lifecycle, modelBuildingDispatcher),
41 | PagedListLoungeBuildModelScope {
42 |
43 | private val modelCacheScope =
44 | CoroutineScope(SupervisorJob(lifecycle.coroutineScope.coroutineContext.job) + modelBuildingDispatcher)
45 |
46 | private val modelCache = PagedListModelCache(
47 | modelBuilder = { position, item -> buildItemModel(position, item) },
48 | rebuildCallback = { requestModelBuild() },
49 | coroutineScope = modelCacheScope,
50 | diffCallback = itemDiffCallback,
51 | )
52 |
53 | /**
54 | * Returns the PagedList currently being displayed.
55 | *
56 | * This is not necessarily the most recent list passed to [submitList].
57 | */
58 | val currentList: PagedList?
59 | get() = modelCache.currentList
60 |
61 | /**
62 | * Builds the model for a given item. This must return a single model for each item.
63 | * If you want to inject headers etc, you can also override [buildModels] function and calls
64 | * [getItemModels] to get all models built by this method.
65 | *
66 | * If the [item] is `null`, you should provide the placeholder. If your [PagedList] is
67 | * configured without placeholders, you don't need to handle the `null` case.
68 | */
69 | abstract fun buildItemModel(position: Int, item: T?): LoungeModel
70 |
71 | /**
72 | * Gets all built models from [buildItemModel]. You can call this method inside [buildModels]
73 | * to change the behavior.
74 | */
75 | override suspend fun getItemModels(): List {
76 | checkIsBuilding("getPagedListModels")
77 | return modelCache.getModels()
78 | }
79 |
80 | override suspend fun buildModels() {
81 | +getItemModels()
82 | }
83 |
84 | /**
85 | * Submit a new paged list. A diff will be calculated between this list and the previous list
86 | * so you may still get cached models from the previous list when calling [getItemModels].
87 | */
88 | fun submitList(pagedList: PagedList?) {
89 | modelCache.submitList(pagedList)
90 | }
91 |
92 | /**
93 | * Clears the cached models then call [requestModelBuild].
94 | * So model build will run for every item in the current [PagedList].
95 | */
96 | fun requestForceModelBuild() {
97 | modelCache.clearModels()
98 | requestModelBuild()
99 | }
100 |
101 | @CallSuper
102 | override fun close() {
103 | modelCacheScope.cancel()
104 | super.close()
105 | }
106 |
107 | override fun notifyGetItemAt(position: Int) {
108 | modelCache.notifyGetItemAt(position)
109 | }
110 | }
111 |
112 | private object DefaultPagedListItemDiffCallback : DiffUtil.ItemCallback() {
113 |
114 | override fun areItemsTheSame(oldItem: Any, newItem: Any): Boolean {
115 | return oldItem == newItem
116 | }
117 |
118 | @SuppressLint("DiffUtilEquals")
119 | override fun areContentsTheSame(oldItem: Any, newItem: Any): Boolean {
120 | return oldItem == newItem
121 | }
122 | }
123 |
--------------------------------------------------------------------------------
/lounge-paging/src/main/java/jp/co/cyberagent/lounge/paging/internal/PagedListModelCache.kt:
--------------------------------------------------------------------------------
1 | package jp.co.cyberagent.lounge.paging.internal
2 |
3 | import androidx.paging.AsyncPagedListDiffer
4 | import androidx.paging.PagedList
5 | import androidx.recyclerview.widget.AsyncDifferConfig
6 | import androidx.recyclerview.widget.DiffUtil
7 | import androidx.recyclerview.widget.ListUpdateCallback
8 | import jp.co.cyberagent.lounge.LoungeModel
9 | import kotlinx.coroutines.CompletableDeferred
10 | import kotlinx.coroutines.CoroutineScope
11 | import kotlinx.coroutines.Dispatchers
12 | import kotlinx.coroutines.channels.Channel
13 | import kotlinx.coroutines.flow.consumeAsFlow
14 | import kotlinx.coroutines.flow.launchIn
15 | import kotlinx.coroutines.flow.onEach
16 | import kotlinx.coroutines.launch
17 | import java.util.concurrent.Executor
18 | import kotlin.math.min
19 |
20 | internal class PagedListModelCache(
21 | private val modelBuilder: (Int, T?) -> LoungeModel,
22 | private val rebuildCallback: () -> Unit,
23 | diffCallback: DiffUtil.ItemCallback,
24 | private val coroutineScope: CoroutineScope,
25 | diffExecutor: Executor? = null, // For test
26 | ) {
27 |
28 | private val modelCache = mutableListOf()
29 |
30 | private val opChannel = Channel(Channel.UNLIMITED)
31 |
32 | init {
33 | opChannel
34 | .consumeAsFlow()
35 | .onEach { handleOp(it) }
36 | .launchIn(coroutineScope)
37 | }
38 |
39 | private val listUpdateCallback: ListUpdateCallback = object : ListUpdateCallback {
40 | override fun onInserted(position: Int, count: Int) {
41 | opChannel.offerSafe(CacheOp.Insert(position, count))
42 | }
43 |
44 | override fun onRemoved(position: Int, count: Int) {
45 | opChannel.offerSafe(CacheOp.Remove(position, count))
46 | }
47 |
48 | override fun onMoved(fromPosition: Int, toPosition: Int) {
49 | opChannel.offerSafe(CacheOp.Move(fromPosition, toPosition))
50 | }
51 |
52 | override fun onChanged(position: Int, count: Int, payload: Any?) {
53 | opChannel.offerSafe(CacheOp.Change(position, count))
54 | }
55 | }
56 |
57 | private val diffConfig = AsyncDifferConfig.Builder(diffCallback)
58 | .apply {
59 | if (diffExecutor != null) {
60 | setBackgroundThreadExecutor(diffExecutor)
61 | }
62 | }
63 | .build()
64 |
65 | private val differ = AsyncPagedListDiffer(
66 | listUpdateCallback,
67 | diffConfig
68 | )
69 |
70 | val currentList: PagedList?
71 | get() = differ.currentList
72 |
73 | fun notifyGetItemAt(position: Int) {
74 | // TODO the position may not be a good value if there are too many injected items.
75 | differ.getItem(min(position, differ.itemCount - 1))
76 | }
77 |
78 | fun submitList(pagedList: PagedList?) {
79 | coroutineScope.launch(Dispatchers.Main.immediate) {
80 | differ.submitList(pagedList)
81 | }
82 | }
83 |
84 | suspend fun getModels(): List {
85 | val models = CompletableDeferred
>()
86 | opChannel.send(CacheOp.Get(models))
87 | return models.await()
88 | }
89 |
90 | fun clearModels() {
91 | opChannel.offerSafe(CacheOp.Clear)
92 | }
93 |
94 | private fun handleOp(op: CacheOp) {
95 | when (op) {
96 | is CacheOp.Insert -> {
97 | (0 until op.count).forEach { _ ->
98 | modelCache.add(op.position, null)
99 | }
100 | rebuildCallback()
101 | }
102 | is CacheOp.Remove -> {
103 | (0 until op.count).forEach { _ ->
104 | modelCache.removeAt(op.position)
105 | }
106 | rebuildCallback()
107 | }
108 | is CacheOp.Move -> {
109 | val model = modelCache.removeAt(op.fromPosition)
110 | modelCache.add(op.toPosition, model)
111 | rebuildCallback()
112 | }
113 | is CacheOp.Change -> {
114 | (op.position until (op.position + op.count)).forEach {
115 | modelCache[it] = null
116 | }
117 | rebuildCallback()
118 | }
119 | is CacheOp.Get -> {
120 | val models = buildCacheModels()
121 | if (models == null) {
122 | // Cache and pagedList are not sync, schedule to build again
123 | opChannel.offerSafe(op)
124 | } else {
125 | op.result.complete(models)
126 | }
127 | }
128 | CacheOp.Clear -> {
129 | modelCache.fill(null)
130 | }
131 | }
132 | }
133 |
134 | private fun buildCacheModels(): List? {
135 | val currentList: List = differ.currentList?.toList().orEmpty()
136 | // Simple check whether modelCache and currentList are sync or not
137 | if (modelCache.size != currentList.size) {
138 | return null
139 | }
140 |
141 | modelCache.indices.forEach { index ->
142 | if (modelCache[index] == null) {
143 | modelCache[index] = modelBuilder(index, currentList[index])
144 | }
145 | }
146 |
147 | @Suppress("UNCHECKED_CAST")
148 | return modelCache.toList() as List
149 | }
150 | }
151 |
--------------------------------------------------------------------------------
/lounge-paging/src/test/java/jp/co/cyberagent/lounge/paging/PagedListRowModelTest.kt:
--------------------------------------------------------------------------------
1 | package jp.co.cyberagent.lounge.paging
2 |
3 | import androidx.lifecycle.Lifecycle
4 | import androidx.lifecycle.LifecycleRegistry
5 | import io.kotest.core.spec.style.FunSpec
6 | import io.kotest.extensions.robolectric.RobolectricTest
7 | import io.kotest.matchers.collections.shouldContainExactly
8 | import io.kotest.matchers.shouldBe
9 | import jp.co.cyberagent.fixture.TestLifecycleOwner
10 | import jp.co.cyberagent.fixture.items
11 | import jp.co.cyberagent.fixture.memoized
12 | import jp.co.cyberagent.lounge.HeaderData
13 | import jp.co.cyberagent.lounge.LambdaLoungeController
14 | import jp.co.cyberagent.lounge.ListRowModel
15 | import jp.co.cyberagent.lounge.LoungeBuildModelScope
16 | import jp.co.cyberagent.lounge.LoungeController
17 | import jp.co.cyberagent.lounge.paging.util.TestModel
18 | import jp.co.cyberagent.lounge.paging.util.asPagedList
19 | import kotlinx.coroutines.CoroutineDispatcher
20 | import kotlinx.coroutines.ExperimentalCoroutinesApi
21 | import kotlinx.coroutines.test.TestCoroutineDispatcher
22 |
23 | @ExperimentalCoroutinesApi
24 | @RobolectricTest
25 | class PagedListRowModelTest : FunSpec({
26 | val owner by memoized { TestLifecycleOwner() }
27 | val dispatcher by memoized { TestCoroutineDispatcher() }
28 | val models = List(10) { TestModel(it + 1L) }
29 |
30 | test("PagedListRowOf") {
31 | val headerData = HeaderData("Title", "Description", "ContentDescription")
32 | val controller = testController(owner.lifecycle, dispatcher) {
33 | pagedListRowOf(
34 | headerData = headerData,
35 | pagedList = models.asPagedList(),
36 | buildItemModel = { _, m -> m!! },
37 | ) {
38 | +it.take(3)
39 | }
40 | }
41 | controller.adapter.size() shouldBe 1
42 | val listRow = controller.adapter[0] as ListRowModel
43 | listRow.headerData shouldBe headerData
44 | listRow.adapter.items shouldContainExactly models.take(3)
45 | }
46 |
47 | test("PagedListRowOf simple") {
48 | val controller = testController(owner.lifecycle, dispatcher) {
49 | pagedListRowOf(
50 | name = "Title",
51 | pagedList = models.asPagedList(),
52 | buildItemModel = { _, m -> m!! },
53 | ) {
54 | +it.take(3)
55 | }
56 | }
57 | controller.adapter.size() shouldBe 1
58 | val listRow = controller.adapter[0] as ListRowModel
59 | listRow.headerData shouldBe HeaderData("Title")
60 | listRow.adapter.items shouldContainExactly models.take(3)
61 | }
62 |
63 | test("PagedListRowFor") {
64 | val headerData = HeaderData("Title", "Description", "ContentDescription")
65 | val controller = testController(owner.lifecycle, dispatcher) {
66 | pagedListRowFor(headerData = headerData, pagedList = models.asPagedList()) { it!! }
67 | }
68 | controller.adapter.size() shouldBe 1
69 | val listRow = controller.adapter[0] as ListRowModel
70 | listRow.headerData shouldBe headerData
71 | listRow.adapter.items shouldContainExactly models
72 | }
73 |
74 | test("PagedListRowFor simple") {
75 | val controller = testController(owner.lifecycle, dispatcher) {
76 | pagedListRowFor(name = "Title", pagedList = models.asPagedList()) { it!! }
77 | }
78 | controller.adapter.size() shouldBe 1
79 | val listRow = controller.adapter[0] as ListRowModel
80 | listRow.headerData shouldBe HeaderData("Title")
81 | listRow.adapter.items shouldContainExactly models
82 | }
83 |
84 | test("PagedListRowForIndexed") {
85 | val headerData = HeaderData("Title", "Description", "ContentDescription")
86 | val controller = testController(owner.lifecycle, dispatcher) {
87 | pagedListRowForIndexed(
88 | headerData = headerData,
89 | pagedList = models.asPagedList()
90 | ) { index, m ->
91 | m!!.copy(key = index + 100L)
92 | }
93 | }
94 | controller.adapter.size() shouldBe 1
95 | val listRow = controller.adapter[0] as ListRowModel
96 | listRow.headerData shouldBe headerData
97 | listRow.adapter.items shouldContainExactly models.mapIndexed { index, m ->
98 | m.copy(key = index + 100L)
99 | }
100 | }
101 |
102 | test("PagedListRowForIndexed simple") {
103 | val controller = testController(owner.lifecycle, dispatcher) {
104 | pagedListRowForIndexed(name = "Title", pagedList = models.asPagedList()) { index, m ->
105 | m!!.copy(key = index + 100L)
106 | }
107 | }
108 | controller.adapter.size() shouldBe 1
109 | val listRow = controller.adapter[0] as ListRowModel
110 | listRow.headerData shouldBe HeaderData("Title")
111 | listRow.adapter.items shouldContainExactly models.mapIndexed { index, m ->
112 | m.copy(key = index + 100L)
113 | }
114 | }
115 | })
116 |
117 | private fun testController(
118 | lifecycle: LifecycleRegistry,
119 | dispatcher: CoroutineDispatcher,
120 | buildModels: suspend LoungeBuildModelScope.() -> Unit,
121 | ): LoungeController {
122 | val controller = LambdaLoungeController(lifecycle, dispatcher)
123 | controller.buildModels = buildModels
124 | controller.requestModelBuild()
125 | lifecycle.currentState = Lifecycle.State.STARTED
126 | return controller
127 | }
128 |
--------------------------------------------------------------------------------
/lounge/src/main/java/jp/co/cyberagent/lounge/TypedRowPresenter.kt:
--------------------------------------------------------------------------------
1 | package jp.co.cyberagent.lounge
2 |
3 | import android.view.ViewGroup
4 | import androidx.annotation.CallSuper
5 | import androidx.leanback.widget.RowPresenter
6 |
7 | /**
8 | * A wrapper around [RowPresenter] which provides generic typed APIs.
9 | */
10 | @Suppress("UNCHECKED_CAST", "TooManyFunctions")
11 | abstract class TypedRowPresenter : RowPresenter() {
12 |
13 | /**
14 | * Called to create a ViewHolder object for a Row. Subclasses will override
15 | * this method to return a different concrete ViewHolder object.
16 | *
17 | * @param parent The parent View for the Row's view holder.
18 | * @return A ViewHolder for the Row's View.
19 | */
20 | protected abstract fun onCreateRow(parent: ViewGroup): VH
21 |
22 | /**
23 | * Binds the given row object to the given ViewHolder.
24 | * Derived classes of [TypedRowPresenter] overriding
25 | * [TypedRowPresenter.onBindRow] must call through the super class's
26 | * implementation of this method.
27 | */
28 | @CallSuper
29 | protected open fun onBindRow(vh: VH, item: T) {
30 | super.onBindRowViewHolder(vh, item)
31 | }
32 |
33 | /**
34 | * Unbinds the given ViewHolder.
35 | * Derived classes of [TypedRowPresenter] overriding [TypedRowPresenter.onUnbindRow]
36 | * must call through the super class's implementation of this method.
37 | */
38 | @CallSuper
39 | protected open fun onUnbindRow(vh: VH) {
40 | super.onUnbindRowViewHolder(vh)
41 | }
42 |
43 | /**
44 | * Called after a [RowPresenter.ViewHolder] is created for a Row.
45 | * Subclasses may override this method and start by calling
46 | * super class's [initializeRow].
47 | *
48 | * @param vh The ViewHolder to initialize for the Row.
49 | */
50 | protected open fun initializeRow(vh: VH) {
51 | super.initializeRowViewHolder(vh)
52 | }
53 |
54 | /**
55 | * Called when the row view's expanded state changes. A subclass may override this method to
56 | * respond to expanded state changes of a Row.
57 | * The default implementation will hide/show the header view. Subclasses may
58 | * make visual changes to the Row View but must not create animation on the
59 | * Row view.
60 | */
61 | protected open fun onRowExpanded(vh: VH, expanded: Boolean) {
62 | super.onRowViewExpanded(vh, expanded)
63 | }
64 |
65 | /**
66 | * This method is only called from [onRowSelected].
67 | * The default behavior is to signal row selected events with a null item parameter.
68 | * A Subclass of [TypedRowPresenter] having child items should override this method and dispatch
69 | * events with item information.
70 | */
71 | protected open fun dispatchItemSelected(vh: VH, selected: Boolean) {
72 | super.dispatchItemSelectedListener(vh, selected)
73 | }
74 |
75 | /**
76 | * Called when the given row view changes selection state. A subclass may override this to
77 | * respond to selected state changes of a Row. A subclass may make visual changes to Row view
78 | * but must not create animation on the Row view.
79 | */
80 | protected open fun onRowSelected(vh: VH, selected: Boolean) {
81 | super.onRowViewSelected(vh, selected)
82 | }
83 |
84 | /**
85 | * Callback when the select level changes. The default implementation applies
86 | * the select level to [androidx.leanback.widget.RowHeaderPresenter.setSelectLevel]
87 | * when [getSelectEffectEnabled] is true. Subclasses may override
88 | * this function and implement a different select effect. In this case,
89 | * the method [isUsingDefaultSelectEffect] should also be overridden to disable
90 | * the default dimming effect.
91 | */
92 | protected open fun onRowSelectLevelChanged(vh: VH) {
93 | super.onSelectLevelChanged(vh)
94 | }
95 |
96 | /**
97 | * Invoked when the row view is attached to the window.
98 | */
99 | protected open fun onRowAttachedToWindow(vh: VH) {
100 | super.onRowViewAttachedToWindow(vh)
101 | }
102 |
103 | /**
104 | * Invoked when the row view is detached from the window.
105 | */
106 | protected open fun onRowDetachedFromWindow(vh: VH) {
107 | super.onRowViewDetachedFromWindow(vh)
108 | }
109 |
110 | // region ---- override parent presenter ----
111 |
112 | final override fun createRowViewHolder(parent: ViewGroup): ViewHolder {
113 | return onCreateRow(parent)
114 | }
115 |
116 | final override fun onBindRowViewHolder(vh: ViewHolder?, item: Any?) {
117 | onBindRow(vh as VH, item as T)
118 | }
119 |
120 | final override fun onUnbindRowViewHolder(vh: ViewHolder?) {
121 | super.onUnbindRowViewHolder(vh)
122 | onUnbindRow(vh as VH)
123 | }
124 |
125 | final override fun initializeRowViewHolder(vh: ViewHolder?) {
126 | initializeRow(vh as VH)
127 | }
128 |
129 | final override fun onRowViewExpanded(vh: ViewHolder?, expanded: Boolean) {
130 | onRowExpanded(vh as VH, expanded)
131 | }
132 |
133 | final override fun dispatchItemSelectedListener(vh: ViewHolder?, selected: Boolean) {
134 | dispatchItemSelected(vh as VH, selected)
135 | }
136 |
137 | final override fun onRowViewSelected(vh: ViewHolder?, selected: Boolean) {
138 | onRowSelected(vh as VH, selected)
139 | }
140 |
141 | final override fun onSelectLevelChanged(vh: ViewHolder?) {
142 | onRowSelectLevelChanged(vh as VH)
143 | }
144 |
145 | final override fun onRowViewAttachedToWindow(vh: ViewHolder?) {
146 | onRowAttachedToWindow(vh as VH)
147 | }
148 |
149 | final override fun onRowViewDetachedFromWindow(vh: ViewHolder?) {
150 | onRowDetachedFromWindow(vh as VH)
151 | }
152 |
153 | // endregion
154 | }
155 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | # Created by https://www.gitignore.io/api/android,intellij,gradle,java,kotlin
3 | # Edit at https://www.gitignore.io/?templates=android,intellij,gradle,java,kotlin
4 |
5 | ### Android ###
6 | # Built application files
7 | *.apk
8 | *.ap_
9 | *.aab
10 |
11 | # Files for the ART/Dalvik VM
12 | *.dex
13 |
14 | # Java class files
15 | *.class
16 |
17 | # Generated files
18 | bin/
19 | gen/
20 | out/
21 | release/*
22 |
23 | # Gradle files
24 | .gradle/
25 | build/
26 |
27 | # Local configuration file (sdk path, etc)
28 | local.properties
29 |
30 | # Proguard folder generated by Eclipse
31 | proguard/
32 |
33 | # Log Files
34 | *.log
35 |
36 | # Android Studio Navigation editor temp files
37 | .navigation/
38 |
39 | # Android Studio captures folder
40 | captures/
41 |
42 | # IntelliJ
43 | *.iml
44 | .idea/workspace.xml
45 | .idea/tasks.xml
46 | .idea/gradle.xml
47 | .idea/assetWizardSettings.xml
48 | .idea/dictionaries
49 | .idea/libraries
50 | .idea/compiler.xml
51 | .idea/misc.xml
52 | .idea/jarRepositories.xml
53 | .idea/kotlinScripting.xml
54 | .idea/runConfigurations.xml
55 | .idea/$CACHE_FILE$
56 | # Android Studio 3 in .gitignore file.
57 | .idea/caches
58 | .idea/modules.xml
59 | # Comment next line if keeping position of elements in Navigation Editor is relevant for you
60 | .idea/navEditor.xml
61 |
62 | # Keystore files
63 | # Uncomment the following lines if you do not want to check your keystore files in.
64 | #*.jks
65 | #*.keystore
66 |
67 | # External native build folder generated in Android Studio 2.2 and later
68 | .externalNativeBuild
69 |
70 | # Google Services (e.g. APIs or Firebase)
71 | # google-services.json
72 |
73 | # Freeline
74 | freeline.py
75 | freeline/
76 | freeline_project_description.json
77 |
78 | # fastlane
79 | fastlane/report.xml
80 | fastlane/Preview.html
81 | fastlane/screenshots
82 | fastlane/test_output
83 | fastlane/readme.md
84 |
85 | # Version control
86 | vcs.xml
87 |
88 | # lint
89 | lint/intermediates/
90 | lint/generated/
91 | lint/outputs/
92 | lint/tmp/
93 | # lint/reports/
94 |
95 | ### Android Patch ###
96 | gen-external-apklibs
97 | output.json
98 |
99 | # Replacement of .externalNativeBuild directories introduced
100 | # with Android Studio 3.5.
101 | .cxx/
102 |
103 | ### Intellij ###
104 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm
105 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
106 |
107 | # User-specific stuff
108 | .idea/**/workspace.xml
109 | .idea/**/tasks.xml
110 | .idea/**/usage.statistics.xml
111 | .idea/**/dictionaries
112 | .idea/**/shelf
113 |
114 | # Generated files
115 | .idea/**/contentModel.xml
116 |
117 | # Sensitive or high-churn files
118 | .idea/**/dataSources/
119 | .idea/**/dataSources.ids
120 | .idea/**/dataSources.local.xml
121 | .idea/**/sqlDataSources.xml
122 | .idea/**/dynamic.xml
123 | .idea/**/uiDesigner.xml
124 | .idea/**/dbnavigator.xml
125 |
126 | # Gradle
127 | .idea/**/gradle.xml
128 | .idea/**/libraries
129 |
130 | # Gradle and Maven with auto-import
131 | # When using Gradle or Maven with auto-import, you should exclude module files,
132 | # since they will be recreated, and may cause churn. Uncomment if using
133 | # auto-import.
134 | # .idea/modules.xml
135 | # .idea/*.iml
136 | # .idea/modules
137 | # *.iml
138 | # *.ipr
139 |
140 | # CMake
141 | cmake-build-*/
142 |
143 | # Mongo Explorer plugin
144 | .idea/**/mongoSettings.xml
145 |
146 | # File-based project format
147 | *.iws
148 |
149 | # IntelliJ
150 |
151 | # mpeltonen/sbt-idea plugin
152 | .idea_modules/
153 |
154 | # JIRA plugin
155 | atlassian-ide-plugin.xml
156 |
157 | # Cursive Clojure plugin
158 | .idea/replstate.xml
159 |
160 | # Crashlytics plugin (for Android Studio and IntelliJ)
161 | com_crashlytics_export_strings.xml
162 | crashlytics.properties
163 | crashlytics-build.properties
164 | fabric.properties
165 |
166 | # Editor-based Rest Client
167 | .idea/httpRequests
168 |
169 | # Android studio 3.1+ serialized cache file
170 | .idea/caches/build_file_checksums.ser
171 |
172 | ### Intellij Patch ###
173 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721
174 |
175 | # *.iml
176 | # modules.xml
177 | # .idea/misc.xml
178 | # *.ipr
179 |
180 | # Sonarlint plugin
181 | .idea/**/sonarlint/
182 |
183 | # SonarQube Plugin
184 | .idea/**/sonarIssues.xml
185 |
186 | # Markdown Navigator plugin
187 | .idea/**/markdown-navigator.xml
188 | .idea/**/markdown-navigator/
189 |
190 | ### Java ###
191 | # Compiled class file
192 |
193 | # Log file
194 |
195 | # BlueJ files
196 | *.ctxt
197 |
198 | # Mobile Tools for Java (J2ME)
199 | .mtj.tmp/
200 |
201 | # Package Files #
202 | *.jar
203 | *.war
204 | *.nar
205 | *.ear
206 | *.zip
207 | *.tar.gz
208 | *.rar
209 |
210 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
211 | hs_err_pid*
212 |
213 | ### Kotlin ###
214 | # Compiled class file
215 |
216 | # Log file
217 |
218 | # BlueJ files
219 |
220 | # Mobile Tools for Java (J2ME)
221 |
222 | # Package Files #
223 |
224 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
225 |
226 | ### Gradle ###
227 | .gradle
228 |
229 | # Ignore Gradle GUI config
230 | gradle-app.setting
231 |
232 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored)
233 | !gradle-wrapper.jar
234 |
235 | # Cache of project
236 | .gradletasknamecache
237 |
238 | # # Work around https://youtrack.jetbrains.com/issue/IDEA-116898
239 | # gradle/wrapper/gradle-wrapper.properties
240 |
241 | ### Gradle Patch ###
242 | **/build/
243 |
244 | # End of https://www.gitignore.io/api/android,intellij,gradle,java,kotlin
245 |
246 | docs/api
247 |
--------------------------------------------------------------------------------
/.idea/codeStyles/Project.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 | xmlns:android
28 |
29 | ^$
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 | xmlns:.*
39 |
40 | ^$
41 |
42 |
43 | BY_NAME
44 |
45 |
46 |
47 |
48 |
49 |
50 | .*:id
51 |
52 | http://schemas.android.com/apk/res/android
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 | .*:name
62 |
63 | http://schemas.android.com/apk/res/android
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 | name
73 |
74 | ^$
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 | style
84 |
85 | ^$
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 | .*
95 |
96 | ^$
97 |
98 |
99 | BY_NAME
100 |
101 |
102 |
103 |
104 |
105 |
106 | .*
107 |
108 | http://schemas.android.com/apk/res/android
109 |
110 |
111 | ANDROID_ATTRIBUTE_ORDER
112 |
113 |
114 |
115 |
116 |
117 |
118 | .*
119 |
120 | .*
121 |
122 |
123 | BY_NAME
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
--------------------------------------------------------------------------------
/sample/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
12 |
18 |
24 |
30 |
36 |
42 |
48 |
54 |
60 |
66 |
72 |
78 |
84 |
90 |
96 |
102 |
108 |
114 |
120 |
126 |
132 |
138 |
144 |
150 |
156 |
162 |
168 |
174 |
180 |
186 |
192 |
198 |
204 |
205 |
--------------------------------------------------------------------------------
/docs/object-adapter.md:
--------------------------------------------------------------------------------
1 | # ObjectAdapter Support
2 |
3 | `ObjectAdapter` is used in many Leanback components (e.g. `BrowseSupportFragment`, `VerticalGridSupportFragment`, et al.) to display list items.
4 | `ObjectAdapter` uses `Presenter`s to create views and bind data to those views.
5 |
6 | Lounge provides `LoungeController` and `LoungeModel` to help you construct `ObjectAdapter` in a declarative programming style.
7 | The implementation of `LoungeController` is referred to the well-known RecyclerView library [Airbnb/Epoxy](https://github.com/airbnb/epoxy).
8 | If you are familiar with Epoxy, Lounge will be easy to use.
9 |
10 | ## Basic Usage
11 |
12 | Described here are the fundamentals for building list UIs with Lounge.
13 |
14 | ### Creating LoungeModel
15 |
16 | `LoungeModel` is the base unit that describe how your views should be displayed via the `Presenter`.
17 |
18 | ```kotlin
19 | data class TextModel(
20 | val name: String
21 | ) : LoungeModel {
22 | override val key: Long = name.toLoungeModelKey()
23 | override val presenter: Presenter = TextModelPresenter
24 | }
25 | ```
26 |
27 | Each `LoungeModel` should have a unique `key` to allow RecyclerView diffing algorithm works.
28 | You can use the extension function `Any.toLoungeModelKey()` to get a key from any object.
29 | To get better performance, it is recommended to also implement `equals()` properly.
30 | In Kotlin, we can easily achieve this via data class.
31 |
32 | The `presenter` is a normal Leanback `Presenter` which can bind the `LoungeModel` to a view.
33 | You can create `Presenter` using the default `ViewHolder` pattern that provided by Leanback itself or using the `DataBindingPresenter` provided by Lounge.
34 | `Presenter` better be a singleton object so it can be shared with multiple instances.
35 |
36 | `DataBindingPresenter` example:
37 |
38 | ```kotlin
39 | object TextModelPresenter : DataBindingPresenter(R.layout.model_text) {
40 | override fun onBind(binding: ModelTextBinding, item: TextModel) {
41 | binding.model = item
42 | }
43 | }
44 | ```
45 |
46 | Or simply using `SimpleDataBindingPresenter`:
47 |
48 | ```kotlin
49 | data class TextModel(
50 | val name: String,
51 | ) : LoungeModel {
52 | override val key: Long = name.toLoungeModelKey()
53 | override val presenter: Presenter
54 | get() = SimpleDataBindingPresenter(R.layout.model_text, BR.model)
55 | }
56 | ```
57 |
58 | ### Using LoungeModel inside LoungeController
59 |
60 | `LoungeController` defines what `LoungeModel`s should be added into the `ObjectAdapter`.
61 |
62 | The controller's `buildModels` method declared which `LoungeModel`s to show.
63 | You are responsible for calling `requestModelBuild` whenever your data changes,
64 | which triggers `buildModels` to run again.
65 |
66 | Example to show a list of `TextModel`:
67 |
68 | ```kotlin
69 | class MyController(lifecycle: Lifecycle) : LoungeController(lifecycle) {
70 |
71 | var names: List = emptyList()
72 | set(value) {
73 | if (field != value) {
74 | field = value
75 | requestModelBuild()
76 | }
77 | }
78 |
79 | override suspend fun buildModels() {
80 | names.forEach {
81 | +TextModel(it)
82 | }
83 | }
84 | }
85 | ```
86 |
87 | Every time `names` changes, we call `requestModelBuild`.
88 | The custom getter of `names` contains a lot of boilerplate code, instead we can use the `loungeProp` delegated property:
89 |
90 | ```kotlin
91 | var names: List by loungeProp(emptyList())
92 | ```
93 |
94 | Similar to Epoxy, `requestModelBuild` requests that models be built but does not guarantee that it will happen immediately.
95 | Calling `requestModelBuild` multiple times will cancel the previous uncompleted build.
96 | This is to decouple model building from data changes.
97 | This way all data updates can be completed in full without worrying about calling requestModelBuild multiple times.
98 |
99 | ### Integrating with Leanback components
100 |
101 | We can get the backing `ObjectAdapter` off the `LoungeController` and set up into Leanback components.
102 | Here are some examples.
103 |
104 | #### Set up for VerticalGridSupportFragment
105 |
106 | ```kotlin
107 | class MyVerticalGripFragment : VerticalGridSupportFragment() {
108 |
109 | private val viewModel by viewModels()
110 | private val controller by lazy { MyController(lifecycle) }
111 |
112 | override fun onCreate(savedInstanceState: Bundle?) {
113 | super.onCreate(savedInstanceState)
114 | gridPresenter = VerticalGridPresenter().apply {
115 | numberOfColumns = 5
116 | }
117 | adapter = controller.adapter
118 | }
119 |
120 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
121 | super.onViewCreated(view, savedInstanceState)
122 | viewModel.names.observe(viewLifecycleOwner) {
123 | controller.names = it
124 | }
125 | }
126 | }
127 | ```
128 |
129 | #### Set up for RowsSupportFragment
130 |
131 | Lounge provides `listRow`, `listRowFor`, `listRowOf` for simplify creating multiple rows UI.
132 |
133 | Create a `LoungeController` that has two rows, await all data becoming available before the first build:
134 |
135 | ```kotlin
136 | class MyRowsController(lifecycle: Lifecycle) : LoungeController(lifecycle) {
137 |
138 | var row1: List? by loungeProp(null)
139 | var row2: List? by loungeProp(null)
140 |
141 | override suspend fun buildModels() {
142 | val row1 = row1 ?: awaitCancellation()
143 | val row2 = row2 ?: awaitCancellation()
144 |
145 | listRowFor(
146 | name = "Row 1",
147 | list = row1
148 | ) {
149 | TextModel(it)
150 | }
151 |
152 | listRowFor(
153 | name = "Row 2",
154 | list = row2
155 | ) {
156 | TextModel(it)
157 | }
158 | }
159 | }
160 | ```
161 |
162 | Use the controller inside `BrowseSupportFragment`:
163 |
164 | ```kotlin
165 | class MyRowsFragment : RowsSupportFragment() {
166 | private val controller by lazy { MyRowsController(lifecycle) }
167 |
168 | override fun onCreate(savedInstanceState: Bundle?) {
169 | super.onCreate(savedInstanceState)
170 | adapter = controller.adapter
171 | }
172 |
173 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
174 | super.onViewCreated(view, savedInstanceState)
175 | // Observe data and update row1, row2
176 | }
177 | }
178 | ```
179 |
180 | ## Advanced Usage
181 |
182 | // TODO
183 |
--------------------------------------------------------------------------------
/lounge/src/test/java/jp/co/cyberagent/lounge/ListRowModelTest.kt:
--------------------------------------------------------------------------------
1 | package jp.co.cyberagent.lounge
2 |
3 | import androidx.lifecycle.Lifecycle
4 | import androidx.lifecycle.LifecycleRegistry
5 | import io.kotest.core.spec.style.FunSpec
6 | import io.kotest.extensions.robolectric.RobolectricTest
7 | import io.kotest.matchers.shouldBe
8 | import jp.co.cyberagent.fixture.TestLifecycleOwner
9 | import jp.co.cyberagent.fixture.memoized
10 | import jp.co.cyberagent.lounge.util.TestModel
11 | import kotlinx.coroutines.CoroutineDispatcher
12 | import kotlinx.coroutines.Dispatchers
13 | import kotlinx.coroutines.ExperimentalCoroutinesApi
14 | import kotlinx.coroutines.test.TestCoroutineDispatcher
15 | import kotlinx.coroutines.test.setMain
16 |
17 | @ExperimentalCoroutinesApi
18 | @RobolectricTest
19 | class ListRowModelTest : FunSpec({
20 | val owner by memoized { TestLifecycleOwner() }
21 | val dispatcher by memoized { TestCoroutineDispatcher() }
22 | val models = List(10) { TestModel(it + 1L) }
23 |
24 | beforeEach {
25 | Dispatchers.setMain(dispatcher)
26 | }
27 |
28 | test("ListRowModel has ListRow info") {
29 | val headerData = HeaderData("Title", "Description", "ContentDescription")
30 | val controller = testController(owner.lifecycle, dispatcher) {}
31 | val listRow = ListRowModel(
32 | key = 1L,
33 | headerData = HeaderData("Title", "Description", "ContentDescription"),
34 | controller = controller,
35 | )
36 | listRow.id shouldBe 1L
37 | listRow.adapter shouldBe controller.adapter
38 | listRow.headerItem.name shouldBe headerData.name
39 | listRow.headerItem.description shouldBe headerData.description
40 | listRow.headerItem.contentDescription shouldBe headerData.contentDescription
41 | }
42 |
43 | test("ListRow") {
44 | val listRowController = object : LoungeController(owner.lifecycle, dispatcher) {
45 | override suspend fun buildModels() = +models
46 | }
47 | val headerData = HeaderData("Title", "Description", "ContentDescription")
48 | val controller = testController(owner.lifecycle, dispatcher) {
49 | listRow(
50 | headerData = headerData,
51 | key = "key",
52 | controller = listRowController
53 | )
54 | }
55 | listRowController.requestModelBuild()
56 | controller.adapter.size() shouldBe 1
57 | val listRow = controller.adapter[0] as ListRowModel
58 | listRow.key shouldBe "key".toLoungeModelKey()
59 | listRow.headerData shouldBe headerData
60 | listRow.controller shouldBe listRowController
61 | models.forEachIndexed { index, m -> listRow.adapter[index] shouldBe m }
62 | }
63 |
64 | test("ListRow name as key") {
65 | val headerData = HeaderData("Title")
66 | val controller = testController(owner.lifecycle, dispatcher) {
67 | listRow(
68 | headerData = headerData,
69 | controller = testController(owner.lifecycle, dispatcher) {}
70 | )
71 | }
72 | val listRow = controller.adapter[0] as ListRowModel
73 | listRow.key shouldBe headerData.toLoungeModelKey()
74 | }
75 |
76 | test("ListRowOf") {
77 | val headerData = HeaderData("Title", "Description", "ContentDescription")
78 | val controller = testController(owner.lifecycle, dispatcher) {
79 | listRowOf(headerData = headerData) {
80 | +models
81 | }
82 | }
83 | controller.adapter.size() shouldBe 1
84 | val listRow = controller.adapter[0] as ListRowModel
85 | listRow.headerData shouldBe headerData
86 | models.forEachIndexed { index, m -> listRow.adapter[index] shouldBe m }
87 | }
88 |
89 | test("ListRowOf simple") {
90 | val controller = testController(owner.lifecycle, dispatcher) {
91 | listRowOf(name = "Title") {
92 | +models
93 | }
94 | }
95 | controller.adapter.size() shouldBe 1
96 | val listRow = controller.adapter[0] as ListRowModel
97 | listRow.headerData shouldBe HeaderData("Title")
98 | models.forEachIndexed { index, m -> listRow.adapter[index] shouldBe m }
99 | }
100 |
101 | test("ListRowFor") {
102 | val headerData = HeaderData("Title", "Description", "ContentDescription")
103 | val controller = testController(owner.lifecycle, dispatcher) {
104 | listRowFor(headerData = headerData, list = models) { it }
105 | }
106 | controller.adapter.size() shouldBe 1
107 | val listRow = controller.adapter[0] as ListRowModel
108 | listRow.headerData shouldBe headerData
109 | models.forEachIndexed { index, m -> listRow.adapter[index] shouldBe m }
110 | }
111 |
112 | test("ListRowFor simple") {
113 | val controller = testController(owner.lifecycle, dispatcher) {
114 | listRowFor(name = "Title", list = models) { it }
115 | }
116 | controller.adapter.size() shouldBe 1
117 | val listRow = controller.adapter[0] as ListRowModel
118 | listRow.headerData shouldBe HeaderData("Title")
119 | models.forEachIndexed { index, m -> listRow.adapter[index] shouldBe m }
120 | }
121 |
122 | test("ListRowForIndexed") {
123 | val headerData = HeaderData("Title", "Description", "ContentDescription")
124 | val controller = testController(owner.lifecycle, dispatcher) {
125 | listRowForIndexed(headerData = headerData, list = models) { index, m ->
126 | m.copy(key = index + 100L)
127 | }
128 | }
129 | controller.adapter.size() shouldBe 1
130 | val listRow = controller.adapter[0] as ListRowModel
131 | listRow.headerData shouldBe headerData
132 | models.forEachIndexed { index, m ->
133 | listRow.adapter[index] shouldBe m.copy(key = index + 100L)
134 | }
135 | }
136 |
137 | test("ListRowForIndexed simple") {
138 | val controller = testController(owner.lifecycle, dispatcher) {
139 | listRowForIndexed(name = "Title", list = models) { index, m ->
140 | m.copy(key = index + 100L)
141 | }
142 | }
143 | controller.adapter.size() shouldBe 1
144 | val listRow = controller.adapter[0] as ListRowModel
145 | listRow.headerData shouldBe HeaderData("Title")
146 | models.forEachIndexed { index, m ->
147 | listRow.adapter[index] shouldBe m.copy(key = index + 100L)
148 | }
149 | }
150 | })
151 |
152 | private fun testController(
153 | lifecycle: LifecycleRegistry,
154 | dispatcher: CoroutineDispatcher,
155 | buildModels: suspend LoungeBuildModelScope.() -> Unit,
156 | ): LoungeController {
157 | val controller = LambdaLoungeController(lifecycle, dispatcher)
158 | controller.buildModels = buildModels
159 | controller.requestModelBuild()
160 | lifecycle.currentState = Lifecycle.State.STARTED
161 | return controller
162 | }
163 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/lounge/src/main/java/jp/co/cyberagent/lounge/LoungeGuidedAction.kt:
--------------------------------------------------------------------------------
1 | package jp.co.cyberagent.lounge
2 |
3 | import android.annotation.SuppressLint
4 | import androidx.leanback.app.GuidedStepSupportFragment
5 | import androidx.leanback.widget.DiffCallback
6 | import androidx.leanback.widget.GuidedAction
7 | import androidx.leanback.widget.GuidedActionDiffCallback
8 |
9 | /**
10 | * A [GuidedAction] with more convenient options.
11 | */
12 | open class LoungeGuidedAction : GuidedAction() {
13 |
14 | /**
15 | * Custom layout id of this action.
16 | */
17 | var layoutId: Int = DEFAULT_LAYOUT_ID
18 |
19 | /**
20 | * Listener invoked when this action is clicked.
21 | */
22 | var onClickedListener: OnGuidedActionClickedListener? = null
23 |
24 | /**
25 | * Listener invoked when this action in sub actions is clicked.
26 | */
27 | var onSubClickedListener: OnSubGuidedActionClickedListener? = null
28 |
29 | /**
30 | * Listener invoked when this action is focused (made to be the current selection).
31 | */
32 | var onFocusedListener: OnGuidedActionFocusedListener? = null
33 |
34 | /**
35 | * Listener invoked when this action has been edited, for example when user clicks confirm button
36 | * in IME window.
37 | */
38 | var onEditedAndProceedListener: OnGuidedActionEditedAndProceedListener? = null
39 |
40 | /**
41 | * Listener invoked when this action has been canceled editing, for example when user closes
42 | * IME window by BACK key.
43 | */
44 | var onEditCanceledListener: OnGuidedActionEditCanceledListener? = null
45 |
46 | /**
47 | * Listener invoked when an action is clicked.
48 | */
49 | fun interface OnGuidedActionClickedListener {
50 | fun onClicked(action: LoungeGuidedAction)
51 | }
52 |
53 | /**
54 | * Listener invoked when an action in sub actions is clicked.
55 | */
56 | fun interface OnSubGuidedActionClickedListener {
57 | fun onClicked(action: LoungeGuidedAction): Boolean
58 | }
59 |
60 | /**
61 | * Listener invoked when an action is focused (made to be the current selection).
62 | */
63 | fun interface OnGuidedActionFocusedListener {
64 | fun onFocused(action: LoungeGuidedAction)
65 | }
66 |
67 | /**
68 | * Listener invoked when an action has been edited, for example when user clicks confirm button
69 | * in IME window.
70 | */
71 | fun interface OnGuidedActionEditedAndProceedListener {
72 | fun onEditedAndProceed(action: LoungeGuidedAction): Long
73 | }
74 |
75 | /**
76 | * Listener invoked when an action has been canceled editing, for example when user closes
77 | * IME window by BACK key.
78 | */
79 | fun interface OnGuidedActionEditCanceledListener {
80 | fun onEditCanceled(action: LoungeGuidedAction)
81 | }
82 |
83 | companion object {
84 | /**
85 | * Indicates the action should use a default layout in [LoungeGuidedActionsStylist].
86 | */
87 | const val DEFAULT_LAYOUT_ID = 0
88 | }
89 | }
90 |
91 | /**
92 | * If this action is a [LoungeGuidedAction], invokes its [LoungeGuidedAction.onClickedListener].
93 | *
94 | * You can override [GuidedStepSupportFragment.onGuidedActionClicked] and call this function with
95 | * the given action.
96 | */
97 | fun onLoungeGuidedActionClicked(action: GuidedAction?) {
98 | (action as? LoungeGuidedAction)?.onClickedListener?.onClicked(action)
99 | }
100 |
101 | /**
102 | * If this action is a [LoungeGuidedAction], invokes its [LoungeGuidedAction.onSubClickedListener]
103 | * and returns the invoked result. Otherwise returns true.
104 | *
105 | * You can override [GuidedStepSupportFragment.onSubGuidedActionClicked] and call this function with
106 | * the given action.
107 | */
108 | fun onSubLoungeGuidedActionClicked(action: GuidedAction?): Boolean {
109 | return (action as? LoungeGuidedAction)?.onSubClickedListener?.onClicked(action) ?: true
110 | }
111 |
112 | /**
113 | * If this action is a [LoungeGuidedAction], invokes its [LoungeGuidedAction.onFocusedListener].
114 | *
115 | * You can override [GuidedStepSupportFragment.onGuidedActionFocused] and call this function with
116 | * the given action.
117 | */
118 | fun onLoungeGuidedActionFocused(action: GuidedAction?) {
119 | (action as? LoungeGuidedAction)?.onFocusedListener?.onFocused(action)
120 | }
121 |
122 | /**
123 | * If this action is a [LoungeGuidedAction], invokes its [LoungeGuidedAction.onEditedAndProceedListener]
124 | * and returns the invoked result. Otherwise returns [GuidedAction.ACTION_ID_NEXT].
125 | *
126 | * You can override [GuidedStepSupportFragment.onGuidedActionEditedAndProceed] and call this function with
127 | * the given action.
128 | */
129 | fun onLoungeGuidedActionEditedAndProceed(action: GuidedAction?): Long {
130 | return (action as? LoungeGuidedAction)?.onEditedAndProceedListener?.onEditedAndProceed(action)
131 | ?: GuidedAction.ACTION_ID_NEXT
132 | }
133 |
134 | /**
135 | * If this action is a [LoungeGuidedAction], invokes its [LoungeGuidedAction.onEditCanceledListener].
136 | *
137 | * You can override [GuidedStepSupportFragment.onGuidedActionEditCanceled] and call this function with
138 | * the given action.
139 | */
140 | fun onLoungeGuidedActionEditCanceled(action: GuidedAction?) {
141 | (action as? LoungeGuidedAction)?.onEditCanceledListener?.onEditCanceled(action)
142 | }
143 |
144 | /**
145 | * DiffCallback used for [LoungeGuidedAction].
146 | *
147 | * @see [GuidedStepSupportFragment.setActionsDiffCallback]
148 | */
149 | object LoungeGuidedActionDiffCallback : DiffCallback() {
150 |
151 | private val guidedActionDiffCallback = GuidedActionDiffCallback.getInstance()
152 |
153 | override fun areItemsTheSame(oldItem: GuidedAction, newItem: GuidedAction): Boolean =
154 | guidedActionDiffCallback.areItemsTheSame(oldItem, newItem)
155 |
156 | @SuppressLint("DiffUtilEquals")
157 | override fun areContentsTheSame(oldItem: GuidedAction, newItem: GuidedAction): Boolean {
158 | val rawCompare = guidedActionDiffCallback.areContentsTheSame(oldItem, newItem)
159 | return if (oldItem is LoungeGuidedAction && newItem is LoungeGuidedAction) {
160 | rawCompare &&
161 | oldItem.layoutId == newItem.layoutId &&
162 | oldItem.onClickedListener == newItem.onClickedListener &&
163 | oldItem.onSubClickedListener == newItem.onSubClickedListener &&
164 | oldItem.onFocusedListener == newItem.onFocusedListener &&
165 | oldItem.onEditedAndProceedListener == newItem.onEditedAndProceedListener &&
166 | oldItem.onEditCanceledListener == newItem.onEditCanceledListener
167 | } else {
168 | rawCompare
169 | }
170 | }
171 | }
172 |
--------------------------------------------------------------------------------
/lounge-paging/src/main/java/jp/co/cyberagent/lounge/paging/PagedListRowModel.kt:
--------------------------------------------------------------------------------
1 | package jp.co.cyberagent.lounge.paging
2 |
3 | import androidx.leanback.widget.HeaderItem
4 | import androidx.leanback.widget.ListRow
5 | import androidx.leanback.widget.ListRowPresenter
6 | import androidx.paging.PagedList
7 | import jp.co.cyberagent.lounge.HeaderData
8 | import jp.co.cyberagent.lounge.ListRowModel
9 | import jp.co.cyberagent.lounge.LoungeBuildModelScope
10 | import jp.co.cyberagent.lounge.LoungeModel
11 | import jp.co.cyberagent.lounge.listRow
12 | import jp.co.cyberagent.lounge.memorizedController
13 |
14 | /**
15 | * Adds a [ListRowModel] that works with [PagedList] to this scope.
16 | * Either [headerData] or [key] must be provided to properly set the [ListRowModel.key].
17 | *
18 | * @param headerData if provided, set a [HeaderItem] with the data to the [ListRow].
19 | * @param key if provided, set it as the [ListRowModel.key].
20 | * @param presenter the [ListRowPresenter] for the [ListRow].
21 | * @param buildItemModel build a item in [PagedList] into a [LoungeModel].
22 | * @param buildModels models added in this scope will be add to the [ListRowModel].
23 | */
24 | suspend fun LoungeBuildModelScope.pagedListRowOf(
25 | headerData: HeaderData? = null,
26 | pagedList: PagedList?,
27 | key: Any? = null,
28 | presenter: ListRowPresenter = ListRowModel.DefaultListRowPresenter,
29 | buildItemModel: (Int, T?) -> LoungeModel,
30 | buildModels: suspend PagedListLoungeBuildModelScope.(List) -> Unit,
31 | ) {
32 | val controllerKey = requireNotNull(key ?: headerData) {
33 | "Require key or headerData to be non-null."
34 | }
35 | val controller = memorizedController(controllerKey) {
36 | LambdaPagedListLoungeController(lifecycle, modelBuildingDispatcher)
37 | }
38 | controller.buildItemModel = buildItemModel
39 | controller.buildModels = buildModels
40 | controller.submitList(pagedList)
41 | controller.requestForceModelBuild()
42 | listRow(
43 | headerData = headerData,
44 | key = key,
45 | controller = controller,
46 | presenter = presenter
47 | )
48 | }
49 |
50 | /**
51 | * Adds a [ListRowModel] that works with [PagedList] to this scope.
52 | * Either [name] or [key] must be provided to properly set the [ListRowModel.key].
53 | *
54 | * @param name if provided, set a [HeaderItem] with the name to the [ListRow].
55 | * @param key if provided, set it as the [ListRowModel.key].
56 | * @param presenter the [ListRowPresenter] for the [ListRow].
57 | * @param buildItemModel build a item in [PagedList] into a [LoungeModel].
58 | * @param buildModels models added in this scope will be add to the [ListRowModel].
59 | */
60 | suspend fun LoungeBuildModelScope.pagedListRowOf(
61 | name: String? = null,
62 | pagedList: PagedList?,
63 | key: Any? = null,
64 | presenter: ListRowPresenter = ListRowModel.DefaultListRowPresenter,
65 | buildItemModel: (Int, T?) -> LoungeModel,
66 | buildModels: suspend PagedListLoungeBuildModelScope.(List) -> Unit,
67 | ) {
68 | pagedListRowOf(
69 | headerData = name?.let { HeaderData(it) },
70 | pagedList = pagedList,
71 | key = key,
72 | presenter = presenter,
73 | buildItemModel = buildItemModel,
74 | buildModels = buildModels,
75 | )
76 | }
77 |
78 | /**
79 | * Adds a [ListRowModel] that works with [PagedList] to this scope.
80 | * Builds the model on each item of the [PagedList].
81 | * Either [headerData] or [key] must be provided to properly set the [ListRowModel.key].
82 | *
83 | * @param headerData if provided, set a [HeaderItem] with the data to the [ListRow].
84 | * @param key if provided, set it as the [ListRowModel.key].
85 | * @param presenter the [ListRowPresenter] for the [ListRow].
86 | * @param buildItemModel build a item in [PagedList] into a [LoungeModel].
87 | */
88 | suspend fun LoungeBuildModelScope.pagedListRowFor(
89 | headerData: HeaderData? = null,
90 | pagedList: PagedList?,
91 | key: Any? = null,
92 | presenter: ListRowPresenter = ListRowModel.DefaultListRowPresenter,
93 | buildItemModel: (T?) -> LoungeModel,
94 | ) {
95 | pagedListRowOf(
96 | headerData = headerData,
97 | pagedList = pagedList,
98 | key = key,
99 | presenter = presenter,
100 | buildItemModel = { _, item -> buildItemModel(item) },
101 | buildModels = { +it }
102 | )
103 | }
104 |
105 | /**
106 | * Adds a [ListRowModel] that works with [PagedList] to this scope.
107 | * Builds the model on each item of the [PagedList].
108 | * Either [name] or [key] must be provided to properly set the [ListRowModel.key].
109 | *
110 | * @param name if provided, set a [HeaderItem] with the name to the [ListRow].
111 | * @param key if provided, set it as the [ListRowModel.key].
112 | * @param presenter the [ListRowPresenter] for the [ListRow].
113 | * @param buildItemModel build a item in [PagedList] into a [LoungeModel].
114 | */
115 | suspend fun LoungeBuildModelScope.pagedListRowFor(
116 | name: String? = null,
117 | pagedList: PagedList?,
118 | key: Any? = null,
119 | presenter: ListRowPresenter = ListRowModel.DefaultListRowPresenter,
120 | buildItemModel: (T?) -> LoungeModel,
121 | ) {
122 | pagedListRowFor(
123 | headerData = name?.let { HeaderData(it) },
124 | pagedList = pagedList,
125 | key = key,
126 | presenter = presenter,
127 | buildItemModel = buildItemModel,
128 | )
129 | }
130 |
131 | /**
132 | * Adds a [ListRowModel] that works with [PagedList] to this scope.
133 | * Builds the model on each item of the [PagedList], providing sequential index with the element.
134 | * Either [headerData] or [key] must be provided to properly set the [ListRowModel.key].
135 | *
136 | * @param headerData if provided, set a [HeaderItem] with the data to the [ListRow].
137 | * @param key if provided, set it as the [ListRowModel.key].
138 | * @param presenter the [ListRowPresenter] for the [ListRow].
139 | * @param buildItemModel build a item in [PagedList] into a [LoungeModel].
140 | */
141 | suspend fun LoungeBuildModelScope.pagedListRowForIndexed(
142 | headerData: HeaderData? = null,
143 | pagedList: PagedList?,
144 | key: Any? = null,
145 | presenter: ListRowPresenter = ListRowModel.DefaultListRowPresenter,
146 | buildItemModel: (Int, T?) -> LoungeModel,
147 | ) {
148 | pagedListRowOf(
149 | headerData = headerData,
150 | pagedList = pagedList,
151 | key = key,
152 | presenter = presenter,
153 | buildItemModel = buildItemModel,
154 | buildModels = { +it }
155 | )
156 | }
157 |
158 | /**
159 | * Adds a [ListRowModel] that works with [PagedList] to this scope.
160 | * Builds the model on each item of the [PagedList], providing sequential index with the element.
161 | * Either [name] or [key] must be provided to properly set the [ListRowModel.key].
162 | *
163 | * @param name if provided, set a [HeaderItem] with the name to the [ListRow].
164 | * @param key if provided, set it as the [ListRowModel.key].
165 | * @param presenter the [ListRowPresenter] for the [ListRow].
166 | * @param buildItemModel build a item in [PagedList] into a [LoungeModel].
167 | */
168 | suspend fun LoungeBuildModelScope.pagedListRowForIndexed(
169 | name: String? = null,
170 | pagedList: PagedList?,
171 | key: Any? = null,
172 | presenter: ListRowPresenter = ListRowModel.DefaultListRowPresenter,
173 | buildItemModel: (Int, T?) -> LoungeModel,
174 | ) {
175 | pagedListRowForIndexed(
176 | headerData = name?.let { HeaderData(it) },
177 | pagedList = pagedList,
178 | key = key,
179 | presenter = presenter,
180 | buildItemModel = buildItemModel,
181 | )
182 | }
183 |
--------------------------------------------------------------------------------