Skip to content

Commit 02d2648

Browse files
committed
add fixes for restoring state in android when fragment is recreated
1 parent 0ef0a4e commit 02d2648

File tree

17 files changed

+212
-48
lines changed

17 files changed

+212
-48
lines changed

app/src/main/java/com/willowtreeapps/namegame/store/BaseNameGameViewFragment.kt

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package com.willowtreeapps.namegame.store
22

3+
import android.os.Bundle
34
import androidx.fragment.app.Fragment
5+
import com.willowtreeapps.common.Logger
46
import com.willowtreeapps.common.Presenter
57
import com.willowtreeapps.common.View
68
import com.willowtreeapps.common.ui.GameResultsPresenter
@@ -14,10 +16,24 @@ open class BaseNameGameViewFragment<TPresenter: Presenter<*>>: Fragment(), Corou
1416
get() = Dispatchers.Main
1517

1618
override lateinit var presenter: TPresenter
19+
private var viewRecreated: Boolean = false
20+
21+
override fun onViewCreated(view: android.view.View, savedInstanceState: Bundle?) {
22+
super.onViewCreated(view, savedInstanceState)
23+
if (savedInstanceState == null)
24+
Logger.d("savedInstanceState == null")
25+
else {
26+
Logger.d("savedInstanceState != null")
27+
viewRecreated = true
28+
}
29+
}
1730

1831
override fun onResume() {
1932
super.onResume()
2033
NameGameApp.gameEngine().attachView(this)
34+
if (viewRecreated) {
35+
presenter.recreateView()
36+
}
2137
}
2238

2339
override fun onPause() {

app/src/main/java/com/willowtreeapps/namegame/store/QuestionFragment.kt

Lines changed: 121 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import android.os.Bundle
77
import android.view.LayoutInflater
88
import android.view.View
99
import android.view.ViewGroup
10+
import android.view.ViewTreeObserver
1011
import android.view.animation.BounceInterpolator
1112
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions
1213
import com.willowtreeapps.common.QuestionViewState
@@ -17,6 +18,7 @@ import nl.dionsegijn.konfetti.models.Size
1718
import android.widget.Button
1819
import androidx.core.content.res.ResourcesCompat
1920
import androidx.interpolator.view.animation.FastOutSlowInInterpolator
21+
import com.willowtreeapps.common.Logger
2022
import com.willowtreeapps.common.ui.QuestionPresenter
2123
import com.willowtreeapps.namegame.*
2224

@@ -33,6 +35,7 @@ class QuestionFragment : BaseNameGameViewFragment<QuestionPresenter>(), Question
3335
}
3436

3537
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
38+
super.onViewCreated(view, savedInstanceState)
3639
initViews()
3740
}
3841

@@ -52,6 +55,65 @@ class QuestionFragment : BaseNameGameViewFragment<QuestionPresenter>(), Question
5255
return false
5356
}
5457

58+
override fun showProfileNotAnimated(viewState: QuestionViewState) {
59+
view?.viewTreeObserver?.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener {
60+
override fun onGlobalLayout() {
61+
Logger.d("viewState: $viewState")
62+
GlideApp.with(this@QuestionFragment).load(viewState.itemImageUrl)
63+
.into(imageView)
64+
txt_question_title.text = viewState.title
65+
when {
66+
viewState.nextButtonVisible -> {
67+
txt_timer.visibility = View.GONE
68+
btn_next.visibility = View.VISIBLE
69+
btn_next.alpha = 1f
70+
btn_end_game.visibility = View.GONE
71+
btn_end_game.alpha = 0f
72+
showCorrectButton(viewState.correctBtnNum)
73+
setButtonText(viewState)
74+
}
75+
viewState.endButtonVisible -> {
76+
txt_timer.visibility = View.GONE
77+
btn_next.visibility = View.GONE
78+
btn_next.alpha = 0f
79+
btn_end_game.visibility = View.VISIBLE
80+
btn_end_game.alpha = 1f
81+
showCorrectButton(viewState.correctBtnNum)
82+
setButtonText(viewState)
83+
}
84+
else -> {
85+
txt_timer.text = viewState.timerText
86+
setButtonText(viewState)
87+
setAllButtonsVisible()
88+
btn_next.visibility = View.GONE
89+
btn_next.alpha = 0f
90+
btn_end_game.visibility = View.GONE
91+
btn_end_game.alpha = 1f
92+
//start timer again
93+
presenter.profileImageIsVisible()
94+
}
95+
96+
}
97+
view?.viewTreeObserver?.removeOnGlobalLayoutListener(this)
98+
}
99+
})
100+
}
101+
102+
private fun showCorrectButton(correctBtnNum: Int) {
103+
val correctBtn = getBtnByNum(correctBtnNum)
104+
if (correctBtn != null) {
105+
correctBtn.visibility = View.VISIBLE
106+
correctBtn.alpha = 1f
107+
restoreX = correctBtn.x
108+
restoreY = correctBtn.y
109+
correctBtn.x = correctBtnX(correctBtn)
110+
correctBtn.y = correctBtnY()
111+
correctBtn.scaleX = 2f
112+
correctBtn.scaleY = 2f
113+
lastCorrectBtn = correctBtn
114+
}
115+
}
116+
55117
override fun showProfile(viewState: QuestionViewState) {
56118
activity?.runOnUiThread {
57119
if (btn_next.visibility == View.VISIBLE) {
@@ -60,7 +122,6 @@ class QuestionFragment : BaseNameGameViewFragment<QuestionPresenter>(), Question
60122
setProfileAndFadeIn(viewState)
61123
}
62124
}
63-
64125
}
65126

