androidandroid-studiokotlinviewmodelkotlin-lateinit

How do I ensure lateinit variable initialization before it is needed?


I have an app that launches the majority of the time, but every 7 or so launches it crashes with the error:

kotlin.UninitializedPropertyAccessException: lateinit property weekdayList has not been initialized

This is a clear error, I'm just not sure how to ensure the variable is initialized early enough in the context of my app.

Things I have tried

I think the problem is in the onCreate function, and that the weekday observe isn't setting the value of the variable faster than the task observe (where the weekdayList variable is needed) is called?


Edit 1

I referenced this but I end up with a similar error

java.lang.IndexOutOfBoundsException: Empty list doesn't contain element at index 1.


Edit 2

I understand how lateinit variables and nullables work at this point, I want to try and clarify this a little better.

The variable weekdayList needs to be initialized to the correct list before I hit the observe for the taskList, otherwise the app will crash.

I have tried setting the variable to be nullable, and Iend up with:

My problem doesn't stand in figuring out if it's null or not, it's trying to guarantee that it won't be null.

Sorry for the confusion


Main Activity

class MainActivity : AppCompatActivity() {
    private lateinit var binding: ActivityMainBinding
    private val plannerViewModel: PlannerViewModel by viewModels {
        PlannerViewModelFactory((application as PlannerApplication).repository)
    }

    private var weekdayList: List<Weekday> = listOf()
    private var taskList: List<Task> = listOf()
    private var taskDayList = mutableListOf<Task>()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        val clearButtonText = binding.clearCardText
        val sundayButtonText = binding.sundayCardText
        val mondayButtonText = binding.mondayCardText
        val tuesdayButtonText = binding.tuesdayCardText
        val wednesdayButtonText = binding.wednesdayCardText
        val thursdayButtonText = binding.thursdayCardText
        val fridayButtonText = binding.fridayCardText
        val saturdayButtonText = binding.saturdayCardText
        val sundayRv: RecyclerView = binding.sundayRv
        val sundayAdapter = TaskRvAdapter(null)
        sundayRv.adapter = sundayAdapter
        sundayRv.layoutManager = LinearLayoutManager(this)
        val mondayRv: RecyclerView = binding.mondayRv
        val mondayAdapter = TaskRvAdapter(null)
        mondayRv.adapter = mondayAdapter
        mondayRv.layoutManager = LinearLayoutManager(this)
        val tuesdayRv: RecyclerView = binding.tuesdayRv
        val tuesdayAdapter = TaskRvAdapter(null)
        tuesdayRv.adapter = tuesdayAdapter
        tuesdayRv.layoutManager = LinearLayoutManager(this)
        val wednesdayRv: RecyclerView = binding.wednesdayRv
        val wednesdayAdapter = TaskRvAdapter(null)
        wednesdayRv.adapter = wednesdayAdapter
        wednesdayRv.layoutManager = LinearLayoutManager(this)
        val thursdayRv: RecyclerView = binding.thursdayRv
        val thursdayAdapter = TaskRvAdapter(null)
        thursdayRv.adapter = thursdayAdapter
        thursdayRv.layoutManager = LinearLayoutManager(this)
        val fridayRv: RecyclerView = binding.fridayRv
        val fridayAdapter = TaskRvAdapter(null)
        fridayRv.adapter = fridayAdapter
        fridayRv.layoutManager = LinearLayoutManager(this)
        val saturdayRv: RecyclerView = binding.saturdayRv
        val saturdayAdapter = TaskRvAdapter(null)
        saturdayRv.adapter = saturdayAdapter
        saturdayRv.layoutManager = LinearLayoutManager(this)


        // Setting day card names
        clearButtonText.text = "Clear"
        sundayButtonText.text = "Sun"
        mondayButtonText.text = "Mon"
        tuesdayButtonText.text = "Tue"
        wednesdayButtonText.text = "Wed"
        thursdayButtonText.text = "Thu"
        fridayButtonText.text = "Fri"
        saturdayButtonText.text = "Sat"
        sundayButtonText.text = "Sun"

        plannerViewModel.allWeekdays.observe(this, {
            weekdayList = it
        })

