Skip to content

Commit

Permalink
Generate model factory methods (#450)
Browse files Browse the repository at this point in the history
* Generate model factory methods

* [modelfactory] Add explicit getId() method to ModelProperties

* [modelfactory] Convert ModelProperties to Kotlin

* [modelfactory] Replace getDrawable with getDrawableRes

* [modelfactory] ProcessorTestUtils: remove unnecessary Kotlin interop annotations

* [modelfactory] Support attribute groups if they don't have multiple attributes of a supported type

* [modelfactory] Add error message

* [modelfactory] Misc

* [modelfactory] Convert ModelProperties back to Java to fix uploadArchives issue

* [modelfactory] Fix CharSequence/String type checks

* [modelfactory] Add support for lists of Epoxy models

* [modelfactory] Fix support for "legacy" models (non @ModelView)

* [modelfactory] Add Kotlin view test

* [modelfactory] Address PR feedback
  • Loading branch information
ngsilverman authored Jun 27, 2018
1 parent 8cdd359 commit ed916a0
Show file tree
Hide file tree
Showing 44 changed files with 4,713 additions and 469 deletions.
1 change: 1 addition & 0 deletions epoxy-modelfactory/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/build
59 changes: 59 additions & 0 deletions epoxy-modelfactory/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import org.gradle.internal.jvm.Jvm

apply plugin: 'com.android.library'
apply from: 'build.workaround-missing-resource.gradle'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt'

android {
compileSdkVersion rootProject.COMPILE_SDK_VERSION

defaultConfig {
minSdkVersion rootProject.MIN_SDK_VERSION
targetSdkVersion rootProject.TARGET_SDK_VERSION
}
}

configurations.all { strategy ->
strategy.resolutionStrategy.force rootProject.deps.junit, rootProject.deps.robolectric,
rootProject.deps.mockito
}

dependencies {
compile project(':epoxy-adapter')
compile rootProject.deps.paris

testCompile project(':epoxy-adapter')
// Need to include the processor directly since we create an instance of it in code for testing
testCompile project(':epoxy-processor')
testCompile project(':epoxy-processortest')
testCompile rootProject.deps.googleTestingCompile
testCompile rootProject.deps.junit
testCompile rootProject.deps.kotlin
// Need to include the processor directly since we create an instance of it in code for testing
testCompile rootProject.deps.parisProcessor
testCompile rootProject.deps.robolectric
testCompile files(getRuntimeJar())
testCompile files(Jvm.current().getToolsJar())

kaptTest project(':epoxy-processor')
}

// Javadoc isn't working well with Kotlin :(
tasks.withType(Javadoc).all { enabled = false }

static def getRuntimeJar() {
try {
final File javaBase = new File(System.getProperty("java.home")).getCanonicalFile()
File runtimeJar = new File(javaBase, "lib/rt.jar")
if (runtimeJar.exists()) {
return runtimeJar
}
runtimeJar = new File(javaBase, "jre/lib/rt.jar")
return runtimeJar.exists() ? runtimeJar : null
} catch (IOException e) {
throw new RuntimeException(e)
}
}

apply from: rootProject.file('gradle/gradle-maven-push.gradle')
23 changes: 23 additions & 0 deletions epoxy-modelfactory/build.workaround-missing-resource.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// Source - https://github.com/nenick/AndroidStudioAndRobolectric/blob/master/app/build.workaround-missing-resource.gradle
// Workaround for missing test resources when running unit tests within android studio.
// This copies the test resources next to the test classes for each variant.
// Tracked at https://github.com/nenick/AndroidStudioAndRobolectric/issues/7
// Original solution comes from https://code.google.com/p/android/issues/detail?id=136013#c10
// See also https://code.google.com/p/android/issues/detail?id=64887
gradle.projectsEvaluated {
// Base path which is recognized by android studio.
def testClassesPath = "${buildDir}/intermediates/classes/test/"
// Copy must be done for each variant.
def variants = android.libraryVariants.collect()

variants.each { variant ->
def variationName = variant.name.capitalize()
def variationPath = variant.buildType.name

// Specific copy task for each variant
def copyTestResourcesTask = project.tasks.create("copyTest${variationName}Resources", Copy)
copyTestResourcesTask.from("${projectDir}/src/test/resources")
copyTestResourcesTask.into("${testClassesPath}/${variationPath}")
copyTestResourcesTask.execute()
}
}
3 changes: 3 additions & 0 deletions epoxy-modelfactory/gradle.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
POM_NAME=Epoxy Model Factory
POM_ARTIFACT_ID=epoxy-modelfactory
POM_PACKAGING=jar
2 changes: 2 additions & 0 deletions epoxy-modelfactory/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<manifest package="com.airbnb.epoxymodelfactory"
xmlns:android="http://schemas.android.com/apk/res/android" />
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package com.airbnb.epoxy;

import android.support.annotation.DrawableRes;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.view.View.OnClickListener;

import com.airbnb.paris.styles.Style;

import java.util.List;

public interface ModelProperties {

@NonNull
String getId();

boolean has(@NonNull String propertyName);

boolean getBoolean(@NonNull String propertyName);

double getDouble(@NonNull String propertyName);

@DrawableRes
int getDrawableRes(@NonNull String propertyName);

@NonNull
List<? extends EpoxyModel<?>> getEpoxyModelList(@NonNull String propertyName);

int getInt(@NonNull String propertyName);

@NonNull
OnClickListener getOnClickListener(@NonNull String propertyName);

@NonNull
String getString(@NonNull String propertyName);

@NonNull
List<String> getStringList(@NonNull String propertyName);

/**
* @return Null to apply the default style.
*/
@Nullable
Style getStyle();
}
3 changes: 3 additions & 0 deletions epoxy-modelfactory/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<resources>
<string name="app_name">Epoxy Model Factory</string>
</resources>
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
package com.airbnb.epoxy

import android.view.View
import com.airbnb.epoxymodelfactory.R
import com.airbnb.paris.styles.Style
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Test

/**
* Asserts that using from(ModelProperties) to create a model applies the property values correctly
*/
class FromModelPropertiesKotlinTest {

@Test
fun getId() {
val model = TestModelPropertiesKotlinViewModel_.from(TestModelProperties(id = "100"))
assertFalse(model.hasDefaultId())
}

@Test
fun getBoolean() {
val model = TestModelPropertiesKotlinViewModel_.from(TestModelProperties(booleanValue = true))
assertEquals(true, model.booleanValue())
}

@Test
fun getDouble() {
val model = TestModelPropertiesKotlinViewModel_.from(TestModelProperties(doubleValue = 42.0))
assertEquals(42.0, model.doubleValue(), 0.0)
}

@Test
fun getDrawableRes() {
val drawableRes = R.drawable.abc_ic_star_black_48dp
val model =
TestModelPropertiesKotlinViewModel_.from(TestModelProperties(drawableRes = drawableRes))
assertEquals(drawableRes, model.drawableRes())
}

@Test
fun getEpoxyModelList() {
val epoxyModelList = emptyList<EpoxyModel<*>>()
val model =
TestModelPropertiesKotlinViewModel_.from(TestModelProperties(epoxyModelList = epoxyModelList))
assertEquals(epoxyModelList, model.epoxyModelList())
}

@Test
fun getInt() {
val model = TestModelPropertiesKotlinViewModel_.from(TestModelProperties(intValue = 51))
assertEquals(51, model.intValue())
}

@Test
fun getOnClickListener() {
val clickListener = View.OnClickListener { }
val model =
TestModelPropertiesKotlinViewModel_.from(TestModelProperties(onClickListener = clickListener))
assertEquals(clickListener, model.onClickListener())
}

@Test
fun getString() {
val model =
TestModelPropertiesKotlinViewModel_.from(TestModelProperties(stringValue = "ModelFactory"))
assertEquals("ModelFactory", model.stringValue())
}

@Test
fun getStringList() {
val stringList = listOf("Model", "Factory")
val model = TestModelPropertiesKotlinViewModel_.from(TestModelProperties(stringList = stringList))
assertEquals(stringList, model.stringList())
}

class TestModelProperties(
private val id: String = "",
private val booleanValue: Boolean? = null,
private val doubleValue: Double? = null,
private val drawableRes: Int? = null,
private val epoxyModelList: List<EpoxyModel<*>>? = null,
private val intValue: Int? = null,
private val onClickListener: View.OnClickListener? = null,
private val stringValue: String? = null,
private val stringList: List<String>? = null,
private val styleValue: Style? = null
) : ModelProperties {
override fun getId() = id

override fun has(propertyName: String): Boolean {
return mapOf(
"booleanValue" to booleanValue,
"doubleValue" to doubleValue,
"drawableRes" to drawableRes,
"epoxyModelList" to epoxyModelList,
"intValue" to intValue,
"onClickListener" to onClickListener,
"stringList" to stringList,
"stringValue" to stringValue
)[propertyName] != null
}

override fun getBoolean(propertyName: String) = booleanValue!!

override fun getDouble(propertyName: String) = doubleValue!!

override fun getDrawableRes(propertyName: String) = drawableRes!!

override fun getEpoxyModelList(propertyName: String) = epoxyModelList!!

override fun getInt(propertyName: String) = intValue!!

override fun getOnClickListener(propertyName: String) = onClickListener!!

override fun getString(propertyName: String) = stringValue!!

override fun getStringList(propertyName: String) = stringList!!

override fun getStyle() = styleValue
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
package com.airbnb.epoxy

import android.view.View
import com.airbnb.epoxymodelfactory.R
import com.airbnb.paris.styles.Style
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Test

/**
* Asserts that using from(ModelProperties) to create a model applies the property values correctly
*/
class FromModelPropertiesTest {

@Test
fun getId() {
val model = TestModelPropertiesViewModel_.from(TestModelProperties(id = "100"))
assertFalse(model.hasDefaultId())
}

@Test
fun getBoolean() {
val model = TestModelPropertiesViewModel_.from(TestModelProperties(booleanValue = true))
assertEquals(true, model.booleanValue())
}

@Test
fun getDouble() {
val model = TestModelPropertiesViewModel_.from(TestModelProperties(doubleValue = 42.0))
assertEquals(42.0, model.doubleValue(), 0.0)
}

@Test
fun getDrawableRes() {
val drawableRes = R.drawable.abc_ic_star_black_48dp
val model =
TestModelPropertiesViewModel_.from(TestModelProperties(drawableRes = drawableRes))
assertEquals(drawableRes, model.drawableRes())
}

@Test
fun getEpoxyModelList() {
val epoxyModelList = emptyList<EpoxyModel<*>>()
val model =
TestModelPropertiesViewModel_.from(TestModelProperties(epoxyModelList = epoxyModelList))
assertEquals(epoxyModelList, model.epoxyModelList())
}

@Test
fun getInt() {
val model = TestModelPropertiesViewModel_.from(TestModelProperties(intValue = 51))
assertEquals(51, model.intValue())
}

@Test
fun getOnClickListener() {
val clickListener = View.OnClickListener { }
val model =
TestModelPropertiesViewModel_.from(TestModelProperties(onClickListener = clickListener))
assertEquals(clickListener, model.onClickListener())
}

@Test
fun getString() {
val model =
TestModelPropertiesViewModel_.from(TestModelProperties(stringValue = "ModelFactory"))
assertEquals("ModelFactory", model.stringValue())
}

@Test
fun getStringList() {
val stringList = listOf("Model", "Factory")
val model = TestModelPropertiesViewModel_.from(TestModelProperties(stringList = stringList))
assertEquals(stringList, model.stringList())
}

class TestModelProperties(
private val id: String = "",
private val booleanValue: Boolean? = null,
private val doubleValue: Double? = null,
private val drawableRes: Int? = null,
private val epoxyModelList: List<EpoxyModel<*>>? = null,
private val intValue: Int? = null,
private val onClickListener: View.OnClickListener? = null,
private val stringValue: String? = null,
private val stringList: List<String>? = null,
private val styleValue: Style? = null
) : ModelProperties {
override fun getId() = id

override fun has(propertyName: String): Boolean {
return mapOf(
"booleanValue" to booleanValue,
"doubleValue" to doubleValue,
"drawableRes" to drawableRes,
"epoxyModelList" to epoxyModelList,
"intValue" to intValue,
"onClickListener" to onClickListener,
"stringList" to stringList,
"stringValue" to stringValue
)[propertyName] != null
}

override fun getBoolean(propertyName: String) = booleanValue!!

override fun getDouble(propertyName: String) = doubleValue!!

override fun getDrawableRes(propertyName: String) = drawableRes!!

override fun getEpoxyModelList(propertyName: String) = epoxyModelList!!

override fun getInt(propertyName: String) = intValue!!

override fun getOnClickListener(propertyName: String) = onClickListener!!

override fun getString(propertyName: String) = stringValue!!

override fun getStringList(propertyName: String) = stringList!!

override fun getStyle() = styleValue
}
}
Loading

0 comments on commit ed916a0

Please sign in to comment.