Android Kotling Extension for Binding a Generic View - android

I have a generic custom view like
class MyGenericCustomView<T>(context: Context, attrs: AttributeSet) : AnotherView(context, attrs) {
...
}
In the Activity/Fragment XML I have:
<package.name.MyGenericCustomView
android:id="#+id/custom_id"
....
/>
If I use the old way, I can get my "typed" custom view using something like:
override fun onCreate(...) {
...
val myCustomView = findViewById<MyGenericCustomView<String>>(R.id.custom_id)
...
}
But if I use the Android Kotlin Extension (synthetic) to have an object named with the same ID, I dont have a way to pass the Generic type, so
//custom_id is of type MyGenericCustomView<*>
One solution is to create a specific class like
class MySpecificCustomView(context: Context, attrs: AttributeSet) : MyGenericCustomView<String>(context, attrs) {
....
}
But I dont want to create this boilerplate class.
Is there any solution to specify a custom type using Kotlin Extensions only?
Thanks

Since you cannot specify the type parameter in the XML and type parameters are also erased in the byte code, you could simply cast the value to the proper generic type MyGenericCustomView<String>.
So something like that should work:
val myView = custom_id as MyGenericCustomView<String>
For better usage in your Activity/Fragment I would personally use lazy { } like this:
class MyActivity() : Activity(…) {
val myView by lazy { custom_id as MyGenericCustomView<String> }
...
}

Related

How to make Custom Components mandatory in Android?

We are working with 5 people on a project.
I have custom TextView component in Android project.
Some of my team friends are using Android Textview (or AppCompatTextView) directly. I want to make it mandatory to use the text view that I created as a custom TextView.
How do I do this? I look forward to your help, thank you.
While coding guidelines and code reviews should catch those issues. You could also create a custom lint check and force your builds to fail on lint errors.
Something like this:
class TextViewDetector : ResourceXmlDetector() {
override fun getApplicableElements(): Collection<String>? {
return listOf(
"android.widget.TextView", "androidx.appcompat.widget.AppCompatTextView"
)
}
override fun visitElement(context: XmlContext, element: Element) {
context.report(
ISSUE, element, context.getLocation(element),
"Do not use TextView"
)
}
companion object {
val ISSUE: Issue = Issue.create(
"id",
"Do not use TextView",
"Use custom view",
CORRECTNESS, 6, Severity.ERROR,
Implementation(TextViewDetector::class.java, RESOURCE_FILE_SCOPE)
)
}
}
There is a guide, an example repository from google and an extensive api guide on how to write custom lint checks.
You can create your own ViewInflater
class MyViewInflater {
fun createView(
parent: View?, name: String?, context: Context,
attrs: AttributeSet, inheritContext: Boolean,
readAndroidTheme: Boolean, readAppTheme: Boolean, wrapContext: Boolean
): View {
// ...
val view: View = when (name) {
"TextView",
"androidx.appcompat.widget.AppCompatTextView",
"com.google.android.material.textview.MaterialTextView" -> createMyTextView(context, attrs)
//other views
}
//...
return view
}
fun createMyTextView(context: Context, attrs: AttributeSet) = MyTextView(context, attrs)
}
and install it in your app theme
<style name="Theme.MyAppTheme" parent="Theme.SomeAppCompatParentTheme">
<item name="viewInflaterClass">package.MyViewInflater</item>
</style>
It will return your View for all tags you specify
See AppCompatViewInflater
There's no technical way to do this. The answer is coding guidelines and code reviews.

Adding actions to a already defined onClickListener

Is there a way to do something like this:
view.setOnClickListener { Log.e("","Hello") }
view.setOnClickListener { Log.e("","World") }
While letting both Log to show.
I have buttons on custom views with some universal actions to execute on click which I initialize them in constructor, now I want to setup some additional behaviors based on situations outside of class. And I notice doing things like above will simply overwrite the onClickListener.
What I can think of is to maybe store Array of Unit in the object and let view execute elements of it as on clicked. Then use accessor to append function to the array.
Is there a more simple way to keep existing actions and append new actions at the end?
with this code you can add click listener as much as you want :
class CustomView #JvmOverloads constructor(
context: Context?,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
private val clickCallBacks = mutableListOf<((view: View) -> Unit)>()
fun addClickListener(listener: ((view: View) -> Unit)) {
clickCallBacks.add(listener)
}
override fun setOnClickListener(l: OnClickListener?) {
super.setOnClickListener {
//TODO do what you want to do
l?.onClick(it)
for (listener in clickCallBacks) {
listener.invoke(it)
}
}
}
}
in your activity or fragment:
customView.view.addClickListener{ Log.e("","Hello") }
customView.view.addClickListener{ Log.e("","World") }
Why not use a flag and choose the behaviour depending on that flag like this.
Using a listener is a recipe for disaster because as it is a listener it is continuously listening ie the code inside will execute whenever that button is pressed and if there are two blocks one will take precedence over the other.
button.setOnClickListener{
when(flag){
"hello" -> Log.e("TAG","Hello")
"world" -> Log.e("TAG","World")
}
}

