0

I have an activity with two spinners. I have made arrays for each spinner containing data from popular foods, but I want the user to be able to add three of their own selections to the lists. The app compiles and installs and runs, BUT when I select the specific activity, the screen closes and either goes to the apps main screen or to the emulator's home screen. Logcat shows:-

java.lang.RuntimeException: Unable to instantiate activity ComponentInfo{com.example.kotlinsql/com.example.kotlinsql.CarbsInput}: java.lang.NullPointerException: Attempt to invoke virtual method 'android.content.SharedPreferences android.content.Context.getSharedPreferences(java.lang.String, int)' on a null object reference

It's where I call the shared preferences. I have tried different contexts but still get errors that vary slightly according to the context and I have included them in the code as remarks.

I have tried moving everything into onCreate, but this gives me an error in the class definition line, because the function "override fun onItemSelected" seems to have to be stand-alone, so must be outside onCreate.

Please help. I have only been learning this for less than a year, and I apologise for any stupid mistakes. No offence is intended.


import android.content.Context
import android.content.Intent
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.view.View
import android.widget.*
import kotlinx.android.synthetic.main.input_carbs.*
import java.time.Clock
import java.time.LocalDateTime
import java.time.ZonedDateTime
import java.time.format.DateTimeFormatter
import android.content.SharedPreferences
import android.content.res.Configuration
import java.security.AccessController.getContext
import kotlin.math.*

class CarbsInput : AppCompatActivity(),AdapterView.OnItemSelectedListener {


    var spinner:Spinner? = null
    var spinner2:Spinner? = null
    val sharedPrefFile = "greenbandbasicpreference"
    
