Can you change the color of a textview in a recyclerview adapter after a certain condition is met in Main Activity? - kotlin

I have a basic function that displays the elapsed time every time the button is pressed. I cannot get the logic in MainActivity to transfer to the recyclerview adapter. I simply want the text output color to change to red after the time passes 5 seconds. I have tried to research how to do this for the past week and I cannot find the exact answer. I'm hoping someone can help.
I have tried it with and without the boolean in the data class. I wasn't sure if that was required.
Here is my code:
Main Activity:`
class MainActivity : AppCompatActivity() {
var startTime = SystemClock.elapsedRealtime()
var displaySeconds = 0
private lateinit var binding: ActivityMainBinding
private val secondsList = generateSecondsList()
private val secondsAdapter = Adapter(secondsList)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
recyclerView.adapter = secondsAdapter
recyclerView.layoutManager = LinearLayoutManager(this)
recyclerView.setHasFixedSize(false)
binding.button.setOnClickListener {
getDuration()
addSecondsToRecyclerView()
}
}
fun getDuration(): Int {
val endTime = SystemClock.elapsedRealtime()
val elapsedMilliSeconds: Long = endTime - startTime
val elapsedSeconds = elapsedMilliSeconds / 1000.0
displaySeconds = elapsedSeconds.toInt()
return displaySeconds
}
private fun generateSecondsList(): ArrayList<Seconds> {
return ArrayList()
}
fun addSecondsToRecyclerView() {
val addSeconds =
Seconds(getDuration(), true)
secondsList.add(addSeconds)
secondsAdapter.notifyItemInserted(secondsList.size - 1)
}
}
Adapter:
var adapterSeconds = MainActivity().getDuration()
class Adapter(
private val rvDisplay: MutableList<Seconds>
) : RecyclerView.Adapter<Adapter.AdapterViewHolder>() {
class AdapterViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val textView1: TextView = itemView.tv_seconds
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AdapterViewHolder {
val myItemView = LayoutInflater.from(parent.context).inflate(
R.layout.rv_item,
parent, false
)
return AdapterViewHolder(myItemView)
}
override fun onBindViewHolder(holder: Adapter.AdapterViewHolder, position: Int) {
val currentDisplay = rvDisplay[position]
currentDisplay.isRed = adapterSeconds > 5
holder.itemView.apply {
val redColor = ContextCompat.getColor(context, R.color.red).toString()
val blackColor = ContextCompat.getColor(context, R.color.black).toString()
if (currentDisplay.isRed) {
holder.textView1.setTextColor(redColor.toInt())
holder.textView1.text = currentDisplay.rvSeconds.toString()
} else {
holder.textView1.setTextColor(blackColor.toInt())
holder.textView1.text = currentDisplay.rvSeconds.toString()
}
}
}
override fun getItemCount() = rvDisplay.size
}
Data Class:
data class Seconds(
var rvSeconds: Int,
var isRed: Boolean
)

when you call secondsList.add(addSeconds) then the data that is already inside secondsList should be updated too.
you could do something like
private var secondsList = generateSecondsList() // make this var
fun addSecondsToRecyclerView() {
val addSeconds =
Seconds(getDuration(), true)
secondsList.add(addSeconds)
if ( /* TODO check if time has passed */) {
secondsList = secondsList.map { it.isRed = true }
secondsAdapter.rvDisplay = secondsList // TODO also make rvDisplay a var
secondsAdapter.notifyDatasetChanged() // also need to tell rv to redraw the all views
} else {
secondsAdapter.notifyItemInserted(secondsList.size - 1)
}
}
that might work, but to be honest it looks bad... There is already a lot of logic inside Activity. Read about MVVM architecture and LiveData, there should be another class called ViewModel that would keep track of time and the data. Activity should be as simple as possible, because it has lifecycle, so if you rotate the screen, all your state will be lost.

Your code isn't really working because of this:
var adapterSeconds = MainActivity().getDuration()
override fun onBindViewHolder(holder: Adapter.AdapterViewHolder, position: Int) {
...
currentDisplay.isRed = adapterSeconds > 5
...
}
You're only setting adapterSeconds right there, so it never updates as time passes. I assume you want to know the moment 5 seconds has elapsed, and then update the RecyclerView at that moment - in that case you'll need some kind of timer task that will fire after 5 seconds, and can tell the adapter to display things as red. Let's deal with that first:
class Adapter( private val rvDisplay: MutableList ) : RecyclerView.Adapter<Adapter.AdapterViewHolder>() {
private var displayRed = false
set(value) {
field = value
// Refresh the display - the ItemChanged methods mean something about the items
// has changed, rather than a structural change in the list
// But you can use notifyDataSetChanged if you want (better to be specific though)
notifyItemRangeChanged(0, itemCount)
}
override fun onBindViewHolder(holder: Adapter.AdapterViewHolder, position: Int) {
if (displayRed) {
// show things as red - you shouldn't need to store that state in the items
// themselves, it's not about them - it's an overall display state, right?
} else {
// display as not red
}
}
So with that setter function, every time you update displayRed it'll refresh the display, which calls onBindViewHolder, which checks displayRed to see how to style things. It's better to put all this internal refreshing stuff inside the adapter - just pass it data and events, let it worry about what needs to happen internally and to the RecyclerView it's managing, y'know?
Now we have a thing we can set to control how the list looks, you just need a timer to change it. Lots of ways to do this - a CountdownTimer, a coroutine, but let's keep things simple for this example and just post a task to the thread's Looper. We can do that through any View instead of creating a Handler:
// in MainActivity
recyclerView.postDelayed({ secondsAdapter.displayRed = true }, 5000)
That's it! Using any view, post a delayed function that tells the adapter to display as red.
It might be more helpful to store that runnable as an object:
private val showRedTask = Runnable { secondsAdapter.displayRed = true }
...
recyclerView.postDelayed(showRedTask, 5000)
because then you can easily cancel it
recyclerView.removeCallbacks(showRedTask)
Hopefully that's enough for you to put some logic together to get what you want. Set displayRed = false to reset the styling, use removeCallbacks to cancel any running task, and postDelayed to start a new countdown. Not the only way to do it, but it's pretty neat!

I finally figured it out using a companion object in Main Activity with a boolean set to false. If the time exceeded 5 seconds, then it set to true.
The adapter was able to recognize the companion object and change the color of seconds to red if they exceeded 5.

Related

CoroutineScope is skipped

For some reasom my CoroutineScope just stopped working, though, nothing crucial has been changed and errors don't show up. The program skips it and my progress bar remains unchanged. Do you have any idea what could have possibly caused this issue?
class HomeFragment : BaseFragment(R.layout.fragment_home) {
private var todayEventsCount: Int = 0
private var todayCheckedEventsCount: Int = 0
private val mHandler = Handler()
private var mAdapter: FirebaseRecyclerAdapter<EventModel, EventsHolder>? = null
private lateinit var mRunnable: Runnable
private lateinit var barList: ArrayList<BarEntry>
private lateinit var barDataSet: BarDataSet
private lateinit var barData: BarData
override fun onResume() {
super.onResume()
initFields()
}
override fun onPause() {
super.onPause()
home_events_list.removeAllViewsInLayout()
todayCheckedEventsCount = 0
}
#SuppressLint("SetTextI18n")
private fun initFields() {
val progress: ProgressBar = ev_progress_bar
val text: TextView = count
getTodayEvents()
CoroutineScope(Dispatchers.IO).launch {
mRunnable = Runnable {
if (mAdapter?.itemCount != 0) {
todayEventsCount = mAdapter?.itemCount!!
for (i in 0 until todayEventsCount) {
if (mAdapter?.getItem(i)!!.checked == "1") {
todayCheckedEventsCount++
progress.progress = todayCheckedEventsCount
text.text = progress.progress.toString() + "/" + progress.max.toString()
}
}
todayCheckedEventsCount = 0
}
mHandler.postDelayed(mRunnable, 30)
}
mHandler.post(mRunnable)
}
initChart()
}
There's no point in posting runnables if you're using a coroutine. Half the reason coroutines exist is to avoid this messy nested juggling of callbacks. There are also a few errors I see in your coroutine:
It runs on Dispatchers.IO, even though it never does anything blocking.
It runs on a throwaway CoroutineScope that is never cancelled. You should never use the CoroutineScope() constructor if you're not immediately assigning it to a variable or property through which you can cancel it at the appropriate time. You should be using viewLifeCycleOwner.lifecycleScope instead, since it's already set up to automatically cancel at the appropriate time to avoid crashes.
Your runnables you're posting are not canceled at the appropriate time, so even if you use the appropriate scope, they can still cause crashes. But as mentioned above, you don't need them in a coroutine.
(mAdapter?.itemCount != 0) will evaluate to true even if mAdapter is null, which will promptly cause a crash when you use mAdapter!!.
Fixed code looks like this. If this still doesn't do anything, you'll want to check to make sure your adapter is not null at the time this is called.
private fun initFields() {
val progress: ProgressBar = ev_progress_bar
val text: TextView = count
getTodayEvents()
viewLifeCycleOwner.lifecycleScope.launch {
val adapter = mAdapter ?: return
while (true) {
if (adapter.itemCount != 0) {
todayEventsCount = adapter.itemCount
for (i in 0 until todayEventsCount) {
if (adapter.getItem(i).checked == "1") {
todayCheckedEventsCount++
progress.progress = todayCheckedEventsCount
text.text = progress.progress.toString() + "/" + progress.max.toString()
}
}
todayCheckedEventsCount = 0
}
delay(30)
}
}
initChart()
}
I didn't try to follow your logic of what these todayEventsCount and todayCheckedEventsCount properties are. They probably should be locally defined variables in the coroutine.
You're also going to need some condition in the while loop that breaks you out of it. I didn't look closely enough to see what that condition should be. Your original code doesn't break the loop of reposting the runnable forever.

Kotlin on Android: How to use LiveData from a database in a fragment?

I use MVVM and have a list of data elements in a database that is mapped through a DAO and repository to ViewModel functions.
Now, my problem is rather banal; I just want to use the data in fragment variables, but I get a type mismatch.
The MVVM introduces a bit of code, and for completeness of context I'll run through it, but I'll strip it to the essentials:
The data elements are of a data class, "Objects":
#Entity(tableName = "objects")
data class Objects(
#ColumnInfo(name = "object_name")
var objectName: String
) {
#PrimaryKey(autoGenerate = true)
var id: Int? = null
}
In ObjectsDao.kt:
#Dao
interface ObjectsDao {
#Query("SELECT * FROM objects")
fun getObjects(): LiveData<List<Objects>>
}
My database:
#Database(
entities = [Objects::class],
version = 1
)
abstract class ObjectsDatabase: RoomDatabase() {
abstract fun getObjectsDao(): ObjectsDao
companion object {
// create database
}
}
In ObjectsRepository.kt:
class ObjectsRepository (private val db: ObjectsDatabase) {
fun getObjects() = db.getObjectsDao().getObjects()
}
In ObjectsViewModel.kt:
class ObjectsViewModel(private val repository: ObjectsRepository): ViewModel() {
fun getObjects() = repository.getObjects()
}
In ObjectsFragment.kt:
class ObjectsFragment : Fragment(), KodeinAware {
private lateinit var viewModel: ObjectsViewModel
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel = ViewModelProvider(this, factory).get(ObjectsViewModel::class.java)
// I use the objects in a recyclerview; rvObjectList
rvObjectList.layoutManager = GridLayoutManager(context, gridColumns)
val adapter = ObjectsAdapter(listOf(), viewModel)
// And I use an observer to keep the recyclerview updated
viewModel.getObjects.observe(viewLifecycleOwner, {
adapter.objects = it
adapter.notifyDataSetChanged()
})
}
}
The adapter:
class ObjectsAdapter(var objects: List<Objects>,
private val viewModel: ObjectsViewModel):
RecyclerView.Adapter<ObjectsAdapter.ObjectsViewHolder>() {
// Just a recyclerview adapter
}
Now, all the above works fine - but my problem is that I don't want to use the observer to populate the recyclerview; in the database I store some objects, but there are more objects that I don't want to store.
So, I try to do this instead (in the ObjectsFragment):
var otherObjects: List<Objects>
// ...
if (condition) {
adapter.objects = viewModel.getObjects()
} else {
adapter.objects = otherObjects
}
adapter.notifyDataSetChanged()
And, finally, my problem; I get type mismatch for the true condition assignment:
Type mismatch: inferred type is LiveData<List> but List was expected
I am unable to get my head around this. Isn't this pretty much what is happening in the observer? I know about backing properties, such as explained here, but I don't know how to do that when my data is not defined in the ViewModel.
We need something to switch data source. We pass switching data source event to viewModel.
mySwitch.setOnCheckedChangeListener { _, isChecked ->
viewModel.switchDataSource(isChecked)
}
In viewModel we handle switching data source
(To use switchMap include implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.4.0")
class ObjectsViewModel(private val repository: ObjectsRepository) : ViewModel() {
// Best practice is to keep your data in viewModel. And it is useful for us in this case too.
private val otherObjects = listOf<Objects>()
private val _loadDataFromDataBase = MutableLiveData<Boolean>()
// In case your repository returns liveData of favorite list
// from dataBase replace MutableLiveData(otherObjects) with repository.getFavorite()
fun getObjects() = _loadDataFromDataBase.switchMap {
if (it) repository.getObjects() else MutableLiveData(otherObjects)
}
fun switchDataSource(fromDataBase: Boolean) {
_loadDataFromDataBase.value = fromDataBase
}
}
In activity/fragment observe getObjects()
viewModel.getObjects.observe(viewLifecycleOwner, {
adapter.objects = it
adapter.notifyDataSetChanged()
})
You can do something like this:
var displayDataFromDatabase = true // Choose whatever default fits your use-case
var databaseList = emptyList<Objects>() // List we get from database
val otherList = // The other list that you want to show
toggleSwitch.setOnCheckedChangeListener { _, isChecked ->
displayDataFromDatabase = isChecked // Or the negation of this
// Update adapter to use databaseList or otherList depending upon "isChecked"
}
viewModel.getObjects.observe(viewLifecycleOwner) { list ->
databaseList = list
if(displayDataFromDatabase)
// Update adapter to use this databaseList
}

Accessing AlarmManager from inside a RecyclerView Adapter

I'm making an Android alarm app in class. The alarms are displayed inside a recyclerview in the main activity, and I want them to be deleted when pressed. I am able to clear it from the alarm database I set up but I cannot access AlarmManager to cancel the alarm, and the PendingIntent's context also appears to be wrong.
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, index: Int) {
val myViewHolder = holder as MyViewHolder
val sdf = SimpleDateFormat("HH:mm EEEE")
myViewHolder.tvAlarmTime.text = sdf.format(alarms[index].milliseconds)
myViewHolder.tvAlarmFrequency.text = alarms[index].frequency
myViewHolder.itemView.setOnClickListener {
launch {
withContext(Dispatchers.IO) {
val db = AlarmDatabase.getDatabase(myViewHolder.tvAlarmTime.context)
db.alarmDao().deleteTriggeredAlarm((alarms[index].id))
}
}
val pi = PendingIntent.getBroadcast(this, (alarms[index].id).toInt(), Intent("alarmTask"), PendingIntent.FLAG_UPDATE_CURRENT)
val alarmMgr = getSystemService(Context.ALARM_SERVICE) as AlarmManager
alarmMgr.cancel(pi)
}
}
In the last 3 lines, the context has a type mismatch as this is a MyAdapter type - I'm not sure what I need to put here, something similar to MainActivity.context I would assume
getSystemService also shows a type inference error as a string, and I assume this is causing the type mismatch for context.ALARM_SERVICE as a string rather than the context.
What is the correct context and how can I access the AlarmManager inside the adapter?
You can use the context of your item view:
val context = myViewHolder.itemView.context
val pi = PendingIntent.getBroadcast(context, (alarms[index].id).toInt(), Intent("alarmTask"), PendingIntent.FLAG_UPDATE_CURRENT)
val alarmMgr = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
alarmMgr.cancel(pi)

Kotlin - How do i set a sharedpreference code so that i open the closed app, it opens the last Activity, where i left it

I made two activities in my application, I want the app to be opened where I left off. In other words, not the default activity but the activity where I was when I last exited the app.
You could set a SplashActivity, where your app start, that will start other activities.
In this SplashActivity, you can set a var lastActivity, that will be a code to keep in which activity you were last time.
You get it with SharedPreference, and then go to the activity.
i.e :
String lastActivity = SharedPreference.getString(...) // I don't really remember the syntax
if (lastActivity == "HelloWorldActivity")
startActivity(HelloWorldActivity.getStartIntent(context))
else if (lastActivity == "GoodByeActivity")
startActivity(GoodByeActivity.getStartIntent(context))
Then, do NOT forget to edit your SharedPreference value EACH TIME you change activity.
I don't know if this is a good practice, but feel free to test this and give your think.
EDIT
First, you need to understand how is Shared Preference File. I think it looks like this :
"app_name"="Your app name"
"last_activity"="Your last activity"
"user_age"="23"
This could be the first activity you launch :
class SplashActivity : AppCompatActivity() {
var lastActivity = ""
override fun onCreate(savedInstanceState : Bundle?) {
super.onCreate()
/*
Here, we will get the SharedPreferencesFile.
Then, we get the value linked to the key TAG_LAST_ACTIVITY (set in companion object)
*/
val sharedPref = this.getSharedPreferences(getString(R.string.shared_preference_file_name), 0)
lastActivity = sharedPref.getString(TAG_LAST_ACTIVITY, "")
var activityToStart : AppCompatActivity? = null
if (lastActivity.isBlank())
activityToStart = YourActivityToStartAtFirstLaunch.getStartIntent(this)
else if (lastActivity.equals(TAG_ACTIVITY_ONE))
activityToStart = ActivityOne.getStartIntent(this)
else if (lastActivity.equals(TAG_ACTIVITY_TWO))
activityToStart = ActivityTwo.getStartIntent(this)
else if
... // Use as many as else if you need, but think about the "when" condition, it is better !
startActivity(activityToStart)
}
companion object {
private const val TAG_LAST_ACTIVITY = "last_activity"
private const val TAG_ACTIVITY_ONE = "activity_one"
private const val TAG_ACTIVITY_TWO = "activity_two"
}
}
And this could be your ActivityOne, for example :
class ActivityOne : AppCompatActivity() {
override fun onCreate(savedInstanceState : Bundle?) {
super.onCreate()
/*
Here, we will modify the variable LAST_ACTIVITY in the shared preferences file by setting it to "activity_one".
So, if the user quit this app now, you will know at next launch in which activity he stopped.
I think it is a better practice to set this in the onPause() or onStopped() method. Think about it ! ;)
*/
val sharedPrefEditor = this.getSharedPreferences(getString(R.string.shared_preference_file_name, 0)).edit()
sharedPrefEditor.putString(TAG_LAST_ACTIVITY, TAG_ACTIVITY_ONE)
sharedPrefEditor.apply()
}
companion object {
fun getStartIntent(context : Context) : Intent = Intent(context, ActivityOne()::class.java)
private const val TAG_ACTIVITY_ONE = "activity_one"
private const val TAG_LAST_ACTIVITY = "last_activity"
}
}
Do not forget to put your shared preference file name in your values/strings.xml file :
<string name="shared_preference_file_name">com.example.yourappname.sharedpref"</string>

Binding label to permantly changing value from thread

I have a really basic question but I couldn't find an answer to it. I already searched via google for people with similar problems but I didn´t find anything useful.
(e.g. https://github.com/edvin/tornadofx-guide/blob/master/part1/11.%20Editing%20Models%20and%20Validation.md )
I have the following ViewModel
class MasterSizeModel(var size : Int) : ViewModel()
{
val value = bind { size.toProperty() }
}
And inject it into another class, where I do the following:
masterSize.size = order.masterStatSize
masterSize is my model.
Now in a third class, I want to bind the value from the label to a label.
private val recvMaster : Label by fxid("recvMaster")
/*....*/
recvMaster.bind(masterSizeModel.value)
But unfortunately, my attempts are failing completely. I can see size from my ModelView is updating as it should, but the changes are not present in the value nither are they shown in the label.
Edit:
I totally forgot to bind to the textProperty(), but I don´t get any further.
recvMaster.textProperty().bind(masterSizeModel.value/*?*/)
Edit 2:
After the request I add my complete code section:
class Setup : View() {
override val root : VBox by fxml()
/*Adding Buttons and Textfields*/
init {
//Binding all checkboxes to their text field
//Input validation....
//start Button
start.setOnAction {
val masterSize = 0
val masterSizeModel = MasterSizeModel((masterSize))
//Open a socket (see code below)
val req = Requester(ipAdress.text, masterSizeModel)
val reqModel = RequesterModel(SimpleObjectProperty<Requester>(req))
val scope = Scope()
setInScope(reqModel, scope)
setInScope(masterSizeModel, scope)
req.sendOrder(SetupOrder(/*Sending stuff throw the network*/))
val overview = find<Overview>(scope)
replaceWith(overview)
}
}
}
class Overview : View() {
override val root : VBox by fxml()
private val req : RequesterModel by inject()
private val masterSizeModel : MasterSizeModel by inject()
private val recvMaster : Label by fxid("recvMaster")
/*Adding buttons and stuff */
init{
/*A loop that will request stats from the server i keept it very simple so i can resolv the view problem first */
var run= true
val thread = thread(start= true, name="StatRequester"){while(run){req.req.get().sendOrder(StatOrder(OrderType.STAT))}}
//Change the label whenever the number of recived messages ist raised
recvMaster.textProperty().set(masterSizeModel.size.toString())
}
}
class Requester(val address: String = "localhost", var masterSize: MasterSizeModel ) : Sender() {
override val socket: ZMQ.Socket = context.socket(ZMQ.REQ)
override val port = "4993"
init {
socket.connect("$protocol$address:$port")
}
override fun sendOrder(order: Order) {
//ZeroMQ requires special care....
val message = packOrder(order)
val wrapper = packOrder(RequestOrder(order.type, message))
socket.send(wrapper,0)
val orderAsString = socket.recvStr(0)
handleOrder(orderAsString)
}
private fun handleOrder(orderString: String)
{
val orderDedec = unpackOrder<RequestOrder>(orderString)
when(orderDedec.type)
{
OrderType.STAT ->{
val order = unpackOrder<StatOrder>(orderDedec.order)
sleep(5000) //sleep for debugging only
println("$masterSize, ${order.masterStatSize}")
//Here I receive a new value and want to update the label in my view
masterSize.size = order.masterStatSize
}
OrderType.STOP ->{
close()
}
else ->{}
}
}
override fun close() {
socket.close()
context.term()
}
}