How to create a View extension function in Kotlin - android

I'm trying to create an extension function in Kotlin. I did try several tutorials, but didn't quite understand, how to implement this one.
I'm trying to create a setWidth() function as such
//Find my_view in the fragment
val myView = v.findViewById<RelativeLayout>(R.id.my_view)
//Then use the extension function
myView.setNewWidth(500)
This is how I've defined my extension function
private fun View?.setNewWidth(i: Int) {
val layoutParams: ViewGroup.LayoutParams = View.layoutParams
layoutParams.width = i
View.layoutParams = layoutParams
}
I don't understand what I need to do here.
I want to call the extension function as myView.ExtensionFunction(), but I don't know how to do that. The tutorials, were un-informative.

I think the main problem here is how the extension function is defined, in particular, the lines that have View.layoutParams - this is calling a static property on View that doesn't exist. You need to use the one from the instance. If you'd write the extension function like so:
private fun View?.setNewWidth(i: Int) {
val layoutParams = this?.layoutParams
layoutParams?.width = i
this?.layoutParams = layoutParams
}
Then you can call the method like you want. Personally, I don't find this so readable and I'd remove the nullability here and write it as:
private fun View.setNewWidth(i: Int) {
val newLayoutParams = layoutParams
newLayoutParams?.width = i
layoutParams = newLayoutParams
}
The only difference is that now you need ?. to call the method if the view is nullable, which I personally find fine - myView?.setNewWidth(123). I assume most of the time you won't have a nullable view.

Ok, So my issue was that I didn't know how to get reference to the calling View. i.e., I didn't know how to call myView and set its property inside the extension function setNewWidth()
So, I tried using this? and it worked.
Then, I did a few changes to the extension function to work for myView which is a Relative Layout.
This is what I worked out:
private fun RelativeLayout?.setWidth(i: Int) {
val layoutParams: ViewGroup.LayoutParams? = this?.layoutParams
layoutParams?.width = i
this?.layoutParams = layoutParams
}

Related

Populate multiple fields in one line in Kotlin

I would like to know if it's possible to populate multiple fields in one line in Kotlin (just for cleaner code)
My Code:
val evh = ExampleViewHolder(binding.root)
evh.mImageView = binding.myImageView
evh.mTextView1 = binding.text1
evh.mTextView2 = binding.text2
I would like to achieve something like this:
(evh.mImageView, evh.mTextView1, evh.mTextView2) = (binding.myImageView, binding.text1, binding.text2)
Is this somewhat possible?
is this somewhat possible?
No, you can't set properties* in such a way in Kotlin.
If you'd like to initialize or change values on some properties in a cleaner way, you can use one of the scope functions:
The Kotlin standard library contains several functions whose sole purpose is to execute a block of code within the context of an object. When you call such a function on an object with a lambda expression provided, it forms a temporary scope. In this scope, you can access the object without its name. Such functions are called scope functions. There are five of them: let, run, with, apply, and also.
In this particular case it seems like apply is the best fit:
val evh = ExampleViewHolder(binding.root).apply {
mImageView = binding.myImageView
mTextView1 = binding.text1
mTextView2 = binding.text2
}
*note that similar inline syntax is valid in Kotlin and it's used in destructing declarations:
val (first, second) = listOf("firstValue", "secondValue")
You could also pass views as arguments to ExampleViewHolder, like this:
class ExampleViewHolder(
val rootView: View, val imageView: ImageView,
val textView1: TextView, val textView2: TextView
) : RecyclerView.ViewHolder(rootView) {...}
And then instantiate the ViewHolder using the apply scope function:
val viewHolder = binding.run {
ExampleViewHolder(root, myImageView, text1, text2)
}
I found this post which says it's possible if you put all views in a data class:
data class ExampleViewHolder(val mImageView: ImageView, val mTextView: TextView, val mTextView2: TextView, val binding: Binding) : RecyclerView.ViewHolder(binding.root)
and then do something like this:
val (mImageView, mTextView1, mTextView2, binding) = ExampleViewHolder(binding.myImageView, binding.text1, binding.text2, binding)
Alternatively you can always put semi-columns in between assignments to one-line it, but it does not get more readable imo:
evh.mImageView = binding.myImageView; evh.mTextView1 = binding.text1; evh.mTextView2 = binding.text2
I would just assign the fields inside your ExampleViewHolder:
class ExampleViewHolder(binding: Binding) : RecyclerView.ViewHolder(binding.root) {
val mImageView = binding.myImageView
val mTextView = binding.text1
val mTextView2 = binding.text2
}
Or even let the ExampleViewHolder do the binding:
class ExampleViewHolder(val binding: Binding) : RecyclerView.ViewHolder(binding.root) {
fun bindTo(item: Any) {
binding.myImageView.visibility = View.GONE
}
}
If an object has already been created, to change several fields you can use scope function "run", like:
evh.run {
mImageView = binding.myImageView
mTextView1 = binding.text1
mTextView2 = binding.text2
}

