How does Kotlin Android Extensions replacement for findViewById prevent null views? - android

I know that Kotlin's Android Extensions creates synthetic properties + caching function to replace any need to call findViewById:
https://stackoverflow.com/a/46482618/1650674
https://www.raywenderlich.com/84-kotlin-android-extensions
https://antonioleiva.com/kotlin-android-extensions/
All of these examples show that the similar java code would look like
private HashMap _$_findViewCache;
...
public View _$_findCachedViewById(int var1) {
if(this._$_findViewCache == null) {
this._$_findViewCache = new HashMap();
}
View var2 = (View)this._$_findViewCache.get(Integer.valueOf(var1));
if(var2 == null) {
var2 = this.findViewById(var1);
this._$_findViewCache.put(Integer.valueOf(var1), var2);
}
return var2;
}
public void _$_clearFindViewByIdCache() {
if(this._$_findViewCache != null) {
this._$_findViewCache.clear();
}
}
What I don't understand is how this prevents potential NPEs? var2 = this.findViewById(var1); may still return null.
Using the example from that last link:
<TextView
android:id="#+id/welcomeMessage"
...
android:text="Hello World!"/>
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
welcomeMessage.text = "Hello Kotlin!"
}
What type is welcomeMessage? TextView or TextView?

What I don't understand is how this prevents potential NPEs?
It doesn't. If you try referencing a widget that does not exist, you crash.
So long as your import statements are only for the relevant layout for your Kotlin code, you should not wind up referencing a widget that does not exist. Where the problem comes in is if you accidentally import the synthetic properties from another layout.
For example, suppose you have a project with activity_main.xml and scrap.xml layouts, and your activity is:
package com.commonsware.android.myapplication
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.view.View
import kotlinx.android.synthetic.main.scrap.*
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
scrapView.visibility = View.GONE
}
}
Here, we are referencing a scrapView view in the scrap layout. We have not inflated that layout, and so this crashes with an IllegalStateException:
Caused by: java.lang.IllegalStateException: scrapView must not be null
at com.commonsware.android.myapplication.MainActivity.onCreate(MainActivity.kt:14)
What type is welcomeMessage? TextView or TextView?
Technically, it is TextView!, where ! means "it's a platform type, so we don't know if it can be null or not". Practically, TextView! is used the same as TextView, which is why you crash if it winds up being null.

While #CommonsWare's answer is correct, I also wanted to spare my 2 cents on this subject.
As #CommonsWare pointed out, you have to import relevant layout in order to be able to use Kotlin Extensions. The tricky part here is, it's not only about importing relevant layout but also inflating the layout before you call it with Kotlin Extensions.
So, if you have something like below
import kotlinx.android.synthetic.main.activity_main.*
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
welcomeMessage.text = "Hello Kotlin!"
setContentView(R.layout.activity_main)
}
}
You will still get
java.lang.IllegalStateException: welcomeMessage must not be null
And your app will crash.

I got crash with NPE also in listener.
I made ZoomRecyclerView, which call onZoom listener method onZoom when zoomed.
This event is then propagated to activity, where i call synthetic imagebuttonview method, to set image resource to set zoom button image if item is zoomed or not.
It simply crash on synthetic call.
Example :
zoomRecycler.onZoom {
// exception here : zoomButton must be not null
zoomButton.setImageDrawable(...)
}
zoomButton.click {
// calling zoom which raise onZoom inside zoomRecycler
zoomRecycler.toggleZoom();
}

Related

Kotlin Change ViewText with an ID provided by a String

