Comprobación y control elegante de frases en tiempo real en Android en Kotlin | lähde: Artem | Octubre de 2020

Muchos proyectos tienen control funcional a través de frases cara a cara (funciones de conmutación). Dónde usarlos para las pruebas, dónde usar las pruebas A / B. Viniendo tales banderas como con el servidor de la empresa, como desde un servidor central (ejemplo, Configuración remota de Firebase). В Mi libro se hizo un enfoque separado para la realización, que se compartiría con el mundo.

Compartimos lo que queremos:

  • proporcionar diferentes tipos de datos de la configuración de la aplicación;
  • es fácil y rápido cambiar las marcas al ensamblar los conjuntos;
  • apoyo de varios investigadores;
  • marcas de marcado.

Por primera vez, se ha agregado la interfaz para acceder a la aplicación de configuración dada. Esta interfaz nos ayudará a utilizar las marcas de configuración en todas partes. Si la configuración no muestra marcas, dibujaremos el error.

interface ApplicationConfig {fun getString(key: String): String

fun getBoolean(key: String): Boolean

fun getLong(key: String): Long

}

class NoValueException(message: String) : Exception(message)

Muchos carteles de aulaоmetro FirebaseRemoteConfig y sus métodos. En nuestra aplicación, utilizó la opinión. Cuando tratamos con él, dio a luz a interfaces muy comunes.

У нас будет несколько источников, потому давайте создадим композитный конфиг, конод обод Адача этого класса пробегаться по всем источникам от самого приоритетного до менее приоритеоного Если в более приоритетном значение найдено, то дальше можно не идти. Ir a la página siguiente

class CompositeApplicationConfig(
private vararg val appConfigs: ApplicationConfig
) : ApplicationConfig {override fun hasFeature(key: String): Boolean =
getValue(key) { hasFeature(key) }override fun getString(key: String): String =
getValue(key) { getString(key) }override fun getBoolean(key: String): Boolean =
getValue(key) { getBoolean(key) }override fun getLong(key: String): Long =
getValue(key) { getLong(key) }private inline fun <reified T> getValue(
key: String,
crossinline getTypedValue: ApplicationConfig.() -> T
): T =
appConfigs
.asSequence()
.map {
try {
it.getTypedValue()
}
catch (e: NoValueException) {
null
}
}
.filter { it != null }
.firstOrNull() ?: throw NoValueException("There is no value for key [$key]")
}

Para probar la colección, agregamos la posibilidad de configurar la configuración desde el disco en el disco.

import java.util.Properties

class DebugApplicationConfig : ApplicationConfig {

private val filePath = "/data/local/tmp/"

private val fileName =
BuildConfig.APPLICATION_ID + ".config"

private val file = File(filePath, fileName)

override fun getString(key: String): String =
properties.getString(key)

override fun getBoolean(key: String): Boolean =
properties.getBoolean(key)

override fun getLong(key: String): Long =
properties.getLong(key)

private fun Properties.getBoolean(key: String): Boolean =
when (val value = getString(key)) {
"true", "false" -> value.toBoolean()
else -> throw NoValueException("There is no value for key [$key]")
}

private fun Properties.getLong(key: String): Long =
getString(key).toLong()

private val properties: Properties
get() {
if (!file.exists()) {
throw NoValueException(
"Config file [${file.absolutePath}] not exist."
)
}
return Properties()
.apply {
FileInputStream(file)
.use(this::load)
}
}
}

Отметить, что при попытке достать значение из DebugApplicationConfig, мы открываем файл для чтения кажды Таким образом появляется возможность обновлять конфиг на лету, не перезапуская приложение. Es especialmente notable, por lo que no es suficiente que los usuarios lo utilicen para usuarios reales.

Para FirebaseRemoteConfig, se ha logrado tal realización.

class FirebaseApplicationConfig constructor(
private val firebaseRemoteConfig: FirebaseRemoteConfig
) : ApplicationConfig {override fun getString(key: String): String =
getValue(key).asString()override fun getBoolean(key: String): Boolean =
getValue(key).asBoolean()override fun getLong(key: String): Long =
getValue(key).asLong()private fun getValue(key: String): FirebaseRemoteConfigValue =
firebaseRemoteConfig.getValue(key)
.apply {
if (source == FirebaseRemoteConfig.VALUE_SOURCE_STATIC) {
throw NoValueException("No value for key [$key]")
}
}
}