    val sharedPreferences: SharedPreferences by lazy { getSharedPreferences(sharedPrefFile, MODE_PRIVATE) }
    val dataModel: CarbsInputModel by lazy { CarbsInputModel(sharedPreferences) }

override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.input_carbs)

        spinner = this.gi_spinner
        spinner2 = this.carbs_per_spinner

        // Create an ArrayAdapter using a simple spinner layout and gIndices array
        val aa = ArrayAdapter(this, android.R.layout.simple_spinner_item, dataModel.gIndices)
        // Set layout to use when the list of choices appear
        aa.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
        // Set Adapter to Spinner
        spinner!!.setAdapter(aa)
        //spinner!!.setSelection(9)//optional, better to leave favourites at top

        val aa2 = ArrayAdapter(this, android.R.layout.simple_spinner_item, dataModel.carbsPer)
        aa2.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
        spinner2!!.setAdapter(aa2)


        input_carbs_btn.setOnClickListener {//set an onclick listener
            enterCarbs()                   }

        backbtn.setOnClickListener {
            val fourth = Intent(this, MainActivity::class.java)//sets "fourth" to be MainActivity
            // start your next activity
            startActivity(fourth)  }

        btn_view_carbs.setOnClickListener { viewCarbs() }

        btn_carb_calc.setOnClickListener {
        var carbPer = et_carbsper.text.toString().toLong()
        var weight = et_weight.text.toString().toLong()
        var carbs = round((weight * carbPer) /100.0).toLong()
        et_carbs.setText(carbs.toString())
    }//end of button onClick listener

}//end of on create






    fun enterCarbs(){//get inputs from keys and calculate carbLife using GI



        var noow = ZonedDateTime.now(Clock.systemUTC())
        var noowSecs: Long = noow.toEpochSecond()
        var noowMins: Long = (noowSecs) / 60
        //var carbLife:Long = 220// this has to be calculated from GI
        var nowLocal = LocalDateTime.now()
        var carbTime: Long = noowMins-1
        var showCarbTime: String = nowLocal.format(DateTimeFormatter.ofPattern("E d MMM kk:mm "))+"local"
        var sharedPrefFile = "greenbandbasicpreference"
        val sharedPreferences: SharedPreferences = getSharedPreferences(sharedPrefFile, Context.MODE_PRIVATE)

        val databaseHandler: DatabaseHandler = DatabaseHandler(this)

        if (et_carbs.text.toString().trim() != "" && et_carbGI.text.toString().trim() != "") {

            val carbs = et_carbs.text.toString().toLong()
            val carbGi = (et_carbGI.text.toString().toLong())
            //val carbLife = 12_000 /carbGi.toLong()// to be replaced with 1-(X/L)^n calculation in stage2
            var carbDecayIndex:Double= sharedPreferences.getFloat("carbDecayIndex_key",0.8F).toDouble()//n
            //public fun carbLifeCalc():Double//L = 10^((log120^n-logGI)/n)
            var logLtoN = log10(120.00.pow(carbDecayIndex))//log120^n

            var logGi = log10(carbGi / 100.00)//logGI
            var carbLife = 10.00.pow((logLtoN - logGi) / carbDecayIndex).toLong()//gives L
            //end of carbLifeCalculation

            val status  =
                databaseHandler.saveCarbs(CarbsModelClass(carbTime, showCarbTime, carbs, carbGi, carbLife))
            if (status > -1) {
                Toast.makeText(applicationContext, "Carbohydrate saved", Toast.LENGTH_LONG).show()
                //MainActivity.evaluateCarbs //want to call this function from here without writing it again
                et_carbs.text.clear()
                et_carbGI.text.clear()

            }
        } else {
            Toast.makeText(
                applicationContext,
                "No field can be blank enter GI as 50 if unknown",
                Toast.LENGTH_LONG
            ).show()
        }
    }//end of function entercarbs



    fun viewCarbs() {
        //creating the instance of DatabaseHandler class
        val databaseHandler: DatabaseHandler = DatabaseHandler(this)
        //calling the viewCarbs method of DatabaseHandler class to read the records
        val carbohs: List<CarbsModelClass> = databaseHandler.viewCarbs()
        //val carbohsArraycarbTime     = Array<String>(carbohs.size) { "null" }//not needed
        val carbohsArrayshowCarbTime = Array<String>(carbohs.size) { "null" }
        val carbohsArraycarbs        = Array<String>(carbohs.size) { "null" }
        val carbohsArraycarbGI       = Array<String>(carbohs.size) { "null" }
        val carbohsArraycarbLife     = Array<String>(carbohs.size) { "null" }

        var index = 0
        for (e in carbohs) {
            //carbohsArraycarbTime[index] = e.carbTime.toString()//not needed
            carbohsArrayshowCarbTime[index] = e.showCarbTime
            carbohsArraycarbs[index] = e.carbs.toString()
            carbohsArraycarbGI[index] = e.carbGi.toString()//note small i inGi
            carbohsArraycarbLife[index] = e.carbLife.toString()
            //index--
            index++
        }
        //creating custom ArrayAdapter
        val myCarbListAdapter = CarbListAdapter(
            context = this,
            //carbTime = carbohsArraycarbTime,//not needed
            showCarbTime = carbohsArrayshowCarbTime,
            carbs = carbohsArraycarbs,
            carbGI = carbohsArraycarbGI,
            carbLife = carbohsArraycarbLife
        )
        lv_carb_view.adapter = myCarbListAdapter
    }//end of fun view carbs

    override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
// see https://stackoverflow.com/questions/9262871/android-two-spinner-onitemselected
        if(parent?.getId() == R.id.gi_spinner) {
            var giFullSelected = dataModel.gIndices[position]
            var gIprelimString : String =
                giFullSelected[0].toString() + giFullSelected[1]//selecting just digits
            var GIprelim = gIprelimString.toLong()
            et_carbGI.setText(gIprelimString)

        }//end of first if
        else{ if (parent?.getId() == R.id.carbs_per_spinner) {
            var carbPerFullSelected = dataModel.carbsPer[position]
            var carbPerString: String =
                carbPerFullSelected[0].toString() + carbPerFullSelected[1]
            var carbPer = carbPerString.toLong()
            et_carbsper.setText(carbPerString)
            var weight = et_weight.text.toString().toLong()
            var carbs = round((weight * carbPer) /100.0).toLong()
            et_carbs.setText(carbs.toString())}//end of second if
            else { Toast.makeText(applicationContext, "parent id "+parent?.getId().toString(), Toast.LENGTH_LONG).show()
                    }//end of second else
        }//end of elseif OR /first else

    }//end of on item selected

    override fun onNothingSelected(parent: AdapterView<*>?) {  }

}//end of class carbs input

New Class CarbsInputModel Below


//start of CarbsInputModel
import android.content.SharedPreferences

