Android setting Night Mode through AppCompatPreferenceActivity template cancels clicking on preference headers - android

My Goal: Setting night mode through preferences and updating the UI in real time.
So far: Done. BUT when I click back to the headers' preference screen I cannot get back in the different preferences' screens again.
To elaborate: My settings are pretty simple at that point. I'm following the preset set by Android Studio (3.1.4) of SettingsActivity, having a template of AppCompatPreferenceActivity. I have one main screen and two deeper ones.
My first screen has two choices: General and About.
Upon selecting General, I load a GeneralPreferenceFragment with one Switch preference, that of "Night Mode".
If I set it on, it switches the theme real time and when I go back, it's also done on my first settings' screen.
The problem: When I do change the theme, go back to the main SettingsScreen and I try to revisit either General or About screens, I cannot go deeper any more! If I switch the preference an even number so that I end up to the initial theme, I can visit them like nothing has happened.
SettingsActivity class
class SettingsActivity : AppCompatPreferenceActivity() {
// To be used for live changes in night mode
private var mCurrentNightMode: Int = 0
private val TAG = "SettingsActivity"
private var mThemeId = 0
/**
* Doing this hack to add my own actionbar on top since it wasn't there on its own
*/
override fun onPostCreate(savedInstanceState: Bundle?) {
super.onPostCreate(savedInstanceState)
Log.d(TAG, "onPostCreate")
val root = findViewById<View>(android.R.id.list).parent.parent.parent as LinearLayout
val bar = LayoutInflater.from(this).inflate(R.layout.settings_toolbar, root, false) as Toolbar
root.addView(bar, 0) // insert at top
bar.setNavigationOnClickListener {finish()}
}
override fun setTheme(resid: Int) {
super.setTheme(resid)
mThemeId = resid
}
override fun onCreate(savedInstanceState: Bundle?) {
if (delegate.applyDayNight() && (mThemeId != 0) ) {
// If DayNight has been applied, we need to re-apply the theme for
// the changes to take effect. On API 23+, we should bypass
// setTheme(), which will no-op if the theme ID is identical to the
// current theme ID.
if (Build.VERSION.SDK_INT >= 23) {
onApplyThemeResource(theme, mThemeId, false);
} else {
setTheme(mThemeId);
}
}
super.onCreate(savedInstanceState)
Log.d("SettingsActivity", "onCreate")
setupActionBar()
// Storing the current value so if we come back we'll check on postResume for any changes
mCurrentNightMode = getCurrentNightMode();
}
// Comparing current and last known night mode value
private fun hasNightModeChanged(): Boolean {
return mCurrentNightMode != getCurrentNightMode()
}
private fun getCurrentNightMode(): Int {
return resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
}
override fun onPostResume() {
super.onPostResume()
// Self-explanatory. When I load back into this screen, if there's a change in the theme, load it back!
if(hasNightModeChanged()) {
recreate()
}
}
/**
* Set up the [android.app.ActionBar], if the API is available.
*/
private fun setupActionBar() {
supportActionBar?.setDisplayHomeAsUpEnabled(true)
}
/**
* {#inheritDoc}
*/
override fun onIsMultiPane(): Boolean {
return isXLargeTablet(this)
}
/**
* {#inheritDoc}
*/
#TargetApi(Build.VERSION_CODES.HONEYCOMB)
override fun onBuildHeaders(target: List<PreferenceActivity.Header>) {
loadHeadersFromResource(R.xml.pref_headers, target)
}
/**
* This method stops fragment injection in malicious applications.
* Make sure to deny any unknown fragments here.
*/
override fun isValidFragment(fragmentName: String): Boolean {
return PreferenceFragment::class.java.name == fragmentName
|| GeneralPreferenceFragment::class.java.name == fragmentName
|| DataSyncPreferenceFragment::class.java.name == fragmentName
|| NotificationPreferenceFragment::class.java.name == fragmentName
|| AboutFragment::class.java.name == fragmentName
}
/**
* This fragment shows general preferences only. It is used when the
* activity is showing a two-pane settings UI.
*/
#TargetApi(Build.VERSION_CODES.HONEYCOMB)
class GeneralPreferenceFragment : PreferenceFragment() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
addPreferencesFromResource(R.xml.pref_general)
setHasOptionsMenu(true)
// Bind the summaries of EditText/List/Dialog/Ringtone preferences
// to their values. When their values change, their summaries are
// updated to reflect the new value, per the Android Design
// guidelines.
findPreference("night_mode").setOnPreferenceChangeListener { preference, newValue ->
val booleanValue = newValue as Boolean
if(booleanValue) {
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)
} else {
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO)
}
activity.recreate()
true
}
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
val id = item.itemId
if (id == android.R.id.home) {
startActivity(Intent(activity, SettingsActivity::class.java))
return true
}
return super.onOptionsItemSelected(item)
}
}
.
.
.
companion object {
// (didn't change anything here)
}
The problem occurs I think on my "recreate" calls. It's like the preference list's onItemClickListener is null or something similar.
Can anyone help?
EDIT: Simplified, now all my logic in in SettingsActivity class, I needn't have it in the abstract class