No ingresamos marcas en la inicialización de la inicialización FirebaseRemoteConfig, para esto hay una oficina separada. ¿Cuándo obtenemos las marcas en la llave de FirebaseRemoteConfig, miramos el campo source. Está llamado a aceptar el signo del signo. Si VALUE_SOURCE_STATICno hay marcas, si VALUE_SOURCE_DEFAULTesta es la señal perfecta, si VALUE_SOURCE_REMOTE, es decir, con el servidor de Firebase. Si te vemos VALUE_SOURCE_STATIC en la fuente, es importante tener en cuenta que no existe tal parámetro en el servidor de Firebase y será posible llevarlo más lejos.

Métodos estándar getString, getBoolean, getLong tu FirebaseRemoteConfig cualquiera de estos valores predeterminados en la oficina donde el servidor Firebase no se usa para el nombre largo que tenemos

Inicialización de la configuración compuesta:

val configs = mutableListOf<ApplicationConfig>()if (BuildConfig.DEBUG) {
configs += DebugApplicationConfig()
}

configs += FirebaseApplicationConfig()
CompositeApplicationConfig(*configs.toTypedArray())

Con una configuración de configuraciones en todo.

La más mínima desviación, que en cualquier caso se basará en la interesante personalidad del idioma Kotlin. Como en otros lenguajes, en Kotlin es posible redistribuir operadores. En el caso de operaciones estándar como composición, reducción y p. Ej. es posible hacer un operador de invocación. Si desea escribir una clase, en la que solo un método público con un operador de llamada es el mismo.

Mira el ejemplo:

class GetAndroidTeamSize {
operator fun invoke(): Int {
return 6
}
}
fun main() {
val getAndroidTeamSize = GetAndroidTeamSize()
val androidTeamSize: Int = getAndroidTeamSize() if (androidTeamSize > 10) {
doNothingJustTalkAllDayLong()
} else {
doTasks()
}
}

Выглядит дико, но пробросив экземпляр такого класса через DI можно вызвать его как функцию в вашем Таким образом, можно выносить куски любой логики в отдельные классы. A estas clases las llamamos aulas. Por tanto, es posible que DI pueda discutir en estas dificultades sobre las que ya no es necesario suministrar código.

Hemos proporcionado los operadores que presentarán los datos a la entrada de algunas abstracciones y también mostrarán el resto del mundo.

Recomendamos aprender las marcas de la configuración en el primer booleano.

class GetApplicationConfigBoolean(
private val applicationConfig: ApplicationConfig
) {
operator fun invoke(
propertyName: String,
defaultValue: Boolean
): Boolean =
try {
applicationConfig.getBoolean(propertyName)
}
catch (e: NoValueException) {
defaultValue
}
}

, Interactor, cheque o característica:

class IsFeatureEnabled(
private val getApplicationConfigBoolean: GetApplicationConfigBoolean
) {
operator fun invoke(
featureName: String,
defaultValue: Boolean = false
): Boolean =
getApplicationConfigBoolean(featureName, defaultValue)
}

Para la función del producto, es posible utilizar dicha interfaz:

class IsNewsPhotoEnabled(
private val isFeatureEnabled: IsFeatureEnabled
) {
operator fun invoke(): Boolean =
isFeatureEnabled("news_photo", false)
}

Todas las clases antes mencionadas se inician a través de un bloque DI y en el lugar final de uso para IsNewsPhotoEnabled, una llamada puede cambiar el registro de solicitud de empleo. Es posible mostrar u ocultar los bloques de la interfaz, es posible cambiar el procesamiento de teclas de las teclas en los botones

newsPhoto.visibility =
if (isNewsPhotoEnabled()) {
View.VISIBLE
} else {
View.GONE
}

Además de los fenómenos de fagos, es posible obtener cualquier dato de la aplicación de configuración. Por ejemplo, es posible obtener un número de tres días de subtitulado y realizar pruebas AB. Aquí, también, el entrevistador podrá participar en las jornadas triples de los subtítulos y orientar la prueba AB

class GetTrialDays(
val getApplicationConfigLong: GetApplicationConfigLong
) {
operator fun invoke(): Long =
getApplicationConfigBoolean("trial_days", 14)
}

Existe una forma similar de controlar a los niños: a través de los enlaces, y esto no son solo enlaces, y se hundeLa solicitud se firmará mediante las reglas del Manifiesto.

Línea directa – este enlace profundo, que es utilizado por el usuario, se adjunta directamente a la aplicación móvil, que es compatible con aquí.

Para utilizar los enlaces en la aplicación, necesitamos agregar un poco a nuestra realización.

UserApplicationConfig tendrá su propia alta prioridad en una configuración compuesta. Sus marcas estarán protegidas en Preferencias compartidas.

