Properly scope the coroutine scope of the WeatherAppSample

ViewModel's scope is now tied to the scope of a WeatherAppSample Composable. Once WeatherAppSample Composable exits the composition tree, the used view model coroutine scope will be disposed.
This commit is contained in:
Nebojsa Vuksic 2025-08-07 10:08:07 +02:00
parent 85f3a32137
commit c137351389
2 changed files with 78 additions and 17 deletions

View File

@ -1,28 +1,52 @@
package org.jetbrains.plugins.template.toolWindow package org.jetbrains.plugins.template.toolWindow
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.remember
import com.intellij.openapi.components.service import com.intellij.openapi.components.service
import com.intellij.openapi.project.DumbAware import com.intellij.openapi.project.DumbAware
import com.intellij.openapi.project.Project import com.intellij.openapi.project.Project
import com.intellij.openapi.wm.ToolWindow import com.intellij.openapi.wm.ToolWindow
import com.intellij.openapi.wm.ToolWindowFactory import com.intellij.openapi.wm.ToolWindowFactory
import kotlinx.coroutines.Dispatchers
import org.jetbrains.jewel.bridge.addComposeTab import org.jetbrains.jewel.bridge.addComposeTab
import org.jetbrains.plugins.template.CoroutineScopeHolder
import org.jetbrains.plugins.template.ui.ChatAppSample import org.jetbrains.plugins.template.ui.ChatAppSample
import org.jetbrains.plugins.template.weatherApp.model.Location
import org.jetbrains.plugins.template.weatherApp.services.LocationsProvider import org.jetbrains.plugins.template.weatherApp.services.LocationsProvider
import org.jetbrains.plugins.template.weatherApp.services.MyLocationsViewModel import org.jetbrains.plugins.template.weatherApp.services.MyLocationsViewModel
import org.jetbrains.plugins.template.weatherApp.services.WeatherForecastService
import org.jetbrains.plugins.template.weatherApp.ui.WeatherAppSample import org.jetbrains.plugins.template.weatherApp.ui.WeatherAppSample
class ComposeSamplesToolWindowFactory : ToolWindowFactory, DumbAware { class ComposeSamplesToolWindowFactory : ToolWindowFactory, DumbAware {
override fun createToolWindowContent(project: Project, toolWindow: ToolWindow) { override fun createToolWindowContent(project: Project, toolWindow: ToolWindow) {
val coroutineScopeHolder = project.service<CoroutineScopeHolder>()
toolWindow.addComposeTab("Weather App") { toolWindow.addComposeTab("Weather App") {
val viewModel = service<MyLocationsViewModel>() val locationProviderApi = remember { service<LocationsProvider>() }
val locationProviderApi = service<LocationsProvider>() val viewModel = remember {
val weatherForecastServiceApi = WeatherForecastService(Dispatchers.IO)
MyLocationsViewModel(
listOf(Location("Munich", "Germany")),
coroutineScopeHolder
.createScope(MyLocationsViewModel::class.java.simpleName),
weatherForecastServiceApi
)
}
DisposableEffect(Unit) {
viewModel.onReloadWeatherForecast()
onDispose { viewModel.cancel() }
}
WeatherAppSample( WeatherAppSample(
viewModel, viewModel,
viewModel, viewModel,
locationProviderApi locationProviderApi
) )
} }
toolWindow.addComposeTab("Chat App") { ChatAppSample() } toolWindow.addComposeTab("Chat App") { ChatAppSample() }
} }

View File

@ -1,9 +1,11 @@
package org.jetbrains.plugins.template.weatherApp.services package org.jetbrains.plugins.template.weatherApp.services
import com.intellij.openapi.components.Service import com.intellij.openapi.application.EDT
import com.intellij.openapi.components.service
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import org.jetbrains.plugins.template.weatherApp.model.Location import org.jetbrains.plugins.template.weatherApp.model.Location
import org.jetbrains.plugins.template.weatherApp.model.SelectableLocation import org.jetbrains.plugins.template.weatherApp.model.SelectableLocation
import org.jetbrains.plugins.template.weatherApp.model.WeatherForecastData import org.jetbrains.plugins.template.weatherApp.model.WeatherForecastData
@ -38,27 +40,48 @@ interface WeatherViewModelApi {
fun onReloadWeatherForecast() fun onReloadWeatherForecast()
} }
@Service /**
class MyLocationsViewModel(cs: CoroutineScope) : MyLocationsViewModelApi, WeatherViewModelApi { * A ViewModel responsible for managing the user's locations and corresponding weather data.
*
* This class coordinates the interaction between the UI, locations, and weather data. It provides
* functionality to add, delete, select locations, and reload weather forecasts. Additionally, it
* supplies observable state flows for the list of selectable locations and the currently selected
* location's weather forecast.
*
* @property myInitialLocations The initial list of user-defined locations.
* @property viewModelScope The coroutine scope in which this ViewModel operates.
* @property weatherService The service responsible for fetching weather forecasts for given locations.
*/
class MyLocationsViewModel(
myInitialLocations: List<Location>,
private val viewModelScope: CoroutineScope,
private val weatherService: WeatherForecastServiceApi,
) : MyLocationsViewModelApi, WeatherViewModelApi {
private val weatherService = service<WeatherForecastService>() private val myLocations = MutableStateFlow(myInitialLocations)
private val myLocations = MutableStateFlow(listOf(Location("Munich", "Germany")))
private val selectedLocationIndex = MutableStateFlow(myLocations.value.lastIndex) private val selectedLocationIndex = MutableStateFlow(myLocations.value.lastIndex)
override val weatherForecast: Flow<WeatherForecastData> = weatherService.weatherForecast private val _weatherForecast = MutableStateFlow(WeatherForecastData.EMPTY)
/**
* A stream of weather forecast data that emits updates whenever the forecast changes.
*
* This property exposes a Flow of [WeatherForecastData], which allows consumers to observe
* the weather forecast information for a selected location.
*/
override val weatherForecast: Flow<WeatherForecastData> = _weatherForecast.asStateFlow()
/**
* A [StateFlow] that emits a list of [SelectableLocation] objects representing the user's
* current locations along with the selection state of each location.
*/
override val myLocationsFlow: StateFlow<List<SelectableLocation>> = myLocations override val myLocationsFlow: StateFlow<List<SelectableLocation>> = myLocations
.combine(selectedLocationIndex) { locations, selectedIndex -> .combine(selectedLocationIndex) { locations, selectedIndex ->
locations.mapIndexed { index, location -> locations.mapIndexed { index, location ->
SelectableLocation(location, index == selectedIndex) SelectableLocation(location, index == selectedIndex)
} }
}.stateIn(cs, SharingStarted.WhileSubscribed(), emptyList()) }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList())
init {
onReloadWeatherForecast()
}
override fun onAddLocation(locationToAdd: Location) { override fun onAddLocation(locationToAdd: Location) {
if (myLocations.value.contains(locationToAdd)) { if (myLocations.value.contains(locationToAdd)) {
@ -98,6 +121,20 @@ class MyLocationsViewModel(cs: CoroutineScope) : MyLocationsViewModelApi, Weathe
} }
override fun onLoadWeatherForecast(location: Location) { override fun onLoadWeatherForecast(location: Location) {
weatherService.loadWeatherForecastFor(location) viewModelScope.launch {
val weatherForecastData = weatherService.loadWeatherForecastFor(location).getOrNull() ?: return@launch
_weatherForecast.value = weatherForecastData
}
} }
}
/**
* Cancels all coroutines running within the context of the ViewModel's scope.
*
* This method is used to release resources and stop ongoing tasks when the ViewModel
* is no longer needed, ensuring proper cleanup of coroutine-based operations.
*/
fun cancel() {
viewModelScope.cancel()
}
}