TextView text changes but does not update in layout (Kotlin - Android)

TextView text changes but does not update in layout. I tried every method I could find but nothing worked. I have a very basic application with a single activity and 3 layouts*.
*This is the first app I make so I tought it would have been simpler this way
The main problems I am facing are two: almost all the informations around are old and in java, and my textView text does not change.. The app is a simple Rock-Paper-Scissor game I'm trying to make as an exercise.
The textViews.text values get updated but the layout always shows the same text...
I have no idea what could be the problem. I am also struggling to understand exactly how all of this is working exactly...like InflateLayout, Context and Android in general. I do not understand much from android's reference.
THERE IS NO INFLATE(), POSTINFLATE(), FORCELAYOUT(), VISIBILITY TOGGLES BECAUSE NONE OF THEM WORKED :(
Excerpt of the code
class MainActivity : AppCompatActivity() {
lateinit var TITLE:TextView
lateinit var PARAGRAPH:TextView
override fun onCreate(savedInstanceState :Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val InflaterInitializer = LayoutInflater.from(applicationContext) as LayoutInflater
val inflater = InflaterInitializer.inflate(R.layout.activity_2, null, false)
TITLE= inflater.findViewById(R.id.title) as TextView
PARAGRAPH= inflater.findViewById(R.id.paragraph) as TextView
}
There are three functions like this:
fun FUNCTION(v :View) {
val userChoice = "XXX"
val computerChoice = getComputerChoice()
if (userChoice == computerChoice) {
FUNCTION_2(computerChoice)
} else {
runOnUiThread {
TITLE.text =
if (computerChoice == "YYY") getString(R.string.YOU_WON) else getString(R.string.YOU_LOSE);
PARAGRAPH.text = getString(R.string.STRING, computerChoice)
}
}; resultScreen()
}
Function_2...
private fun FUNCTION_2(cc :String) {
runOnUiThread {
TITLE.text = getString(R.string.STRING)
PARAGRAPH.text = getString(R.string.STRING, cc)
}; resultScreen()
}
resultScreen() is just a call to setContentView(LAYOUT)
Here's a video of the app and the update problem:
https://imgur.com/a/iWCRMkq
Code complete here: https://github.com/noiwyr/MorraCinese
EDIT
Unfortunately none of the answers actually worked as I hoped, however redesigning the app and using multiple activities with some tweaks solved the issue. You may find the new code in the github repo.
However I would be curious to know if there is a working solution for this question :)
By calling InflaterInitializer.inflate(R.layout.activity_2, null, false) you inflate a new view hierarchy from the specified xml resource, which is not attached to any of your views (these new views are not shown on your screen). Then you found text views from that new view hierarchy and changed their titles.
So, your onCreate method have to look like this:
override fun onCreate(savedInstanceState :Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_2)
TITLE = findViewById(R.id.title)
PARAGRAPH = findViewById(R.id.paragraph)
}
Also, it's redundant to use methods runOnUiThread() (your code already runs on Ui thread) and resultScreen().
You no need anything , you creat over code no problem I suggest you
val InflaterInitializer = LayoutInflater.from(applicationContext) as LayoutInflater val inflater = InflaterInitializer.inflate(R.layout.activity_outcome, null, false)
Comment this above code no need in kotlin
motivoRisultato.text = getString(R.string.scelta_pc, computerChoice)
Simpaly make this type of code
There are quite a few errors in your code, so I'm going to break down the answer with your code. Do find the Comments inline
class MainActivity : AppCompatActivity() {
/**
* First, Follow conventions for naming variables, they are usually in camelcase for variables and functions, Capitalized for Constants.
* Second, lateinit is used to defer the initialization of a variable, for views, such as
* TextView's, you could use the Kotlin Synthentic library which automatically references the Views of your layout.
*/
lateinit var TITLE:TextView
lateinit var PARAGRAPH:TextView
override fun onCreate(savedInstanceState :Bundle?) {
super.onCreate(savedInstanceState)
/**
* Set content view, internally set's the layout file after inflation using the Activity context. Which means, that you do not
* need to specifically inflate the view.
*/
setContentView(R.layout.activity_main)
/**
* This is the reason why your layout doesn't know refresh, what you're doing here is inflating another layout, but not setting it to your activity.
* This is not required as explained above
*/
val InflaterInitializer = LayoutInflater.from(applicationContext) as LayoutInflater
/**
* Inflater inflates a View Object. one would use this approach if they were programatically adding Views
*/
val inflater = InflaterInitializer.inflate(R.layout.activity_2, null, false)
/**
* the below views are pointing to a reference of TextView for the non visible inflated view. Which is the reason why the text is not updated.
*/
TITLE= inflater.findViewById(R.id.title) as TextView
PARAGRAPH= inflater.findViewById(R.id.paragraph) as TextView
}
}
Here's the code to make things work
class MainActivity : AppCompatActivity() {
private var title:TextView? = null
private var paragraph:TextView? = null
override fun onCreate(savedInstanceState :Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
title= inflater.findViewById(R.id.title) as TextView
paragraph= inflater.findViewById(R.id.paragraph) as TextView
}
fun function(v :View) {
val userChoice = "XXX"
val computerChoice = getComputerChoice()
if (userChoice == computerChoice) {
function2(computerChoice)
} else {
title.text = if (computerChoice == "YYY") getString(R.string.YOU_WON) else getString(R.string.YOU_LOSE);
paragraph.text = getString(R.string.STRING, computerChoice)
}
resultScreen()
}
private fun function2(cc :String) {
title.text = getString(R.string.STRING)
paragraph.text = getString(R.string.STRING, cc)
resultScreen()
}
}
If your use case is to show different screens, look at starting more than one Activity and transitioning between them using Intents