class UserApplicationConfig(
private val preferences: SharedPreferences
) : ApplicationConfig {
override fun getString(key: String): String =
preferences
.ifExist(key)
.getString(key, null)!!
override fun getBoolean(key: String): Boolean =
preferences
.ifExist(key)
.getBoolean(key, false)
override fun getLong(key: String): Long =
preferences
.ifExist(key)
.getLong(key, -1)
fun setBooleanValue(key: String, value: Boolean) =
preferences.edit()
.putBoolean(key, value)
.apply()
fun setLongValue(key: String, value: Long) =
preferences.edit()
.putLong(key, value)
.apply()
fun setStringValue(key: String, value: String) =
preferences.edit()
.putString(key, value)
.apply()
fun removeByKey(key: String) =
preferences.edit()
.remove(key)
.apply()
private fun SharedPreferences.ifExist(key: String): SharedPreferences =
apply {
if (!contains(key)) {
throw NoValueException("Key [$key] not exist in preferences")
}
}
}

La anticipación de nuestra actividad especial, que debe ser cautivada y procesada

tales intenciones.

Área de código de manifiesto:

<activity
android:name=".ConfigActivity"
android:label="@string/mybook">
<intent-filter> <action android:name="android.intent.action.VIEW" /> <category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="mybook.ru"
android:pathPrefix="/app/android/config"
android:scheme="https" />
</intent-filter> </activity>

Presentaré el código de dicha actividad con una gran pieza. En el método onCreate проверяем на всякий случай, что действие (acción) соответствует нужному, затем достаем ссылку из datos парсим её в Uri, достаем отдельные пары с пареданными параметрами и сохраняем каждый из параметров в хранилище. Si la clave del parámetro está ahí, el marcado no está predeterminado (no será el segundo parámetro o se utilizará el enlace). De esta forma, es posible hacer enlaces, limpiar escudos y empezar a hacer menos placenteros,

Procesando type-y necesita saber qué tipo de tipo vino y cómo de los métodos para proteger el cuerpo

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
handleIntent(intent)
}
private fun handleIntent(intent: Intent) {
checkAction(intent)
val uri = getData(intent)
handleUriParameters(uri.queryParameters)
finish()
}
private fun checkAction(intent: Intent) {
val action = intent.action
if (action != REQUIRED_ACTION) {
Timber.e("Intent action is [$action]")
val message =
"This activity must be executed with action [$REQUIRED_ACTION] but was [$action]"
throw IllegalArgumentException(message)
}
Timber.d("Intent action is [$action]")
}
private fun getData(intent: Intent): Uri {
val data = intent.data
?: throw IllegalArgumentException("No data passed to activity intent")
Timber.d("Intent data is [$data]")
return data
}
private fun handleUriParameters(parameters: Map<String, String?>) =
parameters.forEach { (parameterName, queryParameterValue) ->
handleParameter(parameterName, queryParameterValue)
}
private fun handleParameter(name: String, rawValue: String?) { if (rawValue.isNullOrBlank()) {
userApplicationConfig.removeByKey(name)
return
}
val type = rawValue.substringBefore('_')
val stringValue = rawValue.removePrefix("$type_")
userApplicationConfig.apply {
when (type) {
"string" -> setStringValue(name, stringValue)
"boolean" -> setBooleanValue(name, stringValue.toBoolean())
"long" -> setLongValue(name, stringValue.toLong())
else -> throw IllegalArgumentException("Unsupported value type [$type]")
}
}
} private val Uri.queryParameters: Map<String, String?>
get() = queryParameterNames.associateWith { getQueryParameter(it) }
companion object {
const val REQUIRED_ACTION = "android.intent.action.VIEW"
}

Ejemplo de conexión para la activación de nuestra función de fagos:

https://mybook.ru/app/android/config?news_photo=boolean_true

Чтобы открыть такую ​​ссылку можно отправить её коллеге или самому себе в чатик, можно создать HTML страничку и открыть её на устройстве в браузере, можно даже распечатать на бумаге в виде QR-кодов и наклеить себе на монитор для быстрого доступа!

Para la depuración, tenemos dichos enlaces a través de ADB desde la terminal:

adb shell am start -W -a android.intent.action.VIEW -d https://mybook.ru/app/android/config?news_photo=boolean_true

Мы получили удобный инструмент для доступа к любым параметрам конфига в коде, изменениям этих паро Интеракторы очень легкие и маленькие, их просто тестировать, из просто мокировать во время тестито То не финальный вариант реализации, у нас есть планы по доработке этого подхода. Si hay alguna duda, ingrésala en los comentarios, te pediremos que la contestes.

Gracias por percibir contigo, el equipo de MyBook Android.

Coautores: Pavel Savinov C Philip Bogdanov.

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *