diff --git a/README.md b/README.md index c48a28e..3d5f737 100644 --- a/README.md +++ b/README.md @@ -17,9 +17,9 @@ You can optionally use the dependency `lyra-coder-gson` if you want to include t ```gradle dependencies { - compile 'com.github.fondesa:lyra:1.0.0' + compile 'com.github.fondesa:lyra:1.0.1' // Use this dependency if you want to include the Gson coder. - compile 'com.github.fondesa:lyra-coder-gson:1.0.0' + compile 'com.github.fondesa:lyra-coder-gson:1.0.1' } ``` @@ -29,7 +29,7 @@ dependencies { com.github.fondesa lyra - 1.0.0 + 1.0.1 pom ``` @@ -118,6 +118,25 @@ public class MainActivity extends Activity { } ``` +The save/restore of the state is supported also in a custom `View`. For example: + +```java +public class AutoSaveEditText extends AppCompatEditText { + @SaveState + CharSequence mText; + + @Override + public Parcelable onSaveInstanceState() { + return Lyra.instance().saveState(this, super.onSaveInstanceState()); + } + + @Override + public void onRestoreInstanceState(Parcelable state) { + super.onRestoreInstanceState(Lyra.instance().restoreState(this, state)); + } +} +``` + As shown above, you can create your own custom `StateCoder`. For example, this coder will save/restore a `String` in `Base64`: ```java diff --git a/bintray-deploy.properties b/bintray-deploy.properties index ca8650c..63ad279 100644 --- a/bintray-deploy.properties +++ b/bintray-deploy.properties @@ -6,5 +6,5 @@ BINTRAY_LIB_ISSUE_TRACKER_URL=https://github.com/Fondesa/Lyra/issues BINTRAY_LIB_GIT_URL=https://github.com/Fondesa/Lyra.git BINTRAY_LIB_GITHUB_REPO=Fondesa/Lyra BINTRAY_LIB_TAGS=android|lyra|saveinstancestate|bundle -BINTRAY_LIB_VERSION=1.0.0 +BINTRAY_LIB_VERSION=1.0.1 BINTRAY_LIB_VERSION_DESCRIPTION=For further information check: https://github.com/Fondesa/Lyra \ No newline at end of file diff --git a/lyra/src/main/java/com/fondesa/lyra/Lyra.java b/lyra/src/main/java/com/fondesa/lyra/Lyra.java index 2f20973..92596c6 100644 --- a/lyra/src/main/java/com/fondesa/lyra/Lyra.java +++ b/lyra/src/main/java/com/fondesa/lyra/Lyra.java @@ -22,10 +22,12 @@ import android.content.Context; import android.os.Build; import android.os.Bundle; +import android.os.Parcelable; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.annotation.RequiresApi; import android.util.Log; +import android.view.View; import com.fondesa.lyra.annotation.SaveState; import com.fondesa.lyra.coder.CoderRetriever; @@ -54,6 +56,7 @@ */ public class Lyra { public static final String SUB_BUNDLE_KEY = "lyra:"; + public static final String VIEW_SUPER_STATE_BUNDLE_KEY = "view:superState"; private static final String TAG = Lyra.class.getSimpleName(); private Application mApplication; @@ -128,7 +131,7 @@ public static String getKeyFromField(@NonNull Field field) { /** * Save the annotated fields' state of a class into a {@link Bundle}. - * The fields of a class will be retrieved through a {@link FieldsRetriever}. + * The fields of the class will be retrieved through a {@link FieldsRetriever}. * The coder to serialize the fields into a {@link Bundle} will be retrieved through a {@link CoderRetriever}. * * @param stateHolder instance of the class with annotated fields @@ -182,7 +185,7 @@ public void saveState(@NonNull Object stateHolder, @NonNull Bundle state) { /** * Restore the annotated fields' state of a class from a {@link Bundle}. - * The fields of a class will be retrieved through a {@link FieldsRetriever}. + * The fields of the class will be retrieved through a {@link FieldsRetriever}. * The coder to deserialize the fields from a {@link Bundle} will be retrieved through a {@link CoderRetriever}. * * @param stateHolder instance of the class with annotated fields @@ -230,6 +233,47 @@ public void restoreState(@NonNull Object stateHolder, @Nullable Bundle state) { } } + /** + * Save the annotated fields' state of a {@link View} class into a {@link Bundle}. + * The {@link Bundle} will contain the parcelable state of the {@link View} + * with the key {@link #VIEW_SUPER_STATE_BUNDLE_KEY}. + * The fields of the class will be retrieved through a {@link FieldsRetriever}. + * The coder to serialize the fields into a {@link Bundle} will be retrieved through a {@link CoderRetriever}. + * + * @param stateHolder instance of the class with annotated fields + * @param state {@link Bundle} in which you want to save the annotated fields + * @return {@link Bundle} containing view state and Lyra sub-bundle + */ + public Parcelable saveState(@NonNull View stateHolder, @Nullable Parcelable state) { + Bundle wrapper = new Bundle(); + // Put the original super state. + wrapper.putParcelable(VIEW_SUPER_STATE_BUNDLE_KEY, state); + // Save the state into the Lyra Bundle. + saveState((Object) stateHolder, wrapper); + return wrapper; + } + + /** + * Restore the annotated fields' state of a {@link View} class from a {@link Parcelable}. + * If the state is a {@link Bundle}, the {@link View}'s state and the fields' values will be restored. + * The fields of the class will be retrieved through a {@link FieldsRetriever}. + * The coder to deserialize the fields from a {@link Bundle} will be retrieved through a {@link CoderRetriever}. + * + * @param stateHolder instance of the class with annotated fields + * @param state {@link Bundle} from which you want to restore the value of the annotated fields + * @return {@link Parcelable} containing {@link View}'s saved state + */ + public Parcelable restoreState(@NonNull View stateHolder, @Nullable Parcelable state) { + if (state instanceof Bundle) { + Bundle wrapper = (Bundle) state; + // Get the original super state. + state = wrapper.getParcelable(VIEW_SUPER_STATE_BUNDLE_KEY); + // Restore the state saved in the Lyra Bundle. + restoreState((Object) stateHolder, wrapper); + } + return state; + } + @NonNull private static SaveState getSaveStateAnnotation(@NonNull Field field) { SaveState saveState = field.getAnnotation(SaveState.class); diff --git a/lyra/src/test/java/com/fondesa/lyra/LyraTest.java b/lyra/src/test/java/com/fondesa/lyra/LyraTest.java index 6af8ba5..87fdae9 100644 --- a/lyra/src/test/java/com/fondesa/lyra/LyraTest.java +++ b/lyra/src/test/java/com/fondesa/lyra/LyraTest.java @@ -17,7 +17,9 @@ package com.fondesa.lyra; import android.app.Application; +import android.content.Context; import android.os.Bundle; +import android.os.Parcelable; import android.support.annotation.NonNull; import com.fondesa.lyra.annotation.SaveState; @@ -32,6 +34,7 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; import org.robolectric.android.ApplicationTestUtil; import java.lang.reflect.Field; @@ -41,6 +44,7 @@ import io.github.benas.randombeans.EnhancedRandomBuilder; import io.github.benas.randombeans.api.EnhancedRandom; +import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.assertFalse; import static junit.framework.Assert.assertNotNull; import static junit.framework.Assert.assertTrue; @@ -92,11 +96,11 @@ public void testKeyFromFieldNotNull() { @Test public void testSaveState() throws Exception { - final DefaultFieldsRetriever retriever = new DefaultFieldsRetriever(); - final Bundle stateBundle = new Bundle(); final TestModels.SubClassAllModifiersAnnotatedFields saveStateObject = new TestModels.SubClassAllModifiersAnnotatedFields(); + final Bundle stateBundle = new Bundle(); + final DefaultFieldsRetriever retriever = new DefaultFieldsRetriever(); Field[] fields = retriever.getFields(saveStateObject.getClass()); for (Field field : fields) { new FieldAccessibleRunner(field) { @@ -125,14 +129,13 @@ public void runWithField(@NonNull Field field) throws Exception { @Test public void testRestoreState() throws Exception { - final DefaultFieldsRetriever retriever = new DefaultFieldsRetriever(); - final DefaultCoderRetriever coderRetriever = new DefaultCoderRetriever(); - final Bundle stateBundle = new Bundle(); - final Bundle lyraBundle = new Bundle(); - final TestModels.SubClassAllModifiersAnnotatedFields saveStateObject = new TestModels.SubClassAllModifiersAnnotatedFields(); + final Bundle stateBundle = new Bundle(); + final Bundle lyraBundle = new Bundle(); + final DefaultFieldsRetriever retriever = new DefaultFieldsRetriever(); + final DefaultCoderRetriever coderRetriever = new DefaultCoderRetriever(); Field[] fields = retriever.getFields(saveStateObject.getClass()); List savedValues = new ArrayList<>(fields.length); for (Field field : fields) { @@ -156,11 +159,11 @@ public void testRestoreState() throws Exception { @Test public void testSaveAndRestoreState() throws Exception { - final DefaultFieldsRetriever retriever = new DefaultFieldsRetriever(); - final Bundle stateBundle = new Bundle(); final TestModels.SubClassAllModifiersAnnotatedFields saveStateObject = new TestModels.SubClassAllModifiersAnnotatedFields(); + final Bundle stateBundle = new Bundle(); + final DefaultFieldsRetriever retriever = new DefaultFieldsRetriever(); Field[] fields = retriever.getFields(saveStateObject.getClass()); for (Field field : fields) { new FieldAccessibleRunner(field) { @@ -181,6 +184,106 @@ public void runWithField(@NonNull Field field) throws Exception { assertThat(fieldRestoredValues, containsInAnyOrder(fieldSavedValues.toArray())); } + @Test + public void testSaveParcelableState() throws Exception { + final Context context = RuntimeEnvironment.application.getApplicationContext(); + final TestModels.SaveStateView saveStateView = new TestModels.SaveStateView(context); + + final DefaultFieldsRetriever retriever = new DefaultFieldsRetriever(); + Field[] fields = retriever.getFields(saveStateView.getClass()); + for (Field field : fields) { + new FieldAccessibleRunner(field) { + @Override + public void runWithField(@NonNull Field field) throws Exception { + field.set(saveStateView, mRandomizer.nextObject(field.getType())); + } + }; + } + + Parcelable parcelable = TestModels.ImplementedParcelable.getDefault(); + Parcelable stateParcelable = Lyra.instance().saveState(saveStateView, parcelable); + + assertTrue(stateParcelable instanceof Bundle); + + Bundle stateBundle = (Bundle) stateParcelable; + List fieldValues = listOfValuesFromFields(saveStateView, fields); + List savedValues = new ArrayList<>(fieldValues.size()); + + Bundle lyraBundle = stateBundle.getBundle(Lyra.SUB_BUNDLE_KEY); + // Assert that the sub Bundle has been written. + assertNotNull(lyraBundle); + + for (String key : lyraBundle.keySet()) { + savedValues.add(lyraBundle.get(key)); + } + + assertThat(savedValues, containsInAnyOrder(fieldValues.toArray())); + } + + @Test + public void testRestoreParcelableState() throws Exception { + final Context context = RuntimeEnvironment.application.getApplicationContext(); + final TestModels.SaveStateView saveStateView = new TestModels.SaveStateView(context); + + Parcelable parcelable = TestModels.ImplementedParcelable.getDefault(); + + final Bundle stateBundle = new Bundle(); + stateBundle.putParcelable(Lyra.VIEW_SUPER_STATE_BUNDLE_KEY, parcelable); + + final Bundle lyraBundle = new Bundle(); + final DefaultFieldsRetriever retriever = new DefaultFieldsRetriever(); + final DefaultCoderRetriever coderRetriever = new DefaultCoderRetriever(); + Field[] fields = retriever.getFields(saveStateView.getClass()); + List savedValues = new ArrayList<>(fields.length); + for (Field field : fields) { + SaveState saveState = field.getAnnotation(SaveState.class); + Class fieldType = field.getType(); + StateCoder stateCoder = coderRetriever.getCoder(saveState, fieldType); + Object randomObj = mRandomizer.nextObject(fieldType); + savedValues.add(randomObj); + //noinspection unchecked + stateCoder.serialize(lyraBundle, Lyra.getKeyFromField(field), randomObj); + } + + // Insert the sub Bundle used by Lyra. + stateBundle.putBundle(Lyra.SUB_BUNDLE_KEY, lyraBundle); + // Restore the state from the sub Bundle. + Parcelable restoredParcelable = Lyra.instance().restoreState(saveStateView, (Parcelable) stateBundle); + assertEquals(restoredParcelable, parcelable); + + List fieldValues = listOfValuesFromFields(saveStateView, fields); + assertThat(savedValues, containsInAnyOrder(fieldValues.toArray())); + } + + @Test + public void testSaveAndRestoreParcelableState() throws Exception { + final Context context = RuntimeEnvironment.application.getApplicationContext(); + final TestModels.SaveStateView saveStateView = new TestModels.SaveStateView(context); + + Parcelable parcelable = TestModels.ImplementedParcelable.getDefault(); + + final DefaultFieldsRetriever retriever = new DefaultFieldsRetriever(); + Field[] fields = retriever.getFields(saveStateView.getClass()); + for (Field field : fields) { + new FieldAccessibleRunner(field) { + @Override + public void runWithField(@NonNull Field field) throws Exception { + field.set(saveStateView, mRandomizer.nextObject(field.getType())); + } + }; + } + + List fieldSavedValues = listOfValuesFromFields(saveStateView, fields); + + Parcelable savedState = Lyra.instance().saveState(saveStateView, parcelable); + Parcelable restoredParcelable = Lyra.instance().restoreState(saveStateView, savedState); + + assertEquals(restoredParcelable, parcelable); + + List fieldRestoredValues = listOfValuesFromFields(saveStateView, fields); + assertThat(fieldRestoredValues, containsInAnyOrder(fieldSavedValues.toArray())); + } + private static List listOfValuesFromFields(@NonNull final Object holder, @NonNull Field[] fields) throws Exception { final List fieldValues = new ArrayList<>(fields.length); for (Field field : fields) { diff --git a/lyra/src/test/java/com/fondesa/lyra/common/TestModels.java b/lyra/src/test/java/com/fondesa/lyra/common/TestModels.java index a3b17ae..1345749 100644 --- a/lyra/src/test/java/com/fondesa/lyra/common/TestModels.java +++ b/lyra/src/test/java/com/fondesa/lyra/common/TestModels.java @@ -18,8 +18,10 @@ import android.annotation.SuppressLint; import android.app.Activity; +import android.content.Context; import android.os.Parcel; import android.os.Parcelable; +import android.view.View; import com.fondesa.lyra.annotation.SaveState; @@ -186,4 +188,17 @@ public static class SaveStateActivity extends Activity { @SaveState float h; } + + public static class SaveStateView extends View { + @SaveState + protected ImplementedSerializable f; + @SaveState + public double g; + @SaveState + float h; + + public SaveStateView(Context context) { + super(context); + } + } } diff --git a/sample/build.gradle b/sample/build.gradle index ba5bee7..54d9038 100644 --- a/sample/build.gradle +++ b/sample/build.gradle @@ -22,7 +22,7 @@ new AndroidSharedPlugin( } ).apply(project) -final def libraryMavenCompileVersion = "1.0.0" +final def libraryMavenCompileVersion = "1.0.1" dependencies { compile "com.android.support:appcompat-v7:$ANDROID_SUPPORT_VERSION" diff --git a/sample/src/main/java/com/fondesa/lyra/sample/App.java b/sample/src/main/java/com/fondesa/lyra/sample/App.java index fa3c0dd..22fe633 100644 --- a/sample/src/main/java/com/fondesa/lyra/sample/App.java +++ b/sample/src/main/java/com/fondesa/lyra/sample/App.java @@ -7,10 +7,6 @@ import com.fondesa.lyra.coder.gson.DefaultGsonCoderRetriever; import com.fondesa.lyra.field.DefaultFieldsRetriever; -/** - * Created by antoniolig on 17/02/17. - */ - public class App extends Application { @Override public void onCreate() { @@ -25,4 +21,4 @@ public void onCreate() { } builder.build(); } -} +} \ No newline at end of file diff --git a/sample/src/main/java/com/fondesa/lyra/sample/BaseMainActivity.java b/sample/src/main/java/com/fondesa/lyra/sample/BaseMainActivity.java index 44d5479..a70fca2 100644 --- a/sample/src/main/java/com/fondesa/lyra/sample/BaseMainActivity.java +++ b/sample/src/main/java/com/fondesa/lyra/sample/BaseMainActivity.java @@ -7,9 +7,6 @@ import java.util.ArrayList; -/** - * Created by antoniolig on 24/02/17. - */ public class BaseMainActivity extends AppCompatActivity { @SaveState Byte mByte; diff --git a/sample/src/main/java/com/fondesa/lyra/sample/MainActivity.java b/sample/src/main/java/com/fondesa/lyra/sample/MainActivity.java index 9cf7ec3..225bedc 100644 --- a/sample/src/main/java/com/fondesa/lyra/sample/MainActivity.java +++ b/sample/src/main/java/com/fondesa/lyra/sample/MainActivity.java @@ -6,12 +6,13 @@ import android.view.View; import android.view.ViewGroup; import android.widget.Button; -import android.widget.FrameLayout; +import android.widget.LinearLayout; import android.widget.TextView; import com.fondesa.lyra.Lyra; import com.fondesa.lyra.annotation.SaveState; import com.fondesa.lyra.coder.gson.base.DefaultGsonCoder; +import com.fondesa.lyra.sample.model.AutoSaveEditText; import com.fondesa.lyra.sample.model.Model; import com.fondesa.lyra.sample.model.ParcelableModel; @@ -30,7 +31,8 @@ public class MainActivity extends BaseMainActivity { protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - FrameLayout root = new FrameLayout(this); + LinearLayout root = new LinearLayout(this); + root.setOrientation(LinearLayout.VERTICAL); int defaultPadding = getResources().getDimensionPixelSize(R.dimen.default_inner_padding); root.setPadding(defaultPadding, defaultPadding, defaultPadding, defaultPadding); @@ -55,11 +57,16 @@ public void onClick(View v) { printInfo(); } }); - FrameLayout.LayoutParams buttonParams = - new FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); + LinearLayout.LayoutParams buttonParams = + new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT); buttonParams.gravity = Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL; root.addView(button, buttonParams); + + AutoSaveEditText autoSaveEditText = new AutoSaveEditText(this); + autoSaveEditText.setId(R.id.auto_save_edit_text); + root.addView(autoSaveEditText, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)); + setContentView(root, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); if (Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH) { diff --git a/sample/src/main/java/com/fondesa/lyra/sample/model/AutoSaveEditText.java b/sample/src/main/java/com/fondesa/lyra/sample/model/AutoSaveEditText.java new file mode 100644 index 0000000..b88d1e1 --- /dev/null +++ b/sample/src/main/java/com/fondesa/lyra/sample/model/AutoSaveEditText.java @@ -0,0 +1,38 @@ +package com.fondesa.lyra.sample.model; + +import android.content.Context; +import android.os.Parcelable; +import android.support.v7.widget.AppCompatEditText; +import android.util.AttributeSet; + +import com.fondesa.lyra.Lyra; +import com.fondesa.lyra.annotation.SaveState; + +public class AutoSaveEditText extends AppCompatEditText { + @SaveState + CharSequence mText; + + public AutoSaveEditText(Context context) { + super(context); + } + + public AutoSaveEditText(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public AutoSaveEditText(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + @Override + public Parcelable onSaveInstanceState() { + mText = getText(); + return Lyra.instance().saveState(this, super.onSaveInstanceState()); + } + + @Override + public void onRestoreInstanceState(Parcelable state) { + super.onRestoreInstanceState(Lyra.instance().restoreState(this, state)); + setText(mText); + } +} \ No newline at end of file diff --git a/sample/src/main/java/com/fondesa/lyra/sample/model/CustomView.java b/sample/src/main/java/com/fondesa/lyra/sample/model/CustomView.java deleted file mode 100644 index ef7c50c..0000000 --- a/sample/src/main/java/com/fondesa/lyra/sample/model/CustomView.java +++ /dev/null @@ -1,41 +0,0 @@ -package com.fondesa.lyra.sample.model; - -import android.content.Context; -import android.os.Build; -import android.os.Parcelable; -import android.support.annotation.RequiresApi; -import android.util.AttributeSet; -import android.view.View; - -/** - * Created by antoniolig on 21/02/17. - */ - -public class CustomView extends View { - public CustomView(Context context) { - super(context); - } - - public CustomView(Context context, AttributeSet attrs) { - super(context, attrs); - } - - public CustomView(Context context, AttributeSet attrs, int defStyleAttr) { - super(context, attrs, defStyleAttr); - } - - @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) - public CustomView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { - super(context, attrs, defStyleAttr, defStyleRes); - } - - @Override - protected Parcelable onSaveInstanceState() { - return super.onSaveInstanceState(); - } - - @Override - protected void onRestoreInstanceState(Parcelable state) { - super.onRestoreInstanceState(state); - } -} diff --git a/sample/src/main/java/com/fondesa/lyra/sample/model/Model.java b/sample/src/main/java/com/fondesa/lyra/sample/model/Model.java index a296918..b862c3b 100644 --- a/sample/src/main/java/com/fondesa/lyra/sample/model/Model.java +++ b/sample/src/main/java/com/fondesa/lyra/sample/model/Model.java @@ -2,9 +2,6 @@ import java.io.Serializable; -/** - * Created by antoniolig on 18/02/17. - */ public class Model implements Serializable { private static final long serialVersionUID = 83186996445369715L; diff --git a/sample/src/main/java/com/fondesa/lyra/sample/model/ParcelableModel.java b/sample/src/main/java/com/fondesa/lyra/sample/model/ParcelableModel.java index ab15455..742b04e 100644 --- a/sample/src/main/java/com/fondesa/lyra/sample/model/ParcelableModel.java +++ b/sample/src/main/java/com/fondesa/lyra/sample/model/ParcelableModel.java @@ -3,10 +3,6 @@ import android.os.Parcel; import android.os.Parcelable; -/** - * Created by antoniolig on 18/02/17. - */ - public class ParcelableModel implements Parcelable { public int id; public String value; @@ -16,7 +12,7 @@ public ParcelableModel(int id, String value) { this.value = value; } - protected ParcelableModel(Parcel in) { + private ParcelableModel(Parcel in) { id = in.readInt(); value = in.readString(); } diff --git a/sample/src/main/res/values/ids.xml b/sample/src/main/res/values/ids.xml new file mode 100644 index 0000000..61d28b9 --- /dev/null +++ b/sample/src/main/res/values/ids.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file