So I have my settings activity set up with my < PreferenceScreen > for choosing the app settings.
What I want to do is have my changes do immediate effect, and not on app restart.
Naturally I wanted to use OnSharedPreferenceChangeListener , but if I try to put a Log in onSharedPreferenceChanged, it never procs on changed selection. My plan was to recreate() on preference changed, so my preference apply function would proc on onCreate() with that recreate().
The problem is, as previously stated that onSharedPreferenceChanged never procs.
I tried registering the listener as it was stated somewhere, but it didn't help.
Can anyone help me?
class SettingsActivity : AppCompatActivity(),
PreferenceFragmentCompat.OnPreferenceStartFragmentCallback,
SharedPreferences.OnSharedPreferenceChangeListener {
val Tag = "My Activity:"
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val prefMen = SharedPreferencesManager(this)
prefMen.loadTheme()
setContentView(R.layout.activity_settings)
if (savedInstanceState == null) {
supportFragmentManager.beginTransaction()
.replace(R.id.content_preference, MainPreference()).commit()
} else {
title = savedInstanceState.getCharSequence(TAG_TITLE)
}
supportFragmentManager.addOnBackStackChangedListener {
if (supportFragmentManager.backStackEntryCount == 0) {
setTitle("Settings")
}
}
setUpToolbar()
}
override fun onStart() {
super.onStart()
getPreferences(MODE_PRIVATE).unregisterOnSharedPreferenceChangeListener(this)
}
override fun onDestroy() {
super.onDestroy()
getPreferences(MODE_PRIVATE).unregisterOnSharedPreferenceChangeListener(this)
}
override fun onSaveInstanceState(outState: Bundle, outPersistentState: PersistableBundle) {
super.onSaveInstanceState(outState, outPersistentState)
outState.putCharSequence(TAG_TITLE, title)
}
override fun onSupportNavigateUp(): Boolean {
if (supportFragmentManager.popBackStackImmediate()) {
return true
}
return super.onSupportNavigateUp()
}
private fun setUpToolbar() {
supportActionBar?.setTitle("Settings")
supportActionBar?.setDisplayHomeAsUpEnabled(true)
supportActionBar?.setDisplayShowHomeEnabled(true)
}
class MainPreference : PreferenceFragmentCompat() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.preferences, rootKey)
}
}
override fun onPreferenceStartFragment(
caller: PreferenceFragmentCompat?,
pref: Preference?
): Boolean {
val args = pref?.extras
val fragment = pref?.fragment?.let {
supportFragmentManager.fragmentFactory.instantiate(
classLoader,
it
).apply {
arguments = args
setTargetFragment(caller, 0)
}
}
fragment?.let {
supportFragmentManager.beginTransaction().replace(R.id.content_preference, it)
.addToBackStack(null).commit()
}
title = pref?.title
return true
}
companion object {
private val TAG_TITLE = "PREFERENCE_ACTIVITY"
}
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
recreate()
}
}
I see two mistakes. The first is that you never register your listener. You have called unregisterOnSharedPreferenceChangeListener() in your onStart() instead of registerOnSharedPreferenceChangeListener().
Secondly, you are not listening to the same SharedPreferences as the settings Fragment is using. PreferenceFragmentCompat by default uses the default shared preferences of the whole application. But you are listening to preferences retrieved with Activity.getPreferences(), which are not the default preferences, but rather a SharedPreferences instance that is named after the Activity.
So you should switch to using default shared preferences, because the private shared preferences of the Activity will not be as easy to get a reference to in other activities.
I would also register in onCreate() instead of onStart() so you aren't registering multiple times. I don't know if that really matters, but the documentation doesn't specify what happens when you repeatedly register the same listener. Maybe it would fire the callback repeatedly, which could end up wasting your time hunting the bug later.
override fun onCreate() {
// ...
PreferenceManager.getDefaultSharedPreferences(this)
.registerOnSharedPreferenceChangeListener(this)
}
override fun onDestroy() {
super.onDestroy()
PreferenceManager.getDefaultSharedPreferences(this)
.unregisterOnSharedPreferenceChangeListener(this)
}
It is possible to specify the SharedPreferences that will be used by PreferenceFragmentCompat, but the default preferences are the easiest to use for settings that you want to access from across your application.
Related
I am making a simple app in kotlin and would like to add an additional option for night mode like this
example from Material Files app
I was thinking of adding a style and applying it programmatically, But I don't know how to do it properly.
Here's my code:
MainActivity
override fun onCreate(savedInstanceState: Bundle?) {
WindowCompat.setDecorFitsSystemWindows(window, false)
super.onCreate(savedInstanceState)
//It is called after super.onCreate because otherwise isNightMode does not return the correct value
when (ThemeHelper.nightModeChoice(this)) {
"nightModeFollowSystem" -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
"nightModeOn" -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)
"nightModeOff" -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO)
}
override fun onResume() {
super.onResume()
//If MainActivity is recreated return to SettingsFragment to have a nice animation
if (intent.extras != null) {
if (intent.extras!!.getBoolean("TEMA_CAMBIATO")) {
intent.putExtra("TEMA_CAMBIATO", false)
navController.navigate(R.id.SettingsFragment)
}
}
}
ThemeHelper,
this part is a bit messy, but it serves to prevent the activity from being recreated unnecessarily.
object ThemeHelper {
private fun isNightMode(context: Context): Boolean {
return context.resources.configuration.uiMode.and(Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES
}
fun nightModeChoice(context: Context): String? {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return prefs.getString("modTema", "nightModeFollowSystem")
}
fun recreate(context: Context, activity: Activity) {
var flag = true
if (isNightMode(context) && nightModeChoice(context) == "nightModeOn") {
Log.d("THEME_HELPER", "Non c'è bisogno di aggiornare")
flag = false
}
if (!isNightMode(context) && nightModeChoice(context) == "nightModeOff") {
Log.d("THEME_HELPER", "Non c'è bisogno di aggiornare")
flag = false
}
if (flag) {
val intent = activity.intent
intent?.putExtra("TEMA_CAMBIATO", true)
activity.finish()
activity.overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out)
activity.startActivity(intent)
}
}
}
SettingsFragment
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
PreferenceManager.getDefaultSharedPreferences(requireContext()).registerOnSharedPreferenceChangeListener(this)
}
override fun onDestroyView() {
super.onDestroyView()
PreferenceManager.getDefaultSharedPreferences(requireContext()).unregisterOnSharedPreferenceChangeListener(this)
}
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
if (isAdded) {
if (key == "modTema") {
ThemeHelper.recreate(requireContext(), requireActivity())
}
}
}
I solved in this way btw:
ThemeHelper
fun applyAmoled(context: Context, view: View){
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
if (prefs.getBoolean("modAmoled", false) && isNightMode(context)){
view.setBackgroundColor(Color.BLACK)
}
}
MainActivity after super.onCreate
ThemeHelper.applyAmoled(this, findViewById(R.id.content_main))
It's not the best solution but it works, I just need to recreate activity when needed
I have a simple MainActivity and if the app is completely killed it looks like onCreate() is called once. If however I back out of the app so it still appears in the background, when I re-open it I get every log message twice. The weirdest part is if I generate a random number it is always the same in the 2 log messages.
I've tried adding android:LaunchMode="singleTop" (also singleInstance singleTask) in the activity and application tags of the Manifest.
class MainActivity : AppCompatActivity() {
private val binding: ActivityMainBinding by lazy {
ActivityMainBinding.inflate(layoutInflater)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val view = binding.root
setContentView(view)
setupViews()
val data: Uri? = intent?.data
DataHolder.getInstance().setItem(data)
Timber.plant(Timber.DebugTree())
setupInjection()
Timber.d("review nanoTime = ${System.nanoTime()}")
Timber.d("review savedInstance = $savedInstanceState")
Timber.d("review random = ${Random.nextInt()}")
}
override fun onPause() {
Timber.d("review onPause()")
super.onPause()
}
override fun onStop() {
Timber.d("review onStop()")
super.onStop()
}
override fun onDestroy() {
Timber.d("review onDestroy()")
super.onDestroy()
finish()
}
override fun onStart() {
Timber.d("review onStart()")
super.onStart()
}
override fun onRestart() {
Timber.d("review onRestart()")
super.onRestart()
}
override fun onResume() {
Timber.d("review onResume()")
super.onResume()
}
private fun setupInjection() {
val appInjector = InjectorImpl(
firebaseAuth = FirebaseAuth.getInstance()
)
Injector.initialize(appInjector)
}
private fun setupViews() = binding.apply {
val navController = findNavController(R.id.nav_host_fragment)
navView.setupWithNavController(navController)
navView.setOnItemSelectedListener { item ->
when (item.itemId){
R.id.navigation_item_calculator -> {
navController.navigate(BuilderFragmentDirections.actionBuilderToCalculator())
}
R.id.navigation_item_builder -> {
navController.navigate(CalculatorFragmentDirections.actionCalculatorToBuilder())
}
}
true
}
navView.setOnItemReselectedListener { }
}
}
Here is a table of the log trace I get when I run the app on my phone from Android studio. Since the random numbers are the same I feel like this is actually a Logging bug in Android studio and the app isn't actually opened twice.
Realized my problem was with my logging library I used.
Timber was planting a new tree but wasn't uprooting old ones from being backed out so there were 2 instances of them. I fixed by putting a Timber.uprootAll() just before Timber.plant(Timber.DebugTree())
I have two different themes, I have to give this choice to the user to select the theme. but I have to call recreate() on every screen. is there any way to set the theme to entire application.
open class BaseActivity : AppCompatActivity(){
lateinit var currentTheme: String
lateinit var previousTheme: String
lateinit var sharedPref: SharedPreferences
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
sharedPref = PreferenceManager.getDefaultSharedPreferences(this)
if(this::currentTheme.isInitialized){
previousTheme = currentTheme
currentTheme = sharedPref.getString(KEY_CURRENT_THEME, LILAC_THEME)!!
}else{
currentTheme = sharedPref.getString(KEY_CURRENT_THEME, LILAC_THEME)!!
previousTheme = currentTheme
}
setAppTheme(currentTheme)
}
override fun onResume() {
super.onResume()
if(this::currentTheme.isInitialized){
previousTheme = currentTheme
currentTheme = sharedPref.getString(KEY_CURRENT_THEME, LILAC_THEME)!!}
if(currentTheme != previousTheme)
recreate()
}
private fun setAppTheme(currentTheme: String) {
when (currentTheme) {
MINT_THEME -> setTheme(R.style.Theme_App_Mint)
BLACK_THEME -> setTheme(R.style.Theme_App_Black)
BLUE_THEME -> setTheme(R.style.Theme_App_Blue)
else -> setTheme(R.style.Theme_App_Lilac)
}
}
}
every activity is extended to this baseactiviy. in which i have to call recreate on onResume to check the difference of theme and to make the decision to change the theme.
I just force restart of application after theme change with user prompt before the restart happens. This way I have to worry about theme load only in onCreate().
I want to use a DropDownPreference for my settings page but despite looking all over the internet, there doesn't seem to be any decent tutorial on how to do this. Does anyone know what should go in the onPreferenceChange method? I previously used a RadioButton but now want to use a DropDownPreference for easier implementation and maintenance.
Activity
class SettingsActivity : AppCompatActivity(), FragmentSettings.PreferenceXchangeListener {
private var mCurrentValue: Boolean = false // False is the default value
override fun onCreate(savedInstanceState: Bundle?) {
val mSharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)
mCurrentValue = mSharedPreferences.getBoolean("preference_dark", false)
if (mCurrentValue) {
setTheme(R.style.MyDarkSettingsTheme)
} else {
setTheme(R.style.MyLightSettingsTheme)
}
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_settings)
val viewllSettingsContainer = settings_container
val root = viewllSettingsContainer.rootView
if (mCurrentValue) {
root.setBackgroundColor(Color.BLACK)
} else {
root.setBackgroundColor(Color.WHITE)
}
val settingsFragment = FragmentSettings()
supportFragmentManager
.beginTransaction()
.replace(R.id.settings_container, settingsFragment)
.commit()
}
override fun onXchange(value:Boolean) {
when {
mCurrentValue != value -> {
mCurrentValue = value
recreate()
}
}
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
android.R.id.home -> {
val intent = parentActivityIntent
intent?.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION)
onBackPressed()
true
}
else ->
super.onOptionsItemSelected(item)
}
}
}
Fragment
class SettingsFragment : PreferenceFragmentCompat(), Preference.OnPreferenceChangeListener,
Preference.OnPreferenceClickListener {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
addPreferencesFromResource(R.xml.app_preferences)
}
}
I think you're over-complicating it. There is no need to involve the fragment in listening to changes to a preference it won't be handling and passing that back to the activity. You can register a preference change listener for all preferences in your Activity and respond accordingly there.
class SettingsActivity : AppCompatActivity(),
SharedPreferences.OnSharedPreferenceChangeListener {
override fun onCreate(savedInstanceState: Bundle?) {
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)
sharedPreferences.registerOnSharedPreferenceChangeListener(this)
//...
}
override fun onDestroy() {
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)
sharedPreferences.unregisterOnSharedPreferenceChangeListener(this)
//...
super.onDestroy()
}
override fun onSharedPreferenceChanged(_: SharedPreferences, key: String) {
when (key){
"preference_dark" -> recreate()
}
}
}
Why is it that my fragment returns a blank screen whenever I use savedInstanceState with it? I've already included the relevant savedInstanceStatecode in my activity, but the associated fragment still doesn't appear at all.
class MyActivity : AppCompatActivity() {
private var mCurrentValue: Boolean = false
private var mTwoPane: Boolean = false
private var activityRecreated: Boolean = false
override fun onCreate(savedInstanceState: Bundle?) {
val mSharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)
mCurrentValue = mSharedPreferences.getBoolean("preference_a", false)
when {
mCurrentValue -> setTheme(R.style.MyDarkTheme)
else -> setTheme(R.style.MyLightTheme)
}
super.onCreate(savedInstanceState)
activityRecreated = savedInstanceState != null
setContentView(R.layout.md)
}
override fun onStart() {
super.onStart()
setContentView(R.layout.md)
mTwoPane = findViewById<View>(R.id.detail_container) != null
val mSharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)
val mNewValue = mSharedPreferences.getBoolean("preference_a", false)
when {
mCurrentValue != mNewValue -> recreate()
}
val mToolbar = findViewById<Toolbar>(R.id.my_toolbar)
setSupportActionBar(mToolbar)
if (activityRecreated) {
val newFragment = MyFragment()
val transaction = supportFragmentManager.beginTransaction()
transaction.replace(R.id.master_container, newFragment)
transaction.commit()
}
}
}
There are a few problems here.
You should be setting your content view in onCreate(), not onStart(). onStart() can be invoked multiple times for the same Activity instance. For instance, if you start your Activity, press the Home button, and then resume your app, you'll go through onPause(), onStop(), then onStart(), onResume(). You only need to initialize your view when the Activity is created.
Your logic to display the Fragment only executes if the Activity is being recreated. I think you likely meant the inverse. You could simply change that to be if (!activityRecreated) but I would instead suggest cleaning this up by moving your view initialization entirely into onCreate() like so, and only checking if the theme state has changed in onStart():
class MyActivity : AppCompatActivity() {
private val useDarkTheme: Boolean = false
private var twoPane: Boolean = false
override fun onCreate(savedInstanceState: Bundle?) {
useDarkTheme = shouldUseDarkTheme()
setTheme(if (useDarkTheme) R.style.MyDarkTheme else R.style.MyLightTheme)
super.onCreate(savedInstanceState)
setContentView(R.layout.md)
// savedInstanceState will be null only the first time the Activity is created
if (savedInstanceState == null) {
supportFragmentManager.beginTransaction()
.replace(R.id.master_container, MyFragment())
.commit()
}
twoPane = findViewById<View>(R.id.detail_container) != null
setSupportActionBar(findViewById(R.id.my_toolbar))
}
override fun onStart() {
super.onStart()
if (useDarkTheme != shouldUseDarkTheme()) {
recreate()
}
}
private fun shouldUseDarkTheme(): Boolean =
PreferenceManager.getDefaultSharedPreferences(this).getBoolean("preference_a", false)
}