Skip to content

Instantly share code, notes, and snippets.

@esmasui
Last active August 29, 2015 14:20
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save esmasui/ba1a1ac035870329ed4a to your computer and use it in GitHub Desktop.
Save esmasui/ba1a1ac035870329ed4a to your computer and use it in GitHub Desktop.
AndroidのInstanceStateの保存・復元をアノテーションでやる再発明
/*
* Copyright (C) 2015 uPhyca Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.uphyca.android;
import android.app.Activity;
import android.net.Uri;
import android.os.Bundle;
import jp.co.oneteam.phonebook.ui.InstanceState;
import jp.co.oneteam.phonebook.ui.InstanceStateAnnotations;
public class ExampleActivity extends Activity{
@InstanceState
Uri exmpleUri;
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
InstanceStateAnnotations.saveInstanceState(this, outState);
}
@Override
protected void onRestoreInstanceState(Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
InstanceStateAnnotations.restoreInstanceState(this, savedInstanceState);
}
}
/*
* Copyright (C) 2015 uPhyca Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.uphyca.android;
import android.os.Bundle;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
/**
* InstanceStateとして状態の保存・復元の対象となるフィールドに注釈する。
* InstanceStateの保存の際に {@link InstanceStateAnnotations#saveInstanceState(Object, Bundle)}、復元の際に {@link InstanceStateAnnotations#restoreInstanceState(Object, Bundle)} を呼び出すことで注釈したフィールドの保存・復元が行われる。
*
* @see InstanceStateAnnotations
*/
@Target(ElementType.FIELD)
@Retention(RUNTIME)
public @interface InstanceState {
}
/*
* Copyright (C) 2015 uPhyca Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package jp.co.oneteam.phonebook.ui;
import android.os.Bundle;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* InstanceStateの保存・復元を行うユーティリティクラス
*
* @see InstanceState
* @see android.app.Activity#onSaveInstanceState(Bundle)
* @see android.app.Activity#onRestoreInstanceState(Bundle)
*/
public abstract class InstanceStateAnnotations {
private static class MetaInfo {
final Field field;
final Method putMethod;
final Method getMethod;
public MetaInfo(Field field, Method putMethod, Method getMethod) {
this.field = field;
this.putMethod = putMethod;
this.getMethod = getMethod;
}
}
private static final Map<Class<?>, List<MetaInfo>> metaInfoListMap = new HashMap<>();
private InstanceStateAnnotations() {
throw new UnsupportedOperationException("No instances");
}
public static void saveInstanceState(Object target, Bundle outState) {
Bundle myState = new Bundle();
final Class<?> clazz = target.getClass();
for (MetaInfo each : getMetaInfoList(clazz)) {
try {
each.putMethod.invoke(myState, each.field.getName(), each.field.get(target));
} catch (IllegalAccessException e) {
throw new IllegalStateException(e);
} catch (InvocationTargetException e) {
throw new IllegalStateException(e);
}
}
outState.putBundle(clazz.getCanonicalName(), myState);
}
public static void restoreInstanceState(Object target, Bundle savedInstanceState) {
if (savedInstanceState == null) {
return;
}
final Class<?> clazz = target.getClass();
Bundle myState = savedInstanceState.getBundle(clazz.getCanonicalName());
if (myState == null) {
return;
}
for (MetaInfo each : getMetaInfoList(clazz)) {
try {
each.field.set(target, each.getMethod.invoke(myState, each.field.getName()));
} catch (IllegalAccessException e) {
throw new IllegalStateException(e);
} catch (InvocationTargetException e) {
throw new IllegalStateException(e);
}
}
}
private static List<MetaInfo> getMetaInfoList(Class<?> clazz) {
List<MetaInfo> metaInfoList = metaInfoListMap.get(clazz);
if (metaInfoList != null) {
return metaInfoList;
}
metaInfoList = buildMetaInfoList(clazz);
metaInfoListMap.put(clazz, metaInfoList);
return metaInfoList;
}
private static List<MetaInfo> buildMetaInfoList(Class<?> clazz) {
List<MetaInfo> metaInfoList = new ArrayList<>();
for (Field each : clazz.getDeclaredFields()) {
if (!each.isAnnotationPresent(InstanceState.class)) {
continue;
}
final Method putMethod = findPutMethod(each.getType());
if (putMethod == null) {
throw new IllegalStateException("No put method found for " + each.getType().getName());
}
final Method getMethod = findGetMethod(each.getType());
if (getMethod == null) {
throw new IllegalStateException("No get method found for " + each.getType().getName());
}
metaInfoList.add(new MetaInfo(each, putMethod, getMethod));
}
return metaInfoList;
}
private static Method findPutMethod(Class<?> type) {
for (Method each : Bundle.class.getMethods()) {
if (!each.getName().startsWith("put")) {
continue;
}
final Class<?>[] parameterTypes = each.getParameterTypes();
if (parameterTypes.length != 2) {
continue;
}
if (!parameterTypes[0].equals(String.class)) {
continue;
}
if (!parameterTypes[1].isAssignableFrom(type)) {
continue;
}
return each;
}
return null;
}
private static Method findGetMethod(Class<?> type) {
for (Method each : Bundle.class.getMethods()) {
if (!each.getName().startsWith("get")) {
continue;
}
final Class<?>[] parameterTypes = each.getParameterTypes();
if (parameterTypes.length != 1) {
continue;
}
if (!parameterTypes[0].equals(String.class)) {
continue;
}
final Class<?> returnType = each.getReturnType();
if (!returnType.isAssignableFrom(type)) {
continue;
}
return each;
}
return null;
}
}
/*
* Copyright (C) 2015 uPhyca Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.uphyca.android;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.support.test.runner.AndroidJUnit4;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @see InstanceStateAnnotations
*/
@RunWith(AndroidJUnit4.class)
public class InstanceStateAnnotationsTest {
static class TestTarget {
@InstanceState
String stringField;
@InstanceState
int intField;
@InstanceState
Long longWrapperField;
@InstanceState
Uri uriField;
@InstanceState
Intent intentField;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
TestTarget target = (TestTarget) o;
if (intField != target.intField) return false;
if (stringField != null ? !stringField.equals(target.stringField) : target.stringField != null)
return false;
if (longWrapperField != null ? !longWrapperField.equals(target.longWrapperField) : target.longWrapperField != null)
return false;
if (uriField != null ? !uriField.equals(target.uriField) : target.uriField != null)
return false;
return !(intentField != null ? !intentField.equals(target.intentField) : target.intentField != null);
}
@Override
public int hashCode() {
int result = stringField != null ? stringField.hashCode() : 0;
result = 31 * result + intField;
result = 31 * result + (longWrapperField != null ? longWrapperField.hashCode() : 0);
result = 31 * result + (uriField != null ? uriField.hashCode() : 0);
result = 31 * result + (intentField != null ? intentField.hashCode() : 0);
return result;
}
}
@Before
public void setUp() throws Exception {
}
@After
public void tearDown() throws Exception {
}
@Test
public void saveAndRestoreInstanceState() throws Exception {
TestTarget target = new TestTarget();
target.stringField = "string";
target.intField = Integer.MAX_VALUE;
target.longWrapperField = Long.MAX_VALUE;
target.uriField = Uri.parse("http://example.com");
target.intentField = new Intent(Intent.ACTION_VIEW, Uri.parse("http://example.com"));
Bundle outState = new Bundle();
InstanceStateAnnotations.saveInstanceState(target, outState);
final TestTarget restoreTarget = new TestTarget();
InstanceStateAnnotations.restoreInstanceState(restoreTarget, outState);
assertThat(restoreTarget).isEqualTo(target);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment