https://kotlinlang.org logo
#android
Title
# android
e

ElawsDev

03/06/2024, 10:24 AM
Hi, is it possible to create combinations of string resources in ViewModel() before sending them to the UI ? I'm struggling with a trivial usecase, presented in the following message (thread) in a simplified way. I'm new to this and went through most of Android Basics for Jetpack Compose. edit : (what does the "thread-please" emoji means ? I moved most of the content of this post to a reply in the thread and still got a second "thread-please" emoji afterwards)
🧵 2
not kotlin but kotlin colored 2
- I need to build a list of 50 x 50 strings (let's call it listC). This list is built from 2 lists of 50 strings : listA and listB. Each string is localized, hence it has to be retrieved from string resources. - This is why I created a MyData class as follows (simplified code syntax) :
Copy code
kotlin
class MyData(val context: Context) {
  var listA : MutableList<String> = mutableListOf()
  var listB : MutableList<String> = mutableListOf()
  var listC : MutableList<String> = mutableListOf()

init{
   initializeData()
}

fun initializeData() {
   listA.apply {
     add(context.resources.getString(R.string.valueA1)
     ...
     add(context.resources.getString(R.string.valueA50)
   }

   listB.apply {
     add(context.resources.getString(R.string.valueB1)
     ...
     add(context.resources.getString(R.string.valueB50)
   }

   for a in listA
     for b in listB
         listC.add("$a: $b")
}
- listC items have the syntax "$a: $b" for simplification, but the string that will be displayed can be either "$a: $b", "$b: $a", "$a" or "$b". - One of these 50 x 50 strings will be shown to user based on business logic defined in ScreenViewModel, therefore data should be retrieved in ScreenViewModel as follows :
Copy code
kotlin
class ScreenViewModel : ViewModel() {

   val context = LocalContext.current
   var myData = MyData(context) 

}
Apparently this is very bad practice but I have absolutely no idea why nor how to do better (apparently it has something to do with ViewModel outliving activities and context may be outdated). Could you please help me learn the proper way to achieve this trivial usecase ? This is a quite difficult topic to grasp for a beginner : which resource should I study to have a better understanding of this issue ? Thank you very much.
a

Akram Raza

03/06/2024, 12:55 PM
Well the answer lies in your question itself. Like you said view model outlives the activities and if you take a look at activity lifecycle , one reason for your activity getting recreated is locale change but view model is not recreated. In fact the main reason for the view model concept was to keep data during such configuration changes(otherwise previously onSavedInsatanceState and onRestoreInstanceState were used to save and restore these data using bundles). And since your strings are localized, and view model is not recreated after the configuration changes which means it will be recreated after locale change also and so it will keep providing the outdated data. So, you will loose what you want to achieve with these localized strings. So that's why you shouldn't use it. And for reference just go through lifecycles of activities and fragments etc to understand them. And a better solution would be use live data in view model, get the strings in activity and use the live data object to reflect and observe the strings and make ui changes.
e

ElawsDev

03/06/2024, 1:27 PM
Thank you @Akram Raza, I will learn about live data, which I don't know yet. I also had this idea : in my data class, instead of storing lists of strings, I could store list of pairs of string resources ID (to account for the possible combinations "$a: $b", "$b: $a", "$a" or "$b" combinations), and then just update my uiState with the value of the pair of string resources ID that the UI needs to display ? Then, only the UI will use stringResource(...) to retrieve the string(s). Does this idea follow good architecture guidelines ? Thank you very much
a

Akram Raza

03/06/2024, 1:59 PM
How is that a data class if you are storing list of strings in it(I think you have misunderstood what a data class means) ? Why not just use an arraylist or map ? I mean there are a lot of collections which can serve this purpose. But since you need localized strings , going with live data and accessing string resources(array) is the best. Good architecture doesn't just mean seperation of concerns or following the lifecycle guidelines but also good readability and easy to maintain, easy to test etc.
e

ElawsDev

03/06/2024, 3:20 PM
@Akram Raza: By "data class", I was referring to the class "MyData" described in my initial post. Is there something wrong if I store a list of @StringResId (or rather a list of pairs of @StringResId here) in one of its properties, for them to be retrieved later in the UI layer ? Concerning the rest of your message, I'm afraid to say my current level of knowledge doesn't really allow me to grasp any of its content. Nonetheless, I will learn about "LiveData". Thank you.
a

Akram Raza

03/06/2024, 4:15 PM
Yes, it's a huge memory leak problem. How ? Suppose you want to add more functionality to this class to be used at various places. Now there also you will initialise an object for this class. And since the init block is initialising those lists, it will do it for all use cases for this class regardless of whether you use these lists or not. As you can see in the attachment. And , if you intend to use this class just once then it doesn't make any sense to create a new class, right ? Just go with an arraylist or any collection which suites you. Just go to kotlin playground , remove the context from constructor and test it with some strings.
IMG_20240306_214613.jpg
e

ElawsDev

03/06/2024, 4:31 PM
I can't use arraylist because I need to build listC from combination of listA and listB :
Copy code
for a in listA
     for b in listB
         listC.add("$a: $b")
I only need a single instance of MyData class : it is the class which holds all the data of my app.
@Akram Raza, then, based on business logic defined in my viewModel, I would simply assign one item of listC to a variable of uiState which is a MutableStateFlow (which is an alternative to LiveData, if I'm correct). So just to make sure : viewModel can't have strings resources (because I can't give it Context), but it will have strings resources ID. MyData class was modified so that listA, listB and listC do not hold strings anymore, but StringResourceID. Then finally, the UI will receive my StringResId values through the uiState. I think this should do the trick ?
a

Akram Raza

03/06/2024, 5:03 PM
Well MutableList is basically an Arraylist if you check it's implementation. The confusion raised here because you keep calling list in message but actually you are using MutableList. Secondly, your class is holding nothing. It just holds 3 arraylists. Which hold strings. I guess it depends on your choice. But better way would be to just have a function to do this operation, since it needs to be done just once. Lastly, yes you can implement this way but it's not good practice actually. Sharing resource id is not a state actually. State means what the ui should show to the user and not what needs to be used to create the ui. I mean just think about it, why have a viewmodel if ui will have to do work of generating data for itself? Anyways, go ahead try. You won't learn until you try new stuff.
e

ElawsDev

03/06/2024, 5:04 PM
If in the viewModel neither string resource nor string resource ID can be used, I'm completely clueless about how I could send data from viewModel to UI... ! I need at least some kind of reference to my data at some point ?
c

Chrimaeon

03/06/2024, 5:19 PM
Resource ID are completely fine. List C with the resource id is your “state” and depending on this list you then in the composable use the string it to retrieve it from the current resources.
1
e

ElawsDev

03/06/2024, 5:21 PM
@Chrimaeon: Thank you! From the previous discussion, I had trouble understanding if it was good practice or not. Are there better alternatives though ? Using the Resource ID is the only solution I can think of for now, since Context can't be provided to viewModel.
c

Chrimaeon

03/06/2024, 5:21 PM
Copy code
class ScreenViewModel : ViewModel() {

   var listC: List<Int> by muableStateOf(emptyList())
     private set

   init () {
     listC = <calucalte list content>
   }
}
e

ElawsDev

03/06/2024, 5:22 PM
Yes, this is exactly what I plan to do !
c

Chrimaeon

03/06/2024, 5:22 PM
yes, totally fine for your use case. better than having a Context in the viewmodel.
2
e

ElawsDev

03/06/2024, 5:25 PM
Thank you very much !
👍 1
a

Akram Raza

03/07/2024, 5:13 AM
@ElawsDev Well, I didn't say that you cannot do it. But it's not good practice due to multiple reasons. No final result provided to ui for Success state , maintainability , not easy to test because your listC will be hardcoded to the ids that you provide here etc. just to name a few.
e

ElawsDev

03/07/2024, 10:26 AM
@Akram Raza: Thank you for taking time to share this solution. However I have 2 questions : 1. I'm not familiar with the ViewModelProvider.Factory : is it necessary ? Can't I simply get my view model by :
Copy code
val listA = resources.getStringArray...
val listB = ...
val myViewModel = MyViewModel(listA, listB)
How does this compare to your solution ? 2. So I understand that I'm passing my ListA and ListB to the viewModel. But then my viewModel will be dependant on Context, since ListA and ListB depend on Context ? If user changes language, my view model will have outdated listA and listB values, right ? So is it really better practice than using resources IDs in the view model ? 3. Also, isn't data flowing from UI to viewModel in opposition to UDF principle ? Thank you for your help
What do you think about this possible alternative @Chrimaeon ?
a

Akram Raza

03/07/2024, 11:00 AM
@ElawsDev 1. No you cannot get viewmodel like any other class. Only ViewModelProvider can instantiate a view model class and the ViewModelProvider.Factor provides the interface to instantiate it with dependencies. For details refer to android devs site about ViewModels. 2. No , viewmodel will not have outdated data because the strings are being accessed via the activity. So when a locale change happens the string with the id in that locale will be provided to the viewmodel. I think this post should make it more clear for you https://medium.com/androiddevelopers/locale-changes-and-the-androidviewmodel-antipattern-84eb677660d9#:~:text=If%20there's%20a%20locale%20change,do%20so%20in%20the%20view. 3. No it's not providing a flow of data to viewmodel. We are instantiating the viewmodel with some dependencies. The flow is still from viewmodel to ui after business logic. And also, this solution is for your use case, you just want to have 2 lists in your viewmodel to generate a 3rd list instantly after instantiating the viewmodel. If your use case needed these lists later on when some ui change would trigger the business code then the implementation will change accordingly.
e

ElawsDev

03/07/2024, 11:11 AM
@Akram Raza : 1. It's odd that you say it's not possible, because all the Android Basics with Jetpack compose tutos do this (as you can see in "Pass the data" paragraph) ? 2. The article says that the viewModel outlives the activity. So once we've instanciated the viewModel in our activity and passed it our localized strings, if the user changes the locale, the viewModel will still have our "old" listA and listB. I don't see where they would get updated, since viewModel is instanciated once and for all.
a

Akram Raza

03/07/2024, 11:15 AM
@ElawsDev Ignore my solution for now. Your implementation with the ids is working for you, so just go with it.
e

ElawsDev

03/07/2024, 10:25 PM
@Akram Raza Ok, I think I understand what you meant a bit better. So indeed, only factory allows to pass something to the ViewModel. We get the strings in the activity and pass them to the ViewModel thanks to the Factory. Great. Now, please tell me if I'm correct or not : If the user changes language, the ViewModel will not be recreated (and this is fine, the goal of the ViewModel is to outlive the activity). So, the user will have to reboot the app for the strings retrieved in Activity to be passed again to the View Model. Is it correct ?
a

Akram Raza

03/08/2024, 6:49 AM
@ElawsDev Well I don't want to go deep diving into how android view models work with di. So just ignore my solution. It's better to proceed gradually and build yourself up. So I would suggest proceed with ids or use a seperate sealed class and make your view model resource free if you want to move a step ahead in your implementation. Btw, you can find multiple explanations using sealed class so that wouldn't be difficult to understand.
e

ElawsDev

03/08/2024, 10:02 AM
I understand. Anyway, thank you for your patience and your help.
7 Views