Skip to content

Commit

Permalink
Introduce mechanism for saving state in Kotlin
Browse files Browse the repository at this point in the history
  • Loading branch information
MarcinMoskala committed Jan 1, 2018
1 parent cde3316 commit bca5227
Show file tree
Hide file tree
Showing 34 changed files with 325 additions and 142 deletions.
6 changes: 3 additions & 3 deletions README.md
Expand Up @@ -47,7 +47,7 @@ public class MainActivity extends BaseActivity {
}
```

And `ActivityStarter.fill(this);` in BaseActivity:
And `ActivityStarter.fill(this, savedInstanceState);` in BaseActivity:

```java
class BaseActivity extends AppCompatActivity {
Expand All @@ -56,7 +56,7 @@ class BaseActivity extends AppCompatActivity {
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ActivityStarter.fill(this);
ActivityStarter.fill(this, savedInstanceState);
}

@Override // This is optional, only when we want to keep arguments changes in case of rotation etc.
Expand Down Expand Up @@ -120,7 +120,7 @@ class StudentDataActivity : BaseActivity() {
}
```

Values are taken lazily and kept as fields, but there are still saved if `ActivityStarter.save(this)` is called in`onSaveInstanceState`. When all properties are provided by delegate, then there is no need to call `ActivityStarter.fill(this)` in `onCreate`.
Values are taken lazily and kept as fields, but there are still saved if `ActivityStarter.save(this)` is called in`onSaveInstanceState`. When all properties are provided by delegate, then there is no need to call `ActivityStarter.fill(this, savedInstanceState)` in `onCreate`.

## Parceler

Expand Down
Expand Up @@ -2,6 +2,7 @@ package activitystarter.compiler.generation

import activitystarter.compiler.model.classbinding.ClassModel
import activitystarter.compiler.model.param.ArgumentModel
import activitystarter.compiler.model.param.FieldAccessType.*
import activitystarter.compiler.utils.*
import com.squareup.javapoet.MethodSpec
import com.squareup.javapoet.TypeName
Expand All @@ -26,26 +27,42 @@ internal class ActivityGeneration(classModel: ClassModel) : IntentBinding(classM

override fun TypeSpec.Builder.addExtraToClass() = this
.addMethod(createSaveMethod())
.addNoSettersAccessors()
.addKotlinSettersAccessors()

private fun MethodSpec.Builder.addFieldSettersCode() {
addStatement("\$T intent = activity.getIntent()", INTENT)
if (classModel.savable) {
for (arg in classModel.argumentModels) {
arg.accessor.makeSetter("") ?: return
val bundleName = "savedInstanceState"
val bundlePredicate = getBundlePredicate(bundleName, arg.keyFieldName)
addCode("if($bundleName != null && $bundlePredicate) {\n")
addBundleSetter(arg, bundleName, "activity", false)
addCode("} else {")
addIntentSetter(arg, "activity")
addCode("}")
val bundleName = "savedInstanceState"
val settableArgs = classModel.argumentModels.filter { it.accessor.isSettable() }
val (fromGetterAccessors, byPropertyAccessors) = settableArgs.partition { it.accessor.fromGetter }
if (byPropertyAccessors.isNotEmpty()) {
addStatement("\$T intent = activity.getIntent()", INTENT)
}
byPropertyAccessors.forEach { arg -> addArgumentAndSavedStateSetters(arg, bundleName) }
fromGetterAccessors.forEach { arg -> addSavedStateSetters(arg, bundleName) }
} else {
addIntentSetters("activity")
addStatement("\$T intent = activity.getIntent()", INTENT)
classModel.argumentModels
.filter { !it.accessor.fromGetter }
.forEach { arg -> addIntentSetter(arg, "activity") }
}
}

private fun MethodSpec.Builder.addArgumentAndSavedStateSetters(arg: ArgumentModel, bundleName: String) {
val bundlePredicate = getBundlePredicate(bundleName, arg.keyFieldName)
addCode("if($bundleName != null && $bundlePredicate) {\n ")
addBundleSetter(arg, bundleName, "activity", false)
addCode("} else { \n ")
addIntentSetter(arg, "activity")
addCode("} \n")
}

private fun MethodSpec.Builder.addSavedStateSetters(arg: ArgumentModel, bundleName: String) {
val bundlePredicate = getBundlePredicate(bundleName, arg.keyFieldName)
addCode("if($bundleName != null && $bundlePredicate) {\n ")
addBundleSetter(arg, bundleName, "activity", false)
addCode("} \n")
}

private fun createSaveMethod(): MethodSpec = this
.builderWithCreationBasicFieldsNoContext("save")
.addParameter(classModel.targetTypeName, "activity")
Expand Down Expand Up @@ -84,8 +101,8 @@ internal class ActivityGeneration(classModel: ClassModel) : IntentBinding(classM
.addStatement("context.startActivityForResult(intent, result)")
.build()

private fun TypeSpec.Builder.addNoSettersAccessors(): TypeSpec.Builder = apply {
classModel.argumentModels.filter { it.noSetter }.forEach { arg ->
private fun TypeSpec.Builder.addKotlinSettersAccessors(): TypeSpec.Builder = apply {
classModel.argumentModels.filter { it.accessor.fromGetter }.forEach { arg ->
addMethod(buildCheckValueMethod(arg))
addMethod(buildGetValueMethod(arg))
}
Expand Down
Expand Up @@ -2,6 +2,7 @@ package activitystarter.compiler.generation

import activitystarter.compiler.model.classbinding.ClassModel
import activitystarter.compiler.model.param.ArgumentModel
import activitystarter.compiler.model.param.FieldAccessType
import activitystarter.compiler.utils.BUNDLE
import activitystarter.compiler.utils.doIf
import com.squareup.javapoet.MethodSpec
Expand All @@ -27,23 +28,37 @@ internal class FragmentGeneration(classModel: ClassModel) : ClassGeneration(clas
.addNoSettersAccessors()

private fun MethodSpec.Builder.addFieldSettersCode() {
addStatement("\$T arguments = fragment.getArguments()", BUNDLE)
if (classModel.savable) {
for (arg in classModel.argumentModels) {
arg.accessor.makeSetter("") ?: return
val bundleName = "savedInstanceState"
val bundlePredicate = getBundlePredicate(bundleName, arg.keyFieldName)
addCode("if($bundleName != null && $bundlePredicate) {\n")
addBundleSetter(arg, bundleName, "fragment", false)
addCode("} else {")
addBundleSetter(arg, "arguments", "fragment", true)
addCode("}")
val bundleName = "savedInstanceState"
val settableArgs = classModel.argumentModels.filter { it.accessor.isSettable() }
val (fromGetterAccessors, byPropertyAccessors) = settableArgs.partition { it.accessor.fromGetter }
if(byPropertyAccessors.isNotEmpty()) {
addStatement("\$T arguments = fragment.getArguments()", BUNDLE)
}
byPropertyAccessors.forEach { arg -> addArgumentAndSavedStateSetters(arg, bundleName) }
fromGetterAccessors.forEach { arg -> addSavedStateSetters(arg, bundleName) }
} else {
addStatement("\$T arguments = fragment.getArguments()", BUNDLE)
addBundleSetters("arguments", "fragment", true)
}
}

private fun MethodSpec.Builder.addArgumentAndSavedStateSetters(arg: ArgumentModel, bundleName: String) {
val bundlePredicate = getBundlePredicate(bundleName, arg.keyFieldName)
addCode("if($bundleName != null && $bundlePredicate) {\n")
addBundleSetter(arg, bundleName, "fragment", false)
addCode("} else {")
addBundleSetter(arg, "arguments", "fragment", true)
addCode("}")
}

private fun MethodSpec.Builder.addSavedStateSetters(arg: ArgumentModel, bundleName: String) {
val bundlePredicate = getBundlePredicate(bundleName, arg.keyFieldName)
addCode("if($bundleName != null && $bundlePredicate) {\n")
addBundleSetter(arg, bundleName, "fragment", false)
addCode("}")
}

private fun createGetFragmentMethod(variant: List<ArgumentModel>) = builderWithCreationBasicFieldsNoContext("newInstance")
.addArgParameters(variant)
.returns(classModel.targetTypeName)
Expand All @@ -69,7 +84,7 @@ internal class FragmentGeneration(classModel: ClassModel) : ClassGeneration(clas
.build()

private fun TypeSpec.Builder.addNoSettersAccessors(): TypeSpec.Builder = apply {
classModel.argumentModels.filter { it.noSetter }.forEach { arg ->
classModel.argumentModels.filter { it.accessor.fromGetter }.forEach { arg ->
addMethod(buildCheckValueMethod(arg))
addMethod(buildGetValueMethod(arg))
}
Expand Down
Expand Up @@ -30,7 +30,7 @@ internal abstract class IntentBinding(classModel: ClassModel) : ClassGeneration(
}
}

protected fun MethodSpec.Builder.addIntentSetters(targetParameterName: String) = apply {
private fun MethodSpec.Builder.addIntentSetters(targetParameterName: String) = apply {
classModel.argumentModels.forEach { arg -> addIntentSetter(arg, targetParameterName) }
}

Expand Down
Expand Up @@ -19,7 +19,6 @@ class ArgumentModel(
val parceler: Boolean
) {
val keyFieldName: String by lazy { camelCaseToUppercaseUnderscore(name) + "_KEY" }
val noSetter = accessor.noSetter
val accessorName = ActivityStarterNameConstruction.getterFieldAccessorName(name)
val checkerName = ActivityStarterNameConstruction.getterFieldCheckerName(name)

Expand Down
@@ -0,0 +1,8 @@
package activitystarter.compiler.model.param

enum class FieldAccessType {
Accessible,
ByMethod,
ByNoIsMethod,
Inaccessible
}
@@ -1,6 +1,7 @@
package activitystarter.compiler.model.param

import activitystarter.compiler.error.Errors
import activitystarter.compiler.model.param.FieldAccessor.Companion.hasNotPrivateMethodNamed
import com.sun.org.apache.xpath.internal.operations.Bool
import javax.lang.model.element.Element
import javax.lang.model.element.Modifier.PRIVATE
import javax.lang.model.element.TypeElement
Expand All @@ -9,10 +10,11 @@ import javax.lang.model.util.ElementFilter
class FieldAccessor private constructor(
val fieldName: String,
private val setterType: FieldAccessType,
private val getterType: FieldAccessType
private val getterType: FieldAccessType,
val fromGetter: Boolean
) {

val noSetter = setterType == FieldAccessType.Inaccessible
fun isSettable() = setterType != FieldAccessType.Inaccessible

fun isAccessible() = setterType != FieldAccessType.Inaccessible && getterType != FieldAccessType.Inaccessible

Expand All @@ -32,32 +34,33 @@ class FieldAccessor private constructor(

companion object {

fun fromGetter(name: String) = FieldAccessor(name, FieldAccessType.Inaccessible, FieldAccessType.ByMethod)
fun fromGetter(getterMethod: Element, name: String): FieldAccessor {
val enclosingElement = getterMethod.enclosingElement as TypeElement
val setterType = getPrivateFieldAccessType(enclosingElement, name, "set", "set")
return FieldAccessor(name, setterType, FieldAccessType.ByMethod, true)
}

fun fromElement(element: Element): FieldAccessor {
val enclosingElement = element.enclosingElement as TypeElement
val fieldName = element.simpleName.toString()
val setterType = getFieldAccessType(enclosingElement, fieldName, element, "set", "set")
val getterType = getFieldAccessType(enclosingElement, fieldName, element, "get", "is")
return FieldAccessor(fieldName, setterType, getterType)
return FieldAccessor(fieldName, setterType, getterType, false)
}

private fun getFieldAccessType(enclosingElement: TypeElement, fieldName: String, element: Element, functionModifier: String, isFunctionModifier: String) = when {
PRIVATE !in element.modifiers -> FieldAccessType.Accessible
hasNotPrivateMethodNamed(enclosingElement, functionModifier + fieldName.capitalize()) -> FieldAccessType.ByMethod
fieldName.substring(0, 2) == "is" && hasNotPrivateMethodNamed(enclosingElement, isFunctionModifier + fieldName.substring(2)) -> FieldAccessType.ByNoIsMethod
else -> FieldAccessType.Inaccessible
else -> getPrivateFieldAccessType(enclosingElement, fieldName, functionModifier, isFunctionModifier)
}

private fun hasNotPrivateMethodNamed(enclosingElement: TypeElement, fieldName: String) = ElementFilter
.methodsIn(enclosingElement.enclosedElements)
.any { e -> e.simpleName.contentEquals(fieldName) && PRIVATE !in e.modifiers }

private enum class FieldAccessType {
Accessible,
ByMethod,
ByNoIsMethod,
Inaccessible
private fun getPrivateFieldAccessType(enclosingElement: TypeElement, fieldName: String, functionModifier: String, isFunctionModifier: String) = when {
enclosingElement.hasNotPrivateMethodNamed(functionModifier + fieldName.capitalize()) -> FieldAccessType.ByMethod
fieldName.startsWith("is") && enclosingElement.hasNotPrivateMethodNamed(isFunctionModifier + fieldName.substring(2)) -> FieldAccessType.ByNoIsMethod
else -> FieldAccessType.Inaccessible
}

private fun TypeElement.hasNotPrivateMethodNamed(methodName: String) = ElementFilter
.methodsIn(enclosedElements)
.any { e -> e.simpleName.contentEquals(methodName) && PRIVATE !in e.modifiers }
}
}
Expand Up @@ -63,7 +63,7 @@ class ArgumentFactory(val enclosingElement: TypeElement, val config: ProjectConf

val returnType: TypeMirror = getter.returnType
val paramType = ParamType.fromType(returnType)
val accessor: FieldAccessor = FieldAccessor.fromGetter(name)
val accessor: FieldAccessor = FieldAccessor.fromGetter(getterElement, name)

val error = getGetterError(knownClassType, paramType, accessor)
if (error != null) {
Expand Down
12 changes: 0 additions & 12 deletions activitystarter/src/main/java/activitystarter/ActivityStarter.java
Expand Up @@ -14,26 +14,14 @@

public final class ActivityStarter {

public static void fill(@NonNull Activity target) {
innerFill(target, null, Bundle.class);
}

public static void fill(@NonNull Activity target, @Nullable Bundle savedInstanceState) {
innerFill(target, savedInstanceState, Bundle.class);
}

public static void fill(@NonNull Fragment target) {
innerFill(target, null, Bundle.class);
}

public static void fill(@NonNull Fragment target, @Nullable Bundle savedInstanceState) {
innerFill(target, savedInstanceState, Bundle.class);
}

public static void fill(@NonNull android.support.v4.app.Fragment target) {
innerFill(target, null, Bundle.class);
}

public static void fill(@NonNull android.support.v4.app.Fragment target, @Nullable Bundle savedInstanceState) {
innerFill(target, savedInstanceState, Bundle.class);
}
Expand Down
Expand Up @@ -4,10 +4,15 @@ import activitystarter.compiler.error.Errors
import org.junit.Test

@Suppress("IllegalIdentifier")
class GetterTest: GenerationTest() {
class ByAccessorsTest : GenerationTest() {

@Test
fun singleGetterTest() {
filePrecessingComparator("getter/Single")
}

@Test
fun getterSetterTest() {
filePrecessingComparator("getter/GetterSetter")
}
}
19 changes: 13 additions & 6 deletions generationExamples/activity/ConflictedOptional
Expand Up @@ -17,21 +17,28 @@ import java.lang.String;

public final class MainActivityStarter {
private static final String NAME_KEY = "com.example.activitystarter.nameStarterKey";

private static final String SURNAME_KEY = "com.example.activitystarter.surnameStarterKey";

public static void fill(MainActivity activity) {
public static void fill(MainActivity activity, Bundle savedInstanceState) {
Intent intent = activity.getIntent();
if(intent.hasExtra(NAME_KEY))
if(savedInstanceState != null && savedInstanceState.containsKey(NAME_KEY)) {
activity.name = savedInstanceState.getString(NAME_KEY);
} else {
if(intent.hasExtra(NAME_KEY))
activity.name = intent.getStringExtra(NAME_KEY);
if(intent.hasExtra(SURNAME_KEY))
}
if(savedInstanceState != null && savedInstanceState.containsKey(SURNAME_KEY)) {
activity.surname = savedInstanceState.getString(SURNAME_KEY);
} else {
if(intent.hasExtra(SURNAME_KEY))
activity.surname = intent.getStringExtra(SURNAME_KEY);
}
}

public static void save(MainActivity activity) {
Bundle bundle = new Bundle();
public static void save(MainActivity activity, Bundle bundle) {
bundle.putString(NAME_KEY, activity.name);
bundle.putString(SURNAME_KEY, activity.surname);
activity.getIntent().putExtras(bundle);
}

public static Intent getIntent(Context context, String name, String surname) {
Expand Down
6 changes: 2 additions & 4 deletions generationExamples/activity/EmptyAnnotated
Expand Up @@ -15,12 +15,10 @@ import android.os.Bundle;

public final class MainActivityStarter {

public static void fill(MainActivity activity) {
public static void fill(MainActivity activity, Bundle savedInstanceState) {
}

public static void save(MainActivity activity) {
Bundle bundle = new Bundle();
activity.getIntent().putExtras(bundle);
public static void save(MainActivity activity, Bundle bundle) {
}

public static Intent getIntent(Context context) {
Expand Down

0 comments on commit bca5227

Please sign in to comment.