I can't believe that I solved it by adding delayedRecreate instead of recreate (was trying different things from another question - this one):
override fun onPostResume() {
super.onPostResume()
// Self-explanatory. When I load back into this screen, if there's a change in the theme, load it back!
if(hasNightModeChanged()) {
delayedRecreate()
}
}
private fun delayedRecreate() {
val handler = Handler()
handler.postDelayed(this::recreate, 1)
}
However, I don't really like the flicker it does when the screen is a bit delayed in recreating. If anyone has any other clues, it would be highly appreciated!

Related

Compose side effects + Jetpack navigation + onBackPressed = Stuck navigation

I am having this issue where I have to navigate when given state gets updated after an asynchronous task gets executed. I am doing it like this:
At ViewModel.kt
fun executeRandomTask() {
viewModelScope.launch {
runAsyncTask()
state = Success
}
}
At Composable.kt
LaunchedEffect(viewModel.state) {
if(viewModel.state is Success) {
navController.navigate("nextScreen")
}
}
Then in the next screen, I click the back navigation button (onBackPressed) and what happens, is that the effect gets launched again. So I end up again in "nextScreen".
When I do this next workaround:
DisposableEffect(viewModel.state) {
if(viewModel.state is Success) {
navController.navigate("nextScreen")
}
onDispose {
viewModel.state = null
}
}
Like this, the viewmodel state gets cleared and it also proves that what is happening is that the navigation controller destroys the previous screen (not sure if it is the intended behavior).
I am not sure about what I should be doing to avoid this, since this is a pretty common scenario and having to clear the state after a certain state is reached looks dirty.
I use SharedFlow for emitting one-time events like this
class MyViewModel : ViewModel() {
private val _eventFlow = MutableSharedFlow<OneTimeEvent>()
val eventFlow = _eventFlow.asSharedFlow()
private fun emitEvent(event: OneTimeEvent) {
viewModelScope.launch { _eventFlow.emit(event) }
}
}
The sealed class for defining events
sealed class OneTimeEvent {
object SomeEvent: OneTimeEvent()
}
And finally in the Screen collect the flow like this
fun MyScreen(viewModel: MyViewModel = hiltViewModel()) {
LaunchedEffect(Unit) {
viewModel.eventFlow.collect { event ->
when(event){
SomeEvent -> {
//Do Something
}
}
}
}
}
So whenever your state changes you can emit some event and take action against it in your Screen.

Android login/logout flow with the new SplashScreen API

