Why MVI?
Introducing MVI
Introducing MVI
State
UI
Action
(ViewModel/Presenter)
(Fragment/ViewController)
User
Intent
Model
View
Reducer
State
Data source
Intent
Transformer
View
Model
Reducer
State
Data source
Intent
Transformer
View
Model
Side effect
Key concepts
- Unidirectional cycle of data
- Non-blocking
- Immutable state
MVI - benefits
MVI - benefits
- Simplicity
- Well defined contract
- Easy unit testing
MVI - benefits
- Screenshot testing
- Interaction testing
- Time-travel debugging
Testing apps
Use cases & repos
Backend
MVI
UI
Testing apps
Use cases & repos
Backend
MVI
UI
Testing apps
Use cases & repos
Backend
MVI
UI
(c) Martin Fowler
Testing apps
Use cases & repos
Backend
MVI
UI
Screenshot
Unit
Interaction
Contract / Integration
Testing apps
Screenshot | E2E UI | |
---|---|---|
Airbnb | ~ 30 000 | none |
Uber | thousands | handful |
Spotify | ~ 1 600 | ~ 500 |
Shopify | ~ 2 300 | ~ 20 |
"Building Mobile Apps at scale - 39 Engineering Challenges"
State
Screenshot testing
State
Screenshot testing
Screenshot testing
Screenshot testing
Screenshot testing
State
Interaction testing
Interaction testing
Interaction testing
Interaction testing
"button_close": {
"view": "ImageView",
"click": [
{
"onExitRequested": {
"params": [
createCircleRequestDomain: "{circleName="Test",coverPhoto="file://something.jpg"}"
]
}
}
]
}
Interaction testing
"button_close": {
"view": "ImageView",
"click": [
"onExitRequested": {
"params": [
createCircleRequestDomain: "{circleName="Test",coverPhoto="file://something.jpg"}"
]
}
]
}
Time travel
Reducer
State
Data source
Intent
Transformer
View
Model
Time travel
State
Intent
Time travel
t
MVP to MVI
MVP to MVI
- Unidirectional cycle of data
- Non-blocking
- Immutable state
MVP to MVI
- Create an MVI framework
- Translate MVP contract to MVI
Create an MVI framework
MVP to MVI
- Create an MVI framework
- Translate MVP contract to MVI
- Move processing to background
- Translate view interactions to MVI
- Refactor view
Create an MVI framework
MVI framework
Reducer
State
Data source
Intent
Transformer
View
Model
Side effect
Reducer
Transformer
Side effect
State
public class MviContainer<STATE, SIDE_EFFECT> {
STATE getState();
void intent(Intent<STATE, SIDE_EFFECT> intent);
void reduce(Reducer<STATE> reducer);
void postSideEffect(SIDE_EFFECT sideEffect);
Subscription subscribeToState(
Output<STATE> stateListener
);
Subscription subscribeToSideEffects(
Output<SIDE_EFFECT> sideEffectListener
);
}
Reducer
Transformer
Side effect
State
public class MviContainer<STATE, SIDE_EFFECT> {
STATE getState();
void intent(Intent<STATE, SIDE_EFFECT> intent);
void reduce(Reducer<STATE> reducer);
void postSideEffect(SIDE_EFFECT sideEffect);
Subscription subscribeToState(
Output<STATE> stateListener
);
Subscription subscribeToSideEffects(
Output<SIDE_EFFECT> sideEffectListener
);
}
Reducer
Transformer
Side effect
State
public class MviContainer<STATE, SIDE_EFFECT> {
STATE getState();
void intent(Intent<STATE, SIDE_EFFECT> intent);
void reduce(Reducer<STATE> reducer);
void postSideEffect(SIDE_EFFECT sideEffect);
Subscription subscribeToState(
Output<STATE> stateListener
);
Subscription subscribeToSideEffects(
Output<SIDE_EFFECT> sideEffectListener
);
}
Reducer
Transformer
Side effect
State
public class MviContainer<STATE, SIDE_EFFECT> {
STATE getState();
void intent(Intent<STATE, SIDE_EFFECT> intent);
void reduce(Reducer<STATE> reducer);
void postSideEffect(SIDE_EFFECT sideEffect);
Subscription subscribeToState(
Output<STATE> stateListener
);
Subscription subscribeToSideEffects(
Output<SIDE_EFFECT> sideEffectListener
);
}
Reducer
Transformer
Side effect
State
public class MviContainer<STATE, SIDE_EFFECT> {
STATE getState();
void intent(Intent<STATE, SIDE_EFFECT> intent);
void reduce(Reducer<STATE> reducer);
void postSideEffect(SIDE_EFFECT sideEffect);
Subscription subscribeToState(
Output<STATE> stateListener
);
Subscription subscribeToSideEffects(
Output<SIDE_EFFECT> sideEffectListener
);
}
Reducer
Transformer
Side effect
State
public class MviContainer<STATE, SIDE_EFFECT> {
STATE getState();
void intent(Intent<STATE, SIDE_EFFECT> intent);
void reduce(Reducer<STATE> reducer);
void postSideEffect(SIDE_EFFECT sideEffect);
Subscription subscribeToState(
Output<STATE> stateListener
);
Subscription subscribeToSideEffects(
Output<SIDE_EFFECT> sideEffectListener
);
}
Reducer
Transformer
Side effect
State
public abstract class MviContainerHost<STATE, SIDE_EFFECT> {
private final MviContainer<STATE, SIDE_EFFECT> container;
public MviContainerHost(STATE initialState) {
this.container = new MviContainer<>(initialState);
}
protected void intent(Intent<STATE, SIDE_EFFECT> intent) {
container.intent(intent);
}
public MviContainer<STATE, SIDE_EFFECT> getContainer() {
return container;
}
}
public void onDeleteCircleConfirmed() {
intent(mvi -> {
mvi.reduce(currentState -> currentState.withCommittingChanges(true));
final String circleId = mvi.getState().editedCircle().get().id();
circlesRepository.deleteCircle(circleId)
.subscribe(
result -> mvi.postSideEffect(CreateOrEditSideEffect.Simple.OPEN_AGGREGATED_FEED),
exception -> {
mvi.reduce(currentState -> currentState.withCommittingChanges(false));
mvi.postSideEffect(CreateOrEditSideEffect.Simple.SHOW_ERROR);
}
);
});
}
Reducer
Transformer
Side effect
State
public void onDeleteCircleConfirmed() {
intent(mvi -> {
mvi.reduce(currentState -> currentState.withCommittingChanges(true));
final String circleId = mvi.getState().editedCircle().get().id();
circlesRepository.deleteCircle(circleId)
.subscribe(
result -> mvi.postSideEffect(CreateOrEditSideEffect.Simple.OPEN_AGGREGATED_FEED),
exception -> {
mvi.reduce(currentState -> currentState.withCommittingChanges(false));
mvi.postSideEffect(CreateOrEditSideEffect.Simple.SHOW_ERROR);
}
);
});
}
Reducer
Transformer
Side effect
State
public void onDeleteCircleConfirmed() {
intent(mvi -> {
mvi.reduce(currentState -> currentState.withCommittingChanges(true));
final String circleId = mvi.getState().editedCircle().get().id();
circlesRepository.deleteCircle(circleId)
.subscribe(
result -> mvi.postSideEffect(CreateOrEditSideEffect.Simple.OPEN_AGGREGATED_FEED),
exception -> {
mvi.reduce(currentState -> currentState.withCommittingChanges(false));
mvi.postSideEffect(CreateOrEditSideEffect.Simple.SHOW_ERROR);
}
);
});
}
Reducer
Transformer
Side effect
State
public void onDeleteCircleConfirmed() {
intent(mvi -> {
mvi.reduce(currentState -> currentState.withCommittingChanges(true));
final String circleId = mvi.getState().editedCircle().get().id();
circlesRepository.deleteCircle(circleId)
.subscribe(
result -> mvi.postSideEffect(CreateOrEditSideEffect.Simple.OPEN_AGGREGATED_FEED),
exception -> {
mvi.reduce(currentState -> currentState.withCommittingChanges(false));
mvi.postSideEffect(CreateOrEditSideEffect.Simple.SHOW_ERROR);
}
);
});
}
Reducer
Transformer
Side effect
State
public void onDeleteCircleConfirmed() {
intent(mvi -> {
mvi.reduce(currentState -> currentState.withCommittingChanges(true));
final String circleId = mvi.getState().editedCircle().get().id();
circlesRepository.deleteCircle(circleId)
.subscribe(
result -> mvi.postSideEffect(CreateOrEditSideEffect.Simple.OPEN_AGGREGATED_FEED),
exception -> {
mvi.reduce(currentState -> currentState.withCommittingChanges(false));
mvi.postSideEffect(CreateOrEditSideEffect.Simple.SHOW_ERROR);
}
);
});
}
Reducer
Transformer
Side effect
State
public void onDeleteCircleConfirmed() {
intent(mvi -> {
mvi.reduce(currentState -> currentState.withCommittingChanges(true));
final String circleId = mvi.getState().editedCircle().get().id();
circlesRepository.deleteCircle(circleId)
.subscribe(
result -> mvi.postSideEffect(CreateOrEditSideEffect.Simple.OPEN_AGGREGATED_FEED),
exception -> {
mvi.reduce(currentState -> currentState.withCommittingChanges(false));
mvi.postSideEffect(CreateOrEditSideEffect.Simple.SHOW_ERROR);
}
);
});
}
Reducer
Transformer
Side effect
State
public void onDeleteCircleConfirmed() {
intent(mvi -> {
mvi.reduce(currentState -> currentState.withCommittingChanges(true));
final String circleId = mvi.getState().editedCircle().get().id();
circlesRepository.deleteCircle(circleId)
.subscribe(
result -> mvi.postSideEffect(CreateOrEditSideEffect.Simple.OPEN_AGGREGATED_FEED),
exception -> {
mvi.reduce(currentState -> currentState.withCommittingChanges(false));
mvi.postSideEffect(CreateOrEditSideEffect.Simple.SHOW_ERROR);
}
);
});
}
Reducer
Transformer
Side effect
State
Step1 - translate contract
State
Side effect
public interface CreateOrEditCircleContract {
interface View extends BaseView {
void showCreateSpinner();
void showEditSpinner();
void redirectToCircleFeedScreen(final String circleId);
void redirectToCircleInviteScreen(final String circleId);
void showCreateCircleErrorDialog();
void showEditCircleErrorDialog();
void showInitEditErrorDialog();
void enableCTA();
void disableCTA();
void exitWithCreateConfirmationDialog();
void exitWithEditConfirmationDialog();
void exitWithoutConfirmationDialog();
void exitToAggregatedFeed();
void showDeleteConfirmationDialog();
void setEditableCircleData(CircleConfigModel result);
}
abstract class Presenter extends SimplePresenter<View> {
...
}
}
OR
public interface CreateOrEditCircleContract {
interface View extends BaseView {
void showCreateSpinner();
void showEditSpinner();
void redirectToCircleFeedScreen(final String circleId);
void redirectToCircleInviteScreen(final String circleId);
void showCreateCircleErrorDialog();
void showEditCircleErrorDialog();
void showInitEditErrorDialog();
void enableCTA();
void disableCTA();
void exitWithCreateConfirmationDialog();
void exitWithEditConfirmationDialog();
void exitWithoutConfirmationDialog();
void exitToAggregatedFeed();
void showDeleteConfirmationDialog();
void setEditableCircleData(CircleConfigModel result);
}
abstract class Presenter extends SimplePresenter<View> {
...
}
}
@AutoValue
public abstract class CreateOrEditState {
public abstract Mode mode();
public enum Mode {
CREATE,
EDIT;
}
}
public interface CreateOrEditCircleContract {
interface View extends BaseView {
void showCreateSpinner();
void showEditSpinner();
void redirectToCircleFeedScreen(final String circleId);
void redirectToCircleInviteScreen(final String circleId);
void showCreateCircleErrorDialog();
void showEditCircleErrorDialog();
void showInitEditErrorDialog();
void enableCTA();
void disableCTA();
void exitWithCreateConfirmationDialog();
void exitWithEditConfirmationDialog();
void exitWithoutConfirmationDialog();
void exitToAggregatedFeed();
void showDeleteConfirmationDialog();
void setEditableCircleData(CircleConfigModel result);
}
abstract class Presenter extends SimplePresenter<View> {
...
}
}
@AutoValue
public abstract class CreateOrEditState {
public abstract Mode mode();
public abstract boolean committingChanges();
public enum Mode {
CREATE,
EDIT;
}
}
public interface CreateOrEditCircleContract {
interface View extends BaseView {
void redirectToCircleFeedScreen(final String circleId);
void redirectToCircleInviteScreen(final String circleId);
void showCreateCircleErrorDialog();
void showEditCircleErrorDialog();
void showInitEditErrorDialog();
void enableCTA();
void disableCTA();
void exitWithCreateConfirmationDialog();
void exitWithEditConfirmationDialog();
void exitWithoutConfirmationDialog();
void exitToAggregatedFeed();
void showDeleteConfirmationDialog();
void setEditableCircleData(CircleConfigModel result);
}
abstract class Presenter extends SimplePresenter<View> {
...
}
}
public interface CreateOrEditSideEffect {
enum Simple implements CreateOrEditSideEffect {
SHOW_ERROR
}
}
public interface CreateOrEditCircleContract {
interface View extends BaseView {
void redirectToCircleFeedScreen(final String circleId);
void redirectToCircleInviteScreen(final String circleId);
void showInitEditErrorDialog();
void enableCTA();
void disableCTA();
void exitWithCreateConfirmationDialog();
void exitWithEditConfirmationDialog();
void exitWithoutConfirmationDialog();
void exitToAggregatedFeed();
void showDeleteConfirmationDialog();
void setEditableCircleData(CircleConfigModel result);
}
abstract class Presenter extends SimplePresenter<View> {
...
}
}
public interface CreateOrEditSideEffect {
enum Simple implements CreateOrEditSideEffect {
SHOW_ERROR,
SHOW_EXIT_CONFIRMATION
}
}
public interface CreateOrEditCircleContract {
interface View extends BaseView {
void redirectToCircleFeedScreen(final String circleId);
void redirectToCircleInviteScreen(final String circleId);
void showInitEditErrorDialog();
void enableCTA();
void disableCTA();
void exitWithoutConfirmationDialog();
void exitToAggregatedFeed();
void showDeleteConfirmationDialog();
void setEditableCircleData(CircleConfigModel result);
}
abstract class Presenter extends SimplePresenter<View> {
...
}
}
public interface CreateOrEditSideEffect {
enum Simple implements CreateOrEditSideEffect {
CLOSE,
OPEN_AGGREGATED_FEED,
SHOW_DELETE_CONFIRMATION,
SHOW_EXIT_CONFIRMATION,
SHOW_ERROR,
SHOW_INIT_EDIT_ERROR
}
}
public interface CreateOrEditCircleContract {
interface View extends BaseView {
void redirectToCircleFeedScreen(final String circleId);
void redirectToCircleInviteScreen(final String circleId);
void enableCTA();
void disableCTA();
void setEditableCircleData(CircleConfigModel result);
}
abstract class Presenter extends SimplePresenter<View> {
...
}
}
public interface CreateOrEditSideEffect {
@AutoValue
abstract class OpenCircleInvite implements CreateOrEditSideEffect {
public abstract String circleId();
...
}
@AutoValue
abstract class OpenCircleFeed implements CreateOrEditSideEffect {
public abstract String circleId();
...
}
enum Simple implements CreateOrEditSideEffect {
...
}
}
public interface CreateOrEditCircleContract {
interface View extends BaseView {
void enableCTA();
void disableCTA();
void setEditableCircleData(CircleConfigModel result);
}
abstract class Presenter extends SimplePresenter<View> {
...
}
}
@AutoValue
public abstract class CreateOrEditState {
public abstract Mode mode();
public abstract boolean committingChanges();
public abstract Optional<CircleConfigModel> editedCircle();
public abstract boolean ctaEnabled();
public enum Mode {
CREATE,
EDIT;
}
}
@AutoValue
public abstract class CreateOrEditState {
public abstract Mode mode();
public abstract boolean committingChanges();
public abstract Optional<CircleConfigModel> editedCircle();
public abstract boolean ctaEnabled();
public CreateOrEditState withCommittingChanges(final boolean committingChanges) {
return toBuilder()
.committingChanges(committingChanges)
.build();
}
public abstract Builder toBuilder();
public static Builder builder() {
return new AutoValue_CreateOrEditState.Builder()
.mode(Mode.CREATE)
.ctaEnabled(false)
.committingChanges(false);
}
@AutoValue.Builder
public abstract static class Builder {
public abstract Builder mode(final Mode mode);
public abstract Builder editedCircle(final CircleConfigModel editedCircle);
public abstract Builder ctaEnabled(final boolean ctaEnabled);
public abstract Builder committingChanges(final boolean committingChanges);
public abstract CreateOrEditState build();
}
public enum Mode {
CREATE,
EDIT;
}
}
@Value.Immutable
public abstract class CreateOrEditState {
@Value.Default
public Mode mode() {
return Mode.CREATE;
}
public abstract Optional<CircleConfigModel> editedCircle();
@Value.Default
public boolean ctaEnabled() {
return false;
}
@Value.Default
public boolean committingChanges() {
return false;
}
public enum Mode {
CREATE,
EDIT;
}
}
public interface CreateOrEditCircleContract {
interface View extends BaseView {
// yay
}
abstract class Presenter extends SimplePresenter<View> {
public abstract void onCheckFormRequested(final CreateOrEditCircleRequestDomain createCircleRequestDomain);
public abstract void onFormSubmitted(final CreateOrEditCircleRequestDomain createCircleRequestDomain);
public abstract void onExitFromCreateCircleFlowRequested(final CreateOrEditCircleRequestDomain createCircleRequestDomain);
public abstract void onEditCircleRequested(final String circleIdToEdit);
public abstract void onDeleteCircleRequested();
public abstract void onDeleteCircleConfirmed();
}
}
Step 2 - processing in background
public class CreateOrEditCirclePresenter extends CreateOrEditCircleContract.Presenter {
public static final String TAG = "CreateCirclePresenter";
private final HasCreateCircleRequestChangedUseCase hasCreateCircleRequestChangedUseCase;
private final CirclesRepository circlesRepository;
private final PrepareEditCircleRequestUseCase prepareEditCircleRequestUseCase;
private final ShouldAllowFormSubmissionUseCase shouldAllowFormSubmissionUseCase;
private CreateOrEditCircleContract.Mode mode = CreateOrEditCircleContract.Mode.CREATE;
private CircleConfigModel editedCircle;
public CreateOrEditCirclePresenter(
final ShouldAllowFormSubmissionUseCase shouldAllowFormSubmissionUseCase,
final HasCreateCircleRequestChangedUseCase hasCreateCircleRequestChangedUseCase,
final CirclesRepository circlesRepository,
final PrepareEditCircleRequestUseCase prepareEditCircleRequestUseCase
) {
this.shouldAllowFormSubmissionUseCase = shouldAllowFormSubmissionUseCase;
this.hasCreateCircleRequestChangedUseCase = hasCreateCircleRequestChangedUseCase;
this.circlesRepository = circlesRepository;
this.prepareEditCircleRequestUseCase = prepareEditCircleRequestUseCase;
}
...
}
public class CreateOrEditCirclePresenter extends
MviContainerHost<ImmutableCreateOrEditState, CreateOrEditSideEffect> {
public static final String TAG = "CreateCirclePresenter";
private final HasCreateCircleRequestChangedUseCase hasCreateCircleRequestChangedUseCase;
private final CirclesRepository circlesRepository;
private final PrepareEditCircleRequestUseCase prepareEditCircleRequestUseCase;
private final ShouldAllowFormSubmissionUseCase shouldAllowFormSubmissionUseCase;
public CreateOrEditCirclePresenter(
final ShouldAllowFormSubmissionUseCase shouldAllowFormSubmissionUseCase,
final HasCreateCircleRequestChangedUseCase hasCreateCircleRequestChangedUseCase,
final CirclesRepository circlesRepository,
final PrepareEditCircleRequestUseCase prepareEditCircleRequestUseCase
) {
super(ImmutableCreateOrEditState.builder().build());
this.shouldAllowFormSubmissionUseCase = shouldAllowFormSubmissionUseCase;
this.hasCreateCircleRequestChangedUseCase = hasCreateCircleRequestChangedUseCase;
this.circlesRepository = circlesRepository;
this.prepareEditCircleRequestUseCase = prepareEditCircleRequestUseCase;
}
...
}
public class CreateOrEditCirclePresenter extends
MviContainerHost<ImmutableCreateOrEditState, CreateOrEditSideEffect> {
...
public void onDeleteCircleConfirmed() {
ifAttached(view -> view.showEditSpinner());
circlesRepository.deleteCircle(editedCircle.id())
.subscribeOnUiThread(
result -> ifAttached(view -> view.exitToAggregatedFeed()),
exception -> ifAttached(view -> view.showEditCircleErrorDialog())
);
}
...
}
public class CreateOrEditCirclePresenter extends
MviContainerHost<ImmutableCreateOrEditState, CreateOrEditSideEffect> {
...
public void onDeleteCircleConfirmed() {
intent(mvi -> {
ifAttached(view -> view.showEditSpinner());
circlesRepository.deleteCircle(editedCircle.id())
.subscribeOnUiThread(
result -> ifAttached(view -> view.exitToAggregatedFeed()),
exception -> ifAttached(view -> view.showEditCircleErrorDialog())
);
}
}
...
}
Step 3 - translate view interactions
public class CreateOrEditCirclePresenter extends
MviContainerHost<ImmutableCreateOrEditState, CreateOrEditSideEffect> {
...
public void onDeleteCircleConfirmed() {
intent(mvi -> {
ifAttached(view -> view.showEditSpinner());
circlesRepository.deleteCircle(editedCircle.id())
.subscribeOnUiThread(
result -> ifAttached(view -> view.exitToAggregatedFeed()),
exception -> ifAttached(view -> view.showEditCircleErrorDialog())
);
}
}
...
}
public class CreateOrEditCirclePresenter extends
MviContainerHost<ImmutableCreateOrEditState, CreateOrEditSideEffect> {
...
public void onDeleteCircleConfirmed() {
intent(mvi -> {
mvi.reduce(currentState -> currentState.withCommittingChanges(true));
circlesRepository.deleteCircle(mvi.getState().editedCircle().get().id())
.subscribe(
result ->
mvi.postSideEffect(CreateOrEditSideEffect.Simple.OPEN_AGGREGATED_FEED),
exception -> {
mvi.reduce(currentState -> currentState.withCommittingChanges(false));
mvi.postSideEffect(CreateOrEditSideEffect.Simple.SHOW_ERROR);
}
);
});
}
...
}
Step 4 - refactor view
override fun onCreate(savedInstanceState: Bundle?) {
...
presenter.subscribe(
lifecycleScope,
lifecycle,
::handleState,
::handleSideEffect
)
}
private fun handleState(createOrEditState: ImmutableCreateOrEditState) {
with(createOrEditState) {
updateCtaState(ctaEnabled())
if (committingChanges()) {
if (mode() == CreateOrEditState.Mode.CREATE) {
showCreateSpinner()
} else {
showEditSpinner()
}
} else {
hideSpinner()
}
if (mode == CreateOrEditState.Mode.EDIT &&
!editInitialized &&
editedCircle() != null) {
editInitialized = true
setEditableCircleData(editedCircle().get())
}
}
}
private fun handleSideEffect(sideEffect: CreateOrEditSideEffect) {
when (sideEffect) {
is CreateOrEditSideEffect.OpenCircleInvite -> redirectToCircleInviteScreen(sideEffect.circleId())
is CreateOrEditSideEffect.OpenCircleFeed -> redirectToCircleFeedScreen(sideEffect.circleId())
CreateOrEditSideEffect.Simple.CLOSE -> exitWithoutConfirmationDialog()
CreateOrEditSideEffect.Simple.OPEN_AGGREGATED_FEED -> exitToAggregatedFeed()
CreateOrEditSideEffect.Simple.SHOW_DELETE_CONFIRMATION -> showDeleteConfirmationDialog()
CreateOrEditSideEffect.Simple.SHOW_EXIT_CONFIRMATION -> if (mode == CreateOrEditState.Mode.CREATE) {
exitWithCreateConfirmationDialog()
} else {
exitWithEditConfirmationDialog()
}
CreateOrEditSideEffect.Simple.SHOW_ERROR -> if (mode == CreateOrEditState.Mode.CREATE) {
showCreateCircleErrorDialog()
} else {
showEditCircleErrorDialog()
}
CreateOrEditSideEffect.Simple.SHOW_INIT_EDIT_ERROR -> showInitEditErrorDialog()
}
}
How does it look on iOS?
class CreateEditCircleViewController: FabulousViewController {
...
@Inject({ $0.presenter.createCreateOrEditCirclePresenter() })
private var presenter: CreateOrEditCirclePresenter
private let disposeBag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
presenter
.subscribe(
stateHandler: { [weak self] state in
self?.handleState(state: state)
},
sideEffectHandler: { [weak self] sideEffect in
self?.handleSideEffect(sideEffect: sideEffect)
}
)
.disposed(by: disposeBag)
}
func handleState(state: CreateOrEditState) {
nameTextField.text = state.circleName()
if state.coverImage().isPresent(),
let imagePath = state.coverImage().get() as? String,
let imageUrl = URL(string: imagePath) {
imageView.image = try? UIImage(url: imageUrl)
}
saveButton.isEnabled = state.ctaEnabled()
spinner.isHidden = state.committingChanges().isFalse
}
@IBAction private func nameChanged(_ sender: Any) {
presenter.onNameChanged(with: nameTextField.text ?? "")
}
@IBAction private func saveButtonTapped(_ sender: Any) {
presenter.onFormSubmitted()
}
func handleSideEffect(sideEffect: CreateOrEditSideEffect) {
switch sideEffect {
case let openCircleInvite as CreateOrEditSideEffect_OpenCircleInvite:
openCircleInviteScreen(circleId: openCircleInvite.circleId())
case let openCircleFeed as CreateOrEditSideEffect_OpenCircleFeed:
openCircleFeedScreen(circleId: openCircleFeed.circleId())
case let simpleEffect as CreateOrEditSideEffect_Simple:
switch simpleEffect.toNSEnum() {
case CreateOrEditSideEffect_Simple_Enum.CLOSE:
dismissScreen()
case CreateOrEditSideEffect_Simple_Enum.OPEN_AGGREGATED_FEED:
dismissToAggregatedFeed()
case CreateOrEditSideEffect_Simple_Enum.SHOW_DELETE_CONFIRMATION:
showDeleteConfirmation()
case CreateOrEditSideEffect_Simple_Enum.SHOW_EXIT_CONFIRMATION:
showExitConfiramtion()
case CreateOrEditSideEffect_Simple_Enum.SHOW_ERROR:
showErrorDialog()
case CreateOrEditSideEffect_Simple_Enum.SHOW_INIT_EDIT_ERROR:
showInitEditErrorDialog()
default:
break
}
default:
break
}
}
Unit testing
@Test
fun `should exit when delete is confirmed and completes successfully`() {
val fakeCircle = fixture<CircleConfigModel>()
val presenter = createPresenter(
circlesRepository = mock {
on { deleteCircle(fakeCircle.id()) } doReturn Task.empty()
}
)
val initialState = ImmutableCreateOrEditState.builder()
.mode(CreateOrEditState.Mode.EDIT)
.editedCircle(fakeCircle)
.build()
val testPresenter = presenter.test(initialState)
testPresenter.testIntent {
onDeleteCircleConfirmed()
}
testPresenter.assert(initialState) {
states(
{ withCommittingChanges(true) }
)
postedSideEffects(CreateOrEditSideEffect.Simple.OPEN_AGGREGATED_FEED)
}
}
testPresenter.assert(defaultInitialState) {
states(
{ withCommittingChanges(true).withMode(CreateOrEditState.Mode.EDIT) },
{ withCommittingChanges(false) }
)
}
Conclusions
To do
- Improve lifecycle management (e.g. ViewModel)
- Side effect caching
- State saving
Next steps
- Do we want this in our app?
- Draw up implementation plan
- Implement missing/extra features
Thanks!
Why MVI?
By Mikołaj Leszczyński
Why MVI?
- 552