0

So I'm working with the google sheets api in an android app and I'm trying to get the credentials in a separate thread. This is what I have:

GoogleSheets is a class I created to get credentials and cell values of my spreadsheet

private lateinit var sheets: GoogleSheets is a instance variable that I declare at the beginning of the class. I am trying to initialize here:

load.setOnClickListener(View.OnClickListener {
            Thread {
                sheets = GoogleSheets(requireContext(), "1fs1U9-LMmkmQbQ2Kn-rNVHIQwh6_frAbwaTp7MSyDIA")
            }.start()
            println(sheets)
            println(sheets.getValues("A1"))
        })

but It's telling me that the sheets variable hasn't been initialized:

kotlin.UninitializedPropertyAccessException: lateinit property sheets has not been initialized

here is the full class:


import android.Manifest
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.Environment
import android.provider.Settings
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import android.widget.EditText
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.RequiresApi
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import com.example.frcscout22.GoogleSheets
import com.example.frcscout22.R


// TODO: AUTOMATICALLY SWITCH TO DATA TAB AFTER LOAD OR CREATE NEW

class Home: Fragment(R.layout.fragment_home) {
    private lateinit var sheets: GoogleSheets
    private val STORAGE_PERMISSION_CODE = 100

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
    }

    @RequiresApi(Build.VERSION_CODES.P)
    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        val view: View = inflater.inflate(R.layout.fragment_home, container, false)

        val load = view.findViewById<Button>(R.id.button3)
        val new = view.findViewById<Button>(R.id.button4)
        val editText = view.findViewById<EditText>(R.id.editTextTextPersonName)

        if (!checkPermission()) {
            println("requested")
            requestPermission()
        }

        new.setOnClickListener(View.OnClickListener {
            val sheets = GoogleSheets(requireContext(),"1fs1U9-LMmkmQbQ2Kn-rNVHIQwh6_frAbwaTp7MSyDIA")
            sheets.setValues("A1", "this is a test", "USER_ENTERED")
            println(sheets.getValues("A1").values)
        })

        load.setOnClickListener(View.OnClickListener {
            Thread {
                sheets = GoogleSheets(requireContext(), "1fs1U9-LMmkmQbQ2Kn-rNVHIQwh6_frAbwaTp7MSyDIA")
            }.start()
            println(sheets)
            println(sheets.getValues("A1"))
        })
        return view
    }

    private fun requestPermission(){
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R){
            //Android is 11(R) or above
            try {
                val intent = Intent()
                intent.action = Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION
                val uri = Uri.fromParts("package", requireActivity().packageName, "Home")
                intent.data = uri
                storageActivityResultLauncher.launch(intent)
            }
            catch (e: Exception){
                val intent = Intent()
                intent.action = Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION
                storageActivityResultLauncher.launch(intent)
            }
        }
        else{
            //Android is below 11(R)
            ActivityCompat.requestPermissions(requireActivity(),
                arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE),
                STORAGE_PERMISSION_CODE
            )
        }
    }

    private val storageActivityResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()){
        //here we will handle the result of our intent
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R){
            //Android is 11(R) or above
            if (Environment.isExternalStorageManager()){
                //Manage External Storage Permission is granted
            }
        }
        else{
            //Android is below 11(R)
        }
    }

    private fun checkPermission(): Boolean{
        return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R){
            //Android is 11(R) or above
            Environment.isExternalStorageManager()
        }
        else{
            //Android is below 11(R)
            val write = ContextCompat.checkSelfPermission(requireContext(), Manifest.permission.WRITE_EXTERNAL_STORAGE)
            val read = ContextCompat.checkSelfPermission(requireContext(), Manifest.permission.READ_EXTERNAL_STORAGE)
            write == PackageManager.PERMISSION_GRANTED && read == PackageManager.PERMISSION_GRANTED
        }
    }
}

I can't figure out why the varible isn't being initialized. Does it have something to do with it being in a thread? How can I fix this problem? Thanks!!

  • Does this answer your question? https://stackoverflow.com/questions/57330766/why-does-my-function-that-calls-an-api-or-launches-a-coroutine-return-an-empty-o – gpunto Nov 16 '22 at 20:15

2 Answers2

1

At a guess the error's telling you it's happening when you do this:

Thread {
    sheets = GoogleSheets(requireContext(), "1fs1U9-LMmkmQbQ2Kn-rNVHIQwh6_frAbwaTp7MSyDIA")
}.start()
println(sheets)

lateinit is you promising the compiler you'll have assigned a value to sheets before anything tries to read it, which you're doing with println(sheets). You're assigning it in that thread you just started - but that's very unlikely to have completed before the println statement runs on the current thread!

You also have to worry about synchronisation if you're involving threading like this - just because your worker thread sets the value of sheets, it doesn't mean the current thread will see that updated value. You can have a look here if you're not familiar with that whole issue and the steps you have to take to ensure things stay consistent.

Your best bet with the code you have is to do your println stuff inside the thread after sheets is assigned. If you do anything more complicated than that, and need to get back on the main thread, you can post a Runnable on a view. Honestly, if you can use coroutines instead that would probably make your life easier in the long run

cactustictacs
  • 17,935
  • 2
  • 14
  • 25
1

You're starting another thread to initialize it, so if you check for it immediately, the other thread hasn't had time to initialize the property yet. This is a misuse of lateinit and you are also failing to utilize thread synchronization, so it is susceptible to other bugs.

I suggest loading the sheet with a coroutine and using a suspend function to retrieve the instance when you need to use it anywhere. When using only coroutines to access the property, you don't need to worry about thread synchronization.

Really, this should go in a class that outlives the Fragment so you don't have to reload it every time the Fragment is recreated, but for simplicity, I'll just keep it in your Fragment for this example.

class Home: Fragment(R.layout.fragment_home) {
    private val loadSheetsDeferred = viewLifecycle.lifecycleScope.async(Dispatchers.IO) {
        GoogleSheets(requireContext(), "1fs1U9-LMmkmQbQ2Kn-rNVHIQwh6_frAbwaTp7MSyDIA")
    }

    private suspend fun getSheets(): GoogleSheets = loadSheetsDeferred.await()

    private val STORAGE_PERMISSION_CODE = 100

    @RequiresApi(Build.VERSION_CODES.P)
    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        //...

        new.setOnClickListener { viewLifecycle.lifecycleScope.launch {
            getSheets().setValues("A1", "this is a test", "USER_ENTERED")
            println(getSheets().getValues("A1").values)
        } }

        load.setOnClickListener { viewLifecycle.lifecycleScope.launch {
            println(getSheets())
            println(getSheets().getValues("A1"))
        } }

        return view
    }

    //...
}
Tenfour04
  • 83,111
  • 11
  • 94
  • 154
  • Thanks for the fast response! Could you tell me what viewLifeCycle is? –  Nov 16 '22 at 23:19
  • It keeps track of view lifecycle events. A Fragment instance might be reused by the OS, so the view is destroyed and then a new one is created. Even if it is not reused, there will be a moment when it is destroyed. By launching your coroutines in the view lifecycle, you ensure that they will be cancelled when the view is destroyed so they don't leak memory, and you know it will be safe to use `requireView()` or `requireContext()` inside of them. The code above is just a quick and dirty example though. You should be creating the GoogleSheets instance in a ViewModel using `viewModelScope`. – Tenfour04 Nov 17 '22 at 00:11
  • The problem above is that the first coroutine that gets the deferred GoogleSheets will be cancelled if the first view is destroyed before it finishes. Then if the Fragment is destroyed, it will not be able to retrieve the sheets instance. If you do this in a ViewModel's `viewModelScope` it will be guaranteed to exist during your fragment's life since a ViewModel outlives a Fragment. – Tenfour04 Nov 17 '22 at 00:14
  • So I need to get my instance of Google sheets as a ViewModel object and pass that in viewLifeCycle? Sorry if I sound dumb, I'm new to this :) –  Nov 17 '22 at 03:29
  • so I'm not getting the error anymore which is good but something weird is happening. The method here isn't being called: ```println(getSheets().getValues("A1").toString())``` I have made sure that the button is being pressed, it's just that the getValues() method just won't return a value. It just stays running forever. Is that a problem with the sheets api? –  Nov 17 '22 at 14:45
  • This could be the issue I was talking about. Maybe `getSheets()` is never returning. I have zero familiarity with the GoogleSheets API, so I don't know exactly how it works. Like I don't know for sure if you really need to instantiate it on a background thread. I think I would create a singleton instance for it. I can revise my answer later today. – Tenfour04 Nov 17 '22 at 14:49