        plannerViewModel.allTasks.observe(this, { tasks ->
            taskList = tasks
            taskDayList = mutableListOf()

            for (i in 1..7) {

                taskDayList = sortTasks(weekdayList[i], taskList)

                when (i) {
                    1 -> {
                        sundayAdapter.submitList(taskDayList)
                        toggleVisibility(taskDayList, binding.sundayInner,
                                binding.sundayCardText, sundayRv, binding.sundayNoTasks)
                    }
                    2 -> {
                        mondayAdapter.submitList(taskDayList)
                        toggleVisibility(taskDayList, binding.mondayInner,
                                binding.mondayCardText, mondayRv, binding.mondayNoTasks)
                    }
                    3 -> {
                        tuesdayAdapter.submitList(taskDayList)
                        toggleVisibility(taskDayList, binding.tuesdayInner,
                                binding.tuesdayCardText, tuesdayRv, binding.tuesdayNoTasks)
                    }
                    4 -> {
                        wednesdayAdapter.submitList(taskDayList)
                        toggleVisibility(taskDayList, binding.wednesdayInner,
                                binding.wednesdayCardText, wednesdayRv, binding.wednesdayNoTasks)
                    }
                    5 -> {
                        thursdayAdapter.submitList(taskDayList)
                        toggleVisibility(taskDayList, binding.thursdayInner,
                                binding.thursdayCardText, thursdayRv, binding.thursdayNoTasks)
                    }
                    6 -> {
                        fridayAdapter.submitList(taskDayList)
                        toggleVisibility(taskDayList, binding.fridayInner,
                                binding.fridayCardText, fridayRv, binding.fridayNoTasks)
                    }
                    7 -> {
                        saturdayAdapter.submitList(taskDayList)
                        toggleVisibility(taskDayList, binding.saturdayInner,
                                binding.saturdayCardText, saturdayRv, binding.saturdayNoTasks)
                    }
                }
            }
        })
    }

    private fun toggleVisibility(taskDayList: List<Task>, inner: ConstraintLayout,
                                 cardText: View, rv: RecyclerView, noTask: View) {
        if (taskDayList.count() == 0 ) {
            val newConstraintSet = ConstraintSet()
            newConstraintSet.clone(inner)
            newConstraintSet.connect(noTask.id, ConstraintSet.TOP,
                    cardText.id, ConstraintSet.BOTTOM)
            newConstraintSet.applyTo(inner)

            newConstraintSet.connect(cardText.id, ConstraintSet.BOTTOM,
                    noTask.id, ConstraintSet.TOP)
            newConstraintSet.applyTo(inner)

            rv.visibility = View.GONE
            noTask.visibility = View.VISIBLE

            Log.i("this", "ran zero")
        } else {
            val newConstraintSet = ConstraintSet()
            newConstraintSet.clone(inner)
            newConstraintSet.connect(rv.id, ConstraintSet.TOP,
                    cardText.id, ConstraintSet.BOTTOM)
            newConstraintSet.applyTo(inner)

            newConstraintSet.connect(cardText.id, ConstraintSet.BOTTOM,
                    rv.id, ConstraintSet.TOP)
            newConstraintSet.applyTo(inner)

            rv.visibility = View.VISIBLE
            noTask.visibility = View.GONE

            Log.i("this", "ran else")
        }
    }

    private fun sortTasks(day: Weekday, tasks: List<Task>): MutableList<Task> {
        val newAdapterList = mutableListOf<Task>()

        tasks.forEach {
            if (it.weekdayId == day.id) {
                newAdapterList.add(it)
            }
        }

        return newAdapterList
    }

    private fun startWeekdayActivity(day: Weekday) {
        val intent = Intent(this, WeekdayActivity::class.java)
        intent.putExtra("dayId", day.id)
        this.startActivity(intent)
    }

    private fun clearDb(taskList: List<Task>) {
        val alertDialog: AlertDialog = this.let { outerIt ->
            val builder = AlertDialog.Builder(outerIt)
            builder.apply {
                setPositiveButton("Clear",
                        DialogInterface.OnClickListener { dialog, id ->
                            if (taskList.count() == 0) {
                                Toast.makeText(context, "No tasks to clear", Toast.LENGTH_SHORT).show()
                            } else {
                                plannerViewModel.deleteAllTasks()
                                Toast.makeText(context, "Tasks cleared", Toast.LENGTH_SHORT).show()
                            }
                        })
                setNegativeButton("Cancel",
                        DialogInterface.OnClickListener { dialog, id ->
                            // User cancelled the dialog
                        })
            }
                    .setTitle("Clear tasks?")
                    .setMessage("Are you sure you want to clear the weeks tasks?")

            builder.create()
        }

        alertDialog.show()
    }

    private fun checkDay(dayIn: String, weekdayList: List<Weekday>) {
        weekdayList.forEach {
            if (dayIn == "clear_card" && it.day == "Clear") {
                clearDb(taskList)
            } else {
                val dayInAbr = dayIn.substring(0, 3).toLowerCase(Locale.ROOT)
                val dayOutAbr = it.day.substring(0, 3).toLowerCase(Locale.ROOT)

                if (dayInAbr == dayOutAbr) {
                    startWeekdayActivity(it)
                }
            }
        }
    }

    fun buttonClick(view: View) {
        when (view.id) {
            R.id.clear_card -> checkDay(view.context.resources.getResourceEntryName(R.id.clear_card).toString(), weekdayList)
            R.id.sunday_card -> checkDay(view.context.resources.getResourceEntryName(R.id.sunday_card).toString(), weekdayList)
            R.id.monday_card -> checkDay(view.context.resources.getResourceEntryName(R.id.monday_card).toString(), weekdayList)
            R.id.tuesday_card -> checkDay(view.context.resources.getResourceEntryName(R.id.tuesday_card).toString(), weekdayList)
            R.id.wednesday_card -> checkDay(view.context.resources.getResourceEntryName(R.id.wednesday_card).toString(), weekdayList)
            R.id.thursday_card -> checkDay(view.context.resources.getResourceEntryName(R.id.thursday_card).toString(), weekdayList)
            R.id.friday_card -> checkDay(view.context.resources.getResourceEntryName(R.id.friday_card).toString(), weekdayList)
            R.id.saturday_card -> checkDay(view.context.resources.getResourceEntryName(R.id.saturday_card).toString(), weekdayList)
        }
    }
}

