I am working on an android project. I want to make an abstract subclass of FrameLayout with an abstract method
#LayoutRes
abstract fun getLayoutToInflate(): Int
In the constructor I want to inflate the layout returned by this method. But the IDE shows a warning about "Calling non-final function in constructor..." at this code
val inflater = LayoutInflater.from(context)
inflatedBanner = inflater.inflate(getLayoutToInflate(), this, true)
This app doesn't build yet. So wrote a simple kotlin code like this to test.
abstract class Base {
val text: String
constructor(text: String) {
this.text = text
println(text + getTextSuffix())
}
abstract fun getTextSuffix(): String
}
class Derived(text: String) : Base(text) {
override fun getTextSuffix() = "_"
}
fun main(args: Array<String>) {
val d = Derived("stuff")
}
This code always prints "stuff_" which means that the overridden abstract method is available in constructor.
Can I rely on this behaviour in my app too? If not, what is the correct way to implement something like this in kotlin?
Kotlin here is no different from Java or most other OOP languages.
As long as you make it clear in the method's contract that the overriding
methods must not access any state in the subclass, you can safely call them from the base class's constructor. If a class breaks this rule, its method will be accessing uninitialized state.
Related
I would like to understand whether variables inside a constructor should be private or public in Kotlin.
What is the significance of having access to modifiers inside the class constructor?
In the below code snippet, the variable service and query are private.
What is the use of keeping them private?
How does it help?
class GithubPagingSource(
private val service: GithubService,
private val query: String
) : PagingSource<Int, Repo>() {
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Repo> {
TODO("Not yet implemented")
}
}
Note: I have read multiple questions and answers related to this area on Stack overflow but could not find any valid answer.
The thing to consider is that definition of constructors in kotlin is different than java. In your provided snippet, the class has a primary constructor. According to the kotlin docs:
The primary constructor is part of the class header: it goes after the class name (and optional type parameters).
For example:
class GithubPagingSource(
service: GithubService,
query: String
)
also:
Note that parameters of the primary constructor can be used in the initializer blocks. They can also be used in property initializers declared in the class body.
So, what should we do if we want to use them inside the body of a function?
In this case, we have to declare class properties by adding var or val to the parameters of the primary constructor:
class GithubPagingSource(
val service: GithubService,
val query: String
) : PagingSource<Int, Repo>() {
init {
println("$query") // query is accessible here
}
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Repo> {
println("$query") // query also is accessible here
}
}
Now, service and query are playing two roles:
first as constructor parameters
second as class variables
On the other hand, the encapsulation principle tells us to keep class variables as much restricted as possible. It means that you should keep them private unless there is a need to be visible from outside.
class GithubPagingSource(
private val service: GithubService,
private val query: String
) : PagingSource<Int, Repo>() {
...
}
So, if we have a reference to an instance of this class:
val githubPagingSource = ...
val query = githubPagingSource.query // is not accessible here
This is my MWE test class, which depends on AndroidX, JUnit 4 and MockK 1.9:
class ViewModelOnClearedTest {
#Test
fun `MyViewModel#onCleared calls Object#function`() = mockkObject(Object) {
MyViewModel::class.members
.single { it.name == "onCleared" }
.apply { isAccessible = true }
.call(MyViewModel())
verify { Object.function() }
}
}
class MyViewModel : ViewModel() {
override fun onCleared() = Object.function()
}
object Object {
fun function() {}
}
Note: the method is protected in superclass ViewModel.
I want to verify that MyViewModel#onCleared calls Object#function. The above code accomplished this through reflection. My question is: can I somehow run or mock the Android system so that the onCleared method is called, so that I don't need reflection?
From the onCleared JavaDoc:
This method will be called when this ViewModel is no longer used and will be destroyed.
So, in other words, how do I create this situation so that I know onCleared is called and I can verify its behaviour?
In kotlin you can override the protected visibility using public and then call it from a test.
class MyViewModel: ViewModel() {
public override fun onCleared() {
///...
}
}
I've just created this extension to ViewModel:
/**
* Will create new [ViewModelStore], add view model into it using [ViewModelProvider]
* and then call [ViewModelStore.clear], that will cause [ViewModel.onCleared] to be called
*/
fun ViewModel.callOnCleared() {
val viewModelStore = ViewModelStore()
val viewModelProvider = ViewModelProvider(viewModelStore, object : ViewModelProvider.Factory {
#Suppress("UNCHECKED_CAST")
override fun <T : ViewModel?> create(modelClass: Class<T>): T = this#callOnCleared as T
})
viewModelProvider.get(this#callOnCleared::class.java)
//Run 2
viewModelStore.clear()//To call clear() in ViewModel
}
TL;DR
In this answer, Robolectric is used to have the Android framework invoke onCleared on your ViewModel. This way of testing is slower than using reflection (like in the question) and depends on both Robolectric and the Android framework. That trade-off is up to you.
Looking at Android's source...
...you can see that ViewModel#onCleared is only called in ViewModelStore (for your own ViewModels). This is a storage class for view models and is owned by ViewModelStoreOwner classes, e.g. FragmentActivity. So, when does ViewModelStore invoke onCleared on your ViewModel?
It has to store your ViewModel, then the store has to be cleared (which you cannot do yourself).
Your view model is stored by the ViewModelProvider when you get your ViewModel using ViewModelProviders.of(FragmentActivity activity).get(Class<T> modelClass), where T is your view model class. It stores it in the ViewModelStore of the FragmentActivity.
The store is clear for example when your fragment activity is destroyed. It's a bunch of chained calls that go all over the place, but basically it is:
Have a FragmentActivity.
Get its ViewModelProvider using ViewModelProviders#of.
Get your ViewModel using ViewModelProvider#get.
Destroy your activity.
Now, onCleared should be invoked on your view model. Let's test it using Robolectric 4, JUnit 4, MockK 1.9:
Add #RunWith(RobolectricTestRunner::class) to your test class.
Create an activity controller using Robolectric.buildActivity(FragmentActivity::class.java)
Initialise the activity using setup on the controller, this allows it to be destroyed.
Get the activity with the controller's get method.
Get your view model with the steps described above.
Destroy the activity using destroy on the controller.
Verify the behaviour of onCleared.
Full example class...
...based on the question's example:
#RunWith(RobolectricTestRunner::class)
class ViewModelOnClearedTest {
#Test
fun `MyViewModel#onCleared calls Object#function`() = mockkObject(Object) {
val controller = Robolectric.buildActivity(FragmentActivity::class.java).setup()
ViewModelProviders.of(controller.get()).get(MyViewModel::class.java)
controller.destroy()
verify { Object.function() }
}
}
class MyViewModel : ViewModel() {
override fun onCleared() = Object.function()
}
object Object {
fun function() {}
}
For Java, if you create your test class in the same package (within the test directory) as of the ViewModel class (here, MyViewModel), then you can call onCleared method from the test class; since protected methods are also package private.
Can I write:
#IdRes
abstract fun getHeaderId(): Int
With a val instead of a fun in kotlin? It complains I need a backing field or delegate when i write:
#IdRes <-- errors
abstract val headerId: Int
Which is the most idiomatic in this case? One-liner with a fun or mess around with a backing field (I'm not used to backing fields, maybe it's change-resistance, i have never really used them so i think they are unpleasant)
Since abstract val or var is just a function without a backing field it cannot be annotated by IdRes annotation but there is a workaround. You can use it like this:
#get:IdRes
abstract val headerId: Int
EDIT:
Why does this works? We need to closer inspect IdRes annotation and its source code:
#Documented
#Retention(CLASS)
#Target({METHOD, PARAMETER, FIELD, LOCAL_VARIABLE})
public #interface IdRes {
}
As we can see this annotation can be used on methods, parameters, fields and local variables. When we use abstract val it's neither of those since it is abstract and we cannot have abstract fields in Java. Normally equivalent of abstract val something: Int in Java is:
private int something
public int getSomething() {
return something;
}
From example, it's easy to see that the private field is what is called backing field of a property and you can't have those as abstract so that was the problem.
As mentioned in #AtulGupta comment, #theKarlo 's answer does not force the subclass to pass in an IdRes.
Therefore, an alternative to
#IdRes
abstract fun getHeaderId(): Int
and
#get:IdRes
abstract val headerId: Int
Is to pass the value into the constructor of the class itself, so that the backing field issue can be avoided and the subclass is forced to pass in an IdRes.
For example:
abstract class SomeClass(#IdRes val idRes: Int)
I have a few cases where I want to add static functions or values in a base class so that I can use them in all subclasses that inherits from it.
One such case is when i want to create generic tags for each class to use in data mapping as a key, like when i want to find fragments or pass data between activities.
For example:
open class BaseClass(){
companionObject{
val TAG: String = this.javaClass.simpleName
}
}
class ChildClass: BaseClass()
class Main: Activity(){
fun startActivity(){
val intent = Intent(this, ChildClass::class.java)
intent.putExtra(ChildClass.TAG, data)
startActivity(intent)
finish()
}
}
Can this be done or am I forced to create an companion object for each class?
I don't know a solution with companions. But you could use a global reified inline function for the specific use case, you mentioned in your question:
open class BaseClass()
class ChildClass: BaseClass()
inline fun <reified T> tagOf() = T::class.java.simpleName
fun main(args: Array<String>) {
println(tagOf<BaseClass>())
println(tagOf<ChildClass>())
}
Hm... I think, you can't do it. As mentioned in this article: https://proandroiddev.com/a-true-companion-exploring-kotlins-companion-objects-dbd864c0f7f5
companion object is really a public static final class in your BaseClass. So, I think, you can't do this.
I am trying to add a "static" method to my MyApplication class in kotlin
I have added (as a property) the variable :
private var context: Context? = null
in method:
override fun onCreate()
I added:
context = applicationContext
then I add a companion object like this
companion object {
#JvmStatic fun getMyApplicationContext(): Context?
{
return MyApplication().context
}
}
when I call this method from other parts of the application like
MyApplication.getMyApplicationContext() it always returns null. I have gleaned all this from several sources but I am not sure if it is anywhere near correct or not.
It sounds like you want a global application context object. Now casting aside my dislike for global variables, I think you are pretty close.
I think you just need to add the variable into the MyApplication classes companion object and use that directly. You only need the #JvmField annotation if you're going to access the field from Java.
class MyApplication {
companion object {
#JvmField
var context: Context? = null
// Not really needed since we can access the variable directly.
#JvmStatic fun getMyApplicationContext(): Context? {
return context
}
}
override fun onCreate() {
...
MyApplication.context = appContext
}
}