The application theme is not saved - android

I have a code that switches themes. But if I restart the application, the standard theme is set. Help me how to make sure that the theme is saved, which was selected last time.
class MainActivity : AppCompatActivity(), KodeinAware, SharedPreferences.OnSharedPreferenceChangeListener {
override val kodein by closestKodein()
private val fusedLocationProviderClient: FusedLocationProviderClient by instance()
private lateinit var binding: ActivityMainBinding
private val locationCallBack = object : LocationCallback() {
override fun onLocationResult(p0: LocationResult) {
super.onLocationResult(p0)
}
}
private lateinit var navController: NavController
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
requestWindowFeature(Window.FEATURE_NO_TITLE)
//setContentView(R.layout.activity_main)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
PreferenceManager.getDefaultSharedPreferences(this)
.registerOnSharedPreferenceChangeListener(this)
navController = Navigation.findNavController(this, R.id.nav_host_fragment)
bottom_nav.setupWithNavController(navController)
NavigationUI.setupActionBarWithNavController(this,navController)
if (hasLocationPermission()) {
bindLocationManager()
}
else {
requestLocationPermission()
}
//AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO)
}
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
if (key == "dark_mode"){
val prefs = sharedPreferences?.getString(key, "1")
when(prefs?.toInt()){
1->{
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO)
}
2->{
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)
}
3->{
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_AUTO_TIME)
}
4->{
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_AUTO_BATTERY)
}
}
}
}
}
This my ListPreference
<ListPreference
android:key="dark_mode"
android:title="Темы"
android:defaultValue="1"
app:useSimpleSummaryProvider="true"
android:entries="#array/dark_mode_entries"
android:entryValues="#array/dark_mode_entries_values"/>

Your problem is you only apply the theme in onSharedPreferenceChanged(), which will only be called at the moment when the setting is modified. You should also do this in onCreate so the setting is applied every time the Activity opens. Since you're doing it in two places, you should break it out into a separate function.
I also cleaned your code a bit. Call this function inside onCreate(), and also call it inside onSharedPreferenceChanged() instead of the code you currently have in that function.
private fun applyDarkModeSetting() {
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)
val settingValue = sharedPreferences.getString("dark_mode", null)?.toIntOrNull() ?: 1
val mode = when (settingValue) {
1 -> AppCompatDelegate.MODE_NIGHT_YES
2 -> AppCompatDelegate.MODE_NIGHT_NO
3 -> AppCompatDelegate.MODE_NIGHT_AUTO_TIME
else -> AppCompatDelegate.MODE_NIGHT_AUTO_BATTERY
}
AppCompatDelegate.setDefaultNightMode(mode)
}

Related

Why do I need invoke collect in Compose UI when I use MutableStateFlow?

