Skip to content

Commit e9a0446

Browse files
committed
Rebind the EditPostRepository when the posts list is recreated
PostsListActivity has no configChanges, so it is recreated on rotation with a freshly injected EditPostRepository, while PostListMainViewModel survives. Its start() returned early on isStarted before assigning the repository and before loading the post into it, so after any rotation the ViewModel kept writing to the previous activity's dead repository while the restored Prepublishing sheet read the new empty one. That is the state the sheet guard was catching: publishing stayed broken for the rest of the activity's life, silently once the sheet began dismissing itself instead of crashing. Rebind on every start instead, and keep currentBottomSheetPostId in sync so the id onSaveInstanceState reads survives a second restore. Also makes PrepublishingViewModel.start()'s hasPost parameter required, so the guard can't be bypassed by omission, and corrects the comment: the guard dismisses on every restore path but does not stop the saved child fragment from being restored and started first, since the dismissal is async.
1 parent d587ead commit e9a0446

4 files changed

Lines changed: 72 additions & 27 deletions

File tree

WordPress/src/main/java/org/wordpress/android/ui/posts/PostListMainViewModel.kt

Lines changed: 28 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -244,9 +244,14 @@ class PostListMainViewModel @Inject constructor(
244244
currentBottomSheetPostId: LocalId,
245245
editPostRepository: EditPostRepository
246246
) {
247+
// The activity is recreated on rotation with a freshly injected EditPostRepository while this
248+
// ViewModel survives, so rebind on every start rather than only the first. Otherwise the
249+
// restored Prepublishing sheet reads an empty repository and this ViewModel goes on writing
250+
// to the previous activity's, which breaks publishing for the rest of the activity's life.
251+
bindEditPostRepository(editPostRepository, site, currentBottomSheetPostId)
252+
247253
if (isStarted) return
248254
this.site = site
249-
this.editPostRepository = editPostRepository
250255

251256
val authorFilterSelection: AuthorFilterSelection = if (isFilteringByAuthorSupported) {
252257
prefs.postListAuthorSelection
@@ -289,23 +294,34 @@ class PostListMainViewModel @Inject constructor(
289294
)
290295
_previewState.value = _previewState.value ?: initPreviewState
291296

292-
currentBottomSheetPostId.let { postId ->
293-
if (postId.value != 0) {
294-
editPostRepository.loadPostByLocalPostId(postId.value)
295-
}
296-
}
297-
298297
lifecycleOwner.lifecycleRegistry.currentState = Lifecycle.State.STARTED
299298

300299
uploadStarter.queueUploadFromSite(site)
301300

302-
editPostRepository.run {
303-
postChanged.observe(lifecycleOwner, Observer {
304-
savePostToDbUseCase.savePostToDb(editPostRepository, site)
305-
})
301+
isStarted = true
302+
}
303+
304+
private fun bindEditPostRepository(
305+
repository: EditPostRepository,
306+
site: SiteModel,
307+
bottomSheetPostId: LocalId
308+
) {
309+
if (this::editPostRepository.isInitialized) {
310+
if (this.editPostRepository === repository) return
311+
// drop the previous activity's repository, or its observer would pin it for our lifetime
312+
this.editPostRepository.postChanged.removeObservers(lifecycleOwner)
313+
}
314+
this.editPostRepository = repository
315+
316+
if (bottomSheetPostId.value != 0) {
317+
// keep the field in sync so onSaveInstanceState can round-trip it again
318+
currentBottomSheetPostId = bottomSheetPostId
319+
repository.loadPostByLocalPostId(bottomSheetPostId.value)
306320
}
307321

308-
isStarted = true
322+
repository.postChanged.observe(lifecycleOwner, Observer {
323+
savePostToDbUseCase.savePostToDb(repository, site)
324+
})
309325
}
310326

311327
override fun onCleared() {

WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/PrepublishingViewModel.kt

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -63,14 +63,16 @@ class PrepublishingViewModel @Inject constructor(private val dispatcher: Dispatc
6363
fun start(
6464
site: SiteModel,
6565
currentScreenFromSavedState: PrepublishingScreen?,
66-
hasPost: Boolean = true
66+
hasPost: Boolean
6767
) {
6868
this.site = site
6969

7070
// The sheet is a DialogFragment, so the FragmentManager restores it after a config change or
71-
// process death even when the host hasn't loaded a post. Guarding here rather than in an
72-
// individual screen's ViewModel covers every screen the sheet can restore into, since the
73-
// saved screen below may be TAGS, CATEGORIES or PUBLISH rather than HOME.
71+
// process death even when the host hasn't loaded a post. Every restore passes through here,
72+
// whatever screen was saved in KEY_SCREEN_STATE, so this dismisses the sheet in all of them.
73+
// Note it does not stop the saved child fragment from being restored and started first: the
74+
// dismissal is async. The non-HOME screens tolerate an empty repository at start time, and
75+
// HOME has its own early return. A new screen that reads the post at start needs one too.
7476
if (!hasPost) {
7577
AppLog.e(T.POSTS, "Prepublishing sheet started without a post; dismissing.")
7678
_dismissBottomSheet.postValue(Event(Unit))

WordPress/src/test/java/org/wordpress/android/ui/posts/PostListMainViewModelTest.kt

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,33 @@ class PostListMainViewModelTest : BaseUnitTest() {
159159
verify(editPostRepository, times(0)).loadPostByLocalPostId(any())
160160
}
161161

162+
@Test
163+
fun `when the activity is recreated, then the new EditPostRepository is rebound and reloaded`() {
164+
// arrange - the activity is recreated on rotation with a freshly injected repository
165+
val bottomSheetPostId = LocalId(2)
166+
viewModel.start(site, PostListRemotePreviewState.NONE, bottomSheetPostId, editPostRepository)
167+
val recreatedRepository = mock<EditPostRepository>()
168+
whenever(recreatedRepository.postChanged).thenReturn(MutableLiveData(Event(PostModel())))
169+
170+
// act - start() runs again on the surviving ViewModel
171+
viewModel.start(site, PostListRemotePreviewState.NONE, bottomSheetPostId, recreatedRepository)
172+
173+
// assert - the second repository must be loaded too, or the restored sheet reads an empty one
174+
verify(recreatedRepository, times(1)).loadPostByLocalPostId(bottomSheetPostId.value)
175+
}
176+
177+
@Test
178+
fun `given a restored bottom sheet post id, when started, then the id is kept for the next save`() {
179+
// arrange
180+
val bottomSheetPostId = LocalId(2)
181+
182+
// act
183+
viewModel.start(site, PostListRemotePreviewState.NONE, bottomSheetPostId, editPostRepository)
184+
185+
// assert - onSaveInstanceState reads this field, so it has to survive a restore round-trip
186+
assertThat(viewModel.currentBottomSheetPostId).isEqualTo(bottomSheetPostId)
187+
}
188+
162189
@Test
163190
fun `if post in EditPostRepository is modified then the savePostToDbUseCase should update the post`() {
164191
// arrange

WordPress/src/test/java/org/wordpress/android/ui/posts/prepublishing/PrepublishingViewModelTest.kt

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ class PrepublishingViewModelTest : BaseUnitTest() {
3535
event = it
3636
}
3737

38-
viewModel.start(mock(), null)
38+
viewModel.start(mock(), null, hasPost = true)
3939

4040
assertThat(event?.peekContent()?.targetScreen).isEqualTo(expectedScreen)
4141
}
@@ -49,7 +49,7 @@ class PrepublishingViewModelTest : BaseUnitTest() {
4949
event = it
5050
}
5151

52-
viewModel.start(mock(), expectedScreen)
52+
viewModel.start(mock(), expectedScreen, hasPost = true)
5353

5454
assertThat(event?.peekContent()?.targetScreen).isEqualTo(expectedScreen)
5555
}
@@ -87,7 +87,7 @@ class PrepublishingViewModelTest : BaseUnitTest() {
8787
event = it
8888
}
8989

90-
viewModel.start(mock(), TAGS)
90+
viewModel.start(mock(), TAGS, hasPost = true)
9191
viewModel.onBackClicked()
9292

9393
assertThat(event?.peekContent()?.targetScreen).isEqualTo(expectedScreen)
@@ -100,7 +100,7 @@ class PrepublishingViewModelTest : BaseUnitTest() {
100100
event = it
101101
}
102102

103-
viewModel.start(mock(), HOME)
103+
viewModel.start(mock(), HOME, hasPost = true)
104104
viewModel.onBackClicked()
105105

106106
assertThat(event).isNotNull
@@ -127,7 +127,7 @@ class PrepublishingViewModelTest : BaseUnitTest() {
127127
event = it
128128
}
129129

130-
viewModel.start(mock(), mock())
130+
viewModel.start(mock(), mock(), hasPost = true)
131131
viewModel.onActionClicked(PrepublishingScreenNavigation.Tags)
132132

133133
assertThat(event?.peekContent()?.targetScreen).isEqualTo(expectedScreen)
@@ -142,7 +142,7 @@ class PrepublishingViewModelTest : BaseUnitTest() {
142142
event = it
143143
}
144144

145-
viewModel.start(mock(), mock())
145+
viewModel.start(mock(), mock(), hasPost = true)
146146
viewModel.onActionClicked(PrepublishingScreenNavigation.Publish)
147147

148148
assertThat(event?.peekContent()?.targetScreen).isEqualTo(expectedScreen)
@@ -157,7 +157,7 @@ class PrepublishingViewModelTest : BaseUnitTest() {
157157
event = it
158158
}
159159

160-
viewModel.start(mock(), mock())
160+
viewModel.start(mock(), mock(), hasPost = true)
161161
viewModel.onActionClicked(PrepublishingScreenNavigation.Categories)
162162

163163
assertThat(event?.peekContent()?.targetScreen).isEqualTo(expectedScreen)
@@ -172,7 +172,7 @@ class PrepublishingViewModelTest : BaseUnitTest() {
172172
event = it
173173
}
174174

175-
viewModel.start(mock(), mock())
175+
viewModel.start(mock(), mock(), hasPost = true)
176176
viewModel.onActionClicked(PrepublishingScreenNavigation.AddCategory)
177177

178178
assertThat(event?.peekContent()?.targetScreen).isEqualTo(expectedScreen)
@@ -187,7 +187,7 @@ class PrepublishingViewModelTest : BaseUnitTest() {
187187
event = it
188188
}
189189

190-
viewModel.start(mock(), mock())
190+
viewModel.start(mock(), mock(), hasPost = true)
191191
viewModel.onActionClicked(PrepublishingScreenNavigation.Social)
192192

193193
assertThat(event?.peekContent()?.targetScreen).isEqualTo(expectedScreen)
@@ -201,15 +201,15 @@ class PrepublishingViewModelTest : BaseUnitTest() {
201201
event = it
202202
}
203203

204-
viewModel.start(mockSite, mock())
204+
viewModel.start(mockSite, mock(), hasPost = true)
205205
viewModel.onActionClicked(Action.NavigateToSharingSettings)
206206

207207
assertThat(event?.peekContent()).isEqualTo(mockSite)
208208
}
209209

210210
@Test
211211
fun `when onSubmitButtonClicked is triggered then bottom sheet should close and listener is triggered`() {
212-
viewModel.start(mock(), mock())
212+
viewModel.start(mock(), mock(), hasPost = true)
213213

214214
viewModel.onSubmitButtonClicked(true)
215215

0 commit comments

Comments
 (0)