Kotlin 协程。 Kotlin Flow 和共享首选项。 awaitClose 永远不会被调用
Kotlin coroutines. Kotlin Flow and shared preferences. awaitClose is never called
我很乐意观察共同偏好的变化。以下是我使用 Kotlin Flow 的方法:
数据源。
interface DataSource {
fun bestTime(): Flow<Long>
fun setBestTime(time: Long)
}
class LocalDataSource @Inject constructor(
@ActivityContext context: Context
) : DataSource {
private val preferences = context.getSharedPreferences(PREFS_FILE_NAME, Context.MODE_PRIVATE)
@ExperimentalCoroutinesApi
override fun bestTime() = callbackFlow {
trySendBlocking(preferences, PREF_KEY_BEST_TIME)
val listener = SharedPreferences.OnSharedPreferenceChangeListener { sharedPreferences, key ->
if (key == PREF_KEY_BEST_TIME) {
trySendBlocking(sharedPreferences, key)
}
}
preferences.registerOnSharedPreferenceChangeListener(listener)
awaitClose { // NEVER CALLED
preferences.unregisterOnSharedPreferenceChangeListener(listener)
}
}
@ExperimentalCoroutinesApi
private fun ProducerScope<Long>.trySendBlocking(
sharedPreferences: SharedPreferences,
key: String?
) {
trySendBlocking(sharedPreferences.getLong(key, 0L))
.onSuccess { }
.onFailure {
Log.e(TAG, "", it)
}
}
override fun setBestTime(time: Long) = preferences.edit {
putLong(PREF_KEY_BEST_TIME, time)
}
companion object {
private const val TAG = "LocalDataSource"
private const val PREFS_FILE_NAME = "PREFS_FILE_NAME"
private const val PREF_KEY_BEST_TIME = "PREF_KEY_BEST_TIME"
}
}
存储库
interface Repository {
fun observeBestTime(): Flow<Long>
fun setBestTime(bestTime: Long)
}
class RepositoryImpl @Inject constructor(
private val dataSource: DataSource
) : Repository {
override fun observeBestTime() = dataSource.bestTime()
override fun setBestTime(bestTime: Long) = dataSource.setBestTime(bestTime)
}
ViewModel
class BestTimeViewModel @Inject constructor(
private val repository: Repository
) : ViewModel() {
// Backing property to avoid state updates from other classes
private val _uiState = MutableStateFlow(0L)
val uiState: StateFlow<Long> = _uiState
init {
viewModelScope.launch {
repository.observeBestTime()
.onCompletion { // CALLED WHEN THE SCREEN IS ROTATED OR HOME BUTTON PRESSED
Log.d("myTag", "viewModelScope onCompletion")
}
.collect { bestTime ->
_uiState.value = bestTime
}
}
}
fun setBestTime(time: Long) = repository.setBestTime(time)
}
片段。
@AndroidEntryPoint
class MetaDataFragment : Fragment(R.layout.fragment_meta_data) {
@Inject
lateinit var timeFormatter: TimeFormatter
@Inject
lateinit var bestTimeViewModel: BestTimeViewModel
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val bestTimeView = view.findViewById<TextView>(R.id.best_time_value)
// Create a new coroutine in the lifecycleScope
viewLifecycleOwner.lifecycleScope.launch {
// repeatOnLifecycle launches the block in a new coroutine every time the
// lifecycle is in the STARTED state (or above) and cancels it when it's STOPPED.
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
// Trigger the flow and start listening for values.
// This happens when lifecycle is STARTED and stops
// collecting when the lifecycle is STOPPED
bestTimeViewModel.uiState
.map { millis ->
timeFormatter.format(millis)
}
.onCompletion { // CALLED WHEN THE SCREEN IS ROTATED OR HOME BUTTON PRESSED
Log.d("MyApp", "onCompletion")
}
.collect {
bestTimeView.text = it
}
}
}
}
}
我注意到 awaitClose
从未被调用过。但这是我的清理代码所在的位置。请指教。如果首先使用 callbackFlow
不是一个好主意,请告诉我(因为您可以看到一些函数是 ExperimentalCoroutinesApi
,这意味着它们的行为可以改变)
问题是,您通过使用
将 ViewModel 当作普通 class 注入
@Inject
lateinit var bestTimeViewModel: BestTimeViewModel
因此,ViewModel 的 viewModelScope
永远不会被取消,因此 Flow 会被永久收集。
根据 Documentation,您应该使用
privat val bestTimeViewModel: BestTimeViewModel by viewModels()
这确保了 ViewModel 的 onCleared
方法会在您的 Fragment 被销毁时被调用,该方法反过来会取消 viewModelScope
。
还要确保您的 ViewModel 带有注释 @HiltViewModel
:
@HiltViewModel
class BestTimeViewModel @Inject constructor(...) : ViewModel()
我找到了一个解决方案,它允许我保存一个简单的数据集(例如偏好)并使用 Kotlin Flow 观察它的变化。是 Preferences DataStore
。
这是我使用的代码实验室和指南:
https://developer.android.com/codelabs/android-preferences-datastore#0
https://developer.android.com/topic/libraries/architecture/datastore
这是我的代码:
import android.content.Context
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.emptyPreferences
import androidx.datastore.preferences.core.longPreferencesKey
import androidx.datastore.preferences.preferencesDataStore
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.map
import java.io.IOException
data class UserPreferences(val bestTime: Long)
private const val USER_PREFERENCES_NAME = "user_preferences"
private val Context.dataStore by preferencesDataStore(
name = USER_PREFERENCES_NAME
)
interface DataSource {
fun userPreferencesFlow(): Flow<UserPreferences>
suspend fun updateBestTime(newBestTime: Long)
}
class LocalDataSource(
@ApplicationContext private val context: Context,
) : DataSource {
override fun userPreferencesFlow(): Flow<UserPreferences> =
context.dataStore.data
.catch { exception ->
// dataStore.data throws an IOException when an error is encountered when reading data
if (exception is IOException) {
emit(emptyPreferences())
} else {
throw exception
}
}
.map { preferences ->
val bestTime = preferences[PreferencesKeys.BEST_TIME] ?: 0L
UserPreferences(bestTime)
}
override suspend fun updateBestTime(newBestTime: Long) {
context.dataStore.edit { preferences ->
preferences[PreferencesKeys.BEST_TIME] = newBestTime
}
}
}
private object PreferencesKeys {
val BEST_TIME = longPreferencesKey("BEST_TIME")
}
以及要添加到 build.gradle
的依赖项:
implementation "androidx.datastore:datastore-preferences:1.0.0"
我很乐意观察共同偏好的变化。以下是我使用 Kotlin Flow 的方法:
数据源。
interface DataSource {
fun bestTime(): Flow<Long>
fun setBestTime(time: Long)
}
class LocalDataSource @Inject constructor(
@ActivityContext context: Context
) : DataSource {
private val preferences = context.getSharedPreferences(PREFS_FILE_NAME, Context.MODE_PRIVATE)
@ExperimentalCoroutinesApi
override fun bestTime() = callbackFlow {
trySendBlocking(preferences, PREF_KEY_BEST_TIME)
val listener = SharedPreferences.OnSharedPreferenceChangeListener { sharedPreferences, key ->
if (key == PREF_KEY_BEST_TIME) {
trySendBlocking(sharedPreferences, key)
}
}
preferences.registerOnSharedPreferenceChangeListener(listener)
awaitClose { // NEVER CALLED
preferences.unregisterOnSharedPreferenceChangeListener(listener)
}
}
@ExperimentalCoroutinesApi
private fun ProducerScope<Long>.trySendBlocking(
sharedPreferences: SharedPreferences,
key: String?
) {
trySendBlocking(sharedPreferences.getLong(key, 0L))
.onSuccess { }
.onFailure {
Log.e(TAG, "", it)
}
}
override fun setBestTime(time: Long) = preferences.edit {
putLong(PREF_KEY_BEST_TIME, time)
}
companion object {
private const val TAG = "LocalDataSource"
private const val PREFS_FILE_NAME = "PREFS_FILE_NAME"
private const val PREF_KEY_BEST_TIME = "PREF_KEY_BEST_TIME"
}
}
存储库
interface Repository {
fun observeBestTime(): Flow<Long>
fun setBestTime(bestTime: Long)
}
class RepositoryImpl @Inject constructor(
private val dataSource: DataSource
) : Repository {
override fun observeBestTime() = dataSource.bestTime()
override fun setBestTime(bestTime: Long) = dataSource.setBestTime(bestTime)
}
ViewModel
class BestTimeViewModel @Inject constructor(
private val repository: Repository
) : ViewModel() {
// Backing property to avoid state updates from other classes
private val _uiState = MutableStateFlow(0L)
val uiState: StateFlow<Long> = _uiState
init {
viewModelScope.launch {
repository.observeBestTime()
.onCompletion { // CALLED WHEN THE SCREEN IS ROTATED OR HOME BUTTON PRESSED
Log.d("myTag", "viewModelScope onCompletion")
}
.collect { bestTime ->
_uiState.value = bestTime
}
}
}
fun setBestTime(time: Long) = repository.setBestTime(time)
}
片段。
@AndroidEntryPoint
class MetaDataFragment : Fragment(R.layout.fragment_meta_data) {
@Inject
lateinit var timeFormatter: TimeFormatter
@Inject
lateinit var bestTimeViewModel: BestTimeViewModel
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val bestTimeView = view.findViewById<TextView>(R.id.best_time_value)
// Create a new coroutine in the lifecycleScope
viewLifecycleOwner.lifecycleScope.launch {
// repeatOnLifecycle launches the block in a new coroutine every time the
// lifecycle is in the STARTED state (or above) and cancels it when it's STOPPED.
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
// Trigger the flow and start listening for values.
// This happens when lifecycle is STARTED and stops
// collecting when the lifecycle is STOPPED
bestTimeViewModel.uiState
.map { millis ->
timeFormatter.format(millis)
}
.onCompletion { // CALLED WHEN THE SCREEN IS ROTATED OR HOME BUTTON PRESSED
Log.d("MyApp", "onCompletion")
}
.collect {
bestTimeView.text = it
}
}
}
}
}
我注意到 awaitClose
从未被调用过。但这是我的清理代码所在的位置。请指教。如果首先使用 callbackFlow
不是一个好主意,请告诉我(因为您可以看到一些函数是 ExperimentalCoroutinesApi
,这意味着它们的行为可以改变)
问题是,您通过使用
将 ViewModel 当作普通 class 注入@Inject
lateinit var bestTimeViewModel: BestTimeViewModel
因此,ViewModel 的 viewModelScope
永远不会被取消,因此 Flow 会被永久收集。
根据 Documentation,您应该使用
privat val bestTimeViewModel: BestTimeViewModel by viewModels()
这确保了 ViewModel 的 onCleared
方法会在您的 Fragment 被销毁时被调用,该方法反过来会取消 viewModelScope
。
还要确保您的 ViewModel 带有注释 @HiltViewModel
:
@HiltViewModel
class BestTimeViewModel @Inject constructor(...) : ViewModel()
我找到了一个解决方案,它允许我保存一个简单的数据集(例如偏好)并使用 Kotlin Flow 观察它的变化。是 Preferences DataStore
。
这是我使用的代码实验室和指南:
https://developer.android.com/codelabs/android-preferences-datastore#0
https://developer.android.com/topic/libraries/architecture/datastore
这是我的代码:
import android.content.Context
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.emptyPreferences
import androidx.datastore.preferences.core.longPreferencesKey
import androidx.datastore.preferences.preferencesDataStore
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.map
import java.io.IOException
data class UserPreferences(val bestTime: Long)
private const val USER_PREFERENCES_NAME = "user_preferences"
private val Context.dataStore by preferencesDataStore(
name = USER_PREFERENCES_NAME
)
interface DataSource {
fun userPreferencesFlow(): Flow<UserPreferences>
suspend fun updateBestTime(newBestTime: Long)
}
class LocalDataSource(
@ApplicationContext private val context: Context,
) : DataSource {
override fun userPreferencesFlow(): Flow<UserPreferences> =
context.dataStore.data
.catch { exception ->
// dataStore.data throws an IOException when an error is encountered when reading data
if (exception is IOException) {
emit(emptyPreferences())
} else {
throw exception
}
}
.map { preferences ->
val bestTime = preferences[PreferencesKeys.BEST_TIME] ?: 0L
UserPreferences(bestTime)
}
override suspend fun updateBestTime(newBestTime: Long) {
context.dataStore.edit { preferences ->
preferences[PreferencesKeys.BEST_TIME] = newBestTime
}
}
}
private object PreferencesKeys {
val BEST_TIME = longPreferencesKey("BEST_TIME")
}
以及要添加到 build.gradle
的依赖项:
implementation "androidx.datastore:datastore-preferences:1.0.0"