in my ViewModel:
private val _itemList = mutableStateListOf<Post>()
val itemList: List<Post> = _itemList
fun likePost(newPost: Post){
val index = _itemList.indexOf(newPost)
_itemList[index] = _itemList[index].copy(isLiked = true)
}
Here my Post data class:
data class Post(
val id: Int,
val name: String,
val isLiked: Boolean = false,
)
And here my Composable:
val postList = viewModel.itemList
LazyRow(content = {
items(postList.size) { i ->
val postItem = postList[i]
PostItem(
name = postItem.name,
isLiked = postItem.isLiked,
likePost = { viewModel.likePost(postItem)}
)
}
})
The change does not update in the UI instantly, I first have to scroll the updated item out of the screen so it recomposes or switch to another Screen and go back to see the change.
For some reason it doesn't like updating, it will add and delete and update instantly. You have to do it this way when updating for our to update the state.
fun likePost(newPost: Post){
val index = _itemList.indexOf(newPost)
_itemList[index] = _itemList[index].copy()
_itemList[index].isLiked = true
}
You are returning a List<> effectively and not MutableStateList from your ViewModel.
If you want the list to not be mutable from the view, I happen to use MutableStateFlow<List<>> and return StateFlow<List<>>. You could also just convert it to a list in your composable.
Edit:
//backing cached list, or could be data source like database
private val deviceList = mutableListOf<Device>()
private val _deviceListState = MutableStateFlow<List<Device>>(emptyList())
val deviceListState: StateFlow<List<BluetoothDevice>> = _deviceListState
//manipulate and publish
fun doSomething() {
_deviceListState.value = deviceList.filter ...
}
In your UI
val deviceListState = viewModel.deviceListState.collectAsState().value
For example, I load data into a List, it`s wrapped by MutableStateFlow, and I collect these as State in UI Component.
The trouble is, when I change an item in the MutableStateFlow<List>, such as modifying attribute, but don`t add or delete, the UI will not change.
So how can I change the UI when I modify an item of the MutableStateFlow?
These are codes:
ViewModel:
data class TestBean(val id: Int, var name: String)
class VM: ViewModel() {
val testList = MutableStateFlow<List<TestBean>>(emptyList())
fun createTestData() {
val result = mutableListOf<TestBean>()
(0 .. 10).forEach {
result.add(TestBean(it, it.toString()))
}
testList.value = result
}
fun changeTestData(index: Int) {
// first way to change data
testList.value[index].name = System.currentTimeMillis().toString()
// second way to change data
val p = testList.value[index]
p.name = System.currentTimeMillis().toString()
val tmplist = testList.value.toMutableList()
tmplist[index].name = p.name
testList.update { tmplist }
}
}
UI:
setContent {
LaunchedEffect(key1 = Unit) {
vm.createTestData()
}
Column {
vm.testList.collectAsState().value.forEachIndexed { index, it ->
Text(text = it.name, modifier = Modifier.padding(16.dp).clickable {
vm.changeTestData(index)
Log.d("TAG", "click: ${index}")
})
}
}
}
Both Flow and Compose mutable state cannot track changes made inside of containing objects.
But you can replace an object with an updated object. data class is a nice tool to be used, which will provide you all copy out of the box, but you should emit using var and only use val for your fields to avoid mistakes.
Check out Why is immutability important in functional programming?
testList.value[index] = testList.value[index].copy(name = System.currentTimeMillis().toString())
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")
}
}
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!
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
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!
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 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)
}
}
}
}
}
...
I have a multiple choice quiz with 4 choices per answer. In the ArrayList with the questions and choices, the correct answer is set to the index of the correct option. I want to shuffle the choices but am not sure how to identify the new index of the correct answer. Any thoughts?
Question object
object ConstantsAnalysis {
const val TOTAL_CORRECT: String = "total_correct"
const val TOTAL_OPP: String = "total_opp"
fun getQuestions3(): ArrayList<Questions3> {
val questionList = ArrayList<Questions3>()
val q1 = Questions3(1, null,
"On a graph, the horizontal line along which data are plotted is the _____",
"y axis", "x axis", "origin", "quadrant", 2, R.string.Jones_1995, null)
questionList.addAll(listOf(q1))
questionList.shuffle()
return questionList
}
}
Data class
data class Questions3(
val id: Int, val image: Int?, val question: String, val option1: String, val option2: String,
val option3: String, val option4: String, val correctAnswer: Int, val dialogBox: Int?, val dialogBox2: Int?)
Shuffle choices
val ansorder = arrayOf(question.option1, question.option2, question.option3, question.option4)
ansorder.shuffle()
radio_button1.text = ansorder[0]
radio_button2.text = ansorder[1]
radio_button3.text = ansorder[2]
radio_button4.text = ansorder[3]
Check answer choice
if (questions3!!.correctAnswer != mSelectedOptionPosition) {
//do x
}
Edit (Since correct answer is a string and the index changes after shuffling, answerView(questions3.correctAnswer, R.drawable.correct_option_border.
class QuestionsActivityAnalysis : AppCompatActivity(), View.OnClickListener {
private var mCurrentPosition:Int = 1
private var mQuestionsList:ArrayList<Questions3>? = null
private var mSelectedOptionPosition:Int = 0
private var mCorrectAnswers: Int = 0
private var mSelectedOptionText: String? = null
private fun shuffle() {
val question = mQuestionsList!![mCurrentPosition - 1]
val ansorder = arrayOf(question.option1, question.option2, question.option3, question.option4)
ansorder.shuffle()
radio_button1.text = ansorder[0]
radio_button2.text = ansorder[1]
radio_button3.text = ansorder[2]
radio_button4.text = ansorder[3]
}
override fun onClick(v: View?) {
when(v?.id){
R.id.radio_button1 -> { selectedOptionView(radio_button1, 1)
mSelectedOptionText = radio_button1.text as String?
}
R.id.radio_button2 -> { selectedOptionView(radio_button2, 2)
mSelectedOptionText = radio_button2.text as String?
}
R.id.radio_button3 -> { selectedOptionView(radio_button3, 3)
mSelectedOptionText = radio_button3.text as String?
}
R.id.radio_button4 -> { selectedOptionView(radio_button4, 4)
mSelectedOptionText = radio_button4.text as String?
}
R.id.btn_submit -> {
val questions3 = mQuestionsList?.get(mCurrentPosition - 1)
if (questions3!!.correctAnswer != mSelectedOptionText) {
} else {
mCorrectAnswers++
}
answerView(questions3.correctAnswer, R.drawable.correct_option_border)
private fun answerView(answer: Int, drawableView: Int) {
when(answer){
1 -> {
radio_button1.background = ContextCompat.getDrawable(this, drawableView)
}
2 -> {
radio_button2.background = ContextCompat.getDrawable(this, drawableView)
}
3 -> {
radio_button3.background = ContextCompat.getDrawable(this, drawableView)
}
4 -> {
radio_button4.background = ContextCompat.getDrawable(this, drawableView)
}
}
}
I would really recommend just creating a data class like this:
data class QuestionOption(val question:String, val isCorrect = false)
Afterwards you can shuffle any way you like and just check if the selected QuestionOption has isCorrect set to true. You get a bunch of benefits and the logic gets simpler.
Edit:
To make it easier to declare questions this way:
In general if you add questions in your code you want only as much necessary code as required. For this you can either declare a good constructor or a function that basically maps your values to a constructor. In your case I'd say
data class Questions3(
val id: Int, val question: String, val option1: String, val option2: String,
val option3: String, val correctOption: String, val image: Int?=null,val dialogBox1: Int?=null,val dialogBox2: Int?=null)
(notice how the optional parameters are last, you don't need to specify them as well thanks to them beeing null by default)
Makes sense, in theory you could also (not too clean but easy) just shuffle option 1-3 & correctOption and then just compare if the correctOption String matches the selected String.
Otherwise as I said, you can always create logic for mapping stuff. Here you can either map from Constructor to another Constructor, same with Functions that return a finished Object.
I using android.arch.paging.PagedListAdapter <Item, RecyclerView.ViewHolder> and Room DB as data source for show items in RecyclerView.
Each item has date day. I also group elements by this date in override fun onCurrentListChanged(currentList: PagedList<Item>?).
Grouping occurs by dividing into two lists (Items and Dates) with fake positions. Total count is sum items count and dates count.
My problem is an IndexOutOfBoundsException: Inconsistency detected from RecyclerView when refresh or load next items.
Possible solutions: 1 - refuse to separate items (not suitable for my task). 2 - Use notifyDataSetChanged() after submit items (but disappear animation and appear blink update viewHolders).
Can anyone help?
private var fakePositionDates: Map<Int, Date> = emptyMap()
private var fakePositionTaskPositions: Map<Int, Int> = emptyMap()
override fun getItemCount(): Int {
return fakePositionTaskPositions.size + fakePositionDates.size
}
override fun submitList(pagedList: PagedList<Task>?) {
super.submitList(pagedList)
}
override fun onCurrentListChanged(currentList: PagedList<Task>?) {
val groupTasksPositions = currentList?.asIterable()
?.mapIndexed { index, task ->
Pair(index, task.publishDate)
}
?.groupBy { it.second }
?.mapValues {
it.value.map { it.first }
}
?.toSortedMap(Comparator { o1, o2 ->
o2.compareTo(o1)
})
?: sortedMapOf()
val fakePositionDates = mutableMapOf<Int, Date>()
val fakePositionTaskPositions = mutableMapOf<Int, Int>()
var fakePosition = 0
groupTasksPositions.forEach { date, realPositions ->
fakePositionDates[fakePosition] = date
fakePosition += 1
realPositions.forEach {
fakePositionTaskPositions[fakePosition] = it
fakePosition += 1
}
}
this.fakePositionTaskPositions = fakePositionTaskPositions
this.fakePositionDates = fakePositionDates
}
Try to add adapter.submitList(null) and adapter.notifyDataSetChanged in your activity or fragment before you call view model function to refresh your recyclerview