ViewTreeViewModelStoreOwner.get(view) always returns null

I have a compound view that I want to create its viewmodel by ViewModelLazy, I need to send the ViewModelStoreOwner of the view to ViewModelLazy but trying to get the ViewModelStoreOwner using ViewTreeViewModelStoreOwner.get(this) always returns null. The compound view itself is a simple view, but I am using it in a recyclerview adapter that resides in a fragment. Right now, I am getting forced to use the parent fragment ViewModelStoreOwner, which is causing all the items in the adapter to have the same viewmodel instance. I searched for an example on how to use ViewTreeViewModelStoreOwner but I can't find one, am I missing something?
Note: I am injecting the viewmodel by dagger-hilt
This example shows how it can be implemented in custom view.
class SummaryView(context: Context, attrs: AttributeSet?) : ConstraintLayout(context, attrs) {
private val viewModel by lazy {
ViewModelProvider(findViewTreeViewModelStoreOwner()!!).get<SummaryViewModel>()
}
override fun onAttachedToWindow() {
super.onAttachedToWindow()
viewModel.summaryModel.observe(findViewTreeLifecycleOwner()!!, ::populateSummaryView)
}
private fun populateSummaryView(summaryModel: SummaryModel) {
// do stuff
}
}
Found this great example here
1, you need update
androidx.activity to 1.2.0
androidx.fragment to 1.3.0
2, activity or fragment set its ViewModelStore to rootViw via setTag, so any view in activity or fragment's rootView gets the same ViewModelStoreOwner and also same ViewModelStore.
// ComponentActivity line 409
ViewTreeViewModelStoreOwner.set(getWindow().getDecorView(), this)
// ViewTreeViewModelStoreOwner line 50
view.setTag(R.id.view_tree_view_model_store_owner, viewModelStoreOwner);

Kotlin easily access resources outside of an activity

I'm new at Android and Kotlin. I'm trying to create a ResourcesHelper class to easily access my custom colors and fonts from any other custom class in my app. But in this helper I don't have any context. I've read ways to get the context extending the Application class but then compiler says I can't access this context in my ResourcesHelper companion object as it would create memory leaks. Also I ended up with optional chained.
Here is how I would like to be able to use it :
class ResourcesHelper {
companion object {
val lightBlue = resources.getColor(R.color.lightBlue)
val customBlue = resources.getColor(R.color.customBlue)
// [...]
val fontAwesome = resources.getFont(R.font.fontawesome)
val lemonMilk = resources.getFont(R.font.lemonmilk)
}
}
enum class ButtonStyle {
MENU,
// [...]
VICTORY
}
class CustomButton(c: Context, attrs: AttributeSet) : Button(c, attrs) {
var isButtonActivated = false
fun setStyle(style: ButtonStyle) {
setBackgroundColor(ResourcesHelper.transparent)
when(style) {
ButtonStyle.MENU -> {
setText(R.string.menu_button)
typeface = ResourcesHelper.lemonMilk
setBackgroundColor(ResourcesHelper.customRed)
setTextColor(ResourcesHelper.white)
}
// [...]
ButtonStyle.VICTORY -> {
setText(R.string.victory_button)
typeface = ResourcesHelper.lemonMilk
setBackgroundColor(ResourcesHelper.customRed)
setTextColor(ResourcesHelper.white)
}
}
}
}
I also read this post Android access to resources outside of activity but it's in Java and I have no idea on how to do it in Kotlin.
I am completely lost on what and how to do this... Or if there is a better way to achieve reaching resources from anywhere.
Thanks for your help
For Color, String, etc system resources you can use the Resources class, as shown below
import android.content.res.Resources
class ResourcesHelper {
companion object {
val lightBlue = Resources.getSystem().getColor(R.color.lightBlue)
}
}
If you want to support different Android versions I'll recommend to use ContextCompat. It provides unified interface to access different resources and backward compatibility for older Android versions.
For AnroidX use androidx.core.content.ContextCompat, for SupportV4: android.support.v4.content.ContextCompat.
val lightBlue = ContextCompat.getColor(context, R.color.lightBlue)
val customBlue = ContextCompat.getColor(context, R.color.customBlue)

How to extend ImageView in an Android-Scala app?

