0

I am working on an Android Studio project where I am using a singleton class to keep track of data (I have already done research on the pros and cons of singleton objects, and I decided it was the best solution for my project). I am, however, running into a few problems that seem to point back to my singleton object, for which I have not been able to find any good solutions on StackOverflow or other developer forums.

  1. The first error I'm getting happens where I call the singleton object in another class.

Note: I am not instantiating this singleton class before using it, because if I understand Kotlin singletons correctly, you don't have to.

for (item in items) {
            with(item.device) {
                if (name == "BLE_DEVICE") {
                    count++
                    Data.addresses.add(address) >>> This is where I call the singleton object <<<
                }
            }
        }
  1. The second error I get comes from my initialization of SharedPreferences in the singleton class.
var sharedPref: SharedPreferences? = MainActivity().getSharedPreferences("MySharedPreferencesFile", Context.MODE_PRIVATE)
  1. The third error I get comes from calling this function from my singleton object.
fun saveSharedPreferences() {
        for ((key, value) in names) {
            if (sharedPref != null) {
                if (!sharedPref?.contains(key)!!) {
                    sharedPref
                        ?.edit()
                        ?.putString(key, value)
                        ?.apply()
                }
            }
        }
    }

FOR REFERENCE:

a. Here are the important lines from my stack trace...

2022-08-30 16:07:05.422 9946-9946/? E/AndroidRuntime: FATAL EXCEPTION: main
    Process: com.punchthrough.blestarterappandroid, PID: 9946
>>> java.lang.ExceptionInInitializerError
     >>> at com.punchthrough.blestarterappandroid.ScanResultAdapter.getItemCount(ScanResultAdapter.kt:62)
        ...
        ...
 >>> Caused by: java.lang.NullPointerException: Attempt to invoke virtual method 'android.content.SharedPreferences android.content.Context.getSharedPreferences(java.lang.String, int)' on a null object reference
    >>> at android.content.ContextWrapper.getSharedPreferences(ContextWrapper.java:174)
    >>> at com.punchthrough.blestarterappandroid.Data.<clinit>(Data.kt:35)
        at com.punchthrough.blestarterappandroid.ScanResultAdapter.getItemCount(ScanResultAdapter.kt:62) 

b. This is my singleton class used for tracking data.

object Data {
    // Format: <Device Address, Name>
    // Used for keeping a record of all the devices, keeping
    //  duplicate advertisements off the screen, and saving the
    //  user-inputted names to the MAC address
    var names: MutableMap<String, String> = mutableMapOf()

    // Format: <Device Address>
    // Used for keeping a count of how many views should be populated
    //  in the RecyclerView
    var addresses = mutableSetOf<String>()

    // TODO: Fix this line to resolve an initialization error
    var sharedPref: SharedPreferences? = MainActivity().getSharedPreferences("MySharedPreferencesFile", Context.MODE_PRIVATE)

    fun saveSharedPreferences() {
        for ((key, value) in names) {
            if (sharedPref != null) {
                if (!sharedPref?.contains(key)!!) {
                    sharedPref
                        ?.edit()
                        ?.putString(key, value)
                        ?.apply()
                }
            }
        }
    }

}

gpeck
  • 39
  • 6
  • 2
    The Android system is responsible for creating or destroying your Activities. You can't just create an Activity from its constructor. Calling `MainActivity()` is not acceptable. – Vadik Sirekanyan Aug 31 '22 at 00:05

1 Answers1

1

Never instantiate an Activity. It simply won't work and cannot be used for any useful purpose. Activities are full of properties that are set up when the OS creates the Activity for you. If you instantiate it yourself, you have a dead object full of null properties that are not supposed to be null.

Kotlin's built-in singleton (object) is unsuitable for singletons that depend on something else because it has no constructor for you to call to initialize the dependencies.

In this case, your singleton would have to be dependent on a Context to be able to use shared preferences, so a Kotlin object is not suitable.

This is how you can create a singleton that needs a context:

class Data private constructor(val context: Context) {

    companion object {
        private var instance: Data? = null

        fun getInstance(context: Context): Data {
            return instance ?: synchronized(this) {
                instance ?: Data(context.applicationContext).also { instance = it }
            }
        }
    }

    val names: MutableMap<String, String> = mutableMapOf()

    val sharedPref: SharedPreferences = context.getSharedPreferences("MySharedPreferencesFile", Context.MODE_PRIVATE)

    fun saveSharedPreferences() { // I simplified this function a bit
        sharedPref.edit {
            for ((key, value) in names) {
                if (!sharedPref.contains(key)) {
                    putString(key, value)
                }
            }
        }
    }

}

And each time you use it, you would need to pass a Context to Data.getInstance.

By the way, I highly discourage combining var with MutableList or MutableSet. It invites mistakes because outside classes won't know whether they should swap out the collection for a new instance or mutate it in place when they want to make changes. And other classes cannot know whether it's safe to cache a copy of the list because it may or may not change out from under them based on something some other class is doing.

Really, I wouldn't recommend ever exposing a MutableCollection (MutableList or MutableSet) or a var read-only Collection publicly from any class. It leaves you open to many possible types of bugs when outside classes can change a collection that a class is using internally, or that is used by multiple classes. Instead, I would make the collections private and expose functions that indirectly modify them such as addName().

Tenfour04
  • 83,111
  • 11
  • 94
  • 154
  • In my project, I have a `MainActivity`, a `ScanResultAdapter`, and a `Data` class (which I replaced with your solution). I was able to successfully access the `Data` class from the `MainActivity` by passing the `context`, but I was (strangely) not able to access the `saveSharedPreferences` function. I also was not able to access the `Data` class at all from the `ScanResultAdapter` class because it wasn't passing a `context`, but rather a `ScanResultAdapter`. Do you know how to resolve this? – gpeck Aug 31 '22 at 20:15
  • The function is a member of the class, not the companion object, so you have to call it on the Data instance you get back from `getInstance()`. If you want to use it from your adapter class, you will have to add a Context property to your adapter’s constructor so you have a Context available for retrieving your Data instance. – Tenfour04 Aug 31 '22 at 20:51