I have seen quite a few examples in Kotlin where an activity class has a companion object to encapsulate the creation of a start intent like the following. It seems particularly Java inspired.
class HomeActivity : AppCompatActivity() {
companion object {
fun newStartIntent(context: Context): Intent {
val intent = Intent(context, HomeActivity::class.java)
return intent
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
this.setContentView(R.layout.activity_home)
// ...
}
// ...
}
Since Kotlin has top level functions, why not skip the companion object and just have a top level function?
fun newHomeActivityStartIntent(context: Context): Intent {
val intent = Intent(context, HomeActivity::class.java)
return intent
}
class HomeActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
this.setContentView(R.layout.activity_home)
// ...
}
// ...
}
There is nothing wrong in your approach actually. I thought of a few reasons why I would choose a companion object over top-level functions:
Top-level functions visible for everyone, thus every time you start typing new... you will get a list of partially irrelevant results;
Companion objects can hold private values that you would not like to make open to the public and keep them visible only within your class but still make them static. Maybe there are some arguments that are calculated under this function invocation and passed with intent, and you would like to hide these calculations or arguments keys;
This is not your case but still relevant: using companion object you can make all constructors private and control all arguments passed to object initialization. This is how Singleton can be created in Kotlin;
Opinionated For me personally it makes things look tidy. I usually extract only simple and relatively vastly used functions. Like Date conversion functions, or math function calculations.
It is a matter of style. Just pick one and be consistent!
Related
Please tell me is it allowed to put ActivityContext/ApplicationContext in a variable as shown in the code below?
class MainActivity : Activity() {
private val context = applicationContext
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
}
As you know in Android, the roots of the garbage collector are: static variables and active threads.
Case 1. We put applicationContext in the context variable.
private val context = applicationContext
Will there be memory leak? I don't think so, because applicationContext is a singleton in the process of running the application.
Case 2. We put activity or this in the context variable.
private val context = this
Will there be memory leak? My intuition tells me not to do this. Because Activity can be destroyed (for example, when the screen is rotated). If I'm right, please explain why there will be a memory leak, because no garbage collector root is used. Maybe I'm wrong and this behavior is allowed.
P.S. Will the situation be similar if we put variable context in companion object?
class MainActivity : Activity() {
companion object {
private val context = applicationContext // or this
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
}
Those are properties, not variables. It's important to understand the distinction, although not for this particular question.
Your very first code block is fine because the application context reference will be cleared when the Activity instance is released, and Activities have shorter lifetimes than the Application.
The code example Case 1 is perfectly fine in any situation (static reference or instance reference) for the reason you said. The Application context is a singleton that lives for the lifetime of your app so it cannot be leaked.
For case 2: if you use this in a companion object, this refers to the companion object, not the Activity instance. (There would be no way for the companion object to statically get an Activity instance on its own because there is no single Activity instance for it to find at any given moment.) However, if you do this, you will indeed leak the Activity instance:
class MainActivity : Activity() {
companion object {
private var context? Context? = null
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
context = this
}
}
// or:
private var context? Context? = null // top-level property
class MainActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
context = this
}
}
An Activity context should never be stored in a static property.
Anything referenced by an object property or top-level property (outside a class) cannot be garbage collected because there is a static reference to it. Not sure what you mean by no garbage collector root being used. object and top-level properties are static references, which you said yourself are roots for the garbage collector.
I am trying to convert some Java code to Kotlin. I have a "heavy" object that I cannot reason about how to initialize properly in the app. The object can take some time to create and I don't want to block except for when the functionality is actually required. I wrote some code that meets my requirements, but it doesn't seem like a good pattern and I was hoping someone tell me what the proper pattern here (will list what I don't like about it after the code):
package com.example.myapplication
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import kotlinx.coroutines.*
import javax.inject.Provider
import kotlinx.coroutines.channels.Channel
class MainActivity : AppCompatActivity() {
lateinit var heavyInitObject: Provider<HeavyInitObject>
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
heavyInitObject = initHeavyObject()
}
fun useTheHeavyInitObject() {
// Need it to block here, before work is done.
val hio = heavyInitObject.get()
}
}
fun initHeavyObject(): Provider<HeavyInitObject> {
val cnl = Channel<HeavyInitObject>(Channel.BUFFERED)
//purposely block to ensure to ensure it is initialized
val provider = Provider { runBlocking { cnl.receive().also { cnl.close() }}}
HeavyInitObject.get(object : HeavyInitObject.HeavyInitObjectListener{
override fun onReady(heavyInitObject: HeavyInitObject) = runBlocking {
cnl.send(heavyInitObject)
}
})
return provider
}
// Mocked library I am using (i.e. I don't have control over the implementation)
class HeavyInitObject {
companion object {
fun get(listener: HeavyInitObjectListener) {
val heavyInitObject = HeavyInitObject()
listener.onReady(heavyInitObject)
}
}
interface HeavyInitObjectListener {
fun onReady(heavyInitObject: HeavyInitObject)
}
}
What I don't like
Should be a val
It naturally really be a val, because the value should never change once initialized.
class MainActivity : AppCompatActivity() {
val heavyInitObject: Provider<HeavyInitObject> = initHeavyObject()
// OR...
val heavyInitObject: HeavyInitObject by lazy {
initHeavyObject().get()
}
The first option seems like it could do too much too fast. Depending on how someone would add MainActivity to the object graph it could really affect startup performance.
The second one is too slow. If we haven't requested the heavy object to be created before it is needed, there will be definite jank in the application when the heavy object is queried the first time.
Is there a good way to have the object be a val while requesting the object to be created in onCreate (understanding that I don't have control over implementation of the underlying library)?
Is channel the right data structure here?
Maybe this is bareable, but I wanted to see if there is a better option. A RENDEZVOUS channel makes more sense, but send suspends until receive is called and I don't want to block anything on thread initializing the object (i.e. since i can't convert the implementation to a suspend function). Switching to a bufferend channel won't block since I only send one element through, but that seems like a hack. What is the best data structure for this task?
Edit:
Thanks to some help in the comments I have improved the second condition (eliminate akward use of channel). I have a couple ideas for how to improve the first condition...
Code for getting rid of channel
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
class MainActivity : AppCompatActivity() {
lateinit var heavyInitObject: Provider<HeavyInitObject>
override suspend fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
heavyInitObject = lifecycleScope.async {deferredHeavyObject()}
}
fun useTheHeavyInitObject() {
// Need it to block here, before work is done.
val hio = heavyInitObject.await()
}
}
suspend fun initHeavyObject(): HeavyInitObject = suspendCancellableCoroutine { continuation ->
HeavyInitObject.get(object : HeavyInitObject.HeavyInitObjectListener {
override fun onReady(heavyInitObject: HeavyInitObject) {
continuation.resume(heavyInitObject)
}
})
}
Code to finalize heavyInitObject
class MainActivity : AppCompatActivity() {
val heavyInitObject by lazy { heavyInitObjectBackingField }
private lateinit var heavyInitObjectBackingField: Deferred<HeavyInitObject>
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
heavyInitObjectBackingField = lifecycleScope.async { deferredHeavyObject()}
}}
I basically get a lateinit val that way... and can be confident I won't get an error for it not being initialized. Ideally, it makes me realize this is overcomplicated, because I can't get under the hood and easily seperate the object initialization from the call back initialization.. Unless anyone else has a better idea?
Channel is kind of weird for returning a single item. I would load it in a Deferred privately and publicly expose a suspend getter that awaits the Deferred result. Once the object is ready, it won’t have to be waited for. And since it’s a suspend function, you can access it via a coroutine without unlocking your main thread.
object HeavyObjectCreator {
private val heavyObject: Deferred<HeavyObject> = GlobalScope.async {
// Long running actions to generate the object…
HeavyObject(params)
}
suspend fun getInstance(): HeavyObject =
heavyObject.await()
}
In your activity you can use lifecycleScope.launch to start a coroutine when you need to do a task that uses the object and it can call the above function to get it in a suspending way. If you want to preload the heavy object, you can put the statement HeavyObjectCreator in onCreate of your Application class or your Activity so the creator object will be instantiated and start the coroutine to load the heavy object.
This is just one example of a way to do it for an object that you’ll be reusing on multiple screens. If you intend to load a new heavy object only on screens that need it, I’d consider putting the contents of the class above directly in a ViewModel and use viewModelScope instead of GlobalScope.
I have a Kotlin class that is becoming very large (a couple of hundreds of lines). It's mainly because this class is the listener of several interfaces. Usually, I split my class functions with extensions (and place them in separate files). However, when I try that with override functions, Android Studio gives me this error:
"Modifier 'override' is not applicable to 'top level function'"
So, is there a workaround? How would you split a large file with many override functions? (In Swift, this is done using extensions or Partials in C#). Here is an image for reference in Android Studio and in Xcode. In Swift, we simply add "extension" and that allows us to write code as if we were writing right within the class:
Instead of having your Activity implement listener interfaces, make them into anonymous object members. I think this is usually cleaner anyway.
class MyActivity: AppCompatActivity() {
private lateinit var binding: MyActivityBinding
private val someButtonListener = OnClickListener {
binding.text = "Button clicked!"
}
override fun onCreate(savedInstanceState: Bundle) {
super.onCreate(savedInstanceState)
binding = MyActivityBinding.inflate(layoutInflater)
binding.button.onClickListener = someButtonListener
}
}
Then, you could break these out into another file by making functions that create them.
class MyActivity: AppCompatActivity() {
private lateinit var binding: MyActivityBinding
override fun onCreate(savedInstanceState: Bundle) {
super.onCreate(savedInstanceState)
binding = MyActivityBinding.inflate(layoutInflater)
binding.button.onClickListener = createSomeButtonListener(binding)
}
}
fun createSomeButtonListener(binding: MyActivityBinding) = OnClickListener {
binding.text = "Button clicked!"
}
If you need to call functions in the Activity from these listeners, you'll have to make your Activity a parameter of the function and expose those functions as public, unfortunately. It's not really proper encapsulation, but you typically don't reference Activities from other classes ever, so it's probably not a big deal for it to have some public functions.
This is my code inspired by the answer:
interface MyInterface {
fun itHappened()
}
class MyClass {
lateinit var listener: MyInterface
}
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val myObj = MyClass()
myObj.listener = anonymousListener()
myObj.listener.itHappened()
}
}
fun MainActivity.anonymousListener() = object : MyInterface {
override fun itHappened() {
Log.d("MyTag", "Clicked")
}
}
In my Android app, I pass custom data (UByteArray) from one activity to another using the parcelable interface.
I am using this data inside multiple fragments, so I rewrote the data class to extend androidx ViewModel and expose LiveData properties to the fragments. Now the UI updates are a lot nicer, but I think I am using it wrong because I overwrite all ViewModel values inside onCreate.
Now my question: What do I need to change to initialize the ViewModel only once?
The following is my current code (abbreviated and renamed for this question):
class ActivityB : AppCompatActivity() {
private val bData: ViewModelB by viewModels()
// ...
override fun onCreate(savedInstanceState: Bundle?) {
// ...
intent.getParcelableExtra<ViewModelB>("id")?.let {
Log.e(TAG, "Found parceled bData $it")
// This seems like a very stupid way to do it, is there a better one?
bData.copyAll(it)
}
}
}
I saw that it is possible to inject SavedState into the ViewModelB constructor, but I don't have a saved state until now, and the data needs to be passed only once.
Should I change the initialization of tagData with by viewModels() to = ViewModelB(intent)?
Or do I need to extend the ViewModelFactory somehow?
Any tip here would be really appreciated, thanks.
I saw that it is possible to inject SavedState into the ViewModelB constructor, but I don't have a saved state until now, and the data needs to be passed only once.
The official solution would be to provide a SavedStateHandle that is initialized with the defaultArgs as the intent.extras of your Activity.
For that, you need to provide an AbstractSavedStateViewModelFactory implementation, OR use SavedStateViewModelFactory (in which case you must define the right constructor in order to have it instantiated via reflection).
class ActivityB : AppCompatActivity() {
private val bData: ViewModelB by viewModels {
SavedStateViewModelFactory(application, this, intent.extras)
}
// ...
override fun onCreate(savedInstanceState: Bundle?) {
// ...
// intent.getParcelableExtra<ViewModelB>("id")?.let {
// Log.e(TAG, "Found parceled bData $it")
}
}
Then in your ViewModel
#Keep
class ViewModelB(val savedStateHandle: SavedStateHandle): ViewModel() {
val uByteData = savedStateHandle.get<UByteArray>("id")
}
Or so. The "id" key must match the same key as is in the intent extras.
Since you have a ViewModel which implements Parcelable, you can get your ViewModelB instance directly from the Intent extra.
The Intent which is used for starting ActivityB may not be != null at the time when ActivityB is instantiated, but you can use
lateinit var bData: ViewModelB
Then in onCreate()
bData = if(intent.hasExtra("id")) intent.getParcelableExtra<ViewModelB>("id") else ViewModelProvider(this).get(ViewModelB::class.java)
I've been looking for some time to android architechture components and lately to the Navigation component.
I'm trying to pass as a parameter an object, so the next fragment can retrieve that data, but to do so I'm required to do one of two things:
Pass it through a Bundle, which will make me implement the Parcelable interface to that object.
Use the "Safeargs" plugin which I've tried and it looks like behind the hood makes use of Bundles and requires the implementation of the Parcelable interface anyway.
The thing about these options is that I've read that Parcelable makes use of reflection and it can get quite expensive regarding time
I have also tried to build a "SharedMasterDetailsViewModel" but with no luck since the observable callbacks are not being performed on my newly created Fragment. (I think LiveData performs the callback before my fragment is created)
Here's some code about how I've tried to approach this
SharedSessionViewModel
class SessionSharedViewModel : ViewModel() {
var sharedSession: LiveData<Session> = MutableLiveData()
private set
fun setSession(data: Session) {
val casted = this.sharedSession as MutableLiveData<Session>
casted.postValue(data)
}
}
MasterFragment
override fun onItemClicked(item: Session) {
sessionSharedViewModel.setSession(item) // Item is a complex object of mine
this#HomeFragment.findNavController().navigate(R.id.sessionDetailsFragment)
}
DetailsFragment
class SessionDetailsFragment : Fragment() {
companion object {
fun newInstance() = SessionDetailsFragment()
}
private lateinit var sharedViewModel: SessionSharedViewModel
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.session_details_fragment, container, false)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
Log.d("SESSIONDETAILS","created!")
super.onActivityCreated(savedInstanceState)
sharedViewModel = ViewModelProviders.of(this).get(SessionSharedViewModel::class.java)
sharedViewModel.sharedSession.observe({this.lifecycle},{ handleUI(it!!)})
}
fun handleUI(sharedSession: Session) {
Toast.makeText(activity, "This is inside new activity: ${sharedSession.title()}", Toast.LENGTH_SHORT)
}
}
My last hope is to serialize my object into a JSON string and reparse that object on the onCreateActivity lyfecycle hook of my Detail fragment but I feel like that is not the proper solution.
In the worst case scenerio I would just pass the id of the object and re-fetch it from the network, but since I already have the info I want to show I'd like to pass it as a parameter.
TL; DR
You can't.
Actual explanation
First thing; the following statement
The thing about these options is that I've read that Parcelable makes use of reflection and it can get quite expensive regarding time.
IS A LIE
Since you implement Parcelable you're just providing methods on how to serialize and deserialize using basic primitive types: IE: writeBytes, readBytes, readInt, writeInt.
Parcelable does NOT use reflection. Serializable does!
While it's true you are forced to use Parcelable jet brains developed a very useful annotation that takes away the pain of having to write the parcelable implementation called #Parcelize.
Sample usage:
#Parcelize
data class User(val username: String, val password: String) : Parcelable
And now you're able to pass instances of the class without writing a single line of Parcelable implementation.
More info here