Viewmodel

class PlannerViewModel(private val repository: DbRepository) : ViewModel() {
    val allWeekdays: LiveData<List<Weekday>> = repository.allWeekdays.asLiveData()
    val allTasks: LiveData<List<Task>> = repository.allTasks.asLiveData()

    fun insertWeekday(weekday: Weekday) = viewModelScope.launch {
        repository.insertWeekday(weekday)
    }

    fun insertTask(task: Task) = viewModelScope.launch {
        repository.insertTask(task)
    }

    fun deleteTask(task: Task) = viewModelScope.launch {
        repository.deleteTask(task)
    }

    fun deleteAllTasks() = viewModelScope.launch {
        repository.deleteAllTasks()
    }
}

class PlannerViewModelFactory(private val repository: DbRepository) : ViewModelProvider.Factory {
    override fun <T : ViewModel?> create(modelClass: Class<T>): T {
        if (modelClass.isAssignableFrom(PlannerViewModel::class.java)) {
            @Suppress("UNCHECKED_CAST")
            return PlannerViewModel(repository) as T
        }
        throw IllegalArgumentException("Unknown ViewModel class")
    }
}

Solution

  • A solution with help from cactustictacs in the comments.

    I moved a lot of the list dependency to a new function called setAdapterList. this allows both observes to run the function, and only the one with both lists initialized will run the code contained. I kept the variables lateinit and it seems to be working so far!

    The Main Change in Main Activity

    ...
    
    private fun setAdapterLists(adapterList: List<TaskRvAdapter>, rvList: List<RecyclerView>) {
            if (this::weekdayList.isInitialized  && this::taskList.isInitialized) {
                adapterList.forEach {
                    taskDayList = mutableListOf()
                    val i = adapterList.indexOf(it)
                    taskDayList = sortTasks(weekdayList[i + 1], taskList)
    
                    Log.i("rvli", rvList[i].toString())
    
                    when (i) {
                        0 -> {
                            adapterList[i].submitList(taskDayList)
                            toggleVisibility(taskDayList, binding.sundayInner,
                                    binding.sundayCardText, rvList[i], binding.sundayNoTasks)
                        }
                        1 -> {
                            adapterList[i].submitList(taskDayList)
                            toggleVisibility(taskDayList, binding.mondayInner,
                                    binding.mondayCardText, rvList[i], binding.mondayNoTasks)
                        }
                        2 -> {
                            adapterList[i].submitList(taskDayList)
                            toggleVisibility(taskDayList, binding.tuesdayInner,
                                    binding.tuesdayCardText, rvList[i], binding.tuesdayNoTasks)
                        }
                        3 -> {
                            adapterList[i].submitList(taskDayList)
                            toggleVisibility(taskDayList, binding.wednesdayInner,
                                    binding.wednesdayCardText, rvList[i], binding.wednesdayNoTasks)
                        }
                        4 -> {
                            adapterList[i].submitList(taskDayList)
                            toggleVisibility(taskDayList, binding.thursdayInner,
                                    binding.thursdayCardText, rvList[i], binding.thursdayNoTasks)
                        }
                        5 -> {
                            adapterList[i].submitList(taskDayList)
                            toggleVisibility(taskDayList, binding.fridayInner,
                                    binding.fridayCardText, rvList[i], binding.fridayNoTasks)
                        }
                        6 -> {
                            adapterList[i].submitList(taskDayList)
                            toggleVisibility(taskDayList, binding.saturdayInner,
                                    binding.saturdayCardText, rvList[i], binding.saturdayNoTasks)
                        }
                    }
                }
            }
        }
        
    
    ...