Why this strange behaviour of the `by lazy` delegate in fragments - android

A few days ago, I posted this question about using synthetic properties when you include the same layout in a screen multiple times.
The answer was awesome but after I tried it more for a few days, I noticed a strange behaviour:
When going forward from the fragment (the one containing the references to the view obtained by the lazy delegate) and then coming back ( I use transaction.commit() and manager.popBackStack(), to do this ), the labels will be empty. I already checked with the debugger if anything is null there, and nothing is.
The only solution that seems to work is replacing the by lazy with lateinit var and assigning them in onViewCreated.
Do you know why? Is the solution I used still "good" as a kotlin idiom?
I include the two pieces of code for the sake of completeness:
Partially working one:
private val foundTitle by lazy { detailContainer1.title }
private val foundDescription by lazy { detailContainer1.description }
private val wantedTitle by lazy { detailContainer2.title }
private val wantedDescription by lazy { detailContainer2.description }
Always working one:
private lateinit var foundTitle: TextView
private lateinit var foundDescription: TextView
private lateinit var wantedTitle: TextView
private lateinit var wantedDescription: TextView
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
foundTitle = detailContainer1.title
foundDescription = detailContainer1.description
wantedTitle = detailContainer2.title
wantedDescription = detailContainer2.description
}
Thanks in advance

Fragment's have their view destroyed when they get removed - but lazy fields do not clear their reference so they are essentially leaking previous view.
If possible You should always have unique view IDs within your project, even if they are not within same layout - having duplicates can cause multiple problems (like yours).
If you were able to use kotlin extensions directly, it would generate code for finding, caching and clearing the view cache when fragments view is destroyed automatically.
Try to "get" views from fragments cache instead of assigning them to fields:
private val foundTitle
get() = detailContainer1.title
private val foundDescription
get() = detailContainer1.description
private val wantedTitle
get() = detailContainer2.title
private val wantedDescription
get() = detailContainer2.description

Related

Can't change the value of MutableList in a Kotlin class

