RecycleView не может быть нулевым — когда асинхронная задача загружается на одном фрагменте, а на другом фрагменте

#android #android-fragments #android-viewpager #kotlin-coroutines

#Android #android-фрагменты #android-viewpager #kotlin-сопрограммы

Вопрос:

У меня есть три фрагмента для действия. На одном я загружаю представление по умолчанию, которое должно быть заполнено данными из Календаря Google, но асинхронно -syncwholeCalendar(). В то время как приложение загрузило представление по умолчанию, приложение выходит из строя, когда загружаются данные календаря и пользователь находится в другом фрагменте действия — говоря, что recyclerview не может быть нулевым. То же самое происходит, когда я выхожу из системы, не дожидаясь завершения синхронизации… Проблема заключается в getEvents(), когда вызывается recyclerview для раздувания его при синхронизации… нет проблем, когда я активен на фрагменте…

Есть ли какой-то шаг, который я пропускаю, или подход, который я принимаю неправильно?

 class Home : Fragment() {
companion object {
    @JvmStatic
    fun start(context: Context) {
        val starter = Intent(context, Home::class.java)
        context.startActivity(starter)
    }

    private const val RC_SIGN_IN = 9001
    private val transport = AndroidHttp.newCompatibleTransport()
    private val jsonFactory: JsonFactory = GsonFactory.getDefaultInstance()
}

var usersReference: DatabaseReference? = null
var firebaseUser: FirebaseUser? = null
private var fabExpanded = false

//Calendar
private lateinit var mService: Calendar
private lateinit var calendar: java.util.Calendar
private lateinit var progressBar: ProgressDialog
private lateinit var firestore: FirebaseFirestore
private var isSynced = false
private var isFirstTime = false
private lateinit var googleCredential: GoogleAccountCredential
private var googleSignInAccount: GoogleSignInAccount? = null


override fun onCreateView(
    inflater: LayoutInflater, container: ViewGroup?,
    savedInstanceState: Bundle?
): View? {
    // Inflate the layout for this fragment
    val view: View = inflater.inflate(R.layout.fragment_home, container, false)



firebaseUser = FirebaseAuth.getInstance().currentUser
    val userId = FirebaseAuth.getInstance().currentUser!!.uid
    firestore = FirebaseFirestore.getInstance()


progressBar = ProgressDialog(activity)
    progressBar.setCancelable(false)
    progressBar.setMessage("Loading...")
    calendar = java.util.Calendar.getInstance()
    isFirstTime = this.requireActivity().getSharedPreferences("PRE", MODE_PRIVATE).getBoolean(
        "first",
        true
    )

    usersReference =
        FirebaseDatabase.getInstance().reference.child("Users").child(firebaseUser!!.uid)
    usersReference!!.addValueEventListener(object : ValueEventListener {

        override fun onDataChange(p0: DataSnapshot) {

            if (p0.exists()) {
                val user: Users? = p0.getValue(Users::class.java)

                if (context != null) {
                    view.calEvenName.text = user!!.getFirstName()
                    view.calMornName.text = user.getFirstName()
                }
            }
        }

        override fun onCancelled(p0: DatabaseError) {

        }
    })

    return view
}

private fun showCalendar() {
    val today = java.util.Calendar.getInstance()
    val datePicker = DatePickerDialog(
        requireActivity(), R.style.DateTimePickerTheme,
        { datePicker: DatePicker, year: Int, month: Int, day: Int ->
            calendar.set(year, month, day)
            getCredentials()
        },
        calendar.get(java.util.Calendar.YEAR),
        calendar.get(java.util.Calendar.MONTH),
        calendar.get(
            java.util.Calendar.DAY_OF_MONTH
        )
    )

//        datePicker.datePicker.minDate = today.timeInMillis
        datePicker.show()
    }

//closes FAB submenus
private fun closeSubMenusFab() {
    layoutFabNote.visibility = View.INVISIBLE
    layoutfabEvent?.visibility = View.INVISIBLE
    layoutfabMsg?.visibility = View.INVISIBLE
    fabbackground.setBackgroundColor(Color.TRANSPARENT)
    home_fab.setImageResource(R.drawable.ic_baseline_fab_24)
    fabExpanded = false
}

//Opens FAB submenus
private fun openSubMenusFab() {
    layoutfabEvent?.visibility = View.VISIBLE
    layoutfabMsg?.visibility = View.VISIBLE
    layoutFabNote?.visibility = View.VISIBLE
    fabbackground.setBackgroundColor(Color.parseColor("#E0F7FA"))

    //Change settings icon to 'X' icon
    home_fab.setImageResource(R.drawable.ic_baseline_close_24)
    fabExpanded = true
}

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


    //Only main FAB is visible in the beginning
    closeSubMenusFab()

    //Agenda Calendar Controls/Links
    homeCalendarDate.setOnClickListener { showCalendar() }

    rightCalArrow.setOnClickListener {
        calendar.add(java.util.Calendar.DAY_OF_MONTH, 1)
        getCredentials()
    }

    leftCalArrow.setOnClickListener {
        calendar.add(java.util.Calendar.DAY_OF_MONTH, -1)
        getCredentials()
    }

    //ResetDate Button
    resetIcon.setOnClickListener {
        //   Toast.makeText(activity,"Reset Button Clicked", Toast.LENGTH_LONG).show()
        calendar = java.util.Calendar.getInstance()
        getCredentials()
    }

    //FAB Buttons
    home_fab.setOnClickListener(View.OnClickListener {
        if (fabExpanded) {
            closeSubMenusFab()
        } else {
            openSubMenusFab()
        }
    })

    fabMsg.setOnClickListener {
        val intent = Intent(activity, MyContacts::class.java)
        activity?.startActivity(intent)
    }

    fabEvent.setOnClickListener {
        val intent = Intent(activity, AddEvent::class.java)
        activity?.startActivity(intent)
    }

    fabNote.setOnClickListener {
        val intent = Intent(activity, EditNote::class.java)
        activity?.startActivity(intent)
    }

    //No Event cal display
    homecreateEvent.setOnClickListener {
        val intent = Intent(activity, AddEvent::class.java)
        activity?.startActivity(intent)
    }

    homesendMessage.setOnClickListener {
        val intent = Intent(activity, MyContacts::class.java)
        activity?.startActivity(intent)
    }

}