Goal: To get a ViewText resource and edit it from an activity, using a mutable string (because then the string can be changed to alter other ViewTexts in the same function).
Context: I'm making a grid using TableRows and TextViews that can be altered to form a sort of map that can be generated from an array.
Issue: The binding command does not recognise strings. See my comment "PROBLEM HERE".
Tried: getResources.getIdentifier but I've been told that reduces performance drastically.
An excerpt from gridmap.xml
<TextView
android:id="#+id/cell1"/>
GridMap.kt
package com.example.arandomadventure
import android.R
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import com.example.arandomadventure.databinding.GridmapBinding
class GridMap : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
//sets the binding and assigns it to view
val binding: GridmapBinding = GridmapBinding.inflate(layoutInflater)
val view = binding.root
setContentView(view)
//creates a string variable
var cellID = "cell1"
//uses binding to set the background colour to teal
binding.cellID.setBackgroundResource(R.color.teal_200) //<- PROBLEM HERE (Unresolved Reference)
//getResources.getIdentifier is not an option as it degrades performance on a larger scale
}
}
A binding object is just an autogenerated class, whose class members are defined by the views in your layout XML. You can't add or access a field on a class with the syntax you showed - binding classes are no different from any other class. If you wanted to be able to access them by name, you could load them into a map
val viewMap = mapOf(
"cell1" to binding.cell1,
"cell2" to binding.cell2,
"cell3" to binding.cell3
)
then you can use the map to access them by name
var cellID = "cell1"
viewMap[cellID].setBackgroundResource(R.color.teal_200)
If you want the map to be a class member, you can set it like this
private lateinit var viewMap: Map<String,View>
override fun onCreate(savedInstanceState: Bundle?) {
//...
viewMap = mapOf(
"cell1" to binding.cell1,
"cell2" to binding.cell2,
"cell3" to binding.cell3
)
}
If your layout has hundreds of views and this becomes cumbersome, you may want to consider adding the views programmatically instead.
Edit
If you want to do this a more ugly, but more automatic way you can use reflection. To do this you need to add this gradle dependency:
implementation "org.jetbrains.kotlin:kotlin-reflect:1.7.0"
then you can build up the map programmatically with all views in the binding.
val viewMap = mutableMapOf<String,View>()
GridmapBinding::class.members.forEach {
try {
val view = it.call(binding) as? View
view?.let { v ->
viewMap[it.name] = v
}
}
catch(e: Exception) {
// skip things that can't be called
}
}
Or you can use this to call a method (keep in mind this will throw if no such class member exists):
var cellID = "cell1"
val view = GridmapBinding::class.members.filter { it.name == cellID }[0].call(binding)

Issue in Display view dynamically code failed

I am very new to Android development and actually stuck with code in tutorial for Kotlin programming for android. The code below is not working and I have tried to find alternative but no luck.
Will appreciate if somebody can help we with the alternative code
below is a code:
package com.example.myapplication
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.widget.Button
import android.widget.TextView
import kotlinx.android.synthetic.main.activity_main.*
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
addSomeViews(count = 5)
}
fun addSomeViews(count: Int) {
for (i in 1..count) {
val textView = TextView(this)
textView.text = "Hey, learner # $i"
textView.textSize = 20f
my_layout.addView(textView)
}
val button = Button(this)
button.text = "Click me!"
my_layout.addView(button)
}
}
That kotlinx.synthetic stuff is deprecated - it doesn't work anymore. Instead of just referencing my_layout directly (the synthetics are supposed to look it up for you and create that variable) you need to find it yourself:
fun addSomeViews(count: Int) {
// lookup the layout viewgroup and create a variable for it
val my_layout = findViewById<ViewGroup>(R.layout.my_layout)
for (i in 1..count) {
val textView = TextView(this)
textView.text = "Hey, learner # $i"
textView.textSize = 20f
// now this variable exists
my_layout.addView(textView)
}
}
Aside from that... this isn't how a beginner should be learning Android imo. Creating views like this is kind of an advanced thing, mostly you never have to do it, and if you do there's a bunch of configuration you need to do on the views to make them display correctly (like the appropriate LayoutParams)
You can do it, but mostly you create your layouts in XML using the layout editor. And Compose is the new thing for writing UI in code, which is probably worth learning. It's up to you obviously, I just wanted to warn you that it's a strange thing for a beginner to be learning, and you might be better trying the Codelabs stuff instead

onClick & What argument to put inside increment()?