I have read the Android official artical.
I see that MutableStateFlow is hot Flow and is observed by Compose to trigger recomposition when they change.
The Code A is from the the Android official artical, it's OK.
I'm very stranger why the author need to invoke collect to get latest value for Compose UI in Code A.
I think the Compose UI can always get the latest value of latestNewsViewModel.uiState, why can't I use Code B do the the same work?
Code A
class LatestNewsActivity : AppCompatActivity() {
private val latestNewsViewModel = // getViewModel()
override fun onCreate(savedInstanceState: Bundle?) {
...
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
latestNewsViewModel.uiState.collect { uiState ->
when (uiState) {
is LatestNewsUiState.Success -> showFavoriteNews(uiState.news)
is LatestNewsUiState.Error -> showError(uiState.exception)
}
}
}
}
}
}
class LatestNewsViewModel(
private val newsRepository: NewsRepository
) : ViewModel() {
private val _uiState = MutableStateFlow(LatestNewsUiState.Success(emptyList()))
val uiState: StateFlow<LatestNewsUiState> = _uiState
init {
viewModelScope.launch {
newsRepository.favoriteLatestNews
.collect { favoriteNews ->
_uiState.value = LatestNewsUiState.Success(favoriteNews)
}
}
}
}
Code B
class LatestNewsActivity : ComponentActivity() {
private val latestNewsViewModel = // getViewModel()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
SoundMeterTheme {
Surface(color = MaterialTheme.colors.background) {
Greeting(latestNewsViewModel)
}
}
}
}
}
#Composable
fun Greeting(latestNewsViewModel: LatestNewsViewModel) {
val myUIState by remember{ latestNewsViewModel.uiState }
when (myUIState) {
is LatestNewsUiState.Success -> showFavoriteNews(uiState.news)
is LatestNewsUiState.Error -> showError(uiState.exception)
}
}
//The same
Add Content
To RaBaKa 78: Thanks!
By your opinion, can I use Code C instead of Code A?
Code C
class LatestNewsActivity : ComponentActivity() {
private val latestNewsViewModel = // getViewModel()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
SoundMeterTheme {
Surface(color = MaterialTheme.colors.background) {
Greeting(latestNewsViewModel)
}
}
}
}
}
#Composable
fun Greeting(latestNewsViewModel: LatestNewsViewModel) {
val myUIState by remember{ latestNewsViewModel.uiState.collectAsState() }
when (myUIState) {
is LatestNewsUiState.Success -> showFavoriteNews(uiState.news)
is LatestNewsUiState.Error -> showError(uiState.exception)
}
}
//The same
Compose need State not StateFlow to recompose accordingly,
you can easily convert StateFlow to State in compose
val myUiState = latestNewsViewModel.uiState.collectAsState()
There is no need of using a remember {} because your StateFlow is from your viewModel, so it can manage the recomposition without remember
So like CODE B you can manually check the state of the StateFLow or convert to State and automatically recompose when the state changes.
The Code A is XML way of doing things where you can call other functions but in Compose you should do that steps in your viewModel
CODE D
class LatestNewsViewModel(
private val newsRepository: NewsRepository
) : ViewModel() {
private val _uiState = MutableStateFlow(LatestNewsUiState.Success(emptyList()))
val uiState: StateFlow<LatestNewsUiState> = _uiState
init {
viewModelScope.launch {
newsRepository.favoriteLatestNews
.collect { favoriteNews ->
_uiState.value = LatestNewsUiState.Success(favoriteNews)
}
}
}
}
#Composable
fun Greeting(latestNewsViewModel: LatestNewsViewModel) {
val myUIState = latestNewsViewModel.uiState.collectAsState()
Column(modifier = Modifier.fillMaxSIze()) {
when(myUIState) {
is LatestNewsUiState.Success -> SuccessComposable(uiState.news)
is LatestNewsUiState.Error -> showError(uiState.exception) -> ErrorComposable(uiState.exception)
}
}
}

Configuration Changes Not Save App State MVVM

