stringResource() not loading strings of current locale - android-jetpack-compose

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.

Related

Preview Composable in Another Language

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

Jetpack Compose collectAsState() does not work with Flow combine()

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)
}
}

LaunchedEffect executing on configuration change even though rememberUpdatedState has not changed

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.

How to pass object in navigation in jetpack compose?

From the documentation, I can pass string, integer etc. But how can I pass objects on navigation?
Note: If I set the argument type parcelable then the app crashes with java.lang.UnsupportedOperationException: Parcelables don't support default values..
composable(
"vendor/details/{vendor}",
arguments = listOf(navArgument("vendor") {
type = NavType.ParcelableType(Vendor::class.java)
})
) {
// ...
}
The following workarounds based on navigation-compose version 2.4.0-alpha05.
I found 2 workarounds for passing objects.
1. Convert the object into JSON string:
Here we can pass the objects using the JSON string representation of the object.
Example code:
val ROUTE_USER_DETAILS = "user-details/user={user}"
// Pass data (I am using Moshi here)
val user = User(id = 1, name = "John Doe") // User is a data class.
val moshi = Moshi.Builder().build()
val jsonAdapter = moshi.adapter(User::class.java).lenient()
val userJson = jsonAdapter.toJson(user)
navController.navigate(
ROUTE_USER_DETAILS.replace("{user}", userJson)
)
// Receive Data
NavHost {
composable(ROUTE_USER_DETAILS) { backStackEntry ->
val userJson = backStackEntry.arguments?.getString("user")
val moshi = Moshi.Builder().build()
val jsonAdapter = moshi.adapter(User::class.java).lenient()
val userObject = jsonAdapter.fromJson(userJson)
UserDetailsView(userObject) // Here UserDetailsView is a composable.
}
}
// Composable function/view
#Composable
fun UserDetailsView(
user: User
){
// ...
}
2. Passing the object using NavBackStackEntry:
Here we can pass data using navController.currentBackStackEntry and receive data using navController.previousBackStackEntry.
Example code:
val ROUTE_USER_DETAILS = "user-details/{user}"
// Pass data
val user = User(id = 1, name = "John Doe") // User is a parcelable data class.
navController.currentBackStackEntry?.arguments?.putParcelable("user", user)
navController.navigate(ROUTE_USER_DETAILS)
// Receive data
NavHost {
composable(ROUTE_USER_DETAILS) { backStackEntry ->
val userObject = navController.previousBackStackEntry?.arguments?.getParcelable<User>("user")
UserDetailsView(userObject) // Here UserDetailsView is a composable.
}
}
// Composable function/view
#Composable
fun UserDetailsView(
user: User
){
// ...
}
Important Note: The 2nd solution will not work if we pop up back stacks on navigate.
Parcelables currently don't support default values so you need to pass your object as String value. Yes it is a work around.. So instead of passing object itself as Parcelize object we can turn that object into JSON (String) and pass it through navigation and then parse that JSON back to Object at destination. You can use GSON for object to json string conversion...
Json To Object
fun <A> String.fromJson(type: Class<A>): A {
return Gson().fromJson(this, type)
}
Object To Json String
fun <A> A.toJson(): String? {
return Gson().toJson(this)
}
User NavType.StringType instead of NavType.ParcelableType..
composable("detail/{item}",
arguments = listOf(navArgument("item") {
type = NavType.StringType
})
) {
it.arguments?.getString("item")?.let { jsonString ->
val user = jsonString.fromJson(User::class.java)
DetailScreen( navController = navController, user = user )
}
}
Now navigate by passing string..
val userString = user.toJson()
navController.navigate(detail/$userString")
EDIT: There is also a limit for the Json-String that you can navigate. If the length of the Json-String is tooo long then the NavController won't recognize your Composable Route eventually throw an exception... Another work around would be to use a Global Variable and set its value in before navigating.. then pass this value as arguments in your Composable Functions..
var globalUser : User? = null // global object somewhere in your code
.....
.....
// While Navigating
click { user->
globalUser = user
navController.navigate(detail/$userString")
}
// Composable
composable( "detail") {
DetailScreen(
navController = navController,
globalUser)
}
NOTE :-> ViewModels can also be used to achieve this..
Let me give you very simple answers.
We have different options like.
Using Arguments but issue with this is that you can't share long or complex objects, only simple types like Int, String, etc.
Now you are thinking about converting objects to JsonString and trying to pass it, but this trick only works for small or easy objects.
Exception look like this:
java.lang.IllegalArgumentException: Navigation destination that matches request NavDeepLinkRequest{ uri="VERY LONG OBJECT STRING" } cannot be found in the navigation graph NavGraph(0x0) startDestination={Destination(0x2e9fc7db) route=Screen_A}
Now we have a Parsable Type in navArgument, but we need to put that object in current backStack and need to retrieve from next screen. The problem with this solution is you need keep that screen in your backStack. You can't PopOut your backStack. Like, if you want to popout your Login Screen when you navigate to Main Screen, then you can't retrieve Object from Login Screen to Main Screen.
You need to Create SharedViewModel. Make sure you only use shared state and only use this technique when above two are not suitable for you.
With Arguments:
You can just make this object Serializable and pass it to the backStackEntry arguments, also you can pass String, Long etc :
data class User (val name:String) : java.io.Serializable
val user = User("Bob")
navController.currentBackStackEntry?.arguments?.apply {
putString("your_key", "key value")
putSerializable("USER", user)
)
}
to get value from arguments you need to do next:
navController.previousBackStackEntry?.arguments?.customGetSerializable("USER")
code for customGetSerializable function:
#Suppress("DEPRECATION")
inline fun <reified T : Serializable> Bundle.customGetSerializable(key: String): T? {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) getSerializable(key, T::class.java)
else getSerializable(key) as? T
}
With savedStateHandle
Sometimes you have nullable arguments, so you can use savedStateHandle:
appState.navController.currentBackStackEntry?.savedStateHandle?.set("USER", user)
and get value:
navController.previousBackStackEntry?.savedStateHandle?.get("USER")
#HiltViewModel
class JobViewModel : ViewModel() {
var jobs by mutableStateOf<Job?>(null)
private set
fun allJob(job:Job)
{
Toast.makeText(context,"addJob ${job.companyName}", Toast.LENGTH_LONG).show()
jobs=job
}
#Composable
fun HomeNavGraph(navController: NavHostController,
) {
val jobViewModel:JobViewModel= viewModel() // note:- same jobViewModel pass
in argument because instance should be same , otherwise value will null
val context = LocalContext.current
NavHost(
navController = navController,
startDestination = NavigationItems.Jobs.route
) {
composable(
route = NavigationItems.Jobs.route
) {
JobsScreen(navController,jobViewModel)
}
composable(
route= NavigationItems.JobDescriptionScreen.route
)
{
JobDescriptionScreen(jobViewModel=jobViewModel)
}
}
}
}
in function argument (jobViewModel: JobViewModel)
items(lazyJobItems) {
job -> Surface(modifier = Modifier.clickable {
if (job != null) {
jobViewModel.allJob(job=job)
navController.navigate(NavigationItems.JobDescriptionScreen.route)
}

Using Vaadin components in a kotlin js project

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 = ""
})

Resources