I'm learning Android development on my own. I thought I had gotten my head around what/how 'liveData.observe' worked. But...
Making a small simple app that listens for input from a Bluetooth device and will display or hide an image based on that bluetooth input.
I have all that working, but now I want to add another feature and i'm running into a wall. The feature is to increase a counter each time the liveData.Observe returns TRUE.
Either I don't understand 'liveData.observe' or i'm trying to use it incorrectly(it is probably both of these things).
I'm working on a function now to increase this counter and this is the spot i'm getting hung up on. My current thinking is to create a seperate function(pigCounter()). But I'm getting nowhere.
onCreate
class MainActivity : AppCompatActivity() {
private var liveData: MutableLiveData<String> = MutableLiveData()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
<SNIP>
...
</SNIP>
liveData.observe(this, androidx.lifecycle.Observer {
imageView_mothership_LED_state.showOrHideImage(it == "1")
})
pigCounter()
}
Below is from activity_main.xml
imageView_mothership_LED_state
<ImageView
android:id="#+id/imageView_mothership_LED_state"
android:layout_width="124dp"
android:layout_height="144dp"
android:scaleType="fitCenter"
android:soundEffectsEnabled="false"
app:layout_constraintBottom_toBottomOf="#+id/imageView_blueYesOrNo"
app:layout_constraintEnd_toEndOf="#+id/imageView_blueYesOrNo"
app:layout_constraintStart_toStartOf="#+id/imageView_blueYesOrNo"
app:layout_constraintTop_toTopOf="#+id/imageView_blueYesOrNo"
app:srcCompat="#drawable/ic_check_black_24dp"
android:contentDescription="Check mark image to indicate if LED is on/off" />
Below is the function that sets the images visibility
showOrHideImage()
// display or don't display check mark image
private fun View.showOrHideImage(imageShow: Boolean) {
visibility = if (imageShow) View.VISIBLE else View.GONE
}
This is the function that handles the incoming bluetooth data.
readBlueToothDataFromMothership()
private fun readBlueToothDataFromMothership(bluetoothSocket: BluetoothSocket) {
Log.i(LOGTAG, Thread.currentThread().name)
val bluetoothSocketInputStream = bluetoothSocket.inputStream
val buffer = ByteArray(1024)
var bytes: Int
//Loop to listen for received bluetooth messages
while (true) {
try {
bytes = bluetoothSocketInputStream.read(buffer)
val readMessage = String(buffer, 0, bytes)
liveData.postValue(readMessage)
} catch (e: IOException) {
e.printStackTrace()
break
}
}
This is the function i'm writing, in that I want to increase a counter each time.
pigCount
private fun pigCount() {
var count = 0
var counterTextView = findViewById<TextView>(R.id.textView_blueCounter)
if(imageView_mothership_LED_state.showOrHideImage(true)) {
counterTextView.text = count.toString()
count++
}
This does not work due to a type mismatch(expecting a Boolean, getting Unit). I've also tried moving this into the liveData.observe function up in onCreate(). But same road block.
Can anyone point me in the right direction. I have a sneaky feeling I am way off on this. And would appreicate a nudge. :)
To count how many times a LiveData returns true it is really easy, you could just use a MediatorLiveData.
Assuming you have a proper LiveData<Boolean>, you could do as follows:
val trueFalse = MutableLiveData<Boolean>()
val counter by lazy {
MediatorLiveData<Int>().apply {
value = 0 // Initialize the counter
// Add the trueFalse as a source of this live data
addSource(trueFalse) { boolVal ->
if(boolVal == true) {
// If the mediator live data had a value, use it, if null use 0; and add +1 to it because the boolVal was true
value = (value ?: 0) + 1
}
}
}
}
/* Somewhere else in the code */
fun setupTextView() {
counter.observe({lifecycle}) {
val tv = findViewById<TextView>(R.id.tv_something)
tv.text = "$it"
}
}
This creates a MediatorLiveData that will have as a source the trueFalse live data.
If the value is true will proceed and post a new value in the counter live data.
Related
I'm newbie use Kotlin on my dev apps android,
and now, I on step learn to implement Update UI with LiveData, Retrofit, Coroutine on Recyclerview. The My Apps:
MainActivity > MainFragment with 3 Tab fragment > HomeFragment, DashboardFragment, and SettingsFragment
I call function to get data from server on onCreateView HomeFragment, and observe this with show shimmer data on my Recylerview when is loading, try update RecyclerView when success, and show view Failed Load -button refresh when error.
The problem is:
Adapter Recyclerview not Update when success get Data from Server. Adapter still show shimmer data
With case error (no Internet), i show view Failed Load, with button refresh. Tap to refresh, i re-call function to get data server, but fuction not work correct. Recyclerview show last data, not show Failed Load again.
Bellow my code
HomeFragment
private var _binding: FragmentHomeBinding? = null
private val binding get() = _binding!!
private lateinit var adapterNews: NewsAdapter
private var shimmerNews: Boolean = false
private var itemsDataNews = ArrayList<NewsModel>()
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
// Inflate the layout for this fragment
_binding = FragmentHomeBinding.inflate(inflater, container, false)
......
newsViewModel = ViewModelProvider(requireActivity()).get(NewsViewModel::class.java)
//news
binding.frameNews.rvNews.setHasFixedSize(true)
binding.frameNews.rvNews.layoutManager = llmh
adapterNews = NewsAdapter(itemsDataNews, shimmerNews)
binding.frameNews.rvNews.adapter = adapterNews
// Observe
//get News
newsViewModel.refresh()
newsViewModel.newsList.observe(
viewLifecycleOwner,
androidx.lifecycle.Observer { newsList ->
newsList?.let {
binding.frameNews.rvNews.visibility = View.VISIBLE
binding.frameNews.rvNews.isNestedScrollingEnabled = true
binding.frameNews.itemNewsLayoutFailed.visibility = View.GONE
if (it.size == 0)
binding.frameNews.root.visibility = View.GONE
else
getDataNews(it)
}
})
newsViewModel.loading.observe(viewLifecycleOwner) { isLoading ->
isLoading?.let {
binding.frameNews.rvNews.visibility = View.VISIBLE
binding.frameNews.rvNews.isNestedScrollingEnabled = false
binding.frameNews.itemNewsLayoutFailed.visibility = View.GONE
getDataNewsShimmer()
}
}
newsViewModel.loadError.observe(viewLifecycleOwner) { isError ->
isError?.let {
binding.frameNews.rvNews.visibility = View.INVISIBLE
binding.frameNews.itemNewsLayoutFailed.visibility = View.VISIBLE
binding.frameNews.btnNewsFailed.setOnClickListener {
newsViewModel.refresh()
}
}
}
....
return binding.root
}
#SuppressLint("NotifyDataSetChanged")
private fun getDataNewsShimmer() {
shimmerNews = true
itemsDataNews.clear()
itemsDataNews.addAll(NewsData.itemsShimmer)
adapterNews.notifyDataSetChanged()
}
#SuppressLint("NotifyDataSetChanged")
private fun getDataNews(list: List<NewsModel>) {
Toast.makeText(requireContext(), list.size.toString(), Toast.LENGTH_SHORT).show()
shimmerNews = false
itemsDataNews.clear()
itemsDataNews.addAll(list)
adapterNews.notifyDataSetChanged()
}
override fun onDestroyView() {
super.onDestroyView()
_binding=null
}
NewsViewModel
class NewsViewModel: ViewModel() {
val newsService = KopraMobileService().getNewsApi()
var job: Job? = null
val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
onError("Exception handled: ${throwable.localizedMessage}")
}
val newsList = MutableLiveData<List<NewsModel>>()
val loadError = MutableLiveData<String?>()
val loading = MutableLiveData<Boolean>()
fun refresh() {
fetchNews()
}
private fun fetchNews() {
loading.postValue(true)
job = CoroutineScope(Dispatchers.IO + exceptionHandler).launch {
val response = newsService.getNewsList()
withContext(Dispatchers.Main) {
if (response.isSuccessful) {
newsList.postValue(response.body()?.data)
loadError.postValue(null)
loading.postValue(false)
} else {
onError("Error : ${response.message()} ")
}
}
}
loadError.postValue("")
loading.postValue( false)
}
private fun onError(message: String) {
loadError.postValue(message)
loading.postValue( false)
}
override fun onCleared() {
super.onCleared()
job?.cancel()
}
}
NewsAdapter
NewsAdapter(
var itemsCells: List<NewsModel?> = emptyList(),
var shimmer: Boolean ,
) :
RecyclerView.Adapter<ViewHolder>() {
private lateinit var context: Context
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): NewsHomeViewHolder {
context = parent.context
return NewsHomeViewHolder(
NewsItemHomeBinding.inflate(LayoutInflater.from(parent.context), parent, false)
)
}
override fun onBindViewHolder(holder: NewsHomeViewHolder, position: Int) {
holder.bind(itemsCells[position]!!)
}
inner class NewsHomeViewHolder(private val binding: NewsItemHomeBinding) :
ViewHolder(binding.root) {
fun bind(newsItem: NewsModel) {
binding.newsItemFlat.newsTitle.text = newsItem.name
binding.newsItemFlatShimmer.newsTitle.text = newsItem.name
binding.newsItemFlat.newsSummary.text = newsItem.name
binding.newsItemFlatShimmer.newsSummary.text = newsItem.name
if (shimmer) {
binding.layoutNewsItemFlat.visibility = View.INVISIBLE
binding.layoutNewsItemFlatShimmer.visibility = View.VISIBLE
binding.layoutNewsItemFlatShimmer.startShimmer()
} else {
binding.layoutNewsItemFlat.visibility = View.VISIBLE
binding.layoutNewsItemFlatShimmer.visibility = View.INVISIBLE
}
}
}
override fun getItemCount(): Int {
return itemsCells.size
}
I hope someone can help me to solve the problem. thanks, sorry for my English.
You have 3 different LiveDatas, right? One with a list of news data, one with a loading error message, and one with a loading state.
You set up an Observer for each of these, and that observer function gets called whenever the LiveData's value updates. That's important, because here's what happens when your request succeeds, and you get some new data:
if (response.isSuccessful) {
newsList.postValue(response.body()?.data)
loadError.postValue(null)
loading.postValue(false)
}
which means newsList updates, then loadError updates, then loading updates.
So your observer functions for each of those LiveDatas run, in that order. But you're not testing the new values (except for null checks), so the code in each one always runs when the value updates. So when your response is successful, this happens:
newsList observer runs, displays as successful, calls getDataNews
loadError observer runs, value is null so nothing happens
loading observer runs, value is false but isn't checked, displays as loading, calls getDataNewsShimmer
So even when the response is successful, the last thing you do is display the loading state
You need to check the state (like loading) before you try to display it. But if you check that loading is true, you'll have a bug in fetchNews - that sets loading = true, starts a coroutine that finishes later, and then immediately sets loading = false.
I'd recommend trying to create a class that represents different states, with a single LiveData that represents the current state. Something like this:
// a class representing the different states, and any data they need
sealed class State {
object Loading : State()
data class Success(val newsList: List<NewsModel>?) : State()
data class Error(val message: String) : State()
}
// this is just a way to keep the mutable LiveData private, so it can't be updated
private val _state = MutableLiveData<State>()
val state: LiveData<State> get() = _state
private fun fetchNews() {
// initial state is Loading, until we get a response
_state.value = State.Loading
job = CoroutineScope(Dispatchers.IO + exceptionHandler).launch {
val response = newsService.getNewsList()
// if you're using postValue I don't think you need to switch to Dispatchers.Main?
_state.postValue(
// when you get a response, the state is now either Success or Error
if (response.isSuccessful) State.Success(response.body()?.data)
else State.Error("Error : ${response.message()} ")
)
}
}
And then you just need to observe that state:
// you don't need to create an Observer object, you can use a lambda!
newsViewModel.state.observe(viewLifecycleOwner) { state ->
// Handle the different possible states, and display the current one
// this lets us avoid repeating 'binding.frameNews' before everything
with(binding.frameNews) {
// You could use a when block, and describe each state explicitly,
// like your current setup:
when(state) {
// just checking equality because Loading is a -singleton object instance-
State.Loading -> {
rvNews.visibility = View.VISIBLE
rvNews.isNestedScrollingEnabled = false
itemNewsLayoutFailed.visibility = View.GONE
getDataNewsShimmer()
}
// Error and Success are both -classes- so we need to check their type with 'is'
is State.Error -> {
rvNews.visibility = View.INVISIBLE
itemNewsLayoutFailed.visibility = View.VISIBLE
btnNewsFailed.setOnClickListener {
newsViewModel.refresh()
}
}
is State.Success -> {
rvNews.visibility = View.VISIBLE
rvNews.isNestedScrollingEnabled = true
itemNewsLayoutFailed.visibility = View.GONE
// Because we know state is a Success, we can access newsList on it
// newsList can be null - I don't know how you want to handle that,
// I'm just treating it as defaulting to size == 0
// (make sure you make this visible when necessary too)
if (state.newsList?.size ?: 0 == 0) root.visibility = View.GONE
else getDataNews(state.newsList)
}
}
// or, if you like, you could do this kind of thing instead:
itemNewsLayoutFailed.visibility = if (state is Error) VISIBLE else GONE
}
}
You also might want to break that display code out into separate functions (like showError(), showList(state.newsList) etc) and call those from the branches of the when, if that makes it more readable
I hope that makes sense! When you have a single value representing a state, it's a lot easier to work with - set the current state as things change, and make your observer handle each possible UI state by updating the display. When it's Loading, make it look like this. When there's an Error, make it look like this
That should help avoid bugs where you update multiple times for multiple things, trying to coordinate everything. I'm not sure why you're seeing that problem when you reload after an error, but doing this might help fix it (or make it easier to see what's causing it)
I have a sensor and by connecting it to an Arduino board I can see the sensor signals in the Serial Plotter in the form of a chart on Arduino IDE.
I want to see the same signals on my APP in real time by using a Bluetooth module.
I'm using the MPAndroidChart library to plot a chart in Kotlin.
To plot a chart using MPAndroidChart and for more information on how to use MPAndroidChart,
I checked the sample program code that published in Google Play from the following link in Github
https://github.com/PhilJay/MPAndroidChart
and converted the Java code to Kotlin, and now I can Plot chart with random numbers.
But I plan to plot my own graph using sensor data that received via Bluetooth.
I wrote a ReceiveData function and I want to get the data with this method and give it to the addEntry function so that instead of plotting a graph with random numbers, I plot my graph with Bluetooth data.
But I have no idea what to do.
By calling the feedMultiple function in
btn_startTest.setOnClickListener { feedMultiple() }
and using the following code in addEntry a graph plotting with random numbers.
data.addEntry(Entry(set.entryCount.toFloat(), (Math.random() * 40).toFloat() + 30f), 0)
But The problem I have is that , I d’not know how to transfer the data that received via Bluetooth to the addEntry function and use them instead of random numbers.
This is the complete code of my application:
class ElectromyographyAnalysis : AppCompatActivity(), OnChartValueSelectedListener {
companion object {
val TAG = "EMGSensor"
val APP_NAME = "EMGSensor"
var m_myUUID: UUID = UUID.fromString("00001101-0000-1000-8000-00805f9b34fb")
var m_bluetoothSocket: BluetoothSocket? = null
lateinit var m_progress: ProgressDialog
lateinit var m_bluetoothAdapter: BluetoothAdapter
var m_isConnected: Boolean = false
lateinit var m_address: String
var xVal: Int = 0
var yVal: Int = 0
}
lateinit var emgChart: LineChart
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.electromyography_analysis_layout)
title = "Electromyography Analysis"
m_address = intent.getStringExtra(SelectDeviceActivity.EXTRA_ADDRESS).toString()
ConnectToDevice(this).execute()
//add this new
window.setFlags(
WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN
)
emgChart = findViewById(R.id.emg_lineChart)
emgChart.setOnChartValueSelectedListener(this)
// enable description text
emgChart.description.isEnabled = true
// enable touch gestures
emgChart.setTouchEnabled(true)
// enable scaling and dragging
// enable scaling and dragging
emgChart.isDragEnabled = true
emgChart.setScaleEnabled(true)
emgChart.setDrawGridBackground(false)
// if disabled, scaling can be done on x- and y-axis separately
emgChart.setPinchZoom(true)
// set an alternative background color
emgChart.setBackgroundColor(Color.LTGRAY)
val data = LineData()
data.setValueTextColor(Color.WHITE)
//add empty data
// add empty data
emgChart.data = data
// get the legend (only possible after setting data)
// get the legend (only possible after setting data)
val l: Legend = emgChart.legend
// modify the legend ...
// modify the legend ...
l.form = LegendForm.LINE
//l.typeface =
l.textColor = Color.WHITE
val xl: XAxis = emgChart.xAxis
//xl.typeface = tfLight
xl.textColor = Color.WHITE
xl.setDrawGridLines(false)
xl.setAvoidFirstLastClipping(true)
xl.isEnabled = true
val leftAxis: YAxis = emgChart.getAxisLeft()
//leftAxis.typeface = tfLight
leftAxis.textColor = Color.WHITE
leftAxis.axisMaximum = 100f
leftAxis.axisMinimum = 0f
leftAxis.setDrawGridLines(true)
val rightAxis: YAxis = emgChart.getAxisRight()
rightAxis.isEnabled = false
btn_startTest.setOnClickListener { feedMultiple() }
}
private fun addEntry() {
val data: LineData = emgChart.data
if (data != null) {
var set = data.getDataSetByIndex(0)
// set.addEntry(...); // can be called as well
if (set == null) {
set = createSet()
data.addDataSet(set)
}
data.addEntry(Entry(set.entryCount.toFloat(), (Math.random() * 40).toFloat() + 30f), 0)
data.notifyDataChanged()
// let the chart know it's data has changed
emgChart.notifyDataSetChanged()
// limit the number of visible entries
emgChart.setVisibleXRangeMaximum(120f)
// chart.setVisibleYRange(30, AxisDependency.LEFT);
// move to the latest entry
emgChart.moveViewToX(data.entryCount.toFloat())
// this automatically refreshes the chart (calls invalidate())
// chart.moveViewTo(data.getXValCount()-7, 55f,
// AxisDependency.LEFT);
}
}
private fun createSet(): LineDataSet {
val set = LineDataSet(null, "Dynamic Data")
set.axisDependency = AxisDependency.LEFT
set.color = ColorTemplate.getHoloBlue()
set.setCircleColor(Color.WHITE)
set.lineWidth = 2f
set.circleRadius = 4f
set.fillAlpha = 65
set.fillColor = ColorTemplate.getHoloBlue()
set.highLightColor = Color.rgb(244, 117, 117)
set.valueTextColor = Color.WHITE
set.valueTextSize = 9f
set.setDrawValues(false)
return set
}
private var thread: Thread? = null
private fun feedMultiple() {
if (thread != null)
thread!!.interrupt()
val runnable = Runnable { addEntry() }
thread = Thread {
for (i in 0..999) {
// Don't generate garbage runnable inside the loop.
runOnUiThread(runnable)
try {
Thread.sleep(25)
} catch (e: InterruptedException) {
e.printStackTrace()
}
}
}
thread!!.start()
}
private fun receiveData() {
val buffer = ByteArray(1024)
var bytes: Int
val handler = Handler()
var stopWorker = false
Log.d(TAG, "Inside ReceiveData")
val workerThread = Thread {
while (!Thread.currentThread().isInterrupted && !stopWorker) {
try {
bytes = m_bluetoothSocket!!.inputStream.read(buffer)
if (bytes > 0) {
val incomingMessage = String(buffer, 0, bytes)
Log.d(TAG, "InputStream : $incomingMessage")
yVal = incomingMessage.toInt()
} else {
Toast.makeText(this , "bytes is less than zero" , Toast.LENGTH_SHORT).show()
}
} catch (ex: IOException) {
stopWorker = true
}
}
}
workerThread.start()
}
To be honest I would use EventBus, so you have a variable listening for data coming from your Bluetooth device.
The eventbus works as a listener. When you receive your data from the Bluetooth device Android save the event and you can use it wherever in your app, passing it between activities too.
But to do that you nees to register and subscribe. Let's start from the first.
Add to your dependencies GreenRobot eventbus in your gradle:
implementation 'org.greenrobot:eventbus:3.2.0'
Create the EventBus (I would make a new object file to make eventbus ordered):
data class DataEvent(var dataToSend: Float){}
Of course, you need to register and subscribe to this events. And how to?
Register your eventbus inside your bluetooth receiver you need to register the data to subscribe (where you get the data package from the bluetooth to be clear):
val eventData: DataEvent = DataEvent(yourData)
EventBus.getDefault().post(eventData)
Subscribe, you need to subscribe of course now or you won't see any data being saved into your event class.
Because you need to plot data, in your ElectromyographyAnalysis Class you need to subscribe to the event:
#Subscribe(threadMode = ThreadMode.MAIN)
public fun onDataReceived(event: DataEvent) {
if (event.dataToSend != null) {
dataToPlot = event.dataToSend
}
addEntry(dataToPlot) // THIS WILL CALL YOUR ADDENTRY()
}
This way you will send to the AddEntry() method every single bit the bluetooth receives and plot them.
But, to do so you need to change a bit your AddEntry():
private fun addEntry(yourData:DataType) {
val data: LineData = emgChart.data
if (data != null) {
var set = data.getDataSetByIndex(0)
if (set1 == null) {
data.addDataSet(set)
}
data.addEntry(
Entry(
set.entryCount.toFloat(),
datatoplot
), 0
)
data.notifyDataChanged()
// let the chart know it's data has changed
emgChart.notifyDataSetChanged()
// limit the number of visible entries
emgChart.setVisibleXRangeMaximum(120f)
// chart.setVisibleYRange(30, AxisDependency.LEFT);
// move to the latest entry
emgChart.moveViewToX(data.entryCount.toFloat())
// this automatically refreshes the chart (calls invalidate())
// chart.moveViewTo(data.getXValCount()-7, 55f,
// AxisDependency.LEFT);
}
}
You won't need feedMultiple Thread and you will have also a better performant real time graph.
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 a livedata int value Its value is initially 3.
In fragment Quiz its value decreases and becomes 0. But in fragment End its value still remains 3, why?
ViewModel
class QuizViewModel : ViewModel() {
private val lives = MutableLiveData<Int>()
var live = 3
init {
lives.value = live
}
fun onWrongAnswer() {
live--
}
fun onPlayAgain() {
live = 3
}
}
QuizFragment
For each wrong answer, the live decreases by 1 and if it is 0 or the questions are finished, the EndFragment is reached.
EndFragment
private fun checkResult() {
if (viewModel.live == 0) {
binding.imageViewResult.setImageResource(R.drawable.sad)
binding.textViewResult.setText(R.string.you_lose_want_to_try_again)
} else {
binding.imageViewResult.setImageResource(R.drawable.won)
binding.textViewResult.setText(R.string.you_win_congratulations)
}
}
This part does not work correctly because it remains as Live 3. But its value decreased and became 0. Why is it 3 again?
You check the Int live instead of the MutableLiveData<Int> lives.
Get rid of live and create a non-mutable public livedata member variable. Update the mutable live data in your viewmodel:
private val _lives = MutableLiveData<Int>(3)
val lives: LiveData<Int> = _lives
fun onWrongAnswer() {
_lives.value = lives.value?.minus(1)
}
fun onPlayAgain() {
_lives.value = 3
}
Observe the value of the lives: LiveData in your fragment like this:
viewModel.lives.observe(viewLifecycleOwner, {live ->
if (live == 0) {
binding.imageViewResult.setImageResource(R.drawable.sad)
binding.textViewResult.setText(R.string.you_lose_want_to_try_again)
} else {
binding.imageViewResult.setImageResource(R.drawable.won)
binding.textViewResult.setText(R.string.you_win_congratulations)
}
})
Note: viewLifecycleOwner is a property from androidx.fragment.app.
In my Weather app, I have a MainFragment which has a button that opens a different fragment (SearchFragment) (via replace), allows a user to select a location and then fetches weather data for that location and loads it in various views including an MPAndroid LineChart. My issue is that whenever I come back from the search fragment, although the new data is fetched for the chart and I'm calling chart.notifyDataSetChanged() & chart.invalidate() (also tried chart.postInvalidate() since it was suggested when working on another thread) after the invalidate() is called the chart simply disappears. What am i missing here?
MainFragment:
const val UNIT_SYSTEM_KEY = "UNIT_SYSTEM"
const val LATEST_CURRENT_LOCATION_KEY = "LATEST_CURRENT_LOC"
class MainFragment : Fragment() {
// Lazy inject the view model
private val viewModel: WeatherViewModel by viewModel()
private lateinit var weatherUnitConverter: WeatherUnitConverter
private val TAG = MainFragment::class.java.simpleName
// View declarations
...
// OnClickListener to handle the current weather's "Details" layout expansion/collapse
private val onCurrentWeatherDetailsClicked = View.OnClickListener {
if (detailsExpandedLayout.visibility == View.GONE) {
detailsExpandedLayout.visibility = View.VISIBLE
detailsExpandedArrow.setImageResource(R.drawable.ic_arrow_up_black)
} else {
detailsExpandedLayout.visibility = View.GONE
detailsExpandedArrow.setImageResource(R.drawable.ic_down_arrow)
}
}
// OnClickListener to handle place searching using the Places SDK
private val onPlaceSearchInitiated = View.OnClickListener {
(activity as MainActivity).openSearchPage()
}
// RefreshListener to update the UI when the location settings are changed
private val refreshListener = SwipeRefreshLayout.OnRefreshListener {
Toast.makeText(activity, "calling onRefresh()", Toast.LENGTH_SHORT).show()
swipeRefreshLayout.isRefreshing = false
}
// OnClickListener to allow navigating from this fragment to the settings one
private val onSettingsButtonClicked: View.OnClickListener = View.OnClickListener {
(activity as MainActivity).openSettingsPage()
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
val view = inflater.inflate(R.layout.main_fragment, container, false)
// View initializations
.....
hourlyChart = view.findViewById(R.id.lc_hourly_forecasts)
return view
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setUpChart()
lifecycleScope.launch {
// Shows a lottie animation while the data is being loaded
//scrollView.visibility = View.GONE
//lottieAnimView.visibility = View.VISIBLE
bindUIAsync().await()
// Stops the animation and reveals the layout with the data loaded
//scrollView.visibility = View.VISIBLE
//lottieAnimView.visibility = View.GONE
}
}
#SuppressLint("SimpleDateFormat")
private fun bindUIAsync() = lifecycleScope.async(Dispatchers.Main) {
// fetch current weather
val currentWeather = viewModel.currentWeatherData
// Observe the current weather live data
currentWeather.observe(viewLifecycleOwner, Observer { currentlyLiveData ->
if (currentlyLiveData == null) return#Observer
currentlyLiveData.observe(viewLifecycleOwner, Observer { currently ->
setCurrentWeatherDate(currently.time.toDouble())
// Get the unit system pref's value
val unitSystem = viewModel.preferences.getString(
UNIT_SYSTEM_KEY,
UnitSystem.SI.name.toLowerCase(Locale.ROOT)
)
// set up views dependent on the Unit System pref's value
when (unitSystem) {
UnitSystem.SI.name.toLowerCase(Locale.ROOT) -> {
setCurrentWeatherTemp(currently.temperature)
setUnitSystemImgView(unitSystem)
}
UnitSystem.US.name.toLowerCase(Locale.ROOT) -> {
setCurrentWeatherTemp(
weatherUnitConverter.convertToFahrenheit(
currently.temperature
)
)
setUnitSystemImgView(unitSystem)
}
}
setCurrentWeatherSummaryText(currently.summary)
setCurrentWeatherSummaryIcon(currently.icon)
setCurrentWeatherPrecipProb(currently.precipProbability)
})
})
// fetch the location
val weatherLocation = viewModel.weatherLocation
// Observe the location for changes
weatherLocation.observe(viewLifecycleOwner, Observer { locationLiveData ->
if (locationLiveData == null) return#Observer
locationLiveData.observe(viewLifecycleOwner, Observer { location ->
Log.d(TAG,"location update = $location")
locationTxtView.text = location.name
})
})
// fetch hourly weather
val hourlyWeather = viewModel.hourlyWeatherEntries
// Observe the hourly weather live data
hourlyWeather.observe(viewLifecycleOwner, Observer { hourlyLiveData ->
if (hourlyLiveData == null) return#Observer
hourlyLiveData.observe(viewLifecycleOwner, Observer { hourly ->
val xAxisLabels = arrayListOf<String>()
val sdf = SimpleDateFormat("HH")
for (i in hourly.indices) {
val formattedLabel = sdf.format(Date(hourly[i].time * 1000))
xAxisLabels.add(formattedLabel)
}
setChartAxisLabels(xAxisLabels)
})
})
// fetch weekly weather
val weeklyWeather = viewModel.weeklyWeatherEntries
// get the timezone from the prefs
val tmz = viewModel.preferences.getString(LOCATION_TIMEZONE_KEY, "America/Los_Angeles")!!
// observe the weekly weather live data
weeklyWeather.observe(viewLifecycleOwner, Observer { weeklyLiveData ->
if (weeklyLiveData == null) return#Observer
weeklyLiveData.observe(viewLifecycleOwner, Observer { weatherEntries ->
// update the recyclerView with the new data
(weeklyForecastRCV.adapter as WeeklyWeatherAdapter).updateWeeklyWeatherData(
weatherEntries, tmz
)
for (day in weatherEntries) { //TODO:sp replace this with the full list once the repo issue is fixed
val zdtNow = Instant.now().atZone(ZoneId.of(tmz))
val dayZdt = Instant.ofEpochSecond(day.time).atZone(ZoneId.of(tmz))
val formatter = DateTimeFormatter.ofPattern("MM-dd-yyyy")
val formattedNowZtd = zdtNow.format(formatter)
val formattedDayZtd = dayZdt.format(formatter)
if (formattedNowZtd == formattedDayZtd) { // find the right week day whose data we want to use for the UI
initTodayData(day, tmz)
}
}
})
})
// get the hourly chart's computed data
val hourlyChartLineData = viewModel.hourlyChartData
// Observe the chart's data
hourlyChartLineData.observe(viewLifecycleOwner, Observer { lineData ->
if(lineData == null) return#Observer
hourlyChart.data = lineData // Error due to the live data value being of type Unit
})
return#async true
}
...
private fun setChartAxisLabels(labels: ArrayList<String>) {
// Populate the X axis with the hour labels
hourlyChart.xAxis.valueFormatter = IndexAxisValueFormatter(labels)
}
/**
* Sets up the chart with the appropriate
* customizations.
*/
private fun setUpChart() {
hourlyChart.apply {
description.isEnabled = false
setNoDataText("Data is loading...")
// enable touch gestures
setTouchEnabled(true)
dragDecelerationFrictionCoef = 0.9f
// enable dragging
isDragEnabled = true
isHighlightPerDragEnabled = true
setDrawGridBackground(false)
axisRight.setDrawLabels(false)
axisLeft.setDrawLabels(false)
axisLeft.setDrawGridLines(false)
xAxis.setDrawGridLines(false)
xAxis.isEnabled = true
// disable zoom functionality
setScaleEnabled(false)
setPinchZoom(false)
isDoubleTapToZoomEnabled = false
// disable the chart's legend
legend.isEnabled = false
// append extra offsets to the chart's auto-calculated ones
setExtraOffsets(0f, 0f, 0f, 10f)
data = LineData()
data.isHighlightEnabled = false
setVisibleXRangeMaximum(6f)
setBackgroundColor(resources.getColor(R.color.bright_White, null))
}
// X Axis setup
hourlyChart.xAxis.apply {
position = XAxis.XAxisPosition.BOTTOM
textSize = 14f
setDrawLabels(true)
setDrawAxisLine(false)
granularity = 1f // one hour
spaceMax = 0.2f // add padding start
spaceMin = 0.2f // add padding end
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
typeface = resources.getFont(R.font.work_sans)
}
textColor = resources.getColor(R.color.black, null)
}
// Left Y axis setup
hourlyChart.axisLeft.apply {
setDrawLabels(false)
setDrawGridLines(false)
setPosition(YAxis.YAxisLabelPosition.OUTSIDE_CHART)
isEnabled = false
isGranularityEnabled = true
// temperature values range (higher than probable temps in order to scale down the chart)
axisMinimum = 0f
axisMaximum = when (getUnitSystemValue()) {
UnitSystem.SI.name.toLowerCase(Locale.ROOT) -> 50f
UnitSystem.US.name.toLowerCase(Locale.ROOT) -> 150f
else -> 50f
}
}
// Right Y axis setup
hourlyChart.axisRight.apply {
setDrawGridLines(false)
isEnabled = false
}
}
}
ViewModel class:
class WeatherViewModel(
private val forecastRepository: ForecastRepository,
private val weatherUnitConverter: WeatherUnitConverter,
context: Context
) : ViewModel() {
private val appContext = context.applicationContext
// Retrieve the sharedPrefs
val preferences:SharedPreferences
get() = PreferenceManager.getDefaultSharedPreferences(appContext)
// This will run only when currentWeatherData is called from the View
val currentWeatherData = liveData {
val task = viewModelScope.async { forecastRepository.getCurrentWeather() }
emit(task.await())
}
val hourlyWeatherEntries = liveData {
val task = viewModelScope.async { forecastRepository.getHourlyWeather() }
emit(task.await())
}
val weeklyWeatherEntries = liveData {
val task = viewModelScope.async {
val currentDateEpoch = LocalDate.now().toEpochDay()
forecastRepository.getWeekDayWeatherList(currentDateEpoch)
}
emit(task.await())
}
val weatherLocation = liveData {
val task = viewModelScope.async(Dispatchers.IO) {
forecastRepository.getWeatherLocation()
}
emit(task.await())
}
val hourlyChartData = liveData {
val task = viewModelScope.async(Dispatchers.Default) {
// Build the chart data
hourlyWeatherEntries.observeForever { hourlyWeatherLiveData ->
if(hourlyWeatherLiveData == null) return#observeForever
hourlyWeatherLiveData.observeForever {hourlyWeather ->
createChartData(hourlyWeather)
}
}
}
emit(task.await())
}
/**
* Creates the line chart's data and returns them.
* #return The line chart's data (x,y) value pairs
*/
private fun createChartData(hourlyWeather: List<HourWeatherEntry>?): LineData {
if(hourlyWeather == null) return LineData()
val unitSystemValue = preferences.getString(UNIT_SYSTEM_KEY, "si")!!
val values = arrayListOf<Entry>()
for (i in hourlyWeather.indices) { // init data points
// format the temperature appropriately based on the unit system selected
val hourTempFormatted = when (unitSystemValue) {
UnitSystem.SI.name.toLowerCase(Locale.ROOT) -> hourlyWeather[i].temperature
UnitSystem.US.name.toLowerCase(Locale.ROOT) -> weatherUnitConverter.convertToFahrenheit(
hourlyWeather[i].temperature
)
else -> hourlyWeather[i].temperature
}
// Create the data point
values.add(
Entry(
i.toFloat(),
hourTempFormatted.toFloat(),
appContext.resources.getDrawable(determineSummaryIcon(hourlyWeather[i].icon), null)
)
)
}
Log.d("MainFragment viewModel", "$values")
// create a data set and customize it
val lineDataSet = LineDataSet(values, "")
val color = appContext.resources.getColor(R.color.black, null)
val offset = MPPointF.getInstance()
offset.y = -35f
lineDataSet.apply {
valueFormatter = YValueFormatter()
setDrawValues(true)
fillDrawable = appContext.resources.getDrawable(R.drawable.gradient_night_chart, null)
setDrawFilled(true)
setDrawIcons(true)
setCircleColor(color)
mode = LineDataSet.Mode.HORIZONTAL_BEZIER
this.color = color // line color
iconsOffset = offset
lineWidth = 3f
valueTextSize = 9f
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
valueTypeface = appContext.resources.getFont(R.font.work_sans_medium)
}
}
// create a LineData object using our LineDataSet
val data = LineData(lineDataSet)
data.apply {
setValueTextColor(R.color.colorPrimary)
setValueTextSize(15f)
}
return data
}
private fun determineSummaryIcon(icon: String): Int {
return when (icon) {
"clear-day" -> R.drawable.ic_sun
"clear-night" -> R.drawable.ic_moon
"rain" -> R.drawable.ic_precipitation
"snow" -> R.drawable.ic_snowflake
"sleet" -> R.drawable.ic_sleet
"wind" -> R.drawable.ic_wind_speed
"fog" -> R.drawable.ic_fog
"cloudy" -> R.drawable.ic_cloud_coverage
"partly-cloudy-day" -> R.drawable.ic_cloudy_day
"partly-cloudy-night" -> R.drawable.ic_cloudy_night
"hail" -> R.drawable.ic_hail
"thunderstorm" -> R.drawable.ic_thunderstorm
"tornado" -> R.drawable.ic_tornado
else -> R.drawable.ic_sun
}
}
}
LazyDeferred:
fun<T> lazyDeferred(block: suspend CoroutineScope.() -> T) : Lazy<Deferred<T>> {
return lazy {
GlobalScope.async {
block.invoke(this)
}
}
}
ScopedFragment :
abstract class ScopedFragment : Fragment(), CoroutineScope {
private lateinit var job: Job
override val coroutineContext: CoroutineContext
get() = job + Dispatchers.Main
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
job = Job()
}
override fun onDestroy() {
job.cancel()
super.onDestroy()
}
}
Without the entire environment it's really hard for me to help you debug the whole thing but I am happy to provide you with a couple of things that seem a little bit off at a first glance.
First of all I would avoid managing all CoroutinesScopes and lifecycles by yourself and it's easy to get it wrong. So I would rely on what the Android team has already done. Take a quick look here, it's really easy to setup and use. The dev experience is great.
Posting Deferred on a LiveData and awaiting on the view side looks like a code smell…
What if there's a network error?
It would result in an exception or cancellation exception being thrown.
What if the task was already perform and causes some type of UI consistency problem? These are a couple of edge cases I would not really want to handle.
Just observe a LiveData since it is its main purpose: it's a value holder and it's intended to live throught several lifecycle events in the Fragment. So once view is recreated the value is ready in the LiveData inside the ViewModel.
Your lazyDeferred function is quite smart but in the Android world it's also dangerous. Those coroutines don't live inside any lifecycle-controlled scope so they have a really high chance to end up being leaked. And trust me, you don't want any coroutines being leaked since they continue their work even after viewmodel and fragment destruction which is something you definetely don't want.
All of these are easily fixable by using the dependency I've mentioned before, which I'll paste here once more
Here's a snippet on how you could use those utilities in your ViewModel to ensure the lifecycle of things nor coroutines are causing any issues:
class WeatherViewModel(
private val forecastRepository: ForecastRepository,
context: Context
) : ViewModel() {
private val appContext = context.applicationContext
// Retrieve the sharedPrefs
val preferences:SharedPreferences
get() = PreferenceManager.getDefaultSharedPreferences(appContext)
// This will run only when currentWeatherData is called from the View
val currentWeatherData = liveData {
val task = viewModelScope.async { forecastRepository.getCurrentWeather() }
emit(task.await())
}
val hourlyWeatherEntries = liveData {
val task = viewModelScope.async { forecastRepository.getHourlyWeather() }
emit(task.await())
}
val weeklyWeatherEntries = liveData {
val task = viewModelScope.async {
val currentDateEpoch = LocalDate.now().toEpochDay()
forecastRepository.getWeekDayWeatherList(currentDateEpoch)
}
emit(task.await())
}
val weatherLocation = liveData {
val task = viewModelScope.async(Dispatchers.IO) {
forecastRepository.getWeatherLocation()
}
emit(task.await())
}
}
By using the following approach all network calls are performed in a parallel fashion and they are all tied to the viewModelScope without writing a single line of handling the CoroutineScope's life. When the ViewModel dies, so will the scope. when the view gets re-created the routines won't execute twice and values will be ready to read.
Regarding the configuration of the chart: I would highly suggest you configure the chart as soon as you have created the view, since it is highly tied together. Configuration is something you want to do just once and may cause visual bugs if some instructions are executed more than once (which I believe it could be happening to you), just saying so because I've had problems with MPAndroid using a Piechart.
More on the chart: All of the logic of constructing the LineData would be better off on a background thread and being exposed through a LiveData in the ViewModel side like you would do with all of the other
val property = liveData {
val deferred = viewModelScope.async(Dispatchers.Default) {
// Heavy building logic like:
createChartData()
}
emit(deferred.await())
}
Pro Kotlin tip: Avoid repeating yourself during those long MPAndroid configurations functions.
Instead of:
view.configureThis()
view.configureThat()
view.enabled = true
Do:
view.apply {
configureThis()
configureThat()
enabled = true
}
I'm sad that I can just give you these hints and being unable to exactly pin-point what your issue is since the bug is heavily related to what is happenning throughout the lifecycle evolution of the runtime but hopefuly this is going to be useful
Answering your comment
If one of your data-streams (LiveData) is dependent on what another data-stream (another LiveData) is going to emit you are looking for LiveData.map and LiveData.switchMap operations.
I imagine that hourlyWeatherEntries is going to be emitting values from time to time.
In that case you can use LiveData.switchMap.
What this does is that everytime the source LiveData emits a value, you're going to get a callback and you are expected to return a new live data with the new value.
You could arrange something like the following:
val hourlyChartData = hourlyWeatherEntries.switchMap { hourlyWeather ->
liveData {
val task = viewModelScope.async(Dispatchers.IO) {
createChartData(hourlyWeather)
}
emit(task.await())
}
}
Using this approach has the benefit that it is completely lazy. That means that NO COMPUTATION is going to take place UNLESS data is being actively observed by some lifecycleOwner. That just means that no callbacks are being triggered unless data is observed in the Fragment
Further explanation on map and switchMap
Since we need to do some asynchronous computation that we don't know when it's going to be done we can't use map. map applies a linear transformation between LiveDatas. Check this out:
val liveDataOfNumbers = liveData { // Returns a LiveData<Int>
viewModelScope.async {
for(i in 0..10) {
emit(i)
delay(1000)
}
}
}
val liveDataOfDoubleNumbers = liveDataOfNumbers.map { number -> number * 2}
This is really useful when the computation is linear and simple. What is happening behind the hood is that the library is handling observing and emitting values for you by means of a MediatorLiveData. What happens here is that whenever liveDataOfNumbers emits a value and liveDataOfDoubleNumbers is being observed the callback gets applied; so the liveDataOfDoubleNumbers is emitting: 0, 2, 4, 6…
The snippet above is equivalent to the following:
val liveDataOfNumbers = liveData { // Returns a LiveData<Int>
viewModelScope.async {
for(i in 0..10) {
emit(i)
delay(1000)
}
}
}
val liveDataOfDoubleNumbers = MediatorLiveData<Int>().apply {
addSource(liveDataOfNumbers) { newNumber ->
// Update MediatorLiveData value when liveDataOfNumbers creates a new number
value = newNumber * 2
}
}
But just using map is much much simpler.
Fantastic!!
Now going to your use-case. Your computation is linear but we want to defer that work to a background coroutine. So we can't exactly tell when something is going to end.
For these use-cases they have created the switchMap operator. What it does it's just the same as map but wraps everything within another LiveData. The intermediate LiveData just acts as a container for the response that is going to come from the coroutine.
So what ends up happening is:
Your coroutine publishes into intermediateLiveData
switchMap does something similar to:
return MediatorLiveData().apply {
// intermediateLiveData is what your callback generates
addSource(intermediateLiveData) { newValue -> this.value = newValue }
} as LiveData
Summing up:
1. Coroutine passes value to intermediateLiveData
2. intermediateLiveData passes value to the hourlyChartData
3. hourlyChartData passes value to the UI
And everything without adding or removing observeForever
Since the liveData {…} is a builder to help us create asynchronous LiveDatas without dealing with the hassle of instantiating them we can use it so our switchMap callback is less verbose.
The function liveData returns a live data of type LiveData<T>. If your repository call already returns a LiveData it's really simple!
val someLiveData = originalLiveData.switchMap { newValue ->
someRepositoryCall(newValue).map { returnedRepoValue -> /*your transformation code here*/}
}
Separate the setupChart and setData logics. Setup chart once out of the observer, inside the observer setData and after that call invalidate().
Try commenting out the invalidate() part and wherever you are calling your search function before it try yourlineChart.clear(); or yourlineChart.clearValues();. This will clear the previous values of the chart and will form chart with new values. So, invalidate() and chart.notifyDataSetChanged() won't not be necessary and it should solve your problem.