Proto DataStore + Preferências do AndroidX no Kotlin

Quase um ano se passou desde que a equipe do Google AndroidX apresentou uma nova biblioteca DataStore para substituir a biblioteca SharedPreferences , mas a popularização da nova lib claramente não é uma tarefa ativa. Caso contrário, não posso explicar 1) um guia incompleto, seguindo apenas o qual, você não construirá um projeto devido à falta de todas as dependências necessárias e tarefas de construção adicionais para o sistema de construção, e 2) a ausência de não-hello -Mundo exemplos semelhantes em CodeLabs, exceto por um, e então, aprimorados não para um exemplo de uso da biblioteca desde o início, mas para a migração de SharedPreferences para o Preferences DataStore... Da mesma forma, todos os artigos no Medium, literalmente ou em outras palavras, repetem tudo o que está escrito no guia do Google ou usam as abordagens erradas para trabalhar com o DataStore, sugerindo envolver o código io assíncrono em runBlocking direto no thread da interface do usuário.





E também seria bom conectar o "traseiro" com o "frontal", por assim dizer: o Google tem a biblioteca AndroidX Preferences do clipe Jetpack, que permite que você jogue um fragmento de material design pronto em dois cliques para gerenciar as configurações do aplicativo e, de uma maneira favorita de geração de código, liberar o desenvolvedor de escrever boilerplate ... No entanto, esta biblioteca propõe o uso de SharedPreferences desatualizados como um repositório e não há um guia oficial para conexão com o DataStore. Nesta nota, gostaria de eliminar as duas deficiências descritas à minha maneira.





Criando uma estrutura para trabalhar com o DataStore

A biblioteca DataStore é dividida em duas partes: uma análoga à anterior chamada Preferences DataStore, que armazena valores de configurações em pares chave-valor e não é segura para o tipo, e a segunda, que armazena configurações em um arquivo de buffers de protocolo e é seguro para o tipo. É mais flexível e versátil, por isso escolhi para meus experimentos.





Para descrever o esquema de configurações, você precisa criar um arquivo adicional no projeto. Primeiro, você precisa mudar o estúdio ou explorador de ideias para o modo Projeto para que toda a estrutura de pastas seja visível e, em seguida, criar um arquivo com a extensão * .proto na pasta app / src / main / proto / (e não pb, como o Google recomenda - com nenhum plugin para verificação de sintaxe, preenchimento automático, etc., nem uma tarefa de construção que gere a classe correspondente 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 .





Existem lugares nas seções de código que eu não comecei a explicar ou citar completamente devido à falta de importância ou obviedade (por exemplo, o valor da constante HOUR_TO_MILLIS), mas se você não puder construir um projeto semelhante de acordo com minha receita, escreva no comentários, vou tentar adicionar todos os lugares obscuros ... Observe que eu peguei todas as partes do código de um projeto totalmente funcional e testado, então você não deve se preocupar com seu desempenho.





Obrigado pela leitura.








All Articles