This code make preview composable dark theme true or not.
#Preview
#Composable
fun AppPreview() {
AppBookTheme(darkTheme = false) {
TheBookApp()
}
}
#Preview
#Composable
fun AppDarkThemePreview() {
AppBookTheme(darkTheme = true,
) {
TheBookApp()
}
}
Is it possible to change the default preview composable language too ?
...
It's not necessary to explain to me how to change the language from the virtual phone or IDE settings ;)
Thanks in advance.
You can use the locale argument to specify the language.
#Preview(locale = "en")
#Composable
fun PreviewComponent() {
...
}
Checkout LocalQualifier
Related
Update
In the application class, I am injecting the ChangeLanguageRepository which sets the app language at each app startup by calling the function changeLanguage.
class ChangeLanguageRepository #Inject constructor(#ApplicationContext private val context: Context) {
fun changeLanguage(languageLocale: String) {
val locale = Locale(languageLocale)
Locale.setDefault(locale)
val config = Configuration()
config.locale = locale
context.resources.updateConfiguration(config, context.resources.displayMetrics)
}
}
I have developed an app using Jetpack Compose in which there is an option to change the language of the app. Now, when the language is changed, all the string resources that are loaded using stringResource() are not updating for the current locale even when the activity or app is restarted.
One of the screens inside the app is loading strings using the Context provided by Dagger Hilt in the Repository as shown below.
class HelplineRepository #Inject constructor(#ApplicationContext private val context: Context) {
fun getHelplines(): List<Helpline> {
val ambulance = Helpline(
Icons.Rounded.Emergency,
context.getString(R.string.helpline_text_ambulance),
context.getString(R.string.helpline_text_ambulance_number)
)
val fireStation = Helpline(
Icons.Rounded.LocalFireDepartment,
context.getString(R.string.helpline_text_fire_station),
context.getString(R.string.helpline_text_fire_station_number)
)
return listOf(ambulance, fireStation, policeControlRoom, womenHelpLine)
}
}
This screen properly shows the localized strings when the language is changed but the rest of the screens where I use the stringResource() inside a composable to load the strings don't update for the current locale.
Note: I have already tried restarting both the activity and the app and have also made sure that the locale is being changed when the user changes the language.
I also found a thread on the issue tracker mentioning a similar problem but couldn't find the solution.
Any help will be appreciated.
Updated answer
Composable code
#Composable
fun LocalizedGreeting() {
val context = LocalContext.current
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.fillMaxSize(),
) {
Column {
Button(
onClick = {
changeLanguage(
context = context,
language = "it",
)
},
) {
Text("Italian")
}
Button(
onClick = {
changeLanguage(
context = context,
language = "en",
)
},
) {
Text("English")
}
Text(
stringResource(
id = R.string.localized_greeting,
)
)
}
}
}
fun changeLanguage(
context: Context,
language: String,
) {
val locale = Locale(language)
Locale.setDefault(locale)
val config = Configuration()
config.locale = locale
context.resources.updateConfiguration(config, context.resources.displayMetrics)
context.startActivity(Intent.makeRestartActivityTask((context as Activity).intent?.component))
}
Video
The resources remain the same as the old answer given below.
Note
After the language is changed, the activity has to be restarted to get the updated strings. If there are multiple activities in the back stack, all of them have to be restarted.
Old Answer
(For using the localized strings without changing the locale programmatically)
To use localized string resources,
Composable code
#Composable
fun LocalizedGreeting() {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.fillMaxSize(),
) {
Text(
stringResource(
id = R.string.localized_greeting,
)
)
}
}
Default strings
res/values/strings.xml
<resources>
<string name="localized_greeting">Hello!</string>
</resources>
Italian strings
res/values-it/strings.xml
<resources>
<string name="localized_greeting">Ciao!</string>
</resources>
jetpackComposeVersion = "1.2.0-beta02"
Using localized strings was straightforward.
Probably you were missing something.
Provide the code you are using for further debugging if required.
I want to load data from Firestore, and combine it with other data using Flow combine()
ViewModel:
private val userCurrentProject = MutableStateFlow("")
val projects = repository
.listenToProject() //listening via Firestore snapshot listener, no problem here
.combine(userCurrentProject) { projects, currentProjectName ->
// combine works and called normally
projects.map { project ->
project.apply {
isUserCurrentProject = name == currentProjectName
}
}
}
fun setCurrentProject(projectName: String) = viewModelScope.launch {
userCurrentProject.emit(projectName)
}
Composables:
fun ProjectListScreen(navController: NavHostController, viewModel: ProjectsViewModel) {
val projects by viewModel.projects.collectAsState(initial = emptyList())
// This is where the problem started
// Lazy column not updated when projects flow is emitting new value
// Even Timber log does not called
Timber.d("Projects : $projects")
LazyColumn {
items(projects) { project ->
ProjectItem(project = project) {
currentlySelectedProject = project
scope.launch { bottomSheetState.show() }
}
}
}
The flow is working normally, but the state never got updated, I don't know why. Maybe this is a problem with collectAsState()?
But the state is updated when I navigate to next screen (add new project screen), then press back (popBackStack)
NB: using asLiveData() with observeAsState() does not work either.
I've finally found the answer
The culprit is that a State of custom object/class behaves differently than a state of primitives (String, Int, etc.)
For a State of object, you need to use copy()
So I just changed this part of ViewModel
val projects = repository
.listenProject()
.combine(userCurrentProject) { projects, currentProjectName ->
projects.map { project ->
// use copy instead of apply
val isCurrentProject = project.name == currentProjectName
project.copy(isUserCurrentProject = isCurrentProject)
}
}
If I'm understanding the documentation correctly a LaunchedEffect should not run again if the rememberUpdatedState has not changed.
If I run something like this code below then it's not working as expected and the value is getting updated again on rotation.
Without the LaunchedEffect the rememberSaveable is remembered on config change and the text in the input is correct (if I type something it's still there). This leads me to believe that the rememberUpdatedState should also not have changed but yet it gets triggered. Why?
What am I doing wrong or is this a bug? Alternatively is there a better way to do this?
Thanks :)
#Composable
fun ThingView(
thingViewModel: ThingViewModel,
id: String?
) {
var thingName by rememberSaveable { mutableStateOf("") }
val scope = rememberCoroutineScope()
LaunchedEffect(rememberUpdatedState(newValue = thingName)) {
scope.launch {
id?.let {
val thing = thingViewModel.getThing(id)
thingName = thing.name
}
}
}
OutlinedTextField(
value = thingName,
onValueChange = { thingName = it },
label = { Text("Name") }
)
}
Edit:
To clarify, the goal is to allow the user enter text in a textField and not have that text cleared on rotation. That would be super annoying for a user and probably not what they expect.
I think what you are seeing is that the LaunchedEffect state gets cleared on rotation when the configuration changes. Consequently I don't think there is any value that you can provide as a key to keep it from running.
There are two options that come to mind. In the style of your original code, you might consider only declaring the LaunchedEffect when you determine the state has changed. For example, you might maintain the last seen id in a saveable, say lastId, and test for changes on entry. It requires you have a sentinel value for lastId initially of course.
#Composable
fun ThingView(
thingViewModel: ThingViewModel,
id: String?
) {
var thingName by rememberSaveable { mutableStateOf("") }
var lastId: String? by rememberSaveable { mutableStateOf(null) }
val scope = rememberCoroutineScope()
if (lastId != id) {
lastId = id
LaunchedEffect(id) {
scope.launch {
id?.let {
val thing = thingViewModel.getThing(id)
thingName = thing.name
}
}
}
}
OutlinedTextField(
value = thingName,
onValueChange = { thingName = it },
label = { Text("Name") }
)
}
Another option that might be more traditional is to use the ViewModel to persist the state across configuration changes. You might update the ViewModel on each onValueChange and have the ViewModel provide the current value back to the OutlinedTextField. In this style you might not need the effect or the coroutine.
Just a last word regarding my understanding about rememberUpdatedState. I believe it is typically intended to allow a long-lived coroutine or callback to reference the current value of a variable in the composable scope without requiring that the coroutine or callback be recreated on each state change. The closure seems to be scoped to the effect and it capture the values at the time of creation. I am still trying to understand it better.
This question is about a Kotlin JS project which uses the Kotlin Frontend Plugin.
I want to use some UI components from the Vaadin Components library.
I have two questions about this:
(1) What would be the best way to include web components in Kotlin JS
=> for my complete code, see the link to the source below. In summary the relevant details are:
build.gradle.kts
kotlinFrontend {
npm {
dependency("#vaadin/vaadin-grid")
}
}
vaadin.grid.Imports.kt
#file:JsModule("#vaadin/vaadin-grid")
#file:JsNonModule
package vaadin.grid
external class GridElement {
companion object
}
Why the companion object? I need it for the workaround (see below).
foo.kt
fun main() {
document.getElementById("container")!!.append {
vaadin_grid {
attributes["id"] = "grid"
}
}
initUI()
}
fun initUI() {
// Force the side-effects of the vaadin modules. Is there a better way?
console.log(GridElement)
val grid = document.querySelector("#grid") /* ?? as GridElement ?? */
}
The console.log is the ugly workaround trick I want to avoid. If I don't do anything with GridElement then it's just not included in my bundle.
The vaadin_grid DSL is defined as a custom kotlinx.html tag which is unrelated code.
(2) I want to keep my code as typed as possible to avoid asDynamic but when I cast the HTMLElement to a Vaadin Element I get ClassCastExceptions (because GridElement is undefined).
For example I want to write something like this:
val grid : GridElement = document.querySelector("#grid") as GridElement
grid.items = ... // vs grid.asDynamic().items which does work
Here is how I define the external GridElement
vaadin/button/Imports.kt
#file:JsModule("#vaadin/vaadin-grid")
#file:JsNonModule
package vaadin.grid
import org.w3c.dom.HTMLElement
abstract external class GridElement : HTMLElement {
var items: Array<*> = definedExternally
}
build/node_modules/#vaadin/vaadin-grid/src/vaadin-grid.js
...
customElements.define(GridElement.is, GridElement);
export { GridElement };
Source example
To run:
From the root of the git repo:
./gradlew 05-kt-frontend-vaadin:build && open 05-kt-frontend-vaadin/frontend.html
I found the answer(s)
For the first question
(1) What would be the best way to include web components in Kotlin JS
Instead of the console.log to trigger the side effects I use require(...)
external fun require(module: String): dynamic
fun main() {
require("#vaadin/vaadin-button")
require("#vaadin/vaadin-text-field")
require("#vaadin/vaadin-grid")
...
}
(credits to someone's answer on the kotlin-frontend-plugin list)
(2) I want to keep my code as typed as possible to avoid asDynamic
Instead of importing #vaadin/vaadin-grid I have to import the file which actually exposes the element. Then it seems to work and I can even add generics to my GridElement:
#file:JsModule("#vaadin/vaadin-grid/src/vaadin-grid")
#file:JsNonModule
package vaadin.grid
import org.w3c.dom.HTMLElement
abstract external class GridElement<T> : HTMLElement {
var items: Array<out T> = definedExternally
}
This way I was able to get rid of all the asDynamics
val firstNameField = document.querySelector("#firstName") as TextFieldElement?
val lastNameField = document.querySelector("#lastName") as TextFieldElement?
val addButton = document.querySelector("#addButton") as ButtonElement?
val grid = document.querySelector("#grid") as GridElement<Person>?
val initialPeople: Array<out Person> = emptyArray()
grid?.items = initialPeople
addButton?.addEventListener("click", {
// Read the new person's data
val person = Person(firstNameField?.value, lastNameField?.value)
// Add it to the items
if(grid != null){
val people = grid.items
grid.items = people.plus(person)
}
// Reset the form fields
firstNameField?.value = ""
lastNameField?.value = ""
})
I just updated my Android Studio to version 3.2 and followed instructions to use androidx.
I've been using a Youtube fragment inside a Fragment activity and everything worked perfectly but, after the update, these 3 simple lines now give me the error "Cannot resolve method 'add(...)'":
YouTubePlayerSupportFragment youTubePlayerFragment = YouTubePlayerSupportFragment.newInstance();
FragmentTransaction transaction = getChildFragmentManager().beginTransaction();
transaction.add(R.id.youtube_fragment, youTubePlayerFragment).commit();
...and when i try to use "replace" instead of "add" it says: "Wrong 2nd argument type. Found: 'com.google.android.youtube.player.YouTubePlayerSupportFragment', required: 'androidx.fragment.app.Fragment'"
...which makes me think that the problem has to do with the new AndroidX feature.
The problem is that the add method wants the second parameter of type:
androidx.fragment.app.Fragment
...but the YouTubePlayerSupportFragment returns a:
android.support.v4.app.Fragment
Does anyone know how to solve this problem?
Is there a way to cast the "android.support.v4.app.Fragment" into the "androidx.fragment.app.Fragment"?
Just use transaction.replace. Ignore the error, it'll work. Google hasn't refactored youtube api library to androidx yet.
Just copy the original java file (com.google.android.youtube.player.YouTubePlayerFragment) to your project to the same package but different class name etc. com.google.android.youtube.player.YouTubePlayerFragmentX, and update the extends class from android.app.Fragment to androidx.fragment.app.Fragment.
The implementation is the same:
YouTubePlayerFragmentX youTubePlayerFragment = YouTubePlayerFragmentX.newInstance();
FragmentTransaction transaction = getChildFragmentManager().beginTransaction();
transaction.add(R.id.youtube_fragment, youTubePlayerFragment).commit();
Tested... it's working.
I've fixed it by following the #Hosszful answer,
I made it easy by just using this file, https://gist.github.com/medyo/f226b967213c3b8ec6f6bebb5338a492
Replace .add
transaction.add(R.id.youtube_fragment, youTubePlayerFragment).commit();
with this .replace
transaction.replace(R.id.youtube_fragment, youTubePlayerFragment).commit();
and copy this class to your project folder (it may need to create the following folders)
java -> com -> google -> android -> youtube -> player -> (here name of)
YouTubePlayerSupportFragmentX.java
then in code replace
YouTubePlayerSupportFragment
to
YouTubePlayerSupportFragmentX.
Many thanks to both #Hosszuful and #Mehdi. I have followed your advice and it worked very nicely.
A few weeks after I asked this question I "translated" my app to Kotlin and, therefore, I tried to translate your answer as well.
This is what I ended up with and it's working for me.
package com.google.android.youtube.player //<--- IMPORTANT!!!!
import android.os.Bundle
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import com.google.android.youtube.player.internal.ab
import java.util.*
class YouTubePlayerSupportFragmentX : Fragment(), YouTubePlayer.Provider {
private val a = ViewBundle()
private var b: Bundle? = null
private var c: YouTubePlayerView? = null
private var d: String? = null
private var e: YouTubePlayer.OnInitializedListener? = null
override fun initialize(var1: String, var2: YouTubePlayer.OnInitializedListener) {
d = ab.a(var1, "Developer key cannot be null or empty")
e = var2
a()
}
private fun a() {
if (c != null && e != null) {
c?.a(this.activity, this, d, e, b)
b = null
e = null
}
}
override fun onCreate(var1: Bundle?) {
super.onCreate(var1)
b = var1?.getBundle("YouTubePlayerSupportFragment.KEY_PLAYER_VIEW_STATE")
}
override fun onCreateView(var1: LayoutInflater, var2: ViewGroup?, var3: Bundle?): android.view.View? {
c = YouTubePlayerView(Objects.requireNonNull(this.activity), null, 0, a)
a()
return c
}
override fun onStart() {
super.onStart()
c?.a()
}
override fun onResume() {
super.onResume()
c?.b()
}
override fun onPause() {
c?.c()
super.onPause()
}
override fun onSaveInstanceState(var1: Bundle) {
super.onSaveInstanceState(var1)
(if (c != null) c?.e() else b)?.let { var2 ->
var1.putBundle("YouTubePlayerSupportFragment.KEY_PLAYER_VIEW_STATE", var2)
}
}
override fun onStop() {
c?.d()
super.onStop()
}
override fun onDestroyView() {
this.activity?.let { c?.c(it.isFinishing) }
c = null
super.onDestroyView()
}
override fun onDestroy() {
if (c != null) {
val var1 = this.activity
c?.b(var1 == null || var1.isFinishing)
}
super.onDestroy()
}
private inner class ViewBundle : YouTubePlayerView.b {
override fun a(var1: YouTubePlayerView, var2: String, var3: YouTubePlayer.OnInitializedListener) {
e?.let { initialize(var2, it) }
}
override fun a(var1: YouTubePlayerView) {}
}
companion object {
fun newInstance(): YouTubePlayerSupportFragmentX {
return YouTubePlayerSupportFragmentX()
}
}
}
There may be better ways to write it down and any help on that regard would be mostly appreciated but, if anyone else was looking for the Kotlin version of this problem's solution, this code should do.
PS: I'm gonna leave #Mehdi's answer as the accepted one because he's also sharing credits with #Hosszuful and because my answer is just the Kotlin version of what they suggest.
I got it working by following code chunk.
Object obj =
getSupportFragmentManager().findFragmentById(R.id.youtube_player_fragment);
if (obj instanceof YouTubePlayerSupportFragment)
youTubePlayerFragment = (YouTubePlayerSupportFragment) obj;
During debugging I found that the fragmentmanager was coming to be instance of YouTubePlayerSupportFragment only. But compiler was not able to cast it when I would write
(YouTubePlayerSupportFragment)
getSupportFragmentManager().findFragmentById(R.id.youtube_player_fragment);
The above code chunk (instanceof ) worked fine.
Suggested solutions did not work, till I tried the comment from Bek: Pierfrancesco Soffritti's android-youtube-player that is maintained and works without a hitch.