[Sample App] Modernized sample app (#1622)

Upgraded all dependencies, migrated RxJava to coroutines, and ViewBinding
diff --git a/LottieSample/build.gradle b/LottieSample/build.gradle
index 7447eb2..db18b90 100644
--- a/LottieSample/build.gradle
+++ b/LottieSample/build.gradle
@@ -2,8 +2,15 @@
 apply plugin: 'kotlin-android'
 apply plugin: 'kotlin-android-extensions'
 apply plugin: 'kotlin-kapt'
+
 androidExtensions {
-  experimental = true
+  features = ["parcelize"]
+}
+
+tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach {
+  kotlinOptions {
+    freeCompilerArgs += ["-Xuse-experimental=kotlinx.coroutines.ExperimentalCoroutinesApi"]
+  }
 }
 
 android {
@@ -34,6 +41,9 @@
       minifyEnabled false
     }
   }
+  viewBinding {
+    enabled = true
+  }
   lintOptions {
     ignore 'InvalidPackage'
     textReport true
@@ -62,60 +72,52 @@
   implementation project(':lottie')
   implementation 'androidx.multidex:multidex:2.0.1'
 
-  implementation "androidx.fragment:fragment:1.2.1"
-  implementation "androidx.appcompat:appcompat:1.1.0"
+  implementation "androidx.fragment:fragment-ktx:1.2.5"
+  implementation "androidx.appcompat:appcompat:1.2.0"
   implementation "androidx.recyclerview:recyclerview:1.1.0"
-  implementation "androidx.paging:paging-runtime-ktx:2.1.1"
-  implementation "androidx.paging:paging-rxjava2-ktx:2.1.1"
+  implementation "androidx.paging:paging-runtime:3.0.0-alpha5"
+  implementation "androidx.paging:paging-runtime-ktx:3.0.0-alpha06"
   implementation "androidx.cardview:cardview:1.0.0"
-  implementation 'androidx.core:core-ktx:1.2.0'
-  implementation 'androidx.constraintlayout:constraintlayout:2.0.0-beta4'
+  implementation 'androidx.core:core-ktx:1.3.1'
+  implementation 'androidx.constraintlayout:constraintlayout:2.0.1'
   implementation "androidx.browser:browser:1.2.0"
   implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
   kapt "androidx.lifecycle:lifecycle-common-java8:2.2.0"
-  implementation "com.google.android.material:material:1.1.0"
+  implementation "com.google.android.material:material:1.2.1"
 
-  implementation 'com.airbnb.android:epoxy:3.9.0'
-  implementation 'com.airbnb.android:epoxy-paging:3.9.0'
-  kapt 'com.airbnb.android:epoxy-processor:3.9.0'
-  implementation 'com.airbnb.android:mvrx:1.3.0'
+  implementation 'com.airbnb.android:epoxy:4.0.0-beta6'
+  kapt 'com.airbnb.android:epoxy-processor:4.0.0-beta6'
+  implementation 'com.airbnb.android:mvrx:1.5.1'
 
   implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion"
   implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlinVersion"
   implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlinVersion"
-  implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.3'
-  implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.3'
+  implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.9'
+  implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9'
   implementation 'com.matthew-tamlin:sliding-intro-screen:3.2.0'
   implementation 'com.dlazaro66.qrcodereaderview:qrcodereaderview:2.0.2'
   implementation 'com.github.PhilJay:MPAndroidChart:v3.0.2'
-  implementation 'com.amazonaws:aws-android-sdk-s3:2.7.+'
-  implementation ('com.amazonaws:aws-android-sdk-mobile-client:2.7.+@aar') { transitive = true }
-  implementation ('com.amazonaws:aws-android-sdk-auth-userpools:2.7.+@aar') { transitive = true }
-  implementation 'com.google.code.gson:gson:2.8.2'
-  implementation 'com.squareup.okhttp3:okhttp:3.11.0'
-  implementation 'com.squareup.retrofit2:retrofit:2.4.0'
-  implementation 'com.squareup.retrofit2:adapter-rxjava2:2.3.0'
-  implementation 'com.squareup.retrofit2:converter-gson:2.4.0'
-  implementation 'io.reactivex.rxjava2:rxandroid:2.1.0'
-  implementation 'io.reactivex.rxjava2:rxjava:2.2.1'
+  implementation 'com.amazonaws:aws-android-sdk-s3:2.8.3'
+  implementation ('com.amazonaws:aws-android-sdk-mobile-client:2.8.3@aar') { transitive = true }
+  implementation ('com.amazonaws:aws-android-sdk-auth-userpools:2.8.3@aar') { transitive = true }
+  implementation 'com.google.code.gson:gson:2.8.6'
+  implementation 'com.squareup.okhttp3:okhttp:4.8.1'
+  implementation 'com.squareup.retrofit2:retrofit:2.9.0'
+  implementation 'com.squareup.retrofit2:adapter-rxjava2:2.9.0'
+  implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
   implementation 'com.github.bumptech.glide:glide:4.8.0'
 
-  debugImplementation("androidx.fragment:fragment-testing:1.2.0-rc04")
+  debugImplementation("androidx.fragment:fragment-testing:1.3.0-alpha08")
 
-  testImplementation 'junit:junit:4.12'
+  testImplementation 'junit:junit:4.13'
 
-  androidTestImplementation 'androidx.test:core:1.2.0'
-  androidTestImplementation 'androidx.test.ext:junit:1.1.0'
-  androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1'
-  androidTestImplementation 'androidx.test.espresso:espresso-idling-resource:3.1.0-alpha4'
-  androidTestImplementation "androidx.fragment:fragment-testing:1.2.0-rc04"
-  androidTestImplementation 'androidx.test:rules:1.2.0'
-  androidTestImplementation 'io.reactivex.rxjava2:rxjava:2.2.1'
-  androidTestImplementation 'io.reactivex.rxjava2:rxandroid:2.1.0'
-  androidTestImplementation 'com.squareup.okhttp3:okhttp:3.11.0'
+  androidTestImplementation 'androidx.test:core:1.3.0-rc01'
+  androidTestImplementation 'androidx.test.ext:junit:1.1.2'
+  androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
+  androidTestImplementation 'androidx.test.espresso:espresso-idling-resource:3.3.0'
+  androidTestImplementation 'androidx.test:rules:1.3.0'
+  androidTestImplementation 'com.squareup.okhttp3:okhttp:4.8.1'
   androidTestImplementation 'io.jsonwebtoken:jjwt:0.9.0'
-  androidTestImplementation 'com.amazonaws:aws-android-sdk-core:2.6.31'
-  androidTestImplementation 'com.amazonaws:aws-android-sdk-s3:2.6.31'
   androidTestImplementation "org.mockito:mockito-android:2.28.2"
-  androidTestImplementation "com.nhaarman.mockitokotlin2:mockito-kotlin:2.1.0"
+  androidTestImplementation "com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0"
 }
diff --git a/LottieSample/src/androidTest/java/com/airbnb/lottie/samples/HappoSnapshotter.kt b/LottieSample/src/androidTest/java/com/airbnb/lottie/samples/HappoSnapshotter.kt
index fed3160..4d3d279 100644
--- a/LottieSample/src/androidTest/java/com/airbnb/lottie/samples/HappoSnapshotter.kt
+++ b/LottieSample/src/androidTest/java/com/airbnb/lottie/samples/HappoSnapshotter.kt
@@ -4,7 +4,6 @@
 import android.graphics.Bitmap
 import android.os.Build
 import android.util.Log
-import com.airbnb.lottie.BuildConfig
 import com.airbnb.lottie.L
 import com.amazonaws.auth.BasicAWSCredentials
 import com.amazonaws.mobileconnectors.s3.transferutility.TransferObserver
@@ -14,20 +13,10 @@
 import com.google.gson.JsonArray
 import com.google.gson.JsonElement
 import com.google.gson.JsonObject
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.Job
-import kotlinx.coroutines.async
-import kotlinx.coroutines.delay
-import kotlinx.coroutines.withContext
-import okhttp3.Call
-import okhttp3.Callback
-import okhttp3.Credentials
-import okhttp3.MediaType
-import okhttp3.OkHttpClient
-import okhttp3.Request
-import okhttp3.RequestBody
-import okhttp3.Response
+import kotlinx.coroutines.*
+import okhttp3.*
+import okhttp3.MediaType.Companion.toMediaType
+import okhttp3.RequestBody.Companion.toRequestBody
 import java.io.File
 import java.io.FileOutputStream
 import java.io.IOException
@@ -61,7 +50,7 @@
     private val happoSecretKey = BC.HappoSecretKey
     private val gitBranch = URLEncoder.encode((if (BC.BITRISE_GIT_BRANCH == "null") BC.GIT_BRANCH else BC.BITRISE_GIT_BRANCH).replace("/", "_"), "UTF-8")
     private val androidVersion = "android${Build.VERSION.SDK_INT}"
-    private val reportNamePrefixes = listOf(BC.GIT_SHA, gitBranch, BuildConfig.VERSION_NAME).filter { it.isNotBlank() }
+    private val reportNamePrefixes = listOf(BC.GIT_SHA, gitBranch, BC.VERSION_NAME).filter { it.isNotBlank() }
     private val reportNames = reportNamePrefixes.map { "$it-$androidVersion" }
 
     private val okhttp = OkHttpClient()
@@ -104,7 +93,7 @@
     }
 
     private suspend fun upload(reportName: String, json: JsonElement) {
-        val body = RequestBody.create(MediaType.get("application/json"), json.toString())
+        val body = json.toString().toRequestBody("application/json".toMediaType())
         val request = Request.Builder()
                 .addHeader("Authorization", Credentials.basic(happoApiKey, happoSecretKey, Charset.forName("UTF-8")))
                 .url("https://happo.io/api/reports/$reportName")
@@ -115,7 +104,7 @@
         if (response.isSuccessful) {
             Log.d(TAG, "Uploaded $reportName to happo")
         } else {
-            throw IllegalStateException("Failed to upload $reportName to Happo. Failed with code ${response.code()}. " + response.body()?.string())
+            throw IllegalStateException("Failed to upload $reportName to Happo. Failed with code ${response.code}. " + response.body?.string())
         }
     }
 
diff --git a/LottieSample/src/androidTest/java/com/airbnb/lottie/samples/LottieTest.kt b/LottieSample/src/androidTest/java/com/airbnb/lottie/samples/LottieTest.kt
index 973b21e..30271c3 100644
--- a/LottieSample/src/androidTest/java/com/airbnb/lottie/samples/LottieTest.kt
+++ b/LottieSample/src/androidTest/java/com/airbnb/lottie/samples/LottieTest.kt
@@ -1,10 +1,10 @@
 package com.airbnb.lottie.samples
 
 import android.Manifest
+import android.content.Context
 import android.content.res.Configuration
 import android.content.res.Resources
 import android.graphics.*
-import android.os.Build.VERSION
 import android.util.DisplayMetrics
 import android.util.Log
 import android.view.View
@@ -12,6 +12,8 @@
 import android.widget.FrameLayout
 import android.widget.ImageView
 import androidx.core.view.updateLayoutParams
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.rules.ActivityScenarioRule
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.LargeTest
 import androidx.test.rule.ActivityTestRule
@@ -19,6 +21,7 @@
 import com.airbnb.lottie.*
 import com.airbnb.lottie.model.KeyPath
 import com.airbnb.lottie.model.LottieCompositionCache
+import com.airbnb.lottie.samples.testing.NoCacheLottieAnimationView
 import com.airbnb.lottie.samples.views.FilmStripView
 import com.airbnb.lottie.value.*
 import com.amazonaws.auth.BasicAWSCredentials
@@ -37,22 +40,18 @@
 import java.util.concurrent.TimeUnit
 import java.util.zip.ZipInputStream
 
