Nice one! TLDR: Return resource id instead of String from ViewModel using a wrapper class, then extract the string from UI layer where context is available.
@rpitpatel1004 Жыл бұрын
Can you point me direction of example for better understanding?
@evanparrish43292 жыл бұрын
This was impressive, Philip. Power to you for constantly improving and thanks for sharing your knowledge!
@PhilippLackner2 жыл бұрын
Glad you enjoyed it!
@Internet_Ghost-GA Жыл бұрын
I really want to become like you in coding . I am so glade to find youtuber like you who share his knowledge with us
@danju1322 жыл бұрын
The timing on this one is crazy, I was just trying and failing to solve this problem :D well, that problem is now solved :) you are the GOAT!
@PhilippLackner2 жыл бұрын
Glad I could help!
@yuriynekhodov31582 жыл бұрын
If you need to show error message, you can create sealed class/interface UiEvent, add its child for every type of error in hierarchy, and then map it in UI layer with when operator and using local context. So you won't need resources in your view model or domain layer.
@prabhatpandey50772 жыл бұрын
I was using a data class to hold either a messageStr or a messageResId and sending that as error text to my UI layer from viewmodel. The thought of using sealed class didn't occur to me. Many thanks for this helpful video.
@edgaremmanuel31972 жыл бұрын
I am a fluttter developer but I learn alot from this channel
@mithilmehta1501 Жыл бұрын
You don't know how much I have learned from you. Your tips and tricks are awesome 🙏
@andres_tomillas2 жыл бұрын
Wow, I've just faced that issue today, and here is your upload! The universe has heard me 😅 Thanks for your videos, you're doing a great job, keep it up!
@JordanTuffery Жыл бұрын
Hi Philip, I just discovered your video ! Fun fact We did litteraly the same approach (even same naming sometimes) in our app last year too, And I'd like to share an improvement about this code, and it would be to add a sealed class called Args in the UiText.StringResource , which will allow you to have arguments in your string like "blabla %s". The final result would look like this : sealed class UiText { data class DynamicString(val value: String) : UiText() data class StringResource( @StringRes val resId: Int, val args: List = emptyList() ) : UiText() { sealed class Args { data class DynamicString(val value: String) : Args() data class UiTextArg(val uiText: UiText): Args() fun toString(context: Context) = when (this) { is DynamicString -> value is UiTextArg -> uiText.asString(context) } } } fun asString(context: Context): String = when (this) { is DynamicString -> value is StringResource -> context.getString( resId, *(args.map { it.toString(context) }.toTypedArray()) ) } }
@mohsenrzna84732 жыл бұрын
Despite the code and the nice approach, it shows your commitment and love to make content on KZbin even when you got cold. Keep up the good work! ;)
@PhilippLackner2 жыл бұрын
I was waiting for someone to notice :D Thanks a lot man!
@clamum96482 жыл бұрын
Philipp you absolute gigachad, you should do a video on how to do layouts/UIs with Jetpack Compose, but not like the million other tutorials out there that position a few little text fields around and change the background color. That's not useful for people trying to learn this for the first time and interested in writing an actual real-world app. So just like the positioning of different elements and putting them together to create real-world app UIs, showing different functionalities of the Modifier, etc. I haven't seen anything good like this, and even the single class on Udemy that seems to be just on layouts doesn't look very extensive and is short.
@leonawroth25162 жыл бұрын
This looks really interesting. I always just passed down the resId and let the view handle the rest. But your solution looks even cleaner. I'll give it a try!
@tutorialexpress18942 жыл бұрын
Hi phillip could you help me to understand how can use navigation components for feature multi module with arguments and DeepLinks?
@nokapr64932 жыл бұрын
Nice video! Just few thoughts: - Why not to use MutableState instead of Channel? What is the advantage of using Channel here? - Also DynamicString seems a bit misleading to me, since the string/text is rather fixed than dynamic (when you change device language, the text doesn't change). Would name it StringValue maybe. But overall very good video.
@legato02 жыл бұрын
I was looking for an answer to this problem for a long time and finally thanks to you the problem was solved 👍👍
@PhilippLackner2 жыл бұрын
Great 👍
@RexTorres2 жыл бұрын
I wish I knew this months ago!! Thanks for this, Philipp!
@TechCrack012 жыл бұрын
Nice trick 👌! However, with a good UiState representation, you shouldn't need to send that string text from your VM to your UI layer at first place.
@foivosstamopoulos9709 Жыл бұрын
Thank you so much Philipp! You are a great tutor!
@khiariyoussef32262 жыл бұрын
I usually don't set messages in the view model, i only send codes or states which can be mapped to the correspond resources in the UI layer.
@PhilippLackner2 жыл бұрын
And what do you do if the error comes from the repo? (sometimes from the API, sometimes a string resource)
@khiariyoussef32262 жыл бұрын
@@PhilippLackner mapping them to custom codes? Usually I let the errors propagate to the ViewModel where I catch them and send a code to the UI instead.
@kwabenaberko53172 жыл бұрын
Exactly. This is what I do as well. In the case of errors, data source errors are converted to domain errors(sealed classes) which the UI receives and maps to the necessary string resources
@javimardeveloper Жыл бұрын
That is really cool. My question is how to use inside a use case, since those should be Android agnostic.
@xenofonkarydis64062 жыл бұрын
That's a very nice solution, although it requires to have your project built upon Jetpack Compose!
@PhilippLackner2 жыл бұрын
No it doesn't, just leave away the composable function and use the other one taking the context instead
@talkativebreakdown23112 жыл бұрын
@@PhilippLackner Hello Philipp, but I can't create LocalContext.current in activity file. Isnt it only for Compose?
@PhilippLackner2 жыл бұрын
@@talkativebreakdown2311 yes, inside an activity just pass this
@hossamqandel56382 жыл бұрын
but is this approach with passing context inside every ui will make nay problem?
@mesutemrecelenk54472 жыл бұрын
Hi Philipp. Thanks for your best practice sharing. Can we collect this message in MainActivity and trigger showing snackbar in our setContent {...} for all composable screens?
@bleachedlizard2 жыл бұрын
One problem I've found with using this pattern is that if you ever want to access these strings by calling the asString function from within the viewmodel (for example, to perform some validation on them) you can't, as you can't have access to the Context from within the viewmodel. Basically, as soon as you wrap the String/resource in a UiText object it becomes inaccessible from anywhere where you don't have access to the Context. Is there any solution to this?
@PhilippLackner2 жыл бұрын
No, because the value of the string isn't fully clear until it reaches the UI layer. Could be any language and you also want to make sure that the ui refreshes when the language is changed. That doesn't work if you read it before reaching the ui since it won't be re-read on a Config change
@withKaaveh2 жыл бұрын
Hey Philipp, Do you make a video and explain how you record and create these tutorials videos after you bought MacBook?
@snow4dv9 ай бұрын
Isn't it better to write an extension composable function and put it in presentation layer? In that case it would be possible to make viewmodel separated from compose (if we don't use compose-specific stuff like mutable states). It also looks more like a shortcut so extension function fits better for it to me
@Renaro2 жыл бұрын
Very good content! Using an interface wrapper for the resources injected in the ViewModel would also work right? Then you can easily provide it and also mock it for unit tests.
@critikalfade2 жыл бұрын
This is simple and helpful. Thanks!
@sameeryadav88572 жыл бұрын
can someone tell me what context is? I have been seeing it from day 1 when i started with app dev but never knew what it means. If there some article or blog explaining it (in easy ways) please link in reply.
@abdelazizyasser35122 жыл бұрын
Hello phillip , thank u so much for your efforts, can i know what's the Channel (include send&receive functions) class in this case ?
@daryldyck79382 жыл бұрын
Very helpful - Thanks! I'm thinking we may be able to do something similar to access dark/light theme color values outside of a composable context...
@PhilippLackner2 жыл бұрын
Yeah that should be possible :)
@ahmedna75432 жыл бұрын
Just sent the resource Id to the channel (observable) -> Channel , send a Channel in case you need api strings too , This needs change in 2 line of code , instead of all this.
@masmmaw2 жыл бұрын
can we not fill in the argument variable (vararg) because not all cases are like in the video right? sometimes we just display static messages in the snackbar.
@PhilippLackner2 жыл бұрын
yeah sure, then you just leave it empty
@samilindo12 жыл бұрын
I follow with an implementation very close to yours, but I make an extension of the View class, which allows me to receivereceive the UiText as a parameter, and I set the value internally using the context of the vie itself
@jatinsachdev2 жыл бұрын
I think that is part of UI - should not be moved to ViewModels. Validation logic in ViewModel should return some status that can be handled on UI to show the proper message from resources.
@PhilippLackner2 жыл бұрын
That works, however IMO the UI should be as "dumb" as possible. The whole purpose of the VM is to take inputs from the UI and update the state for it. Letting the UI decide how to handle specific statuses puts too much logic into the UI layer.
@jatinsachdev2 жыл бұрын
In architecture, nothing is right or wrong. What works works. All boils down to maintenance of the project. Since messages are part of UI, changing it on UI layer separates it from other part of the app. State being part of UI does not make it dumb. There will be some composables that are dumb for sure but some require state.
@PhilippLackner2 жыл бұрын
@@jatinsachdev sure, if you like it more nobody keeps you back. I just prefer a different solution 😄
@jatinsachdev2 жыл бұрын
@@PhilippLackner 😂
@mustafaammar5512 жыл бұрын
Thank you BRO 👍👍👍
@PhilippLackner2 жыл бұрын
Welcome 👍
@UpLiftingU0072 жыл бұрын
Thanks for sharing philip
@PhilippLackner2 жыл бұрын
You are very welcome
@rogercolque Жыл бұрын
Hi good content Please anyone have and example or Tutorial using XML instead of Compose using Koin and idk if will work if have devices for diferents countries. I already have all strings in different languages..
@vahekhachaturian2424 Жыл бұрын
Very nice trick! However, I don't really see how this can be used if you were to write unit tests on viewmodel
@berkc53232 жыл бұрын
Which android studio theme is this?
@dmitrymukhin90502 жыл бұрын
I would advise you to use polymorphism instead of when(this)
@skullkrum202 жыл бұрын
I wonder who suggested this in one of your live streams on Twitch 💪😁
@PhilippLackner2 жыл бұрын
Ahh it was you, I remembered someone did, but forgot who it was :DD
@skullkrum202 жыл бұрын
@@PhilippLackner I have learnt a lot from you, at least once I (well not really teach you coz you would get here eventually anyway) I could help you! 😁 Gotta love the Android community!
@PhilippLackner2 жыл бұрын
Thanks, you indeed did help me with that. Especially in a live stream, rather simple things are often so out of sight :D Gotta keep using your library for future videos!
@s-w2 жыл бұрын
I like it. I've just been passing either strings or string resources, but it makes sense to do this for consistency.
@skullkrum202 жыл бұрын
@@s-w it’s not just for consistency. Imagine you have a TextView that may have to display either text coming from the server or a string resource. Ideally you want to have a single outlet in the ViewModel for that TextView and for that you need something like this
@rpitpatel1004 Жыл бұрын
If we have 10 string resources to use in Ui then need 10 state in VM and it will quickly become messier. If I understand that correctly right?
@PhilippLackner Жыл бұрын
Why, you only need this for strings that require some calculation based on logic. Not for static ones
@maxhartung972 жыл бұрын
Hi Philip, do you have any videos regarding generating views programatically ? Without XML ? If not, do you have any recommendations ?
@PhilippLackner2 жыл бұрын
You just create them as a normal instance, like any other class. Then you add them to an existing layout using layout.addView(yourView)
@mieszkokozma42392 жыл бұрын
A problem with this approach is that two identical strings with params will return false when compared. One way to work it around would be to replace class with data class implementation with fixed amount of parameters instead of vararg. data class StringResource( @StringRes val resId: Int, val arg1: Any? = null, val arg2: Any? = null, val arg3: Any? = null, val arg4: Any? = null, val arg5: Any? = null ) : UiText() fun asString(context: Context): String { return when (this) { is DynamicString -> value is StringResource -> context.getString(resId, *arrayOf(arg1, arg2, arg3, arg4, arg5).filterNotNull().toTypedArray()) } }
@denchic452 жыл бұрын
Great! It seems to me that it is possible to make the arguments type also UiText, on which the asString fun would be called, so it would be possible to do more complex combined strings
@PhilippLackner2 жыл бұрын
But why would you do that, you can use string arguments
@denchic452 жыл бұрын
@@PhilippLackner But what if you need to concatenate 2 string resources?
@PhilippLackner2 жыл бұрын
@@denchic45 then you do uiText1.asString() + uiText2.asString()
@denchic452 жыл бұрын
@@PhilippLackner I could be wrong, but then a warning might appear: "Do not concatenate text rendered with setText. Use a resource string with placeholders."
@cassiobruzasco2 жыл бұрын
@@denchic45 isolate it in a val and then use the val at the setText
@bringoff2 жыл бұрын
What about creating a wrapper interface, let’s say, LocalizationProvider, with getString function, pass an implementation that works like proxy for context to ViewModels in real code and mock the interface for unit tests? Works for both xml and compose.
@niranz77452 жыл бұрын
I agree. I think this approach is better
@汪枭杰2 жыл бұрын
"pass an implementation that works like proxy for context to ViewModels" Will your implementation hold context internally? If yes, it's actually passing context into ViewModel, which may lead to context leak. Another concern for this is, if device locale changes, string in liveData won't be updated. So it's still a better solution to get it in UI layer
@bringoff2 жыл бұрын
@@汪枭杰 application context cannot leak because its lifecycle is the same as the whole application. Activity is recreated after configuration change (or callback is triggered if you change this behavior in manifest) or onConfigurationChanged is called, so you are free to handle it the way you like.
@DiegoNovati12 жыл бұрын
I don't like at all the solution: the ViewModel manages error messages, and it should be done instead by the view layer. Instead of passing a String or a UiText (ViewModel deals with the UI !!!!) you should pass a sealed class defining all the possible errors; the View layer will convert the error in the string to display (in this way the ViewModel notify an error to the view, the view will convert to a string using an extension function for that specific sealed class without breaking the MVVM architecture)
@PhilippLackner2 жыл бұрын
That's another way to do it that just puts more logic in your UI
@DiegoNovati12 жыл бұрын
@@PhilippLackner Not in the View, but in the View layer (using the same @Composable logic, that belongs to View) and you are using a @Composable in your ViewModel.
@PhilippLackner2 жыл бұрын
@@DiegoNovati1 the viewmodel belongs to the presentation layer. And I'm not using composable in the viewmodel, it's used in ui text.
@DiegoNovati12 жыл бұрын
@@PhilippLackner UiText contains a @Composable function (asString()) so it uses a View libraries breaking the MVVM architecture: the ViewModel should have no knowledge of the UView layer. To bypass the problem you should define an interface for the UiText, and defines it in the ViewModel layer, and then implement it in the View layer using the @Composable.
@PhilippLackner2 жыл бұрын
@@DiegoNovati1 who says thats a problem? Then you also wouldn't be allowed to use compose state in a viewmodel lol
@alishernuraliev90752 жыл бұрын
Thank you very much !)
@dev_jeongdaeri2 жыл бұрын
super cool!
@thegreatwarrior49892 жыл бұрын
Can you please make a video about material 3 and jetpack compose insets
@abawell2 жыл бұрын
Thanks for your solution. Personally I use the application context. Is this a problem ?
@PhilippLackner2 жыл бұрын
Yeah, then you can't write local unit tests for your VM and have to make them instrumented tests which take a lot longer
@umnikya787411 ай бұрын
So cool!
@arfinhosain2 жыл бұрын
what is ur code editor font?
@j2shoes288 Жыл бұрын
ViewModel is part of the Presentation layer, and as such, not sure, why you want to avoid the context inside it?
@PhilippLackner Жыл бұрын
To make it testable with JVM tests
@smreha2 жыл бұрын
Great video 👍
@martindinaputra86012 жыл бұрын
hi philipp, about what you said in the video that if we use application context in the viewModel, we wouldn't be able to run the test in the JVM environment. So I actually used this approach via dependency injection like the code below @HiltViewModel class SomeViewModel @Inject constructor( private val repository: SomeRepository, @ApplicationContext private val injectedContext: Context ) : BaseViewModel(repository) { fun someFunction(): String { // do something return injectedContext.getString(R.string.some_string) } } and as for the unit test, I simply mock the Context object and pass it to the viewModel that I'm testing(i'm using mockk for the mocking library) @Test fun `test someFunction`() { val injectedContext = mockk() every { injectedContext.getString(R.string.someString) } returns "some string" val viewModel = SomeViewModel( SomeRepository(), injectedContext ) val result = viewModel.someFunction() assertEquals("some string", result) } it works just fine, but perhaps I'm missing some of the points that you're trying to say in your video about this & that this approach might be a bad practice after all. Please tell me what do you think about this, Thanks!
@amineayachi3352 жыл бұрын
OMG i was looking for this video long time ago i was using strings ids as int then call resources on that int in jetpack compose still didnt watch the video !!
@amineayachi3352 жыл бұрын
Similaire to my solution but with more options thank master great solution
@miladhashemzadeh56262 жыл бұрын
Good Solution
@John-qt6qk2 жыл бұрын
Thanks
@a.bastiao43122 жыл бұрын
I watched half of this video thanks to KZbin suggestion. But what I don't get is why a ViewModel cannot be unit tested if the app context is injected. Since context is an interface it can easily be mocked. Isn't that one of the benefits from dependency injection? Allow provide test doubles or use mocking libraries, right?
@PhilippLackner2 жыл бұрын
You should avoid mocks as much as possible, they can quickly make your test suite break when things change. I only use them for code from 3rd party libraries I don't own or if it's a legacy code base and I have to write some tests to refactor it
@a.bastiao43122 жыл бұрын
@@PhilippLackner you just read the mock part. Why not an interface that can be injected into the viewModel? A simple interface with an implementation that takes context as constructor argument and has a function to provide the strings the way you need. But to answer the mocks and the third parties, isn't that applicable to context?
@PhilippLackner2 жыл бұрын
@@a.bastiao4312 an interface to provide resources works, that's true. However you still need to think about how to pass both string resources and dynamic strings from the repo to the viewmodel
@a.bastiao43122 жыл бұрын
@@PhilippLackner that's why I watched only half of the video. I was never going to have issues to solve that problem. The data layer would provide an error message wrapped on result error and that was passed to tbe ui. The edit text validation isn't related. So to me isn't an issue. The issue are the KZbin recommendations
@Zihad2 жыл бұрын
That is very smart.
@MaisUmSomente2 жыл бұрын
Instead of creating a new function just to pass the context, I couldn't just put the context inside the Composable like this @Composable fun asString(): String { val context = LocalContext.current return when(this) { is DynamicString -> value is StringResource -> context.getString(resId, *args) } }
@Dynamitemedina2 жыл бұрын
Almost didn't see the point until I saw the get string composable.
@bboydarknesz2 жыл бұрын
Jesus chirst, finally this is what I am looking for!!!! But.... Is it really okay to pass the context? It's always memory leaks issue they say..
@PhilippLackner2 жыл бұрын
Passing context to a function is not an issue. It just gets problematic if you store it in a static field (companion object) of an object that has a different life time than the activity the context comes from. Either way, if you use the app context, leaks can't happen.
@bboydarknesz2 жыл бұрын
@@PhilippLackner well that makes sense. Thank you for answering my doubt years!
@jatinvashisht42932 жыл бұрын
Using channels and converting them to flows is a good practice?
@RackaApps2 жыл бұрын
Yes. They'll still behave like a Channel. That's why the asFlow() extension function exists which takes care of that
@jatinvashisht42932 жыл бұрын
@@RackaApps alright, thanks for clarifying
@udhdbdjxisskka2 жыл бұрын
Or if you don't use Compose, you can return from viewModel just the int resource id and do context.getString in the fragment or view.
@PhilippLackner2 жыл бұрын
This is not compose specific, returning the resource ID won't allow you to pass dynamic errors coming from the api
@deeplathia43042 жыл бұрын
How can we achieve the same using xml?
@bheemnegi32292 жыл бұрын
Use extension function to get String, fun error.asString(context: Context) { if(Dynamic){ return DynamicString } else { return context.getString(resId) } } And then set it to text
@ipvoodoo2 жыл бұрын
Avesome! 👍
@cloakrabbit7672 жыл бұрын
You can unit test viewmodel with context and no emulators with Robolectric. But ofc. without Robolectric its faster
@PhilippLackner2 жыл бұрын
Yep that's something you should avoid if possible
@cloakrabbit7672 жыл бұрын
@@PhilippLackner Sure. What I have been doing is creating class ResourceProvider that has fun getString(resId, vararg format) and itself has Context. Other places that inject it its then simpler to mock as it is a constructor parameter. Any downside to this? Thanks for the reply :-)
@PhilippLackner2 жыл бұрын
@@cloakrabbit767 no, good approach as well :)
@MrRahulmalik2 жыл бұрын
What if we are not using compose? Then how do we do that?
@PhilippLackner2 жыл бұрын
Nothing changes
@umuttekin49672 жыл бұрын
No it changes. You need to pass context if you don't have composable. That's mean you cannot access asString function in viewModel. So if you don't have compose in your project, this will not work as expected.
@PhilippLackner2 жыл бұрын
@@umuttekin4967 no, the only thing you need to do for xml is to remove the composable function and just use the other overload that takes the context instead. There's no need to call asString in the VM
@umuttekin49672 жыл бұрын
@@PhilippLackner But the purpose of this video is the using string resources in a VM. If i don't need to call asString in the VM what's the point? I want to access string resources in the VM but if i don't have compose in my project according to this video i can't access it.
@frozen10932 жыл бұрын
@@PhilippLackner could you pls explain it more detailed? I'm new to android dev and can't figure out how to do that. pls
@GeolseuDeiGamers2 жыл бұрын
Is this possible in XML?
@PhilippLackner2 жыл бұрын
yeah, nothing changes
@GeolseuDeiGamers2 жыл бұрын
@@PhilippLackner thank you for the gold content
@aliakram11452 жыл бұрын
Seems like you have flue, if yes get well soon bro.👍
@PhilippLackner2 жыл бұрын
Yeah, I already feel better, thanks! :)
@elelan2 жыл бұрын
👌👌👌
@mieszkokozma42392 жыл бұрын
I went a slightly different way. I'm injecting an instance of getStringUseCase, which internally uses ApplicationContext to resolve strings and is exposing a method "operator fun invoke(stringId: Int, vararg formatArgs: Any): String". It didn't fail me so far, and this way I can have a state with simple String values.
@skullkrum202 жыл бұрын
If you save your resolved strings in the ViewModel, it is wrong. Because if the user changes languages and goes back to your app, your views will not be automatically updated and the previous language will still be used. This is because the ViewModel will get reused and the Strings are already resolved. If you resolve them in the UI, since the fragment/activity is recreated, it will call the "getString" again and so update the language of the text.
@mieszkokozma42392 жыл бұрын
@@skullkrum20 That's correct, however in the application that I work with this doesn't really matter. I prefer the convenience of use over the "bulletproof" solution, which is maybe going to slightly improve the UX for 0.01% of the users once (how often are you changing the language on your phone, apart from the development testing purposes?). That's how I see it. Many strings come already translated from the server which only knows about the local language at the time of data being asked, so we would end up having mixed translations anyways.
@skullkrum202 жыл бұрын
@@mieszkokozma4239 For you it may not be a common use case, but there are a lot of families and contries that speak multiple languages and they may want to sometimes change them, I imagine having to restart the app is annoying in such cases. Also, Android is going to support language per apps, so this use case will become even more common. I do understand the point of mixed languages though, it feels like a bad solution that the server gives you text rather than text ids that then get translated into resource ids that can also be translated. Anyway, like you said, it won't be a big deal most of the times :)
@lglf772 жыл бұрын
Muito complicado seu vídeo, já inicia com milhões de código na tela, não é aprovável para iniciantes
@MrY216102 жыл бұрын
👍 👍 👍 👍
@GG9K71 Жыл бұрын
Make a static app context instance inside your Application, and use it.. 😂
@PhilippLackner Жыл бұрын
Then any classes that use it won't be locally unit testable anymore, since they use Android dependencies
@GG9K71 Жыл бұрын
@@PhilippLackner Yes I know, I was joking
@КонстантинСоловьев-в7п2 жыл бұрын
Son unos XX18LIKE.Uno de los mejoress conciertos Mañas no 10 se l 💯💞😍