I wish the amount to increase by 1 every time the "+" is clicked.
But now when I click "+", it shows like this
Problem(A): increment is red in android:onClick="increment" /> in activity_main.xml
Problem(B): I know I should write something inside () of increment() and I have tried (1)increment(view: View?)=> red alert: parameter 'view' is never used shows up, but Problem (A) will be solved.
increment(view: View?) is modified from java codes.
public void submitOrder(View view) {
display(1);
}
if I replace (view: View?) with something else then red alert: parameter 'view' is never used disappears.
(2)increment(number:Int)(3)increment()
None of them works. (2)(3) will result Unfortunately, Order Coffee has stopped.
Here is my MainActivity.kt
class MainActivity : AppCompatActivity() {
var amount: Int=2
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
fun increment() {
amount++
display(amount)
}
//This method is called when the order button is clicked.
fun submitOrder() {
var amount: Int=2
display(amount)
displayPrice(amount*10)
}
fun decrement() {
amount--
display(amount)
}
//This method displays the given quantity value on the screen.
private fun display(number: Int) {
val amountTextView = findViewById<View>(R.id.amount_text_view) as TextView
amountTextView.text = "" + number
}
/**
* This method displays the given price on the screen.
*/
private fun displayPrice(number: Int) {
val priceTextView = findViewById<View>(R.id.price_text_view) as TextView
priceTextView.setText(NumberFormat.getCurrencyInstance().format(number))
}
}
It doesn't matter if you don't use the view parameter in your onClick method, you can ignore the warning - it's just a cleanup hint, it should be yellow not red. (It's also wrong in this case, because you know you need that View parameter! The compiler doesn't know about the onClick attribute in the XML though, so it thinks it's unused)
If it says the app has stopped, that's a crash and there should be an error log saying where it crashed and why - the crashes you're getting are probably because you're changing the signature of your onClick function. An onClickListener has a single View parameter, so whatever function you use has to match that.
Because you're saying "look for a function called this and use that as the click listener" in your XML file, it doesn't know what that function's going to be until you run the app and try clicking a thing. If you want to do it in code instead, you can do:
plusButton.setOnClickListener() { increment() }
which technically still has the View passed in, as a variable called it by default, but we're not using it so we can just ignore it! I prefer doing things in code like this personally, setting it in XML is kinda brittle and unhelpful sometimes (like you've discovered) but it's just a personal choice really

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

(android.view.View$OnClickListener)' on a null object reference Kotlin

I know there are a lot of similar questions were asked before, but usually, they're written in Java. I followed multiple tutorials but every time there is the same error. I followed this tutorial and it worked for other scripts.
Tried this answer, but didn't work as well.
This question is unique because other question are written in java which doesn't make it any easier for me.
Here is my MainActivity:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
NumberVerButton()
}
private fun NumberVerButton() {
next1.setOnClickListener {
startActivity(Intent(this, NumberVer::class.java))
}
}
}
And the error is the following.
E/AndroidRuntime: FATAL EXCEPTION: main
Process: com.example.wyspiansky, PID: 20258
java.lang.RuntimeException: Unable to start activity ComponentInfo{com.example.wyspiansky/com.example.wyspiansky.NumberVer}: java.lang.NullPointerException: Attempt to invoke virtual method 'void android.widget.Button.setOnClickListener(android.view.View$OnClickListener)' on a null object reference
Caused by: java.lang.NullPointerException: Attempt to invoke virtual method 'void android.widget.Button.setOnClickListener(android.view.View$OnClickListener)' on a null object reference
I just want to move to another activity by clicking a button.
Your next1 variable is never declared or initialized. I hope this is declared somewhere, otherwise, the code should have got a compilation error.
Hence my observation is the next1 variable was not initialized. You need to declare the next1 as a button and then initialize the next1 variable using the findViewById method to tag this button with the specific view reference id from your layout.
class MainActivity : AppCompatActivity() {
var next1: Button? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
next1 = findViewById(R.id.button) as Button
NumberVerButton()
}
private fun NumberVerButton() {
next1?.setOnClickListener {
startActivity(Intent(this, MainActivity::class.java))
}
}
}
Hope that helps.
Well first of all you need an object to refer to, looking at your source code you are using next1 object that is not declared any where in your source code. and also who knows what type of object is that usually
import android.support.v7.app.AppCompatActivity
import android.os.Bundle
import android.widget.Button
import android.widget.TextView
class MainActivity : AppCompatActivity() {
//First you prepare your objects to use them
private var text1: TextView? = null
private var next1: Button? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
//here you assign your references
//between the Layout xml file and your objects
text1 = findViewById(R.id.textView1)
next1 = findViewById(R.id.button1)
//here you set the click listener to your object
// remember, it need to be a View object, in this case
// is a Button which button heirs all from the parent class View
next1!!.setOnClickListener{
text1!!.text = "Hello S K, your click listener Works"
}
}
}
also you need to have your Layout with the same id names in the android:id xml property for every object
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".MainActivity">
<TextView
android:id="#+id/textView1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"/>
<Button
android:id="#+id/button1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</LinearLayout>
Well basically you need to attach whatever your UI Layout objects to your source code in order to control them, if you don't instantiate your objects correctly you will have NullPointer Exception because your source code will try to reference your next1 object but it was never declared and neither instantiated.
COMMENT: Honestly if you want good practices, you should consider to develop in Java and keep using Kotlin until you really master it. in some point all the old developers like me we need to witch to kotlin but for now at least i don't know anyone that can replicate all the old source code to kotlin without any issues.

Categories

Resources