Presque un an s'est écoulé depuis que l'équipe Google AndroidX a présenté une nouvelle bibliothèque DataStore pour remplacer la bibliothèque SharedPreferences , mais la vulgarisation de la nouvelle bibliothèque n'est clairement pas une tâche active. Sinon, je ne peux pas expliquer 1) un guide incomplet, à la suite duquel, vous ne construirez pas du tout de projet en raison du manque de toutes les dépendances nécessaires et des tâches de construction supplémentaires pour le système de construction, et 2) l'absence de non-bonjour -World exemples similaires dans CodeLabs, à l'exception d'un, puis affinés non pas pour un exemple d'utilisation de la bibliothèque à partir de zéro, mais pour la migration de SharedPreferences vers le Preferences DataStore... De même, tous les articles sur Medium, littéralement ou en d'autres termes, répètent tout ce qui est écrit dans le guide Google, ou utilisent les mauvaises approches pour travailler avec le DataStore, suggérant d'encapsuler le code io asynchrone dans runBlocking directement sur le thread d'interface utilisateur.
Et ce serait aussi bien de connecter le "arrière" avec le "avant", pour ainsi dire: Google a la bibliothèque AndroidX Preferences du clip Jetpack, qui vous permet de lancer un fragment de conception de matériel prêt à l'emploi en deux clics pour gérer les paramètres de l'application et, d'une manière préférée de génération de code, libérer le développeur de l'écriture de passe-partout ... Cependant, cette bibliothèque propose d'utiliser les SharedPreferences obsolètes comme référentiel, et il n'y a pas de guide officiel pour se connecter au DataStore. Dans cette note, je voudrais éliminer les deux lacunes décrites à ma manière.
Création d'un cadre pour travailler avec le magasin de données
La bibliothèque DataStore est divisée en deux parties : une analogue de la précédente appelée Preferences DataStore, qui stocke les valeurs de paramètres dans des paires clé-valeur et n'est pas de type sécurisé, et la seconde, qui stocke les paramètres dans un fichier de tampons de protocole et est de type sécurisé. Il est plus flexible et polyvalent, je l'ai donc choisi pour mes expériences.
Pour décrire le schéma de paramètres, vous devez créer un fichier supplémentaire dans le projet. Tout d'abord, vous devez basculer le studio ou l'explorateur d'idées en mode Projet afin que toute la structure du dossier soit visible, puis créer un fichier avec l'extension * .proto dans le dossier app / src / main / proto / (et non pb, comme le recommande Google - avec Ni un plugin pour la vérification de la syntaxe, la saisie semi-automatique, etc., ni une tâche de construction qui génère la classe correspondante ne fonctionnera).
Protocol buffer Google, . , :
syntax = "proto3";
option java_package = "...";
option java_multiple_files = true;
message ProtoSettings {
bool translate_to_ru = 1;
map<string, int64> last_sync = 2;
int32 refresh_interval = 3;
}
, , , - -long Kotlin, unix- ( c data , simple name ).
build.gradle- :
plugins {
...
id "com.google.protobuf" version "0.8.12"
}
...
dependencies {
...
//DataStore
implementation "androidx.datastore:datastore:1.0.0-beta01"
implementation "com.google.protobuf:protobuf-javalite:3.11.0"
implementation "androidx.preference:preference-ktx:1.1.1"
}
protobuf {
protoc {
artifact = "com.google.protobuf:protoc:3.11.0"
}
generateProtoTasks {
all().each { task ->
task.builtins {
java {
option 'lite'
}
}
}
}
}
proto- , java DataStore proto.
DataStore: / , Flow. set- builder. Flow , , , collect & Co .
! deprecated- Flow toList toSet, (flow never completes, so this terminal operation never completes).
boilerplate , . , Google , :
@Suppress("BlockingMethodInNonBlockingContext")
object SettingsSerializer : Serializer<ProtoSettings> {
override val defaultValue: ProtoSettings = ProtoSettings.getDefaultInstance()
override suspend fun readFrom(input: InputStream): ProtoSettings {
return try {
ProtoSettings.parseFrom(input)
} catch (exception: InvalidProtocolBufferException) {
Log.e("SETTINGS", "Cannot read proto. Create default.")
defaultValue
}
}
override suspend fun writeTo(t: ProtoSettings, output: OutputStream) = t.writeTo(output)
}
Serializer ( ) .
- , : -, , , -, , , -, Hilt :
class Settings @Inject constructor(val settings: DataStore<ProtoSettings>) {
companion object {
const val HOUR_TO_MILLIS = 60 * 60 * 1000 // hours to milliseconds
const val TRANSLATE_SWITCH = "translate_to_ru"
const val REFRESH_INTERVAL_BAR = "refresh_interval"
const val IS_PREFERENCES_CHANGED = "preferences_changed"
}
val saved get() = settings.data.take(1)
suspend fun translateToRu(value: Boolean) = settings.updateData {
it.toBuilder().setTranslateToRu(value).build()
}
suspend fun saveLastSync(cls: String) = settings.updateData {
it.toBuilder().putLastSync(cls, System.currentTimeMillis()).build()
}
suspend fun refreshInterval(hours: Int) = settings.updateData {
it.toBuilder().setRefreshInterval(hours * HOUR_TO_MILLIS).build()
}
fun checkNeedSync(cls: String) = saved.map {
it.lastSyncMap[cls]?.run {
System.currentTimeMillis() - this > saved.refreshInterval
} ?: true
}
}
@Module
@InstallIn(SingletonComponent::class)
class SettingsModule {
@Provides
@Singleton
fun provideSettings(@ApplicationContext context: Context) = Settings(context.dataStore)
private val Context.dataStore: DataStore<ProtoSettings> by dataStore(
fileName = "settings.proto",
serializer = SettingsSerializer
)
}
, saved, flow take(1). , , . collect, , , emit . first(), flow . last(), , .. flow.
DataStore
. , , . Kotlin , sealed :
sealed class Result
data class Success<out T>(val data: T): Result()
data class Error(val msg: String, val error: ErrorType): Result()
object Loading : Result()
, :
fun <T> fetchItems(
itemsType: String,
remoteApiCallback: suspend () -> Response<ApiResponse<T>>,
localApiCallback: suspend () -> List<T>,
saveApiCallback: suspend (List<T>) -> Unit,
): Flow<Result> = settings.checkNeedSync(itemsType).transform { needSync ->
var remoteFailed = true
emit(Loading)
localApiCallback().let { local ->
if (needSync || local.isEmpty()) {
if (networkHelper.isNetworkConnected()) {
remoteApiCallback().apply {
if (isSuccessful) body()?.docs?.let { remote ->
settings.saveLastSync(itemsType)
remoteFailed = false
emit(Success(remote))
saveApiCallback(remote)
}
else emit(Error(errorBody().toString(), ErrorType.REMOTE_API_ERROR))
}
} else emit(Error("No internet connection!", ErrorType.NO_INTERNET_CONNECTION))
}
if (remoteFailed)
emit(if (local.isNotEmpty()) Success(local) else Error("No local saved data", ErrorType.NO_SAVED_DATA))
}
}
.flowOn(Dispatchers.IO)
.catch { e ->
...
}
( ) : , . :
fun getSomething() = fetchItems<Something>("Something", remoteApi::getSomething, localApi::getSomething, localApi::saveSomething)
fun getSmthOther() = fetchItems<Other>("Other", remoteApi::getSmthOther, localApi::getSmthOther, localApi::saveSmthOther)
, reified , , T::class.simpleName, inline, crossinline/noinline, . inline , , /, .
checkNeedSync flow, SettingsRepository, flow Result transform. : Loading ( ui - ), . , , , . , checkNeedSync (take (1)), emit - checkNeedSync fetchItems. - , , , . , .
androidX . AndroidX Preference User interface/Settings, SharedPreferences ( Google DataStore PreferenceDataStore).
preferences.xml
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:android="http://schemas.android.com/apk/res/android">
<PreferenceCategory android:title="@string/experimentalTitle">
<SwitchPreferenceCompat
android:defaultValue="false"
android:key="translate_to_ru"
android:summaryOff="@string/aiTranslateOffText"
android:summaryOn="@string/aiTranslateOnText"
android:title="@string/aiTranslateTitle" />
</PreferenceCategory>
<PreferenceCategory android:title="@string/synchronizeTitle">
<SeekBarPreference
android:defaultValue="2"
android:key="refresh_interval"
android:title="@string/refreshIntervalTitle"
android:summary="@string/refreshSummary"
android:max="24"
app:min="0"
app:seekBarIncrement="1"
app:showSeekBarValue="true" />
</PreferenceCategory>
</PreferenceScreen>
:
material design , guides. , summaryOff/summaryOn - , , . default value. key, .
Navigation . , , :
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
...
R.id.preferences -> findNavController().navigate(MainFragmentDirections.actionShowPreferences())
}
return super.onOptionsItemSelected(item)
}
( , , ), Navigation SavedStateHandle, onCreateView observer BackStack':
findNavController().currentBackStackEntry?.let {
it.savedStateHandle.getLiveData<Boolean>(Settings.IS_PREFERENCES_CHANGED).observe(viewLifecycleOwner) { isChanged ->
if (isChanged) {
viewModel.armRefresh()
it.savedStateHandle.remove<Boolean>(Settings.IS_PREFERENCES_CHANGED)
}
}
}
, , .. LiveData, , .
, DataStore savedStateHandle . findPreference, findViewById, setOnPreferenceChangeListener:
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.preferences, rootKey)
requireActivity().title = getString(R.string.preferencesTitle)
val translateSwitch = findPreference<SwitchPreferenceCompat>(Settings.TRANSLATE_SWITCH)?.apply {
setOnPreferenceChangeListener { _, value ->
lifecycleScope.launch { settings.translateToRu(value as Boolean) }
findNavController().previousBackStackEntry?.let {
it.savedStateHandle[Settings.IS_PREFERENCES_CHANGED] = true
}
true
}
}
val refreshSeekBar = findPreference<SeekBarPreference>(Settings.REFRESH_INTERVAL_BAR)?.apply {
setOnPreferenceChangeListener { _, value ->
lifecycleScope.launch { settings.refreshInterval(value as Int) }
findNavController().previousBackStackEntry?.let {
it.savedStateHandle[Settings.IS_PREFERENCES_CHANGED] = true
}
true
}
}
settings.saved.collectOnFragment(this) {
translateSwitch?.isChecked = it.translateToRu
refreshSeekBar?.value = it.refreshInterval / Settings.HOUR_TO_MILLIS
}
}
collectOnFragment flow
fun <T> Flow<T>.collectOnFragment(
fragment: Fragment,
state: Lifecycle.State = Lifecycle.State.RESUMED,
block: (T) -> Unit
) {
fragment.lifecycleScope.launch {
flowWithLifecycle(fragment.lifecycle, state)
.collect {
block(it)
}
}
}
, setOnPreferenceChangeListener value Any, value as Boolean value as Int, .
. , Kotlin DataStore, runBlocking , 4-min-to-read- ( Google, ).
, Jetpack- ui c material design .
Il y a des endroits dans les sections de code que je n'ai pas commencé à expliquer ou à citer complètement par manque d'importance ou d'évidence (par exemple, la valeur de la constante HOUR_TO_MILLIS), mais si vous ne pouvez pas construire un projet similaire selon ma recette, écrivez dans le commentaires, je vais essayer d'ajouter tous les endroits obscurs... Notez que j'ai pris toutes les parties du code d'un projet entièrement fonctionnel et testé, vous ne devriez donc pas vous soucier de ses performances.
Merci d'avoir lu.