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
Related
I am passing TextToSpeech from Fragment to my RecyclerView Adapter.
While passing it, I am also sending a flag textToSpeechSupported to confirm whether the currently set device language is supported for TextToSpeech announcement or not.
But every time the value of this flag is being set as false, even though I am setting the value to true in onCreate.
It seems there is an issue with my implementation approach.
But I tired to debug and also added Logs.
Also I did multiple test and tried various other combinations. But every time the flag textToSpeechSupported value is being passed is false, even if the language is supported.
Am I missing something here.
I need the flag textToSpeechSupported value to be true if the device language is supported by TextToSpeech
Please guide.
class WelcomeFragment : Fragment() {
private lateinit var welcomeAdapter: WelcomeAdapter
private lateinit var textToSpeech: TextToSpeech
private var textToSpeechSupported: Boolean = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
textToSpeech = TextToSpeech(requireContext()) { status ->
if (status == SUCCESS) {
val result = textToSpeech.setLanguage(Locale.getDefault())
textToSpeechSupported =
!(result == TextToSpeech.LANG_NOT_SUPPORTED || result == TextToSpeech.LANG_MISSING_DATA)
}
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
welcomeAdapter = WelcomeAdapter(textToSpeech, textToSpeechSupported)
binding.adapter = welcomeAdapter
}
}
class Welcomedapter(private val textToSpeech: TextToSpeech, private val textToSpeechSupported: Boolean) : ListAdapter<Welcome, ViewHolder>(WelcomeDiffCallback()) {
//....
class ViewHolder private constructor(val binding: ContainerWelcomeBinding) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: Welcome, textToSpeech: TextToSpeech, textToSpeechSupported: Boolean) {
binding.apply {
welcomeMessageText.text = item.welcome
textToSpeechImage.setOnClickListener {
if (textToSpeechSupported) {
textToSpeech.speak(item.welcome, TextToSpeech.QUEUE_FLUSH, null)
} else {
// Send an event for Toast saying that language is not supported for Text to Speech
}
}
}
}
}
}
The objective/goal is to ensure that value of flag textToSpeechSupported is correctly calculated and passed to Recycler View Adapter.
The problem is that onViewCreated is being called BEFORE the tts has had time to initialize, so you are accessing textToSpeechSupported too early and you're always getting your default (false) value.
So, instead of calling:
welcomeAdapter = WelcomeAdapter(textToSpeech, textToSpeechSupported)
binding.adapter = welcomeAdapter
from inside onViewCreated, make a new function:
fun thisFunctionRunsAFTERtheTTSisInitialized() {
// put that code here instead.
}
And then call that function from inside your onCreate so it will run AFTER the tts has initialized:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
textToSpeech = TextToSpeech(requireContext()) { status ->
if (status == SUCCESS) {
val result = textToSpeech.setLanguage(Locale.getDefault())
textToSpeechSupported =
!(result == TextToSpeech.LANG_NOT_SUPPORTED || result == TextToSpeech.LANG_MISSING_DATA)
thisFunctionRunsAFTERtheTTSisInitialized() // <---------------
}
}
}
It would be better to pass an interface or function to adapter
e.g
class Welcomedapter(private val block : () -> Unit) : ListAdapter<Welcome, ViewHolder>(WelcomeDiffCallback()) {
//....
class ViewHolder private constructor(val binding: ContainerWelcomeBinding) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: Welcome, textToSpeech: TextToSpeech, textToSpeechSupported: Boolean) {
binding.apply {
welcomeMessageText.text = item.welcome
textToSpeechImage.setOnClickListener {
block()
}
}
}
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
welcomeAdapter = WelcomeAdapter {
if (textToSpeechSupported) {
textToSpeech.speak(item.welcome, TextToSpeech.QUEUE_FLUSH, null)
} else {
// show the toast here now
}
}
binding.adapter = welcomeAdapter
}
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.
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)
}
I am using Kotlin Android Extension to access view directly by their id.
I have a progress bar which I access directly in fragment using id i.e progress_bar
<ProgressBar
android:id="#+id/progress_bar"
style="#style/Widget.AppCompat.ProgressBar.Horizontal"
android:layout_width="match_parent"
android:layout_height="15dp"
android:indeterminate="true"/>
In fragment, I am showing and hiding it with this code
progress_bar.visibility = if (visible) View.VISIBLE else View.GONE
It is working perfectly until I rotate the screen. After that, it throws the exception
java.lang.IllegalStateException: progress_bar must not be null.
The variable gets null on screen rotation. How to solve this problem?
Fragment code
class SingleAppFragment : Fragment() {
private lateinit var appName: String
companion object {
fun newInstance(appName: String = ""): SingleAppFragment {
val fragment = SingleAppFragment()
val args = Bundle()
args.putString(Constants.EXTRA_APP_NAME, appName)
fragment.arguments = args
return fragment
}
}
private var mListener: OnFragmentInteractionListener? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
appName = if (arguments != null && !arguments.getString(Constants.EXTRA_APP_NAME).isEmpty()) {
arguments.getString(Constants.EXTRA_APP_NAME)
} else {
Constants.APP_NAME_FACEBOOK
}
}
override fun onCreateView(inflater: LayoutInflater?, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
return inflater!!.inflate(R.layout.fragment_single_app, container, false)
}
override fun onViewCreated(view: View?, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
initView()
setEventListeners()
}
private fun initView() {
var canShowSnackBar = true
web_single_app.webViewClient = object : WebViewClient() {
override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
super.onPageStarted(view, url, favicon)
showHideProgressBar(true)
canShowSnackBar = true
}
override fun onPageFinished(view: WebView?, url: String?) {
super.onPageFinished(view, url)
showHideProgressBar(false)
}
override fun onReceivedError(view: WebView?, request: WebResourceRequest?, error: WebResourceError?) {
web_single_app.stopLoading()
if (canShowSnackBar) {
mListener?.onErrorWebView()
canShowSnackBar = false
}
}
}
web_single_app.settings.javaScriptEnabled = true
web_single_app.loadUrl(Constants.APP_NAME_URL_MAP[appName])
}
private fun setEventListeners() {
back_web_control.setOnClickListener({
web_single_app.goBack()
})
}
fun showHideProgressBar(visible: Boolean) {
progress_bar_web_control.visibility = if (visible) View.VISIBLE else View.GONE
}
fun loadUrl(appName: String) {
web_single_app.loadUrl(Constants.APP_NAME_URL_MAP[appName])
}
override fun onAttach(context: Context?) {
super.onAttach(context)
if (context is OnFragmentInteractionListener) {
mListener = context
}
}
override fun onDetach() {
super.onDetach()
mListener = null
}
interface OnFragmentInteractionListener {
fun onErrorWebView()
}
}
Steps to reproduce:
Start Activity
Fragment get loaded
At Fragment load, I load an URL and show a progress bar
At loading the URL I rotate the phone and the progress bar variable gets null
In my case this bug happens from time to time. Of course, onViewCreated() is a good method to place your code in. But sometimes it's strangely not enough. And setRetainInstance(true) may help, may not. So sometimes this helps: access your Views with a view variable. You can even access them inside onCreateView(). You can use ?. for a guarantee that an application won't crash (of course, some views won't update in this case). If you wish to get context, use view.context.
In my case this bug reproduced only in Kotlin coroutines.
private fun showProgress(view: View) {
view.progressBar?.visibility = View.VISIBLE
}
private fun hideProgress(view: View) {
view.progressBar?.visibility = View.GONE
}
Then in code:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
showData(view)
}
private fun showData(view: View) {
showProgress(view)
adapter = SomeAdapter()
adapter.setItems(items)
val divider = SomeItemDecoration(view.context)
view.recycler_view?.run {
addItemDecoration(divider)
adapter = this#SomeFragment.adapter
layoutManager = LinearLayoutManager(view.context)
setHasFixedSize(true)
}
hideProgress(view)
}
In which method do you get the progress_bar by Id?
Please consider the fragment state lifecycle. Maybe you try to load it when the view is not ready yet.
Ensure your progress_bar variable is assigned only after the view is ready. For example in the
onViewCreated method.
See here the official Android lifecycle:
Update
As #CoolMind pointed out the diagram doesn't show the method onViewCreated.
The complete Android Activity/Fragment lifecycle can be found here:
Add retain intance true to the fragment so that it will not be destroyed when an orientation changes occurs
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
retainInstance=true
}
Also do a null check using safe call operator before accessing views
fun showHideProgressBar(visible: Boolean) {
progress_bar_web_control?.visibility = if (visible) View.VISIBLE else View.GONE
}