Skip to content

Commit 79ece5a

Browse files
author
Arkadiusz Pałka
committed
fix: change state initialization lifecycle
Request change for android. Activity is calling save instance state on child fragments before the view was attached when the currentState was uninitialized which leads to crash. We are using currentState in onSaveInstanceState to keep state in android. Presenter's initializeState gives flexibility to init a state depending on platform specific requirements. Also fixed type, expose exceptions classes to give possibility to handle exceptions from library.
1 parent a49f4f9 commit 79ece5a

File tree

9 files changed

+72
-18
lines changed

9 files changed

+72
-18
lines changed
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
package pl.valueadd.mvi.exception
22

3-
internal class ViewNotAttachedException :
3+
class ViewNotAttachedException internal constructor() :
44
RuntimeException("View was called before that has been attached to presenter")
Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
package pl.valueadd.mvi.exception
22

3-
import java.lang.RuntimeException
4-
5-
internal class ViewWasNotDetachedException : RuntimeException("Detach previous view first.")
3+
class ViewWasNotDetachedException internal constructor() :
4+
RuntimeException("Detach previous view first.")

mvi-presenter/src/main/java/pl/valueadd/mvi/presenter/BaseMviPresenter.kt

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,9 @@ abstract class BaseMviPresenter<VS : IBaseViewState, PS : IBasePartialState, VI
2929

3030
/**
3131
* Returns [view][IBaseView] but may throw [ViewNotAttachedException] if called in wrong place.
32+
* @throws ViewNotAttachedException if called in wrong place
3233
*/
34+
@get:Throws(ViewNotAttachedException::class)
3335
protected val view: V
3436
get() = internalView ?: throw ViewNotAttachedException()
3537

@@ -53,12 +55,12 @@ abstract class BaseMviPresenter<VS : IBaseViewState, PS : IBasePartialState, VI
5355
private var internalView: V? = null
5456

5557
/**
56-
* Use to determine when a intents have to be binded.
58+
* Use to determine when a intents have to be bound.
5759
*/
5860
private var wasViewAttachedOnce = false
5961

6062
/**
61-
* A disposable container of temporarily binded view intents.
63+
* A disposable container of temporarily bound view intents.
6264
*/
6365
private var currentViewIntentsDisposable: Disposable? = null
6466

@@ -68,12 +70,12 @@ abstract class BaseMviPresenter<VS : IBaseViewState, PS : IBasePartialState, VI
6870
private var viewStateReducerDisposable: Disposable? = null
6971

7072
/**
71-
* A disposable of currently binded consumer for emission of wrapped intents.
73+
* A disposable of currently bound consumer for emission of wrapped intents.
7274
*/
7375
private var viewStateConsumerDisposable: Disposable? = null
7476