I am building a movie app. There is a recyclerview matching parent size,and 1 search action button(SearchView).
When I search for a movie,everything is working fine,but when I change orientation,the activity just lose it's state. the recyclerview turns empty and I need to search for the movie again.
I am using MVVM and I know its not suppose to happen..
Thank you!
This is the Repository:
class MainRepository {
private val searchAfterMutableLiveData = MutableLiveData<List<Movie>>()
private val apiService : GetFromApi = APIService.retrofitClientRequest
private val apiKey = "censored"
fun searchAfter(searchAfter : String) : MutableLiveData<List<Movie>>{
apiService.searchAfter(apiKey,searchAfter)?.enqueue(object : Callback<MovieListResult?> {
override fun onResponse(
call: Call<MovieListResult?>,
response: Response<MovieListResult?>
) {
if (response.isSuccessful){
searchAfterMutableLiveData.value = response.body()?.moviesResults
Log.e("SearchMovieListResults","Result: ${searchAfterMutableLiveData.value}")
}
}
override fun onFailure(call: Call<MovieListResult?>, t: Throwable) {
Log.e("SearchMovieListResults","Failed: ${t.message}")
}
})
return searchAfterMutableLiveData
}
}
This is the ViewModel:
class MainViewModel : ViewModel(){
fun getMovieBySearch(searchAfter : String) : LiveData<List<Movie>>{
return mainRepository.searchAfter(searchAfter)
}
}
This is the MainActivity:
class MainActivity : AppCompatActivity() {
private val mainViewModel : MainViewModel by viewModels()
private lateinit var mainRecyclerView : RecyclerView
private lateinit var mainAdapter : MainRecyclerViewAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
initRecyclerView()
}
private fun initRecyclerView() {
mainRecyclerView = findViewById(R.id.mainRecyclerView)
mainRecyclerView.setHasFixedSize(true)
mainRecyclerView.layoutManager = GridLayoutManager(this,1)
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.main_menu,menu)
val searchView = menu.findItem(R.id.menu_search_movie).actionView as androidx.appcompat.widget.SearchView
searchView.queryHint = "Search By Name,Actor .."
searchView.setOnQueryTextListener(object : androidx.appcompat.widget.SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(whileTextChange: String?): Boolean {
//Clear SearchView
searchView.isIconified = true
searchView.setQuery("", false)
searchView.onActionViewCollapsed()
mainViewModel.getMovieBySearch(whileTextChange.toString()).observe(this#MainActivity,object : Observer<List<Movie>?> {
override fun onChanged(newList: List<Movie>?) {
if (newList != null) {
mainAdapter = MainRecyclerViewAdapter(newList)
mainRecyclerView.adapter = mainAdapter
//mainAdapter.changeCurrentList(newList)
}
}
})
return false
}
override fun onQueryTextChange(whileTextChange: String?): Boolean {
Log.e("onQueryTextChange","Text: $whileTextChange")
return false
}
})
return true
}
}
You need to save the desired state in the viewmodel. For example,
var persistedMovies = arrayListOf<Movie>()
and when the search returns a valid response,
mainViewModel.persistedMovies = newList
Now the list is scoped to the viewmodel and persists through orientation changes.

How to set actionbar title in the Fragment programmatically if I need to go to roomDatabase and jetPackDatastore for the values for it?

