I have a question regarding objects in kotlin, to which I couldn't find a satisfying answer so far.
It's basically the following scenario: I've got some user data, that I want to make available through the entire app, without having to pass it through activities.
To achieve this, I created an object with two properties that are instantiated with the user's data in the startup activity. Is this a safe way to store and make the user's data available for all activities or will it get lost on the way?
Example:
object CurrentUserManager {
lateinit var userId: String,
lateinit var userName: String
}
LoginActivity {
...
onCreate(...){
val user = ApiCall.Login();
CurrentUserManager.userId = user.id
CurrentUserManager.userName = user.name
}
}
MainActivity {
...
onCreate(...){
Toast.makeText(this, "Hello ${CurrentUserManager.userName} with ID: ${CurrentUserManager.userId}", Toast.LENGTH_SHORT).show()
}
}
Is this unsafe/bad practice and if so why and which pattern should I use to achieve the expected outcome?
Thanks,
lionthefox
Short answer: it's safe as long as your Android process does not end.
Long answer: this boils down to the discussion about lifetime of singletons and static variables in Java. There have already been some answers to this very question, so I won't re-iterate those here:
Android static object lifecycle
Is it safe to use static class variables in an android application
Lifetime of a static variable in Android
use intent put extras or maintain a static Object
Related
I need a variable accessed and updated by two or more services or activities. I am thinking of using interface. A class that implements that interface to be initialized in Base Application class or via hilt. I am not entirely sure how to do that.
Is this possible and if so, is this good idea?
Can I use any other way to pass variable from service to service or activity to service?
Possible psuedoCode for idea.
interface foo(){
variable a
fun setA(var : p)
}
class JustAClass: foo{
variable a;
fun setA(var : p){
a = p
}
}
BaseApp(): Application(){
JustAClass jac = JustAClass();
}
activityA(){
BaseApp.jac.setA(99)
}
serviceB(){
variable c = BaseApp.jac.a
}
Yes, you could do it the way you describe, but there are a few easier ways...
You can use a Singleton. In Kotlin, any Object is automatically a statically accessible Singleton. So, something like this:
object SharedData{
var item1 = "test"
var item2 = false
}
can be accessed anywhere in your app by using SharedData.item1 or SharedData.item2. You can also make those variables into LiveData or Kotlin Flow if you want to post updates and observe them between activities/services/etc.
You can also store your data in SharedPreferences. It'll provide more permanent storage that will survive app restarts, but it reads and writes data to internal storage, so you need to deal with IO overhead.
You can also store your data in other permament data structures, like a Room Database, but I think that would be overkill for just communicating between processes.
I have a BaseViewModel that basically has the function to get the user data like so:
abstract class BaseViewModel(
private val repository: BaseRepository
) : ViewModel() {
private var _userResponse: MutableLiveData<Resource<UserResponse>> = MutableLiveData()
val userResponse: LiveData<Resource<UserResponse>> get() = _userResponse
fun getUserData() = viewModelScope.launch {
_userResponse.value = Resource.Loading
_userResponse.value = repository.getLoggedInUserData()
}
}
In my Fragment, I access this data by just calling viewModel.getUserData(). This works. However, I'd like to now be able to edit the data. For example, the data class of UserResponse looks like this:
data class UserResponse(
var id: Int,
var username: String,
var email: String
)
In other fragments, I'd like to edit username and email for example. How do I do access the UserResponse object and edit it? Is this a good way of doing things? The getUserData should be accessed everywhere and that is why I'm including it in the abstract BaseViewModel. Whenever the UserResponse is null, I do the following check:
if (viewModel.userResponse.value == null) {
viewModel.getUserData()
}
If you want to be able to edit the data in userResponse, really what you're talking about is changing the value it holds, right? The best way to do that is through the ViewModel itself:
abstract class BaseViewModel(
private val repository: BaseRepository
) : ViewModel() {
private var _userResponse: MutableLiveData<Resource<UserResponse>> = MutableLiveData()
val userResponse: LiveData<Resource<UserResponse>> get() = _userResponse
fun setUserResponse(response: UserResponse) {
_userResponse.value = response
}
...
}
This has a few advantages - first, the view model is responsible for holding and managing the data, and provides an interface for reading, observing, and updating it. Rather than having lots of places where the data is manipulated, those places just call this one function instead. That makes it a lot easier to change things later, if you need to - the code that calls the function might not need to change at all!
This also means that you can expand the update logic more easily, since it's all centralised in the VM. Need to write the new value to a SavedStateHandle, so it's not lost if the app goes to the background? Just throw that in the update function. Maybe persist it to a database? Throw that in. None of the callers need to know what's happening in there
The other advantage is you're actually setting a new value on the LiveData, which means your update behaviour is consistent and predictable. If the user response changes (either a whole new one, or a change to the current one) then everything observeing that LiveData sees the update, and can decide what to do with it. It's less brittle than this idea that one change to the current response is "new" and another change is "an update" and observers will only care about one of those and don't need to be notified of the other. Consistency in how changes are handled will avoid bugs being introduced later, and just make it easier to reason about what's going on
There's nothing stopping you from updating the properties of the object held in userResponse, just like there's nothing stopping you from holding a List in a LiveData, and adding elements to that list. Everything with a reference to that object will see the new data, but only if they look at it. The point of LiveData and the observer pattern is to push updates to observers, so they can react to changes (like, say, updating text displayed in a UI). If you change one of the vars in that data class, how are you going to make sure everything that needs to see those changes definitely sees them? How can you ensure that will always happen, as the app gets developed, possibly by other people? The observer pattern is about simplifying that logic - update happens, observers are notified, the end
If you are going to do things this way, then I'd still recommend putting an update function in your VM, and let that update the vars. You get the same benefits - centralising the logic, enabling things like persistence if it ever becomes necessary, etc. It could be as simple as
fun setUserResponse(response: UserResponse) {
_userResponse.value?.run {
id = response.id
username = response.username
email = response.email
}
}
and if you do decide to go with the full observer pattern for all changes later, everything is already calling the function the right way, no need for changes there. Or you could just make separate updateEmail(email: String) etc functions, whatever you want to do. But putting all that logic in the VM is a good idea, it's kinda what it's there for
Oh and you access that object through userResponse.value if you want to poke at it - but like I said, better to do that inside a function in the VM, keep that implementation detail, null-safety etc in one place, so callers don't need to mess with it
The ideal way to update userResponse you should change/edit _userResponse so that your userResponse we'll give you the updated data.
it should be something like this
_userResponse.value = Resource<UserResponse>()
I used a lot of static data in Activities, Adapters, Application, etc with like
companion object{
const val SEND_MY_DATA = "sendta"
const val SEND_MY_DATA_1 = "sendta1"
const val SEND_MY_DATA_2 = "sendta2"
}
to have common name for intent extras to match the same name between two activities. So, this static data are used in the activity & in another activity, and even some adapters.
And also I used this in Application class like
// this is used somewhere.
fun updateContext(){
appContext = applicationContext
}
companion object{
var appContext: Context? = null
fun myFunction(context: Context){
// use context param here.
}
}
Is this a bad approach or not? Is there any better way to improve this?
Just a note about saving data in the Application class, I encountered this by dealing with local resources in Room.
Generally, as in the case of the linked question, it can be a good solution (Android Studio shows a memory leak warning).
According to your needs you have to be careful to save data in this way, because Android OS can actually kill processes, including your Application instance.
To determine which processes should be killed when low on memory, Android places each process into an "importance hierarchy" based on the components running in them and the state of those components.
Check Processes and Application Lifecycle for more information.
A complete discussion about this can be found on this post.
If you're going to make a static application Context reference, I think this is cleaner:
companion object {
lateinit var context: Context
private set
}
override fun onCreate() {
super.onCreate()
context = applicationContext
}
But if you use dependency injection, you shouldn't need it. The singleton Context pattern makes unit testing difficult.
As for storing your constants, companion objects are fine. They do result in an extra class that's compiled, but that should be trivial since you shouldn't have very many activities.
I am using Companion object in service to expose my LiveData to a fragment. Is this okay to use or will it cause me problems like memory leaks?
In my service:
companion object {
val timeLeftInSeconds = MutableLiveData<Long>(0)}
In my fragment:
LockoutService.timeLeftInSeconds.observe(viewLifecycleOwner, Observer {...})
No it's fine because companion object is kinda like static fields, but I highly recommend to use a repository instead because it will increase you code readability and makes it more robust. Something like
object AppRepository{
val timeLeftInSeconds = MutableLiveData<Long>(0)}
}
And in fragment
AppRepository.timeLeftInSeconds.observe(viewLifecycleOwner
No it's totally alright because companion objects are like static properties in java and are not bound to the class you define them in.
Also you can put it in the same file, outside of your service
LockoutService.kt
val timeLeftInSeconds = MutableLiveData<Long>(0)}
class LockoutService {...}
And access it without mentioning the service name
I am making an app with some friends, and we have decided to go with the MVVM pattern. However, their understanding of the pattern differs from mine.
My question is: If we have a data that we would like to reuse in other views, can we store them as properties in a repository (seeing as the repository pattern is a singleton) and access them from other viewmodels?
Here's a generic example of what I mean:
object AnimalRepository {
val favoriteBreed : Breed? = null
}
and we would access it like so:
class DogViewModel(
application: Application
) : AndroidViewModel(application){
val animalRepository = AnimalRepository
fun setFavoriteBreed(favBreed: Breed) {
animalRepository.favoriteBreed = favBreed
}
fun getFavoriteBreed() : Breed {
return animalRepository.favoriteBreed
}
In this case, i did not make use of LiveData for simplicity purposes.
The debate arose from our different interpretations of this section of Android's guide to app architecture:
https://developer.android.com/jetpack/docs/guide#truth
This is how personally I would use a Repository for and also it is how the repository can be used. Repository is the place where we have data coming from. So that makes it easy for any view or the activity to access the data from directly the Repository and can be used from any ViewModel. Does this answer your question or you need more details?