Little addition: Depending on your situation you may want to use SharingStarted.Lazily instead of WhileSubscribed, especially if you want to keep the value of the flow when the app goes in the background
Thank you so much for featuring my article on your video and your shoutout! Great and clear explanation. I really like your approach of verifying solutions through unit tests :)
You can mitigate the downside of approach #2 with a simple and lazy (pun intended) workaround - in unit tests, just lazy-init the ViewModel with `by lazy` instead of putting it in a @Before function. That way, the loading on init would not trigger until the unit test body references the ViewModel. You can still use @Before to set up mocks and test doubles, just don't touch the ViewModel.
I follow the first approach because it works well for test cases. To avoid the usual issues, I make sure that data is loaded only once by using a flag inside base ViewModel and call the load data function inside the scope. This prevents the function from being called multiple times during recomposition.
I solved this issue by a quite different approach, I created a data wrapper class called Resource, and Resource has a function called fetch(FetchType (could be Fetch, Refresh Or Invalidate), a lamda that fetches the data (calling to the usecase) Then based on the type of fetching it knows if should I call the lambda or not, ex: if I loaded the data already and called Fetch, then it won't call the lambda, If I loaded the data and called Refresh, it will try to fetch new data and update the exist one, but if it failed it will keep the exist one And the Invalidate case will delete the exist data and try to fetch new one even if the data is loaded, because it's invalid anymore This is very helpful and clear in code.
Both 2 and 3 seems decent. In #1 the problem is also mitigated a lot of time by having data cached locally (which is common). So if for example most recently stored load is younger then X hours you use local data (unless user explicitly refreshes or something). This helps with config changes as only local database is accessed.
Imagine next scenario: Screen A with a list, which is loaded on entering the screen, screen B is details screen. If you spend more than 5 seconds on a screen B, when you go back, screen A will load data again. Third approach is the worst one.
Well, in that case, I think we can either increase the threshold (if you believe the data needs to be reloaded after some time) or simply change the strategy to SharingStarted.Lazily. As mentioned, we have full control over the behavior based on our needs
I agree. The same problem also arises when the app goes into the background while not leaving the Screen A with the initial data. One solution would be to use SharingStarted.Eagerly or Lazily. To my best knowledge, it will always keep the data in the hot flow. The first one will do it at init time of the flow so not the best solution (similar to solution 2 > init block). Therefore the Lazily approach seems to be the best: it will trigger the flow only once and only when the first subscriber comes along.
@@Shakenbeer Well, in that case, I think we can either increase the threshold (if you believe the data needs to be reloaded after some time) or simply change the strategy to SharingStarted.Lazily. As mentioned, we have full control over the behavior based on our needs.
You are right, there is no perfect way to load initial data, everyone should choose the most appropriate method according to the actual situation. For me, the second way is good and sufficient.
Just to share, there is a way to test with the second approach, (when using the ViewModel init block to load data), all you need to do is delay the ViewModel initialisation, in other words, do not initiate the ViewModel in your test class setup function, rather do it in each of your actual tests. in my view this is the most naturally way and less boiler plate in code, although the launched effect with a flag should work but I think that is a bit unnatural compared to using the given ViewModel init block for free. (sadly option 3 using 5 second delay should be avoided others explained why already :) ,
Advice: if you adopt this way of loading data, wrap all shareIn calls in extension method, otherwise you may miss some parameters next time. Also relying on Flow for your business logic is tricky. Flows are very hard to understand, I would even say impossible
What can I say? This approach is quite better than others (For these cases where you have an initial loading screen), I disagree with something about the main Android documentation (mentioned in the Medium blog), it doesn't mention that the best approach is the init block code, actually, I have been reading up in the architecture labs and they explicitly say that is bad practice to fetch the data in the init block specially by side effects and unit test behaviour (as you mentioned in the video).
Hi Philipp, just wanted to thank you for all your hard work ... I am using your videos to help me learn Android Compose/Kotlin development, very informative and enjoyable videos ... Thanks Philipp ... (Mark Suckerberg 😂)
Philipp there is another disadvantage of using init block, if you use states to manage your composable, and you have a lazyColoum which uses the data from states. you will see a lag on opening of the screen. Given that your api return data within 1 to 2 sec. Even using loading state won't remove that split second lag.
LaunchedEffect is the most flexible and clean approach. Problems occur only when your presentation layer is not suitable for that. Consider UDF approaches like MVI, MVVM+UDF, ELM, TEA, Redux... State of your screen will let you know if an operation should be executed. If you use MVVM+UDF then check state in a guard statement before loadData(). If you use MVI then it is reduer's responsibility to check if data should be loaded. Even in pure MVVM you can use isLoading LiveData in a guard statement at the beginning of loadData()
"Comparing ready-made solutions for implementing the MVI architecture on Android" by Abuzar on medium - Introduction to MVI and UDF in general (plus a list of popular MVI libraries)
"David González - Unidirectional Data Flow with..." on youtube - Nice talk about UDF implementation "Михаил Левченко - Итак, вы выбрали UDF-архитектуру. Как моделировать стейт?" on youtube - Great talk about UDF and state design, If you speak russian
"Algebraic Data Types (ADT) in Scala" by Daniel Ciocîrlan - To design good screen state I also recommend to learn about algebraic data types. The article is for Scala but works with Kotlin sealed classes as well Also consider drawing state machines for your screen to design screen state well
Hey @Philipp You are doing great work for so many developers. Please do let me know if you have created a video or a blog for a sample app includes MVVM+KOIN+KTOR Basically all kotlin only stuffs. if yes please share that or if not can you please make a video or a blog on that will be so helpful.
Very interesting approach that works for MVVM as far as I can understand! How would that work for MVI where the whole screen state is held in only one observable?! (there is no separate isLoading property in the ViewModel)
@GakisStylianos What he mentioned are the well-known and existing approaches. So yeah, what he mentioned is the best existing way for those who write tests. One can create their own approach. While such things take time to design and implement, sometimes your use case forces you to. And Phillipp already mentioned a use case where this approach will not suit you. It is if you do not want the screen to reload when user goes away from the app and comes back. Some API calls are expensive. I know you can do database caching to solve this. But now we are complicating it. If we go as simple as caching in a variable in the ViewModel itself, then we force caching in tests that might not be desired.
Niceee video!! love it! one question which should be the initialValue if for example the state is a list of Person? And this works for a Compose Multiplatform project? i've been initialazie data in the init block
Thanks for the video!! One question please: If I have a sole UI state variable in my view model and want to keep it like that, how do I use the flow in the view model? I do not want to provide a 2nd variable from the view model to the jetpack compose screen.
good video, but i still rather the ViewModel init approach, well it is normal to pass the repositories as parameters in the ViewModel constructor, just doing this the tests troubles are solved, well you just mock the repositories methods and attributes and when create the ViewModel in the test you pass the mocked repository
thank you so much philipp i appleid this but i fall with one problem seeks for it's solution , see i had been using launcheffect in my app and then i swithch to init , i face a probelm : when i use navigations viewmodel also get killed causing every time realoading of initial data , i can resloved it by just hosting the viewmodel to teh navigation but it is a little big app doing so for evry screen is good ??
How will I load data I have to take some arguments received in fragment, like a lookupUri, and I need to fetch data in the viewmodel using the lookupUri?
Doesn't this means that if the app goes in the background for more than x seconds the state will be lost ? This approach may cover a configuration change due to rotation but fails other scenarios where an init block does not.
I will stateIn the list in viewmodel, and map the flow with a sealed wrapper then emit loading status when onstart. Do not maintain a read-write flow like this in viewmodel.
You don't necessarily want to set the state to loading if you've already had data emitted before and you don't want to refresh that. You may want to just show the old data as-is without flickering a "loading" state in-between. Especially if fetching the data again is almost instant by grabbing it from a cache or a database for example
I do not like the WhileSubscribed approach. Do you also refresh your data after user staying on the screen for more than 5s, I do not think so, unnecessary complexity introduced. I would stick with Lazily. Also, do you need that @Before annotation for the purpose you mentioned? @Test annotaction doc says: The Test annotation tells JUnit that the public void method to which it is attached can be run as a test case. To run the method, JUnit first constructs a fresh instance of the class then invokes the annotated method. I think it is meant for external resources.
i don't know about others, but when i leave an app and come back, i would like to see it just like i left it caching could fix that, but i don't think it's worth the complexity so i will be sticking to the init block for now
You can get this with approach #3 as well if you store the last emitted value and at the time when you get a new observer you check if the last emitted value was data already. If yes you can just not do anything in your flow block, and just let the StateFlow show the last emitted value as-is. All this while still getting the benefits of that approach that init and LaunchedEffect do not give you.
Init block is not the best way, it will make more difficult the unit tests, and you can not control things, the loadData() is called the same time you create the ViewModel, if something is happening/changing at the same time the loadData() is called you can not control it, makes the unit test difficult
Can anyone tell me how to store the data of edit text inside a recycler view with flows as if i connect the edit text directly to the state flow then , app will be in a feedback loop and i wont be able to update the edit text
hi phillipp, what do you think about putting this in compose: var isFirstInitialized by rememberSaveable { mutableStateOf(false) } if (isFirstInitialized.not()) { viewModel.loadData() isFirstInitialized = true } it will only be called one time, it wont be called when configuration changed.
Nah thanks but no. I code my own state machine, this technique predates compose and is very effective. Initial state = "Created'. When the first onStart event arrives, my state machine switches from "Created" to "Started" state and executes a transition function. The subsequent OnStart events do not trigger the previous transition function but another function called refresh. "Started" state remains the same state, no state transition.
If the parameter comes from user interaction, then you don’t need to initiate the data when the viewmodel is created. If the parameter comes from a previous screen as navigation arguments, then use SavedStateHandle in the viewmodel
Little addition: Depending on your situation you may want to use SharingStarted.Lazily instead of WhileSubscribed, especially if you want to keep the value of the flow when the app goes in the background
Thank you so much for featuring my article on your video and your shoutout!
Great and clear explanation. I really like your approach of verifying solutions through unit tests :)
Really awesome info. Thanks a lot @skydoves and @PhilippLackner
You can mitigate the downside of approach #2 with a simple and lazy (pun intended) workaround - in unit tests, just lazy-init the ViewModel with `by lazy` instead of putting it in a @Before function. That way, the loading on init would not trigger until the unit test body references the ViewModel. You can still use @Before to set up mocks and test doubles, just don't touch the ViewModel.
Dude... this is so simple.. I am mind blown. Wtf XD
All these steps got pros and cons. The wisest idea is to get the best solution for your own problem.
I follow the first approach because it works well for test cases. To avoid the usual issues, I make sure that data is loaded only once by using a flag inside base ViewModel and call the load data function inside the scope. This prevents the function from being called multiple times during recomposition.
That's Also a way to do it 🙌
I solved this issue by a quite different approach, I created a data wrapper class called Resource, and Resource has a function called fetch(FetchType (could be Fetch, Refresh Or Invalidate), a lamda that fetches the data (calling to the usecase)
Then based on the type of fetching it knows if should I call the lambda or not, ex: if I loaded the data already and called Fetch, then it won't call the lambda, If I loaded the data and called Refresh, it will try to fetch new data and update the exist one, but if it failed it will keep the exist one
And the Invalidate case will delete the exist data and try to fetch new one even if the data is loaded, because it's invalid anymore
This is very helpful and clear in code.
Can you please share your code somewhere on discord or wherever you say.
Both 2 and 3 seems decent. In #1 the problem is also mitigated a lot of time by having data cached locally (which is common). So if for example most recently stored load is younger then X hours you use local data (unless user explicitly refreshes or something). This helps with config changes as only local database is accessed.
Agreed, either we can utilize the SavedStateHandle
Imagine next scenario: Screen A with a list, which is loaded on entering the screen, screen B is details screen. If you spend more than 5 seconds on a screen B, when you go back, screen A will load data again. Third approach is the worst one.
Well, in that case, I think we can either increase the threshold (if you believe the data needs to be reloaded after some time) or simply change the strategy to SharingStarted.Lazily. As mentioned, we have full control over the behavior based on our needs
I agree. The same problem also arises when the app goes into the background while not leaving the Screen A with the initial data.
One solution would be to use SharingStarted.Eagerly or Lazily. To my best knowledge, it will always keep the data in the hot flow. The first one will do it at init time of the flow so not the best solution (similar to solution 2 > init block). Therefore the Lazily approach seems to be the best: it will trigger the flow only once and only when the first subscriber comes along.
I've already tested what you're saying here. And you're correct!!
@@Shakenbeer Well, in that case, I think we can either increase the threshold (if you believe the data needs to be reloaded after some time) or simply change the strategy to SharingStarted.Lazily. As mentioned, we have full control over the behavior based on our needs.
You are right, there is no perfect way to load initial data, everyone should choose the most appropriate method according to the actual situation. For me, the second way is good and sufficient.
Just to share, there is a way to test with the second approach, (when using the ViewModel init block to load data), all you need to do is delay the ViewModel initialisation, in other words, do not initiate the ViewModel in your test class setup function, rather do it in each of your actual tests. in my view this is the most naturally way and less boiler plate in code, although the launched effect with a flag should work but I think that is a bit unnatural compared to using the given ViewModel init block for free. (sadly option 3 using 5 second delay should be avoided others explained why already :) ,
Advice: if you adopt this way of loading data, wrap all shareIn calls in extension method, otherwise you may miss some parameters next time. Also relying on Flow for your business logic is tricky. Flows are very hard to understand, I would even say impossible
Nice sharing. Personally, I'd just go with the init block approach. The third one just looks hard to understand.
This approach was discussed years ago in android developers youtube channel in very first video related to flow, stateflow and shared flow...
What can I say? This approach is quite better than others (For these cases where you have an initial loading screen), I disagree with something about the main Android documentation (mentioned in the Medium blog), it doesn't mention that the best approach is the init block code, actually, I have been reading up in the architecture labs and they explicitly say that is bad practice to fetch the data in the init block specially by side effects and unit test behaviour (as you mentioned in the video).
Hi Philipp, just wanted to thank you for all your hard work ... I am using your videos to help me learn Android Compose/Kotlin development, very informative and enjoyable videos ... Thanks Philipp ... (Mark Suckerberg 😂)
Philipp there is another disadvantage of using init block, if you use states to manage your composable, and you have a lazyColoum which uses the data from states. you will see a lag on opening of the screen. Given that your api return data within 1 to 2 sec. Even using loading state won't remove that split second lag.
There isn't any lag if you initialize the UI state with the loading state from the start
Good to see something what I use for a while
LaunchedEffect is the most flexible and clean approach. Problems occur only when your presentation layer is not suitable for that. Consider UDF approaches like MVI, MVVM+UDF, ELM, TEA, Redux... State of your screen will let you know if an operation should be executed.
If you use MVVM+UDF then check state in a guard statement before loadData().
If you use MVI then it is reduer's responsibility to check if data should be loaded.
Even in pure MVVM you can use isLoading LiveData in a guard statement at the beginning of loadData()
Well, even using pure MVVM you can use isLoading LiveData as a guard statement and LaunchedEffect is good for you
Do you have any article or repo i can refer to related to UDF approaches?
"Comparing ready-made solutions for implementing the MVI architecture on Android" by Abuzar on medium - Introduction to MVI and UDF in general (plus a list of popular MVI libraries)
"David González - Unidirectional Data Flow with..." on youtube - Nice talk about UDF implementation
"Михаил Левченко - Итак, вы выбрали UDF-архитектуру. Как моделировать стейт?" on youtube - Great talk about UDF and state design, If you speak russian
"Algebraic Data Types (ADT) in Scala" by Daniel Ciocîrlan - To design good screen state I also recommend to learn about algebraic data types. The article is for Scala but works with Kotlin sealed classes as well
Also consider drawing state machines for your screen to design screen state well
Hey @Philipp You are doing great work for so many developers.
Please do let me know if you have created a video or a blog for a sample app includes MVVM+KOIN+KTOR Basically all kotlin only stuffs. if yes please share that or if not can you please make a video or a blog on that will be so helpful.
Well explained. Thanks man.
Very interesting approach that works for MVVM as far as I can understand! How would that work for MVI where the whole screen state is held in only one observable?! (there is no separate isLoading property in the ViewModel)
then you do the same for the state flow
What do you think about this approach?
private val uiState = MutableStateFlow(false)
val _uiState by lazy {
loadData()
uiState
}
Your StateFlow is going mutable to usage point (your composable) so this kills the core idea of backing field
@@ilyastoletov You could just expose it as StateFlow, `val _uiState: StateFlow by lazy...`
@@ilyastoletov you should do it
val _uiState: StateFlow ...
loadData() will be triggered when the property is accessed, not when flow collection is triggered, which may be not what you want.
Thanks Philipp and Article owner
Thanks!
This is what I wanted it. Thank you
Good method, but I would not say the only correct way. Different use cases require different solutions.
Well, he clearly showed why you don't want to do the other approaches. What use case do you have where the final approach doesn't suit your needs?
@GakisStylianos What he mentioned are the well-known and existing approaches. So yeah, what he mentioned is the best existing way for those who write tests.
One can create their own approach. While such things take time to design and implement, sometimes your use case forces you to.
And Phillipp already mentioned a use case where this approach will not suit you. It is if you do not want the screen to reload when user goes away from the app and comes back. Some API calls are expensive.
I know you can do database caching to solve this. But now we are complicating it. If we go as simple as caching in a variable in the ViewModel itself, then we force caching in tests that might not be desired.
Niceee video!! love it! one question which should be the initialValue if for example the state is a list of Person? And this works for a Compose Multiplatform project? i've been initialazie data in the init block
Awesome content!
thanks a lot it was really helpful
Thanks for the video!! One question please: If I have a sole UI state variable in my view model and want to keep it like that, how do I use the flow in the view model? I do not want to provide a 2nd variable from the view model to the jetpack compose screen.
interesting.. thanks
Perfect timing I was just reading this article by Antonio Leiva 🔥
Thanks for the clear explanation 💪
Thank you so much
Google, take notes how one complicated topic is supposed to be explained.
good video, but i still rather the ViewModel init approach, well it is normal to pass the repositories as parameters in the ViewModel constructor, just doing this the tests troubles are solved, well you just mock the repositories methods and attributes and when create the ViewModel in the test you pass the mocked repository
Your test troubles aren't solved just by passing a repo. The mocked repo will still load data when the VM is initialized
thank you so much philipp i appleid this but i fall with one problem seeks for it's solution , see i had been using launcheffect in my app and then i swithch to init , i face a probelm : when i use navigations viewmodel also get killed causing every time realoading of initial data , i can resloved it by just hosting the viewmodel to teh navigation but it is a little big app doing so for evry screen is good ??
Philipp please make a video that explains the implemtation of sockJs with jetpack compose, it's urgent please
Hi Phillipp, what is the font you are using?
how to trigger loading data if we pass arguments throught the navigation and need, for example, id to load data?
Launched effect
@@tvhome2334 You can also use the fact that viewModels have "keys". YOu can use this key to pass simple data like an ID or a name
via SavedStateHandle that we can retrieve in ViewModel constructor
@@sergeykharuk5614What if we use navigation savedStateHandle (the one that is not cached into the vm’s SavedState)?
really makes you think that every answer has a different opinion
Many thanks!!!
Wonderful 👍👍❤
Hey what if I want to pass an argument on loadData(pageId:String), as pageId is a argument from previous page
How will I load data I have to take some arguments received in fragment, like a lookupUri, and I need to fetch data in the viewmodel using the lookupUri?
Sir can you make a video on firebase storage integration on kmp desktop application
Doesn't this means that if the app goes in the background for more than x seconds the state will be lost ? This approach may cover a configuration change due to rotation but fails other scenarios where an init block does not.
I will stateIn the list in viewmodel, and map the flow with a sealed wrapper then emit loading status when onstart. Do not maintain a read-write flow like this in viewmodel.
You don't necessarily want to set the state to loading if you've already had data emitted before and you don't want to refresh that. You may want to just show the old data as-is without flickering a "loading" state in-between. Especially if fetching the data again is almost instant by grabbing it from a cache or a database for example
Can you make the video telling about Data Source
4:02 this disadvantage can be fixed by setting proper key parameters
@@FyUajYpUlM39 no matter what key you choose, it will be called again after a config change
I do not like the WhileSubscribed approach. Do you also refresh your data after user staying on the screen for more than 5s, I do not think so, unnecessary complexity introduced. I would stick with Lazily. Also, do you need that @Before annotation for the purpose you mentioned? @Test annotaction doc says: The Test annotation tells JUnit that the public void method to which it is attached can be run as a test case. To run the method, JUnit first constructs a fresh instance of the class then invokes the annotated method. I think it is meant for external resources.
You need to add check whether data is already present in ui state object before calling loadData in onStart().
Guessing Phillip meant @BeforeClass rather, still don't see the point yeah
i don't know about others, but when i leave an app and come back, i would like to see it just like i left it caching could fix that, but i don't think it's worth the complexity so i will be sticking to the init block for now
You can get this with approach #3 as well if you store the last emitted value and at the time when you get a new observer you check if the last emitted value was data already. If yes you can just not do anything in your flow block, and just let the StateFlow show the last emitted value as-is.
All this while still getting the benefits of that approach that init and LaunchedEffect do not give you.
Oh yes. Philipp really knows what we need as Android developers. You are a hero in our digital age!
The deeper I try to get into Android development, the more complicated it seems to get. Not a pleasant experience!
I just have one doubt what if our use case methods is a suspend function. In cases it's not suspend it works but how to tackle suspension from onstart
Init block is not the best way, it will make more difficult the unit tests, and you can not control things,
the loadData() is called the same time you create the ViewModel, if something is happening/changing at the same time the loadData() is called you can not control it,
makes the unit test difficult
Perfect
but what if the load data requires an argument?
Can anyone tell me how to store the data of edit text inside a recycler view with flows as if i connect the edit text directly to the state flow then , app will be in a feedback loop and i wont be able to update the edit text
@philipp is king
Overall, such a 5-second delay may cause performance issues during the time.
Why?
hi phillipp, what do you think about putting this in compose:
var isFirstInitialized by rememberSaveable {
mutableStateOf(false)
}
if (isFirstInitialized.not()) {
viewModel.loadData()
isFirstInitialized = true
}
it will only be called one time, it wont be called when configuration changed.
What if you don't need to collect the result. Like an analytic call. If nothing collects the result, the initiation will never happen.
It's enough that there's anything that's collected, doesn't have to be related to the result
@@PhilippLackner yeah but response of the analytic request usually doesn't need to be collected. it doesn't even need to produce a flow.
How if our inisial data required context to received it ? (I am not using DI)
the simplest solution, you can create a singleton to hold an application context reference and consume it
Another approach would be to use AndroidViewModel that gives the application instance
Nah thanks but no. I code my own state machine, this technique predates compose and is very effective.
Initial state = "Created'. When the first onStart event arrives, my state machine switches from "Created" to "Started" state and executes a transition function.
The subsequent OnStart events do not trigger the previous transition function but another function called refresh. "Started" state remains the same state, no state transition.
How to call the loadData method with passing parameter?
If the parameter comes from user interaction, then you don’t need to initiate the data when the viewmodel is created. If the parameter comes from a previous screen as navigation arguments, then use SavedStateHandle in the viewmodel
this is why i love android, every approach is WRONG
:like
Thisssssssss
In my view, your code should be readable and straightforward. The Google's approach doesn't meet my requirements, I wouldn't recommend it