How to implement a Kotlin function to a View

I need to know how to implement functions to a ConstraintLayout in Kotlin.
I need something like this:
fun applyCustomPropeties(){
//some stuff
}
val rootLayout = findViewById<ConstraintLayout>(R.id.rootLayout)
rootLayout.applyCustomPropeties()
Thanks.
You can add an extension function:
fun ConstraintLayout.applyCustomProperties() {
//some stuff
//you can use "this" keyword here
}
That extension is resolved "statically", so no matter where you put that code. Now, you can do what you want:
val rootLayout = findViewById<ConstraintLayout>(R.id.rootLayout)
rootLayout.applyCustomPropeties()

Kotlin smart cast not working for LinearLayout.LayoutParams

I have implemented a function that I used to pass to anko`s applyRecursively.
Inside this function, I would like to add a marginEnd is the view is inside an LinearLayout, so I wrote the following code:
when(view.layoutParams) {
is LinearLayout.LayoutParams -> {
view.layoutParams.marginEnd = view.resources.getDimensionPixelSize(R.dimen.min_spacing)
}
}
And I receive the error that the view.layoutParams is a mutable property that could have been changed. So I had to force the cast:
when(view.layoutParams) {
is LinearLayout.LayoutParams -> {
(view.layoutParams as LinearLayout.LayoutParams).marginEnd = view.resources.getDimensionPixelSize(R.dimen.min_spacing)
}
}
Looking here at stackoverflow I saw that Kotlin don't smart cast in variables that can be nullable, but the view.layoutParams is not nullable, so why the smart cast can't infer the type?
Smart cast won't work in this case, because the type of the variable you did the type check on could have changed since that check passed successfully (for example, by a different thread), and if it did, you'd get a runtime exception when you attempt to cast it.
The solution is either to do the cast manually as you did, or to introduce a temporary val to your function, which smart cast will work on, since we know its type won't change:
val params = view.layoutParams
when(params) {
is LinearLayout.LayoutParams -> {
params.marginEnd = view.resources.getDimensionPixelSize(R.dimen.min_spacing)
}
}
Edit: As an additional note, you could also introduce this variable by using let:
view.layoutParams.let { params ->
when(params) {
is LinearLayout.LayoutParams -> {
params.marginEnd = view.resources.getDimensionPixelSize(R.dimen.min_spacing)
}
}
}

Horizontal LinearLayout in Anko

What is a good way to do a horizontalLayout in anko / kotlin ? verticalLayout works fine - could set orientation on it but it feels wrong. Not sure what I am missing there.
Just use a linearLayout() function instead.
linearLayout {
button("Some button")
button("Another button")
}
Yeah, LinearLayout is by default horizontal, but I tend to be extra specific and rather use a separate horizontalLayout function for that.
You can simply add the horizontalLayout function to your project:
val HORIZONTAL_LAYOUT_FACTORY = { ctx: Context ->
val view = _LinearLayout(ctx)
view.orientation = LinearLayout.HORIZONTAL
view
}
inline fun ViewManager.horizontalLayout(#StyleRes theme: Int = 0, init: _LinearLayout.() -> Unit): _LinearLayout {
return ankoView(HORIZONTAL_LAYOUT_FACTORY, theme, init)
}
I have opened a feature request at Anko: https://github.com/Kotlin/anko/issues/413

Categories

Resources