Below is my viewmodel class body
private var _movieState = mutableStateOf(false)
val movieState = _movieState
private val query = if (_movieState.value) ListState.POPULAR_PLAYING else ListState.ALL_PLAYING
val moviesData: Flow<PagingData<Movie>> = Pager(PagingConfig(pageSize = 10)) {
MoviePagingSource(movieRepository, query.string)
}.flow
And I have Two function which populate the Boolean value
fun setListToPopular(){
_movieState.value = true
}
fun setListToAllNowPlaying(){
_movieState.value = false
}
which will be triger from UI
Now i have one mistake once the code of viewmodel runs my if block is over but i want to observe that Boolean state within it.
how should i do that ?
Related
So I have Two ViewModels in my Calculator App in which I all reference in my Compose NavGraph so I can use the same ViewModel instance. I set a Boolean State(historyCheck) in the first ViewModel and I set it too true to "Clear" the History I set which is a history of Calculations I am trying to retrieve from both ViewModels. The issue now is that the Boolean State "strCalcViewModel.historyCheck" changes before the variable above it get's assigned which then makes the 'if' statement I setup fail which in turns makes the whole Implementation also fail as it is always set to false.
This is my code Below...
My Compose NavGraph.
#Composable
fun ComposeNavigation(
navController: NavHostController,
) {
/**
* Here We declare an Instance of our Two ViewModels, their states and History States. This is because we don't want to have the same States for the two Screens.
*/
val strCalcViewModel = viewModel<CalculatorViewModel>()
val sciCalcViewModel = viewModel<ScientificCalculatorViewModel>()
val strCalcState = strCalcViewModel.strState
val sciCalcState = sciCalcViewModel.sciState
val strHistoryState = strCalcViewModel.historyState
val sciHistoryState = sciCalcViewModel.historyState
// This holds our current available 'HistoryState' based on where the Calculation was performed(Screens) by the USER.
var currHistory by remember { mutableStateOf(CalculatorHistoryState()) }
if(strCalcViewModel.historyCheck) {
currHistory = strHistoryState
strCalcViewModel.historyCheck = false // this gets assigned before the 'currHistory' variable above thereBy making the the if to always be false
} else {
currHistory = sciHistoryState
}
NavHost(
navController = navController,
startDestination = "main_screen",
) {
composable("main_screen") {
MainScreen(
navController = navController, state = strCalcState, viewModel = strCalcViewModel
)
}
composable("first_screen") {
FirstScreen(
navController = navController, state = sciCalcState, viewModel = sciCalcViewModel
)
}
composable("second_screen") {
SecondScreen(
navController = navController, historyState = currHistory, viewModel = strCalcViewModel
)
}
}
}
Then my ViewModel
private const val TAG = "CalculatorViewModel"
class CalculatorViewModel : ViewModel() {
var strState by mutableStateOf(CalculatorState())
// This makes our state accessible by outside classes but still readable
private set
var historyState by mutableStateOf(CalculatorHistoryState())
private set
private var leftBracket by mutableStateOf(true)
private var check = 0
var checkState by mutableStateOf(false)
var historyCheck by mutableStateOf(false)
// Function to Register our Click events
fun onAction(action : CalculatorAction) {
when(action) {
is CalculatorAction.Number -> enterNumber(action.number)
is CalculatorAction.Decimal -> enterDecimal()
is CalculatorAction.Clear -> {
strState = CalculatorState()
check = 0
}
is CalculatorAction.ClearHistory -> checkState = true
is CalculatorAction.Operation -> enterStandardOperations(action.operation)
is CalculatorAction.Calculate -> performStandardCalculations()
is CalculatorAction.Delete -> performDeletion()
is CalculatorAction.Brackets -> enterBrackets()
}
}
// We are Basically making the click events possible by modifying the 'state'
private fun performStandardCalculations() {
val primaryStateChar = strState.primaryTextState.last()
val primaryState = strState.primaryTextState
val secondaryState = strState.secondaryTextState
if (!(primaryStateChar == '(' || primaryStateChar == '%')) {
strState = strState.copy(
primaryTextState = secondaryState
)
strState = strState.copy(secondaryTextState = "")
// Below, we store our Calculated Values in the History Screen after it has been Calculated by the USER.
historyState = historyState.copy(
historySecondaryState = secondaryState
)
historyState = historyState.copy(
historyPrimaryState = primaryState
)
historyCheck = true // this is where I assign it to true when I complete my Calculations and pass it to the history State
} else {
strState = strState.copy(
secondaryTextState = "Format error"
)
strState = strState.copy(
color = ferrari
)
}
}
}
You're checking the if condition and assigning a new value to your viewModel variable in the Compose function, it's not correct! you should use side-effects
LaunchedEffect(strCalcViewModel.historyCheck) {
if(strCalcViewModel.historyCheck) {
currHistory = strHistoryState
strCalcViewModel.historyCheck = false
} else {
currHistory = sciHistoryState
}}
Whenever there is a new change in strCalcViewModel.historyCheck this block will run
you can check out here for more info Side-effects in Compose
Based on Sadegh.t's Answer I got it working but didn't write it the exact same way and used a different Implementation which I will post now.
I Still used a side-effect but instead of checking for a change in the "historyCheck", I checked for a change in the 'State' itself and also instead of using a Boolean variable, I used the State itself for the basis of the Condition. So here is my answer based on Sadegh.t's original answer.
var currHistory by remember { mutableStateOf(CalculatorHistoryState()) }
LaunchedEffect(key1 = strCalcState) {
if(strCalcState.secondaryTextState.isEmpty()) {
currHistory = strHistoryState
}
}
LaunchedEffect(key1 = sciCalcState) {
if(sciCalcState.secondaryTextState.isEmpty()) {
currHistory = sciHistoryState
}
}
I made a state with StateFlow with 2 lists. This is working good. I want to sort these lists according to a parameter that user will decide how to sort.
This is my code in ViewModel:
#HiltViewModel
class SubscriptionsViewModel #Inject constructor(
subscriptionsRepository: SubscriptionsRepository
) : ViewModel() {
private val _sortState = MutableStateFlow(
SortSubsType.ByDate
)
val sortState: StateFlow<SortSubsType> = _sortState.asStateFlow()
val uiState: StateFlow<SubscriptionsUiState> = combine(
subscriptionsRepository.getActiveSubscriptionsStream(_sortState.value),
subscriptionsRepository.getArchivedSubscriptionsStream(_sortState.value)
) { activeSubscriptions, archiveSubscriptions ->
SubscriptionsUiState.Subscriptions(
activeSubscriptions = activeSubscriptions,
archiveSubscriptions = archiveSubscriptions,
)
}
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = SubscriptionsUiState.Loading
)
fun sortSubscriptions(sortType: SortSubsType) {
_sortState.value = sortType
}
}
sealed interface SubscriptionsUiState {
object Loading : SubscriptionsUiState
data class Subscriptions(
val activeSubscriptions: List<Subscription>,
val archiveSubscriptions: List<Subscription>,
) : SubscriptionsUiState
object Empty : SubscriptionsUiState
}
sortSubscriptions - is the function called from #Composable screen. Like this:
fun sortSubscriptions() {
viewModel.sortSubscriptions(sortType = selectedSortType.asSortSubsType())
isSortDialogVisible = false
}
Without the sort function, everything works. My question is how to fix this code so that the state changes when the sortState is changed. This is my first try working with StateFlow.
The problem is that when you create your uiState flow with combine, you just use the current value of sortState and never react to its changes.
You need something like this:
val uiState = sortState.flatMapLatest { sortValue ->
combine(
getActiveSubscriptionsStream(sortValue),
getArchivedSubscriptionsStream(sortValue)
) { ... }
}.stateIn(...)
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
I'm have a LazyColumn that renders a list of items. However, I now want to fetch more items to add to my lazy list. I don't want to re-render items that have already been rendered in the LazyColumn, I just want to add the new items.
How do I do this with a StateFlow? I need to pass a page String to fetch the next group of items, but how do I pass a page into the repository.getContent() method?
class FeedViewModel(
private val resources: Resources,
private val repository: FeedRepository
) : ViewModel() {
// I need to pass a parameter to `repository.getContent()` to get the next block of items
private val _uiState: StateFlow<UiState> = repository.getContent()
.map { content ->
UiState.Ready(content)
}.catch { cause ->
UiState.Error(cause.message ?: resources.getString(R.string.error_generic))
}.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(stopTimeoutMillis = SUBSCRIBE_TIMEOUT_FOR_CONFIG_CHANGE),
initialValue = UiState.Loading
)
val uiState: StateFlow<UiState>
get() = _uiState
And in my UI, I have this code to observe the flow and render the LazyColumn:
val lifecycleAwareUiStateFlow: Flow<UiState> = remember(viewModel.uiState, lifecycleOwner) {
viewModel.uiState.flowWithLifecycle(lifecycleOwner.lifecycle, Lifecycle.State.STARTED)
}
val uiState: UiState by lifecycleAwareUiStateFlow.collectAsState(initial = UiState.Loading)
#Composable
fun FeedLazyColumn(
posts: List<Post> = listOf(),
scrollState: LazyListState
) {
LazyColumn(
modifier = Modifier.padding(vertical = 4.dp),
state = scrollState
) {
// how to add more posts???
items(items = posts) { post ->
Card(post)
}
}
}
I do realize there is a Paging library for Compose, but I'm trying to implement something similar, except the user is in charge of whether or not to load next items.
This is the desired behavior:
I was able to solve this by adding the new posts to the old posts before emitting it. See comments below for the relevant lines.
private val _content = MutableStateFlow<Content>(Content())
private val _uiState: StateFlow<UiState> = repository.getContent()
.mapLatest { content ->
_content.value = _content.value + content // ADDED THIS
UiState.Ready(_content.value)
}.catch { cause ->
UiState.Error(cause.message ?: resources.getString(R.string.error_generic))
}.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(stopTimeoutMillis = SUBSCRIBE_TIMEOUT_FOR_CONFIG_CHANGE),
initialValue = UiState.Loading
)
private operator fun Content.plus(content: Content): Content = Content(
posts = this.posts + content.posts,
youTubeNextPageToken = content.youTubeNextPageToken
)
class YouTubeDataSource(private val apiService: YouTubeApiService) :
RemoteDataSource<YouTubeResponse> {
private val nextPageToken = MutableStateFlow<String?>(null)
fun setNextPageToken(nextPageToken: String) {
this.nextPageToken.value = nextPageToken
}
override fun getContent(): Flow<YouTubeResponse> = flow {
// retrigger emit when nextPageToken changes
nextPageToken.collect {
emit(apiService.getYouTubeSnippets(it))
}
}
}
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.