I want to add the random generated integer into my MutableList in Player class when I use the random integer generator method located in Player class in my fragment then I want to pass this MutableList to Fragment with using Livedata(I'm not sure if i'm doing right with using livedata).Then show the MutableList in TextView.But MutableList returns the default value not after adding.
So what am i doing wrong ? What should i do ?
Thank you
MY CLASS
open class Player {
//property
private var playerName : String
private var playerHealth : Int
var playerIsDead : Boolean = false
//constructor
constructor(playerName:String,playerHealth:Int){
this.playerName = playerName
this.playerHealth = playerHealth
}
var numberss: MutableList<Int> = mutableListOf()
fun attack(){
//Create a random number between 1 and 10
var damage = (1..10).random()
//Subtract health points from the opponent
Log.d("TAG-DAMAGE-WRITE","$damage")
numberss.add(damage)
Log.d("TAG-DAMAGE-ADD-TO-LIST","$numberss")
Log.d("TAG-NUMBER-LIST-LATEST-VERSION","$numberss")
}
}
MY FRAGMENT
class ScreenFragment : Fragment() {
var nickname : String? = null
private lateinit var viewModel : DenemeViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
return inflater.inflate(R.layout.fragment_screen, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel = ViewModelProvider(this).get(DenemeViewModel::class.java)
arguments?.let {
nickname = ScreenFragmentArgs.fromBundle(it).nickName
}
sFtextView.text = "Player : $nickname"
action()
}
private fun action(){
val health1 = (1..50).random()
val health2 = (1..50).random()
val superMan = Superman("Dusman",health1,)
while(superMan.playerIsDead == false){
//attack each other
superMan.attack()
sFtextsonuc.text = "Superman oldu"
viewModel.setData()
observeLiveData()
superMan.playerIsDead = true
}
}
fun observeLiveData(){
viewModel.damageList.observe(viewLifecycleOwner, Observer { dmgList ->
dmgList?.let {
sFtextsonuc.text = it.toString()
Log.d("TAG-THE-LIST-WE'VE-SENT-TO-FRAGMENT","$it")
}
})
}
}
MY VIEWMODEL
class DenemeViewModel : ViewModel() {
val damageList:MutableLiveData<MutableList<Int>> = MutableLiveData()
fun setData(){
damageList.value = Superman("",2).numberss
Log.d("TAG-VIEWMODEL","${damageList.value}")
}
}
MY LOG
PHOTO OF THE LOGS
Your Superman is evidently part of some ongoing game, not something that should be created and then destroyed inside the action function. So you need to store it in a property. This kind of state is usually stored in a ViewModel on Android so it can outlive the Fragment.
Currently, you are creating a Superman in your action function, but anything created in a function is automatically sent do the garbage collector (destroyed) if you don't store the reference in a property outside the function.
Every time you call the Superman constructor, such as in your line damageList.value = Superman("",2).numberss, you are creating a new instance of a Superman that has no relation to the one you were working with in the action() function.
Also, I recommend that you do not use LiveData until you fully grasp the basics of OOP: what object references are, how to pass them around and store them, and when they get sent to the garbage collector.
So, I would change your ViewModel to this. Notice we create a property and initialize it with a new Superman. Now this Superman instance will exist for as long as the ViewModel does, instead of only inside some function.
class DenemeViewModel : ViewModel() {
val superman = Superman("Dusman", (1..50).random())
}
Then in your frgament, you can get this same Superman instance anywhere you need to use it, whether it be to deal some damage to it or get the current value of its numberss to put in an EditText.
Also, I noticed in your action function that you have a while loop that repeatedly deals damage until Superman is dead. (It also incorrectly observes live data over and over in the loop, but we'll ignore that.) The problem with this is that the while loop is processed to completion immediately, so you won't ever see any of the intermediate text. You will only immediately see the final text. You probably want to put some delays inside the loop to sort of animate the series of events that are happening. You can only delay easily inside a coroutine, so you'll need to wrap the while loop in a coroutine launch block. In a fragment when working with views, you should do this with viewLifecycleOwner.lifecycleScope.launch.
Finally, if you set playerIsDead to true on the first iteration of the loop, you might as well remove the whole loop. I'm guessing you want to wrap an if-condition around that line of code. But since your code above doesn't modify player health yet, there's no sensible way to determine when a player should be dead, so I've commented out the while loop
private fun action() = viewLifecycleOwner.lifecycleScope.launch {
val superMan = viewModel.superman
//while(superMan.playerIsDead == false){
//attack each other
superMan.attack()
sFtextsonuc.text = "Superman oldu"
delay(300) // give user a chance to read the text before changing it
sFtextsonuc.text = superMan.numberss.joinToString()
delay(300) // give user a chance to read the text before changing it
// TODO superMan.playerIsDead = true
//}
}

Kotlin Errors After Converting from Java

I do not have very much experience with Kotlin, been doing most of my development in Java. I have converted my ArtistFragment to Kotlin in order to solve another issue. But after converting it I am getting to errors that I do not know how to resolve.
The first one is Smart cast to 'RecyclerView!' is impossible, because 'recyclerView' is a mutable property that could have been changed by this time and the second one is Cannot create an instance of an abstract class
I searched through Stackoverflow, but since I don't really understand the what the problem is I am not sure if they are relevant to my problems.
Here is my converted ArtistFragment:
class ArtistFragment : Fragment() {
var spanCount = 3 // 2 columns
var spacing = 20 // 20px
var includeEdge = true
private var recyclerView: RecyclerView? = null
private var adapter: ArtistAdapter? = null
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
val view = inflater.inflate(R.layout.fragment_artist, container, false)
recyclerView = view.findViewById(R.id.artistFragment)
recyclerView.setLayoutManager(GridLayoutManager(activity, 3))
LoadData().execute("")
return view
}
abstract inner class LoadData : AsyncTask<String?, Void?, String>() {
protected fun doInBackground(vararg strings: String): String {
if (activity != null) {
adapter = ArtistAdapter(activity, ArtistLoader().artistList(activity))
}
return "Executed"
}
override fun onPostExecute(s: String) {
recyclerView!!.adapter = adapter
if (activity != null) {
recyclerView!!.addItemDecoration(GridSpacingItemDecoration(spanCount, spacing, includeEdge))
}
}
override fun onPreExecute() {
super.onPreExecute()
}
}
}
I am seeing Smart cast to 'RecyclerView!' is impossible, because 'recyclerView' is a mutable property that could have been changed by this time here in this section: recyclerView.setLayoutManager(GridLayoutManager(activity, 3))
I am seeing Cannot create an instance of an abstract class here in this section LoadData().execute("")
I hoping someone can explain these error and how to fix them.
Thanks
As your recyclerview is a nullable property, You need to modify your code as below -
recyclerView?.setLayoutManager(GridLayoutManager(activity, 3))
Remove abstract from class
inner class LoadData : AsyncTask<String?, Void?, String>()
Smart cast to 'RecyclerView!' is impossible, because 'recyclerView' is a mutable property that could have been changed by this time is because recyclerView is a var. Kotlin has the concept of mutable (var) and immutable (val) variables, where a val is a fixed value, but a var can be reassigned.
So the issue is that recyclerView is a nullable type (RecyclerView?) and it could be null - so you need to make sure it's not null before you access it, right? findViewById returns a non-null RecyclerView, which gets assigned to recyclerView, so the compiler knows it can't be null.
But then it gets to the next line - what if the value of recyclerView has changed since the previous line? What if something has modified it, like on another thread? It's a var so that's possible, and the compiler can't make any guarantees about whether it's null or not anymore. So it can't "smart cast to RecyclerView!" (! denotes non-null) and let you just treat it as a non-null type in your code. You have to null check it again.
What Priyanka's code (recyclerView?.setLayoutManager(GridLayoutManager(activity, 3))) does is null-checks recyclerView, and only makes the call if it's non-null. Under the hood, it's copying recyclerView to a temporary val (which isn't going to change) and checks that, and then executes the call on that.
This is a pretty standard thing with Kotlin, and not just for nullable types - any var could possibly change at any moment, so it's typical to assign it to a temporary variable so you can do the stuff on that stable instance. Kotlin has a bunch of scope functions which are convenient for doing things on an object, and one of the benefits is that you know the reference you're acting on isn't going to change halfway through