I'm using the new SplashScreen API for android 12 but I'm a bit confused on the login flow now. I have one activity and multiple fragments as advised by google , the mainActivity is where the splashScreen launches and the user's supposed to be directed to either login fragment or the homefragment.
my question is how do I implement this work flow with the new SplashAPI? What fragment should be the startDestination? I don't want to use the popTo attribute since it doesn't look pretty to always show the loginfragment and then direct the user to Homefragment.
If someone could explain this to me I'd be grateful.
Homefragment should be the startDestination. Conditionally navigate to loginfragment and pop back to Homefragment after authentication.
Refer to the following video by Ian Lake.
https://www.youtube.com/watch?v=09qjn706ITA
I have a workaround for that you can set the content of the activity after you check if the user is authorized or not and save the user state in a ViewModel or something, then set your content upon this state.
I leverage the power of setKeepOnScreenCondition function from core-splashscreen library.
SplashInstaller.kt
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
SplashInstaller(
activity = this,
beforeHide = {
// Check if logged in or not asyncrounsly
delay(2000)
},
afterHide = {
setContent()
// start with you desired destination up on the check you did in beforeHide
})
}
/**
* #author Mohamed Elshaarawy on Oct 14, 2021.
*/
class SplashInstaller<A : ComponentActivity>(
private val activity: A,
visibilityPredicate: () -> Boolean = { BuildConfig.BUILD_TYPE != "debug" },
private val beforeHide: suspend A.() -> Unit = { delay(2000) },
private val afterHide: A.() -> Unit
) : CoroutineScope by activity.lifecycleScope {
private val isSplashVisibleChannel by lazy { Channel<Boolean>() }
private val isAfterCalled by lazy { Channel<Boolean>(capacity = 1) }
private val splashSuspensionJob = launch(start = CoroutineStart.LAZY) {
activity.beforeHide()
isSplashVisibleChannel.send(false)
}
init {
if (visibilityPredicate()) {
splashSuspensionJob.start()
installSplash()
} else afterSplash()
}
private fun installSplash() {
activity.installSplashScreen().setKeepOnScreenCondition {
val isVisible = isSplashVisibleChannel.tryReceive().getOrNull() ?: true
if (!isVisible) {
afterSplash()
}
isVisible
}
}
private fun afterSplash() {
if (isAfterCalled.tryReceive().getOrNull() != true) {
isAfterCalled.trySend(true)
activity.afterHide()
}
}
}
This solution uses
androidx.core:core-splashscreen:1.0.0-beta01
androidx.lifecycle:lifecycle-runtime-ktx:2.4.0
org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2

custom back button - Android Navigation

I have a form and want to get a confirmation message from the user before the user leaves it.
i want provide custom back button when user touch this button:
i try this:
val onBackPressedCallback = object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
}
}
requireActivity().onBackPressedDispatcher.addCallback(this,onBackPressedCallback)
but only seems to work for providing custom back behavior to the built-in software/hardware back button and not the back arrow button
How can I do this?
Use onSupportNavigateUp, and replace yourCurrentFragmentID to your current fragment id. All these should be done in MainActivity.
navController = findNavController(R.id.nav_host_fragment)
setupActionBarWithNavController(navController!!)
override fun onSupportNavigateUp(): Boolean {
return when(navController?.currentDestination?.id) {
R.id.yourCurrentFragmentID -> {
showDialog()
true
}
else -> navController?.navigateUp()!!
}
}
Edit
If your fragment already use onOptionsItemSelected, you can handle the logic by checking itemId.
override fun onOptionsItemSelected(item: MenuItem): Boolean {
val id = item.itemId
if (id == R.id.save) {
// your save button code logic
}else if(id ==android.R.id.home){
// display confirmation message
}
return super.onOptionsItemSelected(item)
}
Use this code to set Activity on backpress.
override fun onBackPressed() {
if (isDiscardChanges) {
discardDialog()
} else {
super.onBackPressed()
}
}
If you only want to go back from Activity, then you can add within AndroidManifest.xml child class Activity.
android:parentActivityName="Parent_Activity_Name

How to change the view of the previous fragment after returning to it using the back button?