I try to set title on the toolbar by choosing category of that the fragment shows, for this I need to go to the jetPack dataStore to take the catigoryNumber: Int and use this int to fetch the respective categoryName from roomDatabase; I wrote the code (look the code on the Fragment below) and the title has become "StandaloneCoroutine{..." (and I don't see what is written next 'cause of not enough place in the toolbar).
How to set the title I need? Appreciate any help
In Fragment:
#AndroidEntryPoint
class VocabularyFragment : Fragment(R.layout.recycler_layout),
VocabularyAdapter.OnVocItemClickListener {
private val viewModel: VocabularyViewModel by viewModels()
private lateinit var searchView: SearchView
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val binding = RecyclerLayoutBinding.bind(view)
val vocabularyAdapter = VocabularyAdapter(this)
binding.apply {
recyclerView.apply {
adapter = vocabularyAdapter
layoutManager = LinearLayoutManager(requireContext())
setHasFixedSize(true)
itemAnimator = null // ХЗ НАДО ЛИ
}
}
viewModel.words.observe(viewLifecycleOwner) {
vocabularyAdapter.submitList(it)
}
setHasOptionsMenu(true)
viewModel.categoryNumber.observe(viewLifecycleOwner) {
(activity as MainActivity).supportActionBar?.title = viewModel.getCategoryName(it).toString()
} // I believe that this method is incorrect
}
In ViewModel:
class VocabularyViewModel #ViewModelInject constructor(
private val wordDao: WordDao,
private val preferencesManager: PreferencesManager,
) : ViewModel() {
val preferencesFlow = preferencesManager.preferencesFlow
// other methods
val categoryNumber = preferencesFlow.asLiveData()
fun onChooseCategoryClick(chosenCategory: Int) = viewModelScope.launch {
preferencesManager.updateCategoryChosen(chosenCategory)
}
fun getCategoryName(categoryNumber: Int) = viewModelScope.launch {
wordDao.getCategoryName(categoryNumber)
}
In Dao:
#Query("SELECT categoryName FROM category_table WHERE categoryNumber = :categoryNumber")
fun getCategoryName(categoryNumber: Int): Flow<String>
In PreferencesManager:
val preferencesFlow = dataStore.data
.catch { exception ->
if (exception is IOException) {
Log.e(TAG, "Error reading preferences", exception)
emit(emptyPreferences())
} else {
throw exception
}
}
.map { preferences ->
val categoryChosen = preferences[PreferencesKeys.CATEGORY_CHOSEN] ?: 0
categoryChosen
}
MainActivity:
class MainActivity : AppCompatActivity() {
private lateinit var navController: NavController
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val navHostFragment = supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
navController = navHostFragment.findNavController()
bottom_nav.setupWithNavController(navController)
setSupportActionBar(toolbar)
setupActionBarWithNavController(navController)
}
override fun onSupportNavigateUp(): Boolean {
return navController.navigateUp() || super.onSupportNavigateUp()
}
}
In XML NavGraph file:
<fragment
android:id="#+id/vocabularyFragment"
android:name="space.rodionov.swedishdriller.VocabularyFragment"
android:label="Vocabulary"
tools:layout="#layout/recycler_layout" >
<argument
android:name="categoryChosen"
app:argType="integer"
android:defaultValue="0" />
</fragment>
In your VocabularyViewModel you're setting getCategoryName directly to co-routine by using =
fun getCategoryName(categoryNumber: Int) = viewModelScope.launch {
wordDao.getCategoryName(categoryNumber)
}
If you're looking to fetch Name synchronously here then you should have
fun getCategoryName(categoryNumber: Int) = wordDao.getCategoryName(categoryNumber)
And in your query DAO
#Query("SELECT categoryName FROM category_table WHERE categoryNumber = :categoryNumber")
fun getCategoryName(categoryNumber: Int): String

RxBinding 'clicks()' method not triggering again when coming back from another activity