How to reduce a healthlevel by 1 when a button is clicked in kotlin?

I'm starting out with kotlin. and I'm having trouble understanding OnClickListener. Here After setting the initial health level to 10, I need to reduce the health level by 1 and display it. So far I have initiliased the healthlevel and set the onclick, but How do declare the function to reduce it by 1 and call it when the button is clicked?
val TAG = "MyMessage"
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
Log.i("LIFECYCLE", "OnCreate")
}
private var healthLevel: Int = 10 //Set the initial health level to 10
private lateinit var healthLevelTextView: TextView
private lateinit var sneezeBtn: Button
private lateinit var takeMedicationButton: Button
private lateinit var blowNoseButton: Button
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putInt("Answer", healthLevel)
Log.i(TAG, "Called SaveInstanceState()")
}
sneezeBtn.setOnClickListener{ _ ->
// the function goes here
}
Vitor has the answer, but just as a couple of alternatives...
You might want to create a function to update and display your value together:
fun setHealth(health: Int) {
healthLevel = health
healthLevelTextView.text = healthLevel
}
That couples the update with the display change, so the two things always happen together. And if you always set healthLevel with this function (instead of setting the variable directly), the display and value will always be in sync
override fun onCreate(savedInstanceState: Bundle?) {
// set your view variables, so the TextView is ready to update
setHealth(INITIAL_HEALTH_VALUE)
And if you like, Kotlin lets us put a setter function on the variable itself
var healthLevel: Int = 10
set(value) {
field = value
healthLevelTextView.text = health
}
so every time you change the value of healthLevel, it also updates the display. You can use the observable delegate too
var healthLevel: Int by Delegates.observable(10) { _, oldValue, newValue ->
healthLevelTextView.text = newValue
}
These are more advanced than just setting the value and updating the view yourself, just pointing out that they exist as alternatives and ways to keep your logic in one place. Also with these examples, they only run the code after you first set a value - they have a default of 10 in both cases, but initialising that default won't run the setter code or the observable function. So you'd still need to go healthLevel = 10 to get the text to display the initial value.
You can change your healthLevel variable by using:
sneezeBtn.setOnClickListener {
healthLevel--
healthLevelTextView.setText(healthLevel.toString())
}
As a side note, if you use lateinit you lose one of the best features of Kotlin, which is Null Safety. If you don't know how to use that yet, I recommend you to start learning it as soon as possible.
You can also use a very nice feature of Android with kotlin, where the objects for your views are generated automatically in your Activity, you just have to type your XML views id, instead of having to use findViewById everywhere.
I assume, your code sample is within an Activity class and you have a layout file activity_main.xml which looks something like this:
...
<TextView
android:id="#+id/healthLevelTextViewId"
...
/>
<Button
android:id="#+id/sneezeBtnId"
...
/>
...
This would be the code with your desired functionality:
class YourActivity : Activity {
private var healthLevel: Int = 10
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// retrieve references to the text view and button
val healthLevelTextView = findViewById<TextView>(R.id.healthLevelTextViewId)
val sneezeBtn = findViewById<Button>(R.id.sneezeBtnId)
// the OnClickListener must be initialized within a method body, as it is not a method itself.
sneezeBtn.setOnClickListener { _ ->
// implementation from the answer from Vitor Ramos
healthLevel--
healthLevelTextView.setText(healthLevel.toString())
}
}
}
You can further improve the code:
Add apply plugin: 'kotlin-android-extensions' to your app's build.gradle file. Then you can reference your views directly using their IDs. So instead of
val sneezeBtn = findViewById<Button>(R.id.sneezeBtnId)
sneezeBtn.setOnClickListener { ... }
you can use the Id directly like this:
sneezeBtnId.setOnClickListener { ... }
AndroidStudio will give you a hint to add an import for sneezeBtnId.
Use View Models, LiveData and Data Binding. This is the recommended, but a little bit more advanced technique.

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

