【问题标题】:Kotlin coroutines. Kotlin Flow and shared preferences. awaitClose is never calledKotlin 协程。 Kotlin Flow 和共享偏好。 awaitClose 永远不会被调用
【发布时间】:2021-07-24 12:23:34
【问题描述】:

我很想观察共同偏好的变化。以下是我使用 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)
}

视图模型

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,这意味着它们的行为可以改变)

【问题讨论】:

    标签: android kotlin callback kotlin-coroutines kotlin-flow


    【解决方案1】:

    我找到了一个解决方案,可以让我保存一个简单的数据集(例如首选项)并使用 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"
    

    【讨论】:

      【解决方案2】:

      问题是,您正在注入 ViewModel,就好像它只是一个普通类一样,通过使用

      @Inject
      lateinit var bestTimeViewModel: BestTimeViewModel
      

      因此,ViewModel 的 viewModelScope 永远不会被取消,因此 Flow 会被永久收集。

      根据Documentation,您应该使用

      privat val bestTimeViewModel: BestTimeViewModel by viewModels()
      

      这样可以确保在销毁 Fragment 时调用 ViewModel 的 onCleared 方法,该方法反过来会取消 viewModelScope

      还要确保您的 ViewModel 带有 @HiltViewModel 注释:

      @HiltViewModel
      class BestTimeViewModel @Inject constructor(...) : ViewModel()
      

      【讨论】:

      猜你喜欢
      • 2018-01-10
      • 2019-07-15
      • 1970-01-01
      • 2018-05-11
      • 2019-07-24
      • 1970-01-01
      • 2018-09-02
      • 1970-01-01
      • 2011-08-19
      相关资源
      最近更新 更多