Переменная lateinit не инициализируется в потоке

Итак, я работаю с API-интерфейсом Google Sheets в приложении для Android и пытаюсь получить учетные данные в отдельном потоке. Вот что у меня есть:

GoogleSheets — это класс, который я создал для получения учетных данных и значений ячеек моей электронной таблицы.

private lateinit var sheets: GoogleSheets — это переменная экземпляра, которую я объявляю в начале класса. Я пытаюсь инициализировать здесь:

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

но это говорит мне, что переменная листов не была инициализирована:

kotlin.UninitializedPropertyAccessException: lateinit property sheets has not been initialized

вот полный класс:


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
        }
    }
}

Я не могу понять, почему переменная не инициализируется. Это как-то связано с тем, что он находится в потоке? Как я могу решить эту проблему? Спасибо!!

Отвечает ли это на ваш вопрос? stackoverflow.com/questions/57330766/…

gpunto 16.11.2022 21:15
0
1
91
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Полагаю, ошибка говорит вам, что это происходит, когда вы делаете это:

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

lateinit вы обещаете компилятору, что вы присвоите значение sheets, прежде чем что-либо попытается его прочитать, что вы делаете с println(sheets). Вы назначаете его в том потоке, который вы только что начали, но очень маловероятно, что он завершится до того, как оператор println запустится в текущем потоке!

Вам также нужно беспокоиться о синхронизации, если вы используете такие потоки — только потому, что ваш рабочий поток устанавливает значение sheets, это не означает, что текущий поток увидит это обновленное значение. Вы можете посмотреть здесь, если вы не знакомы со всей этой проблемой и шагами, которые вы должны предпринять, чтобы обеспечить согласованность.

Лучше всего с кодом, который у вас есть, делать свои println вещи внутри потока после того, как sheets назначен. Если вы делаете что-то более сложное и вам нужно вернуться к основному потоку, вы можете postRunnable в представлении. Честно говоря, если вы можете использовать сопрограммы вместо этого, это, вероятно, облегчит вашу жизнь в долгосрочной перспективе.

Ответ принят как подходящий

Вы запускаете другой поток для его инициализации, поэтому, если вы немедленно проверите его, другой поток еще не успел инициализировать свойство. Это неправильное использование lateinit, и вы также не можете использовать синхронизацию потоков, поэтому он подвержен другим ошибкам.

Я предлагаю загрузить лист с сопрограммой и использовать функцию приостановки для извлечения экземпляра, когда вам нужно использовать его где угодно. При использовании только сопрограмм для доступа к свойству вам не нужно беспокоиться о синхронизации потоков.

На самом деле, это должно быть в классе, который переживет фрагмент, чтобы вам не приходилось перезагружать его каждый раз, когда фрагмент воссоздается, но для простоты я просто оставлю его в вашем фрагменте для этого примера.

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
    }

    //...
}

Спасибо за быстрый ответ! Не могли бы вы рассказать мне, что такое viewLifeCycle?

Elliot Scher 17.11.2022 00:19

Он отслеживает события жизненного цикла представления. Экземпляр Fragment может повторно использоваться ОС, поэтому представление уничтожается, а затем создается новое. Даже если он не будет использован повторно, наступит момент, когда он будет уничтожен. Запуская свои сопрограммы в жизненном цикле представления, вы гарантируете, что они будут отменены при уничтожении представления, чтобы они не утекали в память, и вы знаете, что будет безопасно использовать requireView() или requireContext() внутри них. Однако приведенный выше код — это просто быстрый и грязный пример. Вы должны создавать экземпляр GoogleSheets в ViewModel, используя viewModelScope.

Tenfour04 17.11.2022 01:11

Вышеупомянутая проблема заключается в том, что первая сопрограмма, которая получает отложенные GoogleSheets, будет отменена, если первое представление будет уничтожено до его завершения. Затем, если фрагмент будет уничтожен, он не сможет получить экземпляр листов. Если вы сделаете это в viewModelScope ViewModel, он будет гарантированно существовать в течение жизни вашего фрагмента, поскольку ViewModel переживет фрагмент.

Tenfour04 17.11.2022 01:14

Итак, мне нужно получить свой экземпляр листов Google как объект ViewModel и передать его в viewLifeCycle? Извините, если покажусь глупым, я новичок в этом :)

Elliot Scher 17.11.2022 04:29

поэтому я больше не получаю ошибку, что хорошо, но происходит что-то странное. Метод здесь не вызывается: println(getSheets().getValues("A1").toString()) Я убедился, что кнопка нажата, просто метод getValues() просто не возвращает значение. Он просто работает вечно. Это проблема с API листов?

Elliot Scher 17.11.2022 15:45

Это может быть проблема, о которой я говорил. Может быть, getSheets() никогда не вернется. Я не знаком с API Google Sheets, поэтому точно не знаю, как он работает. Как будто я не знаю точно, действительно ли вам нужно создавать экземпляр в фоновом потоке. Я думаю, что я бы создал для него экземпляр singleton. Я могу пересмотреть свой ответ позже сегодня.

Tenfour04 17.11.2022 15:49

Другие вопросы по теме