-/**
- * Run these with: ./gradlew recordMode screenshotTests
- * If you run that command, it completes successfully, and nothing shows up in git, then you
- * haven't broken anything!
- */
 @ExperimentalCoroutinesApi
 @RunWith(AndroidJUnit4::class)
 @LargeTest
 class LottieTest {
 
+    @Suppress("DEPRECATION")
     @get:Rule
-    var snapshotActivityRule = ActivityTestRule(SnapshotTestActivity::class.java)
-    private val activity get() = snapshotActivityRule.activity
+    val snapshotActivityRule = ActivityScenarioRule(SnapshotTestActivity::class.java)
+    private val application get() = ApplicationProvider.getApplicationContext<Context>()
 
     @get:Rule
-    var permissionRule = GrantPermissionRule.grant(
+    val permissionRule = GrantPermissionRule.grant(
             Manifest.permission.WRITE_EXTERNAL_STORAGE,
             Manifest.permission.READ_EXTERNAL_STORAGE
     )
@@ -62,11 +61,11 @@
     private lateinit var snapshotter: HappoSnapshotter
 
     private val bitmapPool by lazy { BitmapPool() }
-    private val dummyBitmap by lazy { BitmapFactory.decodeResource(activity.resources, R.drawable.airbnb); }
+    private val dummyBitmap by lazy { BitmapFactory.decodeResource(application.resources, R.drawable.airbnb) }
 
-    private val filmStripViewPool = ObjectPool<FilmStripView> {
-        FilmStripView(activity).apply {
-            setImageAssetDelegate(ImageAssetDelegate { dummyBitmap })
+    private val filmStripViewPool = ObjectPool {
+        FilmStripView(application).apply {
+            setImageAssetDelegate { dummyBitmap }
             setFontAssetDelegate(object : FontAssetDelegate() {
                 override fun getFontPath(fontFamily: String?): String {
                     return "fonts/Roboto.ttf"
@@ -77,8 +76,8 @@
     }
     @Suppress("DEPRECATION")
     private val animationViewPool = ObjectPool<LottieAnimationView> {
-        val animationViewContainer = FrameLayout(activity)
-        NoCacheLottieAnimationView(activity).apply {
+        val animationViewContainer = FrameLayout(application)
+        NoCacheLottieAnimationView(application).apply {
             animationViewContainer.addView(this)
         }
     }
@@ -86,9 +85,9 @@
     @Before
     fun setup() {
         L.DBG = false
-        snapshotter = HappoSnapshotter(activity)
+        snapshotter = HappoSnapshotter(application)
         prodAnimationsTransferUtility = TransferUtility.builder()
-                .context(activity)
+                .context(application)
                 .s3Client(AmazonS3Client(BasicAWSCredentials(BuildConfig.S3AccessKey, BuildConfig.S3SecretKey)))
                 .defaultBucket("lottie-prod-animations")
                 .build()
@@ -96,7 +95,6 @@
     }
 
     @Test
-    @ObsoleteCoroutinesApi
     fun testAll() = runBlocking {
         withTimeout(TimeUnit.MINUTES.toMillis(45)) {
             snapshotFailure()
@@ -128,7 +126,7 @@
             capacity = 10
     ) {
         for (animation in animations) {
-            val file = File(activity.cacheDir, animation.key)
+            val file = File(application.cacheDir, animation.key)
             file.deleteOnExit()
             prodAnimationsTransferUtility.download(animation.key, file).await()
             send(file)
@@ -178,7 +176,9 @@
         filmStripViewPool.release(filmStripView)
         LottieCompositionCache.getInstance().clear()
         snapshotter.record(bitmap, name, variant)
-        activity.recordSnapshot(name, variant)
+        snapshotActivityRule.scenario.onActivity { activity ->
+            activity.recordSnapshot(name, variant)
+        }
         bitmapPool.release(bitmap)
     }
 
@@ -190,7 +190,7 @@
     }
 
     private fun listAssets(assets: MutableList<String> = mutableListOf(), pathPrefix: String = ""): List<String> {
-        activity.getAssets().list(pathPrefix)?.forEach { animation ->
+        application.assets.list(pathPrefix)?.forEach { animation ->
             val pathWithPrefix = if (pathPrefix.isEmpty()) animation else "$pathPrefix/$animation"
             if (!animation.contains('.')) {
                 listAssets(assets, pathWithPrefix)
@@ -209,7 +209,7 @@
     ) {
         for (asset in assets) {
             log("Parsing $asset")
-            val composition = LottieCompositionFactory.fromAssetSync(activity, asset).value
+            val composition = LottieCompositionFactory.fromAssetSync(application, asset).value
                     ?: throw java.lang.IllegalArgumentException("Unable to parse $asset.")
             send(asset to composition)
         }
@@ -225,8 +225,11 @@
         animationView.layoutParams = FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)
         animationView.scale = 1f
         animationView.scaleType = ImageView.ScaleType.FIT_CENTER
-        val widthSpec = View.MeasureSpec.makeMeasureSpec(activity.resources.displayMetrics.widthPixels, View.MeasureSpec.EXACTLY)
-        val heightSpec = View.MeasureSpec.makeMeasureSpec(activity.resources.displayMetrics.heightPixels, View.MeasureSpec.EXACTLY)
+        val widthSpec = View.MeasureSpec.makeMeasureSpec(application.resources.displayMetrics
+                .widthPixels,
+                View.MeasureSpec.EXACTLY)
+        val heightSpec = View.MeasureSpec.makeMeasureSpec(application.resources.displayMetrics
+                .heightPixels, View.MeasureSpec.EXACTLY)
         val animationViewContainer = animationView.parent as ViewGroup
         animationViewContainer.measure(widthSpec, heightSpec)
         animationViewContainer.layout(0, 0, animationViewContainer.measuredWidth, animationViewContainer.measuredHeight)
@@ -237,7 +240,9 @@
         val snapshotName = "Failure"
         val snapshotVariant = "Default"
         snapshotter.record(bitmap, snapshotName, snapshotVariant)
-        activity.recordSnapshot(snapshotName, snapshotVariant)
+        snapshotActivityRule.scenario.onActivity { activity ->
+            activity.recordSnapshot(snapshotName, snapshotVariant)
+        }
         bitmapPool.release(bitmap)
     }
 
@@ -922,37 +927,41 @@
     }
 
     private suspend fun testNightMode() {
-        var newConfig = Configuration(activity.getResources().getConfiguration())
-		newConfig.uiMode = newConfig.uiMode and Configuration.UI_MODE_NIGHT_MASK.inv();
-		newConfig.uiMode = newConfig.uiMode or Configuration.UI_MODE_NIGHT_NO;
-		val dayContext = activity.createConfigurationContext(newConfig)
+        var newConfig = Configuration(application.resources.configuration)
+		newConfig.uiMode = newConfig.uiMode and Configuration.UI_MODE_NIGHT_MASK.inv()
+        newConfig.uiMode = newConfig.uiMode or Configuration.UI_MODE_NIGHT_NO
+		val dayContext = application.createConfigurationContext(newConfig)
         var result = LottieCompositionFactory.fromRawResSync(dayContext, R.raw.day_night)
         var composition = result.value!!
         var drawable = LottieDrawable()
-        drawable.setComposition(composition)
+        drawable.composition = composition
         var bitmap = bitmapPool.acquire(drawable.intrinsicWidth, drawable.intrinsicHeight)
         var canvas = Canvas(bitmap)
         log("Drawing day_night day")
         drawable.draw(canvas)
         snapshotter.record(bitmap, "Day/Night", "Day")
-        activity.recordSnapshot("Day/Night", "Day")
+        snapshotActivityRule.scenario.onActivity { activity ->
+            activity.recordSnapshot("Day/Night", "Day")
+        }
         LottieCompositionCache.getInstance().clear()
         bitmapPool.release(bitmap)
 
-        newConfig = Configuration(activity.getResources().getConfiguration())
-        newConfig.uiMode = newConfig.uiMode and Configuration.UI_MODE_NIGHT_MASK.inv();
-        newConfig.uiMode = newConfig.uiMode or Configuration.UI_MODE_NIGHT_YES;
-        val nightContext = activity.createConfigurationContext(newConfig)
+        newConfig = Configuration(application.resources.configuration)
+        newConfig.uiMode = newConfig.uiMode and Configuration.UI_MODE_NIGHT_MASK.inv()
+        newConfig.uiMode = newConfig.uiMode or Configuration.UI_MODE_NIGHT_YES
+        val nightContext = application.createConfigurationContext(newConfig)
         result = LottieCompositionFactory.fromRawResSync(nightContext, R.raw.day_night)
         composition = result.value!!
         drawable = LottieDrawable()
-        drawable.setComposition(composition)
+        drawable.composition = composition
         bitmap = bitmapPool.acquire(drawable.intrinsicWidth, drawable.intrinsicHeight)
         canvas = Canvas(bitmap)
         log("Drawing day_night day")
         drawable.draw(canvas)
         snapshotter.record(bitmap, "Day/Night", "Night")
-        activity.recordSnapshot("Day/Night", "Night")
+        snapshotActivityRule.scenario.onActivity { activity ->
+            activity.recordSnapshot("Day/Night", "Night")
+        }
         LottieCompositionCache.getInstance().clear()
         bitmapPool.release(bitmap)
     }
@@ -975,18 +984,20 @@
     }
 
     private suspend fun withDrawable(assetName: String, snapshotName: String, snapshotVariant: String, callback: (LottieDrawable) -> Unit) {
-        val result = LottieCompositionFactory.fromAssetSync(activity, assetName)
+        val result = LottieCompositionFactory.fromAssetSync(application, assetName)
         val composition = result.value
                 ?: throw IllegalArgumentException("Unable to parse $assetName.", result.exception)
         val drawable = LottieDrawable()
-        drawable.setComposition(composition)
+        drawable.composition = composition
         callback(drawable)
         val bitmap = bitmapPool.acquire(drawable.intrinsicWidth, drawable.intrinsicHeight)
         val canvas = Canvas(bitmap)
         log("Drawing $assetName")
         drawable.draw(canvas)
         snapshotter.record(bitmap, snapshotName, snapshotVariant)
-        activity.recordSnapshot(snapshotName, snapshotVariant)
+        snapshotActivityRule.scenario.onActivity { activity ->
+            activity.recordSnapshot(snapshotName, snapshotVariant)
+        }
         LottieCompositionCache.getInstance().clear()
         bitmapPool.release(bitmap)
     }
@@ -997,7 +1008,7 @@
             snapshotVariant: String = "default",
             callback: (LottieAnimationView) -> Unit
     ) {
-        val result = LottieCompositionFactory.fromAssetSync(activity, assetName)
+        val result = LottieCompositionFactory.fromAssetSync(application, assetName)
         val composition = result.value
                 ?: throw IllegalArgumentException("Unable to parse $assetName.", result.exception)
         val animationView = animationViewPool.acquire()
@@ -1006,8 +1017,11 @@
         animationView.scale = 1f
         animationView.scaleType = ImageView.ScaleType.FIT_CENTER
         callback(animationView)
-        val widthSpec = View.MeasureSpec.makeMeasureSpec(activity.resources.displayMetrics.widthPixels, View.MeasureSpec.EXACTLY)
-        val heightSpec = View.MeasureSpec.makeMeasureSpec(activity.resources.displayMetrics.heightPixels, View.MeasureSpec.EXACTLY)
+        val widthSpec = View.MeasureSpec.makeMeasureSpec(application.resources.displayMetrics
+                .widthPixels,
+                View.MeasureSpec.EXACTLY)
+        val heightSpec = View.MeasureSpec.makeMeasureSpec(application.resources.displayMetrics
+                .heightPixels, View.MeasureSpec.EXACTLY)
         val animationViewContainer = animationView.parent as ViewGroup
         animationViewContainer.measure(widthSpec, heightSpec)
         animationViewContainer.layout(0, 0, animationViewContainer.measuredWidth, animationViewContainer.measuredHeight)
@@ -1017,7 +1031,9 @@
         animationView.draw(canvas)
         animationViewPool.release(animationView)
         snapshotter.record(bitmap, snapshotName, snapshotVariant)
-        activity.recordSnapshot(snapshotName, snapshotVariant)
+        snapshotActivityRule.scenario.onActivity { activity ->
+            activity.recordSnapshot(snapshotName, snapshotVariant)
+        }
         bitmapPool.release(bitmap)
     }
 
@@ -1027,7 +1043,7 @@
             snapshotVariant: String = "default",
             callback: (FilmStripView) -> Unit
     ) {
-        val result = LottieCompositionFactory.fromAssetSync(activity, assetName)
+        val result = LottieCompositionFactory.fromAssetSync(application, assetName)
         val composition = result.value
                 ?: throw IllegalArgumentException("Unable to parse $assetName.", result.exception)
         snapshotComposition(snapshotName, snapshotVariant, composition, callback)
diff --git a/LottieSample/src/main/AndroidManifest.xml b/LottieSample/src/main/AndroidManifest.xml
index 74388c1..34964be 100644
--- a/LottieSample/src/main/AndroidManifest.xml
+++ b/LottieSample/src/main/AndroidManifest.xml
@@ -41,10 +41,6 @@
         <activity
             android:name=".QRScanActivity"
             android:screenOrientation="portrait" />
-        <activity android:name=".FullScreenActivity" />
-        <activity
-            android:name=".TestColorFilterActivity"
-            android:screenOrientation="portrait" />
         <activity android:name=".DynamicActivity" />
         <activity
             android:name=".PlayerActivity"
@@ -63,9 +59,9 @@
             </intent-filter>
         </activity>
         <activity android:name=".DynamicTextActivity" />
-        <activity android:name=".ListActivity" />
+        <activity android:name=".WishListActivity" />
         <activity
-            android:name=".FilmStripSnapshotActivity"
+            android:name=".testing.FilmStripSnapshotActivity"
             android:exported="true" />
 
         <activity
diff --git a/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/AppIntroActivity.kt b/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/AppIntroActivity.kt
index cfcdba7..511249d 100644
--- a/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/AppIntroActivity.kt
+++ b/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/AppIntroActivity.kt
@@ -1,36 +1,26 @@
 package com.airbnb.lottie.samples
 
 import android.os.Bundle
-import androidx.fragment.app.Fragment
-import android.view.LayoutInflater
-import android.view.View
-import android.view.ViewGroup
 import android.view.animation.Interpolator
 import android.widget.Scroller
+import androidx.fragment.app.Fragment
 import com.airbnb.lottie.LottieAnimationView
+import com.airbnb.lottie.samples.utils.inflate
+import com.airbnb.lottie.samples.utils.lerp
 import com.matthewtamlin.sliding_intro_screen_library.buttons.IntroButton
 import com.matthewtamlin.sliding_intro_screen_library.core.IntroActivity
 import com.matthewtamlin.sliding_intro_screen_library.core.LockableViewPager
 
 
 class AppIntroActivity : IntroActivity() {
-    private val ANIMATION_TIMES = floatArrayOf(0f, 0.3333f, 0.6666f, 1f, 1f)
-
     private val animationView: LottieAnimationView by lazy {
         rootView.inflate(R.layout.app_intro_animation_view, false) as LottieAnimationView
     }
     private val viewPager: LockableViewPager by lazy {
-        findViewById<LockableViewPager>(R.id.intro_activity_viewPager)
+        findViewById(R.id.intro_activity_viewPager)
     }
 
-    override fun generatePages(savedInstanceState: Bundle?): Collection<Fragment> {
-        return listOf(
-                EmptyFragment.newInstance(),
-                EmptyFragment.newInstance(),
-                EmptyFragment.newInstance(),
-                EmptyFragment.newInstance()
-        )
-    }
+    override fun generatePages(savedInstanceState: Bundle?) = listOf(EmptyFragment(), EmptyFragment(), EmptyFragment(), EmptyFragment())
 
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
@@ -59,18 +49,7 @@
         animationView.progress = startProgress.lerp(endProgress, positionOffset)
     }
 
-    class EmptyFragment : androidx.fragment.app.Fragment() {
-
-        override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
-            return container!!.inflate(R.layout.fragment_empty, false)
-        }
-
-        companion object {
-            internal fun newInstance(): EmptyFragment {
-                return EmptyFragment()
-            }
-        }
-    }
+    class EmptyFragment : Fragment(R.layout.empty_fragment)
 
     private fun setViewPagerScroller() {
         try {
@@ -91,4 +70,8 @@
             // Do nothing.
         }
     }
+
+    companion object {
+        private val ANIMATION_TIMES = floatArrayOf(0f, 0.3333f, 0.6666f, 1f, 1f)
+    }
 }
diff --git a/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/BaseEpoxyFragment.kt b/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/BaseEpoxyFragment.kt
deleted file mode 100644
index 1c18749..0000000
--- a/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/BaseEpoxyFragment.kt
+++ /dev/null
@@ -1,35 +0,0 @@
-package com.airbnb.lottie.samples
-
-import android.os.Bundle
-import android.view.LayoutInflater
-import android.view.View
-import android.view.ViewGroup
-import com.airbnb.epoxy.AsyncEpoxyController
-import com.airbnb.epoxy.EpoxyController
-import com.airbnb.lottie.samples.R.id.recyclerView
-import com.airbnb.mvrx.BaseMvRxFragment
-import kotlinx.android.synthetic.main.fragment_base.*
-import kotlinx.android.synthetic.main.fragment_base.view.*
-
-
-private class BaseEpoxyController(
-        private val buildModelsCallback: EpoxyController.() -> Unit
-) : AsyncEpoxyController() {
-    override fun buildModels() {
-        buildModelsCallback()
-    }
-}
-
-abstract class BaseEpoxyFragment : BaseMvRxFragment() {
-
-    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? =
-            inflater.inflate(R.layout.fragment_base, container, false).apply {
-                recyclerView.setController(BaseEpoxyController { buildModels() })
-            }
-
-    override fun invalidate() {
-        recyclerView.requestModelBuild()
-    }
-
-    abstract fun EpoxyController.buildModels()
-}
\ No newline at end of file
diff --git a/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/BullseyeActivity.kt b/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/BullseyeActivity.kt
index 1ad64ed..b418c7c 100644
--- a/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/BullseyeActivity.kt
+++ b/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/BullseyeActivity.kt
@@ -2,34 +2,35 @@
 
 import android.graphics.PointF
 import android.os.Bundle
-import androidx.customview.widget.ViewDragHelper
-import androidx.appcompat.app.AppCompatActivity
 import android.view.View
+import androidx.appcompat.app.AppCompatActivity
+import androidx.customview.widget.ViewDragHelper
 import com.airbnb.lottie.LottieProperty
 import com.airbnb.lottie.model.KeyPath
+import com.airbnb.lottie.samples.databinding.BullseyeActivityBinding
+import com.airbnb.lottie.samples.utils.viewBinding
 import com.airbnb.lottie.value.LottieRelativePointValueCallback
-import kotlinx.android.synthetic.main.activity_bullseye.*
 
 class BullseyeActivity : AppCompatActivity() {
+    private val binding: BullseyeActivityBinding by viewBinding()
 
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
-        setContentView(R.layout.activity_bullseye)
 
         val largeValueCallback = LottieRelativePointValueCallback(PointF(0f, 0f))
-        animationView.addValueCallback(KeyPath("First"), LottieProperty.TRANSFORM_POSITION, largeValueCallback)
+        binding.animationView.addValueCallback(KeyPath("First"), LottieProperty.TRANSFORM_POSITION, largeValueCallback)
 
         val mediumValueCallback = LottieRelativePointValueCallback(PointF(0f, 0f))
-        animationView.addValueCallback(KeyPath("Fourth"), LottieProperty.TRANSFORM_POSITION, mediumValueCallback)
+        binding.animationView.addValueCallback(KeyPath("Fourth"), LottieProperty.TRANSFORM_POSITION, mediumValueCallback)
 
         val smallValueCallback = LottieRelativePointValueCallback(PointF(0f, 0f))
-        animationView.addValueCallback(KeyPath("Seventh"), LottieProperty.TRANSFORM_POSITION, smallValueCallback)
+        binding.animationView.addValueCallback(KeyPath("Seventh"), LottieProperty.TRANSFORM_POSITION, smallValueCallback)
 
         var totalDx = 0f
         var totalDy = 0f
 
-        val viewDragHelper = ViewDragHelper.create(containerView, object : ViewDragHelper.Callback() {
-            override fun tryCaptureView(child: View, pointerId: Int) = child == targetView
+        val viewDragHelper = ViewDragHelper.create(binding.containerView, object : ViewDragHelper.Callback() {
+            override fun tryCaptureView(child: View, pointerId: Int) = child == binding.targetView
 
             override fun clampViewPositionVertical(child: View, top: Int, dy: Int): Int {
                 return top
@@ -48,7 +49,7 @@
             }
         })
 
-        containerView.viewDragHelper = viewDragHelper
+        binding.containerView.viewDragHelper = viewDragHelper
     }
 
     private fun getPoint(dx: Float, dy: Float, factor: Float) = PointF(dx * factor, dy * factor)
diff --git a/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/DynamicActivity.kt b/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/DynamicActivity.kt
index 8970d49..495803e 100644
--- a/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/DynamicActivity.kt
+++ b/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/DynamicActivity.kt
@@ -1,13 +1,15 @@
 package com.airbnb.lottie.samples
 
+import android.annotation.SuppressLint
 import android.graphics.PointF
 import android.os.Bundle
-import androidx.appcompat.app.AppCompatActivity
 import android.util.Log
+import androidx.appcompat.app.AppCompatActivity
 import com.airbnb.lottie.LottieProperty
 import com.airbnb.lottie.model.KeyPath
+import com.airbnb.lottie.samples.databinding.DynamicActivityBinding
+import com.airbnb.lottie.samples.utils.viewBinding
 import com.airbnb.lottie.utils.MiscUtils
-import kotlinx.android.synthetic.main.activity_dynamic.*
 
 private val COLORS = arrayOf(
     0xff5a5f,
@@ -17,41 +19,38 @@
 private val EXTRA_JUMP = arrayOf(0f, 20f, 50f)
 
 class DynamicActivity : AppCompatActivity() {
+    private val binding: DynamicActivityBinding by viewBinding()
+
     private var speed = 1
     private var colorIndex = 0
     private var extraJumpIndex = 0
 
-    companion object {
-        val TAG = DynamicActivity::class.simpleName
-    }
-
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
-        setContentView(R.layout.activity_dynamic)
 
-        speedButton.setOnClickListener {
+        binding.speedButton.setOnClickListener {
             speed = ++speed % 4
             updateButtonText()
         }
 
-        colorButton.setOnClickListener {
+        binding.colorButton.setOnClickListener {
             colorIndex = (colorIndex + 1) % COLORS.size
             updateButtonText()
         }
 
-        jumpHeight.setOnClickListener {
+        binding.jumpHeight.setOnClickListener {
             extraJumpIndex = (extraJumpIndex + 1) % EXTRA_JUMP.size
             updateButtonText()
             setupValueCallbacks()
         }
 
-        animationView.addLottieOnCompositionLoadedListener { _ ->
-            animationView.resolveKeyPath(KeyPath("**")).forEach {
+        binding.animationView.addLottieOnCompositionLoadedListener { _ ->
+            binding.animationView.resolveKeyPath(KeyPath("**")).forEach {
                 Log.d(TAG, it.keysToString())
                 setupValueCallbacks()
             }
         }
-        animationView.setFailureListener { e ->
+        binding.animationView.setFailureListener { e ->
             Log.e(TAG, "Failed to load animation!", e)
         }
 
@@ -59,7 +58,7 @@
     }
 
     private fun setupValueCallbacks() {
-        animationView.addValueCallback(KeyPath("LeftArmWave"), LottieProperty.TIME_REMAP) { frameInfo ->
+        binding.animationView.addValueCallback(KeyPath("LeftArmWave"), LottieProperty.TIME_REMAP) { frameInfo ->
             2 * speed.toFloat() * frameInfo.overallProgress
         }
 
@@ -67,11 +66,11 @@
         val leftArm = KeyPath("LeftArmWave", "LeftArm", "Group 6", "Fill 1")
         val rightArm = KeyPath("RightArm", "Group 6", "Fill 1")
 
-        animationView.addValueCallback(shirt, LottieProperty.COLOR) { COLORS[colorIndex] }
-        animationView.addValueCallback(leftArm, LottieProperty.COLOR) { COLORS[colorIndex] }
-        animationView.addValueCallback(rightArm, LottieProperty.COLOR) { COLORS[colorIndex] }
+        binding.animationView.addValueCallback(shirt, LottieProperty.COLOR) { COLORS[colorIndex] }
+        binding.animationView.addValueCallback(leftArm, LottieProperty.COLOR) { COLORS[colorIndex] }
+        binding.animationView.addValueCallback(rightArm, LottieProperty.COLOR) { COLORS[colorIndex] }
         val point = PointF()
-        animationView.addValueCallback(KeyPath("Body"),
+        binding.animationView.addValueCallback(KeyPath("Body"),
             LottieProperty.TRANSFORM_POSITION) { frameInfo ->
             val startX = frameInfo.startValue.x
             var startY = frameInfo.startValue.y
@@ -87,8 +86,13 @@
         }
     }
 
+    @SuppressLint("SetTextI18n")
     private fun updateButtonText() {
-        speedButton.text = "Wave: ${speed}x Speed"
-        jumpHeight.text = "Extra jump height ${EXTRA_JUMP[extraJumpIndex]}"
+        binding.speedButton.text = "Wave: ${speed}x Speed"
+        binding.jumpHeight.text = "Extra jump height ${EXTRA_JUMP[extraJumpIndex]}"
+    }
+
+    companion object {
+        val TAG = DynamicActivity::class.simpleName
     }
 }
diff --git a/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/DynamicTextActivity.kt b/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/DynamicTextActivity.kt
index 71dd6b2..ae0ad19 100644
--- a/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/DynamicTextActivity.kt
+++ b/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/DynamicTextActivity.kt
@@ -1,20 +1,21 @@
 package com.airbnb.lottie.samples
 
 import android.os.Bundle
-import androidx.appcompat.app.AppCompatActivity
 import android.text.Editable
 import android.text.TextWatcher
+import androidx.appcompat.app.AppCompatActivity
 import com.airbnb.lottie.TextDelegate
-import kotlinx.android.synthetic.main.activity_dynamic_text.*
+import com.airbnb.lottie.samples.databinding.DynamicTextActivityBinding
+import com.airbnb.lottie.samples.utils.viewBinding
 
 class DynamicTextActivity : AppCompatActivity() {
+    private val binding: DynamicTextActivityBinding by viewBinding()
 
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
-        setContentView(R.layout.activity_dynamic_text)
 
-        val textDelegate = TextDelegate(dynamicTextView)
-        nameEditText.addTextChangedListener(object: TextWatcher {
+        val textDelegate = TextDelegate(binding.dynamicTextView)
+        binding.nameEditText.addTextChangedListener(object: TextWatcher {
             override fun afterTextChanged(s: Editable?) {
                 textDelegate.setText("NAME", s.toString())
             }
@@ -23,6 +24,6 @@
 
             override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
         })
-        dynamicTextView.setTextDelegate(textDelegate)
+        binding.dynamicTextView.setTextDelegate(textDelegate)
     }
 }
diff --git a/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/EmptyActivity.kt b/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/EmptyActivity.kt
index ec76d6b..f23d617 100644
--- a/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/EmptyActivity.kt
+++ b/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/EmptyActivity.kt
@@ -1,14 +1,15 @@
 package com.airbnb.lottie.samples
 
 import android.os.Bundle
-import android.view.View
 import androidx.appcompat.app.AppCompatActivity
+import com.airbnb.lottie.samples.databinding.EmptyActivityBinding
+import com.airbnb.lottie.samples.utils.viewBinding
 
 class EmptyActivity : AppCompatActivity() {
+    private val binding: EmptyActivityBinding by viewBinding()
 
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
-        setContentView(R.layout.activity_empty)
-        findViewById<View>(R.id.finish).setOnClickListener { finish() }
+        binding.finish.setOnClickListener { finish() }
     }
 }
diff --git a/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/FullScreenActivity.kt b/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/FullScreenActivity.kt
deleted file mode 100644
index 07014cb..0000000
--- a/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/FullScreenActivity.kt
+++ /dev/null
@@ -1,16 +0,0 @@
-package com.airbnb.lottie.samples
-
-import android.os.Bundle
-import androidx.appcompat.app.AppCompatActivity
-
-/**
- * To have a full screen animation, make an animation that is wider than the screen and set the
- * scaleType to centerCrop.
- */
-class FullScreenActivity : AppCompatActivity() {
-
-    public override fun onCreate(savedInstanceState: Bundle?) {
-        super.onCreate(savedInstanceState)
-        setContentView(R.layout.fragment_full_screen)
-    }
-}
diff --git a/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/ListActivity.kt b/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/ListActivity.kt
deleted file mode 100644
index b3fefe8..0000000
--- a/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/ListActivity.kt
+++ /dev/null
@@ -1,43 +0,0 @@
-package com.airbnb.lottie.samples
-
-import android.os.Bundle
-import androidx.appcompat.app.AppCompatActivity
-import com.airbnb.epoxy.EpoxyController
-import com.airbnb.epoxy.EpoxyRecyclerView
-import com.airbnb.lottie.samples.views.WishListIconView
-import com.airbnb.lottie.samples.views.listingCard
-import com.airbnb.lottie.samples.views.marquee
-import kotlinx.android.synthetic.main.activity_list.*
-
-class ListActivity : AppCompatActivity() {
-
-    override fun onCreate(savedInstanceState: Bundle?) {
-        super.onCreate(savedInstanceState)
-        setContentView(R.layout.activity_list)
-
-        setSupportActionBar(toolbar)
-        supportActionBar?.setDisplayShowTitleEnabled(false)
-        toolbar.setNavigationOnClickListener { finish() }
-
-        recyclerView.buildModelsWith(object : EpoxyRecyclerView.ModelBuilderCallback {
-            override fun buildModels(controller: EpoxyController) {
-                controller.buildModels()
-            }
-        })
-    }
-
-    private fun EpoxyController.buildModels() {
-        marquee {
-            id("marquee")
-            title("List")
-            subtitle("Loading the same animation many times in a list")
-        }
-
-        repeat(100) {
-            listingCard {
-                id(it)
-                clickListener { view -> (view as WishListIconView).toggleWishlisted() }
-            }
-        }
-    }
-}
\ No newline at end of file
diff --git a/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/LottieApplication.kt b/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/LottieApplication.kt
index 451d150..d4affd8 100644
--- a/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/LottieApplication.kt
+++ b/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/LottieApplication.kt
@@ -2,6 +2,7 @@
 
 import androidx.multidex.MultiDexApplication
 import com.airbnb.lottie.L
+import com.airbnb.lottie.samples.api.LottiefilesApi
 import com.google.gson.FieldNamingPolicy
 import com.google.gson.GsonBuilder
 import okhttp3.OkHttpClient
diff --git a/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/LottiefilesApi.kt b/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/LottiefilesApi.kt
deleted file mode 100644
index be114f6..0000000
--- a/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/LottiefilesApi.kt
+++ /dev/null
@@ -1,22 +0,0 @@
-package com.airbnb.lottie.samples
-
-import com.airbnb.lottie.samples.model.AnimationResponse
-import com.airbnb.lottie.samples.model.AnimationResponseV2
-import io.reactivex.Single
-import retrofit2.http.GET
-import retrofit2.http.Path
-import retrofit2.http.Query
-
-interface LottiefilesApi {
-    @GET("v1/recent")
-    fun getRecent(@Query("page") page: Int): Single<AnimationResponse>
-
-    @GET("v1/popular")
-    fun getPopular(@Query("page") page: Int): Single<AnimationResponse>
-
-    @GET("v2/featured")
-    fun getCollection(): Single<AnimationResponseV2>
-
-    @GET("v1/search/{query}")
-    fun search(@Path("query") query: String, @Query("page") page: Int): Single<AnimationResponse>
-}
\ No newline at end of file
diff --git a/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/LottiefilesFragment.kt b/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/LottiefilesFragment.kt
index 479e765..cfaa7b9 100644
--- a/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/LottiefilesFragment.kt
+++ b/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/LottiefilesFragment.kt
@@ -1,22 +1,29 @@
 package com.airbnb.lottie.samples
 
+import android.content.Context
 import android.os.Bundle
 import android.view.View
-import androidx.lifecycle.Observer
-import androidx.paging.DataSource
-import androidx.paging.LivePagedListBuilder
-import androidx.paging.PageKeyedDataSource
-import com.airbnb.epoxy.EpoxyModel
-import com.airbnb.epoxy.paging.PagedListEpoxyController
+import android.view.ViewGroup
+import androidx.core.view.isVisible
+import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.viewModelScope
+import androidx.paging.*
+import androidx.recyclerview.widget.DiffUtil
+import androidx.recyclerview.widget.RecyclerView
+import com.airbnb.lottie.samples.api.LottiefilesApi
+import com.airbnb.lottie.samples.databinding.LottiefilesFragmentBinding
 import com.airbnb.lottie.samples.model.AnimationData
+import com.airbnb.lottie.samples.model.AnimationResponse
 import com.airbnb.lottie.samples.model.CompositionArgs
-import com.airbnb.lottie.samples.views.AnimationItemViewModel_
-import com.airbnb.lottie.samples.views.lottiefilesTabBar
-import com.airbnb.lottie.samples.views.marquee
-import com.airbnb.lottie.samples.views.searchInputItemView
+import com.airbnb.lottie.samples.utils.MvRxViewModel
+import com.airbnb.lottie.samples.utils.hideKeyboard
+import com.airbnb.lottie.samples.utils.viewBinding
+import com.airbnb.lottie.samples.views.AnimationItemView
 import com.airbnb.mvrx.*
-import kotlinx.android.synthetic.main.fragment_epoxy_recycler_view.*
-import kotlin.properties.Delegates
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.launch
 
 data class LottiefilesState(
         val mode: LottiefilesMode = LottiefilesMode.Recent,
@@ -28,17 +35,16 @@
     private var mode = initialState.mode
     private var query = initialState.query
 
-    val pagedList = LivePagedListBuilder<Int, AnimationData>(object : DataSource.Factory<Int, AnimationData>() {
-        override fun create(): DataSource<Int, AnimationData> {
-            return LottiefilesDataSource(api, mode, query)
-        }
-    }, 16).build()
+    private var dataSource: LottiefilesDataSource? = null
+    val pager = Pager(PagingConfig(pageSize = 16)) {
+        LottiefilesDataSource(api, mode, query).also { dataSource = it }
+    }.flow.cachedIn(viewModelScope)
 
     init {
         selectSubscribe(LottiefilesState::mode, LottiefilesState::query) { mode, query ->
             this.mode = mode
             this.query = query
-            pagedList.value?.dataSource?.invalidate()
+            dataSource?.invalidate()
         }
     }
 
@@ -57,136 +63,90 @@
 class LottiefilesDataSource(
         private val api: LottiefilesApi,
         val mode: LottiefilesMode,
-        val query: String
-) : PageKeyedDataSource<Int, AnimationData>() {
-    override fun loadInitial(params: LoadInitialParams<Int>, callback: LoadInitialCallback<Int, AnimationData>) {
-        if (mode == LottiefilesMode.Search && query.isEmpty()) {
-            callback.onResult(emptyList(), null, null)
-            return
-        }
+        private val query: String
+) : PagingSource<Int, AnimationData>() {
 
-        try {
-            val response = when (mode) {
-                LottiefilesMode.Popular -> api.getPopular(1)
-                LottiefilesMode.Recent -> api.getRecent(1)
-                LottiefilesMode.Search -> api.search(query, 1)
-            }.blockingGet()
-
-            callback.onResult(
-                    response.data,
-                    0,
-                    response.total,
-                    null,
-                    2.takeIf {
-                        !response.nextPageUrl.isNullOrEmpty()
-                    }
-            )
-        } catch (e: Exception) {
-            callback.onError(e)
-        }
-    }
-
-    override fun loadAfter(params: LoadParams<Int>, callback: LoadCallback<Int, AnimationData>) {
-        loadPage(params.key, callback)
-    }
-
-    override fun loadBefore(params: LoadParams<Int>, callback: LoadCallback<Int, AnimationData>) {
-        // ignored, prepend will never happen.
-    }
-
-    private fun loadPage(page: Int, callback: LoadCallback<Int, AnimationData>) {
-        try {
+    override suspend fun load(params: LoadParams<Int>): LoadResult<Int, AnimationData> {
+        val page = params.key ?: 1
+        return try {
             val response = when (mode) {
                 LottiefilesMode.Popular -> api.getPopular(page)
                 LottiefilesMode.Recent -> api.getRecent(page)
-                LottiefilesMode.Search -> api.search(query, page)
-            }.blockingGet()
-
-            callback.onResult(
-                    response.data,
-                    (page + 1).takeIf {
-                        !response.nextPageUrl.isNullOrEmpty()
+                LottiefilesMode.Search -> {
+                    if (query.isBlank()) {
+                        AnimationResponse(page, emptyList(), "", page, null, "", 0, "", 0, 0)
+                    } else {
+                        api.search(query, page)
                     }
-            )
+                }
+            }
 
+            LoadResult.Page(
+                    response.data,
+                    if (page == 1) null else page + 1,
+                    (page + 1).takeIf { page < response.lastPage }
+            )
         } catch (e: Exception) {
-            callback.onError(e)
+            LoadResult.Error(e)
         }
     }
 }
 
-class LottiefilesFragment : BaseMvRxFragment(R.layout.fragment_epoxy_recycler_view) {
+class LottiefilesFragment : BaseMvRxFragment(R.layout.lottiefiles_fragment) {
+    private val binding: LottiefilesFragmentBinding by viewBinding()
     private val viewModel: LottiefilesViewModel by fragmentViewModel()
 
-    private val controller by lazy {
-        object : PagedListEpoxyController<AnimationData>() {
+    private object AnimationItemDataDiffCallback : DiffUtil.ItemCallback<AnimationData>() {
+        override fun areItemsTheSame(oldItem: AnimationData, newItem: AnimationData) = oldItem.id == newItem.id
 
-            var mode by Delegates.observable(LottiefilesMode.Recent) { _, _, _ -> requestModelBuild() }
+        override fun areContentsTheSame(oldItem: AnimationData, newItem: AnimationData) = oldItem == newItem
+    }
 
-            override fun buildItemModel(currentPosition: Int, item: AnimationData?): EpoxyModel<*> {
-                return if (item == null) {
-                    AnimationItemViewModel_().id(-currentPosition)
-                } else {
-                    AnimationItemViewModel_()
-                            .id(item.id)
-                            .previewUrl(item.preview)
-                            .title(item.title)
-                            .previewBackgroundColor(item.bgColorInt)
-                            .onClickListener { _ ->
-                                val intent = PlayerActivity.intent(requireContext(), CompositionArgs(animationData = item))
-                                requireContext().startActivity(intent)
-                            }
-
-                }
-            }
-
-            override fun addModels(models: List<EpoxyModel<*>>) {
-                marquee {
-                    id("lottiefiles")
-                    title(R.string.lottiefiles)
-                    subtitle(R.string.lottiefiles_airbnb)
-                }
-
-                lottiefilesTabBar {
-                    id("mode")
-                    mode(mode)
-                    recentClickListener { _ ->
-                        viewModel.setMode(LottiefilesMode.Recent)
-                        requireContext().hideKeyboard()
-                    }
-                    popularClickListener { _ ->
-                        viewModel.setMode(LottiefilesMode.Popular)
-                        requireContext().hideKeyboard()
-                    }
-                    searchClickListener { _ ->
-                        viewModel.setMode(LottiefilesMode.Search)
-                        requireContext().hideKeyboard()
-                    }
-                }
-
-                if (mode == LottiefilesMode.Search) {
-                    searchInputItemView {
-                        id("search")
-                        searchClickListener(viewModel::setQuery)
-                    }
-                }
-                super.addModels(models)
+    private class AnimationItemViewHolder(context: Context) : RecyclerView.ViewHolder(AnimationItemView(context)) {
+        fun bind(data: AnimationData?) {
+            val view = itemView as AnimationItemView
+            view.setTitle(data?.title)
+            view.setPreviewUrl(data?.preview)
+            view.setPreviewBackgroundColor(data?.bgColorInt)
+            view.setOnClickListener {
+                val intent = PlayerActivity.intent(view.context, CompositionArgs(animationData = data))
+                view.context.startActivity(intent)
             }
         }
     }
 
-    override fun onCreate(savedInstanceState: Bundle?) {
-        super.onCreate(savedInstanceState)
-        viewModel.pagedList.observe(this, Observer {
-            controller.submitList(it)
-        })
+
+    private val adapter = object : PagingDataAdapter<AnimationData, AnimationItemViewHolder>(AnimationItemDataDiffCallback) {
+        override fun onBindViewHolder(holder: AnimationItemViewHolder, position: Int) = holder.bind(getItem(position))
+
+        override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = AnimationItemViewHolder(parent.context)
+
     }
 
     override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
-        recyclerView.setController(controller)
+        binding.recyclerView.adapter = adapter
+        viewLifecycleOwner.lifecycleScope.launch {
+            viewModel.pager.collectLatest(adapter::submitData)
+        }
+        binding.tabBar.setRecentClickListener {
+            viewModel.setMode(LottiefilesMode.Recent)
+            requireContext().hideKeyboard()
+        }
+        binding.tabBar.setPopularClickListener {
+            viewModel.setMode(LottiefilesMode.Popular)
+            requireContext().hideKeyboard()
+        }
+        binding.tabBar.setSearchClickListener {
+            viewModel.setMode(LottiefilesMode.Search)
+            requireContext().hideKeyboard()
+        }
+        binding.searchView.query.onEach { query ->
+            viewModel.setQuery(query)
+        }.launchIn(viewLifecycleOwner.lifecycleScope)
     }
 
     override fun invalidate(): Unit = withState(viewModel) { state ->
-        controller.mode = state.mode
+        binding.searchView.isVisible = state.mode == LottiefilesMode.Search
+        binding.tabBar.setMode(state.mode)
     }
 }
\ No newline at end of file
diff --git a/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/Mode.kt b/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/LottiefilesMode.kt
similarity index 100%
rename from LottieSample/src/main/kotlin/com/airbnb/lottie/samples/Mode.kt
rename to LottieSample/src/main/kotlin/com/airbnb/lottie/samples/LottiefilesMode.kt
diff --git a/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/MainActivity.kt b/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/MainActivity.kt
index 2ae2d19..c36a34e 100644
--- a/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/MainActivity.kt
+++ b/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/MainActivity.kt
@@ -7,15 +7,17 @@
 import androidx.browser.customtabs.CustomTabsIntent
 import androidx.core.net.toUri
 import androidx.fragment.app.Fragment
+import com.airbnb.lottie.samples.databinding.MainActivityBinding
+import com.airbnb.lottie.samples.utils.viewBinding
 import com.google.android.material.bottomnavigation.BottomNavigationView
-import kotlinx.android.synthetic.main.activity_main.*
 
 class MainActivity : AppCompatActivity(), BottomNavigationView.OnNavigationItemSelectedListener {
+    private val binding: MainActivityBinding by viewBinding()
+
     override fun onCreate(savedInstanceState: Bundle?) {
         AppCompatDelegate.setCompatVectorFromResourcesEnabled(true)
         super.onCreate(savedInstanceState)
-        setContentView(R.layout.activity_main)
-        bottomNavigation.setOnNavigationItemSelectedListener(this)
+        binding.bottomNavigation.setOnNavigationItemSelectedListener(this)
 
         savedInstanceState ?: showFragment(ShowcaseFragment())
     }
diff --git a/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/MvRxViewModel.kt b/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/MvRxViewModel.kt
deleted file mode 100644
index 41ad0ea..0000000
--- a/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/MvRxViewModel.kt
+++ /dev/null
@@ -1,6 +0,0 @@
-package com.airbnb.lottie.samples
-
-import com.airbnb.mvrx.BaseMvRxViewModel
-import com.airbnb.mvrx.MvRxState
-
-open class MvRxViewModel<S : MvRxState>(initialState: S) : BaseMvRxViewModel<S>(initialState, BuildConfig.DEBUG)
\ No newline at end of file
diff --git a/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/OkHttpCallback.kt b/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/OkHttpCallback.kt
deleted file mode 100644
index 6bd1017..0000000
--- a/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/OkHttpCallback.kt
+++ /dev/null
@@ -1,15 +0,0 @@
-package com.airbnb.lottie.samples
-
-import okhttp3.Call
-import okhttp3.Callback
-import okhttp3.Response
-import java.io.IOException
-
-internal open class OkHttpCallback(
-        val onResponse: ((call: Call, response: Response) -> Unit)? = null,
-        val onFailure: ((call: Call, exception: IOException) -> Unit)? = null
-): Callback {
-    override fun onResponse(call: Call, response: Response) = onResponse?.invoke(call, response) ?: Unit
-    override fun onFailure(call: Call, response: IOException) = onFailure?.invoke(call, response) ?: Unit
-
-}
\ No newline at end of file
diff --git a/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/OnSeekBarChangeListenerAdapter.kt b/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/OnSeekBarChangeListenerAdapter.kt
index ac4dc0c..523461a 100644
--- a/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/OnSeekBarChangeListenerAdapter.kt
+++ b/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/OnSeekBarChangeListenerAdapter.kt
@@ -2,13 +2,13 @@
 
 import android.widget.SeekBar
 
-internal open class OnSeekBarChangeListenerAdapter(
-        val onProgressChanged: ((seekBar: SeekBar, progress: Int, fromUser: Boolean) -> Unit)? = null,
-        val onStartTrackingTouch: ((seekBar: SeekBar) -> Unit)? = null,
-        val onStopTrackingTouch: ((seekBar: SeekBar) -> Unit)? = null
-): SeekBar.OnSeekBarChangeListener {
+internal class OnSeekBarChangeListenerAdapter(
+        private val onProgressChanged: ((seekBar: SeekBar, progress: Int, fromUser: Boolean) -> Unit)? = null,
+        private val onStartTrackingTouch: ((seekBar: SeekBar) -> Unit)? = null,
+        private val onStopTrackingTouch: ((seekBar: SeekBar) -> Unit)? = null
+) : SeekBar.OnSeekBarChangeListener {
     override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) =
-        onProgressChanged?.invoke(seekBar, progress, fromUser) ?: Unit
+            onProgressChanged?.invoke(seekBar, progress, fromUser) ?: Unit
 
     override fun onStartTrackingTouch(seekBar: SeekBar) =
             onStartTrackingTouch?.invoke(seekBar) ?: Unit
diff --git a/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/PlayerActivity.kt b/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/PlayerActivity.kt
index 8b1cb7f..99be680 100644
--- a/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/PlayerActivity.kt
+++ b/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/PlayerActivity.kt
@@ -6,11 +6,10 @@
 import androidx.appcompat.app.AppCompatActivity
 import com.airbnb.lottie.samples.model.CompositionArgs
 
-class PlayerActivity : AppCompatActivity() {
+class PlayerActivity : AppCompatActivity(R.layout.player_activity) {
 
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
-        setContentView(R.layout.activity_player)
 
         if (savedInstanceState == null) {
             val args = intent.getParcelableExtra(PlayerFragment.EXTRA_ANIMATION_ARGS) ?:
diff --git a/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/PlayerFragment.kt b/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/PlayerFragment.kt
index 632bbac..0f92052 100644
--- a/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/PlayerFragment.kt
+++ b/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/PlayerFragment.kt
@@ -7,12 +7,14 @@
 import android.graphics.Typeface
 import android.os.Bundle
 import android.util.Log
-import android.view.*
+import android.view.Menu
+import android.view.MenuInflater
+import android.view.MenuItem
+import android.view.View
 import android.widget.EditText
 import androidx.appcompat.app.AppCompatActivity
 import androidx.core.content.ContextCompat
 import androidx.core.view.children
-import androidx.core.view.get
 import androidx.core.view.isVisible
 import androidx.fragment.app.Fragment
 import androidx.lifecycle.Lifecycle
@@ -22,8 +24,9 @@
 import com.airbnb.epoxy.EpoxyRecyclerView
 import com.airbnb.lottie.*
 import com.airbnb.lottie.model.KeyPath
+import com.airbnb.lottie.samples.databinding.PlayerFragmentBinding
 import com.airbnb.lottie.samples.model.CompositionArgs
-import com.airbnb.lottie.samples.views.BackgroundColorView
+import com.airbnb.lottie.samples.utils.viewBinding
 import com.airbnb.lottie.samples.views.BottomSheetItemView
 import com.airbnb.lottie.samples.views.BottomSheetItemViewModel_
 import com.airbnb.lottie.samples.views.ControlBarItemToggleView
@@ -37,34 +40,27 @@
 import com.github.mikephil.charting.data.LineDataSet
 import com.google.android.material.bottomsheet.BottomSheetBehavior
 import com.google.android.material.snackbar.Snackbar
-import kotlinx.android.synthetic.main.bottom_sheet_key_paths.*
-import kotlinx.android.synthetic.main.bottom_sheet_render_times.*
-import kotlinx.android.synthetic.main.bottom_sheet_warnings.*
-import kotlinx.android.synthetic.main.control_bar.*
-import kotlinx.android.synthetic.main.control_bar_background_color.*
-import kotlinx.android.synthetic.main.control_bar_player_controls.*
-import kotlinx.android.synthetic.main.control_bar_scale.*
-import kotlinx.android.synthetic.main.control_bar_speed.*
-import kotlinx.android.synthetic.main.control_bar_trim.*
-import kotlinx.android.synthetic.main.fragment_player.*
+import kotlin.math.abs
 import kotlin.math.min
 import kotlin.math.roundToInt
 
-class PlayerFragment : BaseMvRxFragment() {
+class PlayerFragment : BaseMvRxFragment(R.layout.player_fragment) {
+    private val binding: PlayerFragmentBinding by viewBinding()
+    private val viewModel: PlayerViewModel by fragmentViewModel()
 
     private val transition = AutoTransition().apply { duration = 175 }
     private val renderTimesBehavior by lazy {
-        BottomSheetBehavior.from(renderTimesBottomSheet).apply {
+        BottomSheetBehavior.from(binding.bottomSheetRenderTimes.root).apply {
             peekHeight = resources.getDimensionPixelSize(R.dimen.bottom_bar_peek_height)
         }
     }
     private val warningsBehavior by lazy {
-        BottomSheetBehavior.from(warningsBottomSheet).apply {
+        BottomSheetBehavior.from(binding.bottomSheetWarnings.root).apply {
             peekHeight = resources.getDimensionPixelSize(R.dimen.bottom_bar_peek_height)
         }
     }
     private val keyPathsBehavior by lazy {
-        BottomSheetBehavior.from(keyPathsBottomSheet).apply {
+        BottomSheetBehavior.from(binding.bottomSheetKeyPaths.root).apply {
             peekHeight = resources.getDimensionPixelSize(R.dimen.bottom_bar_peek_height)
         }
     }
@@ -81,36 +77,31 @@
     }
 
     private val animatorListener = AnimatorListenerAdapter(
-            onStart = { playButton.isActivated = true },
+            onStart = { binding.controlBarPlayerControls.playButton.isActivated = true },
             onEnd = {
-                playButton.isActivated = false
-                animationView.performanceTracker?.logRenderTimes()
+                binding.controlBarPlayerControls.playButton.isActivated = false
+                binding.animationView.performanceTracker?.logRenderTimes()
                 updateRenderTimesPerLayer()
             },
             onCancel = {
-                playButton.isActivated = false
+                binding.controlBarPlayerControls.playButton.isActivated = false
             },
             onRepeat = {
-                animationView.performanceTracker?.logRenderTimes()
+                binding.animationView.performanceTracker?.logRenderTimes()
                 updateRenderTimesPerLayer()
             }
     )
 
-    private val viewModel: PlayerViewModel by fragmentViewModel()
-
-    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View =
-            inflater.inflate(R.layout.fragment_player, container, false)
-
     @SuppressLint("SetTextI18n")
     override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
         super.onViewCreated(view, savedInstanceState)
-        (requireActivity() as AppCompatActivity).setSupportActionBar(toolbar)
+        (requireActivity() as AppCompatActivity).setSupportActionBar(binding.toolbar)
         (requireActivity() as AppCompatActivity).supportActionBar?.setDisplayShowTitleEnabled(false)
         setHasOptionsMenu(true)
 
-        lottieVersionView.text = getString(R.string.lottie_version, BuildConfig.VERSION_NAME)
+        binding.controlBarPlayerControls.lottieVersionView.text = getString(R.string.lottie_version, BuildConfig.VERSION_NAME)
 
-        animationView.setFontAssetDelegate(object : FontAssetDelegate() {
+        binding.animationView.setFontAssetDelegate(object : FontAssetDelegate() {
             override fun fetchFont(fontFamily: String?): Typeface {
                 return Typeface.DEFAULT
             }
@@ -119,118 +110,118 @@
         val args = arguments?.getParcelable<CompositionArgs>(EXTRA_ANIMATION_ARGS)
                 ?: throw IllegalArgumentException("No composition args specified")
         args.animationData?.bgColorInt?.let {
-            backgroundButton1.setBackgroundColor(it)
-            animationContainer.setBackgroundColor(it)
+            binding.controlBarBackgroundColor.backgroundButton1.setBackgroundColor(it)
+            binding.animationContainer.setBackgroundColor(it)
             invertColor(it)
         }
 
         args.animationDataV2?.bgColorInt?.let {
-            backgroundButton1.setBackgroundColor(it)
-            animationContainer.setBackgroundColor(it)
+            binding.controlBarBackgroundColor.backgroundButton1.setBackgroundColor(it)
+            binding.animationContainer.setBackgroundColor(it)
             invertColor(it)
         }
 
-        minFrameView.setOnClickListener { showMinFrameDialog() }
-        maxFrameView.setOnClickListener { showMaxFrameDialog() }
+        binding.controlBarTrim.minFrameView.setOnClickListener { showMinFrameDialog() }
+        binding.controlBarTrim.maxFrameView.setOnClickListener { showMaxFrameDialog() }
         viewModel.selectSubscribe(PlayerState::minFrame, PlayerState::maxFrame) { minFrame, maxFrame ->
-            animationView.setMinAndMaxFrame(minFrame, maxFrame)
+            binding.animationView.setMinAndMaxFrame(minFrame, maxFrame)
             // I think this is a lint bug. It complains about int being <ErrorType>
             //noinspection StringFormatMatches
-            minFrameView.setText(resources.getString(R.string.min_frame, animationView.minFrame.toInt()))
+            binding.controlBarTrim.minFrameView.setText(resources.getString(R.string.min_frame, binding.animationView.minFrame.toInt()))
             //noinspection StringFormatMatches
-            maxFrameView.setText(resources.getString(R.string.max_frame, animationView.maxFrame.toInt()))
+            binding.controlBarTrim.maxFrameView.setText(resources.getString(R.string.max_frame, binding.animationView.maxFrame.toInt()))
         }
 
         viewModel.fetchAnimation(args)
         viewModel.asyncSubscribe(PlayerState::composition, onFail = {
-            Snackbar.make(coordinatorLayout, R.string.composition_load_error, Snackbar.LENGTH_LONG).show()
+            Snackbar.make(binding.coordinatorLayout, R.string.composition_load_error, Snackbar.LENGTH_LONG).show()
             Log.w(L.TAG, "Error loading composition.", it)
         }) {
-            loadingView.isVisible = false
+            binding.loadingView.isVisible = false
             onCompositionLoaded(it)
         }
 
-        borderToggle.setOnClickListener { viewModel.toggleBorderVisible() }
+        binding.controlBar.borderToggle.setOnClickListener { viewModel.toggleBorderVisible() }
         viewModel.selectSubscribe(PlayerState::borderVisible) {
-            borderToggle.isActivated = it
-            borderToggle.setImageResource(
+            binding.controlBar.borderToggle.isActivated = it
+            binding.controlBar.borderToggle.setImageResource(
                     if (it) R.drawable.ic_border_on
                     else R.drawable.ic_border_off
             )
-            animationView.setBackgroundResource(if (it) R.drawable.outline else 0)
+            binding.animationView.setBackgroundResource(if (it) R.drawable.outline else 0)
         }
 
-        hardwareAccelerationToggle.setOnClickListener {
-            val renderMode = if (animationView.layerType == View.LAYER_TYPE_HARDWARE) {
+        binding.controlBar.hardwareAccelerationToggle.setOnClickListener {
+            val renderMode = if (binding.animationView.layerType == View.LAYER_TYPE_HARDWARE) {
                 RenderMode.SOFTWARE
             } else {
                 RenderMode.HARDWARE
             }
-            animationView.setRenderMode(renderMode)
-            hardwareAccelerationToggle.isActivated = animationView.layerType == View.LAYER_TYPE_HARDWARE
+            binding.animationView.setRenderMode(renderMode)
+            binding.controlBar.hardwareAccelerationToggle.isActivated = binding.animationView.layerType == View.LAYER_TYPE_HARDWARE
         }
 
-        enableApplyingOpacityToLayers.setOnClickListener {
-            val isApplyingOpacityToLayersEnabled = !enableApplyingOpacityToLayers.isActivated
-            animationView.setApplyingOpacityToLayersEnabled(isApplyingOpacityToLayersEnabled)
-            enableApplyingOpacityToLayers.isActivated = isApplyingOpacityToLayersEnabled
+        binding.controlBar.enableApplyingOpacityToLayers.setOnClickListener {
+            val isApplyingOpacityToLayersEnabled = !binding.controlBar.enableApplyingOpacityToLayers.isActivated
+            binding.animationView.setApplyingOpacityToLayersEnabled(isApplyingOpacityToLayersEnabled)
+            binding.controlBar.enableApplyingOpacityToLayers.isActivated = isApplyingOpacityToLayersEnabled
         }
 
-        viewModel.selectSubscribe(PlayerState::controlsVisible) { controlsContainer.animateVisible(it) }
+        viewModel.selectSubscribe(PlayerState::controlsVisible) { binding.controlBarPlayerControls.controlsContainer.animateVisible(it) }
 
-        viewModel.selectSubscribe(PlayerState::controlBarVisible) { controlBar.animateVisible(it) }
+        viewModel.selectSubscribe(PlayerState::controlBarVisible) { binding.controlBar.root.animateVisible(it) }
 
-        renderGraphToggle.setOnClickListener { viewModel.toggleRenderGraphVisible() }
+        binding.controlBar.renderGraphToggle.setOnClickListener { viewModel.toggleRenderGraphVisible() }
         viewModel.selectSubscribe(PlayerState::renderGraphVisible) {
-            renderGraphToggle.isActivated = it
-            renderTimesGraphContainer.animateVisible(it)
-            renderTimesPerLayerButton.animateVisible(it)
-            lottieVersionView.animateVisible(!it)
+            binding.controlBar.renderGraphToggle.isActivated = it
+            binding.controlBarPlayerControls.renderTimesGraphContainer.animateVisible(it)
+            binding.controlBarPlayerControls.renderTimesPerLayerButton.animateVisible(it)
+            binding.controlBarPlayerControls.lottieVersionView.animateVisible(!it)
         }
 
-        backgroundColorToggle.setOnClickListener { viewModel.toggleBackgroundColorVisible() }
-        closeBackgroundColorButton.setOnClickListener { viewModel.setBackgroundColorVisible(false) }
+        binding.controlBar.backgroundColorToggle.setOnClickListener { viewModel.toggleBackgroundColorVisible() }
+        binding.controlBarBackgroundColor.closeBackgroundColorButton.setOnClickListener { viewModel.setBackgroundColorVisible(false) }
         viewModel.selectSubscribe(PlayerState::backgroundColorVisible) {
-            backgroundColorToggle.isActivated = it
-            backgroundColorContainer.animateVisible(it)
+            binding.controlBar.backgroundColorToggle.isActivated = it
+            binding.controlBarBackgroundColor.backgroundColorContainer.animateVisible(it)
         }
 
-        scaleToggle.setOnClickListener { viewModel.toggleScaleVisible() }
-        closeScaleButton.setOnClickListener { viewModel.setScaleVisible(false) }
+        binding.controlBar.scaleToggle.setOnClickListener { viewModel.toggleScaleVisible() }
+        binding.controlBarScale.closeScaleButton.setOnClickListener { viewModel.setScaleVisible(false) }
         viewModel.selectSubscribe(PlayerState::scaleVisible) {
-            scaleToggle.isActivated = it
-            scaleContainer.animateVisible(it)
+            binding.controlBar.scaleToggle.isActivated = it
+            binding.controlBarScale.scaleContainer.animateVisible(it)
         }
 
-        trimToggle.setOnClickListener { viewModel.toggleTrimVisible() }
-        closeTrimButton.setOnClickListener { viewModel.setTrimVisible(false) }
+        binding.controlBar.trimToggle.setOnClickListener { viewModel.toggleTrimVisible() }
+        binding.controlBarTrim.closeTrimButton.setOnClickListener { viewModel.setTrimVisible(false) }
         viewModel.selectSubscribe(PlayerState::trimVisible) {
-            trimToggle.isActivated = it
-            trimContainer.animateVisible(it)
+            binding.controlBar.trimToggle.isActivated = it
+            binding.controlBarTrim.trimContainer.animateVisible(it)
         }
 
-        mergePathsToggle.setOnClickListener { viewModel.toggleMergePaths() }
+        binding.controlBar.mergePathsToggle.setOnClickListener { viewModel.toggleMergePaths() }
         viewModel.selectSubscribe(PlayerState::useMergePaths) {
-            animationView.enableMergePathsForKitKatAndAbove(it)
-            mergePathsToggle.isActivated = it
+            binding.animationView.enableMergePathsForKitKatAndAbove(it)
+            binding.controlBar.mergePathsToggle.isActivated = it
         }
 
-        speedToggle.setOnClickListener { viewModel.toggleSpeedVisible() }
-        closeSpeedButton.setOnClickListener { viewModel.setSpeedVisible(false) }
+        binding.controlBar.speedToggle.setOnClickListener { viewModel.toggleSpeedVisible() }
+        binding.controlBarSpeed.closeSpeedButton.setOnClickListener { viewModel.setSpeedVisible(false) }
         viewModel.selectSubscribe(PlayerState::speedVisible) {
-            speedToggle.isActivated = it
-            speedContainer.isVisible = it
+            binding.controlBar.speedToggle.isActivated = it
+            binding.controlBarSpeed.speedContainer.isVisible = it
         }
         viewModel.selectSubscribe(PlayerState::speed) {
-            animationView.speed = it
-            speedButtonsContainer
+            binding.animationView.speed = it
+            binding.controlBarSpeed.speedButtonsContainer
                     .children
                     .filterIsInstance<ControlBarItemToggleView>()
                     .forEach { toggleView ->
-                        toggleView.isActivated = toggleView.getText().replace("x", "").toFloat() == animationView.speed
+                        toggleView.isActivated = toggleView.getText().replace("x", "").toFloat() == binding.animationView.speed
                     }
         }
-        speedButtonsContainer
+        binding.controlBarSpeed.speedButtonsContainer
                 .children
                 .filterIsInstance(ControlBarItemToggleView::class.java)
                 .forEach { child ->
@@ -244,68 +235,68 @@
                 }
 
 
-        loopButton.setOnClickListener { viewModel.toggleLoop() }
+        binding.controlBarPlayerControls.loopButton.setOnClickListener { viewModel.toggleLoop() }
         viewModel.selectSubscribe(PlayerState::repeatCount) {
-            animationView.repeatCount = it
-            loopButton.isActivated = animationView.repeatCount == ValueAnimator.INFINITE
+            binding.animationView.repeatCount = it
+            binding.controlBarPlayerControls.loopButton.isActivated = binding.animationView.repeatCount == ValueAnimator.INFINITE
         }
 
-        playButton.isActivated = animationView.isAnimating
+        binding.controlBarPlayerControls.playButton.isActivated = binding.animationView.isAnimating
 
-        seekBar.setOnSeekBarChangeListener(OnSeekBarChangeListenerAdapter(
+        binding.controlBarPlayerControls.seekBar.setOnSeekBarChangeListener(OnSeekBarChangeListenerAdapter(
                 onProgressChanged = { _, progress, _ ->
-                    if (seekBar.isPressed && progress in 1..4) {
-                        seekBar.progress = 0
+                    if (binding.controlBarPlayerControls.seekBar.isPressed && progress in 1..4) {
+                        binding.controlBarPlayerControls.seekBar.progress = 0
                         return@OnSeekBarChangeListenerAdapter
                     }
-                    if (animationView.isAnimating) return@OnSeekBarChangeListenerAdapter
-                    animationView.progress = progress / seekBar.max.toFloat()
+                    if (binding.animationView.isAnimating) return@OnSeekBarChangeListenerAdapter
+                    binding.animationView.progress = progress / binding.controlBarPlayerControls.seekBar.max.toFloat()
                 }
         ))
 
-        animationView.addAnimatorUpdateListener {
-            currentFrameView.text = updateFramesAndDurationLabel(animationView)
+        binding.animationView.addAnimatorUpdateListener {
+            binding.controlBarPlayerControls.currentFrameView.text = updateFramesAndDurationLabel(binding.animationView)
 
-            if (seekBar.isPressed) return@addAnimatorUpdateListener
-            seekBar.progress = ((it.animatedValue as Float) * seekBar.max).roundToInt()
+            if (binding.controlBarPlayerControls.seekBar.isPressed) return@addAnimatorUpdateListener
+            binding.controlBarPlayerControls.seekBar.progress = ((it.animatedValue as Float) * binding.controlBarPlayerControls.seekBar.max).roundToInt()
         }
-        animationView.addAnimatorListener(animatorListener)
-        playButton.setOnClickListener {
-            if (animationView.isAnimating) animationView.pauseAnimation() else animationView.resumeAnimation()
-            playButton.isActivated = animationView.isAnimating
+        binding.animationView.addAnimatorListener(animatorListener)
+        binding.controlBarPlayerControls.playButton.setOnClickListener {
+            if (binding.animationView.isAnimating) binding.animationView.pauseAnimation() else binding.animationView.resumeAnimation()
+            binding.controlBarPlayerControls.playButton.isActivated = binding.animationView.isAnimating
             postInvalidate()
         }
 
-        animationView.setOnClickListener {
+        binding.animationView.setOnClickListener {
             // Click the animation view to re-render it for debugging purposes.
-            animationView.invalidate()
+            binding.animationView.invalidate()
         }
 
-        scaleSeekBar.setOnSeekBarChangeListener(OnSeekBarChangeListenerAdapter(
+        binding.controlBarScale.scaleSeekBar.setOnSeekBarChangeListener(OnSeekBarChangeListenerAdapter(
                 onProgressChanged = { _, progress, _ ->
                     val minScale = minScale()
                     val maxScale = maxScale()
                     val scale = minScale + progress / 100f * (maxScale - minScale)
-                    animationView.scale = scale
-                    scaleText.text = "%.0f%%".format(scale * 100)
+                    binding.animationView.scale = scale
+                    binding.controlBarScale.scaleText.text = "%.0f%%".format(scale * 100)
                 }
         ))
 
-        arrayOf<BackgroundColorView>(
-                backgroundButton1,
-                backgroundButton2,
-                backgroundButton3,
-                backgroundButton4,
-                backgroundButton5,
-                backgroundButton6
+        arrayOf(
+                binding.controlBarBackgroundColor.backgroundButton1,
+                binding.controlBarBackgroundColor.backgroundButton2,
+                binding.controlBarBackgroundColor.backgroundButton3,
+                binding.controlBarBackgroundColor.backgroundButton4,
+                binding.controlBarBackgroundColor.backgroundButton5,
+                binding.controlBarBackgroundColor.backgroundButton6
         ).forEach { bb ->
             bb.setOnClickListener {
-                animationContainer.setBackgroundColor(bb.getColor())
+                binding.animationContainer.setBackgroundColor(bb.getColor())
                 invertColor(bb.getColor())
             }
         }
 
-        renderTimesGraph.apply {
+        binding.controlBarPlayerControls.renderTimesGraph.apply {
             setTouchEnabled(false)
             axisRight.isEnabled = false
             xAxis.isEnabled = false
@@ -329,17 +320,17 @@
             axisLeft.addLimitLine(ll2)
         }
 
-        renderTimesPerLayerButton.setOnClickListener {
+        binding.controlBarPlayerControls.renderTimesPerLayerButton.setOnClickListener {
             updateRenderTimesPerLayer()
             renderTimesBehavior.state = BottomSheetBehavior.STATE_COLLAPSED
         }
 
-        closeRenderTimesBottomSheetButton.setOnClickListener {
+        binding.bottomSheetRenderTimes.closeRenderTimesBottomSheetButton.setOnClickListener {
             renderTimesBehavior.state = BottomSheetBehavior.STATE_HIDDEN
         }
         renderTimesBehavior.state = BottomSheetBehavior.STATE_HIDDEN
 
-        warningsButton.setOnClickListener {
+        binding.controlBar.warningsButton.setOnClickListener {
             withState(viewModel) { state ->
                 if (state.composition()?.warnings?.isEmpty() != true) {
                     warningsBehavior.state = BottomSheetBehavior.STATE_COLLAPSED
@@ -348,16 +339,16 @@
             }
         }
 
-        closeWarningsBottomSheetButton.setOnClickListener {
+        binding.bottomSheetWarnings.closeWarningsBottomSheetButton.setOnClickListener {
             warningsBehavior.state = BottomSheetBehavior.STATE_HIDDEN
         }
         warningsBehavior.state = BottomSheetBehavior.STATE_HIDDEN
 
-        keyPathsToggle.setOnClickListener {
+        binding.controlBar.keyPathsToggle.setOnClickListener {
             keyPathsBehavior.state = BottomSheetBehavior.STATE_COLLAPSED
         }
 
-        closeKeyPathsBottomSheetButton.setOnClickListener {
+        binding.bottomSheetKeyPaths.closeKeyPathsBottomSheetButton.setOnClickListener {
             keyPathsBehavior.state = BottomSheetBehavior.STATE_HIDDEN
         }
         keyPathsBehavior.state = BottomSheetBehavior.STATE_HIDDEN
@@ -365,7 +356,7 @@
 
     private fun showMinFrameDialog() {
         val minFrameView = EditText(context)
-        minFrameView.setText(animationView.minFrame.toInt().toString())
+        minFrameView.setText(binding.animationView.minFrame.toInt().toString())
         AlertDialog.Builder(context)
                 .setTitle(R.string.min_frame_dialog)
                 .setView(minFrameView)
@@ -378,7 +369,7 @@
 
     private fun showMaxFrameDialog() {
         val maxFrameView = EditText(context)
-        maxFrameView.setText(animationView.maxFrame.toInt().toString())
+        maxFrameView.setText(binding.animationView.maxFrame.toInt().toString())
         AlertDialog.Builder(context)
                 .setTitle(R.string.max_frame_dialog)
                 .setView(maxFrameView)
@@ -396,8 +387,8 @@
 
     private fun invertColor(color: Int) {
         val isDarkBg = color.isDark()
-        animationView.isActivated = isDarkBg
-        toolbar.isActivated = isDarkBg
+        binding.animationView.isActivated = isDarkBg
+        binding.toolbar.isActivated = isDarkBg
     }
 
     private fun Int.isDark(): Boolean {
@@ -406,7 +397,7 @@
     }
 
     override fun onDestroyView() {
-        animationView.removeAnimatorListener(animatorListener)
+        binding.animationView.removeAnimatorListener(animatorListener)
         super.onDestroyView()
     }
 
@@ -432,24 +423,24 @@
     private fun onCompositionLoaded(composition: LottieComposition?) {
         composition ?: return
 
-        animationView.setComposition(composition)
-        hardwareAccelerationToggle.isActivated = animationView.layerType == View.LAYER_TYPE_HARDWARE
-        animationView.setPerformanceTrackingEnabled(true)
+        binding.animationView.setComposition(composition)
+        binding.controlBar.hardwareAccelerationToggle.isActivated = binding.animationView.layerType == View.LAYER_TYPE_HARDWARE
+        binding.animationView.setPerformanceTrackingEnabled(true)
         var renderTimeGraphRange = 4f
-        animationView.performanceTracker?.addFrameListener { ms ->
+        binding.animationView.performanceTracker?.addFrameListener { ms ->
             if (lifecycle.currentState != Lifecycle.State.RESUMED) return@addFrameListener
-            lineDataSet.getEntryForIndex((animationView.progress * 100).toInt()).y = ms
-            renderTimeGraphRange = Math.max(renderTimeGraphRange, ms * 1.2f)
-            renderTimesGraph.setVisibleYRange(0f, renderTimeGraphRange, YAxis.AxisDependency.LEFT)
-            renderTimesGraph.invalidate()
+            lineDataSet.getEntryForIndex((binding.animationView.progress * 100).toInt()).y = ms
+            renderTimeGraphRange = renderTimeGraphRange.coerceAtLeast(ms * 1.2f)
+            binding.controlBarPlayerControls.renderTimesGraph.setVisibleYRange(0f, renderTimeGraphRange, YAxis.AxisDependency.LEFT)
+            binding.controlBarPlayerControls.renderTimesGraph.invalidate()
         }
 
         // Scale up to fill the screen
-        scaleSeekBar.progress = 100
+        binding.controlBarScale.scaleSeekBar.progress = 100
 
-        keyPathsRecyclerView.buildModelsWith(object : EpoxyRecyclerView.ModelBuilderCallback {
+        binding.bottomSheetKeyPaths.keyPathsRecyclerView.buildModelsWith(object : EpoxyRecyclerView.ModelBuilderCallback {
             override fun buildModels(controller: EpoxyController) {
-                animationView.resolveKeyPath(KeyPath("**")).forEachIndexed { index, keyPath ->
+                binding.animationView.resolveKeyPath(KeyPath("**")).forEachIndexed { index, keyPath ->
                     BottomSheetItemViewModel_()
                             .id(index)
                             .text(keyPath.keysToString())
@@ -465,36 +456,36 @@
     }
 
     private fun updateRenderTimesPerLayer() {
-        renderTimesContainer.removeAllViews()
-        animationView.performanceTracker?.sortedRenderTimes?.forEach {
+        binding.bottomSheetRenderTimes.renderTimesContainer.removeAllViews()
+        binding.animationView.performanceTracker?.sortedRenderTimes?.forEach {
             val view = BottomSheetItemView(requireContext()).apply {
                 set(
                         it.first!!.replace("__container", "Total"),
                         "%.2f ms".format(it.second!!)
                 )
             }
-            renderTimesContainer.addView(view)
+            binding.bottomSheetRenderTimes.renderTimesContainer.addView(view)
         }
     }
 
     private fun updateWarnings() = withState(viewModel) { state ->
         // Force warning to update
-        warningsContainer.removeAllViews()
+        binding.bottomSheetWarnings.warningsContainer.removeAllViews()
 
         val warnings = state.composition()?.warnings ?: emptySet<String>()
-        if (!warnings.isEmpty() && warnings.size == warningsContainer.childCount) return@withState
+        if (!warnings.isEmpty() && warnings.size == binding.bottomSheetWarnings.warningsContainer.childCount) return@withState
 
-        warningsContainer.removeAllViews()
+        binding.bottomSheetWarnings.warningsContainer.removeAllViews()
         warnings.forEach {
             val view = BottomSheetItemView(requireContext()).apply {
                 set(it)
             }
-            warningsContainer.addView(view)
+            binding.bottomSheetWarnings.warningsContainer.addView(view)
         }
 
         val size = warnings.size
-        warningsButton.setText(resources.getQuantityString(R.plurals.warnings, size, size))
-        warningsButton.setImageResource(
+        binding.controlBar.warningsButton.setText(resources.getQuantityString(R.plurals.warnings, size, size))
+        binding.controlBar.warningsButton.setImageResource(
                 if (warnings.isEmpty()) R.drawable.ic_sentiment_satisfied
                 else R.drawable.ic_sentiment_dissatisfied
         )
@@ -512,7 +503,7 @@
         )
     }
 
-    private fun beginDelayedTransition() = TransitionManager.beginDelayedTransition(container, transition)
+    private fun beginDelayedTransition() = TransitionManager.beginDelayedTransition(binding.container, transition)
 
     companion object {
         const val EXTRA_ANIMATION_ARGS = "animation_args"
@@ -530,12 +521,12 @@
         val currentFrame = animation.frame.toString()
         val totalFrames = ("%.0f").format(animation.maxFrame)
 
-        val animationSpeed: Float = Math.abs(animation.speed)
+        val animationSpeed: Float = abs(animation.speed)
 
         val totalTime = ((animation.duration / animationSpeed) / 1000.0)
         val totalTimeFormatted = ("%.1f").format(totalTime)
 
-        val progress = (totalTime / 100.0) * (Math.round(animation.progress * 100.0))
+        val progress = (totalTime / 100.0) * ((animation.progress * 100.0).roundToInt())
         val progressFormatted = ("%.1f").format(progress)
 
         return "$currentFrame/$totalFrames\n$progressFormatted/$totalTimeFormatted"
diff --git a/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/PlayerViewModel.kt b/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/PlayerViewModel.kt
index dc7ad22..a6bce9f 100644
--- a/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/PlayerViewModel.kt
+++ b/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/PlayerViewModel.kt
@@ -7,8 +7,11 @@
 import com.airbnb.lottie.LottieCompositionFactory
 import com.airbnb.lottie.LottieTask
 import com.airbnb.lottie.samples.model.CompositionArgs
+import com.airbnb.lottie.samples.utils.MvRxViewModel
 import com.airbnb.mvrx.*
 import java.io.FileInputStream
+import kotlin.math.max
+import kotlin.math.min
 
 data class PlayerState(
         val composition: Async<LottieComposition> = Uninitialized,
@@ -20,7 +23,6 @@
         val scaleVisible: Boolean = false,
         val speedVisible: Boolean = false,
         val trimVisible: Boolean = false,
-        val useHardwareAcceleration: Boolean = false,
         val useMergePaths: Boolean = false,
         val minFrame: Int = 0,
         val maxFrame: Int = 0,
@@ -80,16 +82,14 @@
 
     fun setTrimVisible(visible: Boolean) = setState { copy(trimVisible = visible) }
 
-    fun toggleHardwareAcceleration() = setState { copy(useHardwareAcceleration = !useHardwareAcceleration) }
-
     fun toggleMergePaths() = setState { copy(useMergePaths = !useMergePaths) }
 
     fun setMinFrame(minFrame: Int) = setState {
-        copy(minFrame = Math.max(minFrame, composition()?.startFrame?.toInt() ?: 0))
+        copy(minFrame = max(minFrame, composition()?.startFrame?.toInt() ?: 0))
     }
 
     fun setMaxFrame(maxFrame: Int) = setState {
-        copy(maxFrame = Math.min(maxFrame, composition()?.endFrame?.toInt() ?: 0))
+        copy(maxFrame = min(maxFrame, composition()?.endFrame?.toInt() ?: 0))
     }
 
     fun setSpeed(speed: Float) = setState { copy(speed = speed) }
diff --git a/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/PreviewFragment.kt b/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/PreviewFragment.kt
index 47961ca..cdb3d1b 100644
--- a/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/PreviewFragment.kt
+++ b/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/PreviewFragment.kt
@@ -10,14 +10,11 @@
 import android.widget.ArrayAdapter
 import android.widget.EditText
 import android.widget.Toast
-import androidx.core.app.ActivityCompat.requestPermissions
-import androidx.core.app.ActivityCompat.startActivityForResult
-import androidx.core.content.ContextCompat.startActivity
 import com.airbnb.epoxy.EpoxyController
-import com.airbnb.lottie.samples.R.id.coordinatorLayout
 import com.airbnb.lottie.samples.model.CompositionArgs
+import com.airbnb.lottie.samples.utils.BaseEpoxyFragment
+import com.airbnb.lottie.samples.utils.hasPermission
 import com.airbnb.lottie.samples.views.marquee
-import kotlinx.android.synthetic.main.fragment_player.*
 
 private const val RC_FILE = 1000
 private const val RC_CAMERA_PERMISSION = 1001
@@ -110,7 +107,7 @@
                 if (grantResults.firstOrNull() == PackageManager.PERMISSION_GRANTED) {
                     startActivity(QRScanActivity.intent(requireContext()))
                 } else {
-                    Snackbar.make(coordinatorLayout, R.string.qr_permission_denied, Snackbar.LENGTH_LONG).show()
+                    Snackbar.make(binding.root, R.string.qr_permission_denied, Snackbar.LENGTH_LONG).show()
                 }
             }
         }
diff --git a/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/PreviewItemView.kt b/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/PreviewItemView.kt
index 79f1c3c..42e068f 100644
--- a/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/PreviewItemView.kt
+++ b/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/PreviewItemView.kt
@@ -1,15 +1,15 @@
 package com.airbnb.lottie.samples
 
 import android.content.Context
-import androidx.annotation.DrawableRes
 import android.util.AttributeSet
-import android.view.View
 import android.widget.LinearLayout
+import androidx.annotation.DrawableRes
 import com.airbnb.epoxy.CallbackProp
 import com.airbnb.epoxy.ModelProp
 import com.airbnb.epoxy.ModelView
 import com.airbnb.epoxy.TextProp
-import kotlinx.android.synthetic.main.list_item_preview.view.*
+import com.airbnb.lottie.samples.databinding.ListItemPreviewBinding
+import com.airbnb.lottie.samples.utils.viewBinding
 
 @ModelView(autoLayout = ModelView.Size.MATCH_WIDTH_WRAP_HEIGHT)
 class PreviewItemView @JvmOverloads constructor(
@@ -17,24 +17,24 @@
         attrs: AttributeSet? = null,
         defStyleAttr: Int = 0
 ) : LinearLayout(context, attrs, defStyleAttr) {
+    private val binding: ListItemPreviewBinding by viewBinding()
 
     init {
         orientation = VERTICAL
-        inflate(R.layout.list_item_preview)
     }
 
     @TextProp
     fun setTitle(title: CharSequence) {
-        titleView.text = title
+        binding.titleView.text = title
     }
 
     @ModelProp
     fun setIcon(@DrawableRes icon: Int) {
-        iconView.setImageResource(icon)
+        binding.iconView.setImageResource(icon)
     }
 
     @CallbackProp
-    fun setClickListener(clickListener: View.OnClickListener?) {
-        container.setOnClickListener(clickListener)
+    fun setClickListener(clickListener: OnClickListener?) {
+        binding.container.setOnClickListener(clickListener)
     }
 }
\ No newline at end of file
diff --git a/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/QRScanActivity.kt b/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/QRScanActivity.kt
index 19c654a..6789387 100644
--- a/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/QRScanActivity.kt
+++ b/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/QRScanActivity.kt
@@ -6,11 +6,14 @@
 import android.os.Bundle
 import android.os.Vibrator
 import androidx.appcompat.app.AppCompatActivity
+import com.airbnb.lottie.samples.databinding.QrscanActivityBinding
 import com.airbnb.lottie.samples.model.CompositionArgs
+import com.airbnb.lottie.samples.utils.vibrateCompat
+import com.airbnb.lottie.samples.utils.viewBinding
 import com.dlazaro66.qrcodereaderview.QRCodeReaderView
-import kotlinx.android.synthetic.main.activity_qrscan.*
 
 class QRScanActivity : AppCompatActivity(), QRCodeReaderView.OnQRCodeReadListener {
+    private val binding: QrscanActivityBinding by viewBinding()
     private val vibrator by lazy { getSystemService(Context.VIBRATOR_SERVICE) as Vibrator }
 
     // Sometimes the qr code is read twice in rapid succession. This prevents it from being read
@@ -19,24 +22,23 @@
 
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
-        setContentView(R.layout.activity_qrscan)
 
-        qrView.setQRDecodingEnabled(true)
-        qrView.setAutofocusInterval(2000L)
-        qrView.setBackCamera()
-        qrView.setOnQRCodeReadListener(this)
-        qrView.setOnClickListener { qrView.forceAutoFocus() }
+        binding.qrView.setQRDecodingEnabled(true)
+        binding.qrView.setAutofocusInterval(2000L)
+        binding.qrView.setBackCamera()
+        binding.qrView.setOnQRCodeReadListener(this)
+        binding.qrView.setOnClickListener { binding.qrView.forceAutoFocus() }
     }
 
     override fun onResume() {
         super.onResume()
-        qrView.startCamera()
+        binding.qrView.startCamera()
         hasReadQrCode = false
     }
 
     override fun onPause() {
         super.onPause()
-        qrView.stopCamera()
+        binding.qrView.stopCamera()
     }
 
     override fun onQRCodeRead(url: String, pointFS: Array<PointF>) {
diff --git a/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/ShowcaseFragment.kt b/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/ShowcaseFragment.kt
index 5636530..f5e879a 100644
--- a/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/ShowcaseFragment.kt
+++ b/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/ShowcaseFragment.kt
@@ -2,30 +2,26 @@
 
 import android.content.Intent
 import com.airbnb.epoxy.EpoxyController
+import com.airbnb.lottie.samples.api.LottiefilesApi
 import com.airbnb.lottie.samples.model.AnimationResponseV2
 import com.airbnb.lottie.samples.model.CompositionArgs
 import com.airbnb.lottie.samples.model.ShowcaseItem
+import com.airbnb.lottie.samples.utils.BaseEpoxyFragment
+import com.airbnb.lottie.samples.utils.MvRxViewModel
 import com.airbnb.lottie.samples.views.animationItemView
 import com.airbnb.lottie.samples.views.loadingView
 import com.airbnb.lottie.samples.views.marquee
 import com.airbnb.lottie.samples.views.showcaseCarousel
-import com.airbnb.mvrx.Async
-import com.airbnb.mvrx.MvRxState
-import com.airbnb.mvrx.MvRxViewModelFactory
-import com.airbnb.mvrx.Uninitialized
-import com.airbnb.mvrx.ViewModelContext
-import com.airbnb.mvrx.fragmentViewModel
-import com.airbnb.mvrx.withState
-import io.reactivex.schedulers.Schedulers
+import com.airbnb.mvrx.*
+import kotlinx.coroutines.Dispatchers
 
 data class ShowcaseState(val response: Async<AnimationResponseV2> = Uninitialized) : MvRxState
 
 class ShowcaseViewModel(initialState: ShowcaseState, api: LottiefilesApi) : MvRxViewModel<ShowcaseState>(initialState) {
     init {
-        api.getCollection()
-                .subscribeOn(Schedulers.io())
-                .retry(3)
-                .execute { copy(response = it) }
+        suspend {
+            api.getCollection()
+        }.execute(Dispatchers.IO) { copy(response = it) }
     }
 
     companion object : MvRxViewModelFactory<ShowcaseViewModel, ShowcaseState> {
@@ -55,7 +51,7 @@
                 startActivity(Intent(requireContext(), BullseyeActivity::class.java))
             },
             ShowcaseItem(R.drawable.showcase_preview_lottie, R.string.showcase_item_recycler_view) {
-                startActivity(Intent(requireContext(), ListActivity::class.java))
+                startActivity(Intent(requireContext(), WishListActivity::class.java))
             }
     )
     private val viewModel: ShowcaseViewModel by fragmentViewModel()
diff --git a/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/SimpleAnimationActivity.kt b/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/SimpleAnimationActivity.kt
index 047dbaf..08d5185 100644
--- a/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/SimpleAnimationActivity.kt
+++ b/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/SimpleAnimationActivity.kt
@@ -6,22 +6,20 @@
 import com.airbnb.lottie.LottieComposition
 import com.airbnb.lottie.LottieCompositionFactory
 import com.airbnb.lottie.LottieDrawable
-import com.airbnb.lottie.model.LottieCompositionCache
-import kotlinx.android.synthetic.main.activity_simple_animation.*
-import kotlinx.android.synthetic.main.activity_simple_animation.view.*
-import java.lang.IllegalArgumentException
+import com.airbnb.lottie.samples.databinding.SimpleAnimationActivityBinding
+import com.airbnb.lottie.samples.utils.viewBinding
 
 /**
  * Useful for performance debugging.
  * adb shell am start -n com.airbnb.lottie/.samples.SimpleAnimationActivity --es animation LottieLogo1.json --activity-clear-top
  */
 class SimpleAnimationActivity : AppCompatActivity() {
+    private val binding: SimpleAnimationActivityBinding by viewBinding()
 
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
-        setContentView(R.layout.activity_simple_animation)
         var composition: LottieComposition? = null
-        parse.setOnClickListener {
+        binding.parse.setOnClickListener {
             val assetName = intent.extras?.getString("animation") ?: ""
             val start = System.currentTimeMillis()
             composition = LottieCompositionFactory.fromAssetSync(this, assetName, null).value
@@ -29,16 +27,16 @@
             Toast.makeText(this@SimpleAnimationActivity, "Done ${System.currentTimeMillis() - start}", Toast.LENGTH_SHORT).show()
         }
 
-        setComposition.setOnClickListener {
+        binding.setComposition.setOnClickListener {
             val start = System.currentTimeMillis()
             val drawable = LottieDrawable()
             drawable.setComposition(composition)
             Toast.makeText(this@SimpleAnimationActivity, "Done ${System.currentTimeMillis() - start}", Toast.LENGTH_SHORT).show()
         }
 
-        play.setOnClickListener {
-            composition?.let { animationView.setComposition(it) }
-            animationView.playAnimation()
+        binding.play.setOnClickListener {
+            composition?.let { binding.animationView.setComposition(it) }
+            binding.animationView.playAnimation()
         }
     }
 }
\ No newline at end of file
diff --git a/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/SnapshotTestActivity.kt b/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/SnapshotTestActivity.kt
index 60bc19a..faa86ac 100644
--- a/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/SnapshotTestActivity.kt
+++ b/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/SnapshotTestActivity.kt
@@ -1,39 +1,17 @@
 package com.airbnb.lottie.samples
 
-import android.graphics.Bitmap
-import android.graphics.BitmapFactory
-import android.graphics.Canvas
-import android.graphics.Color
-import android.graphics.PorterDuff
-import android.graphics.Typeface
-import android.os.Bundle
 import androidx.appcompat.app.AppCompatActivity
-import androidx.core.view.isVisible
-import com.airbnb.lottie.FontAssetDelegate
-import com.airbnb.lottie.ImageAssetDelegate
-import com.airbnb.lottie.LottieComposition
-import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool
-import kotlinx.android.synthetic.main.activity_film_strip_snapshots.*
-import kotlinx.android.synthetic.main.activity_snapshot_tests.*
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.async
-import kotlinx.coroutines.launch
-import kotlin.coroutines.resume
-import kotlin.coroutines.suspendCoroutine
+import com.airbnb.lottie.samples.databinding.SnapshotTestsActivityBinding
+import com.airbnb.lottie.samples.utils.viewBinding
 
 class SnapshotTestActivity : AppCompatActivity() {
-
-    override fun onCreate(savedInstanceState: Bundle?) {
-        super.onCreate(savedInstanceState)
-        setContentView(R.layout.activity_snapshot_tests)
-    }
+    private val binding: SnapshotTestsActivityBinding by viewBinding()
 
     fun recordSnapshot(snapshotName: String, snapshotVariant: String) {
-        counterTextView.post {
-            statusTextView.text = if (snapshotVariant == "default") snapshotName else "$snapshotName - $snapshotVariant"
-            val count = counterTextView.text.toString().toInt()
-            counterTextView.text = "${count + 1}"
+        binding.counterTextView.post {
+            binding.statusTextView.text = if (snapshotVariant == "default") snapshotName else "$snapshotName - $snapshotVariant"
+            val count = binding.counterTextView.text.toString().toInt()
+            binding.counterTextView.text = "${count + 1}"
         }
     }
 }
\ No newline at end of file
diff --git a/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/TestColorFilterActivity.java b/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/TestColorFilterActivity.java
deleted file mode 100644
index 7877cfb..0000000
--- a/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/TestColorFilterActivity.java
+++ /dev/null
@@ -1,13 +0,0 @@
-package com.airbnb.lottie.samples;
-
-import android.os.Bundle;
-import androidx.annotation.Nullable;
-import androidx.appcompat.app.AppCompatActivity;
-
-public class TestColorFilterActivity extends AppCompatActivity {
-
-  @Override protected void onCreate(@Nullable Bundle savedInstanceState) {
-    super.onCreate(savedInstanceState);
-    setContentView(R.layout.activity_test_color_filter);
-  }
-}
\ No newline at end of file
diff --git a/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/TypographyDemoActivity.kt b/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/TypographyDemoActivity.kt
index 64cbbb8..9853911 100644
--- a/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/TypographyDemoActivity.kt
+++ b/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/TypographyDemoActivity.kt
@@ -4,22 +4,24 @@
 import android.view.View
 import android.view.ViewTreeObserver
 import androidx.appcompat.app.AppCompatActivity
-import kotlinx.android.synthetic.main.activity_typography_demo.*
+import com.airbnb.lottie.samples.databinding.TypographyDemoActivityBinding
+import com.airbnb.lottie.samples.utils.viewBinding
 
 class TypographyDemoActivity : AppCompatActivity() {
+    private val binding: TypographyDemoActivityBinding by viewBinding()
+
     private val layoutListener = ViewTreeObserver.OnGlobalLayoutListener {
-        scrollView.fullScroll(View.FOCUS_DOWN)
+        binding.scrollView.fullScroll(View.FOCUS_DOWN)
     }
 
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
-        setContentView(R.layout.activity_typography_demo)
-        fontView.viewTreeObserver.addOnGlobalLayoutListener(layoutListener)
+        binding.fontView.viewTreeObserver.addOnGlobalLayoutListener(layoutListener)
     }
 
 
     override fun onDestroy() {
-        fontView.viewTreeObserver.removeOnGlobalLayoutListener(layoutListener)
+        binding.fontView.viewTreeObserver.removeOnGlobalLayoutListener(layoutListener)
         super.onDestroy()
     }
 }
diff --git a/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/WishListActivity.kt b/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/WishListActivity.kt
new file mode 100644
index 0000000..d066e9d
--- /dev/null
+++ b/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/WishListActivity.kt
@@ -0,0 +1,45 @@
+package com.airbnb.lottie.samples
+
+import android.os.Bundle
+import androidx.appcompat.app.AppCompatActivity
+import com.airbnb.epoxy.EpoxyController
+import com.airbnb.epoxy.EpoxyRecyclerView
+import com.airbnb.lottie.samples.databinding.ListActivityBinding
+import com.airbnb.lottie.samples.utils.viewBinding
+import com.airbnb.lottie.samples.views.listingCard
+import com.airbnb.lottie.samples.views.marquee
+
+class WishListActivity : AppCompatActivity() {
+    private val binding: ListActivityBinding by viewBinding()
+
+    private val wishListedItems = mutableSetOf<Int>()
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        binding.recyclerView.buildModelsWith(object : EpoxyRecyclerView.ModelBuilderCallback {
+            override fun buildModels(controller: EpoxyController) {
+                controller.buildModels()
+            }
+        })
+    }
+
+    private fun EpoxyController.buildModels() {
+        marquee {
+            id("marquee")
+            title("List")
+            subtitle("Loading the same animation many times in a list")
+        }
+
+        repeat(100) { index ->
+            listingCard {
+                id(index)
+                isWishListed(wishListedItems.contains(index))
+                onToggled { isWishListed ->
+                    if (isWishListed) wishListedItems.add(index)
+                    else wishListedItems.remove(index)
+                    binding.recyclerView.requestModelBuild()
+                }
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/api/LottiefilesApi.kt b/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/api/LottiefilesApi.kt
new file mode 100644
index 0000000..8f26bc8
--- /dev/null
+++ b/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/api/LottiefilesApi.kt
@@ -0,0 +1,21 @@
+package com.airbnb.lottie.samples.api
+
+import com.airbnb.lottie.samples.model.AnimationResponse
+import com.airbnb.lottie.samples.model.AnimationResponseV2
+import retrofit2.http.GET
+import retrofit2.http.Path
+import retrofit2.http.Query
+
+interface LottiefilesApi {
+    @GET("v1/recent")
+    suspend fun getRecent(@Query("page") page: Int): AnimationResponse
+
+    @GET("v1/popular")
+    suspend fun getPopular(@Query("page") page: Int): AnimationResponse
+
+    @GET("v2/featured")
+    suspend fun getCollection(): AnimationResponseV2
+
+    @GET("v1/search/{query}")
+    suspend fun search(@Path("query") query: String, @Query("page") page: Int): AnimationResponse
+}
\ No newline at end of file
diff --git a/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/model/AnimationData.kt b/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/model/AnimationData.kt
index c36abbb..e324c27 100644
--- a/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/model/AnimationData.kt
+++ b/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/model/AnimationData.kt
@@ -1,7 +1,7 @@
 package com.airbnb.lottie.samples.model
 
 import android.os.Parcelable
-import com.airbnb.lottie.samples.toColorIntSafe
+import com.airbnb.lottie.samples.utils.toColorIntSafe
 import kotlinx.android.parcel.Parcelize
 
 @Parcelize
diff --git a/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/model/AnimationDataV2.kt b/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/model/AnimationDataV2.kt
index 7c64b4a..f892d77 100644
--- a/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/model/AnimationDataV2.kt
+++ b/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/model/AnimationDataV2.kt
@@ -1,7 +1,7 @@
 package com.airbnb.lottie.samples.model
 
 import android.os.Parcelable
-import com.airbnb.lottie.samples.toColorIntSafe
+import com.airbnb.lottie.samples.utils.toColorIntSafe
 import kotlinx.android.parcel.Parcelize
 
 @Parcelize
diff --git a/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/FilmStripSnapshotActivity.kt b/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/testing/FilmStripSnapshotActivity.kt
similarity index 84%
rename from LottieSample/src/main/kotlin/com/airbnb/lottie/samples/FilmStripSnapshotActivity.kt
rename to LottieSample/src/main/kotlin/com/airbnb/lottie/samples/testing/FilmStripSnapshotActivity.kt
index 6157758..aab597f 100644
--- a/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/FilmStripSnapshotActivity.kt
+++ b/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/testing/FilmStripSnapshotActivity.kt
@@ -1,4 +1,4 @@
-package com.airbnb.lottie.samples
+package com.airbnb.lottie.samples.testing
 
 import android.Manifest
 import android.content.pm.PackageManager
@@ -9,27 +9,25 @@
 import androidx.appcompat.app.AppCompatActivity
 import androidx.core.app.ActivityCompat
 import androidx.core.content.ContextCompat
-import androidx.core.view.children
 import androidx.core.view.doOnNextLayout
 import com.airbnb.lottie.L
-import com.airbnb.lottie.LottieAnimationView
 import com.airbnb.lottie.LottieCompositionFactory
-import kotlinx.android.synthetic.main.activity_film_strip_snapshots.*
+import com.airbnb.lottie.samples.databinding.FilmStripSnapshotsActivityBinding
+import com.airbnb.lottie.samples.utils.viewBinding
 import java.io.File
 import java.io.FileInputStream
 import java.io.FileOutputStream
-import kotlin.IllegalStateException
 
 private const val RC_PERMISSION = 12345
 
 class FilmStripSnapshotActivity : AppCompatActivity() {
+    private val binding: FilmStripSnapshotsActivityBinding by viewBinding()
 
     // TODO: fix this.
     @Suppress("DEPRECATION")
     private val rootDir = "${Environment.getExternalStorageDirectory()}/lottie"
     private val animationsDir = File("$rootDir/animations")
     private val snapshotsDir = File("$rootDir/snapshots")
-    private val dummyBitmap by lazy { BitmapFactory.decodeResource(resources, R.drawable.airbnb) }
 
     private val bitmap = Bitmap.createBitmap(1000, 1000, Bitmap.Config.ARGB_8888)
     private val canvas = Canvas(bitmap)
@@ -39,13 +37,12 @@
 
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
-        setContentView(R.layout.activity_film_strip_snapshots)
         Log.i(L.TAG, "Starting Snapshots")
 
         if (!animationsDir.exists() || !animationsDir.isDirectory) throw IllegalStateException("Animations directory ($animationsDir) does not exist!")
         if (!snapshotsDir.exists()) snapshotsDir.mkdirs()
 
-        filmStripView.doOnNextLayout {
+        binding.filmStripView.doOnNextLayout {
             createSnapshots()
             Log.i(L.TAG, "Finished Snapshots")
             finish()
@@ -78,17 +75,18 @@
             return
         }
 
-        Log.d(L.TAG, "Found ${animationsDir.listFiles().size} files.")
-        animationsDir.listFiles()
+        val files = animationsDir.listFiles() ?: emptyArray()
+        Log.d(L.TAG, "Found ${files.size} files.")
+        files
                 .filter { it.name.endsWith(".json") }
                 .forEach { file ->
                     Log.d(L.TAG, "Creating snapshotFilmstrip for ${file.name}")
                     val fis = FileInputStream(file)
                     val result = LottieCompositionFactory.fromJsonInputStreamSync(fis, file.name)
                     val composition = result.value ?: throw IllegalStateException("Unable to parse composition for $file", result.exception)
-                    filmStripView.setComposition(composition)
+                    binding.filmStripView.setComposition(composition)
                     canvas.clear()
-                    filmStripView.draw(canvas)
+                    binding.filmStripView.draw(canvas)
 
                     val outputFileName = file.name.replace(".json", ".png")
                     val outputFilePath = "${Environment.getExternalStorageDirectory()}/lottie/snapshots/$outputFileName"
diff --git a/LottieSample/src/androidTest/java/com/airbnb/lottie/samples/NoCacheLottieAnimationView.kt b/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/testing/NoCacheLottieAnimationView.kt
similarity index 90%
rename from LottieSample/src/androidTest/java/com/airbnb/lottie/samples/NoCacheLottieAnimationView.kt
rename to LottieSample/src/main/kotlin/com/airbnb/lottie/samples/testing/NoCacheLottieAnimationView.kt
index eb148d6..e87fc90 100644
--- a/LottieSample/src/androidTest/java/com/airbnb/lottie/samples/NoCacheLottieAnimationView.kt
+++ b/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/testing/NoCacheLottieAnimationView.kt
@@ -1,4 +1,4 @@
-package com.airbnb.lottie.samples
+package com.airbnb.lottie.samples.testing
 
 import android.content.Context
 import android.util.AttributeSet
diff --git a/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/utils/ActivityViewBindingDelegate.kt b/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/utils/ActivityViewBindingDelegate.kt
new file mode 100644
index 0000000..3393702
--- /dev/null
+++ b/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/utils/ActivityViewBindingDelegate.kt
@@ -0,0 +1,33 @@
+package com.airbnb.lottie.samples.utils
+
+import android.app.Activity
+import android.view.LayoutInflater
+import androidx.viewbinding.ViewBinding
+import kotlin.properties.ReadOnlyProperty
+import kotlin.reflect.KProperty
+
+/**
+ * Create bindings for a view similar to bindView.
+ *
+ * To use, just call:
+ * private val binding: HomeWorkoutDetailsActivityBinding by viewBinding()
+ * with your binding class and access it as you normally would.
+ */
+inline fun <reified T : ViewBinding> Activity.viewBinding() = ActivityViewBindingDelegate(T::class.java, this)
+
+class ActivityViewBindingDelegate<T : ViewBinding>(
+        private val bindingClass: Class<T>,
+        val activity: Activity
+) : ReadOnlyProperty<Activity, T> {
+    private var binding: T? = null
+
+    override fun getValue(thisRef: Activity, property: KProperty<*>): T {
+        binding?.let { return it }
+
+        val inflateMethod = bindingClass.getMethod("inflate", LayoutInflater::class.java)
+        @Suppress("UNCHECKED_CAST")
+        binding = inflateMethod.invoke(null, thisRef.layoutInflater) as T
+        thisRef.setContentView(binding!!.root)
+        return binding!!
+    }
+}
\ No newline at end of file
diff --git a/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/utils/BaseEpoxyFragment.kt b/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/utils/BaseEpoxyFragment.kt
new file mode 100644
index 0000000..04889ad
--- /dev/null
+++ b/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/utils/BaseEpoxyFragment.kt
@@ -0,0 +1,32 @@
+package com.airbnb.lottie.samples.utils
+
+import android.os.Bundle
+import android.view.View
+import com.airbnb.epoxy.AsyncEpoxyController
+import com.airbnb.epoxy.EpoxyController
+import com.airbnb.lottie.samples.R
+import com.airbnb.lottie.samples.databinding.BaseFragmentBinding
+import com.airbnb.mvrx.BaseMvRxFragment
+
+
+private class BaseEpoxyController(
+        private val buildModelsCallback: EpoxyController.() -> Unit
+) : AsyncEpoxyController() {
+    override fun buildModels() {
+        buildModelsCallback()
+    }
+}
+
+abstract class BaseEpoxyFragment : BaseMvRxFragment(R.layout.base_fragment) {
+    protected val binding: BaseFragmentBinding by viewBinding()
+
+    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+        binding.recyclerView.setController(BaseEpoxyController { buildModels() })
+    }
+
+    override fun invalidate() {
+        binding.recyclerView.requestModelBuild()
+    }
+
+    abstract fun EpoxyController.buildModels()
+}
\ No newline at end of file
diff --git a/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/utils/FragmentViewBindingDelegate.kt b/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/utils/FragmentViewBindingDelegate.kt
new file mode 100644
index 0000000..e69610a
--- /dev/null
+++ b/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/utils/FragmentViewBindingDelegate.kt
@@ -0,0 +1,58 @@
+package com.airbnb.lottie.samples.utils
+
+import android.os.Handler
+import android.os.Looper
+import android.view.View
+import androidx.fragment.app.Fragment
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleObserver
+import androidx.lifecycle.OnLifecycleEvent
+import androidx.viewbinding.ViewBinding
+import kotlin.properties.ReadOnlyProperty
+import kotlin.reflect.KProperty
+
+/**
+ * Create bindings for a view similar to bindView.
+ *
+ * To use, just call
+ * private val binding: FHomeWorkoutDetailsBinding by viewBinding()
+ * with your binding class and access it as you normally would.
+ */
+inline fun <reified T : ViewBinding> Fragment.viewBinding() = FragmentViewBindingDelegate(T::class.java, this)
+
+class FragmentViewBindingDelegate<T : ViewBinding>(
+        bindingClass: Class<T>,
+        val fragment: Fragment
+) : ReadOnlyProperty<Fragment, T> {
+    private val clearBindingHandler by lazy(LazyThreadSafetyMode.NONE) { Handler(Looper.getMainLooper()) }
+    private var binding: T? = null
+
+    private val bindMethod = bindingClass.getMethod("bind", View::class.java)
+
+    init {
+        fragment.viewLifecycleOwnerLiveData.observe(fragment) { viewLifecycleOwner ->
+            viewLifecycleOwner.lifecycle.addObserver(object : LifecycleObserver {
+                @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
+                fun onDestroy() {
+                    // Lifecycle listeners are called before onDestroyView in a Fragment.
+                    // However, we want views to be able to use bindings in onDestroyView
+                    // to do cleanup so we clear the reference one frame later.
+                    clearBindingHandler.post { binding = null }
+                }
+            })
+        }
+    }
+
+    @Suppress("UNCHECKED_CAST")
+    override fun getValue(thisRef: Fragment, property: KProperty<*>): T {
+        binding?.let { return it }
+
+        val lifecycle = fragment.viewLifecycleOwner.lifecycle
+        if (!lifecycle.currentState.isAtLeast(Lifecycle.State.INITIALIZED)) {
+            error("Cannot access view bindings. View lifecycle is ${lifecycle.currentState}!")
+        }
+
+        binding = bindMethod.invoke(null, thisRef.requireView()) as T
+        return binding!!
+    }
+}
diff --git a/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/utils/MvRxViewModel.kt b/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/utils/MvRxViewModel.kt
new file mode 100644
index 0000000..bcc1883
--- /dev/null
+++ b/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/utils/MvRxViewModel.kt
@@ -0,0 +1,28 @@
+package com.airbnb.lottie.samples.utils
+
+import androidx.lifecycle.viewModelScope
+import com.airbnb.lottie.samples.BuildConfig
+import com.airbnb.mvrx.*
+import kotlinx.coroutines.*
+
+abstract class MvRxViewModel<S : MvRxState>(initialState: S) : BaseMvRxViewModel<S>(initialState, BuildConfig.DEBUG) {
+    /**
+     * This uses [Dispatchers.Main.immediate] by default to mimic [viewModelScope].
+     */
+    fun <T : Any?> (suspend () -> T).execute(
+            dispatcher: CoroutineDispatcher = Dispatchers.Main.immediate,
+            reducer: S.(Async<T>) -> S
+    ): Job {
+        setState { reducer(Loading()) }
+        return viewModelScope.launch(dispatcher) {
+            try {
+                val result = invoke()
+                setState { reducer(Success(result)) }
+            } catch (e: CancellationException) {
+                throw e
+            } catch (e: Exception) {
+                setState { reducer(Fail(e)) }
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/TypeExtensions.kt b/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/utils/TypeExtensions.kt
similarity index 98%
rename from LottieSample/src/main/kotlin/com/airbnb/lottie/samples/TypeExtensions.kt
rename to LottieSample/src/main/kotlin/com/airbnb/lottie/samples/utils/TypeExtensions.kt
index 2a72e7f..cbf297c 100644
--- a/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/TypeExtensions.kt
+++ b/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/utils/TypeExtensions.kt
@@ -1,4 +1,4 @@
-package com.airbnb.lottie.samples
+package com.airbnb.lottie.samples.utils
 
 import android.app.Activity
 import android.content.Context
diff --git a/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/utils/ViewViewBindingDelegate.kt b/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/utils/ViewViewBindingDelegate.kt
new file mode 100644
index 0000000..49123a2
--- /dev/null
+++ b/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/utils/ViewViewBindingDelegate.kt
@@ -0,0 +1,37 @@
+package com.airbnb.lottie.samples.utils
+
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import androidx.viewbinding.ViewBinding
+import kotlin.properties.ReadOnlyProperty
+import kotlin.reflect.KProperty
+
+/**
+ * Create bindings for a view similar to bindView.
+ *
+ * To use, just call:
+ * private val binding: FHomeWorkoutDetailsBinding by viewBinding()
+ * with your binding class and access it as you normally would.
+ */
+inline fun <reified T : ViewBinding> ViewGroup.viewBinding() = ViewBindingDelegate(T::class.java, this)
+
+class ViewBindingDelegate<T : ViewBinding>(
+        private val bindingClass: Class<T>,
+        val view: ViewGroup
+) : ReadOnlyProperty<ViewGroup, T> {
+    private var binding: T? = null
+
+    override fun getValue(thisRef: ViewGroup, property: KProperty<*>): T {
+        binding?.let { return it }
+
+        @Suppress("UNCHECKED_CAST")
+        binding = try {
+            val inflateMethod = bindingClass.getMethod("inflate", LayoutInflater::class.java, ViewGroup::class.java)
+            inflateMethod.invoke(null, LayoutInflater.from(thisRef.context), thisRef)
+        } catch (e: NoSuchMethodException) {
+            val inflateMethod = bindingClass.getMethod("inflate", LayoutInflater::class.java, ViewGroup::class.java, Boolean::class.java)
+            inflateMethod.invoke(null, LayoutInflater.from(thisRef.context), thisRef, true) as T
+        } as T
+        return binding!!
+    }
+}
\ No newline at end of file
diff --git a/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/views/AnimationItemView.kt b/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/views/AnimationItemView.kt
index e94c588..c372e09 100644
--- a/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/views/AnimationItemView.kt
+++ b/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/views/AnimationItemView.kt
@@ -8,9 +8,9 @@
 import com.airbnb.epoxy.ModelView
 import com.airbnb.epoxy.TextProp
 import com.airbnb.lottie.samples.R
-import com.airbnb.lottie.samples.inflate
-import com.airbnb.lottie.samples.setImageUrl
-import kotlinx.android.synthetic.main.item_view_showcase_animation.view.*
+import com.airbnb.lottie.samples.databinding.ItemViewShowcaseAnimationBinding
+import com.airbnb.lottie.samples.utils.setImageUrl
+import com.airbnb.lottie.samples.utils.viewBinding
 
 @ModelView(autoLayout = ModelView.Size.MATCH_WIDTH_WRAP_HEIGHT)
 class AnimationItemView @JvmOverloads constructor(
@@ -18,33 +18,30 @@
         attrs: AttributeSet? = null,
         defStyleAttr: Int = 0
 ) : FrameLayout(context, attrs, defStyleAttr) {
-
-    init {
-        inflate(R.layout.item_view_showcase_animation)
-    }
+    private val binding: ItemViewShowcaseAnimationBinding by viewBinding()
 
     @ModelProp
     fun setPreviewUrl(url: String?) {
-        imageView.setImageUrl(url)
+        binding.imageView.setImageUrl(url)
     }
 
     @TextProp
     fun setTitle(title: CharSequence?) {
-        titleView.text = title
+        binding.titleView.text = title
     }
 
     @ModelProp
     fun setPreviewBackgroundColor(@ColorInt bgColor: Int?) {
         if (bgColor == null) {
-            imageView.setBackgroundResource(R.color.loading_placeholder)
-            imageView.setImageDrawable(null)
+            binding.imageView.setBackgroundResource(R.color.loading_placeholder)
+            binding.imageView.setImageDrawable(null)
         } else {
-            imageView.setBackgroundColor(bgColor)
+            binding.imageView.setBackgroundColor(bgColor)
         }
     }
 
     @ModelProp(options = [ModelProp.Option.DoNotHash])
     override fun setOnClickListener(l: OnClickListener?) {
-        container.setOnClickListener(l)
+        binding.container.setOnClickListener(l)
     }
 }
\ No newline at end of file
diff --git a/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/views/BottomSheetItemView.kt b/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/views/BottomSheetItemView.kt
index 6dc4352..4986ea1 100644
--- a/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/views/BottomSheetItemView.kt
+++ b/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/views/BottomSheetItemView.kt
@@ -8,9 +8,8 @@
 import androidx.core.view.isVisible
 import com.airbnb.epoxy.ModelProp
 import com.airbnb.epoxy.ModelView
-import com.airbnb.lottie.samples.R
-import com.airbnb.lottie.samples.inflate
-import kotlinx.android.synthetic.main.item_view_bottom_sheet.view.*
+import com.airbnb.lottie.samples.databinding.ItemViewBottomSheetBinding
+import com.airbnb.lottie.samples.utils.viewBinding
 
 @ModelView(autoLayout = ModelView.Size.MATCH_WIDTH_WRAP_HEIGHT)
 class BottomSheetItemView @JvmOverloads constructor(
@@ -18,16 +17,13 @@
         attrs: AttributeSet? = null,
         defStyleAttr: Int = 0
 ) : FrameLayout(context, attrs, defStyleAttr) {
-
-    init {
-        inflate(R.layout.item_view_bottom_sheet)
-    }
+    private val binding: ItemViewBottomSheetBinding by viewBinding()
 
     @SuppressLint("SetTextI18n")
     fun set(left: String, right: String? = null) {
-        leftTextView.text = left
-        rightTextView.isVisible = !TextUtils.isEmpty(right)
-        rightTextView.text = right
+        binding.leftTextView.text = left
+        binding.rightTextView.isVisible = !TextUtils.isEmpty(right)
+        binding.rightTextView.text = right
     }
 
     @ModelProp
diff --git a/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/views/ControlBarItemToggleView.kt b/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/views/ControlBarItemToggleView.kt
index 60a1941..0b2d990 100644
--- a/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/views/ControlBarItemToggleView.kt
+++ b/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/views/ControlBarItemToggleView.kt
@@ -2,27 +2,28 @@
 
 import android.content.Context
 import android.graphics.Color
-import androidx.annotation.DrawableRes
-import androidx.core.content.ContextCompat
-import androidx.core.graphics.drawable.DrawableCompat
 import android.util.AttributeSet
 import android.view.View
 import android.widget.ImageView
 import android.widget.LinearLayout
+import androidx.annotation.DrawableRes
+import androidx.core.content.ContextCompat
+import androidx.core.graphics.drawable.DrawableCompat
 import androidx.core.view.isVisible
 import androidx.core.view.setPadding
 import com.airbnb.lottie.samples.R
-import com.airbnb.lottie.samples.getText
-import kotlinx.android.synthetic.main.item_view_control_bar.view.*
+import com.airbnb.lottie.samples.databinding.ItemViewControlBarBinding
+import com.airbnb.lottie.samples.utils.getText
+import com.airbnb.lottie.samples.utils.viewBinding
 
 class ControlBarItemToggleView @JvmOverloads constructor(
         context: Context,
         attrs: AttributeSet? = null,
         defStyleAttr: Int = 0
 ) : LinearLayout(context, attrs, defStyleAttr) {
+    private val binding: ItemViewControlBarBinding by viewBinding()
 
     init {
-        inflate(context, R.layout.item_view_control_bar, this)
         orientation = HORIZONTAL
         setBackgroundResource(R.drawable.control_bar_item_view_background)
         setPadding(resources.getDimensionPixelSize(R.dimen.control_bar_button_padding))
@@ -31,14 +32,14 @@
 
             val textRes = typedArray.getResourceId(R.styleable.ControlBarItemToggleView_text, 0)
             if (textRes != 0) {
-                textView.text = getText(textRes)
+                binding.textView.text = getText(textRes)
             }
 
             val drawableRes = typedArray.getResourceId(R.styleable.ControlBarItemToggleView_src, 0)
             if (drawableRes == 0) {
-                imageView.isVisible = false
+                binding.imageView.isVisible = false
             } else {
-                imageView.setImageResource(drawableRes)
+                binding.imageView.setImageResource(drawableRes)
             }
 
             typedArray.recycle()
@@ -55,14 +56,14 @@
         }
     }
 
-    fun getText() = textView.text.toString()
+    fun getText() = binding.textView.text.toString()
 
     fun setText(text: String) {
-        textView.text = text
+        binding.textView.text = text
     }
 
     fun setImageResource(@DrawableRes drawableRes: Int) {
-        imageView.setImageResource(drawableRes)
-        childDrawableStateChanged(imageView)
+        binding.imageView.setImageResource(drawableRes)
+        childDrawableStateChanged(binding.imageView)
     }
 }
\ No newline at end of file
diff --git a/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/views/FilmStripView.kt b/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/views/FilmStripView.kt
index bfd045b..99bb21a 100644
--- a/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/views/FilmStripView.kt
+++ b/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/views/FilmStripView.kt
@@ -2,28 +2,24 @@
 
 import android.content.Context
 import android.util.AttributeSet
-import android.view.ViewGroup
 import android.widget.FrameLayout
 import androidx.core.view.children
 import com.airbnb.lottie.FontAssetDelegate
 import com.airbnb.lottie.ImageAssetDelegate
 import com.airbnb.lottie.LottieAnimationView
 import com.airbnb.lottie.LottieComposition
-import com.airbnb.lottie.samples.R
-import com.airbnb.lottie.samples.inflate
+import com.airbnb.lottie.samples.databinding.FilmStripViewBinding
+import com.airbnb.lottie.samples.utils.viewBinding
 
 class FilmStripView @JvmOverloads constructor(
         context: Context,
         attrs: AttributeSet? = null,
         defStyleAttr: Int = 0
 ) : FrameLayout(context, attrs, defStyleAttr) {
+    private val binding: FilmStripViewBinding by viewBinding()
 
     private val animationViews by lazy {
-        findViewById<ViewGroup>(R.id.grid_layout).children.filterIsInstance(LottieAnimationView::class.java)
-    }
-
-    init {
-        inflate(R.layout.film_strip_view)
+        binding.gridLayout.children.filterIsInstance(LottieAnimationView::class.java)
     }
 
     fun setComposition(composition: LottieComposition) {
diff --git a/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/views/ListingCard.kt b/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/views/ListingCard.kt
index 8191327..01fc7a8 100644
--- a/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/views/ListingCard.kt
+++ b/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/views/ListingCard.kt
@@ -2,25 +2,44 @@
 
 import android.content.Context
 import android.util.AttributeSet
-import android.view.View
 import android.widget.FrameLayout
 import com.airbnb.epoxy.CallbackProp
 import com.airbnb.epoxy.ModelProp
 import com.airbnb.epoxy.ModelView
-import com.airbnb.lottie.samples.R
-import kotlinx.android.synthetic.main.listing_card.view.*
+import com.airbnb.epoxy.OnViewRecycled
+import com.airbnb.lottie.samples.databinding.ListingCardBinding
+import com.airbnb.lottie.samples.utils.viewBinding
 
 @ModelView(autoLayout = ModelView.Size.MATCH_WIDTH_WRAP_HEIGHT)
 class ListingCard @JvmOverloads constructor(
-        context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
+        context: Context,
+        attrs: AttributeSet? = null,
+        defStyleAttr: Int = 0
 ) : FrameLayout(context, attrs, defStyleAttr) {
-
-    init {
-        inflate(context, R.layout.listing_card, this)
-    }
+    private val binding: ListingCardBinding by viewBinding()
 
     @CallbackProp
-    fun setClickListener(listener: View.OnClickListener?) {
-        wishListIcon.setOnClickListener(listener)
+    fun onToggled(listener: ((isWishListed: Boolean) -> Unit)?) {
+        binding.wishListIcon.setOnClickListener(when (listener) {
+            null -> null
+            else -> { _ ->
+                listener(binding.wishListIcon.progress == 0f)
+            }
+        })
+    }
+
+    @ModelProp
+    fun isWishListed(isWishListed: Boolean) {
+        val targetProgress = if (isWishListed) 1f else 0f
+        binding.wishListIcon.speed = if (isWishListed) 1f else -1f
+        if (binding.wishListIcon.progress != targetProgress) {
+            binding.wishListIcon.playAnimation()
+        }
+    }
+
+    @OnViewRecycled
+    fun onRecycled() {
+        binding.wishListIcon.pauseAnimation()
+        binding.wishListIcon.progress = 0f
     }
 }
\ No newline at end of file
diff --git a/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/views/LoadingView.kt b/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/views/LoadingView.kt
index 0d4fd97..37ee501 100644
--- a/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/views/LoadingView.kt
+++ b/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/views/LoadingView.kt
@@ -5,7 +5,7 @@
 import android.widget.FrameLayout
 import com.airbnb.epoxy.ModelView
 import com.airbnb.lottie.samples.R
-import com.airbnb.lottie.samples.inflate
+import com.airbnb.lottie.samples.utils.inflate
 
 @ModelView(autoLayout = ModelView.Size.MATCH_WIDTH_WRAP_HEIGHT)
 class LoadingView @JvmOverloads constructor(
diff --git a/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/views/LottiefilesTabBar.kt b/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/views/LottiefilesTabBar.kt
index 35b0eff..f2ab393 100644
--- a/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/views/LottiefilesTabBar.kt
+++ b/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/views/LottiefilesTabBar.kt
@@ -7,9 +7,8 @@
 import com.airbnb.epoxy.ModelProp
 import com.airbnb.epoxy.ModelView
 import com.airbnb.lottie.samples.LottiefilesMode
-import com.airbnb.lottie.samples.R
-import com.airbnb.lottie.samples.inflate
-import kotlinx.android.synthetic.main.lottiefiles_tab_bar.view.*
+import com.airbnb.lottie.samples.databinding.LottiefilesTabBarBinding
+import com.airbnb.lottie.samples.utils.viewBinding
 
 @ModelView(autoLayout = ModelView.Size.MATCH_WIDTH_WRAP_HEIGHT)
 class LottiefilesTabBar @JvmOverloads constructor(
@@ -17,30 +16,27 @@
         attrs: AttributeSet? = null,
         defStyleAttr: Int = 0
 ) : LinearLayout(context, attrs, defStyleAttr) {
-
-    init {
-        inflate(R.layout.lottiefiles_tab_bar)
-    }
+    private val binding: LottiefilesTabBarBinding by viewBinding()
 
     @ModelProp
     fun setMode(mode: LottiefilesMode) {
-        popularView.isActivated = mode == LottiefilesMode.Popular
-        recentView.isActivated = mode == LottiefilesMode.Recent
-        searchView.isActivated = mode == LottiefilesMode.Search
+        binding.popularView.isActivated = mode == LottiefilesMode.Popular
+        binding.recentView.isActivated = mode == LottiefilesMode.Recent
+        binding.searchView.isActivated = mode == LottiefilesMode.Search
     }
 
     @ModelProp(options = [ModelProp.Option.DoNotHash])
     fun setPopularClickListener(listener: View.OnClickListener) {
-        popularView.setOnClickListener(listener)
+        binding.popularView.setOnClickListener(listener)
     }
 
     @ModelProp(options = [ModelProp.Option.DoNotHash])
-    fun setRecentClickListener(listener: View.OnClickListener) {
-        recentView.setOnClickListener(listener)
+    fun setRecentClickListener(listener: OnClickListener) {
+        binding.recentView.setOnClickListener(listener)
     }
 
     @ModelProp(options = [ModelProp.Option.DoNotHash])
-    fun setSearchClickListener(listener: View.OnClickListener) {
-        searchView.setOnClickListener(listener)
+    fun setSearchClickListener(listener: OnClickListener) {
+        binding.searchView.setOnClickListener(listener)
     }
 }
\ No newline at end of file
diff --git a/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/views/Marquee.kt b/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/views/Marquee.kt
index f7a4908..350c504 100644
--- a/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/views/Marquee.kt
+++ b/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/views/Marquee.kt
@@ -6,10 +6,10 @@
 import com.airbnb.epoxy.ModelView
 import com.airbnb.epoxy.TextProp
 import com.airbnb.lottie.samples.R
-import com.airbnb.lottie.samples.getText
-import com.airbnb.lottie.samples.inflate
-import com.airbnb.lottie.samples.setVisibleIf
-import kotlinx.android.synthetic.main.marquee.view.*
+import com.airbnb.lottie.samples.databinding.MarqueeBinding
+import com.airbnb.lottie.samples.utils.getText
+import com.airbnb.lottie.samples.utils.setVisibleIf
+import com.airbnb.lottie.samples.utils.viewBinding
 
 @ModelView(autoLayout = ModelView.Size.MATCH_WIDTH_WRAP_HEIGHT)
 class Marquee @JvmOverloads constructor(
@@ -17,9 +17,9 @@
         attrs: AttributeSet? = null,
         defStyleAttr: Int = 0
 ) : LinearLayout(context, attrs, defStyleAttr) {
+    private val binding: MarqueeBinding by viewBinding()
 
     init {
-        inflate(R.layout.marquee)
         orientation = VERTICAL
         attrs?.let {
             val typedArray = context.obtainStyledAttributes(it, R.styleable.Marquee, 0, 0)
@@ -40,12 +40,12 @@
 
     @TextProp
     fun setTitle(title: CharSequence) {
-        titleView.text = title
+        binding.titleView.text = title
     }
 
     @TextProp
     fun setSubtitle(subtitle: CharSequence?) {
-        subtitleView.text = subtitle
-        subtitleView.setVisibleIf(!subtitle.isNullOrEmpty())
+        binding.subtitleView.text = subtitle
+        binding.subtitleView.setVisibleIf(!subtitle.isNullOrEmpty())
     }
 }
\ No newline at end of file
diff --git a/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/views/SearchInputItemView.kt b/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/views/SearchInputItemView.kt
index 0037c64..d384a60 100644
--- a/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/views/SearchInputItemView.kt
+++ b/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/views/SearchInputItemView.kt
@@ -2,44 +2,26 @@
 
 import android.content.Context
 import android.util.AttributeSet
-import android.view.KeyEvent
-import android.view.inputmethod.EditorInfo
-import android.view.inputmethod.InputMethodManager
 import android.widget.FrameLayout
-import androidx.core.content.getSystemService
-import com.airbnb.epoxy.ModelProp
-import com.airbnb.epoxy.ModelView
-import com.airbnb.lottie.samples.R
-import com.airbnb.lottie.samples.inflate
-import kotlinx.android.synthetic.main.item_view_search_input.view.*
+import androidx.core.widget.doAfterTextChanged
+import com.airbnb.lottie.samples.databinding.ItemViewSearchInputBinding
+import com.airbnb.lottie.samples.utils.viewBinding
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
 
-@ModelView(autoLayout = ModelView.Size.MATCH_WIDTH_WRAP_HEIGHT)
 class SearchInputItemView @JvmOverloads constructor(
         context: Context,
         attrs: AttributeSet? = null,
         defStyleAttr: Int = 0
 ) : FrameLayout(context, attrs, defStyleAttr) {
+    private val binding: ItemViewSearchInputBinding by viewBinding()
+
+    private val _query = MutableStateFlow("")
+    val query: StateFlow<String> = _query
 
     init {
-        inflate(R.layout.item_view_search_input)
-        searchEditText.setOnEditorActionListener { _, actionId, event ->
-            if (actionId == EditorInfo.IME_ACTION_SEARCH && event?.action == KeyEvent.ACTION_DOWN) {
-                searchButton.callOnClick()
-                return@setOnEditorActionListener true
-            } else if (event?.keyCode == KeyEvent.KEYCODE_ENTER && event.action == KeyEvent.ACTION_DOWN) {
-                searchButton.callOnClick()
-                return@setOnEditorActionListener true
-            }
-            return@setOnEditorActionListener false
-        }
-    }
-
-    @ModelProp(options = [ModelProp.Option.DoNotHash])
-    fun setSearchClickListener(listener: (String) -> Unit) {
-        searchButton.setOnClickListener {
-            val inputMethodManager = context.getSystemService<InputMethodManager>()!!
-            inputMethodManager.hideSoftInputFromWindow(windowToken, 0)
-            listener(searchEditText.text.toString())
+        binding.searchEditText.doAfterTextChanged { text ->
+            _query.value = text?.toString() ?: ""
         }
     }
 }
\ No newline at end of file
diff --git a/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/views/SectionHeaderView.kt b/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/views/SectionHeaderView.kt
index 47a4806..a9dc8fe 100644
--- a/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/views/SectionHeaderView.kt
+++ b/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/views/SectionHeaderView.kt
@@ -2,14 +2,12 @@
 
 import android.content.Context
 import android.util.AttributeSet
-import android.view.View
 import android.widget.FrameLayout
 import com.airbnb.epoxy.ModelProp
 import com.airbnb.epoxy.ModelView
 import com.airbnb.epoxy.TextProp
-import com.airbnb.lottie.samples.R
-import com.airbnb.lottie.samples.inflate
-import kotlinx.android.synthetic.main.marquee.view.*
+import com.airbnb.lottie.samples.databinding.SectionHeaderViewBinding
+import com.airbnb.lottie.samples.utils.viewBinding
 
 @ModelView(autoLayout = ModelView.Size.MATCH_WIDTH_WRAP_HEIGHT)
 class SectionHeaderView @JvmOverloads constructor(
@@ -17,14 +15,11 @@
         attrs: AttributeSet? = null,
         defStyleAttr: Int = 0
 ) : FrameLayout(context, attrs, defStyleAttr) {
-
-    init {
-        inflate(R.layout.section_header_view)
-    }
+    private val binding: SectionHeaderViewBinding by viewBinding()
 
     @TextProp
     fun setTitle(title: CharSequence) {
-        titleView.text = title
+        binding.titleView.text = title
     }
 
     @ModelProp(options = [ModelProp.Option.DoNotHash])
diff --git a/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/views/ShowcaseDemoItemView.kt b/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/views/ShowcaseDemoItemView.kt
index 1b27347..7f2b743 100644
--- a/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/views/ShowcaseDemoItemView.kt
+++ b/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/views/ShowcaseDemoItemView.kt
@@ -5,10 +5,9 @@
 import android.widget.FrameLayout
 import com.airbnb.epoxy.ModelProp
 import com.airbnb.epoxy.ModelView
-import com.airbnb.lottie.samples.R
-import com.airbnb.lottie.samples.inflate
+import com.airbnb.lottie.samples.databinding.ItemViewShowcaseDemoBinding
 import com.airbnb.lottie.samples.model.ShowcaseItem
-import kotlinx.android.synthetic.main.item_view_showcase_demo.view.*
+import com.airbnb.lottie.samples.utils.viewBinding
 
 @ModelView(autoLayout = ModelView.Size.WRAP_WIDTH_WRAP_HEIGHT)
 class ShowcaseDemoItemView @JvmOverloads constructor(
@@ -16,17 +15,14 @@
         attrs: AttributeSet? = null,
         defStyleAttr: Int = 0
 ) : FrameLayout(context, attrs, defStyleAttr) {
-
-    init {
-        inflate(R.layout.item_view_showcase_demo)
-    }
+    private val binding: ItemViewShowcaseDemoBinding by viewBinding()
 
     @ModelProp
     fun setShowcaseItem(item: ShowcaseItem) {
-        imageView.setImageResource(item.drawableRes)
+        binding.imageView.setImageResource(item.drawableRes)
 
-        titleView.text = resources.getText(item.titleRes)
+        binding.titleView.text = resources.getText(item.titleRes)
 
-        cardView.setOnClickListener({ item.clickListener() })
+        binding.cardView.setOnClickListener { item.clickListener() }
     }
 }
\ No newline at end of file
diff --git a/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/views/TabBarItemView.kt b/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/views/TabBarItemView.kt
index a8b85c7..9151da6 100644
--- a/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/views/TabBarItemView.kt
+++ b/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/views/TabBarItemView.kt
@@ -3,30 +3,25 @@
 import android.content.Context
 import android.util.AttributeSet
 import android.widget.LinearLayout
+import androidx.core.content.withStyledAttributes
 import com.airbnb.lottie.samples.R
-import com.airbnb.lottie.samples.getText
-import com.airbnb.lottie.samples.inflate
-import kotlinx.android.synthetic.main.tab_item.view.*
+import com.airbnb.lottie.samples.databinding.TabItemBinding
+import com.airbnb.lottie.samples.utils.viewBinding
 
 class TabBarItemView @JvmOverloads constructor(
         context: Context,
         attrs: AttributeSet? = null,
         defStyleAttr: Int = 0
 ) : LinearLayout(context, attrs, defStyleAttr) {
+    private val binding: TabItemBinding by viewBinding()
 
     init {
-        inflate(R.layout.tab_item)
         orientation = VERTICAL
 
-        attrs?.let {
-            val ta = context.obtainStyledAttributes(it, R.styleable.TabBarItemView, 0, 0)
-
-            val titleRes = ta.getResourceId(R.styleable.TabBarItemView_titleText, 0)
-            if (titleRes != 0) {
-                titleView.text = getText(titleRes)
+        context.withStyledAttributes(attrs, R.styleable.TabBarItemView) {
+            if (hasValue(R.styleable.TabBarItemView_titleText)) {
+                binding.titleView.text = getText(R.styleable.TabBarItemView_titleText)
             }
-
-            ta.recycle()
         }
     }
 }
\ No newline at end of file
diff --git a/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/views/WishListIconView.kt b/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/views/WishListIconView.kt
index a741cd9..1a78533 100644
--- a/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/views/WishListIconView.kt
+++ b/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/views/WishListIconView.kt
@@ -2,10 +2,7 @@
 
 import android.content.Context
 import android.util.AttributeSet
-import com.airbnb.epoxy.ModelProp
-import com.airbnb.epoxy.ModelView
 import com.airbnb.lottie.LottieAnimationView
-import kotlinx.android.synthetic.main.listing_card.view.*
 
 class WishListIconView @JvmOverloads constructor(
         context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
diff --git a/LottieSample/src/main/res/layout/activity_test_color_filter.xml b/LottieSample/src/main/res/layout/activity_test_color_filter.xml
deleted file mode 100644
index b0e7efe..0000000
--- a/LottieSample/src/main/res/layout/activity_test_color_filter.xml
+++ /dev/null
@@ -1,26 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<LinearLayout
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:app="http://schemas.android.com/apk/res-auto"
-    android:layout_width="match_parent"
-    android:layout_height="match_parent"
-    android:orientation="vertical">
-
-    <com.airbnb.lottie.LottieAnimationView
-        android:id="@+id/yellow_color_filter"
-        android:layout_width="50dp"
-        android:layout_height="50dp"
-        android:background="#ffffff"
-        app:lottie_colorFilter="#ffff00"
-        app:lottie_rawRes="@raw/hamburger_arrow"/>
-
-
-    <com.airbnb.lottie.LottieAnimationView
-        android:id="@+id/null_color_filter"
-        android:layout_width="50dp"
-        android:layout_height="50dp"
-        android:background="#ffffff"
-        app:lottie_colorFilter="@null"
-        app:lottie_fileName="HamburgerArrow.json"/>
-
-</LinearLayout>
\ No newline at end of file
diff --git a/LottieSample/src/main/res/layout/fragment_base.xml b/LottieSample/src/main/res/layout/base_fragment.xml
similarity index 100%
rename from LottieSample/src/main/res/layout/fragment_base.xml
rename to LottieSample/src/main/res/layout/base_fragment.xml
diff --git a/LottieSample/src/main/res/layout/activity_bullseye.xml b/LottieSample/src/main/res/layout/bullseye_activity.xml
similarity index 100%
rename from LottieSample/src/main/res/layout/activity_bullseye.xml
rename to LottieSample/src/main/res/layout/bullseye_activity.xml
diff --git a/LottieSample/src/main/res/layout/fragment_choose_asset.xml b/LottieSample/src/main/res/layout/choose_asset_fragment.xml
similarity index 100%
rename from LottieSample/src/main/res/layout/fragment_choose_asset.xml
rename to LottieSample/src/main/res/layout/choose_asset_fragment.xml
diff --git a/LottieSample/src/main/res/layout/activity_dynamic.xml b/LottieSample/src/main/res/layout/dynamic_activity.xml
similarity index 100%
rename from LottieSample/src/main/res/layout/activity_dynamic.xml
rename to LottieSample/src/main/res/layout/dynamic_activity.xml
diff --git a/LottieSample/src/main/res/layout/activity_dynamic_text.xml b/LottieSample/src/main/res/layout/dynamic_text_activity.xml
similarity index 100%
rename from LottieSample/src/main/res/layout/activity_dynamic_text.xml
rename to LottieSample/src/main/res/layout/dynamic_text_activity.xml
diff --git a/LottieSample/src/main/res/layout/activity_empty.xml b/LottieSample/src/main/res/layout/empty_activity.xml
similarity index 100%
rename from LottieSample/src/main/res/layout/activity_empty.xml
rename to LottieSample/src/main/res/layout/empty_activity.xml
diff --git a/LottieSample/src/main/res/layout/fragment_empty.xml b/LottieSample/src/main/res/layout/empty_fragment.xml
similarity index 100%
rename from LottieSample/src/main/res/layout/fragment_empty.xml
rename to LottieSample/src/main/res/layout/empty_fragment.xml
diff --git a/LottieSample/src/main/res/layout/fragment_epoxy_recycler_view.xml b/LottieSample/src/main/res/layout/epoxy_recycler_view_fragment.xml
similarity index 100%
rename from LottieSample/src/main/res/layout/fragment_epoxy_recycler_view.xml
rename to LottieSample/src/main/res/layout/epoxy_recycler_view_fragment.xml
diff --git a/LottieSample/src/main/res/layout/activity_film_strip_snapshots.xml b/LottieSample/src/main/res/layout/film_strip_snapshots_activity.xml
similarity index 100%
rename from LottieSample/src/main/res/layout/activity_film_strip_snapshots.xml
rename to LottieSample/src/main/res/layout/film_strip_snapshots_activity.xml
diff --git a/LottieSample/src/main/res/layout/film_strip_view.xml b/LottieSample/src/main/res/layout/film_strip_view.xml
index ddc889a..a019ab2 100644
--- a/LottieSample/src/main/res/layout/film_strip_view.xml
+++ b/LottieSample/src/main/res/layout/film_strip_view.xml
@@ -6,127 +6,127 @@
     android:layout_gravity="center"
     android:columnCount="5">
 
-    <com.airbnb.lottie.samples.NoCacheLottieAnimationView
+    <com.airbnb.lottie.samples.testing.NoCacheLottieAnimationView
         android:id="@+id/animation_1"
         android:layout_width="@dimen/film_strip_size"
         android:layout_height="@dimen/film_strip_size" />
 
-    <com.airbnb.lottie.samples.NoCacheLottieAnimationView
+    <com.airbnb.lottie.samples.testing.NoCacheLottieAnimationView
         android:id="@+id/animation_2"
         android:layout_width="@dimen/film_strip_size"
         android:layout_height="@dimen/film_strip_size" />
 
-    <com.airbnb.lottie.samples.NoCacheLottieAnimationView
+    <com.airbnb.lottie.samples.testing.NoCacheLottieAnimationView
         android:id="@+id/animation_3"
         android:layout_width="@dimen/film_strip_size"
         android:layout_height="@dimen/film_strip_size" />
 
-    <com.airbnb.lottie.samples.NoCacheLottieAnimationView
+    <com.airbnb.lottie.samples.testing.NoCacheLottieAnimationView
         android:id="@+id/animation_4"
         android:layout_width="@dimen/film_strip_size"
         android:layout_height="@dimen/film_strip_size" />
 
-    <com.airbnb.lottie.samples.NoCacheLottieAnimationView
+    <com.airbnb.lottie.samples.testing.NoCacheLottieAnimationView
         android:id="@+id/animation_5"
         android:layout_width="@dimen/film_strip_size"
         android:layout_height="@dimen/film_strip_size" />
 
-    <com.airbnb.lottie.samples.NoCacheLottieAnimationView
+    <com.airbnb.lottie.samples.testing.NoCacheLottieAnimationView
         android:id="@+id/animation_6"
         android:layout_width="@dimen/film_strip_size"
         android:layout_height="@dimen/film_strip_size" />
 
-    <com.airbnb.lottie.samples.NoCacheLottieAnimationView
+    <com.airbnb.lottie.samples.testing.NoCacheLottieAnimationView
         android:id="@+id/animation_7"
         android:layout_width="@dimen/film_strip_size"
         android:layout_height="@dimen/film_strip_size" />
 
-    <com.airbnb.lottie.samples.NoCacheLottieAnimationView
+    <com.airbnb.lottie.samples.testing.NoCacheLottieAnimationView
         android:id="@+id/animation_8"
         android:layout_width="@dimen/film_strip_size"
         android:layout_height="@dimen/film_strip_size" />
 
-    <com.airbnb.lottie.samples.NoCacheLottieAnimationView
+    <com.airbnb.lottie.samples.testing.NoCacheLottieAnimationView
         android:id="@+id/animation_9"
         android:layout_width="@dimen/film_strip_size"
         android:layout_height="@dimen/film_strip_size" />
 
-    <com.airbnb.lottie.samples.NoCacheLottieAnimationView
+    <com.airbnb.lottie.samples.testing.NoCacheLottieAnimationView
         android:id="@+id/animation_10"
         android:layout_width="@dimen/film_strip_size"
         android:layout_height="@dimen/film_strip_size" />
 
-    <com.airbnb.lottie.samples.NoCacheLottieAnimationView
+    <com.airbnb.lottie.samples.testing.NoCacheLottieAnimationView
         android:id="@+id/animation_11"
         android:layout_width="@dimen/film_strip_size"
         android:layout_height="@dimen/film_strip_size" />
 
-    <com.airbnb.lottie.samples.NoCacheLottieAnimationView
+    <com.airbnb.lottie.samples.testing.NoCacheLottieAnimationView
         android:id="@+id/animation_12"
         android:layout_width="@dimen/film_strip_size"
         android:layout_height="@dimen/film_strip_size" />
 
-    <com.airbnb.lottie.samples.NoCacheLottieAnimationView
+    <com.airbnb.lottie.samples.testing.NoCacheLottieAnimationView
         android:id="@+id/animation_13"
         android:layout_width="@dimen/film_strip_size"
         android:layout_height="@dimen/film_strip_size" />
 
-    <com.airbnb.lottie.samples.NoCacheLottieAnimationView
+    <com.airbnb.lottie.samples.testing.NoCacheLottieAnimationView
         android:id="@+id/animation_14"
         android:layout_width="@dimen/film_strip_size"
         android:layout_height="@dimen/film_strip_size" />
 
-    <com.airbnb.lottie.samples.NoCacheLottieAnimationView
+    <com.airbnb.lottie.samples.testing.NoCacheLottieAnimationView
         android:id="@+id/animation_15"
         android:layout_width="@dimen/film_strip_size"
         android:layout_height="@dimen/film_strip_size" />
 
-    <com.airbnb.lottie.samples.NoCacheLottieAnimationView
+    <com.airbnb.lottie.samples.testing.NoCacheLottieAnimationView
         android:id="@+id/animation_16"
         android:layout_width="@dimen/film_strip_size"
         android:layout_height="@dimen/film_strip_size" />
 
-    <com.airbnb.lottie.samples.NoCacheLottieAnimationView
+    <com.airbnb.lottie.samples.testing.NoCacheLottieAnimationView
         android:id="@+id/animation_17"
         android:layout_width="@dimen/film_strip_size"
         android:layout_height="@dimen/film_strip_size" />
 
-    <com.airbnb.lottie.samples.NoCacheLottieAnimationView
+    <com.airbnb.lottie.samples.testing.NoCacheLottieAnimationView
         android:id="@+id/animation_18"
         android:layout_width="@dimen/film_strip_size"
         android:layout_height="@dimen/film_strip_size" />
 
-    <com.airbnb.lottie.samples.NoCacheLottieAnimationView
+    <com.airbnb.lottie.samples.testing.NoCacheLottieAnimationView
         android:id="@+id/animation_19"
         android:layout_width="@dimen/film_strip_size"
         android:layout_height="@dimen/film_strip_size" />
 
-    <com.airbnb.lottie.samples.NoCacheLottieAnimationView
+    <com.airbnb.lottie.samples.testing.NoCacheLottieAnimationView
         android:id="@+id/animation_20"
         android:layout_width="@dimen/film_strip_size"
         android:layout_height="@dimen/film_strip_size" />
 
-    <com.airbnb.lottie.samples.NoCacheLottieAnimationView
+    <com.airbnb.lottie.samples.testing.NoCacheLottieAnimationView
         android:id="@+id/animation_21"
         android:layout_width="@dimen/film_strip_size"
         android:layout_height="@dimen/film_strip_size" />
 
-    <com.airbnb.lottie.samples.NoCacheLottieAnimationView
+    <com.airbnb.lottie.samples.testing.NoCacheLottieAnimationView
         android:id="@+id/animation_22"
         android:layout_width="@dimen/film_strip_size"
         android:layout_height="@dimen/film_strip_size" />
 
-    <com.airbnb.lottie.samples.NoCacheLottieAnimationView
+    <com.airbnb.lottie.samples.testing.NoCacheLottieAnimationView
         android:id="@+id/animation_23"
         android:layout_width="@dimen/film_strip_size"
         android:layout_height="@dimen/film_strip_size" />
 
-    <com.airbnb.lottie.samples.NoCacheLottieAnimationView
+    <com.airbnb.lottie.samples.testing.NoCacheLottieAnimationView
         android:id="@+id/animation_24"
         android:layout_width="@dimen/film_strip_size"
         android:layout_height="@dimen/film_strip_size" />
 
-    <com.airbnb.lottie.samples.NoCacheLottieAnimationView
+    <com.airbnb.lottie.samples.testing.NoCacheLottieAnimationView
         android:id="@+id/animation_25"
         android:layout_width="@dimen/film_strip_size"
         android:layout_height="@dimen/film_strip_size" />
diff --git a/LottieSample/src/main/res/layout/fragment_font.xml b/LottieSample/src/main/res/layout/font_fragment.xml
similarity index 100%
rename from LottieSample/src/main/res/layout/fragment_font.xml
rename to LottieSample/src/main/res/layout/font_fragment.xml
diff --git a/LottieSample/src/main/res/layout/fragment_full_screen.xml b/LottieSample/src/main/res/layout/fragment_full_screen.xml
deleted file mode 100644
index 12e3234..0000000
--- a/LottieSample/src/main/res/layout/fragment_full_screen.xml
+++ /dev/null
@@ -1,9 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<com.airbnb.lottie.LottieAnimationView xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:app="http://schemas.android.com/apk/res-auto"
-    android:layout_width="match_parent"
-    android:layout_height="match_parent"
-    android:scaleType="centerCrop"
-    app:lottie_rawRes="@raw/full_screen"
-    app:lottie_loop="true"
-    app:lottie_autoPlay="true" />
\ No newline at end of file
diff --git a/LottieSample/src/main/res/layout/fragment_player.xml b/LottieSample/src/main/res/layout/fragment_player.xml
deleted file mode 100644
index 468fe4e..0000000
--- a/LottieSample/src/main/res/layout/fragment_player.xml
+++ /dev/null
@@ -1,60 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<androidx.coordinatorlayout.widget.CoordinatorLayout
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:app="http://schemas.android.com/apk/res-auto"
-    android:id="@+id/coordinatorLayout"
-    android:layout_width="match_parent"
-    android:layout_height="match_parent"
-    android:background="@android:color/white">
-    <LinearLayout
-        android:id="@+id/container"
-        android:layout_width="match_parent"
-        android:layout_height="match_parent"
-        android:orientation="vertical">
-        <FrameLayout
-            android:id="@+id/animationContainer"
-            android:layout_width="match_parent"
-            android:layout_height="0dp"
-            android:layout_weight="1">
-
-            <com.airbnb.lottie.LottieAnimationView
-                android:id="@+id/animationView"
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content"
-                android:layout_gravity="center"
-                android:background="@drawable/outline"
-                app:lottie_autoPlay="true"/>
-
-            <ProgressBar
-                android:id="@+id/loadingView"
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content"
-                android:layout_gravity="center"/>
-
-            <View
-                android:layout_width="match_parent"
-                android:layout_height="@dimen/divider_height"
-                android:layout_gravity="bottom"
-                android:background="@color/divider"/>
-        </FrameLayout>
-
-        <include layout="@layout/control_bar_speed" />
-        <include layout="@layout/control_bar_scale" />
-        <include layout="@layout/control_bar_background_color" />
-        <include layout="@layout/control_bar_trim" />
-        <include layout="@layout/control_bar_player_controls" />
-        <include layout="@layout/control_bar" />
-    </LinearLayout>
-
-    <androidx.appcompat.widget.Toolbar
-        android:id="@+id/toolbar"
-        android:layout_width="match_parent"
-        android:layout_height="?attr/actionBarSize"
-        android:layout_gravity="top"
-        app:navigationIcon="@drawable/ic_nav_close_selector"
-        app:title=""/>
-
-    <include layout="@layout/bottom_sheet_render_times" />
-    <include layout="@layout/bottom_sheet_warnings" />
-    <include layout="@layout/bottom_sheet_key_paths" />
-</androidx.coordinatorlayout.widget.CoordinatorLayout>
\ No newline at end of file
diff --git a/LottieSample/src/main/res/layout/fragment_todo.xml b/LottieSample/src/main/res/layout/fragment_todo.xml
deleted file mode 100644
index e3c78e8..0000000
--- a/LottieSample/src/main/res/layout/fragment_todo.xml
+++ /dev/null
@@ -1,13 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<FrameLayout
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    android:layout_width="match_parent"
-    android:layout_height="match_parent">
-
-    <TextView
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:layout_gravity="center"
-        android:text="TODO" />
-
-</FrameLayout>
\ No newline at end of file
diff --git a/LottieSample/src/main/res/layout/item_view_search_input.xml b/LottieSample/src/main/res/layout/item_view_search_input.xml
index e731a42..b9c5e87 100644
--- a/LottieSample/src/main/res/layout/item_view_search_input.xml
+++ b/LottieSample/src/main/res/layout/item_view_search_input.xml
@@ -1,34 +1,18 @@
 <?xml version="1.0" encoding="utf-8"?>
 <merge xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:app="http://schemas.android.com/apk/res-auto"
     xmlns:tools="http://schemas.android.com/tools"
     tools:parentTag="android.widget.FrameLayout">
 
-    <LinearLayout
+    <EditText
+        android:id="@+id/searchEditText"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
         android:layout_marginLeft="16dp"
         android:layout_marginRight="16dp"
-        android:orientation="horizontal">
-
-        <EditText
-            android:id="@+id/searchEditText"
-            android:layout_width="0dp"
-            android:layout_height="wrap_content"
-            android:layout_weight="1"
-            android:backgroundTint="@color/divider"
-            android:hint="@string/search"
-            android:inputType="text"
-            android:imeOptions="actionSearch"
-            android:textColorHint="@color/text_color_placeholder"/>
-
-        <ImageButton
-            android:id="@+id/searchButton"
-            android:layout_width="24dp"
-            android:layout_height="24dp"
-            android:layout_gravity="center_vertical"
-            android:background="?attr/selectableItemBackgroundBorderless"
-            app:srcCompat="@drawable/ic_search"/>
-    </LinearLayout>
-
+        android:layout_weight="1"
+        android:backgroundTint="@color/divider"
+        android:hint="@string/search"
+        android:imeOptions="actionSearch"
+        android:inputType="text"
+        android:textColorHint="@color/text_color_placeholder" />
 </merge>
\ No newline at end of file
diff --git a/LottieSample/src/main/res/layout/activity_list.xml b/LottieSample/src/main/res/layout/list_activity.xml
similarity index 65%
rename from LottieSample/src/main/res/layout/activity_list.xml
rename to LottieSample/src/main/res/layout/list_activity.xml
index 7a3f48d..7293bf8 100644
--- a/LottieSample/src/main/res/layout/activity_list.xml
+++ b/LottieSample/src/main/res/layout/list_activity.xml
@@ -9,10 +9,4 @@
         android:layout_width="match_parent"
         android:layout_height="match_parent"/>
 
-    <androidx.appcompat.widget.Toolbar
-        android:id="@+id/toolbar"
-        android:layout_width="match_parent"
-        android:layout_height="?attr/actionBarSize"
-        app:navigationIcon="@drawable/ic_back_black"/>
-
 </FrameLayout>
\ No newline at end of file
diff --git a/LottieSample/src/main/res/layout/lottiefiles_fragment.xml b/LottieSample/src/main/res/layout/lottiefiles_fragment.xml
new file mode 100644
index 0000000..715791c
--- /dev/null
+++ b/LottieSample/src/main/res/layout/lottiefiles_fragment.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical">
+
+    <com.airbnb.lottie.samples.views.Marquee
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        app:subtitleText="@string/lottiefiles_airbnb"
+        app:titleText="@string/lottiefiles" />
+
+    <com.airbnb.lottie.samples.views.LottiefilesTabBar
+        android:id="@+id/tab_bar"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content" />
+
+    <com.airbnb.lottie.samples.views.SearchInputItemView
+        android:id="@+id/search_view"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content" />
+
+    <androidx.recyclerview.widget.RecyclerView
+        android:id="@+id/recyclerView"
+        android:layout_width="match_parent"
+        android:layout_height="0dp"
+        android:layout_weight="1"
+        app:layoutManager="LinearLayoutManager" />
+</LinearLayout>
\ No newline at end of file
diff --git a/LottieSample/src/main/res/layout/activity_main.xml b/LottieSample/src/main/res/layout/main_activity.xml
similarity index 100%
rename from LottieSample/src/main/res/layout/activity_main.xml
rename to LottieSample/src/main/res/layout/main_activity.xml
diff --git a/LottieSample/src/main/res/layout/activity_player.xml b/LottieSample/src/main/res/layout/player_activity.xml
similarity index 100%
rename from LottieSample/src/main/res/layout/activity_player.xml
rename to LottieSample/src/main/res/layout/player_activity.xml
diff --git a/LottieSample/src/main/res/layout/player_fragment.xml b/LottieSample/src/main/res/layout/player_fragment.xml
new file mode 100644
index 0000000..6fb6857
--- /dev/null
+++ b/LottieSample/src/main/res/layout/player_fragment.xml
@@ -0,0 +1,86 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:id="@+id/coordinatorLayout"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:background="@android:color/white">
+
+    <LinearLayout
+        android:id="@+id/container"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:orientation="vertical">
+
+        <FrameLayout
+            android:id="@+id/animationContainer"
+            android:layout_width="match_parent"
+            android:layout_height="0dp"
+            android:layout_weight="1">
+
+            <com.airbnb.lottie.LottieAnimationView
+                android:id="@+id/animationView"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_gravity="center"
+                android:background="@drawable/outline"
+                app:lottie_autoPlay="true" />
+
+            <ProgressBar
+                android:id="@+id/loadingView"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_gravity="center" />
+
+            <View
+                android:layout_width="match_parent"
+                android:layout_height="@dimen/divider_height"
+                android:layout_gravity="bottom"
+                android:background="@color/divider" />
+        </FrameLayout>
+
+        <include
+            android:id="@+id/control_bar_speed"
+            layout="@layout/control_bar_speed" />
+
+        <include
+            android:id="@+id/control_bar_scale"
+            layout="@layout/control_bar_scale" />
+
+        <include
+            android:id="@+id/control_bar_background_color"
+            layout="@layout/control_bar_background_color" />
+
+        <include
+            android:id="@+id/control_bar_trim"
+            layout="@layout/control_bar_trim" />
+
+        <include
+            android:id="@+id/control_bar_player_controls"
+            layout="@layout/control_bar_player_controls" />
+
+        <include
+            android:id="@+id/control_bar"
+            layout="@layout/control_bar" />
+    </LinearLayout>
+
+    <androidx.appcompat.widget.Toolbar
+        android:id="@+id/toolbar"
+        android:layout_width="match_parent"
+        android:layout_height="?attr/actionBarSize"
+        android:layout_gravity="top"
+        app:navigationIcon="@drawable/ic_nav_close_selector"
+        app:title="" />
+
+    <include
+        android:id="@+id/bottom_sheet_render_times"
+        layout="@layout/bottom_sheet_render_times" />
+
+    <include
+        android:id="@+id/bottom_sheet_warnings"
+        layout="@layout/bottom_sheet_warnings" />
+
+    <include
+        android:id="@+id/bottom_sheet_key_paths"
+        layout="@layout/bottom_sheet_key_paths" />
+</androidx.coordinatorlayout.widget.CoordinatorLayout>
\ No newline at end of file
diff --git a/LottieSample/src/main/res/layout/activity_qrscan.xml b/LottieSample/src/main/res/layout/qrscan_activity.xml
similarity index 100%
rename from LottieSample/src/main/res/layout/activity_qrscan.xml
rename to LottieSample/src/main/res/layout/qrscan_activity.xml
diff --git a/LottieSample/src/main/res/layout/activity_simple_animation.xml b/LottieSample/src/main/res/layout/simple_animation_activity.xml
similarity index 100%
rename from LottieSample/src/main/res/layout/activity_simple_animation.xml
rename to LottieSample/src/main/res/layout/simple_animation_activity.xml
diff --git a/LottieSample/src/main/res/layout/activity_snapshot_tests.xml b/LottieSample/src/main/res/layout/snapshot_tests_activity.xml
similarity index 100%
rename from LottieSample/src/main/res/layout/activity_snapshot_tests.xml
rename to LottieSample/src/main/res/layout/snapshot_tests_activity.xml
diff --git a/LottieSample/src/main/res/layout/activity_typography_demo.xml b/LottieSample/src/main/res/layout/typography_demo_activity.xml
similarity index 100%
rename from LottieSample/src/main/res/layout/activity_typography_demo.xml
rename to LottieSample/src/main/res/layout/typography_demo_activity.xml
diff --git a/LottieSample/src/main/res/layout/fragment_warnings.xml b/LottieSample/src/main/res/layout/warnings_fragment.xml
similarity index 100%
rename from LottieSample/src/main/res/layout/fragment_warnings.xml
rename to LottieSample/src/main/res/layout/warnings_fragment.xml
diff --git a/LottieSample/src/main/res/values/attrs.xml b/LottieSample/src/main/res/values/attrs.xml
index 94acd1a..57a674c 100644
--- a/LottieSample/src/main/res/values/attrs.xml
+++ b/LottieSample/src/main/res/values/attrs.xml
@@ -7,7 +7,7 @@
     </declare-styleable>
     <declare-styleable name="Marquee">
         <attr name="titleText" />
-        <attr name="subtitleText" />
+        <attr name="subtitleText" format="string"/>
     </declare-styleable>
     <declare-styleable name="TabBarItemView">
         <attr name="titleText" />
diff --git a/build.gradle b/build.gradle
index b9600ba..a29dbd9 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,7 +1,7 @@
 import org.ajoberstar.grgit.Grgit
 
 buildscript {
-  ext.kotlinVersion = '1.3.61'
+  ext.kotlinVersion = '1.4.0'
 
   repositories {
     jcenter()
@@ -12,7 +12,7 @@
   }
   dependencies {
     classpath 'org.ajoberstar:grgit:1.9.3'
-    classpath 'com.android.tools.build:gradle:3.5.3'
+    classpath 'com.android.tools.build:gradle:4.1.0-rc02'
     classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
     classpath "org.jetbrains.kotlin:kotlin-android-extensions:$kotlinVersion"
     classpath 'org.ajoberstar:grgit:1.9.3'
@@ -37,7 +37,7 @@
 }
 
 ext {
-  git = Grgit.open()
+  git = Grgit.open(currentDir: project.rootDir)
   gitSha = git.head().id
   gitBranch = git.branch.getCurrent().name
 }
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index feee4a3..4999dc7 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@
 distributionPath=wrapper/dists
 zipStoreBase=GRADLE_USER_HOME
 zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-6.0.1-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-6.6.1-all.zip
diff --git a/issue-repro/src/main/java/com/airbnb/lottie/issues/MainActivity.kt b/issue-repro/src/main/java/com/airbnb/lottie/issues/MainActivity.kt
index adc07e2..ba04542 100755
--- a/issue-repro/src/main/java/com/airbnb/lottie/issues/MainActivity.kt
+++ b/issue-repro/src/main/java/com/airbnb/lottie/issues/MainActivity.kt
@@ -1,16 +1,11 @@
 package com.airbnb.lottie.issues
 
-import android.content.Intent
 import android.os.Bundle
 import androidx.appcompat.app.AppCompatActivity
-import androidx.core.app.ActivityOptionsCompat
-import com.airbnb.lottie.issues.R
-import kotlinx.android.synthetic.main.activity_main.*
 
-class MainActivity : AppCompatActivity() {
+class MainActivity : AppCompatActivity(R.layout.activity_main) {
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
-        setContentView(R.layout.activity_main)
         // Reproduce any issues here.
     }
 }