private fun getCredentials() {
    val date = SimpleDateFormat("E, MMM dd, yyyy", Locale.getDefault()).format(calendar.time)
    homeCalendarDate.text = date
//        progressBar.show()
        googleSignInAccount = GoogleSignIn.getLastSignedInAccount(activity)
        googleCredential = GoogleAccountCredential.usingOAuth2(
            activity,
            listOf(CalendarScopes.CALENDAR)
        )
            .setBackOff(ExponentialBackOff())
            .setSelectedAccountName(googleSignInAccount?.account?.name)

    googleCredential.selectedAccountName = googleSignInAccount?.account?.name
    mService = Calendar.Builder(
        transport, jsonFactory, googleCredential
    )
        .setApplicationName(getString(R.string.app_name))
        .build()

    if (!isSynced) {
        Log.d(TAG, "getCredentials: syncing data")
        if (!isFirstTime) {
            getEvents()
        }
        syncWholeCalendar()
    } else {
        getEvents()
    }
}

private fun getEvents() {
    val noEvents = view?.findViewById<LinearLayout>(R.id.no_events)
    val homeAgenda = view?.findViewById<RecyclerView>(R.id.homeAgenda)
    GlobalScope.launch {
        Log.d(
            TAG,
            "Time current: ${calendar.get(java.util.Calendar.DAY_OF_MONTH)}, ${calendar.get(java.util.Calendar.HOUR_OF_DAY)}, ${
                calendar.get(java.util.Calendar.MINUTE)
            }"
        )
        calendar.set(java.util.Calendar.HOUR_OF_DAY, 0)
        calendar.set(java.util.Calendar.MINUTE, 0)
        calendar.set(java.util.Calendar.SECOND, 0)
        calendar.set(java.util.Calendar.MILLISECOND, 0)

        val db = AppDatabase.getInstance(requireActivity())
        val events = db.getEvents(calendar.timeInMillis)
        val listOfEvents = ArrayList<MyEvent>()
        for (event in events) {
            if (event.isPrimary) {
                listOfEvents.add(event)
            } else {
                listOfEvents.add(0, event)
            }
        }

        Log.d(TAG, "getEvents: ${events.size}")

        requireActivity().runOnUiThread {
            try {
                **val recyclerView: RecyclerView? = homeAgenda
                recyclerView!!.layoutManager = LinearLayoutManager(activity)
                recyclerView.adapter = EventsAdapter(**
                    requireContext(), listOfEvents, calendar.timeInMillis
                )
                if (listOfEvents.size != 0) {
                    noEvents!!.visibility = View.GONE
                } else {
                    noEvents!!.visibility = View.VISIBLE
                }
            } catch (e: UserRecoverableAuthIOException) {
                requireActivity().runOnUiThread { progressBar.dismiss() }
                startActivityForResult(e.intent, RC_SIGN_IN)
            } catch (e: IOException) {
                requireActivity().runOnUiThread { progressBar.dismiss() }
                e.printStackTrace()
            }
        }
    }
}