I have tried many solutions found in google by the keywords: multiple constructors, scala, inheritance, subclasses.
None seems to work for this occasion. ImageView has three constructors:
ImageView(context)
ImageView(context,attribute set)
ImageView(context,attribute set, style)
In scala you can only extend one of them. And the solution of using the more complete constructor (ImageView(context,attribute set, style)) and passing default values does not work either because the constructor ImageView(context) does something completely different than the other two constructors.
Some solutions of using a trait or a companion object does not seem to work because the CustomView must be a class! I mean I am not the only one who uses this class (so I could write the scala code any way I wanted) there is also the android-sdk who uses this class and yes it must be a class.
target is to have a CustomView which extends ImageView and all of these work:
new CustomView(context)
new CustomView(context,attribute set)
new CustomView(context,attribute set, style)
Please let me know if you need any further clarification on this tricky matter!
According to Martin Odersky (the creator of Scala), that is not possible.
In http://scala-programming-language.1934581.n4.nabble.com/scala-calling-different-super-constructors-td1994456.html:
"is there a way to call different super-constructors within different
class-constructors - or does all have to go up to the main-constructor and only
one super-constructor is supported?
No, it has to go through the main constructor. That's one detail where
Scala is more restrictive than Java."
I think your best approach is to implement your views in Java.
It sounds like you may just be better off writing the subclass in Java. Otherwise, if your assumption about the SDK using the three argument constructor is correct, then you could use a trait and a class with a companion object. The SDK would use the three argument constructor of CustomView which also implements a trait containing any additional behavior you need:
trait TCustomView {
// additional behavior here
}
final class CustomView(context: Context, attributes: AttributeSet, style: Int)
extends ImageView(context, attributes, style) with TCustomView
In the application code, you could use the one, two or three argument version in this way:
object CustomView {
def apply(c: Context, a: AttributeSet, s: Int) =
new ImageView(c, a, s) with TCustomView
def apply(context: Context, attributes: AttributeSet) =
new ImageView(context, attributes) with TCustomView
def apply(context: Context) = new ImageView(context) with TCustomView
}
CustomView(context)
CustomView(context, attributes)
CustomView(context, attributes, style)
Seems like a lot of work. Depending on your goals, you might be able to add additional behavior with implicits:
implicit def imageViewToCustomView(view: ImageView) = new {
def foo = ...
}
This question lead me to several design considerations which I would like to share with you.
My first consideration is that if a Java class has been correctly designed the availability of multiple constructors should be a sign of the fact that some class properties might have default value.
Scala provides default values as a language feature, so that you do not have to bother about providing multiple constructors at all, you can simply provide default value for some arguments of your constructor. This approach leads to a much cleaner API than three different constructors which produce this kind of behaviour as you are making clear why you do not need to specify all the parameters.
My second consideration is that this is not always applicable in case you are extending classes of a third-party library. However:
if you are extending a class of an open source library and the class is correctly designed, you can simply investigate the default values of the constructor argument
if you are extending a class which is not correctly designed (i.e. where overloaded constructors are not an API to default some parameters) or where you have no access to the source, you can replace inheritance with composition and provide an implicit conversion.
class CustomView(c: Context, a: Option[AttributeSet]=None, s: Option[Int]=None){
private val underlyingView:ImageView = if(a.isDefined)
if (s.isDefined)
new ImageView(c,a.get,s.get)
else
new ImageView(c,a.get)
else
new ImageView(c)
}
object CustomView {
implicit def asImageView(customView:CustomView):ImageView = customView.underlyingView
}
According to the Android View documentation View(Context) is used when constructed from code and View(Context, AttributeSet) and View(Context, AttributeSet, int) (and since API level 21 View(Context, AttributeSet, int, int)) are used when the View is inflated from XML.
The XML constructor all just call the same constructor, the one with the most arguments which is the only one with any real implementation, so we can use default arguments in Scala. The "code constructor" on the other hand may have another implementation, so it is better to actually call in from Scala as well.
The following implementation may be a solution:
private trait MyViewTrait extends View {
// implementation
}
class MyView(context: Context, attrs: AttributeSet, defStyle: Int = 0)
extends View(context, attrs, defStyle) with MyViewTrait {}
object MyView {
def apply(context: Context) = new View(context) with MyViewTrait
}
The "code constructor" may then be used like:
var myView = MyView(context)
(not a real constructor).
And the other once like:
var myView2 = new MyView(context, attrs)
var myView3 = new MyView(context, attrs, defStyle)
which is the way the SDK expects them.
Analogously for API level 21 and higher the class can be defined as:
class MyView(context: Context, attrs: AttributeSet, defStyle: Int = 0, defStyleRes: Int = 0)
extends View(context, attrs, defStyle, defStyleRes) with MyViewTrait {}
and the forth constructor can be used like:
var myView4 = new MyView(context, attrs, defStyle, defStyleRes)
Update:
It gets a bit more complicated if you try to call a protected method in View, like setMeasuredDimension(int, int) from the trait. Java protected methods cannot be called from traits. A workaround is to implement an accessor in the class and object implementations:
private trait MyViewTrait extends View {
protected def setMeasuredDimensionAccessor(w: Int, h: Int): Unit
def callingSetMeasuredDimensionAccessor(): Unit = {
setMeasuredDimensionAccessor(1, 2)
}
}
class MyView(context: Context, attrs: AttributeSet, defStyle: Int = 0)
extends View(context, attrs, defStyle) with MyViewTrait {
override protected def setMeasuredDimensionAccessor(w: Int, h: Int) =
setMeasuredDimension(w, h)
}
object MyView {
def apply(context: Context) = new View(context) with MyViewTrait {
override protected def setMeasuredDimensionAccessor(w: Int, h: Int) =
setMeasuredDimension(w, h)
}
}

Categories

Resources