fix: make settings dialog recreation-safe

This commit is contained in:
2026-03-18 13:32:20 -04:00
parent f3349f5dee
commit 769b893959
5 changed files with 82 additions and 40 deletions

View File

@@ -492,6 +492,24 @@ class BoardsFlowTest {
.check(matches(isDisplayed()))
}
@Test
fun settingsDialogSurvivesActivityRecreate() {
MainActivity.dependencies.apiClientFactory = {
FakeBoardsApiClient(
boards = mutableListOf(BoardSummary("1", "Alpha")),
templates = emptyList(),
)
}
val scenario = ActivityScenario.launch(BoardsActivity::class.java)
openSettingsFromDrawer()
scenario.recreate()
onView(withId(R.id.settingsFragmentContainer)).check(matches(isDisplayed()))
onView(withId(R.id.settingsSaveAndCloseButton)).check(matches(isDisplayed()))
}
private fun openSettingsFromDrawer() {
onView(withId(R.id.boardsDrawerLayout)).perform(DrawerActions.open())
onView(withId(R.id.drawerSettingsButton)).perform(click())

View File

@@ -49,6 +49,15 @@ class BoardsActivity : AppCompatActivity() {
private lateinit var apiKeyStore: ApiKeyStore
private lateinit var apiClient: KanbnApiClient
internal val sessionStoreForSettingsDialog: SessionStore
get() = sessionStore
internal val apiKeyStoreForSettingsDialog: ApiKeyStore
get() = apiKeyStore
internal val apiClientForSettingsDialog: KanbnApiClient
get() = apiClient
private lateinit var swipeRefresh: SwipeRefreshLayout
private lateinit var recyclerView: RecyclerView
private lateinit var emptyStateText: TextView
@@ -413,7 +422,6 @@ class BoardsActivity : AppCompatActivity() {
val themeChanged = bundle.getBoolean(SettingsDialogFragment.RESULT_KEY_THEME_CHANGED)
viewModel.onSettingsApplied(
credentialsChanged = credentialsChanged,
themeChanged = themeChanged,
)
}
}
@@ -423,11 +431,7 @@ class BoardsActivity : AppCompatActivity() {
return
}
SettingsDialogFragment
.newInstance(
sessionStore = sessionStore,
apiClient = apiClient,
apiKeyStore = apiKeyStore,
)
.newInstance()
.show(supportFragmentManager, SettingsDialogFragment.TAG)
}

View File