private fun syncWholeCalendar() {
    GlobalScope.launch {
        try {
            val calendars = mService.CalendarList().list().execute()
            val calendarsList = calendars.items
            for (cal in calendarsList) {
                val events = mService.events().list(cal.id)
                    .setOrderBy("startTime")
                    .setSingleEvents(true)
                    .execute()

                // .setTimeMin(dateFrom)
                val eventsList = events.items
                for (event in eventsList) {
                    event.isLocked = !cal.isPrimary
                    FirebaseFirestore.getInstance().collection("Users")
                        .document(FirebaseAuth.getInstance().currentUser!!.uid)
                        .collection("events")
                        .document(event.id).set(event)
                    var startTimeInMilli: Long = java.util.Calendar.getInstance().timeInMillis
                    if (event.start.dateTime != null) {
                        startTimeInMilli = event.start.dateTime.value
                    } else if (event.start.date != null) {
                        startTimeInMilli = getAbsoluteDate(event.start.date.value)
                    }

                    var endTimeInMilli = java.util.Calendar.getInstance().timeInMillis

                    if (event.end.dateTime != null) {
                        endTimeInMilli = event.end.dateTime.value
                    } else if (event.end.date != null) {
                        endTimeInMilli = getAbsoluteDate(event.start.date.value)
                        Log.d(
                            TAG,
                            "syncWholeCalendar:${event.summary}, ${event.start.date} ${event.end.date}"
                        )
                    }
                    val db = AppDatabase.getInstance(requireActivity())
                    val startTime = getAbsoluteDate(startTimeInMilli)
                    val endTime = getAbsoluteDate(endTimeInMilli)
                    db.insert(
                        MyEvent(
                            event.id,
                            event.summary,
                            event.description,
                            null,
                            null,
                            startTimeInMilli,
                            endTimeInMilli,
                            cal.isPrimary,
                            true,
                            startTime,
                            endTime,
                            event.location,
                            null,
                            null
                        )
                    )
                    Log.d(
                        TAG,
                        "syncWholeCalendar: ${event.start.dateTime}, ${event.start.date}"
                    )
                }
            }
            isSynced = true
            requireActivity().getSharedPreferences("PRE", MODE_PRIVATE)
                .edit().putBoolean("first", false).apply()
            getEvents()

            Log.d(TAG, "syncWholeCalendar: Data Synced Successfully")
        } catch (e: UserRecoverableAuthIOException) {
            requireActivity().runOnUiThread { progressBar.dismiss() }
            startActivityForResult(e.intent, RC_SIGN_IN)
        } catch (e: IOException) {
            requireActivity().runOnUiThread { progressBar.dismiss() }
            e.printStackTrace()
        }
    }
}

override fun onPause() {
    super.onPause()
    closeSubMenusFab()
}

override fun onResume() {
    super.onResume()
    getCredentials()
}
 

}

Ответ №1:

Проблема в GlobalScope том, что . Когда вы удаляетесь от фрагмента / действия, его представления уничтожаются, но GlobalScope ограничивают задачу жизненным циклом приложения, поэтому она не уничтожается, и последующее обращение к любым представлениям приведет к сбою приложения с помощью NullPointerException .

Поэтому вместо этого привяжите фоновые действия к жизненному циклу фрагмента с помощью lifecycleOwner.lifecycleScope или lifecycle.coroutineScope . ознакомьтесь с официальными документами для получения дополнительной информации.

Комментарии:

1. спасибо rcs! в этом так много смысла, есть дополнительный вопрос… в жизненном цикле моя синхронизация будет отменена при переходе к различному фрагменту / действию… таким образом, данные календаря Google не будут импортированы … это не было бы отличным пользовательским интерфейсом, учитывая, что пользователь уже дал разрешение… как мне запустить синхронизацию без активного представления?

2. попробуйте сохранить данные локально, используя базу данных room, пусть recyclerview зависит от локальных данных для отображения содержимого и использует activitylifecycle или область viewmodel, ограниченную действием, для запуска фоновой задачи, поскольку global scope не рекомендуется использовать.