Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions ChangeLog.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
Version:1.6.0
* Add BaseManagerImpl. Logic and data shared by multiple controllers can be put into managers and injected into controllers.
* Delegate fragment's onViewReady lifecycle will be called after state of all controllers are restored if activity is killed by OS

Version:1.5.3
* MvcGraph able to inject concrete class with a public constructor
* Fix bug that sub fragments' controller do not restore state
Expand Down
4 changes: 2 additions & 2 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,8 @@ ext {
gitUrl = 'https://github.com/kejunxia/AndroidMvc.git' // Git repository URL
version = [
major: 1,
minor: 5,
patch : 3
minor: 6,
patch : 0
]
libGroup = 'com.shipdream'
libVersion = "${version.major}.${version.minor}.${version.patch}"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,13 +75,14 @@ public void onConstruct() {
}

private MODEL createModelInstance() {
if (getModelClassType() == null) {
Class<MODEL> type = getStateType();
if (type == null) {
return null;
} else {
try {
return new ReflectUtils.newObjectByType<>(getModelClassType()).newInstance();
return new ReflectUtils.newObjectByType<>(type).newInstance();
} catch (Exception e) {
throw new RuntimeException("Fail to instantiate model by its default constructor");
throw new RuntimeException("Fail to instantiate state by its default constructor");
}
}
}
Expand All @@ -97,10 +98,9 @@ public void onDisposed() {
}

/**
* Model represents the state of view that this controller is managing.
* @return @return Null if the controller doesn't need to get its state saved and restored
* automatically when {@link #getModelClassType()} returns null. e.g. The controller
* always loads resource from remote services so that its state can be thought persisted by the
* remote services. Otherwise returns the model of the controller
* automatically when {@link #getModelClassType()} returns null. Otherwise the model.
*/
@Override
public MODEL getModel() {
Expand All @@ -112,8 +112,7 @@ public MODEL getModel() {
* which is also the model the controller.
*
* @return Null if the controller doesn't need to get its state saved and restored
* automatically. e.g. The controller always loads resource from remote services so that
* its state can be thought persisted by the remote services. Otherwise the model of the controller
* automatically. Otherwise same as {@link #getModel()}
*/
@Override
final public MODEL getState() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,8 +109,12 @@ public void post(Object event) {
} catch (IllegalAccessException e) {
logger.warn(e.getMessage(), e);
} catch (InvocationTargetException e) {
String msg = e.getMessage();
if (msg == null || msg.isEmpty() && e.getCause() != null) {
msg = e.getCause().getMessage();
}
throw new RuntimeException("Not able to post event - "
+ event.getClass().getName() + " due to error: " + e.getMessage(), e);
+ event.getClass().getName() + " due to error: " + msg, e.getCause());
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package com.shipdream.lib.android.mvc.manager;

import com.shipdream.lib.android.mvc.Constructable;
import com.shipdream.lib.android.mvc.Disposable;
import com.shipdream.lib.android.mvc.StateKeeper;
import com.shipdream.lib.android.mvc.StateManaged;
import com.shipdream.lib.poke.util.ReflectUtils;

/**
* Abstract manager with state that needs to be managed. A stateful manager can be shared by multiple
* controllers. For example, LoginManager is an good example that will manage the state of logged in
* user. The log in user object usually is a part of the state to remember the logged in user.
*
* <p>
* Managers should only be serving controllers and not visible to views.
* </p>
*/
public abstract class BaseManagerImpl<STATE> implements StateManaged<STATE>,
Constructable, Disposable {
private STATE state;

/**
* Bind state to this manager.
* @param state non-null state
* @throws IllegalArgumentException thrown when null is being bound
*/
public void bindState(STATE state) {
if (state == null) {
throw new IllegalArgumentException("Can't bind a null state to a manage explicitly.");
}
this.state = state;
}

/**
* Called when the manager is injected for the first time or restored when a new instance of
* this manager needs to be instantiated.
*
* <p>The model of the manager will be instantiated by model's default no-argument constructor.
* However, if the manager needs to be restored, a new instance of state restored by
* {@link #restoreState(Object)} will replace the state created by this method.</p>
*/
public void onConstruct() {
state = createModelInstance();
}

private STATE createModelInstance() {
Class<STATE> type = getStateType();
if (type == null) {
return null;
} else {
try {
return new ReflectUtils.newObjectByType<>(type).newInstance();
} catch (Exception e) {
throw new RuntimeException("Fail to instantiate state by its default constructor");
}
}
}

/**
* Called when the manager is disposed. This occurs when the manager is de-referenced and
* not retained by any other objects.
*/
@Override
public void onDisposed() {
}

/**
* @return Null if the manager doesn't need to get its state saved and restored automatically.
*/
@Override
final public STATE getState() {
return state;
}

/**
* @return The class type of the state of the controller that will be used by this manager to
* instantiate its state in {@link #onConstruct()}
*/
@Override
abstract public Class<STATE> getStateType();

/**
* Restore the state of the manager.
* <p>
* Note that if the manager doesn't need its state saved and restored automatically and return
* null in {@link #getStateType()}, then this method will have no effect.
* </p>
*
* @param restoredState The restored state by {@link StateKeeper} that will be bound to the
* manager when the views of app are restored.
*/
@Override
final public void restoreState(STATE restoredState) {
if (getStateType() != null) {
bindState(restoredState);
}
onRestored();
}

/**
* Called when the manager is restored after {@link #restoreState(Object)} is called.
*/
public void onRestored() {
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
package com.shipdream.lib.android.mvc.manager;

import org.junit.Assert;
import org.junit.Test;

public class TestStatefulManager {
@Test(expected = IllegalArgumentException.class)
public void should_throw_exception_when_bind_null_to_stateful_manager() {
BaseManagerImpl manager = new BaseManagerImpl() {
@Override
public Class getStateType() {
return String.class;
}
};

manager.bindState(null);
}

@Test
public void should_rebind_state_after_restoring_manager() {
BaseManagerImpl<String> manager = new BaseManagerImpl() {

@Override
public Class getStateType() {
return String.class;
}
};

Assert.assertNull(manager.getState());

manager.restoreState("A");

Assert.assertEquals("A", manager.getState());
}

@Test
public void should_call_on_restore_call_back_after_manager_is_restored() {
class MyManager extends BaseManagerImpl<String> {
private boolean called = false;

@Override
public Class getStateType() {
return String.class;
}

@Override
public void onRestored() {
super.onRestored();
called = true;
}
};

MyManager manager = new MyManager();

Assert.assertFalse(manager.called);

manager.restoreState("A");

Assert.assertTrue(manager.called);
}

public void should_create_state_instance_on_construct_when_the_state_type_is_specified_for_a_stateful_manager() {
class MyManager extends BaseManagerImpl<String> {
@Override
public Class getStateType() {
return String.class;
}
};
MyManager manager = new MyManager();

Assert.assertNull(manager.getState());

manager.onConstruct();

Assert.assertNotNull(manager.getState());
}

public void should_NOT_create_state_instance_on_construct_when_the_state_type_is_null_for_a_stateful_manager() {
class MyManager extends BaseManagerImpl {
@Override
public Class getStateType() {
return null;
}
};
MyManager manager = new MyManager();

Assert.assertNull(manager.getState());

manager.onConstruct();

Assert.assertNull(manager.getState());
}

@Test(expected = RuntimeException.class)
public void should_throw_excpetion_out_when_creating_state_failed() {
class BadClass {
{int x = 1 / 0;}
}

class MyManager extends BaseManagerImpl<BadClass> {
@Override
public Class<BadClass> getStateType() {
return BadClass.class;
}
};

MyManager manager = new MyManager();

manager.onConstruct();
}

@Test(expected = IllegalArgumentException.class)
public void should_throw_excpetion_when_binding_null_to_stateful_manager() {
class MyManager extends BaseManagerImpl<String> {
@Override
public Class<String> getStateType() {
return String.class;
}
};

MyManager manager = new MyManager();

manager.bindState(null);
}

@Test
public void should_be_able_to_successfully_bind_state_to_stateful_manager() {
class MyManager extends BaseManagerImpl<String> {
@Override
public Class<String> getStateType() {
return String.class;
}
};

MyManager manager = new MyManager();

Assert.assertNotEquals("B", manager.getState());

manager.bindState("B");

Assert.assertEquals("B", manager.getState());
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
import android.util.Log;

import com.shipdream.lib.android.mvc.view.BaseTestCase;
import com.shipdream.lib.android.mvc.view.nav.MvcTestActivityNavigation;
import com.shipdream.lib.android.mvc.view.test.R;

import org.junit.Test;
Expand All @@ -30,10 +29,10 @@
import static android.support.test.espresso.matcher.ViewMatchers.withId;
import static android.support.test.espresso.matcher.ViewMatchers.withText;

public class TestInjectedStateManagedObjects extends BaseTestCase<InjectionTestActivity> {
public class TestInjectedStateManagedObjects extends BaseTestCase<InjectionTestActivityStateManagedObjects> {

public TestInjectedStateManagedObjects() {
super(InjectionTestActivity.class);
super(InjectionTestActivityStateManagedObjects.class);
}

@Override
Expand All @@ -43,15 +42,12 @@ public void tearDown() throws Exception {
}

@Test
public void should_manage_state_of_nested_stateManagedObjects() throws Throwable {
public void test_should_manage_state_of_nested_stateManagedObjects() throws Throwable {
if (!isDontKeepActivities()) {
Log.i(getClass().getSimpleName(), "testLifeCyclesWhenKeepActivities not tested as Don't Keep Activities setting is disabled");
return;
}

navigationController.navigate(this).to(MvcTestActivityNavigation.Loc.D);
waitTest();

onView(withId(R.id.textA)).check(matches(withText("")));

onView(withId(R.id.fragment_injection_root)).perform(click());
Expand All @@ -63,10 +59,10 @@ public void should_manage_state_of_nested_stateManagedObjects() throws Throwable
onView(withId(R.id.textA)).check(matches(withText("2:B")));

pressHome();
waitTest(2000);
waitTest(1000);

bringBack();
waitTest(2000);
waitTest(1000);

onView(withId(R.id.fragment_injection_root)).perform(click());
onView(withId(R.id.textA)).check(matches(withText("3:C")));
Expand Down
Loading