I am using 'RxJava binding APIs for Android UI widgets' to trigger click events on buttons or textview.
PFB code(Edited) that using to trigger the event
class BookAgentActivity : BaseActivity(), BookAgentView {
#Inject
#field:Named("activity")
lateinit var compositeDisposable: CompositeDisposable
#Inject
lateinit var bookAgentViewModelFactory: BookAgentViewModelFactory
private lateinit var bookAgentViewModel: BookAgentViewModel
private lateinit var cityLocalityJson: CityLocalitiesMO
override fun getLayoutId(): Int {
return R.layout.activity_book_agent
}
override fun initializeDagger() {
IleApplication.getRoomComponent().inject(this)
}
override fun initializeViewModel() {
bookAgentViewModel = ViewModelProviders.of(this, bookAgentViewModelFactory).get(BookAgentViewModel::class.java)
bookAgentViewModel.setView(this)
}
override fun setUpUi() {
gradientStatusBar()
cityLocalityJson = appPreferences.cityLocalitiesJson
compositeDisposable.add(bookAgentCityLocationTV.clicks().observeOn(AndroidSchedulers.mainThread()).subscribe {
startActivity(Intent(this, AreaLocalitiesActivity::class.java)
.putExtra(AppConstants.COMING_FROM_AGENT_KEY, true))
})
compositeDisposable.add(filtersButton.clicks().observeOn(AndroidSchedulers.mainThread()).subscribe {
startActivity(Intent(this, FiltersMainActivity::class.java)
.putExtra(AppConstants.FILTER_TYPE_KEY, AppConstants.AGENT_FILTER))
})
compositeDisposable.add(searchAgentsButton.clicks()
.subscribe { startActivity(Intent(this#BookAgentActivity, SearchAgentActivity::class.java)) })
}
override fun onSuccess(response: Any) {
if (response is AgentsDetailAPIResponse) {
response.let {
val agentDetailsList = it.data
if (agentDetailsList != null && agentDetailsList.size > 0) {
updateAgentPinsOnMap(agentDetailsList)
}
}
}
}
override fun onDestroy() {
super.onDestroy()
compositeDisposable.clear()
}
}
:) Above code works fine for the first time
:( But after coming back from BookAgentActivity (onBackPressed())
Click events are not working for searchAgentsButton as well as for other views too.
Have tried including combinations of other lines of code like below:
.subscribeOn(Schedulers.newThread())
.observeOn(AndroidSchedulers.mainThread())
.share()
But none of the above things are working.

Retain ViewModel through orientation change

I have a LoginActivity, where ViewModel is injected using dagger. LoginActivity calls an API through ViewModel upon click of a button. Meanwhile, if the screen rotates, it triggers onDestroy of LoginActivity and there, I dispose that API call. After this, in onCreate(), new instance of ViewModel is injected and because of this, my state is lost & I need to tap again in order to make API call.
Here's my LoginActivity:
class LoginActivity : AppCompatActivity() {
#Inject
lateinit var loginViewModel: LoginViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
AndroidInjection.inject(this)
val loginBinding = DataBindingUtil.setContentView<LoginBindings>(this, R.layout.activity_login)
loginBinding.loginVm = loginViewModel
loginBinding.executePendingBindings()
}
override fun onDestroy() {
super.onDestroy()
loginViewModel.onDestroy()
}
}
Here's my LoginViewModel:
class LoginViewModel(private val validator: Validator,
private val resourceProvider: ResourceProvider,
private val authenticationDataModel: AuthenticationDataModel) : BaseViewModel() {
val userName = ObservableField("")
val password = ObservableField("")
val userNameError = ObservableField("")
val passwordError = ObservableField("")
fun onLoginTapped() {
// Validations
if (!validator.isValidUsername(userName.get())) {
userNameError.set(resourceProvider.getString(R.string.invalid_username_error))
return
}
if (!validator.isValidPassword(password.get())) {
passwordError.set(resourceProvider.getString(R.string.invalid_password_error))
return
}
val loginRequest = LoginRequest(userName.get(), password.get())
addToDisposable(authenticationDataModel.loginUser(loginRequest)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doOnSubscribe { isApiCallInProgress.set(true) }
.doOnDispose {
LogManager.e("LoginViewModel", "I am disposed, save me!!")
isApiCallInProgress.set(false) }
.subscribe({ user ->
isApiCallInProgress.set(false)
LogManager.e("LoginViewModel", user.name)
}, { error ->
isApiCallInProgress.set(false)
error.printStackTrace()
}))
}
}
My BaseViewModel:
open class BaseViewModel {
private val disposables = CompositeDisposable()
val isApiCallInProgress = ObservableBoolean(false)
fun addToDisposable(disposable: Disposable) {
disposables.add(disposable)
}
fun onDestroy() {
disposables.clear()
}
}
Here's the module which provides the ViewModel:
#Module
class AuthenticationModule {
#Provides
fun provideLoginViewModel(validator: Validator, resourceProvider: ResourceProvider,
authenticationDataModel: AuthenticationDataModel): LoginViewModel {
return LoginViewModel(validator, resourceProvider, authenticationDataModel)
}
#Provides
fun provideAuthenticationRepo(): IAuthenticationRepo {
return AuthRepoApiImpl()
}
}
How can I retain my ViewModel through orientation changes. (Note: I am NOT using Architecture Components' ViewModel). Should I make my ViewModel Singleton? Or is there any other way of doing it?

Categories

Resources