class CarbsInputModel(private val sharedPreferences:SharedPreferences) {
   // val sharedPrefFile = "greenbandbasicpreference"
    //val sharedPreferences:SharedPreferences = getSharedPreferences(sharedPrefFile, MODE_PRIVATE)
    val sharedFav1Value: String? = sharedPreferences.getString("fav1_key", "50 50 defaultone")
    val sharedFav2Value: String? = sharedPreferences.getString("fav2_key", "50 50 defaultwo")
    val sharedFav3Value: String? = sharedPreferences.getString("fav3_key", "50 50 defaulthree")
    val favDescr1:String = sharedFav1Value?.takeLastWhile { !it.isDigit() }.toString().trim()
    val favDescr2:String = sharedFav2Value?.takeLastWhile { !it.isDigit() }.toString().trim()
    val favDescr3:String = sharedFav3Value?.takeLastWhile { !it.isDigit() }.toString().trim()
    val favData1:String = sharedFav1Value?.takeWhile { !it.isLetter() } .toString()
    val favData2:String = sharedFav2Value?.takeWhile { !it.isLetter() } .toString()
    val favData3:String = sharedFav3Value?.takeWhile { !it.isLetter() } .toString()
    val favCarbPerString1 = favData1.take(3).trim()
    val favCarbPerString2 = favData2.take(3).trim()
    val favCarbPerString3 = favData3.take(3).trim()
    val favGiString1 = favData1.takeLast(4).trim()
    val favGiString2 = favData2.takeLast(4).trim()
    val favGiString3 = favData3.takeLast(4).trim()

    val favFullCarbPer1 = favCarbPerString1+" "+favDescr1+" "
    val favFullCarbPer2 = favCarbPerString2+" "+favDescr2+" "
    val favFullCarbPer3 = favCarbPerString3+" "+favDescr3+" "
    val favFullGi1 = favGiString1+" "+favDescr1+" "
    val favFullGi2 = favGiString2+" "+favDescr2+" "
    val favFullGi3 = favGiString3+" "+favDescr3+" "
}//end of class Carbs Input Model

Attempted tidy code for override function. This still does absolutely Nothing

//trying to tidy up code
 override fun onItemSelected(parent: AdapterView<*>, view: View, position: Int, id: Long) {
 when {
     parent.id == R.id.gi_spinner -> {
         var giFullSelected = dataModel.gIndices[position]
         var gIprelimString: String =
             giFullSelected[0].toString() + giFullSelected[1]//selecting just leading digits
         et_carbGI.setText(gIprelimString)
     }

     parent.id == R.id.carbs_per_spinner -> {
         var carbPerFullSelected = dataModel.carbsPer[position]
         var carbPerString: String =
             carbPerFullSelected[0].toString() + carbPerFullSelected[1]
         var carbPer = carbPerString.toLong()
         et_carbsper.setText(carbPerString)
         var weight = et_weight.text.toString().toLong()
         var carbs = round((weight * carbPer) / 100.0).toLong()
         et_carbs.setText(carbs.toString())
     }

     else -> {
         Toast.makeText(applicationContext, "parent id " + parent?.getId().toString(),
             Toast.LENGTH_LONG ).show()
     }
    }//end of when
}//end of override fun onitemselected
JJCNSNR
  • 9
  • 3

1 Answers1

1

When you assign a value at the declaration site like this:

 val sharedPreferences:SharedPreferences = getSharedPreferences(sharedPrefFile, MODE_PRIVATE)

the function(s) you are calling to create the object that will be assigned to the property is getting called at the time the Activity is instantiated by Android. Unfortunately, this is too early to be calling anything that relies on the Activity being fully instantiated and set up, for example, anything that needs the Context as a constructor parameter.

The easy fix for this is to make these properties instantiate themselves lazily, so they are created after the Activity is already fully instantiated:

 val sharedPreferences: SharedPreferences by lazy { getSharedPreferences(sharedPrefFile, MODE_PRIVATE) }

An alternate solution is the use a lateinit var and prepare the item in onCreate():

lateinit var sharedPreferences: SharedPreferences

// ...

override fun onCreate(bundle: SavedInstanceState) {
    super.onCreate(bundle)
    sharedPreferences = getSharedPreferences(sharedPrefFile, MODE_PRIVATE)
}

