0

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 tried moving variables around, making "inner" and "outer" variables, one within onCreate and an underscore led variable as the class variable.

  • Changing the viewmodel so that it will wait until the call to the db has finished (I couldn't make this work, but mostly because I wasn't sure how to do it).

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:

  • skipping parts of the program when it's null (not an option)
  • crashing with a null pointer exception (if set to non-nullable)
  • no tasks get assigned to any day, which means no recyclerviews get updated, thus making the app appear to contain no tasks when it does.
  • weekday buttons that don't work because there is no weekdayList for them to compare against to launch the next activity

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")
    }
}
schnondle
  • 121
  • 14
  • 1
    https://stackoverflow.com/questions/37618738/how-to-check-if-a-lateinit-variable-has-been-initialized – YaMiN Aug 04 '21 at 03:17
  • 1
    Theres no guarantee that ```plannerViewModel.allWeekdays.observe(this, { weekdayList = it })``` will run sync and you have your property initialized when you use just after this code, you must move your code that uses the observale var to inside the observer – Marcos Vasconcelos Aug 04 '21 at 04:02
  • 1
    Why do you copy all your binding values to local variables? That serves no purpose. – Tenfour04 Aug 04 '21 at 04:19
  • 1
    This is answered in this question https://stackoverflow.com/a/46584412/4491971 – Mohit Ajwani Aug 04 '21 at 04:36
  • 1
    You don't need to *observe* your viewmodel datas in your activity. Use **Binding Adapters** to bind your data to the RV. It saves your time and makes things easier. https://developer.android.com/topic/libraries/data-binding/binding-adapters – A.David Aug 04 '21 at 08:23
  • @A.David I looked more into this and made a view that used data binding, but it actually doesn't solve my problem. I need the `weekdayList` to be initialized before the `observe` on `taskList`. The reason is because it needs to be able to check which task belongs to which day, it actually has nothing to do with the view until the end, which is the part that works when the `weekdayList` is initialized first – schnondle Aug 05 '21 at 04:12
  • I see, check my answer below – A.David Aug 05 '21 at 08:22

5 Answers5

1

A variable declared as lateinit just means that you are sure that when the object is dereferenced it will not be null. In your case you are calling a method from weekdayList object before it is assigned a value. It is important to understand the concept clearly and why your code works.

Happy Coding!

HaroldSer
  • 2,025
  • 2
  • 12
  • 23
  • Thanks for the answer! I think I understand the concept of `lateinit`. This seems like a synchronization problem to me, right? There's no garuntee that it will be given a value before the second observe is called. How would I fix this? – schnondle Aug 04 '21 at 04:35
  • As the other answer suggests, you can make the variable nullable and then before using it check if it is not null and if it is not then and only then use it. – HaroldSer Aug 04 '21 at 04:40
  • but in that case, the `taskList` can't be organized to the appropriate days, so it would display incorrect information. How would I go about fixing that issue? – schnondle Aug 04 '21 at 05:00
1

You can use the "isInitialized" method, for checking "lateinit" variable is initialized or not.

Please refer the below article for the this-

https://blog.mindorks.com/how-to-check-if-a-lateinit-variable-has-been-initialized

1

lateinit is a way for you to have a var without an initial value when you declare it. It's a nice way to avoid taking something that will never be null, and making it nullable (and having to null check it forever) just so you can temporarily set it to null as a placeholder that nothing will ever see.

What you're doing is promising the compiler "ok, I'm not going to provide a value when the class is constructed, but I promise I'll set it to something before anything tries to read it". You're telling the compiler to trust you, that you know how your code works, and you can guarantee it'll all be ok.

Your problem is that it seems you can't guarantee that things won't try to read that property before you write to it. Your state can either be "has a value", or "doesn't have a value", and the rest of your code could encounter either state.

The "no value" state is basically null, so you should probably make the variable nullable instead, and initialise it as null. Kotlin has all that nice null-safety stuff to help your code handle it, until you do get a value. lateinit seems like the wrong tool for the job, even if you check ::isInitialized it's just making your life a lot harder when the null-checking stuff is right there!

cactustictacs
  • 17,935
  • 2
  • 14
  • 25
  • Thank you for this description of `lateinit` and nullables/null checking, it's very clear. The problem I'm having currently isn't that I'm unclear about this concept anymore, it's that so much of the code depends on that one variable being initialized that it isn't an option for it to not be. If `weekdayList` is `null`, none of the `recyclerview`s get set, making the app virtually useless. I don't need skipping sections with null checks, I need guarantee that the variable is set before it's needed – schnondle Aug 05 '21 at 03:45
  • 1
    But you can't guarantee that, right? You're trying to read it before it's been set, which happens in that observer lambda. That's going to happen later, and you have no control over when that will happen, or the order that your observers will fire (or at least, that's a complex thing to reason about, and you shouldn't be relying on it). What you should do instead, is make your code flow more *reactive*. When you get the `weekdayList` value in that observer, make *that* call some update function that populates the recycler. You can call the same from your `allTasks` observer – cactustictacs Aug 05 '21 at 18:31
  • 1
    The general idea is your observers fire with some new data, and you go "oh ok, time to do an update". But if you're not ready to update, because you're relying on some other data (like `weekdayList` or `allTasks`) that you don't have yet, you store what you have, so it's there for the next update attempt. When an observer gets a new value and you have all the pieces you need, *then* you can do an update and populate the recyclerview. You need to think in an asynchronous style, where stuff happens at various arbitrary times, instead of the usual way where your code fires in a predictable order – cactustictacs Aug 05 '21 at 18:35
  • Thank you! I think this was the answer I needed. Updating the original question with corrected code in just a moment, let me know if it looks more like what you are talking about (more importantly, it seems to work without running unnecessary things) – schnondle Aug 06 '21 at 05:26
  • I posted my answer, but if you want the points you can copy and post it and I'll accept yours and delete mine. Also let me know if I need to update it at all. I appreciate your help! – schnondle Aug 06 '21 at 05:35
  • 1
    @schnondle no worries mate, glad you got the idea! – cactustictacs Aug 06 '21 at 19:21
0

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

...

schnondle
  • 121
  • 14
0

Use lazy properties , refer to this doc for more informations:

assuming weekDayList is the property you want to successfully initialize ->

private var weekDayList: List<WeekDay> by lazy {
    //return your first value
    listOf<WeekDay>()
}

Here is a useful link about LifeCycleAware Lazy properties: blog Although, it is not required.

A.David
  • 172
  • 9