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!!
CodePudding user response:
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
CodePudding user response:
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 = GlobalScope.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 { viewLifecycleScope.launch {
getSheets().setValues("A1", "this is a test", "USER_ENTERED")
println(getSheets().getValues("A1").values)
} }
load.setOnClickListener { viewLifecycleScope.launch {
println(getSheets())
println(getSheets().getValues("A1"))
} }
return view
}
//...
}