7577
/**
76-
* A subject to pass emission of wrapped intents to currently binded view's consumer.
78+
* A subject to pass emission of wrapped intents to currently bound view's consumer.
7779
*/
7880
private val viewStateBehaviorSubject: BehaviorSubject<VS> by lazy {
7981
BehaviorSubject.createDefault(currentState)
@@ -89,6 +91,16 @@ abstract class BaseMviPresenter<VS : IBaseViewState, PS : IBasePartialState, VI
8991

9092
//region Lifecycle methods
9193

94+
/**
95+
* If view hasn't attached the first time, the presenter calls [IBaseView.provideInitialViewState]
96+
* to set initial value of [currentState]
97+
*/
98+
override fun initializeState(view: V) {
99+
if (!wasViewAttachedOnce) {
100+
currentState = view.provideInitialViewState()
101+
}
102+
}
103+
92104
/**
93105
* If view is attached the first time, the presenter subscribes its provided intents.
94106
* Each time subscribes to view state's consumer and bind view's intents.
@@ -97,6 +109,7 @@ abstract class BaseMviPresenter<VS : IBaseViewState, PS : IBasePartialState, VI
97109
*
98110
* @throws ViewWasNotDetachedException if previous view was not detached
99111
*/
112+
@Throws(ViewWasNotDetachedException::class)
100113
override fun attachView(view: V) {
101114
if (this.internalView != null) {
102115
throw ViewWasNotDetachedException()
@@ -105,7 +118,6 @@ abstract class BaseMviPresenter<VS : IBaseViewState, PS : IBasePartialState, VI
105118
this.internalView = view
106119

107120
if (!wasViewAttachedOnce) {
108-
currentState = view.provideInitialViewState()
109121
startObservingCurrentViewStateSubject()
110122
wasViewAttachedOnce = true
111123
}

mvi-presenter/src/main/java/pl/valueadd/mvi/presenter/IMviPresenter.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ package pl.valueadd.mvi.presenter
22

33
interface IMviPresenter<V : IBaseView<*, *>> {
44

5+
fun initializeState(view: V)
6+
57
fun attachView(view: V)
68

79
fun detachView()

mvi-presenter/src/test/java/pl/valueadd/mvi/presenter/BaseMviPresenterTest.kt

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ import io.mockk.verify
88
import io.reactivex.Observable
99
import io.reactivex.schedulers.Schedulers
1010
import io.reactivex.subjects.PublishSubject
11-
import org.junit.jupiter.api.Assertions
11+
/* ktlint-disable no-wildcard-imports */
12+
import org.junit.jupiter.api.Assertions.*
1213
import org.junit.jupiter.api.BeforeEach
1314
import org.junit.jupiter.api.Test
1415
import org.junit.jupiter.api.extension.ExtendWith
@@ -47,11 +48,12 @@ class BaseMviPresenterTest {
4748
val firstView = createMockView(Observable.never())
4849
val secondView: IBaseView<TestViewState, TestViewIntent> = mockk()
4950

51+
presenter.initializeState(firstView)
5052
presenter.attachView(firstView)
5153

5254
// When
5355
// Then
54-
Assertions.assertThrows(ViewWasNotDetachedException::class.java) {
56+
assertThrows(ViewWasNotDetachedException::class.java) {
5557
presenter.attachView(secondView)
5658
}
5759
}
@@ -61,6 +63,7 @@ class BaseMviPresenterTest {
6163
// Given
6264
val viewIntentsSubject = PublishSubject.create<TestViewIntent>()
6365
val mockView = createMockView(viewIntentsSubject)
66+
presenter.initializeState(mockView)
6467

6568
// When
6669
presenter.attachView(mockView)
@@ -75,14 +78,15 @@ class BaseMviPresenterTest {
7578
// Given
7679
val viewIntentsSubject = PublishSubject.create<TestViewIntent>()
7780
val mockView = createMockView(viewIntentsSubject)
81+
presenter.initializeState(mockView)
7882
presenter.attachView(mockView)
7983

8084
// When
8185
presenter.detachView()
8286

8387
// Then
8488
verify(exactly = 1) { mockView.provideViewIntents() }
85-
assert(viewIntentsSubject.hasObservers() == false)
89+
assertFalse(viewIntentsSubject.hasObservers())
8690
}
8791

8892
@Test
@@ -94,6 +98,7 @@ class BaseMviPresenterTest {
9498
val mockView = createMockView(Observable.never())
9599

96100
every { mockReducer.reduce(any(), testPresenterPartialState) } returns expectedTestViewState
101+
presenter.initializeState(mockView)
97102
presenter.attachView(mockView)
98103

99104
// When
@@ -117,6 +122,7 @@ class BaseMviPresenterTest {
117122
val reducedViewState = TestViewState(1)
118123
every { mockMapper.mapViewIntentToPartialState(testViewIntent) } returns testPartialStatePublishSubject
119124
every { mockReducer.reduce(any(), testPartialState) } returns reducedViewState
125+
presenter.initializeState(mockView)
120126
presenter.attachView(mockView)
121127
viewIntentsSubject.onNext(testViewIntent) // For example user press login button
122128

@@ -139,6 +145,7 @@ class BaseMviPresenterTest {
139145

140146
every { mockMapper.mapViewIntentToPartialState(testViewIntent) } returns testPartialStatePublishSubject
141147
every { mockReducer.reduce(any(), testPartialState) } returns reducedViewState
148+
presenter.initializeState(mockView)
142149
presenter.attachView(mockView)
143150
viewIntentsSubject.onNext(testViewIntent) // For example user press login button
144151
presenter.detachView() // View of fragment is destroyed by system
@@ -161,6 +168,7 @@ class BaseMviPresenterTest {
161168
val mockView = createMockView(viewIntentsSubject)
162169
every { mockMapper.mapViewIntentToPartialState(testViewIntent) } returns testPartialStatePublishSubject
163170
every { mockReducer.reduce(any(), testPartialState) } returns reducedViewState
171+
presenter.initializeState(mockView)
164172
presenter.attachView(mockView)
165173
viewIntentsSubject.onNext(testViewIntent) // For example user press login button
166174

@@ -186,6 +194,7 @@ class BaseMviPresenterTest {
186194

187195
every { mockMapper.mapViewIntentToPartialState(testViewIntent) } returns testPartialStatePublishSubject
188196
every { mockReducer.reduce(any(), testPartialState) } returns reducedViewState
197+
presenter.initializeState(mockView)
189198
presenter.attachView(mockView)
190199
viewIntentsSubject.onNext(testViewIntent) // For example user press login button
191200

@@ -208,15 +217,16 @@ class BaseMviPresenterTest {
208217

209218
every { mockMapper.mapViewIntentToPartialState(testViewIntent) } returns testPartialStatePublishSubject
210219
every { mockReducer.reduce(any(), testPartialState) } returns reducedViewState
220+
presenter.initializeState(mockView)
211221
presenter.attachView(mockView)
212222
viewIntentsSubject.onNext(testViewIntent) // For example user press login button
213223

214224
// When
215225
presenter.destroy()
216226

217227
// Then
218-
assert(testPartialStatePublishSubject.hasObservers() == false)
219-
assert(presenterPublishSubject.hasObservers() == false)
228+
assertFalse(testPartialStatePublishSubject.hasObservers())
229+
assertFalse(presenterPublishSubject.hasObservers())
220230
}
221231

222232
@Test
@@ -228,6 +238,7 @@ class BaseMviPresenterTest {
228238
every { mockThrowable.stackTrace } returns emptyArray()
229239
every { mockThrowable.cause } returns null
230240
every { mockTestLogger.logError(any()) } returns Unit
241+
presenter.initializeState(mockView)
231242
presenter.attachView(mockView)
232243

233244
// When
@@ -246,6 +257,7 @@ class BaseMviPresenterTest {
246257
every { mockThrowable.stackTrace } returns emptyArray()
247258
every { mockThrowable.cause } returns null
248259
every { mockTestLogger.logError(any()) } returns Unit
260+
presenter.initializeState(mockView)
249261
presenter.attachView(mockView)
250262

251263
// When

mvi/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ android {
2727
unitTests.all {
2828
useJUnitPlatform()
2929
}
30+
unitTests.returnDefaultValues = true
3031
}
3132

3233
buildTypes {

mvi/src/main/java/pl/valueadd/mvi/fragment/delegate/fragment/MviFragmentDelegateImpl.kt

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package pl.valueadd.mvi.fragment.delegate.fragment
22

33
import android.os.Bundle
4+
import androidx.annotation.CallSuper
45
import pl.valueadd.mvi.presenter.BaseMviPresenter
56
import pl.valueadd.mvi.presenter.IBaseView
67

@@ -14,22 +15,27 @@ open class MviFragmentDelegateImpl<V : IBaseView<*, *>>(
1415
protected val presenter: BaseMviPresenter<*, *, *, V>
1516
) : MviFragmentDelegate {
1617

18+
@CallSuper
1719
override fun onCreate(savedInstanceState: Bundle?) {
18-
// no-op
20+
presenter.initializeState(fragment)
1921
}
2022

23+
@CallSuper
2124
override fun onSaveInstanceState(outState: Bundle) {
2225
// no-op
2326
}
2427

28+
@CallSuper
2529
override fun onStart() {
2630
presenter.attachView(fragment)
2731
}
2832

33+
@CallSuper
2934
override fun onStop() {
3035
presenter.detachView()
3136
}
3237

38+
@CallSuper
3339
override fun onDestroy() {
3440
presenter.destroy()
3541
}

mvi/src/main/java/pl/valueadd/mvi/fragment/delegate/fragment/MviFragmentSaveInstanceStateDelegateImpl.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,13 @@ class MviFragmentSaveInstanceStateDelegateImpl<V : IBaseView<VS, *>, VS : IBaseV
2626
private set
2727

2828
override fun onCreate(savedInstanceState: Bundle?) {
29+
super.onCreate(savedInstanceState)
2930
this.restoredViewState =
3031
savedInstanceState?.getParcelable(VIEW_STATE_BUNDLE_KEY)
3132
}
3233

3334
override fun onSaveInstanceState(outState: Bundle) {
35+
super.onSaveInstanceState(outState)
3436
outState.putParcelable(
3537
VIEW_STATE_BUNDLE_KEY,
3638
presenter.currentState as Parcelable

mvi/src/test/java/pl/valueadd/mvi/fragment/base/BaseMviFragmentTest.kt

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
package pl.valueadd.mvi.fragment.base
22

3-
import io.mockk.every
4-
import io.mockk.mockk
5-
import io.mockk.verify
3+
/* ktlint-disable no-wildcard-imports */
4+
import io.mockk.*
65
import io.reactivex.Observable
76
import io.reactivex.schedulers.Schedulers
87
import kotlinx.android.parcel.Parcelize
8+
import org.junit.jupiter.api.AfterEach
99
import org.junit.jupiter.api.BeforeEach
1010
import org.junit.jupiter.api.Test
1111
import pl.valueadd.mvi.IBaseViewState
@@ -27,6 +27,26 @@ class BaseMviFragmentTest {
2727
fragment.presenter = mockPresenter
2828
}
2929

30+
@AfterEach
31+
fun tearDown() {
32+
clearAllMocks()
33+
}
34+
35+
@Test
36+
fun `Should call initialize state on presenter on create`() {
37+
// Given
38+
val mockActivity = mockk<TestActivity>(relaxed = true) {
39+
every { supportDelegate } returns mockk(relaxed = true)
40+
}
41+
fragment.onAttach(mockActivity)
42+
43+
// When
44+
fragment.onCreate(null)
45+
46+
// Then
47+
verify(exactly = 1) { mockPresenter.initializeState(fragment) }
48+
}
49+
3050
@Test
3151
fun `Should attach view to presenter on start`() {
3252
// Given

0 commit comments

Comments
 (0)