I have an activity with two fragments.
The second one is called when I click on something to the first.
What I want is this : if i click on "back" button, I want to go back to the first fragment (that is working), but I want to set the visibility to VISIBLE on an element (if the first fragment is called with back press only)
How do I do that ?
I tried something like this (in my main fragment), I've found the idea in another topic, but this is trigger always in my main activity :
override fun onResume() {
super.onResume()
view?.isFocusableInTouchMode = true
view?.requestFocus()
view?.setOnKeyListener { v, keyCode, event ->
if(event.action == KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_BACK){
Log.i(TAG, "backpress pressed")
return#setOnKeyListener true
}
return#setOnKeyListener false
}
}
Temporary solution :
I've created a companion object with a value true or false and I change it everytime I need it, but it's temporary only.
Assuming your second Fragment replaces the first (i.e. using FragmentTransaction#replace), your first Fragment (we'll call them FragmentA and FragmentB) will be paused (i.e. onPause() will be called on FragmentA).
When you press the back button, the backstack will be popped, and FragmentA will be resumed (i.e. onResume() will be called).
What I would recommend, is to save a boolean flag in FragmentA, and set it to true when you show FragmentB. Then, in FragmentA#onResume, you can check if the flag is set to true, and set it back to false while handing the case that you wanted.
For example, something like:
private const val STATE_WAITING_FOR_FRAGMENT_B = "state_waiting_b"
class FragmentA : Fragment() {
private var isWaitingForFragmentB: Boolean = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (savedInstanceState != null) {
isWaitingForFragmentB = savedInstanceState.getBoolean(STATE_WAITING_FOR_FRAGMENT_B)
}
}
override fun onResume() {
super.onResume()
if (isWaitingForFragmentB) {
isWaitingForFragmentB = false
// handle your view state here
}
}
override fun onSaveInstanceState(savedInstanceState: Bundle) {
super.onSaveInstanceState(savedInstanceState)
savedInstanceState.putBoolean(
STATE_WAITING_FOR_FRAGMENT_B,
isWaitingForFragmentB
)
}
private fun showFragmentB() {
isWaitingForFragmentB = true
// do fragment transaction here
}
}
I'm not good at grammar.
First fragment do not call resume function when returning.
You must create callback with interface.
A good approach should be passing some flag, on the second fragment, by activity intent and to capture it on the first Fragment on onResume()
If you need extra info, just let me know

unsaved warning on back pressed in fragment

I want to show dialog when user press back or quit from fragment if there are some data unsaved. I am trying to override onbackpressed but unfortunately I got error lateinit property barcodeList has not been initialized. how to solve it?
here is my script on activity:
override fun onBackPressed() {
val theFragment = supportFragmentManager.fragments
for(i in 0 until theFragment.size)
{
if(theFragment[i].tag == "stocker_fragment")
{
StockerFragment().onBackPressed()
}
}
}
and this is in fragment:
fun onBackPressed() {
var check = false
// this barcodeList variable error.
for(i in 0 until barcodeList.size)
{
if(barcodeList[i].barcode.trim()=="")
{
check = true
break
}
}
if (check)
{
AlertHelper(context).onBackPressedAlert()
}
}
FYI: I have initialized barcodeList on onCreateView and everything is fine. only error in onBackPressed.
And my last question is, how do i know if user quit from fragment without pressing back button?
I think the problem is in your onBackPressed() implementation in the Activity. With the line StockerFragment().onBackPressed() you are creating a new instance of the StockerFragment and calling onBackPressed() on it, rather than calling it on the instance that is actively being used.
You should be able to adjust your Activity onBackPressed() like so:
override fun onBackPressed() {
val theFragment = supportFragmentManager.fragments
for(i in 0 until theFragment.size)
{
if(theFragment[i].tag == "stocker_fragment")
{
(theFragment[i] as StockerFragment).onBackPressed()
}
}
}
You can also make this a bit more kotliny like so:
override fun onBackPressed() {
supportFragmentManager.fragments.forEach { fragment ->
if (fragment is StockerFragment) {
fragment.onBackPressed()
}
}
}
You'll probably also want to figure out a way to decide whether the fragment's onBackPressed has determined that the Activity should stick around or not. Then, if the fragment is happy, you should call super.onBackPressed() in the Activity so that the expected back behavior (leave the Activity) happens.

Categories

Resources