@@ -129,7 +129,7 @@ class BoardsViewModel(
fetchBoards(initial = false, refresh = true)
}
fun onSettingsApplied(credentialsChanged: Boolean, themeChanged: Boolean) {
fun onSettingsApplied(credentialsChanged: Boolean) {
if (!credentialsChanged) {
return
}

View File

@@ -1,11 +1,13 @@
package space.hackenslacker.kanbn4droid.app.settings
import android.app.Dialog
import android.content.Context
import android.os.Bundle
import android.view.LayoutInflater
import android.widget.Button
import android.widget.ProgressBar
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate
import androidx.core.os.bundleOf
import androidx.fragment.app.DialogFragment
@@ -13,29 +15,36 @@ import androidx.fragment.app.commitNow
import androidx.lifecycle.lifecycleScope
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.coroutines.launch
import space.hackenslacker.kanbn4droid.app.BoardsActivity
import space.hackenslacker.kanbn4droid.app.MainActivity
import space.hackenslacker.kanbn4droid.app.R
import space.hackenslacker.kanbn4droid.app.auth.ApiKeyStore
import space.hackenslacker.kanbn4droid.app.auth.HttpKanbnApiClient
import space.hackenslacker.kanbn4droid.app.auth.KanbnApiClient
import space.hackenslacker.kanbn4droid.app.auth.PreferencesApiKeyStore
import space.hackenslacker.kanbn4droid.app.auth.SettingsApplyCoordinator
import space.hackenslacker.kanbn4droid.app.auth.SettingsApplyResult
import space.hackenslacker.kanbn4droid.app.auth.SessionPreferences
import space.hackenslacker.kanbn4droid.app.auth.SessionStore
class SettingsDialogFragment(
internal var sessionStore: SessionStore? = null,
internal var apiClient: KanbnApiClient? = null,
internal var apiKeyStore: ApiKeyStore? = null,
internal var coordinator: SettingsApplyCoordinator? = null,
) : DialogFragment() {
class SettingsDialogFragment : DialogFragment() {
lateinit var sessionStore: SessionStore
private set
private lateinit var apiClient: KanbnApiClient
private lateinit var apiKeyStore: ApiKeyStore
private var progress: ProgressBar? = null
private var saveButton: Button? = null
private var errorText: TextView? = null
private var controlsEnabled: Boolean = true
override fun onAttach(context: Context) {
super.onAttach(context)
resolveDependencies(context)
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val resolvedSessionStore = requireNotNull(sessionStore) { "SessionStore is required" }
val resolvedApiClient = requireNotNull(apiClient) { "KanbnApiClient is required" }
val resolvedApiKeyStore = requireNotNull(apiKeyStore) { "ApiKeyStore is required" }
val dialogView = LayoutInflater.from(requireContext()).inflate(R.layout.dialog_settings, null)
childFragmentManager.commitNow {
@@ -61,25 +70,21 @@ class SettingsDialogFragment(
dialog.setOnShowListener {
saveButton?.setOnClickListener {
onSaveClicked(resolvedSessionStore, resolvedApiClient, resolvedApiKeyStore)
onSaveClicked()
}
setApplyInProgress(false)
}
return dialog
}
private fun onSaveClicked(
sessionStore: SessionStore,
apiClient: KanbnApiClient,
apiKeyStore: ApiKeyStore,
) {
private fun onSaveClicked() {
if (!controlsEnabled) {
return
}
val resolvedCoordinator = coordinator
?: MainActivity.dependencies.settingsApplyCoordinatorFactory?.invoke(
requireActivity() as androidx.appcompat.app.AppCompatActivity,
val hostActivity = requireActivity() as AppCompatActivity
val resolvedCoordinator = MainActivity.dependencies.settingsApplyCoordinatorFactory?.invoke(
hostActivity,
sessionStore,
apiClient,
apiKeyStore,
@@ -177,6 +182,31 @@ class SettingsDialogFragment(
AppCompatDelegate.setDefaultNightMode(mode)
}
private fun resolveDependencies(context: Context) {
val host = activity as? BoardsActivity
if (host != null) {
sessionStore = host.sessionStoreForSettingsDialog
apiClient = host.apiClientForSettingsDialog
apiKeyStore = host.apiKeyStoreForSettingsDialog
return
}
val appCompatActivity = activity as? AppCompatActivity
if (appCompatActivity != null) {
sessionStore = MainActivity.dependencies.sessionStoreFactory?.invoke(appCompatActivity)
?: SessionPreferences(appCompatActivity.applicationContext)
apiClient = MainActivity.dependencies.apiClientFactory?.invoke()
?: HttpKanbnApiClient()
apiKeyStore = MainActivity.dependencies.apiKeyStoreFactory?.invoke(appCompatActivity)
?: PreferencesApiKeyStore(appCompatActivity)
return
}
sessionStore = SessionPreferences(context.applicationContext)
apiClient = HttpKanbnApiClient()
apiKeyStore = PreferencesApiKeyStore(context)
}
companion object {
const val TAG: String = "settings_dialog"
const val REQUEST_KEY_SETTINGS_APPLIED: String = "settings_applied_result"
@@ -185,18 +215,8 @@ class SettingsDialogFragment(
private const val SETTINGS_PREFS_TAG: String = "settings_preferences_fragment"
fun newInstance(
sessionStore: SessionStore,
apiClient: KanbnApiClient,
apiKeyStore: ApiKeyStore,
coordinator: SettingsApplyCoordinator? = null,
): SettingsDialogFragment {
return SettingsDialogFragment(
sessionStore = sessionStore,
apiClient = apiClient,
apiKeyStore = apiKeyStore,
coordinator = coordinator,
)
fun newInstance(): SettingsDialogFragment {
return SettingsDialogFragment()
}
}
}

View File

@@ -432,7 +432,7 @@ class BoardsViewModelTest {
}
val viewModel = newViewModel(api)
viewModel.onSettingsApplied(credentialsChanged = true, themeChanged = false)
viewModel.onSettingsApplied(credentialsChanged = true)
advanceUntilIdle()
assertTrue(api.listBoardsCalls >= 1)
@@ -449,7 +449,7 @@ class BoardsViewModelTest {
}
val viewModel = newViewModel(api)
viewModel.onSettingsApplied(credentialsChanged = false, themeChanged = true)
viewModel.onSettingsApplied(credentialsChanged = false)
advanceUntilIdle()
assertEquals(0, api.listBoardsCalls)
@@ -468,7 +468,7 @@ class BoardsViewModelTest {
val viewModel = newViewModel(api, sessionStore = sessionStore)
val eventDeferred = async { viewModel.events.first() }
viewModel.onSettingsApplied(credentialsChanged = true, themeChanged = false)
viewModel.onSettingsApplied(credentialsChanged = true)
advanceUntilIdle()
val event = eventDeferred.await()