66127
override fun showCorrectAnswer(viewState: QuestionViewState, isEndGame: Boolean) {
@@ -101,6 +162,9 @@ class QuestionFragment : BaseNameGameViewFragment<QuestionPresenter>(), Question
101162
}
102163
}
103164

165+
private fun correctBtnX(btn: Button) = imageView.x + (imageView.width - btn.width) / 2
166+
private fun correctBtnY() = imageView.y + imageView.height
167+
104168
/**
105169
* Hides the incorrect buttons and animates the correct name to be centered below profile image
106170
*/
@@ -109,10 +173,10 @@ class QuestionFragment : BaseNameGameViewFragment<QuestionPresenter>(), Question
109173
val correctBtn = getBtnByNum(viewState.correctBtnNum)
110174
val selectedBtn = getBtnByNum(viewState.selectedBtnNum)
111175

112-
fun View.hideOrMoveAnimation(): AnimatorSet {
176+
fun Button.hideOrMoveAnimation(): AnimatorSet {
113177
return if (this == correctBtn) {
114-
val endX = imageView.x + (imageView.width - this.width) / 2
115-
val endY = imageView.y + imageView.height
178+
val endX = correctBtnX(this)
179+
val endY = correctBtnY()
116180

117181
val animX = ObjectAnimator.ofFloat(this, View.X, endX)
118182
val animY = ObjectAnimator.ofFloat(this, View.Y, endY)
@@ -133,26 +197,29 @@ class QuestionFragment : BaseNameGameViewFragment<QuestionPresenter>(), Question
133197
lastCorrectBtn = correctBtn
134198
lastSelectedBtn = selectedBtn
135199

136-
val anim1 = button1.hideOrMoveAnimation()
137-
val anim2 = button2.hideOrMoveAnimation()
138-
val anim3 = button3.hideOrMoveAnimation()
139-
val anim4 = button4.hideOrMoveAnimation()
200+
val anim1 = button1?.hideOrMoveAnimation()
201+
val anim2 = button2?.hideOrMoveAnimation()
202+
val anim3 = button3?.hideOrMoveAnimation()
203+
val anim4 = button4?.hideOrMoveAnimation()
140204

141-
val set = AnimatorSet()
142-
set.playTogether(anim1, anim2, anim3, anim4)
143-
set.onComplete {
144-
val btn = if (isEndGame) {
145-
btn_end_game
146-
} else {
147-
btn_next
148-
}
149-
if (btn != null) {
150-
btn.visibility = View.VISIBLE
151-
btn.alpha = 0F
152-
btn.animate().alpha(1f)
205+
//TODO replace with isViewCreated fun
206+
if (anim1 != null) {
207+
val set = AnimatorSet()
208+
set.playTogether(anim1, anim2, anim3, anim4)
209+
set.onComplete {
210+
val btn = if (isEndGame) {
211+
btn_end_game
212+
} else {
213+
btn_next
214+
}
215+
if (btn != null) {
216+
btn.visibility = View.VISIBLE
217+
btn.alpha = 0F
218+
btn.animate().alpha(1f)
219+
}
153220
}
221+
set.start()
154222
}
155-
set.start()
156223
}
157224

158225
private fun showButtons() {
@@ -169,29 +236,48 @@ class QuestionFragment : BaseNameGameViewFragment<QuestionPresenter>(), Question
169236
private fun fadeNextButton(after: () -> Unit) {
170237
btn_next.animate().alpha(0f).withEndAction {
171238
lastCorrectBtn?.alpha = 0f
172-
btn_next.visibility = View.GONE
239+
btn_next?.visibility = View.GONE
173240
after()
174241
}
175242
}
176243

177244
private fun setProfileAndFadeIn(viewState: QuestionViewState) {
178245
with(viewState) {
179-
txt_question_title.text = title
180-
GlideApp.with(this@QuestionFragment).load(itemImageUrl)
181-
.transition(DrawableTransitionOptions.withCrossFade())
182-
.onComplete {
183-
showButtons()
184-
txt_timer.visibility = View.VISIBLE
185-
presenter.profileImageIsVisible()
186-
}
187-
.into(imageView)
246+
if (txt_question_title != null) {
247+
txt_question_title.text = title
248+
GlideApp.with(this@QuestionFragment).load(itemImageUrl)
249+
.transition(DrawableTransitionOptions.withCrossFade())
250+
.onComplete {
251+
showButtons()
252+
txt_timer.visibility = View.VISIBLE
253+
presenter.profileImageIsVisible()
254+
}
255+
.into(imageView)
256+
setButtonText(viewState)
257+
}
258+
}
259+
}
260+
261+
private fun setButtonText(viewState: QuestionViewState) {
262+
with(viewState) {
188263
button1.text = button1Text
189264
button2.text = button2Text
190265
button3.text = button3Text
191266
button4.text = button4Text
192267
}
193268
}
194269

270+
private fun setAllButtonsVisible() {
271+
button1.visibility = View.VISIBLE
272+
button2.visibility = View.VISIBLE
273+
button3.visibility = View.VISIBLE
274+
button4.visibility = View.VISIBLE
275+
button1.alpha = 1f
276+
button2.alpha = 1f
277+
button3.alpha = 1f
278+
button4.alpha = 1f
279+
}
280+
195281
override fun setTimerText(viewState: QuestionViewState) {
196282
activity?.runOnUiThread {
197283
txt_timer.scaleX = 0f
@@ -228,10 +314,10 @@ class QuestionFragment : BaseNameGameViewFragment<QuestionPresenter>(), Question
228314
.setDuration(500)
229315
.withEndAction {
230316
showWrongAnswer(viewState, isEndGame)
231-
txt_timer.animate().alpha(0f)
232-
.withEndAction {
233-
txt_timer.visibility = View.VISIBLE
234-
txt_timer.setTextColor(restoreColor)
317+
txt_timer?.animate()?.alpha(0f)
318+
?.withEndAction {
319+
txt_timer?.visibility = View.VISIBLE
320+
txt_timer?.setTextColor(restoreColor)
235321
}
236322
}
237323

common/src/commonMain/kotlin/com/willowtreeapps/common/AppState.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ data class AppState(val isLoadingItems: Boolean = false,
66
val errorMsg: String = "",
77
val currentQuestionIndex: Int = 0,
88
val waitingForNextQuestion: Boolean = false,
9-
val waitingForResultsTap: Boolean = false,
109
val questionClock: Int = -1,
1110
val questionTitle: String = "",
1211
val questions: List<Question> = listOf(),
@@ -34,6 +33,8 @@ data class AppState(val isLoadingItems: Boolean = false,
3433

3534
fun isGameComplete(): Boolean = currentQuestionIndex >= questions.size || (currentQuestionIndex == questions.size - 1 && questions[currentQuestionIndex].status != Question.Status.UNANSWERED)
3635

36+
fun isCurrentQuestionAnswered(): Boolean = currentQuestion?.status != Question.Status.UNANSWERED
37+
3738
val numCorrect: Int
3839
get() = questions.count { it.status == Question.Status.CORRECT }
3940
}
@@ -85,4 +86,3 @@ data class UserSettings(val numQuestions: Int,
8586
fun defaults() = UserSettings(3, categoryId = QuestionCategoryId.CATS)
8687
}
8788
}
88-

common/src/commonMain/kotlin/com/willowtreeapps/common/GameEngine.kt

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ package com.willowtreeapps.common
33
import com.beyondeye.reduks.SimpleStore
44
import com.beyondeye.reduks.middlewares.applyMiddleware
55
import com.beyondeye.reduks.middlewares.thunkMiddleware
6+
import com.willowtreeapps.common.middleware.*
67
import com.willowtreeapps.common.middleware.NavigationMiddleware
7-
import com.willowtreeapps.common.middleware.Navigator
88
import com.willowtreeapps.common.middleware.SettingsMiddleware
99
import com.willowtreeapps.common.middleware.ViewEffectsMiddleware
1010
import com.willowtreeapps.common.repo.LocalStorageSettingsRepository
@@ -20,20 +20,22 @@ class GameEngine(navigator: Navigator,
2020
private val viewEffectsMiddleware = ViewEffectsMiddleware()
2121
private val presenterFactory by lazy { PresenterFactory(this, networkContext) }
2222
val vibrateUtil = VibrateUtil(application)
23-
private val settingsMiddleware by lazy { SettingsMiddleware(LocalStorageSettingsRepository(userSettings(application)), networkContext) }
23+
private val settingsMiddleware by lazy { SettingsMiddleware(LocalStorageSettingsRepository(userSettings(application)), networkContext) }
2424

2525
val appStore by lazy {
2626
SimpleStore(AppState.INITIAL_STATE, ::reducer)
2727
.applyMiddleware(::thunkMiddleware,
2828
viewEffectsMiddleware::dispatch,
2929
navigationMiddleware::dispatch,
30-
settingsMiddleware::dispatch)
30+
settingsMiddleware::dispatch,
31+
::loggerMiddleware)
3132
}
3233

3334
init {
3435
appStore.dispatch(Actions.LoadAllSettingsAction())
3536
}
36-
fun <T: Presenter<*>>attachView(view: View<T>) = presenterFactory.attachView(view as View<Presenter<*>>)
37+
38+
fun <T : Presenter<*>> attachView(view: View<T>) = presenterFactory.attachView(view as View<Presenter<*>>)
3739

3840
fun detachView(view: View<*>) = presenterFactory.detachView(view)
3941
}

common/src/commonMain/kotlin/com/willowtreeapps/common/Reducers.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ internal fun reducer(state: AppState, action: Any): AppState =
3131
state.copy(questions = newQuestions, waitingForNextQuestion = true)
3232
}
3333
is NextQuestionAction -> state.copy(waitingForNextQuestion = false, currentQuestionIndex = state.currentQuestionIndex + 1)
34-
is GameCompleteAction -> state.copy(waitingForResultsTap = true, waitingForNextQuestion = false, currentQuestionIndex = state.currentQuestionIndex + 1)
34+
is GameCompleteAction -> state.copy(waitingForNextQuestion = false, currentQuestionIndex = state.currentQuestionIndex + 1)
3535
is StartOverAction, is ResetGameStateAction -> AppState.INITIAL_STATE.copy(settings = state.settings)
3636
is StartQuestionTimerAction -> state.copy(questionClock = action.initialValue)
3737
is DecrementCountDownAction -> state.copy(questionClock = state.questionClock - 1)

common/src/commonMain/kotlin/com/willowtreeapps/common/TimerThunks.kt

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,19 +16,22 @@ class TimerThunks(private val backgroundContext: CoroutineContext, val store: St
1616
* the timer and start the new one.
1717
*/
1818
fun startCountDownTimer(initialValue: Int): ThunkImpl<AppState> = ThunkFn { dispatcher, state ->
19-
if (timerJob == null || timerJob?.isActive == false) {
19+
if (timerJob == null || timerJob?.isCompleted == true) {
20+
Logger.d("Launching new Timer")
2021
store.dispatch(Actions.StartQuestionTimerAction(initialValue))
2122
timerJob = launchTimer(1000, CoroutineScope(coroutineContext)) {
22-
2323
if (store.state.questionClock > 0) {
2424
store.dispatch(Actions.DecrementCountDownAction())
2525
} else {
2626
store.dispatch(Actions.TimesUpAction())
2727
timerJob?.cancel()
28+
2829
}
2930
}
31+
timerJob?.invokeOnCompletion {
32+
Logger.d("TIMERJOB is complete: ${it?.message}")
33+
}
3034
}
31-
Any()
3235
}
3336

3437
fun stopTimer() {

common/src/commonMain/kotlin/com/willowtreeapps/common/boundary/TransformFunctions.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ fun AppState.toQuestionViewState(): QuestionViewState {
2626
button3Text = choice3,
2727
button4Text = choice4,
2828
correctBtnNum = correctBtnNum,
29+
nextButtonVisible = this.waitingForNextQuestion && !isGameComplete(),
30+
endButtonVisible = isGameComplete(),
2931
timerText = timerText,
3032
selectedBtnNum = selectedBtnNum ?: -1)
3133
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package com.willowtreeapps.common.middleware
2+
3+
import com.beyondeye.reduks.Store
4+
import com.willowtreeapps.common.Actions
5+
import com.willowtreeapps.common.AppState
6+
import com.willowtreeapps.common.Logger
7+
8+
fun loggerMiddleware(store: Store<AppState>, nextDispatcher: (Any) -> Any, action: Any): Any {
9+
val result = nextDispatcher(action)
10+
Logger.d("DISPATCH action: ${action::class.simpleName}")
11+
return result
12+
}

0 commit comments

Comments
 (0)