Office Hours Transcript: 2021-11-04

john joined

hello, john!

 

how can I help you today?

Hello Mark

 

I need to better understand how viewmodels work

 

First thing, what is meant by "you can’t pass arguments to viewmodels". I tried this and it works fine, what is wrong?

 

class MyViewModel(a:A,b:B): ViewModel() {
do stuff
}
======
@Composable
fun MyComposable(viewModel:MyViewModel){
val x = viewModel.getX()
}
===============
class MyClass{
MyComposable(MyViewModel(a,b))
}

you will find that your viewmodel does not work across configuration changes, which is the primary reason in Android for having a viewmodel

Do you mean the data will be reset when there’s a rotation, for example?

correct

I see. Thank you.

for viewmodels to be retained across the configuration change, they have to be managed by the Jetpack ViewModel system

 

typically, that means that the Jetpack creates your viewmodel instances for you

 

if you need to have constructor parameters, there are recipes for doing that, using a ViewModelProvider.Factory or a dependency inversion framework (Dagger/Hilt, Koin, etc.)

 

but you cannot just create an instance of MyViewModel yourself as you are in that code

By "you can’t", do you mean "your code won’t work when there is a configuration change"?

Alright

if you are not worrying about configuration changes, then probably you do not need to use the Jetpack ViewModel system at all

Second: Is it true that it is bad to pass a context to a view model. If it is, what should you do when, for example, you need to use a receiver that is triggered with ACTION_TIME_TICK (it has to be registered within the class, it can’t be in a separate one).

Is it true that it is bad to pass a context to a view model

Using the Application singleton is fine. You can do that via AndroidViewModel or your dependency inversion framework (if you use one).

 

you need to use a receiver that is triggered with ACTION_TIME_TICK (it has to be registered within the class, it can’t be in a separate one)

I would be somewhat surprised if a viewmodel were the right place for that, but the Application singleton would work. You would unregister the receiver in onCleared() of the ViewModel.

I need to update a value every minute, and display that value on the screen. Where else would you suggest I put the receiver?

I would not use ACTION_TIME_TICK at all for that, unless you have some specific reason to do so. I would use ScheduledExecutorService or some other in-process timing solution.

