Preferencias de Proto DataStore + AndroidX en Kotlin

Ha pasado casi un a帽o desde que el equipo de Google AndroidX present贸 una nueva biblioteca DataStore para reemplazar la biblioteca SharedPreferences , pero la popularizaci贸n de la nueva biblioteca claramente no es una tarea activa. De lo contrario, no puedo explicar 1) una gu铆a incompleta, solo despu茅s de la cual, no crear谩 un proyecto en absoluto debido a la falta de todas las dependencias necesarias y tareas de compilaci贸n adicionales para el sistema de compilaci贸n, y 2) la ausencia de no hola -Ejemplos similares de todo el mundo en CodeLabs, excepto uno, y luego, mejorado no para un ejemplo de uso de la biblioteca desde cero, sino para la migraci贸n de SharedPreferences al Preferences DataStore... De manera similar, todos los art铆culos en Medium, literalmente o en otras palabras, repiten todo lo que est谩 escrito en la gu铆a de Google, o usan los enfoques incorrectos para trabajar con DataStore, sugiriendo envolver el c贸digo io asincr贸nico en runBlocking directamente en el hilo de la interfaz de usuario.





Y tambi茅n ser铆a bueno conectar la "parte trasera" con la "delantera", por as铆 decirlo: Google tiene la biblioteca de preferencias de AndroidX del clip Jetpack, que le permite lanzar un fragmento de dise帽o de material listo para usar en dos clics para administrar la configuraci贸n de la aplicaci贸n y, en una forma favorita de generaci贸n de c贸digo, liberar al desarrollador de escribir texto repetitivo ... Sin embargo, esta biblioteca propone utilizar SharedPreferences obsoletas como repositorio, y no existe una gu铆a oficial para conectarse al DataStore. En esta nota, me gustar铆a eliminar las dos deficiencias descritas a mi manera.





Crear un marco para trabajar con DataStore

La biblioteca DataStore se divide en dos partes: una an谩loga a la anterior llamada Preferences DataStore, que almacena valores de configuraci贸n en pares clave-valor y no es de tipo seguro, y la segunda, que almacena la configuraci贸n en un archivo de b煤fer de protocolo y es de tipo seguro. Es m谩s flexible y vers谩til, as铆 que lo eleg铆 para mis experimentos.





Para describir el esquema de configuraci贸n, debe crear un archivo adicional en el proyecto. Primero, debe cambiar el estudio o el explorador de ideas al modo Proyecto para que toda la estructura de la carpeta sea visible, y luego crear un archivo con la extensi贸n * .proto en la carpeta app / src / main / proto / (y no pb, como recomienda Google, sin un complemento para verificaci贸n de sintaxis, autocompletado, etc., ni una tarea de compilaci贸n que genere la clase correspondiente funcionar谩).





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 .





Hay lugares en las secciones de c贸digo que no comenc茅 a explicar o citar completamente debido a la poca importancia o la obviedad (por ejemplo, el valor de la constante HOUR_TO_MILLIS), pero si no puede construir un proyecto similar de acuerdo con mi receta, escriba en el comentarios, intentar茅 agregar todos los lugares oscuros ... Tenga en cuenta que tom茅 todas las partes del c贸digo de un proyecto completamente funcional y probado, por lo que no debe preocuparse por su rendimiento.





Gracias por leer.








All Articles