I usually prefer the lazy method because it avoid splitting the declaration and the assignment so the code is easier to read. And it allows you to use val instead of var so the intent is clearer.

However, you also have many properties that are reliant on the SharedPreference instance, so they would all have to use one of the above solutions as well, which will lead to very verbose code. I recommend that you move all of these properties into a separate class that uses the SharedPreferences as a constructor paraamter. For example:

class CarbsInputModel(private val sharedPreferences: SharedPreferences) {

    val sharedFav1Value: String? = sharedPreferences.getString("fav1_key", "50 50 defaultone")
    val sharedFav2Value: String? = sharedPreferences.getString("fav2_key", "50 50 defaultwo")
    val sharedFav3Value: String? = sharedPreferences.getString("fav3_key", "50 50 defaulthree")
    
    // etc...
}

and then in your activity:

class CarbsInput : AppCompatActivity(),AdapterView.OnItemSelectedListener {

    var spinner:Spinner? = null
    var spinner2:Spinner? = null
    val sharedPrefFile = "greenbandbasicpreference"
    val sharedPreferences: SharedPreferences by lazy { getSharedPreferences(sharedPrefFile, MODE_PRIVATE) }
    val dataModel: CarbsInputModel by lazy { CarbsInputModel(sharedPreferences) }

}

And then access your properties through the dataModel property. It is also better design practice to separate your UI and your functions that modify the data, so you could put those functions in your data model class.

You might also want to read up on how to use a ViewModel class. It would possibly be a more scalable solution than what I put above.

Tenfour04
  • 83,111
  • 11
  • 94
  • 154
  • Thank you very much for a very constructive answer. I haven't implemented it yet,but I will. I will also read about ViewModel. – JJCNSNR Oct 31 '20 at 08:10
  • Just tried to implement this answer but it's not working. same visible result still giving NPE exceptions but at different lines. How do I give full details? – JJCNSNR Oct 31 '20 at 11:41
  • You can edit your question to add the new code or put it on Pastebin or Github Gist and I’ll take a look. – Tenfour04 Oct 31 '20 at 12:09
  • when i type "class CarbsInputModel(private val sharedPreferences: SharedPreferences) the "val" is greyed out and there's an error message "constructor parameter is never used as a property" – JJCNSNR Oct 31 '20 at 12:14
  • ive tried to add the amendments in to the code in the question and here is the URL of the error in Paste Bin (new to me so not sure how to share)https://pastebin.com/9rA8feQC – JJCNSNR Oct 31 '20 at 12:30
  • It's the code I need to see, not the error. Somewhere you are still accessing the context before the Activity is ready. The grayed out `val` is just a lint hint that you could remove it if you want to since you never access it after instantiation. But if you expand the responsibilities of the class, you probably will want it later. – Tenfour04 Oct 31 '20 at 12:48
  • OK I will overwrite all the old code with the new code from the two classes. should the declarations of the array be moved also? – JJCNSNR Oct 31 '20 at 14:14
  • You're accessing something from your model in the declaration of `gIndices` and `carbsPer`, so you are still accessing the context before the Activity is ready. That's what I was talking about in my answer above. Anything that is dependent on the SharedPreference *also* needs to be lazy. But since these are just more data, I would move them into the model as well. – Tenfour04 Oct 31 '20 at 14:49
  • I have edited the code in the question. Thanks...Your answer has solved my question, in that the activity opens and works. But the Item selected listener functions do nothing. Nothing crashes, but no values are put into the edit text fields eg line 163 and 174. Also the fall-back toast that I have inserted at line 175 does not show. Is this another context issue? – JJCNSNR Nov 01 '20 at 12:03
  • You can use the debugger to figure out which branch of code is getting called to narrow down the issue. But you might want to clean it up a bit first. It’s an unusual construction to nest an if/else inside an else branch. It could be if, if else, else with no nesting, but a when statement would be even clearer. – Tenfour04 Nov 01 '20 at 12:47
  • have tried to tidy. see amended Question. Looks to me as if Nothing is being called when items are selected – JJCNSNR Nov 01 '20 at 15:44
  • You seem to be missing `spinner.onItemSelectedListener = this`. – Tenfour04 Nov 01 '20 at 16:38