What’s wrong with ACTION_TIME_TICK?

  • you have no control over it
  • it requires platform APIs, such as `Context,
  • it is difficult to mock in testing as a result of the preceding bullet
 

you are welcome to use it if you want – but, if I were on your development team, I would want to see arguments for using ACTION_TIME_TICK, rather that considering it to be the default choice

 

or, to put it another way, I hear about ACTION_TIME_TICK approximately once every two years, so I do not think that it is particularly popular as a mechanism

Why is it bad that it requires platform APIs? Only because of testing? (sorry for asking annoying questions…)

testing is a large portion of it, particularly unit testing

 

you also get into the question that sent you down this path: where do you get the Context from?

 

and are you using a good Context that avoids memory leaks in the particular use case?

 

I suspect it may even be less code to use ScheduledExecutorService, though neither are especially complex

Very fair point, I never understood why you needed a context to use those kinds of receivers.

it stems from some API design choices made ~15 years ago

 

a lot of modern Android app development is working around the design choices that did not hold up well to "the test of time"

I see!

 

You can do that via AndroidViewModel or your dependency inversion framework (if you use one).

do you mean use AndroidViewModel OR ViewModel + DI?

yes; you do not need AndroidViewModel if you are using the popular DI frameworks

I see! Thank you for making this clear.

 

And I guess it is the same when I want to use a Room database? I can use ViewModel + DI instead of AndroidViewModel?

your viewmodel would not necessarily need a Context to use Room

 

ideally, your Room database is managed by some sort of singleton, and that needs a Context

 

if you are not using DI, you may wind up initializing that singleton from your Application object – if you want to lazy-initialize Room, then perhaps your viewmodel would need a Context

 

but, seriously, use DI – while Hilt makes Dagger a bit easier, Koin is really quite simple and works fine for smaller projects

I need to learn more about that. If I were to use AndroidViewModel on the other hand, I need to pass an application. I see people pass "this" from their activities, but isn’t that a Context?

If I were to use AndroidViewModel on the other hand, I need to pass an application

that is handled by the Jetpack ViewModel system for you, when it instantiates your viewmodel

 

if you are implementing your own ViewModelProvider.Factory and therefore need to instantiate the viewmodel yourself in the factory, then you should pass in an Application to avoid the memory leak

 

besides, AndroidViewModel takes an Application, not just a general Context

Yes, so where do I get the Application from? (when I look at code samples, they use "this", so I’m confused)

call getApplication() on some other Context, such as an Activity

Like "this.getApplication()"?

you would not need the this. part, but yes

 

BTW, my apologies: Context has getApplicationContext()

I see. Thank you.

 

About DI, I’m a Python so I never used it before. After researching it, I was under the impression that for simple projects you didn’t really need. Isn’t it simpler to do it "by hand"? Do you recommend to always use no matter what?

Isn’t it simpler to do it "by hand"?

That depends entirely on what "it" is! 😁

 

for an Android app project that is more than just some simple activity, I would use Koin

 

for an Android app project with 2+ team members, I would use Dagger/Hilt

 

that is due to some Android-specific considerations, like Context and configuration changes and stuff

 

so, for example, a have not used Koin for various command-line Kotlin utilities that I have written

for an Android app project with 2+ team members, I would use Dagger/Hilt

Interesting comment. Would you mind explaining why the number of team members matter?

there are two major types of dependency inversion: dependency injection and "the service locator pattern"

 

to keep the story simple, dependency injection does most of its work at compile time via code generation

 

while service locators are purely runtime solutions

 

the downside of a purely runtime solution, in strongly-typed languages like Java and Kotlin, is that you lose type safety

 

with dependency injection, you maintain type safety, at the cost (at least in the Java/Kotlin world) of greater setup complexity

 

with one developer, and a project sized for one developer, the loss of type safety for "injected" values is not a big deal – that developer probably will not make mistakes

 

once you start getting into bigger projects and having to involve multiple people, it becomes more likely that you will encounter bugs that type safety would have caught at compile time, but because you are using a service locator, you don’t find out until runtime and you crash

 

personally, my dividing line for when the overhead of Dagger/Hilt is worthwhile would be a project with 2+ maintainers, but that’s just me – there are projects out there that use Koin and other service locators that have multiple developers

 

but, for my own solo stuff, Koin suffices and is very simple to set up

Thank you for educating me on this. Very interesting.

 

One last question.

if you are not worrying about configuration changes

When people talk configuration changes, the only example I’ve ever heard is "rotation". But I guess you can forbid the app to rotate. What other examples might there be?

there are lots and lots of configuration changes:

 
  • changing locale/language
  • toggling dark mode
  • connecting/disconnecting a keyboard
  • swapping SIMs (for devices with externally-available SIM slots)
  • on Android 12, changing the wallpaper
  • etc.
 

I see. So was

if you are not worrying about configuration changes

kind of semi-sarcastic comment? (which is totally fine 😄)

you can opt out of most of the configuration changes’ default behavior of destroying and recreating your activities and fragments – the manifest attribute from my previous link covers that

 

and, in Jetpack Compose development, that approach has been gaining momentum

 

outright ignoring configuration changes – neither opting out of them nor using ViewModel correctly – is bad

 

however, a lot of apps that do not support both portrait and landscape ignore configuration changes

 

so, for example, Android 12’s change-the-wallpaper configuration change is one that you cannot opt out of (https://commonsware.com/blog/2021/10/31/android-12-wallpaper-changes-recreate-activities.html), and that causes problems for a bunch of apps

 

the whole wallpaper thing happens to have come to everyone’s attention (including mine) this week

Interesting! I shall look into that.

 

BTW, what’s the reason we don’t just only use "remember" in Jetpack Compose?
Is it mostly to "separate business logic"?

remember() only works inside a composable

 

if you have state that you are updating from outside of a composable, such as from Room, you will need some other mechanism than literally remember()

Theoritically you can do almost anything inside the composable, can’t you? The only time I had an issue was when I tried to select a file (registerForActivityResult)

that starts to get more into "separation of concerns"

True. Thank you so much for time Mark.

you’re welcome!

john left