Kotlin: Read Only access of Immutable Type to an Internal Variable of a Mutable Type

While learning ViewModels in Android, a problem has arisen that feels like Kotlin was meant to solve. In the code below, we can see that MutableLiveData values are being use to edit values and indicators. However, we do not want these mutable values to be exposed to anything else, specifically members of an Android lifecycle. We DO want Android Lifecycle members to have access to read values but not set them. Therefore, the 3 exposed functions, displayed below, are of the LiveData<> immutable type.
Is there an easier or more concise way to expose read only values that can be edited internally? This seems like what Kotlin was made to avoid: boilerplate verbosity.
class HomeListViewModel: ViewModel(){
//Private mutable data
private val repositories = MutableLiveData<List<Repo>>()
private val repoLoadError = MutableLiveData<Boolean>()
private val loading = MutableLiveData<Boolean>()
//Exposed uneditable LIveData
fun getRepositories():LiveData<List<Repo>> = repositories
fun getLoadError(): LiveData<Boolean> = repoLoadError
fun getLoadingStatuses(): LiveData<Boolean> = loading
init{...//Do some stuff to MutableLiveData<>
}
}
A non-Android scenario that might be similar is:
class ImmutableAccessExample{
private val theThingToBeEditedInternally = mutableListOf<String>()
fun theThingToBeAccessedPublicly(): List<String> = theThingToBeEditedInternally
init {
theThingToBeEditedInternally.add(0, "something")
}
}
I don't know if it is possible to avoid the verbosity. But, I've seen that before and it is usually declared as a property.
private val _repositories = MutableLiveData<List<Repo>>()
val repositories : LiveData<List<Repo>>
get() = _repositories
This is the convention, see the doc here in Names for backing properties
If a class has two properties which are conceptually the same but one is part of a public API and another is an implementation detail, use an underscore as the prefix for the name of the private property:
Following the idea of this post:
class HomeListViewModel: ViewModel(){
val repositories: LiveData<List<Repo>> = MutableLiveData()
init {
repositories as MutableLiveData
...//Do some stuff to repositories
}
}
I haven't found any elegant solution to this problem however this is how I handle it.
private val selectedPositionLiveData = MutableLiveData<Int>()
fun getSelectedPosition() = selectedPositionLiveData as LiveData<Int>
The View observes via the public getter method and there's no need to define a second member in the ViewModel. I probably favour this approach due to my Java background with explicit getters but this seems to me to be as clean and concise as any of the other workarounds.
val doesn't have a setter since it's readonly but if you want a var you can do this
var repositories = MutableLiveData<List<String>>()
private set
var repoLoadError = MutableLiveData<Boolean>()
private set
var loading = MutableLiveData<Boolean>()
private set
This will give you a private setter and a public getter

Categories

Resources