Русский
Русский
English
Статистика
Реклама

Разработка под android

Перевод Миграция с LiveData на Kotlins Flow

08.06.2021 16:18:29 | Автор: admin

LiveData была нужна нам еще в 2017 году. Паттерн наблюдателя облегчил нам жизнь, но такие опции, как RxJava, в то время были слишком сложными для новичков. Команда Architecture Components создала LiveData: очень авторитетный класс наблюдаемых хранилищ данных, разработанный для Android. Он был простым, чтобы облегчить начало работы, а для более сложных случаев реактивных потоков рекомендовалось использовать RxJava, используя преимущества интеграции между ними.

DeadData?

LiveData по-прежнему остается нашим решением для Java-разработчиков, новичков и простых ситуаций. В остальном, хорошим вариантом является переход на Kotlin Flows. Flows (потоки) все еще имеют крутую кривую обучения, но они являются частью языка Kotlin, поддерживаемого Jetbrains; кроме того, на подходе Compose, который хорошо сочетается с реактивной моделью.

Мы уже говорили об использовании Flows для соединения различных частей вашего приложения, за исключением представления и ViewModel. Теперь, когда у нас есть более безопасный способ сбора потоков из пользовательских интерфейсов Android, мы можем создать полное руководство по миграции.

В этом посте вы узнаете, как отобразить Flows в представление, как собирать их, и как точно настроить под конкретные нужды.

Flow: простые вещи труднее, а сложные легче

LiveData выполняла одну вещь и делала это хорошо: она показывала данные, одновременно кэшируя последние значения и учитывая жизненные циклы Android. Позже мы узнали, что она также может запускать корутины и создавать сложные преобразования, но это требовало немного больше усилий.

Давайте рассмотрим некоторые паттерны LiveData и их эквиваленты Flow:

#1: Показ результата однократной операции с модифицированным держателем данных

Это классический паттерн, в котором вы мутируете держатель состояния с результатом выполнения корутины:

Показ результата однократной операции с модифицированным (Mutable) держателем данных (LiveData)Показ результата однократной операции с модифицированным (Mutable) держателем данных (LiveData)
<!-- Copyright 2020 Google LLC.   SPDX-License-Identifier: Apache-2.0 -->class MyViewModel {    private val _myUiState = MutableLiveData<Result<UiState>>(Result.Loading)    val myUiState: LiveData<Result<UiState>> = _myUiState    // Load data from a suspend fun and mutate state    init {        viewModelScope.launch {             val result = ...            _myUiState.value = result        }    }}

Чтобы сделать то же самое с потоками, мы используем модифицированный StateFlow:

Показ результата однократной операции с модифицированным держателем данных (StateFlow)Показ результата однократной операции с модифицированным держателем данных (StateFlow)
class MyViewModel {    private val _myUiState = MutableStateFlow<Result<UiState>>(Result.Loading)    val myUiState: StateFlow<Result<UiState>> = _myUiState    // Load data from a suspend fun and mutate state    init {        viewModelScope.launch {             val result = ...            _myUiState.value = result        }    }}

StateFlow это особый вид SharedFlow (который является особым типом Flow), наиболее близкий к LiveData:

  • У него всегда есть значение.

  • У него только одно значение.

  • Он поддерживает несколько наблюдателей (поэтому поток является общим).

  • Он всегда воспроизводит последнее значение при подписке, независимо от количества активных наблюдателей.

При отображении состояния пользовательского интерфейса для представления используйте StateFlow. Это безопасный и эффективный наблюдатель, предназначенный для хранения состояния пользовательского интерфейса.

#2: Показ результата однократной операции

Это эквивалент предыдущего сниппета, демонстрирующий результат вызова корутины без модифицированного теневого свойства.

В LiveData мы использовали для этого конструктор корутин liveData:

Показ результата однократной операции (LiveData)Показ результата однократной операции (LiveData)
class MyViewModel(...) : ViewModel() {    val result: LiveData<Result<UiState>> = liveData {        emit(Result.Loading)        emit(repository.fetchItem())    }}

Поскольку держатели состояния всегда имеют значение, хорошей идеей будет обернуть наше UI-состояние в какой-нибудь класс Result, который поддерживает такие состояния, как Loading, Success и Error.

Эквивалент Flow немного сложнее, потому что вам придется выполнить некоторую настройку:

Показ результата однократной операции (StateFlow)Показ результата однократной операции (StateFlow)
class MyViewModel(...) : ViewModel() {    val result: StateFlow<Result<UiState>> = flow {        emit(repository.fetchItem())    }.stateIn(        scope = viewModelScope,         started = WhileSubscribed(5000), // Or Lazily because it's a one-shot        initialValue = Result.Loading    )}

stateIn это оператор Flow, который преобразует его в StateFlow. Давайте пока доверимся этим параметрам, так как позже нам понадобится более сложная информация для правильного объяснения.

#3: Однократная загрузка данных с параметрами

Допустим, вы хотите загрузить некоторые данные, которые зависят от ID пользователя, и вы получаете эту информацию от AuthManager, который показывает Flow:

Однократная загрузка данных с параметрами (LiveData)Однократная загрузка данных с параметрами (LiveData)

С помощью LiveData можно сделать примерно следующее:

class MyViewModel(authManager..., repository...) : ViewModel() {    private val userId: LiveData<String?> =         authManager.observeUser().map { user -> user.id }.asLiveData()    val result: LiveData<Result<Item>> = userId.switchMap { newUserId ->        liveData { emit(repository.fetchItem(newUserId)) }    }}

switchMap это преобразование, тело которого выполняется, а результат подписывается при изменении userId.

Если нет причин для того, чтобы userId был LiveData, лучшей альтернативой этому является объединение потоков с Flow и окончательное преобразование полученного результата в LiveData.

class MyViewModel(authManager..., repository...) : ViewModel() {    private val userId: Flow<UserId> = authManager.observeUser().map { user -> user.id }    val result: LiveData<Result<Item>> = userId.mapLatest { newUserId ->       repository.fetchItem(newUserId)    }.asLiveData()}

Выполнение этого действия с помощью Flows выглядит очень похоже:

Однократная загрузка данных с параметрами (StateFlow)Однократная загрузка данных с параметрами (StateFlow)
class MyViewModel(authManager..., repository...) : ViewModel() {    private val userId: Flow<UserId> = authManager.observeUser().map { user -> user.id }    val result: StateFlow<Result<Item>> = userId.mapLatest { newUserId ->        repository.fetchItem(newUserId)    }.stateIn(        scope = viewModelScope,         started = WhileSubscribed(5000),         initialValue = Result.Loading    )}

Обратите внимание, что если вам нужна большая гибкость, вы также можете использовать transformLatest и emit элементы в явном виде:

val result = userId.transformLatest { newUserId ->        emit(Result.LoadingData)        emit(repository.fetchItem(newUserId))    }.stateIn(        scope = viewModelScope,         started = WhileSubscribed(5000),         initialValue = Result.LoadingUser // Note the different Loading states    )

#4: Наблюдение за потоком данных с параметрами

Теперь сделаем пример более реактивным. Данные не извлекаются, а наблюдаются, поэтому мы автоматически распространяем изменения в источнике данных на пользовательский интерфейс.

Продолжая наш пример: вместо вызова fetchItem на источнике данных мы используем гипотетический оператор observeItem, который возвращает Flow.

С помощью LiveData вы можете преобразовать поток в LiveData и все обновления emitSource:

Наблюдение за потоком с параметрами (LiveData)Наблюдение за потоком с параметрами (LiveData)
class MyViewModel(authManager..., repository...) : ViewModel() {    private val userId: LiveData<String?> =         authManager.observeUser().map { user -> user.id }.asLiveData()    val result = userId.switchMap { newUserId ->        repository.observeItem(newUserId).asLiveData()    }}

Или, лучше всего, объединить оба потока с помощью flatMapLatest и преобразовать только выход в LiveData:

class MyViewModel(authManager..., repository...) : ViewModel() {    private val userId: Flow<String?> =         authManager.observeUser().map { user -> user?.id }    val result: LiveData<Result<Item>> = userId.flatMapLatest { newUserId ->        repository.observeItem(newUserId)    }.asLiveData()}

Имплементация Flow похожа, но в ней нет преобразований LiveData:

Наблюдение за потоком с параметрами (StateFlow)Наблюдение за потоком с параметрами (StateFlow)
class MyViewModel(authManager..., repository...) : ViewModel() {    private val userId: Flow<String?> =         authManager.observeUser().map { user -> user?.id }    val result: StateFlow<Result<Item>> = userId.flatMapLatest { newUserId ->        repository.observeItem(newUserId)    }.stateIn(        scope = viewModelScope,         started = WhileSubscribed(5000),         initialValue = Result.LoadingUser    )}

StateFlow будет получать обновления каждый раз, когда меняется пользователь или изменяются его данные в репозитории.

#5 Объединение нескольких источников: MediatorLiveData -> Flow.combine

MediatorLiveData позволяет вам наблюдать за одним или несколькими источниками обновлений (наблюдаемыми LiveData) и что-то делать, когда они получают новые данные. Обычно вы обновляете значение MediatorLiveData:

val liveData1: LiveData<Int> = ...val liveData2: LiveData<Int> = ...val result = MediatorLiveData<Int>()result.addSource(liveData1) { value ->    result.setValue(liveData1.value ?: 0 + (liveData2.value ?: 0))}result.addSource(liveData2) { value ->    result.setValue(liveData1.value ?: 0 + (liveData2.value ?: 0))}

Эквивалент Flow намного проще:

val flow1: Flow<Int> = ...val flow2: Flow<Int> = ...val result = combine(flow1, flow2) { a, b -> a + b }

Можно также использовать функцию combineTransform или zip.

Настройка открытого StateFlow (оператор stateIn)

Ранее мы использовали stateIn для преобразования регулярного потока в StateFlow, но это требует некоторой настройки. Если вы не хотите вдаваться в подробности прямо сейчас и вам просто нужно копировать-вставлять, я рекомендую использовать эту комбинацию:

val result: StateFlow<Result<UiState>> = someFlow    .stateIn(        scope = viewModelScope,         started = WhileSubscribed(5000),         initialValue = Result.Loading    )

Однако если вы не уверены в этом, казалось бы, случайном 5-секундном параметре started, читайте дальше.

StateIn имеет 3 параметра (из документации):

@param scope the coroutine scope in which sharing is started.@param started the strategy that controls when sharing is started and stopped.@param initialValue the initial value of the state flow.This value is also used when the state flow is reset using the [SharingStarted.WhileSubscribed] strategy with the `replayExpirationMillis` parameter.

started может принимать 3 значения:

  • Lazily: начать, когда появится первый подписчик, и остановить, когда scope будет отменен.

  • Eagerly: начать немедленно и остановить, когда scope будет отменен.

  • WhileSubscribed: Это сложно.

Для одноразовых операций вы можете использовать Lazily или Eagerly. Однако, если вы наблюдаете за другими потоками, вам следует использовать WhileSubscribed для выполнения небольших, но важных оптимизаций, как описано ниже.

Стратегия WhileSubscribed

WhileSubscribed отменяет восходящий поток, когда нет коллекторов. StateFlow, созданный с помощью stateIn, передает данные в View, но он также наблюдает за потоками, поступающими из других слоев или приложения (восходящий поток). Поддержание этих потоков активными может привести к напрасной трате ресурсов, например, если они продолжают считывать данные из других источников, таких как подключение к базе данных, аппаратные датчики и т.д. Когда ваше приложение переходит в фоновый режим, будет хорошо, если вы остановите эти корутины.

WhileSubscribed принимает два параметра:

public fun WhileSubscribed(    stopTimeoutMillis: Long = 0,    replayExpirationMillis: Long = Long.MAX_VALUE)

Таймаут остановки

Из документации:

stopTimeoutMillis настраивает задержку (в миллисекундах) между исчезновением последнего абонента и остановкой восходящего потока. По умолчанию она равна нулю (остановка происходит немедленно).

Это полезно, поскольку вы не хотите отменять восходящие потоки, если представление перестало воспринимать данные на долю секунды. Это происходит постоянно например, когда пользователь поворачивает устройство и представление разрушается и воссоздается в быстрой последовательности.

Решение в конструкторе корутины liveData заключалось в добавлении задержки в 5 секунд, после которой корутина будет остановлена, если нет подписчиков. WhileSubscribed(5000) делает именно это:

class MyViewModel(...) : ViewModel() {    val result = userId.mapLatest { newUserId ->        repository.observeItem(newUserId)    }.stateIn(        scope = viewModelScope,         started = WhileSubscribed(5000),         initialValue = Result.Loading    )}

Этот подход отвечает всем требованиям:

  • Когда пользователь отправляет ваше приложение в фоновый режим, обновления, поступающие с других уровней, прекращаются через пять секунд, что позволяет экономить заряд батареи.

  • Последнее значение все еще будет кэшироваться, чтобы, когда пользователь вернется к нему, представление сразу же получило данные.

  • Подписки перезапускаются, и новые значения будут поступать, обновляя экран, когда они доступны.

Истечение срока воспроизведения

replayExpirationMillis настраивает задержку (в миллисекундах) между завершением работы программы совместного доступа и сбросом кэша воспроизведения (что делает кэш пустым для оператора shareIn и возвращает кэшированное значение к исходному initialValue для stateIn). По умолчанию он равен Long.MAX_VALUE (кэш воспроизведения сохраняется постоянно, буфер никогда не сбрасывается). Используйте нулевое значение для немедленного истечения срока действия кэша.

Наблюдение StateFlow из представления

Как мы уже видели, для представления очень важно сообщить StateFlows во ViewModel, что они больше не прослушиваются. Однако, как и во всем, что связано с жизненными циклами, все не так просто.

Для того чтобы собрать поток, вам нужна корутина. Действия и фрагменты предлагают множество конструкторов корутин:

  • Activity.lifecycleScope.launch: запускает корутину немедленно и отменяет ее при завершении активности.

  • Fragment.lifecycleScope.launch: немедленно запускает корутину и отменяет ее при завершении фрагмента.

LaunchWhenStarted, launchWhenResumed...

Специализированные версии launch, называемые launchWhenX, будут ждать, пока lifecycleOwner находится в состоянии X, и приостановят выполнение корутины, когда lifecycleOwner упадет ниже состояния X. Важно отметить, что они не отменяют выполнение программы до тех пор, пока жизненный цикл не будет закончен.

Сбор потоков с помощью launch/launchWhenX небезопасенСбор потоков с помощью launch/launchWhenX небезопасен

Получение обновлений, когда приложение находится в фоновом режиме, может привести к сбоям, что решается приостановкой сбора в View. Однако восходящие потоки остаются активными, пока приложение находится в фоновом режиме, что может привести к трате ресурсов.

Это означает, что все, что мы делали до сих пор для настройки StateFlow, было бы совершенно бесполезно; однако в нашем распоряжении есть новый API.

lifecycle.repeatOnLifecycle на помощь

Этот новый конструктор корутин (доступный в lifecycle-runtime-ktx 2.4.0-alpha01) делает именно то, что нам нужно: он запускает корутины в определенном состоянии и останавливает их, когда уровень жизненного цикла опускается ниже этого состояния.

Различные методы сбора потокаРазличные методы сбора потока

Например, во Фрагменте:

onCreateView(...) {    viewLifecycleOwner.lifecycleScope.launch {        viewLifecycleOwner.lifecycle.repeatOnLifecycle(STARTED) {            myViewModel.myUiState.collect { ... }        }    }}

Сбор начнется, когда представление фрагмента будет STARTED, продолжится до RESUMED и остановится, когда оно вернется к STOPPED. Читайте об этом в статье Более безопасный способ сбора потоков из пользовательских интерфейсов Android.

Сочетание API repeatOnLifecycle с приведенным выше руководством по StateFlow обеспечит вам наилучшую производительность при рациональном использовании ресурсов устройства.

StateFlow выставляется с помощью WhileSubscribed(5000) и собирается с помощью repeatOnLifecycle(STARTED)StateFlow выставляется с помощью WhileSubscribed(5000) и собирается с помощью repeatOnLifecycle(STARTED)

Предупреждение: Поддержка StateFlow, недавно добавленная в Data Binding, использует launchWhenCreated для сбора обновлений, и она начнет использовать repeatOnLifecycle` вместо этого, когда достигнет стабильности.

Для Data Binding вы должны использовать Flows везде и просто добавить asLiveData(), чтобы отобразить их в представлении. Привязка данных будет обновлена, когда lifecycle-runtime-ktx 2.4.0 станет стабильным.

Резюме

Лучшим способом предоставления данных из ViewModel и сбора их из представления является:

  • Выставить StateFlow, используя стратегию WhileSubscribed, с таймаутом. [пример]

  • Собирать с помощью repeatOnLifecycle. [пример].

Любая другая комбинация будет поддерживать восходящие потоки активными, расходуя ресурсы:


Перевод материала подготовлен в рамках курса "Android Developer. Basic". Если вам интересно узнать о курсе больше, приходите на день открытых дверей онлайн. На нем вы сможете узнать подробнее о программе и формате обучения, познакомиться с преподавателем.

Подробнее..

Аналог R.string в android приложении

20.06.2021 12:09:43 | Автор: admin

Всем привет! Меня зовут Владимир, я Android-разработчик в компании Альфа-Капитал. Наверняка любое мобильное приложение в процессе развития нуждается в гибкой настройке текстовой информации за счет серверной части. В этой статье я поделюсь мыслями и решениями нашей команды. Также я покажу пример генерации кода с помощью gradle скрипта, сильно упростивший жизнь android команде.

С чего всё начиналось

В нашем приложении довольно много текстовой информации для пользователей, начиная от подсказок и описаний и заканчивая дисклеймерами и большими информационными экранами. Мы работаем в сфере финансовых технологий, и порой чтобы соответствовать требованиям Центробанка необходимо быстрое обновление информации. Это иногда нужно делать и по запросам наших юристов. Кроме того, порой требуется экран, где на первой неделе будет один текст, а на второй неделе другой текст.

Сразу оговорюсь: в текущем виде (и в ближайшем будущем) бизнес ориентирован на Россию, поэтому в проекте нет необходимости поддерживать несколько языков в приложении.

Столкнувшись с проблемой обновления информации, мы стали задумываться о хранении текстов (которые в перспективе придется корректировать) не в приложении, а на сервере. Часть (например, большой список дисклеймеров для юристов) удалось вытащить. Но оставалось еще множество строк, которые сложно как-то объединить и для которых не хотелось каждый раз делать сетевой запрос и экран загрузки.

Сначала мы попробовали держать тексты на Firebase. По функциональности такое решение вполне подходило, к тому же оно добавляло версионирование и возможность создания a/b тестов. Вскоре стало ясно, что это все-таки не то, что нам нужно. Тогда мы сформулировали свои требования:

  1. Удобный и единый источник текстов для всех мобильных платформ (android/ios);

  2. Обновление текстов в рантайме при старте приложения (для обновления важных мест без выпуска фиксов/релизов);

  3. В приложении мы не должны страдать от необходимости выполнения сетевого запроса или показа лоадинга ради загрузки лексем;

  4. Обновление текстов должно быть доступно без вмешательства разработчиков (т.е. чтобы условный аналитик / тестировщик смог спокойно обновить тексты при необходимости);

  5. Максимально простое создание дефолтных значений, которые будут лежать в приложении.

Firebase Remote Config не подошел слишком хороший функционал для простых текстов. У нас быстро получился большой список необходимых лексем, а их добавление / редактирование становилось слишком сложным. Нелегкой задачей была и установка дефолтных значений в приложении. Нам хотелось чего-то попроще.

Мы решили, что самым оптимальным будет объединение необходимых текстов в JSON файл. Почему именно JSON, а не XML, который кажется более нативным для Android? Так показалось удобней для обеих команд (Android и iOS). JSON понятный формат данных, его легко разберет любая платформа. Этот файл можно легко скачать, положить в проект и получить дефолтные данные.Схема работает и в обратную сторону. Пришла задача с новым текстом? Нужно добавить новые строки в проект, закинуть этот же JSON c ключами на сервер.

Пример json файла:

{ "screen1_text1": "Text 1", "screen1_text2": "Text 2 \nnext line", "screen1_text3": "Text 3", "screen1_text4": "Text 4"}

Первая реализация

В итоге мы получили JSON файл с текстами на сервере, этот же файл храним в проекте в папке assets. Сначала мы создали объект Lexemator, у которого можно по ключу запросить какой-то текст. При старте приложение подкачивает тексты с сервера в Lexemator, а если что-то пошло не так, берет дефолтные текста из папки assets.

object Lexemator {fun getString(key: String): String}

Использование в коде выглядит следующим образом:

class MainActivity : Activity() {   override fun onCreate(savedInstanceState: Bundle?) { ...       val textView = findViewById<TextView>(R.id.text1)       textView.text = Lexemator.getString("screen1_text1")   }}

По сравнению с Firebase стало лучше, но был один существенный недостаток: достаточно одной ошибки в ключе, и мы получаем не тот текст. Нам хотелось получить статическую поддержку, похожую на R.string, где Android Studio подсказывала бы константы и проект не компилировался бы при ошибке.

Это была предыстория, теперь переходим к коду.

Gradle - наше всё

Раз в проекте имеется JSON файл с текстами, значит, уже на этапе сборки мы понимаем, какие есть ключи для разных текстов. Если каких-то ключей нет, то они либо не нужны, либо их всё равно надо добавить для дефолтных значений. Выходит, на этапе сборки можно сгенерировать код, который будет содержать ключи для текстов. Мы решили сделать это с помощью gradle task.

Ниже представлен получившийся скрипт

import groovy.json.JsonSlurper/*** Таска ищет файл с текстами с названием strings.json и создает объект LL.* Для каждого текста из strings.json создает переменную LL.key внутри объекта** Если файла strings.json не существует - скрипт кинет Exception.** Чтобы сгенерить текста заново, достаточно перебилдить проект, или изменить файл strings.json*/def classFileName = "LL"def stringsFileName = "strings.json"def filePath = project.rootProject.getProjectDir().path + "/app/src/main/assets/json"def outputPath = project.rootProject.getProjectDir().path + "/app/build/generated/strings"def inputFile = new File(filePath + "/${stringsFileName}")def outputFile = new File(outputPath + "/${classFileName}.kt")task createStrings {   /**    * Если что-то изменится в inputFile, то при следующей сборке будет заново сгенерирован    * outputFile.    * Если ничего не изменилось, и outputFile уже есть, таска будет помечена "UP-TO-DATE" и    * не будет выполняться лишний раз.    */   inputs.file(inputFile)   outputs.file(outputFile)   doLast {       if (!inputFile.exists()) {           throw RuntimeException("файл ${inputFile} не найден")       }       println("Начало создания файла ${outputFile.path}")       outputFile.delete()       outputFile.createNewFile()       /**        * Тройные кавычки нужны для того, чтобы перевод строки (\n) в strings.json        * не ломал строки в созданном LL.kt файле.        */       def s1 = """package com.obolonnyy.lexemator//<!--Этот файл создан автоматически gradle скриптом из create_strings.gradle -->object ${classFileName} {"""       def s2 =               """      fun addLexems(map: Map<String, String>) {       map.forEach { k, v -> addLexem(k, v) }   }   fun addLexem(key: String, value: String) {       when(key) {"""       def json = new JsonSlurper().parse(inputFile)       assert json instanceof Map       json.each { entry ->           s1 += "    var ${entry.key} = \"\"\"${entry.value}\"\"\"\n        private set\n"           s2 += "            \"${entry.key}\" -> ${entry.key} = value\n"       }       def result = s1 + "\n\n" + s2 + """        }   }}"""       outputFile.write(result)       println("файл ${outputFile.path} успешно создан.")   }}/*** Показываем, что созданный файл теперь тоже является частью проекта.* Без этого мы не сможем использовать созданный LL.kt класс в своих классах.*/android {   sourceSets {       main {           java {               srcDirs += outputPath           }       }   }}

Скрипт создает object LL, у которого есть список ключей (поля типа String, с приватным сеттером) с дефолтными значениями, и две функции для обновления значения ключей. При старте приложения мы запрашиваем с сервера текста и обновляем значения через функцию addLexems().

Комментарий про название объекта LL: сначала мы думали назвать L (от слова Lexemator), чтобы было привычно как с R, но мешала константа android.icu.lang.UCharacter.GraphemeClusterBreak.L.Поэтому, мы не придумали ничего лучше, чем назвать класс LL.

Сгенерированный объект LL выглядит следующим образом:

//<!--Этот файл создан автоматически gradle скриптом из create_strings.gradle -->object LL {   var screen1_text1 = """Text 1"""       private set   var screen1_text2 = """Text 2next line"""       private set   var screen1_text3 = """Text 3"""       private set   var screen1_text4 = """Text 4"""       private set     fun addLexems(map: Map<String, String>) {       map.forEach { k, v -> addLexem(k, v) }   }   fun addLexem(key: String, value: String) {       when(key) {           "screen1_text1" -> screen1_text1 = value           "screen1_text2" -> screen1_text2 = value           "screen1_text3" -> screen1_text3 = value           "screen1_text4" -> screen1_text4 = value       }   }}

Пример использования объекта LL в коде выглядит следующим образом:

class MainActivity : Activity() {   override fun onCreate(savedInstanceState: Bundle?) {...       val textView = findViewById<TextView>(R.id.text1)       textView.text = LL.screen1_text1   }}

Получилось довольно просто и привычно.

Итоги

Мы сделали механизм управления текстами в приложении без необходимости перевыпуска релиза. Тексты хранятся на сервере, обновляются через git репозиторий. Для бизнеса планируется создать админку для управления текстами. Для Android команды мы сделали удобный механизм работы с этими текстами и статическую поддержку текстов в коде. Сейчас наш JSON файл насчитывает 180 различных строк, и найденное решение всех устраивает.

Рабочий пример можно найти по ссылке.

Подробнее..

Открылся набор в Indie Games Accelerator и Indie Games Festival от Google Play

14.06.2021 12:13:27 | Автор: admin

Indie Games Accelerator и Indie Games Festival две программы для независимых (инди) разработчиков мобильных игр, организованных командой Google Play. Программы направлены на то, чтобы помочь небольшим игровым студиям и разработчикам стать популярнее в Google Play независимо от того, на какой стадии находятся их проекты.

В этом году обе программы пройдут в онлайн-формате, заявки принимаются до 1 июля подробности под катом.

Для нас важно поддерживать не только крупные международные компании, но и небольшие инди-команды благодаря своей креативности и увлеченности играми, они создают уникальные и интересные проекты. Если вы работаете над уникальным проектом и хотите, чтобы о нем узнал мир, предлагаем вам принять участие в одной (или обоих сразу) из наших программ Indie Games Accelerator и Indie Games Festival.

Каждая из программ направлена на то, чтобы помочь небольшим игровым студиям стать популярнее в Google Play независимо от того, на какой стадии находятся их проекты: в акселератор принимаются проекты на этапе идеи или прототипа, фестиваль подойдет для игр, находящихся в активном оперировании. Заявки на обе программы принимаются до 1 июля.

Indie Games Accelerator: обучение и менторская поддержка

Эта программа разработана для начинающих инди-разработчиков, которые работают над прототипами будущих игр или делают тестовые релизы.

Проекты, которые пройдут отбор и станут участниками акселератора, смогут присоединиться к 12-недельной образовательной программе, а также получат возможность поработать над своими проектами вместе с экспертами из Google, крупных игровых студий и венчурных фондов. Rovio, Game Insight, Zynga, Play Ventures, Unity Technologies, Belka Games с полным списком менторов и условиями участия можно ознакомиться здесь.

В этом году в акселерационной программе участвуют более 70 стран, заявки на Indie Games Accelerator из России, Украины и Беларуси будут приниматься впервые!

Indie Games Festival: промо-кампании для финалистов

Эта программа разработана для небольших команд, которые недавно выпустили игру и хотят дальше работать над проектом без помощи паблишера или инвесторов.

Основные критерии отбора: инновационность, увлекательность и дизайн. Среди призов: фичеринг на Google Play и промо-кампании для 3 игр-победителей стоимостью 100 000 евро.

Условия участия: в программе участвуют 29 стран Европы, включая Россию, Украину и Беларусь; максимальное количество человек в команде 50, игра должна быть выпущена на Google Play не ранее 3 марта 2020 г. Подробнее с правилами участия и критериями отбора можно ознакомиться здесь.

В прошлом году в финал конкурса прошло три проекта из России: My Diggy Dog 2 от King Bird Games, Color Spots от UX Apps и Tricky Castle от Team Tricky подать заявку можно до 1 июля.

Подробнее..

Дайджест интересных материалов для мобильного разработчика 395 (24 30 мая)

30.05.2021 20:12:29 | Автор: admin
В этом дайджесте переезд на Swift и 36 секунд доступности, валидация встроенных покупок и кросс-системное тестирование, симпатичный чейнджлог, проблемы с неткодом, переезд Coinbase на React Nativeи многое другое!



Этот дайджест доступен в виде еженедельной рассылки. А ежедневно новости мы рассылаем в Telegram-канале.

iOS

Как Лёня с React на Swift переезжал
Доступность на iOS началась с 36 секунд
Самые популярные SDK после выхода iOS 14.5
Всемирная конференция Apple для разработчиков начнётся 7 июня и пройдёт в онлайн-формате
Эван Шпигель поддержал налог App Store и меры защиты Apple
Как управлять поведением клавиатуры в iOS-приложениях
MVP архитектура для iOS
Как разрабатывать приложения для iOS без Mac
Как использовать SnapKit в ваших iOS-приложениях
Как использовать Firebase Remote Config с Swift 5
3 способа стилизации представлений SwiftUI
HMS ML Kit: перевод в реальном времени (iOS Swift)
ScrollingContentViewController: простое создание скроллируемого View
NotificationToast: тосты для iOS
CalendarKit: календарь для iOS, iPadOS и macOS

Android

Интеграция и серверная валидация инаппов для стора Google Play как защититься от читеров
Обновляемся на новую версию API Android по наставлению Google
Создаем приложение для Android быстро и просто
Почему Kotlin лучше Java?
Особенности тестирования Android без Google-сервисов
Получаем результат правильно(Часть2). FragmentResultAPI
Как начинающему Android-разработчику прокачать свои навыки: 5 open source проектов для изучения
Полезные расширения Kotlin для Android
Hilt стабилен. Более простая инъекция зависимостей на Android
Повышаем уровень своего класса данных Kotlin с помощью расширений
Историческое введение в модель реактивного состояния Compose
Совершенно новое Состояние в Jetpack Compose
Улучшение преобразования кода Java в Kotlin: пример
Структурированный параллелизм в действии
Начните отсюда: 5 упражнений для подготовки вашего приложения к работе с большими экранами
Начинаем работать с WorkManager
Простые инструментальные тесты (UI-тесты) для Android в 2021 году
Введение в Security By Design
KodeEditor: редактор кода для Android
SuperForwardView: перемотка в стиле Netflix

Разработка

Почему мы решили создать отдел кросс-системного тестирования
Лаги, джиттер и потеря пакетов: откуда берутся проблемы с неткодом и как их решать
7 QA-шных грехов, которые помогут или помешают тестировщику (стать тем, кем ты хочешь)
За что банит Apple(и Google)
Как написать симпатичный чейнджлог: опыт Авито
Без тимлида не обойтись, а что насчет техлида?
Как сохранить нервы тестировщика или ускорить регресс с 8 до 2 часов
Как я хотел поработать нативным Android разработчиком, но устроился Flutter разрабом
Dart: Быстрые неизменяемые коллекции
6 способов снизить когнитивную нагрузку от интерфейса
Podlodka #217: фасилитация
Flutter Dev Podcast #27: как работает рендеринг UI
Как Coinbase перешел на React Native
Stack Overflow запустил новый ежегодный опрос разработчиков
Fuchsia получила свое первой устройство
Мой SaaS добился MRR $12.5K за один месяц: вот чему я научился
Куда уходят программисты?
Онлайн-конференция Google for Games Developer Summit 2021 пройдет в июле
Проблема дизайна это сами дизайнеры
Пользователям плевать на дизайн: как устроен хороший UX на самом деле
Хотите стать лучшим UX дизайнером? Создавайте эмоциональный дизайн
Лучшие языки программирования для изучения в 2021 году
10 вещей, которые хорошо знают опытные разработчики
Почему софтверные компании часто отвергают хороших программистов
Наплевать на доступность
Самые востребованные языки программирования в 2021 году
Избегайте блокировки CI/CD делайте свои сборки более портативными
Flutter: CRUD с использованием Firebase Cloud Firestore
Одна привычка, чтобы стать лучшим разработчиком
Что нового во Flutter 2.2
Библиотека разработчика от Google

Аналитика, маркетинг и монетизация

Датасет о мобильных приложениях
Реклама мобильных игр в первом полугодии 2021: мировая статистика
RevenueCat закрыл Серию B при оценке в $300 млн
Платформа отладки Lightrun получила $23 млн
Платформа потери веса Noom привлекла $540 млн
Тренды мобильных приложений 2021: отчет Adjust
Дейтинг-приложения предложат улучшения прошедшим вакцинацию
Google запускает рекламные кампании приложений на десктопах
Netflix думает над выходом на игровой рынок
Одних технологий недостаточно: что раздражает рекламный рынок в Apple и как она зарабатывает на закрытости системы

AI, Устройства, IoT

ML: нечеловеческие технологии для человеческих цен
TinyML. Сжимаем нейросеть
SberCloud + Intel oneAPI = льготное облако для ML-разработчиков
IBM разработала датасет Project CodeNet для обучения ИИ программированию
Как сделать бизнес на AR/VR
Mail.ru Group открыла новый набор на бесплатное обучение в Академию больших данных MADE
Microsoft использовала GPT-3 для создания кода на естественном языке
Best Buy начинает продажи смартфона для пожилых

Предыдущий дайджест. Если у вас есть другие интересные материалы или вы нашли ошибку пришлите, пожалуйста, в почту.
Подробнее..

Проекты в Gradle 7 как не зависеть от зависимостей

03.06.2021 16:16:28 | Автор: admin

Привет! Меня зовут Ксения Кайшева, я пишу приложения под Android в компании 65apps. Сегодня расскажу о новой возможности, которая позволяет централизованно описывать зависимости на проектах с системой сборки Gradle.

На текущий момент существует множество вариантов описания зависимостей в проектах, использующих Gradle. Рекомендуемого стандарта нет, поэтому используются самые разные подходы: кто-то просто перечисляет зависимости в блоке dependencies, кто-то выносит зависимости в отдельный файл, блок ext и т.д. И для новых разработчиков не всегда очевидно, что, где и как используется в большом и многомодульном проекте.

В 7й версии Gradle представлена новая функция, позволяющая описывать все зависимости централизованно. Эта функция находится на стадии превью, и чтобы воспользоваться ей в файле settings.gradle(.kts) необходимо добавить строку:

enableFeaturePreview("VERSION_CATALOGS")

Так выглядит использование (описанных в централизованном каталоге) зависимостей в любом build.gradle скрипте:

dependencies {
implementation libs.lifecycle.runtime
implementation libs.lifecycle.viewmodel.ktx
implementation libs.lifecycle.extentions
implementation libs.lifecycle.livedata.ktx
}

Здесь:

libs это сам каталог
lifecycle.runtime это зависимость в этом каталоге.

Каталог описывается в settings.gradle(.kts) файле:

dependencyResolutionManagement {
versionCatalogs {
libs {
alias('lifecycle-runtime')
.to(androidx.lifecycle:lifecycle-runtime:2.2.0')
alias('lifecycle-viewmodel-ktx').to(androidx.lifecycle', 'lifecycle-viewmodel-ktx').version {
strictly '[2.2.0, 2.3.0['
prefer '2.3.1'
}
}
}
}

Псевдоним (alias) должен состоять из имен, которые могут быть разделены тире, нижним подчеркиванием или точкой.

Разделение через тире является рекомендованным.

Сами имена должны состоять из буквенных символов, предпочтительно в нижнем регистре. Числа можно использовать в конце имени.

Например,

lifecycle-runtime
lifecycle_runtime
lifecycle.runtime
junit5-test-core
spek-runner-junit5

Недопустимо иметь псевдоним для зависимости, которая также принадлежит вложенной группе. Например, lifecycle-viewmodel и lifecycle-viewmodel-ktx.

Gradle рекомендует в таком случае использовать регистр для различения.
Например, lifecycleViewmodel и lifecycleViewmodelKtx.

Версии можно объявлять отдельно и затем ссылаться на них в описаниях самих зависимостей:

dependencyResolutionManagement {
versionCatalogs {
libs {
version('lifecycle', '2.3.1')
alias('lifecycle-viewmodel-ktx').to('androidx.lifecycle', 'lifecycle-viewmodel-ktx').versionRef('lifecycle')
}
}
}

Объявленные таким образом версии также доступны из любогоbuild.gradle файла:

version = libs.versions.lifecycle.get()

Часто бывает, что некоторые зависимости используются вместе. В каталоге такие зависимости можно объединять в пакеты и подключать их сразу через псевдоним пакета. То есть пакет это список нескольких зависимостей:

dependencyResolutionManagement {
versionCatalogs {
libs {
version('lifecycle', '2.3.1')
alias('lifecycle-runtime').to('androidx.lifecycle, 'lifecycle-runtime').versionRef('lifecycle')
alias('lifecycle-viewmodel-ktx').to('androidx.lifecycle, 'lifecycle-viewmodel-ktx').versionRef('lifecycle')
bundle('lifecycle',
['lifecycle-runtime', 'lifecycle-viewmodel-ktx'])
}
}
}

Подключение пакета зависимостей будет выглядеть так:

dependencies {
implementation libs.bundles.lifecycle
}

Добавление одного пакета эквивалентно добавлению всех зависимостей из пакета по отдельности.

Помимо описания каталога в settings.gradle(.kts) файле, есть более простая возможность собрать все зависимости вместе использовать toml-файл каталоге gradle: libs.versions.toml.

То есть, плюс еще один стандарт к представленному стандарту описания зависимостей.

По умолчанию libs.versions.toml файл будет входом в libs каталог. Можно также изменить имя каталога по умолчанию, например:

dependencyResolutionManagement {
defaultLibrariesExtensionName.set('deps')
}

Toml-файл состоит из 3 основных разделов:

[versions] - раздел для объявления версий
[libraries] - раздел для объявления зависимостей
[bundles] - раздел для объявления пакетов зависимостей

Например,

[versions]
lifecycle = "2.3.1"

[libraries]
lifecycle-runtime = { module = "androidx.lifecycle:lifecycle-runtime", version.ref = "lifecycle" }
lifecycle-viewmodel-ktx = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "lifecycle" }

[bundles]
dagger = ["lifecycle-runtime", "lifecycle-viewmodel-ktx"]

Версию можно объявить просто как строку с номером, и в этом случае она считается обязательной версией, или в расширенном варианте, комбинируя информацию о версиях разного уровня:

[versions]
any-lib1 = 1.0
any-lib2 = { strictly = "[1.0, 2.0[", prefer = "1.2" }

Более подробно о расширенном варианте версии по ссылке

Семантика объявления номера версии по ссылке

Саму зависимость можно прописать, указав полное имя или отделив версию от группы и имени:

[libraries]
any-lib = "com.company:anylib:1.4"
any-other-lib = { module = "com.company:other", version="1.4" }
any-other-lib2 = { group = "com.company", name="alternate", version="1.4" }
anylib-full-format = { group = "com.company", name="alternate", version={ require = "1.4" } }

Если необходимо сослаться на версию, объявленную в [versions] разделе, то следует использовать свойство version.ref:

[versions]
some = "1.4"

[libraries]
any-lib = { group = "com.company", name="anylib", version.ref="some" }

Можно использовать несколько toml-файлов.Для этого нужно указать, как импортировать соответствующий файл:

dependencyResolutionManagement {
versionCatalogs {
testLibs {
from(files('gradle/test-libs.versions.toml'))
}
}
}

Также есть возможность создать каталог зависимостей и использовать его между разными проектами в рамках одной компании, например.

Подробнее по ссылке

Новая функциональность Gradle в целом неплоха, с ней действительно удобнее централизованно описывать все зависимости в проекте. Но пока есть некоторые неудобства в работе с ней. Например, не подсвечивается номер версии зависимости, которую можно обновить. Хотя, уже есть плагины решающие эту проблему.

При использовании Groovy не работает автоподстановка при указании зависимости в build.gradle файле и, соответственно, нет возможности провалиться в описание зависимости при нажатии на нее. Исправлять это не планируют. Решение для автоподстановки использовать Kotlin DSL.

Подробнее..

Дайджест интересных материалов для мобильного разработчика 396 (31 мая 6 июня)

06.06.2021 16:09:40 | Автор: admin
Сегодня в нашем дайджесте архитектурные паттерны и победители Swift Student Challenge, инициализация цепочек и цветов Fuchsia, инди-акселератор и инди-фестиваль от Google, Android 12 для разработчиков, $643 млрд из App Store и многое другое!



Этот дайджест доступен в виде еженедельной рассылки. А ежедневно новости мы рассылаем в Telegram-канале.

iOS

Архитектурные паттерны в iOS: привет от дядюшки Боба, или Clean Architecture
Тернистый путь внедрения Swift Package Manager. Доклад Яндекса
Swift и CoreData. Или как построить Swift ORM на основе Objective-C ORM
Как сделать экран подтверждения СМС-кода на iOS
Мои приложения для разработчиков вышли в топ iOS и Mac App Store: сколько это принесло?
WWDC21: Школьники и студенты из России победители Swift Student Challenge
Объявлены номинанты Apple Design Awards 2021
Добавляем поддержку Siri в iOS-приложение за считанные минуты
Как сериализовать и десериализовать объекты в iOS
Как улучшить время компиляции и выполнения Xcode
Удаление фона с помощью Core ML и SwiftUI
Как извлечь функциональность из устаревшего iOS-кода
Приложение для чата без пароля для iOS с Auth0
Как добавить Swift-код в качестве кастомной LLDB команды
Design to Code: превращая дизайн в код
SPIndicator: индикатор в стиле Apple

Android

Проекты в Gradle 7: как не зависеть от зависимостей
Всё о PendingIntents
Инициализация Rx цепочки
Proto DataStore + AndroidX Preferences на Kotlin
Подробный обзор Android 12 для разработчиков
Введение в систему Снапшотов Compose
Недоверенные события касания
Понимаем юнит-тесты для Android в 2021
Polestar предлагает эмулятор для разработчиков, создающих приложения для Android Automotive
QA-инженеры, функциональное и UI-тестирование в Azimo
10 лучших библиотек для разработчиков Android в 2021 году
Сохранение данных на Android с помощью Room Database и Data Store Руководство для начинающих
CheckboxQuestions: вопросы и чекбоксы
Compose Space Invaders: игра для декстопа на Jetpack Compose
Carousel Recyclerview: красивая карусель

Разработка

Как художнику найти работу мечты в геймдеве. А также советы по оформлению портфолио
4 технических решения, которые делают API сервис успешным
C# vs Kotlin
Как и зачем Mail.ru Group провела редизайн мобильной версии главной страницы портала
Mobile People Talks: какого же цвета Fuchsia?
Podlodka #218: схемотехника
HarmonyOS заработала на смартфонах
Новый SDK от Loomдобавляет видео-сообщения в любые веб-приложения
Facebook открывает Messenger API в Instagram для всех
Задачи с собеседований: зарплата
Дизайн приложений: примеры для вдохновения #44
Stack Overflow продан за $1.8 млрд
Что не так с Flutter?
Исследование продакт-менеджеров 2021 от Product Plan
Как оставаться в физической и ментальной форме, продолжая программировать
О создании гибкого пользовательского интерфейса на примере Instagram Threads
Представляем новый язык дизайна Material You от Google
Сеты бесплатных иконок для разработчиков и дизайнеров
Как привлечь первых 100 клиентов в SaaS: 5 простых шагов
Следующим стартапом на триллион станет образовательная компания
5 задач для автоматизации с помощью Python
Я не мог быстро тратить деньги, и это чуть не убило мой стартап
Flutter 2.2: создаем первую Universal Windows Program (UWP)
Мой код плохо пахнет, но все в порядке
Как создать свою первую Облачную функцию Firebase
5 вещей, которые я узнал после двух лет работы инженером-программистом в Microsoft
Test-driven Development для создания пользовательских интерфейсов
Мой опыт интервью в Twitter
Flutter: создание красивых приложений для Windows удобная структура дизайна и навигация
Вселенная no-code/low-code стартапов и ее игроки
Пример дизайна: Safe Space wellness-приложение для Android
База данных с вопросам из интервью в Apple

Аналитика, маркетинг и монетизация

В Android также ограничивают действие рекламного идентификатора
make sense: О запуске агротех-стартапа
Voodoo открывает летний конкурс гиперказуальных игр
Google запускает Indie Games Accelerator и Indie Games Festival
Продажи в App Store в 2020 выросли на 24% до $643 млрд
Создатели читов для PUBG Mobile заработали $77 млн
3 лучшие техники геймификации
Greg: приложение для любителей растений
Маркетплейс для разработчиков Malt получил 80 млн
Социальная сеть Poparazzi стала 1 App Store: секреты роста
Проектирование продуктов, формирующих привычки
Ошибки при расчете юнит-экономики
9 способов встроить виральность в ваш продукт
Как создать отличные скриншоты для страницы приложения в App Store

AI, Устройства, IoT

Учиться, учиться, и ещё раз учиться?
Теория игр как механизм для анализа крупномасштабных данных

Предыдущий дайджест. Если у вас есть другие интересные материалы или вы нашли ошибку пришлите, пожалуйста, в почту.
Подробнее..

Создание прекрасных приложений с помощью Xamarin.Forms

09.06.2021 10:16:43 | Автор: admin

Есть вопрос, который мне постоянно задают в Твиттере: как создавать приложения с крутым дизайном с помощью Xamarin.Forms? Это отличный вопрос, ведь любой может создавать красивые приложения, немного вдохновившись и поработав над стилем. Я не дизайнер и не претендую на звание дизайнера, но есть много отличных источников вдохновения для дизайна приложений, включая Dribbble, Uplabs и другие. Эти дизайны от талантливых людей со всего мира могут повлиять на внешний вид ваших собственных приложений.

Приложение для ресторана от Oludayo AlliПриложение для ресторана от Oludayo Alli

Встроенные возможности Xamarin.Forms

В Xamarin.Forms есть несколько функций, которые можно использовать, чтобы воплотить важные проекты в жизнь. Зачем вам что-то, кроме нового Shapes API для рисования фигур, линий, многоугольников и многого другого. Хотите, чтобы ваши собственные элементы управления были единообразными? Как насчет добавления Material Design с помощью одной строчки кода. А еще сгруппируйте свои коллекции с помощью CarouselView в сочетании с IndicatorView и, конечно же, CollectionView.

Приложение "Галерея напитков" от Javier SurezПриложение "Галерея напитков" от Javier Surez

Пойдите дальше с кастомными элементами управления от сообщества

Xamarin Community Toolkit добавляет отличные элементы управления, включая DockLayout, Shield, TabView и другие. Но есть еще более потрясающие элементы управления от сообщества, включая потрясающие Magic Gradients, PancakeView, MaterialFrame, CardView, Shadows и многие другие. Наконец, мы не можем забыть SkiaSharp, систему 2D-графики общего назначения для .NET.

Компонентная экосистема

Переиспользуемые компоненты пользовательского интерфейса от ведущих поставщиков компонентов, таких как Telerik, UX Divers, GrapeCity и Syncfusion, помогут вам быстро повысить продуктивность. Обязательно ознакомьтесь с множеством вариантов, когда будете готовы начать работу.

Вдохновляйтесь

Наш коллега Хавьер уже несколько лет собирает интересные примеры крутых приложений с открытым исходным кодом, созданных с помощью Xamarin.Forms, которые вы можете изучить на GitHub. Выше вы уже видели несколько из этих приложений, но вот еще, чтобы вас вдохновить.

Кошелек карт от Altevir Кошелек карт от Altevir Приложение для авиаперелетов от Leomaris ReyesПриложение для авиаперелетов от Leomaris ReyesКнига рецептов от Steven ThewissenКнига рецептов от Steven ThewissenCake app от Shaw YuCake app от Shaw Yu

И есть еще очень много прекрасных дизайнов. Вы даже можете добавить свой собственный, просто создав pull request в репозитории Хавьера на GitHub.

Adobe XD Exporter

Многие дизайны, которые вы найдете в интернете или получите от вашего дизайнера, могут быть созданы с помощью таких инструментов, как Adobe XD. Вы можете легко импортировать цвета и стили в свое приложение Xamarin.Forms благодаря экспортеру XD в Xamarin.Forms (его автор -- наш коллега Kym Phillpotts).

Создавайте красивые приложения

Расскажите нам о своих приложениях, оставив комментарии ниже или отправив pull request в репозиторий Хавьера на GitHub.

Подробнее..

Дайджест интересных материалов для мобильного разработчика 397 (7 13 июня)

13.06.2021 14:11:44 | Автор: admin
В этом дайджесте обсуждаем конференцию WWDC и ее последствия, быстрые обновления Android и ответственность команд, автоматизацию с помощью таблиц, применение КММ, цвета, элементы управления и многое другое.



Этот дайджест доступен в виде еженедельной рассылки. А ежедневно новости мы рассылаем в Telegram-канале.

iOS

Xcode Cloud, SharePlay, Focus самое важное с Keynote WWDC21
Apple убивает TeamCity, Bitrise, Appcenter, Fastlane, Firebase, Sentry и иже с ними. Краткий обзор Xcode Cloud
Делаем OpenVPN клиент для iOS
iOS интервью в Vivid
Лучшие приложения для iPhone весят почти в 4 раза больше, чем пять лет назад
Mobile People Talks: WWDC21
Apple выпускает бета-версии прошивок AirPods для разработчиков
iOS 4 воссоздали как приложение для iPhone
Дырявим вьюхи на Swift
Apple уточняет правила публикации в App Store
Apple разрешит пользователям остаться на iOS 14
Новые функции iPadOS 15
Apple представила новые технологии и инструменты для разработчиков приложений
watchOS 8: новые функции доступа, возможности подключения и практики осознанности
Apple представила iOS 15
Главное в iOS 15 для дизайнеров
Доклад Platforms State of the Union с WWDC 2021
Отчеты о сбоях iOS с LLDB
Онбординг SwiftUI в приложении UIKit
Лучшая маршрутизация глубоких ссылок в iOS-приложении
Что мы узнали из инцидента с OOM в iOS-приложении Pinterest
Делаем бесконечную прокрутку фотографий в iOS
Что нового в SwiftUI после WWDC21
SwiftUI двунаправленный список SnapList
Использование SwiftUI с View Model, написанной на Kotlin Multiplatform Mobile
Пишем первое приложение для iOS с помощью Realm, SwiftUI и Combine
Что нового в StoreKit 2
Как мы используем SwiftUI в приложении Medium
Что нового в SwiftUI 3.0?
Как сделать иконку для темного режима для вашего приложения
iOS 15 привносит атрибутные строки в SwiftUI
Понимаем AsyncImage в SwiftUI
Indicate: тосты в стиле AirPods
SimpleAnalytics: своя аналитика для iOS

Android

Долгая дорога к быстрым обновлениям Android
Миграция с LiveData на Kotlins Flow
Бесконечная автопрокрутка списков с помощью RecyclerView и LazyLists в Compose
Разработчики могут подать заявку на снижение комиссии до 15% через Play Store
Flutter Dev Podcast #28: Google I/O 2021
Корутины обработки ошибок
Проблема трех фреймворков в Kotlin Multiplatform Mobile
Современная архитектура Android с шаблоном проектирования MVI
Азбука модульности Android в 2021 году
Навигация в Jetpack Compose
Несколько бэк-стэков
Работа с сетью в Kotlin Ktor на Android
Автогенерация пользовательских размеров для Android с помощью Kotlin
Глубокое погружение в интернационализацию приложений для Android на Jetpack Compose
Датабиндинг в Android
Чистая архитектура Android [точка зрения]
Создаем приложение CoroutineScope с помощью Hilt
Пагинация в Android с Paging 3, Retrofit и Kotlin Flow
CompleteKotlin: автодополнение для всех платформ
TimeRangePicker: круглый range picker для Android

Разработка

Кто, где, когда: система компонентов для разделения зон ответственности команды
Автоматизация или смерть: как управлять тысячами единиц игрового контента с помощью гугл-таблиц
Appwrite, open-source бэкэнд-платформа
Роль QA Lead в продуктовой компании: особенности и зоны ответственности
Вызов кода Go из Dart с использованием cgo и Dart FFI на простом примере
Создание прекрасных приложений с помощью Xamarin.Forms
We need to go deeper: как пасхалка в приложении Delivery Club сократила субъективное время ожидания еды
Тестируем и визуализируем с помощью Mind Map
Автоплатеж, автооплата или автопополнение? UX-кейс
Как стать тестировщиком с нуля
Podlodka #219: выбор первой профессии в IT
Дизайн приложений: лауреаты премии Apple Design Awards 2021
КММ на практике или выбор кроссплатформенного фреймворка для Леруа Мерлен
Исследование разработчиков HackerEarth 2021
Отключенные кнопки не должны путать пользователей
Распродажа книг по Data Science и аналитике данных в Humble Bundle
6 мощных инструментов для разработчиков, использующих Mac
Как мы улучшили сегментированные элементы управления (segmented control)
Руководство для новичков по применению цвета в UI дизайне
Пошаговое руководство по работе в Figma. Урок по созданию мобильного приложения
Принцип IBM Leadership-as-a-service обеспечивает профессиональный рост команд
Топ-5 шаблонов проектирования распределенных систем
ELI5: Flipper кроссплатформенный дебагер
Как ежедневно улучшать навыки архитектуры ПО
Анимированный TabBar Coinbase в React Native
Как проверять код Junior разработчику
Разработчики не могут исправить плохой менеджмент

Аналитика, маркетинг и монетизация

WWDC 2021: новое и полезное для разработчика, ASO спецалиста, маркетолога мобильных приложений
Почему подписываются пользователи? Как повысить конверсию мобильных приложений
Classplus: Spotify для образования
AppsFlyer: на 570% выросло количество неорганических установок финансовых приложений в России
Стратегия победителя: как покорить весь мир, начиная с Якутска? Кейс inDriver и Rocket10
Кейс: как вытеснить конкурентов из поиска, оптимизируя инаппы?
Практические инструменты и преимущества отслеживания удалений мобильных приложений
Онбординг в мобильном приложении: как поддерживать интерес пользователей

AI, Устройства, IoT

Как Яндекс применил генеративные нейросети для поиска ответов
Маленький и быстрый BERT для русского языка
Начинаются продажи карманной игровой приставки Playdate
Защищенный смартфон, контролируемый ФБР, раскрыл действия сотен преступников

Предыдущий дайджест. Если у вас есть другие интересные материалы или вы нашли ошибку пришлите, пожалуйста, в почту.
Подробнее..

Дайджест интересных материалов для мобильного разработчика 398 (14 20 июня)

20.06.2021 12:09:43 | Автор: admin
В этой подборке исследуем StoreKit 2, распознаем лица и позы на Android, улучшаем производительность React-приложений, учим сквирклморфизм и многое другое!



Этот дайджест доступен в виде еженедельной рассылки. А ежедневно новости мы рассылаем в Telegram-канале.

iOS

За что App Store может отклонить приложение: чек-лист
Meet StoreKit 2
Тим Кук: на Android в 47 раз больше вредоносных программ, чем на iOS
Новый антимонопольный акт может заставить Apple продать App Store
Что нового во встроенных покупках в iOS 15 WWDC 21
Строим лабиринты с SwiftUI
iOS 15: заметные дополнения к UIKit
Info.plist отсутствует в Xcode 13 вот как его вернуть
ScrollView в XCode 11
Создаем игры на SwiftUI с помощью SpriteKit
Мастерим списки в SwiftUI
Как лучше структурировать свои проекты в Xcode
Глубокое погружение в Акторы в Swift 5.5
Разработка функций iOS-приложения в виде модулей в Xcode
Как делать видеозвонки с помощью SwiftUI
Euler: вычислительный фреймворк на Swift
WorldMotion: положение устройства относительно Земли

Android

Как использовать Android Data Binding в пользовательских представлениях?
AppSearch из Jetpack вышел в альфа-версии
Распознавание лиц и поз за 40 минут
Android Broadcast: новости #10
Создайте свою библиотеку KMM
История моего первого а-ха-момента с Jetpack Compose
Как стать ассоциированным разработчиком Android (Kotlin Edition)
Анимации Jetpack Compose в реальном времени
RecyclerView с NestedScrollView: лучшие практики
Android Bitbucket Pipeline CI/CD с Firebase App Distribution
CompileSdkVersion и targetSdkVersion в чем отличие?
Нижняя панель навигации Android с Jetpack Compose
Интеграция Google Sign-in в Android-приложение
Focus в Jetpack Compose
DashedView: полосатые View
Screen Tracker: название видимого Activity/Fragment
SquircleView: красивые View

Разработка

5 000 000 строк кода, 500 репозиториев: зачем мы адаптировали приложение AliExpress для Рунета
Десятикратное улучшение производительности React-приложения
gRPC + Dart, Сервис + Клиент, напишем
Podlodka #220: волонтерство в IT
Хороший день разработчика: Good Day Project от GitHub
К 2024 году 80% технологических продуктов будут создавать непрофессионалы
Сквирклморфизм (Squirclemorphism) в дизайне интерфейсов
12 рекомендаций, которые помогут улучшить процесс регистрации и входа в систему
React Native в Wix Архитектура (глубокое погружение)
Как узнать плохой код? 8 вещей
5 лучших пакетов Flutter, которые вы должны знать
Советы по кодинг интервью в Google
Как стать плохим разработчиком

Аналитика, маркетинг и монетизация

Гайд по тестированию рекламы для мобильных приложений
Вслед за Apple и Google комиссию магазина приложений снизила Amazon
make sense: О инфлюенсер-маркетинге
UserLeap получает еще $38 млн на отслеживание пользовательского опыта
Классическая MMORPG RuneScape запускается на iOS и Android
Маркетологи в мобайле: Александр Плёнкин (Vprok.ru Перекрёсток)
Почему такие скриншоты пустая трата времени? (пока у вас нет 4,000 загрузок в месяц)
Amplitude получил еще $150 млн
$100 млн для Free Fire: как младший брат может обогнать старшего на уже сложившемся рынке?
App Annie: рынок мобильных игр в России в 2020 вырос на 25% до $933 млн
Темные паттерны и уловки в мобильных приложениях
Использование BigQuery и Firebase Analytics для привлечения, вовлечения и оценки пользователей

AI, Устройства, IoT

Запускаем DOOM на лампочке
Быстрое обнаружение Covid-19 на рентгеновских снимках с помощью Raspberry Pi
Как я учу Python на Raspberry Pi 400 в библиотеке
Топ-5 преемников GPT-3, о которых вы должны знать в 2021 году

? Предыдущий дайджест. Если у вас есть другие интересные материалы или вы нашли ошибку пришлите, пожалуйста, в почту.
Подробнее..

Основы Flutter для начинающих (Часть I)

30.05.2021 12:21:38 | Автор: admin

Вступление

Добрый день всем желающим познакомиться с Flutter!

У меня появилось горячее желание поделиться с вам моими знаниями, которые я накопил за несколько месяцев.

Возможно мой опыт совсем мал по сравнению с гуру программистами, я все же попытаюсь сделать что-нибудь полезное для вас.

Результатом нашей работы будет небольшое Flutter приложение, которое будет брать данные из JSONPlaceholder.

Первый шаг - Настройка и установка компонентов

Ну что ж, приступим.

Переходим на страницу установки: Install - Flutter и загружаем Flutter для своей платформы

Затем устанавливаем редактор или IDE по инструкции Set up an editor

Я буду использовать Android Studio IDE от Google.

Для разработки на Android Studio нужно установить Flutter плагин (в инструкции Set up an editor, описано как это сделать).

Второй шаг - Создание проекта

Выбираем Flutter Application

Далее указываем название приложения (имя Flutter приложения должно иметь нижний регистр, отдельные слова могут разделяться нижним подчеркиванием).

Затем указываем package name (используется для того, чтобы идентифицировать наше приложение среди других в Google Play или Apple Store, его впоследствии можно будет изменить, более подробно об Android Application ID или об Apple App ID):

Нажимаем Finish.

Третий шаг - создание первоначальной структуры приложения

Очищаем main.dart файл от ненужного кода:

import 'package:flutter/material.dart';// main() является главной функцией с которой начинается // выполнение приложения// возвращает виджет приложенияvoid main() => runApp(MyApp());// В Flutter все является виджетом (кнопки,списки, текст и т.д.)// виджет - это отдельный компонент, который может быть отрисован// на экране (не путать с Android виджетами)// Наиболее простые виджеты наследуются от StatelessWidget класса// и не имеют состоянияclass MyApp extends StatelessWidget {// функция build отвечает за построение иерархии виджетов  @override  Widget build(BuildContext context) {// виджет MaterialApp - главный виджет приложения, который  // позволяет настроить тему и использовать    // Material Design для разработки.    return MaterialApp(    // заголовок приложения      // обычно виден, когда мы сворачиваем приложение      title: 'Json Placeholder App',      // настройка темы, мы ещё вернёмся к этому      theme: ThemeData(        primarySwatch: Colors.blue,      ),      // указываем исходную страницу, которую мы создадим позже      home: HomePage(),    );  }}

Затем создаем пакет (код должен быть всегда огранизован, дабы сделать его понятнее):

Называем его pages:

Затем создаем в пакете файл home_page.dart:

И реализуем нашу первую страницу:

import 'package:flutter/material.dart';// StatefulWidget имеет состояние, с которым// позже мы будем работать через функцию// setState(VoidCallback fn);// обратите внимание setState принимает другую функциюclass HomePage extends StatefulWidget {  // StatefulWidget должен возвращать класс,  // которые наследуется от State  @override  _HomePageState createState() => _HomePageState();}// В треугольных скобках мы указываем наш StatefulWidget // для которого будет создано состояние// нижнее подчеркивание _ используется для того, // чтобы скрыть доступ к _HomePageState  из других файлов// нижнее подчеркивание аналогия private в Java / Kotlinclass _HomePageState extends State<HomePage> {    // функция buil, как мы уже отметили, строит  // иерархию наших любимых виджетов  @override  Widget build(BuildContext context) {    // В большинстве случаев Scaffold используется,    // как корневой виджет для страницы или экрана    // Scaffold позволяет вам указать AppBar, BottomNavigationBar,    // Drawer, FloatingActionButton и другие не менее важные    // компоненты (виджеты).    return Scaffold(      // мы создаем AppBar с текстом "Home Page"      appBar: AppBar(title: Text("Home page")),      // указываем текст в качестве тела Scaffold      // текст предварительно вложен в Center виджет,      // чтобы выровнять его по центру        body: Center(        child: Text(          "Hello, JSON Placeholder!!!",          // Также выравниваем текст внутри самого виджета Text          textAlign: TextAlign.center,          // Theme.of(context) позволяет получить доступ к           // текущему ThemeData, который был указан в MaterialApp          // После получения ThemeData мы можем использовать          // различные его стили (например headline3, как здесь)          style: Theme.of(context).textTheme.headline3,        )      )    );  }  }

Обратите внимание на мощь Flutter - мы можем вкладывать различные виджеты друг в друга, комбинировать их и создавать более сложные структуры

Четвертый шаг - запуск

Ну что ж, пора испытать наше приложение.

Не забудьте импортировать HomePage в main файл:

import 'pages/home_page.dart';

Важный момент: старайтесь использовать относительный путь, когда импортируете файлы своего проекта, вместо подобного:

import 'package:json_placeholder_app/pages/home_page.dart';

По моему небольшому опыту могут возникнуть проблемы, когда вы захотите поменять имя приложения в pubspec.yaml файле (pubspec.yaml находиться в корневой директории проекта):

Если вы уже используете подобные импорты, это не критично, имя приложения всегда можно будет поменять отдельно для iOS или Android.

Переходим к запуску, выбираем устройство на котором будет выполняться приложение (в данном случае я использую реальное устройство, мой Honor 30i), и нажимаем Run:

Та дам!

Если вас раздражает надпись DEBUG в правом верхнем углу, то её можно убрать:

import 'package:flutter/material.dart';// main() является главной функцией с которой начинается // выполнение приложения// возвращает виджет приложенияvoid main() => runApp(MyApp());// В Flutter все является виджетом (кнопки,списки, текст и т.д.)// виджет - это отдельный компонент, который может быть отрисован// на экране (не путайте с Android виджетами)// Наиболее простые виджеты наследуются от StatelessWidget класса// и не имеют состоянияclass MyApp extends StatelessWidget {// функция build отвечает за построение иерархии виджетов  @override  Widget build(BuildContext context) {    // виджет MaterialApp - главный виджет приложения, который    // позволяет настроить тему и использовать    // Material Design для разработки.    return MaterialApp(      // заголовок приложения      // обычно виден, когда мы сворачиваем приложение      title: 'Json Placeholder App',      // убираем баннер      debugShowCheckedModeBanner: false,      // настройка темы, мы ещё вернёмся к этому      theme: ThemeData(        primarySwatch: Colors.blue,      ),      // указываем исходную страницу, которую мы создадим позже      home: HomePage(),    );  }}

Также обратите внимание, когда вы запустите приложение, вы можете использовать hot reload:

Hot Reload позволяет буквально за 2-5 секунд внести изменения, когда ваше приложение выполняется.

Это довольно приятная опция, которая ускорит вашу разработку.

При каждом вызове Hot Reload происходит перезапуск build функции. (вся иерархия виджетов перестраивается)

Будьте внимательны: не во всех ситуциях Hot Reload срабатывает и изменения отражаются в приложении, поэтому в таких ситуациях нужно перезапускать приложение полностью.

Также есть довольно интересный факт: размер отладочного приложения на Flutter с одним экраном, которое мы только что создали:

Этого бояться не стоит, т.к. release Flutter приложения будет весить гораздо меньше.

Отладочное приложение содержит много дополнительной информации, а также к этому добавляется поддержка Hot Reload.

Четвертый шаг - использование состояния

Состоянием можно считать набор переменных и данных в определенный момент времени, которое впоследствии может быть изменено.

Ну что ж попробуем реализовать небольшую анимацию которая будет запускаться по кнопки:

import 'package:flutter/material.dart';// StatefulWidget имеет состояние, с которым// позже мы будем работать через функцию// setState(VoidCallback fn);// обратите внимание setState принимает другую функциюclass HomePage extends StatefulWidget {  // StatefulWidget должен возвращать класс,  // которые наследуется от State  @override  _HomePageState createState() => _HomePageState();}// В треугольных скобках мы указываем наш StatefulWidget// для которого будет создано состояние// нижнее подчеркивание используется для того, чтобы // скрыть доступ к _HomePageState из других файлов// нижнее подчеркивание - аналогия private в Java / Kotlinclass _HomePageState extends State<HomePage> {  // добавим переменную, которая будет нашим состоянием  // т.к. _counter мы будем использовать только внутри нашего  // класса, то сделаем его недоступным для других классов  // _counter будет хранить значение счетчика  var _counter = 0;  // build как мы уже отметили, строит  // иерархию наших любимых виджетов  @override  Widget build(BuildContext context) {    // В большинстве случаев Scaffold используется    // как корневой виджет для страницы или экрана    // Scaffold позволяет вам указать AppBar, BottomNavigationBar,    // Drawer, FloatingActionButton и другие не менее важные    // компноненты (виджеты).    return Scaffold(      // мы создаем AppBar с текстом "Home page"      appBar: AppBar(title: Text("Home page")),      // указываем текст в качестве тела Scaffold      // текст предварительно вложен в Center виджет,      // чтобы выровнять его по центру      body: Center(        // добавляем AnimatedSwitcher, который и будет управлять        // нашей анимацией        child: AnimatedSwitcher(          // обратите внимание: const указывает          // на то, что нам известно значение Duration во время          // компиляции и мы не будем его менять во время выполнения          // класс Duration позволяет указать задержку в разных          // единицах измерения (секунды, миллисекунды и т.д.)          duration: const Duration(milliseconds: 900),          // AnimatedSwitcher создает reverse эффект,          // то  есть эффект возврата анимации к первоначальному          // состоянию, что выглядит не всегда красиво,          // поэтому я указал reverseDuration в 0          // вы можете поэкспериментировать с этим значением          reverseDuration: const Duration(milliseconds: 0),          child: Text(            // вывод значения счетчика            // при каждой перерисовки виджетов _counter             // увеличивается на единицу            "$_counter",            // здесь самое интересное            // когда мы изменяем значение _counter            // и вызываем функцию setState, компоненты            // перерисовываются и AnimatedSwitcher сравнивает            // предыдущий key своего дочернего виджета с текущим,            // если они не совпадают, то вопроизводит анимацию            key: ValueKey<int>(_counter),            // Также выравниваем текст внутри самого виджета Text            textAlign: TextAlign.center,            // Theme.of(context) позволяет получить доступ к            // текущему ThemeData, который мы указали в MaterialApp            // После получения ThemeData мы можем использовать            // различные его стили (например headline3, как здесь)            style: Theme.of(context).textTheme.headline3,          ),        )      ),      // добавляем кнопку      // FloatingActionButton - круглая кнопка в правом нижнем углу      floatingActionButton: FloatingActionButton(        // указываем иконку        // Flutter предлагает нам большой спектр встроенных иконок        child: Icon(Icons.animation),        onPressed: () {          // наконец то мы дошли до функции setState          // которая даст сигнал, что пора перерисовывать           // наши виджеты.           // здесь мы просто увеличиваем наш счетчик          setState(() {            _counter++;          });        },      ),    );  }}

Выполняем приложение:

Та дам! Выглядит здорово!

Заключение

Статья получилась достаточно информативной и по моему мнению полезной для новичков.

Я постараюсь сделать цикл статей, в которых мы будет разрабатывать одно приложение, постепенно получая новые знания и совершенствуясь.

Примерный план:

1) Часть 1 (текущая статья) - введение в разработку, первое приложение, понятие состояния;

2) Часть 2 - BottomNavigationBar и Navigator;

3) Часть 3 - MVC. Мы будем использовать именно этот паттерн, как один из самый простых;

4) Часть 4 - http пакет. Создание Repository класса, первые запросы, вывод списка постов;

5) Часть 5 - Работа с картинками, вывод картинок в виде сетки, получение картинок из сети, добавление своих в приложение;

6) Часть 6 - Создание своей темы, добавление кастомных шрифтов и анимации;

7) Часть 7 - Немного о тестировании;

Надеюсь, что я смогу принести вам пользу и вы не закидаете меня тухлыми помидорами))

Все пожелания в комментариях)

До скорой встречи!

Подробнее..

Инициализация Rx цепочки

31.05.2021 14:10:41 | Автор: admin

Всем привет, меня зовут Иван, я Android-разработчик. Сегодня хочу поделится своим опытом работы с RxJava2 и рассказать, как происходит инициализация цепочки. Почему я вообще решил поднять эту тему? Пообщавшись со знакомыми разработчиками, я понял, что не каждый кто использует этот инструмент понимает, как он работает. И тогда я решил разобраться как же устроены подписки в RxJava2 и в какой последовательности вся работа инициализируется. Я не нашел ни одной статьи, поясняющей это. В свете этого я полез в исходники, чтобы посмотреть, как же все работает и набросал для себя небольшую шпаргалку, которая выросла в данную статью.

В этой статье я не буду описывать что такое Observable, Observer и все остальные сущности, которые используются в RxJava2. Если вы решили прочитать данную статью, то я предполагаю, что вы уже знакомы с этой информацией. А если вы всё же не знакомы с этими понятия, то я рекомендую перед началом чтения ознакомиться с ними.

Вот с чего можно начать:

Грокаем* RxJava

Исследуем RxJava 2 для Android

Давайте посмотрим, как работает простейшая цепочка:

Observable.just (1, 2, 3, 4, 5).map {}.filter {}.subscribe();

По верхам

Сначала опишу вкратце каждый шаг, через который пройдем в этой цепочке (шаги начинаются сверху вниз):

  • Создается объект в операторе just ObservableFromArray.

  • Создается объект в операторе map ObservableMap, который принимает в конструктор ссылку на ранее созданный объект в операторе just.

  • Создается объект в операторе filter ObservableFilter, который принимает в конструктор ссылку на ранее созданный объект в map, в котором уже хранится ссылка на just.

  • После создания всех Observableов у последнего Observable в цепочки вызывается метод subscribe() (в нашем случае это ObservableFilter созданный в операторе filter) в котором создается новый Observer, который и будет обрабатывать все полученные события.

  • В методе ObservableFilter.subscribe() вызывается следующий метод ObservableFilter.subscribeActual(), в котором создается внутренний Observer, в случае с оператором filter, это FilterObserver. В этот внутренний Observer передается ссылка на первый созданный Observer в ObservableFilter.subscribe().

  • Вызывается ObservableMap.subscribe() в котором так же вызывается ObservableMap.subscribeActual()и создается внутренний Observer, в случае с оператором map, это MapObserver, в который передается ссылка на FilterObserver.

  • Вызывается ObservableFromArray.subscribe() и после ObservableFromArray.subscribeActual(), и уже там вызывается метод onSubscribe()у переданного в ObservableFromArray.subscribeActual() Observerа.

  • onSubscribe() вызывается у каждого нижележащего Observerа в цепочке.

  • ObservableFromArray начинает излучать все события в метод onNext() нижележащего Observerа.

Визуальное представление описанной выше схемы.Визуальное представление описанной выше схемы.

Создание источников данных

Теперь давайте рассмотрим описанные выше шаги подробнее, сначала попадаем в метод just() где происходит проверка каждого значения на null, далее идет вызов метода fromArray(), который возвращает Observable.

public static <T> Observable<T> just(T item1, T item2, T item3, T item4, T item5) {   ObjectHelper.requireNonNull(item1, "item1 is null");   ObjectHelper.requireNonNull(item2, "item2 is null");   ObjectHelper.requireNonNull(item3, "item3 is null");   ObjectHelper.requireNonNull(item4, "item4 is null");   ObjectHelper.requireNonNull(item5, "item5 is null");   return fromArray(item1, item2, item3, item4, item5);}

В fromArray() проверяется, что метод принимает в себя не пустой массив и имеет больше одного элемента.

public static <T> Observable<T> fromArray(T... items) {   ObjectHelper.requireNonNull(items, "items is null");   if (items.length == 0) {       return empty();   }   if (items.length == 1) {       return just(items[0]);   }   return RxJavaPlugins.onAssembly(new ObservableFromArray<T>(items));}

После прохода через все эти шаги создается новый экземпляр класса ObservableFromArray, который на вход принимает массив с данными.

После этого новый объект передается в метод onAssembly(), так как нет необходимости как-то еще модифицировать Observable, то возвращается тот же источник, который поступал на вход.

public static <T> Observable<T> onAssembly(@NonNull Observable<T> source) {   Function<? super Observable, ? extends Observable> f = onObservableAssembly;   if (f != null) {       return apply(f, source);   }   return source;}

onAssembly() проверяет хотим ли перехватить текущий Observable и как-то модифицировать его, например таким образом:

RxJavaPlugins.setOnObservableAssembly(o -> {if (o instanceof ObservableFromArray) {    return new ObservableFromArray<>(new Integer[] { 4, 5, 6 });}return o;}); Observable.just(1, 2, 3).filter(v -> v > 3).test().assertResult(4, 5, 6);
Только что созданный ObservableFromArrayТолько что созданный ObservableFromArray

Вызывается следующий оператор в цепочке map(). Оператор проходит почти через те же шаги, что были описаны выше. Сначала делается проверка на null, потом создается новый экземпляр класса ObservableMap.

public final <R> Observable<R> map(Function<? super T, ? extends R> mapper) {   ObjectHelper.requireNonNull(mapper, "mapper is null");   return RxJavaPlugins.onAssembly(new ObservableMap<T, R>(this, mapper));}

Вот тут появляется небольшое отличие, в конструктор ObservableMap передается не только mapper, который будет преобразовывать одно значение в другое, но также принимает в конструктор this (source). В данном случае this это ссылка на экземпляр класса ObservableFromArray созданный на предыдущем шаге. ObservableMap расширяет абстрактный класс AbstractObservableWithUpstream, в котором и храниться source.

AbstractObservableWithUpstream абстрактный класс, который реализуют Observable операторы и в котором хранится ссылка на вышележащий источник данных.

Далее происходит вызов метода onAssembly() и возвращение созданного Observable.

Обновленная схема с созданным ObservableMapОбновленная схема с созданным ObservableMap

Переходим к следующему оператору в цепочки filter(). В нем не происходит ничего нового, за исключением того, что создается объект ObservableFilter и в его конструктор в this передается ссылка на экземпляр ObservableMap (у которого уже есть ссылка на ObservableFromArray, как показано на схеме выше) созданный на предыдущем шаге.

public final Observable<T> filter(Predicate<? super T> predicate) {   ObjectHelper.requireNonNull(predicate, "predicate is null");   return RxJavaPlugins.onAssembly(new ObservableFilter<T>(this, predicate));}
Обновленная схема с созданным ObservableFilterОбновленная схема с созданным ObservableFilter

Начало подписки

Последний оператор в цепочке subscribe(), который вызывает перегруженную версию метода. В нашем случае обрабатывается только onNext(). Метод subscribe() вызывается у ObservableFilter, который был последним созданным Observable в цепочке.

public final Disposable subscribe(Consumer<? super T> onNext) {   return subscribe(onNext, Functions.ON_ERROR_MISSING, Functions.EMPTY_ACTION, Functions.emptyConsumer());}

В перегруженном методе сначала проверяются все переданные параметры на null, далее создается объект класса LambdaObserver и происходит подписка.

public final Disposable subscribe(Consumer<? super T> onNext, Consumer<? super Throwable> onError,       Action onComplete, Consumer<? super Disposable> onSubscribe) {   ObjectHelper.requireNonNull(onNext, "onNext is null");   ObjectHelper.requireNonNull(onError, "onError is null");   ObjectHelper.requireNonNull(onComplete, "onComplete is null");   ObjectHelper.requireNonNull(onSubscribe, "onSubscribe is null");   LambdaObserver<T> ls = new LambdaObserver<T>(onNext, onError, onComplete, onSubscribe);   subscribe(ls);   return ls;}

А вот и сам метод в котором и происходит подписка.

public final void subscribe(Observer<? super T> observer) {   ObjectHelper.requireNonNull(observer, "observer is null");   try {       observer = RxJavaPlugins.onSubscribe(this, observer);       ObjectHelper.requireNonNull(observer, "The RxJavaPlugins.onSubscribe hook returned a null Observer. Please change the handler provided to RxJavaPlugins.setOnObservableSubscribe for invalid null returns. Further reading: https://github.com/ReactiveX/RxJava/wiki/Plugins");       subscribeActual(observer);   } catch (NullPointerException e) {      ...... }}

В методе subscribeActual() производится подписка на источник данных и в него же передается созданный ранее LambdaObserver. subscribeActual() вызывается в классе ObservableFilter. И вот что там происходит.

public void subscribeActual(Observer<? super T> observer) {   source.subscribe(new FilterObserver<T>(observer, predicate));}

Создается новый объект класса FilterObserver, который принимает в конструктор LambdaObserver созданный ранее и предикат для фильтрации, которые хранится в ObservableFilter в виде поля класса.

Класс FilterObserver расширяет класс BasicFuseableObserver, в котором уже реализован метод onSubscribe(). BasicFuseableObserver это абстрактный класс, который реализуют промежуточные Observerы. Если посмотреть исходники, то его реализуют только 6 классов, два из которых это FilterObserver и MapObserver. В методе BasicFuseableObserver.onSubscribe() также вызывается метод onSubscribe() у нижележащего Observerа, который передавался в конструктор этого класса. А выглядит это вот так:

public final void onSubscribe(Disposable d) {   if (DisposableHelper.validate(this.upstream, d)) {       this.upstream = d;       if (d instanceof QueueDisposable) {           this.qd = (QueueDisposable<T>)d;       }       if (beforeDownstream()) {           downstream.onSubscribe(this);           afterDownstream();       }   }}

После того как подписались на ObservableFilter и создали объект FilterObserver, он передается в source.subscribe(). Хочу напомнить, что source это объект класса ObservableMap, переданный ранее в цепочке. У объекта ObservableMap вызывается метод subscribe().

public final void subscribe(Observer<? super T> observer) {   ObjectHelper.requireNonNull(observer, "observer is null");   try {       observer = RxJavaPlugins.onSubscribe(this, observer);       ObjectHelper.requireNonNull(observer, "The RxJavaPlugins.onSubscribe hook returned a null Observer. Please change the handler provided to RxJavaPlugins.setOnObservableSubscribe for invalid null returns. Further reading: https://github.com/ReactiveX/RxJava/wiki/Plugins");       subscribeActual(observer);   } catch (NullPointerException e) {      ...... }}

Далее происходит те же шаги, в методе subscribe() вызывается subscribeActual(), оба этих метода вызываются у ObservableMap. В subscribeActual() создается новый MapObserver с переданным в качестве параметра экземпляром FilterObserver и функцией mapperа.

public void subscribeActual(Observer<? super U> t) {   source.subscribe(new MapObserver<T, U>(t, function));}
public void subscribeActual(Observer<? super T> observer) {   FromArrayDisposable<T> d = new FromArrayDisposable<T>(observer, array);   observer.onSubscribe(d);   if (d.fusionMode) {       return;   }   d.run();}

Все рассмотренные Observerы расширяли абстрактный класс BasicFuseableObserver, в котором уже реализован метод onSubscribe() и так же есть ссылка на нижележащий Observer, у которого так же вызывается метод onSubscribe().

В конце метода subscribeActual() вызывается метод run(), в котором и начинается излучение всех данных в нижележащие Observerы.

void run() {   T[] a = array;   int n = a.length;   for (int i = 0; i < n && !isDisposed(); i++) {       T value = a[i];       if (value == null) {           downstream.onError(new NullPointerException("The element at index " + i + " is null"));           return;       }       downstream.onNext(value);   }   if (!isDisposed()) {       downstream.onComplete();   }}

Соответственно вызываются onNext() для передачи значений в нижележащие Observerы и потом onComplete() при завершении излучения данных, либо может произойти ошибка и вызовется onError(), который завершит всю цепочку.

Визуальное представление процесса создания и подписокВизуальное представление процесса создания и подписок

Вывод

Observableы вкладываются друг в друга и вызывают callbackи для создания Observerов, которые и будут обрабатывать получаемые данные и передавать их дальше по цепочки.

Метод onSubscribe() вызывается до начала отправки данных и это надо иметь ввиду если вы пользуетесь такими оператора как doOnSubscribe().

На каждый оператор создается как минимум 3 объекта:

  • Анонимный класс передаваемый в оператор

  • Observable создаваемый внутри оператора

  • Observer обрабатывающий получаемые данные

Потому при использовании операторов стоит иметь ввиду, что каждый оператор аллоцирует память для несколько объектов и не стоит добавлять операторы в цепочку, только потому что можно.

RxJava мощный инструмент, но нужно понимать как он работает и для каких задач его использовать. Если вам нужно просто выполнение сетевого запроса в фоновом потоке и последующее выполнение результата на главном потоке, то это как стрелять из пушки по воробьям, попасть можно, а вот последствия могут быть серьезными.

Подробнее..

Основы Flutter для начинающих (Часть II)

31.05.2021 18:12:13 | Автор: admin

Вступление

Добрый денек!

Мы продолжаем изучать Flutter.

И в этой статье мы познакомимся с файлом pubspec.yaml, а также поработаем с Flutter в командной строке.

Ну что ж, приступим!

Наш план
  • Часть 1 - введение в разработку, первое приложение, понятие состояния;

  • Часть 2 (текущая статья) - файл pubspec.yaml и использование flutter в командной строке;

  • Часть 3 - BottomNavigationBar и Navigator;

  • Часть 4 - MVC. Мы будем использовать именно этот паттерн, как один из самый простых;

  • Часть 5 - http пакет. Создание Repository класса, первые запросы, вывод списка постов;

  • Часть 6 - Работа с картинками, вывод картинок в виде сетки, получение картинок из сети, добавление своих в приложение;

  • Часть 7 - Создание своей темы, добавление кастомных шрифтов и анимации;

  • Часть 8 - Немного о тестировании;

Файл pubspec.yaml

Расширение .yaml указывает на то, что мы используем YAML формат данных (более подробнее в Википедии).

Это довольно простой формат, ориентированный на удобство представления данных.

pubspec.yaml находится в корневой директории проекта и служит для общей настройки, добавления зависимостей, шрифтов, картинок в ваш проект.

Немного об организации файлов:

  • .dart-tool содержит информацию для Dart Tools (набор различных утилит для работы с кодом Dart)

  • .idea была создана самой Android Studio и хранит настройки проекта

  • build содержит файлы сборки, в том числе и наш release apk

  • ios папка содержит нативный код iOS и предназначена для отдельной настройки iOS приложения, а также его публикации через XCode

  • android папка содержит нативный код Android и предназначена для отдельной настройки Android приложения

  • lib содержит непосредственно наш код на Dart

  • test предназначена для тестов

    Далее идет несколько файлов:

  • README.md и .gitignore - это файлы Git

  • о pubspec.yaml мы говорили выше, а pubspec.lock содержит информацию о версиях наших pub-пакетов.

  • .metadata содержит необходимую информацию для обновления Flutter

  • .packages дополнительная информация о пакетах

Рассмотрим минимальный pubspec.yaml:

# имя Flutter приложения# обычно данное имя используется в качестве# названия pub-пакета. Это важно лишь в том случае,# если вы разрабатываете свой pub-пакет и собираетесь# выложить его в общий доступ# как я уже отметил имя Android и iOS приложения впоследствии# можно будет изменить отдельно для каждой из платформname: json_placeholder_app# краткое описание на английскомdescription: json_placeholder_app is an demo application# в данном случае мы не собираемся# опубликовывать pub-пакет и поэтому# запрещаем команду flutter publishpublish_to: 'none' # версия Android и iOS приложения# состоит из 2 частей, разделенных знаком плюса# первая часть - это имя версии, которое будет# видно для пользователей, например 1.1.5# вторая часть позволяет Google Play и Apple Store# отличать разные версии нашего приложения (например: 5)version: 1.0.0+1# версия Dart SDKenvironment:  sdk: ">=2.7.0 <3.0.0"# блок зависимостейdependencies:  flutter:    sdk: flutter  # использование иконок для Cupertino компонентов  # Cupertino компоненты - это компоненты в стили iOS  # В данном приложении мы не будем использовать их и поэтому  # удалим ненужный pub-пакет  #cupertino_icons: ^1.0.2# зависимости для разработки# в данном случае подключено тестированиеdev_dependencies:  flutter_test:    sdk: flutter# в данной секции вы можете подключить шрифты и assets файлы# об этом мы поговорим позжеflutter:  # указываем, что мы используем MaterialApp иконки и наше  # приложение соответствует Material Design  uses-material-design: true

Немного о pub-пакетах

Все pub-пакеты расположены на pub.dev. Здесь вы можете найти довольно большое количество интересных и полезных пакетов и плагинов для ваших приложений.

Все pub-пакеты делятся на собственно пакеты и плагины.

В чем же отличие пакета от плагина?

Пакет - это код на Dart с pubspec.yaml файлом, а плагин - подвид пакета, который содержит нативный код какой-либо платформы.

Например плагин camera позволяет получить доступ к камере на Android и iOS устройствах и содержит нативный код отдельно для Android (папка android) и отдельно для iOS (папка ios)

Добавление зависимостей

Настало время подключить необходимые библиотеки в наш проект, которые мы впоследствии будем использовать на протяжении всей разработки нашего приложения.

Для этого нам нужно указать необходимые pub пакеты в блоке зависимостей:

# имя Flutter приложения# обычно данное имя используется в качестве# названия pub-пакета. Это важно лишь в том случае,# если вы разрабатываете свой pub-пакет и собираетесь# выложить его в общий доступ# как я уже отметил имя Android и iOS приложения впоследствии# можно будет изменить отдельно для каждой из платформname: json_placeholder_app# краткое описание на английскомdescription: json_placeholder_app is an demo application# в данном случае мы не собираемся# опубликовывать pub-пакет и поэтому# запрещаем команду flutter publishpublish_to: 'none' # версия Android и iOS приложения# состоит из 2 частей, разделенных плюсом# первая часть - это имя версии, которое будет# видно для пользователей, например 1.1.5# вторая часть позволяет Google Play и Apple Store# отличать разные версии нашего приложения (например: 5)version: 1.0.0+1# версия Dart SDKenvironment:  sdk: ">=2.7.0 <3.0.0"  # блок зависимостейdependencies:  flutter:    sdk: flutter      # подключение необходимых pub-пакетов    # используется для произвольного размещения  # компонентов в виде сетки  flutter_staggered_grid_view: ^0.4.0    # мы будем использовать MVC паттерн  mvc_pattern: ^7.0.0    # большая часть данных будет браться из сети,  # поэтому мы будем использовать http для  # осуществления наших запросов  http: ^0.13.3    # зависимости для разработки# в данном случае подключено тестированиеdev_dependencies:  flutter_test:    sdk: flutter# в данной секции вы можете подключить шрифты и assets файлы# об этом мы поговорим позжеflutter:  # указываем, что мы используем MaterialApp иконки и наше  # приложение соответствует Material Design  uses-material-design: true

Пока на этом все!

Flutter в командной строке

Для начала нужно запустить командную строку или терминал.

У меня Debian 10, поэтому я буду использовать терминал.

Теперь надо определить местоположения главного flutter скрипта.

Возможно во время установки Flutter вы установили переменные окружения и теперь вы можете использовать команды Flutter без указания пути:

В противном случае вам нужно прописать полный путь к Flutter:

В директории Flutter есть папка bin, в которой лежит главный скрипт - flutter.

Данный скрипт позволяет собирать и очищать ваш проект, загружать зависимости, управлять устройствами, на которых приложение будет выполняться, а также обновлять сам Flutter.

Ну что ж давайте пройдемся по основным командам.

Создание проекта

Для создания нового проекта нужно воспользоваться командой:

# также перед созданием проекта можно отключить web поддержку# с помощью команды: flutter config --no-enable-webflutter create new_flutter_app

Результат:

Установка зависимостей

Для установки зависимостей нужно выполнить:

flutter pub get

Вы также можете использовать встроенный терминал в Android Studio:

Получение доступных устройств

flutter devices

Результат:

Здесь мы видем мой Honor и Chrome браузер (т.к. включена web поддержка)

Запуск

Для запуска нужно указать устройство через параметр -d

flutter run -d JYXNW20805003141

Результат:

Получение скрина

Теперь когда мы запустили наше приложение, мы можем получить скрин с экрана нашего устройства c помощью команды:

# -d указываем устройство# -o путь в файлу, куда будет сохранен наш скринflutter screenshot -d JYXNW20805003141 -o ~/Downloads/screen_1.png

Результат:

Скрин:

Сборка релиза

Не будем вдаваться в глубокие подробности сборки приложения.

Данный этап мы рассмотрим в заключительных уроках.

Для создания release apk выполните:

flutter build apk --release

Результат:

В данном случае мы имеем неподписанный apk с набором всех архитектур (armeabi-v7a, arm64-v8a и 86_64).

Лучшим вариантом является использование опции --split-per-abi для разделения архитектур по разным файлам:

flutter build apk --split-per-abi

Результат:

Допольнительные команды

Определение версии Flutter:

flutter --version

Обновление Flutter:

flutter upgrade

Чтобы получить справку по какой-либо команде нужно использовать --help опцию:

flutter create --help

Результат:

Заключение

Как и предыдущая, статья получилось довольно информативной и без лишней воды.

Ну что ж поздравляю вас, если вы дочитали до конца, надеюсь вы узнали что-то полезное для себя.

Не забывайте оставлять пожелания в комментах)))

Далее переходим к навигации.

Подробнее..

Перевод Всё о PendingIntents

01.06.2021 18:16:36 | Автор: admin

PendingIntent являются важной частью фреймворка Android, но большинство доступных ресурсов для разработчиков сосредоточены на деталях их имплементации ссылка на токен, поддерживаемый системой а не на их использовании.

Поскольку Android 12 содержит важные нововведения в отношении отложенных намерений, включая определение того, является ли PendingIntent изменяемым или неизменяемым, я подумал, что будет полезно рассказать о том, что делают отложенные намерения, как система использует их, и почему вам иногда может понадобиться изменяемый PendingIntent.

Что такое PendingIntent?

Объект PendingIntent оборачивает функциональность объекта Intent, позволяя вашему приложению указать, что другое приложение должно сделать от вашего имени в ответ на будущее действие. Например, обернутое намерение может быть вызвано при срабатывании будильника или когда пользователь нажимает на уведомление.

Ключевым аспектом отложенных намерений является то, что другое приложение вызывает намерение от имени вашего приложения. То есть при вызове намерения другое приложение использует идентификатор вашего приложения.

Чтобы PendingIntent имел такое же поведение, как и обычный Intent, система вызывает PendingIntent с тем же идентификатором, с которым он был создан. В большинстве ситуаций, таких как тревога и уведомления, это идентификатор самого приложения.

Давайте рассмотрим различные способы, с помощью которых наши приложения могут работать с PendingIntent, и почему их нужно использовать именно таким образом.

Распространенный случай

Самый распространенный и наиболее простой способ использования PendingIntent - это действие, связанное с уведомлением:

val intent = Intent(applicationContext, MainActivity::class.java).apply {    action = NOTIFICATION_ACTION    data = deepLink}val pendingIntent = PendingIntent.getActivity(    applicationContext,    NOTIFICATION_REQUEST_CODE,    intent,    PendingIntent.FLAG_IMMUTABLE)val notification = NotificationCompat.Builder(        applicationContext,        NOTIFICATION_CHANNEL    ).apply {        // ...        setContentIntent(pendingIntent)        // ...    }.build()notificationManager.notify(    NOTIFICATION_TAG,    NOTIFICATION_ID,    notification)

Мы видим, что создаем стандартный тип Intent, который будет открывать наше приложение, а затем просто оборачиваем его в PendingIntent, прежде чем добавить его в наше уведомление.

В данном случае, поскольку у нас есть точное действие, которое необходимо выполнить, то создаем PendingIntent, не подлежащий изменению приложением, куда мы его передаем, используя флаг FLAG_IMMUTABLE.

После вызова NotificationManagerCompat.notify() все готово. Система отобразит уведомление и, когда пользователь нажмет на него, вызовет PendingIntent.send() для нашего PendingIntent, запуская наше приложение.

Обновление неизменяемого PendingIntent

Вы можете подумать, что если приложению нужно обновить PendingIntent, то он должен быть изменяемым, но это не всегда так! Приложение, создающее PendingIntent, всегда может обновить его, передав флаг FLAG_UPDATE_CURRENT:

val updatedIntent = Intent(applicationContext, MainActivity::class.java).apply {   action = NOTIFICATION_ACTION   data = differentDeepLink}// Because we're passing `FLAG_UPDATE_CURRENT`, this updates// the existing PendingIntent with the changes we made above.val updatedPendingIntent = PendingIntent.getActivity(   applicationContext,   NOTIFICATION_REQUEST_CODE,   updatedIntent,   PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT)// The PendingIntent has been updated.

Чуть позже поговорим о том, почему может возникнуть желание сделать PendingIntent изменяемым.

Технология Inter-app APIs

Распространенный случай полезен не только для взаимодействия с системой. Хотя для получения обратного вызова после выполнения действия чаще всего используются startActivityForResult() и onActivityResult(), это не единственный способ.

Представьте себе приложение для онлайн-заказа, которое предоставляет API для интеграции с ним приложений. Оно может принять PendingIntent как extra к своему собственному Intent, которое используется для запуска процесса заказа еды. Приложение заказа запускает PendingIntent только после того, как заказ будет доставлен.

В данном случае приложение заказа использует PendingIntent, а не отправляет результат действия, потому что доставка заказа может занять значительное время, и нет смысла заставлять пользователя ждать, пока это происходит.

Мы создадим неизменяемый PendingIntent, потому что не хотим, чтобы приложение онлайн-заказа меняло что-либо в нашем Intent. Необходимо, чтобы его отправили в том виде, в каком он есть, когда заказ будет доставлен.

Изменяемые PendingIntents

Что если мы будем разработчиками приложения для заказа и захотим добавить функцию, позволяющую пользователю набрать сообщение, отправляемое обратно в вызывающее приложение? Возможно, чтобы вызывающее приложение могло показать что-то вроде: "Сейчас время PIZZA!".

Ответом на этот вопрос является использование изменяемого PendingIntent.

Поскольку PendingIntent это, по сути, обертка вокруг Intent, можно подумать, что существует метод PendingIntent.getIntent(), который можно вызвать для получения и обновления обернутого Intent, но это не так. Так как же это работает?

Помимо метода send() в PendingIntent, который не принимает никаких параметров, есть несколько других версий, включая эту, которая принимает Intent:

fun PendingIntent.send(    context: Context!,     code: Int,     intent: Intent?)

Этот параметр intent не заменяет Intent, содержащийся в PendingIntent, а скорее используется для заполнения параметров из обернутого Intent, которые не были предоставлены при создании PendingIntent.

Давайте рассмотрим пример.

val orderDeliveredIntent = Intent(applicationContext, OrderDeliveredActivity::class.java).apply {   action = ACTION_ORDER_DELIVERED}val mutablePendingIntent = PendingIntent.getActivity(   applicationContext,   NOTIFICATION_REQUEST_CODE,   orderDeliveredIntent,   PendingIntent.FLAG_MUTABLE)

Этот PendingIntent может быть передан нашему приложению онлайн-заказа. После завершения доставки приложение заказа может принять сообщение customerMessage и отправить его обратно в качестве дополнительного намерения, как это сделано ниже:

val intentWithExtrasToFill = Intent().apply {   putExtra(EXTRA_CUSTOMER_MESSAGE, customerMessage)}mutablePendingIntent.send(   applicationContext,   PENDING_INTENT_CODE,   intentWithExtrasToFill)

Тогда вызывающее приложение увидит дополнительное EXTRA_CUSTOMER_MESSAGE в своем Intent и сможет отобразить сообщение.

Важные соображения при объявлении изменяемости отложенного намерения (pending intent)

  • При создании изменяемого PendingIntent ВСЕГДА явно задавайте компонент, который будет запущен в этом Intent. Это можно реализовать так, как мы сделали выше, явно задав точный класс, который будет его получать, либо с помощью вызова Intent.setComponent().

  • В вашем приложении может быть такой случай, когда проще вызвать Intent.setPackage(). Будьте очень осторожны с возможностью сопоставления нескольких компонентов, если вы сделаете это. Лучше указать конкретный компонент для получения Intent, если это вообще возможно.

  • Если вы попытаетесь переопределить значения в PendingIntent, который был создан с FLAG_IMMUTABLE, произойдет тихий сбой, и исходный обернутый Intent будет передан без изменений.

Помните, что приложение всегда может обновить свой собственный PendingIntent, даже если они неизменяемы. Единственная причина сделать PendingIntent изменяемым - если другое приложение будет иметь возможность каким-то образом обновить обернутый Intent.

Подробности о флагах

Мы немного рассказали о нескольких флагах, которые можно использовать при создании PendingIntent, но есть и другие, заслуживающие внимания.

FLAG_IMMUTABLE: Указывает, что Intent внутри PendingIntent не может быть изменен другими приложениями, которые передают Intent в PendingIntent.send(). Приложение всегда может использовать FLAG_UPDATE_CURRENT для изменения своих собственных PendingIntent.

До Android 12 PendingIntent, созданный без этого флага, по умолчанию был изменяемым.

В версиях Android до Android 6 (API 23) PendingIntents всегда изменяемы.

FLAG_MUTABLE: Указывает, что Intent внутри PendingIntent должен позволять приложению обновлять его содержимое путем объединения значений из параметра намерения PendingIntent.send().

Всегда заполняйте ComponentName обернутого Intent любого изменяемого PendingIntent. Невыполнение этого требования может привести к уязвимостям в системе безопасности!

Этот флаг был добавлен в Android 12. До Android 12 любые PendingIntents, созданные без флага FLAG_IMMUTABLE, были неявно изменяемыми.

FLAG_UPDATE_CURRENT: Запрашивает, чтобы система обновила существующий PendingIntent новыми дополнительными данными, а не создавала новый PendingIntent. Если PendingIntent не был зарегистрирован, то регистрируется этот.

FLAG_ONE_SHOT: Позволяет отправить PendingIntent только один раз (через PendingIntent.send()). Это может быть важно при передаче PendingIntent другому приложению, если содержащийся в нем Intent может быть отправлен только один раз. Такое требование обусловлено удобством или необходимостью предотвратить многократное выполнение приложением какого-либо действия.

Использование FLAG_ONE_SHOT предотвращает такие проблемы, как "атаки повторного воспроизведения (replay attacks)".

FLAG_CANCEL_CURRENT: Отменяет текущий PendingIntent, если он уже существует, перед регистрацией нового. Это может быть важно, если определенный PendingIntent был отправлен одному приложению, а вы хотите отправить его другому приложению, потенциально обновляя данные. Используя FLAG_CANCEL_CURRENT, первое приложение больше не сможет вызвать отправку, но второе приложение сможет.

Получение PendingIntents

Иногда система или другие фреймворки предоставляют PendingIntent как ответ на вызов API. Одним из примеров является метод MediaStore.createWriteRequest(), который был добавлен в Android 11.

static fun MediaStore.createWriteRequest(    resolver: ContentResolver,     uris: MutableCollection<Uri>): PendingIntent

Резюме

Мы говорили о том, что PendingIntent можно рассматривать как обертку вокруг Intent, которая позволяет системе или другому приложению запустить Intent, созданный одним приложением, в качестве этого приложения, в определенное время в будущем.

Мы также говорили о том, что PendingIntents обычно должен быть неизменяемым и что это не мешает приложению обновлять свои собственные объекты PendingIntent. Это можно сделать, используя флаг FLAG_UPDATE_CURRENT в дополнение к FLAG_IMMUTABLE.

Мы также говорили о мерах предосторожности, которые необходимо предпринять - заполнить ComponentName обернутого Intent - если PendingIntent должен быть изменяемым.

Наконец, мы говорили о том, что иногда система или фреймворки могут предоставлять PendingIntent нашему приложению, чтобы мы могли решить, как и когда их запускать.

Обновления в PendingIntent были лишь одной из функций в Android 12, направленной на повышение безопасности приложений. Обо всех изменениях в предварительной версии читайте здесь.

Хотите еще больше? Мы призываем вас протестировать свои приложения на новой предварительной версии ОС для разработчиков и поделиться с нами своими впечатлениями!


Перевод материала подготовлен в рамках курса "Android Developer. Basic". Если вам интересно узнать о курсе подробнее, приходите на день открытых дверей онлайн, где преподаватель расскажет о формате и программе обучения.

Подробнее..

Основы Flutter для начинающих (Часть III)

02.06.2021 16:12:10 | Автор: admin

Поздравляю, по крайней мере, всех живущих в Сибири с наступлением лета!)))

Сегодня довольно непростая тема - навигация.

Мы рассмотрим как устроена навигация в Flutter, что вообще нужно чтобы перейти с одного экраны на другой и конечно же не забудем о передачи аргументов между экранами.

И напоследок весьма распространенный use case: создание BottomNavigationBar.

'Ну что ж не будем терять ни минуты, начинаем!

Наш план
  • Часть 1- введение в разработку, первое приложение, понятие состояния;

  • Часть 2 - файл pubspec.yaml и использование flutter в командной строке;

  • Часть 3 (текущая статья) - BottomNavigationBar и Navigator;

  • Часть 4 - MVC. Мы будем использовать именно этот паттерн, как один из самый простых;

  • Часть 5 - http пакет. Создание Repository класса, первые запросы, вывод списка постов;

  • Часть 6 - Работа с картинками, вывод картинок в виде сетки, получение картинок из сети, добавление своих в приложение;

  • Часть 7 - Создание своей темы, добавление кастомных шрифтов и анимации;

  • Часть 8 - Немного о тестировании;

Navigator и стэк навигации

Flutter довольно прост в плане навигации, здесь нет фрагментов и Activity.

Все довольно просто: каждая страница это виджет, который называется Route.

Навигация осуществляется через объект Navigator:

// Navigator.of(context) получает состояние Navigator// виджета: NavigatorState, которое имеет push и pop методы// push помещает новую страницу на вершину стека Navigator// pop наоборот удаляет текущую страницу из вершины стэка// MaterialPageRoute в основном используется для создания// анимации между экранамиNavigator.of(context).push(MaterialPageRoute(builder: (context) => OurPage()));

Рассмотрим стэк Navigator'a на конкретном примере.

У нас есть два экрана: список книг и информация о книге.

Первый экран, который появится при запуске приложения - это список книг:

Затем мы переходим на страницу с информацией об одной из книг:

В этот момент наша новая страница находится на вершине стэка и поэтому мы не имеем доступа к списку книг.

Далее мы нажимаем кнопку Back или Up (стрелка в левом верхнем углу) и снова возвращаемся к первоначальному состоянию:

В первом случае нужно использовать push(route), во втором pop() метод.

Переходим непосредственно к практике!

Создание навигации между двумя экранами

Сделаем небольшой список персонажей из сериала My Little Pony с переходом на страницу описания каждого персонажа.

Для начала создадим новую страницу в папке pages:

Затем напишем немного кода:

import 'package:flutter/material.dart';// класс пони, который будет хранить имя и описание, а также idclass Pony {  final int id;  final String name;  final String desc;  Pony(this.id, this.name, this.desc);}// создаем список пони// final указывает на то, что мы больше// никогда не сможем присвоить имени ponies// другой список поняшекfinal List<Pony> ponies = [  Pony(      0,      "Twillight Sparkle",      "Twilight Sparkle is the central main character of My Little Pony Friendship is Magic. She is a female unicorn pony who transforms into an Alicorn and becomes a princess in Magical Mystery Cure"  ),  Pony(      1,      "Starlight Glimmer",      "Starlight Glimmer is a female unicorn pony and recurring character, initially an antagonist but later a protagonist, in the series. She first possibly appears in My Little Pony: Friends Forever Issue and first explicitly appears in the season five premiere."  ),  Pony(      2,      "Applejack",      "Applejack is a female Earth pony and one of the main characters of My Little Pony Friendship is Magic. She lives and works at Sweet Apple Acres with her grandmother Granny Smith, her older brother Big McIntosh, her younger sister Apple Bloom, and her dog Winona. She represents the element of honesty."  ),  Pony(      3,      "Pinkie Pie",      "Pinkie Pie, full name Pinkamena Diane Pie,[note 2] is a female Earth pony and one of the main characters of My Little Pony Friendship is Magic. She is an energetic and sociable baker at Sugarcube Corner, where she lives on the second floor with her toothless pet alligator Gummy, and she represents the element of laughter."  ),  Pony(      4,      "Fluttershy",      "Fluttershy is a female Pegasus pony and one of the main characters of My Little Pony Friendship is Magic. She lives in a small cottage near the Everfree Forest and takes care of animals, the most prominent of her charges being Angel the bunny. She represents the element of kindness."  ),];// PonyListPage не будет иметь состояния,// т.к. этот пример создан только для демонстрации// навигации в действииclass PonyListPage extends StatelessWidget {    // build как мы уже отметили, строит  // иерархию наших любимых виджетов  @override  Widget build(BuildContext context) {    return Scaffold(      appBar: AppBar(title: Text("Pony List Page")),      // зададим небольшие отступы для списка      body: Padding(        // объект EdgeInsets хранит четыре важные double переменные:        // left, top, right, bottom - отступ слева, сверху, справа и снизу        // EdgeInsets.all(10) - задает одинаковый отступ со всех сторон        // EdgeInsets.only(left: 10, right: 15) - задает отступ для        // определенной стороны или сторон        // EdgeInsets.symmetric - позволяет указать одинаковые        // отступы по горизонтали (left и right) и по вертикали (top и bottom)        padding: EdgeInsets.symmetric(vertical: 15, horizontal: 10),        // создаем наш список          child: ListView(            // map принимает другую функцию, которая            // будет выполняться над каждым элементом            // списка и возвращать новый элемент (виджет Material).            // Результатом map является новый список            // с новыми элементами, в данном случае            // это Material виджеты            children: ponies.map<Widget>((pony) {              // Material используется для того,              // чтобы указать цвет элементу списка              // и применить ripple эффект при нажатии на него              return Material(                color: Colors.pinkAccent,                // InkWell позволяет отслеживать                // различные события, например: нажатие                child: InkWell(                  // splashColor - цвет ripple эффекта                  splashColor: Colors.pink,                  // нажатие на элемент списка                  onTap: () {                    // добавим немного позже                  },                  // далее указываем в качестве                  // элемента Container с вложенным Text                  // Container позволяет указать внутренние (padding)                  // и внешние отступы (margin),                  // а также тень, закругление углов,                  // цвет и размеры вложенного виджета                  child: Container(                      padding: EdgeInsets.all(15),                      child: Text(                          pony.name,                          style: Theme.of(context).textTheme.headline4.copyWith(color: Colors.white)                      )                  ),                ),              );              // map возвращает Iterable объект, который необходимо              // преобразовать в список с помощью toList() функции            }).toList(),          )      ),    );  }}

Обратите внимание, что в Dart можно определять переменные вне классов, как в нашем случае со списком.

Теперь переходим к созданию PonyDetailPage:

import 'package:flutter/material.dart';import 'pony_list_page.dart';// также, как и PonyListPage наша страница// не будет иметь состоянияclass PonyDetailPage extends StatelessWidget {  // в качестве параметра мы будет получать id пони  final int ponyId;  // конструктор PonyDetailPage принимает ponyId,  // который будет присвоен нашему ранее  // объявленному полю  PonyDetailPage(this.ponyId);  @override  Widget build(BuildContext context) {    // получаем пони по его id    // обратите внимание: мы импортируем ponies     // из файла pony_list_page.dart    final pony = ponies[ponyId];    return Scaffold(      appBar: AppBar(        title: Text("Pony Detail Page"),      ),      body: Padding(        // указываем отступ для контента        padding: EdgeInsets.all(15),        // Column размещает дочерние виджеты в виде колонки        // crossAxisAlignment - выравнивание по ширине (колонка) или        // по высоте (строка)        // mainAxisAlignment работает наоборот        // в данном случае мы растягиваем дочерние элементы        // на всю ширину колонки        child: Column(          crossAxisAlignment: CrossAxisAlignment.stretch,          children: [            Container(                padding: EdgeInsets.all(10),                // вы не можете указать color для Container,                // т.к. свойство decoration было определено                // color: Colors.pinkAccent,                                // BoxDecoration имеет дополнительные свойства,                // посравнению с Container,                // такие как: gradient, borderRadius, border, shape                // и boxShadow                // здесь мы задаем радиус закругления левого и правого                // верхних углов                decoration: BoxDecoration(                  borderRadius: BorderRadius.only(                      topLeft: Radius.circular(15),                      topRight: Radius.circular(15)                  ),         // цвет Container'а мы указываем в BoxDecoration                  color: Colors.pinkAccent,                ),                child: Text(                    // указываем имя pony                    pony.name,                    style: Theme.of(context).textTheme.headline4.copyWith(color: Colors.white),                )            ),            Container(                padding: EdgeInsets.all(10),                child: Text(                    // указываем описание pony                    pony.desc,                    style: Theme.of(context).textTheme.bodyText1                )            )          ],        ),      )    );  }}

Осталось только организовать саму навигацию.

Добавьте следующий код в PonyListPage:

// нажатие на элемент спискаonTap: () {  // Здесь мы используем сокращенную форму:  // Navigator.of(context).push(route)  // PonyDetailPage принимает pony id,  // который мы и передалиNavigator.push(context, MaterialPageRoute(  builder: (context) => PonyDetailPage(pony.id)  ));},

Также не забудем заменить домашнюю страницу:

@override  Widget build(BuildContext context) {    // виджет MaterialApp - главный виджет приложения, который    // позволяет настроить тему и использовать    // Material Design для разработки.    return MaterialApp(      // заголовок приложения      // обычно виден, когда мы сворачиваем приложение      title: 'Json Placeholder App',      // убираем баннер      debugShowCheckedModeBanner: false,      // настройка темы, мы ещё вернёмся к этому      theme: ThemeData(        primarySwatch: Colors.blue,      ),      // теперь у нас домашная страница - PonyListPage      home: PonyListPage(),    );  }

Запуск

Теперь кликаем на любой элемент:

Та дам! Мы также можем вернуться обратно, если нажмем кнопку Back или стрелку в левом верхнем углу.

Также можно реализовать свою логику обратного перехода:

// Получаем NavigatorState и уничтожает последний элемент // из стэка навигации (PonyDetailPage)// мы можем передать второй аргумент, если хотим вернуть результатNavigator.pop(context, result)

Пока на этом все. О навигации можно написать целый цикл статей.

Ещё пара слов о нововведениях: появился новый Navigator API 2.0, о котором есть довольно хорошая статья.

Мы останавливаться не будем и переходим к BottomNavigationBar.

BottomNavigationBar и свои Navigator'ы

Я 100% уверен, что вы встречали нижнее меню, по которому можно переходить на различные экраны:

Здесь вы можете увидеть четыре элемента меню (домашная страница, поиск, уведомления и сообщения).

Давайте реализуем что-нибудь похожее.

Сначала создадим новую папку models, а в ней файл tab.dart:

Затем создадим класс Tab и перечисление TabItem:

import 'package:flutter/material.dart';// будет хранить основную информацию // об элементах менюclass MyTab {  final String name;  final MaterialColor color;  final IconData icon;  const MyTab({this.name, this.color, this.icon});}// пригодиться для определения // выбранного элемента меню// у нас будет три пункта меню и три страницы:// посты, альбомы и заданияenum TabItem { POSTS, ALBUMS, TODOS }

Переходим к более сложной части, реализации главной страницы:

import 'package:flutter/material.dart';import "../models/tab.dart";// Наша главная страница будет содержать состояниеclass HomePage extends StatefulWidget {  @override  _HomePageState createState() => _HomePageState();}class _HomePageState extends State<HomePage> {  // GlobalKey будет хранить уникальный ключ,  // по которому мы сможем получить доступ  // к виджетам, которые уже находяться в иерархии  // NavigatorState - состояние Navigator виджета  final _navigatorKeys = {    TabItem.POSTS: GlobalKey<NavigatorState>(),    TabItem.ALBUMS: GlobalKey<NavigatorState>(),    TabItem.TODOS: GlobalKey<NavigatorState>(),  };  // текущий выбранный элемент  var _currentTab = TabItem.POSTS;  // выбор элемента меню  void _selectTab(TabItem tabItem) {    setState(() => _currentTab = tabItem);  }  @override  Widget build(BuildContext context) {    // WillPopScope переопределяет поведения    // нажатия кнопки Back    return WillPopScope(      // логика обработки кнопки back может быть разной      // здесь реализована следующая логика:      // когда мы находимся на первом пункте меню (посты)      // и нажимаем кнопку Back, то сразу выходим из приложения      // в противном случае выбранный элемент меню переключается      // на предыдущий: c заданий на альбомы, с альбомов на посты,      // и после этого только выходим из приложения      onWillPop: () async {          if (_currentTab != TabItem.POSTS) {            if (_currentTab == TabItem.TODOS) {              _selectTab(TabItem.ALBUMS);            } else {              _selectTab(TabItem.POSTS);            }            return false;          } else {            return true;          }      },      child: Scaffold(        // Stack размещает один элемент над другим        // Проще говоря, каждый экран будет находится        // поверх другого, мы будем только переключаться между ними        body: Stack(children: <Widget>[          _buildOffstageNavigator(TabItem.POSTS),          _buildOffstageNavigator(TabItem.ALBUMS),          _buildOffstageNavigator(TabItem.TODOS),        ]),        // MyBottomNavigation мы создадим позже        bottomNavigationBar: MyBottomNavigation(          currentTab: _currentTab,          onSelectTab: _selectTab,        ),      ),);  }  // Создание одного из экранов - посты, альбомы или задания  Widget _buildOffstageNavigator(TabItem tabItem) {    return Offstage(      // Offstage работает следующим образом:      // если это не текущий выбранный элемент      // в нижнем меню, то мы его скрываем      offstage: _currentTab != tabItem,      // TabNavigator мы создадим позже      child: TabNavigator(        navigatorKey: _navigatorKeys[tabItem],        tabItem: tabItem,      ),    );  }}

Данной подход состоит в том, что мы создаем отдельный Navigator для постов, альбомов и заданий, у каждого из них будет свой стэк навигации.

Далее с помощью виджета Offstage мы показывает только тот экран, который был выбран.

Также мы переопределили нажатие на кнопку back - WillPopScope.

Теперь создадим нижнее меню в новом файле bottom_navigation.dart:

import 'package:flutter/material.dart';import '../models/tab.dart';// создаем три пункта меню// const обозначает, что tabs является // постоянной ссылкой и мы больше// ничего не сможем ей присвоить,// иначе говоря, она определена во время компиляцииconst Map<TabItem, MyTab> tabs = {  TabItem.POSTS : const MyTab(name: "Posts", color: Colors.red, icon: Icons.layers),  TabItem.ALBUMS : const MyTab(name: "Albums", color: Colors.blue, icon: Icons.image),  TabItem.TODOS : const MyTab(name: "Todos", color: Colors.green, icon: Icons.edit)};class MyBottomNavigation extends StatelessWidget {  // MyBottomNavigation принимает функцию onSelectTab  // и текущую выбранную вкладку  MyBottomNavigation({this.currentTab, this.onSelectTab});  final TabItem currentTab;  // ValueChanged<TabItem> - функциональный тип,  // то есть onSelectTab является ссылкой на функцию,  // которая принимает TabItem объект  final ValueChanged<TabItem> onSelectTab;  @override  Widget build(BuildContext context) {    // Используем встроенный виджет BottomNavigationBar для    // реализации нижнего меню    return BottomNavigationBar(        selectedItemColor: _colorTabMatching(currentTab),        selectedFontSize: 13,        unselectedItemColor: Colors.grey,        type: BottomNavigationBarType.fixed,        currentIndex: currentTab.index,        // пункты меню        items: [          _buildItem(TabItem.POSTS),          _buildItem(TabItem.ALBUMS),          _buildItem(TabItem.TODOS),        ],        // обработка нажатия на пункт меню        // здесь мы делаем вызов функции onSelectTab,        // которую мы получили через конструктор        onTap: (index) => onSelectTab(            TabItem.values[index]        )    );  }  // построение пункта меню  BottomNavigationBarItem _buildItem(TabItem item) {    return BottomNavigationBarItem(        // указываем иконку        icon: Icon(          _iconTabMatching(item),          color: _colorTabMatching(item),        ),        // указываем метку или название        label: tabs[item].name,    );  }  // получаем иконку элемента  IconData _iconTabMatching(TabItem item) => tabs[item].icon;  // получаем цвет элемента  Color _colorTabMatching(TabItem item) {    return currentTab == item ? tabs[item].color : Colors.grey;  }}

И реализуем TabNavigator (tab_navigator.dart):

import 'package:flutter/material.dart';import '../models/tab.dart';import 'pony_list_page.dart';class TabNavigator extends StatelessWidget {  // TabNavigator принимает:  // navigatorKey - уникальный ключ для NavigatorState  // tabItem - текущий пункт меню  TabNavigator({this.navigatorKey, this.tabItem});  final GlobalKey<NavigatorState> navigatorKey;  final TabItem tabItem;  @override  Widget build(BuildContext context) {    // наконец-то мы дошли до этого момента    // здесь мы присваиваем navigatorKey     // только, что созданному Navigator'у    // navigatorKey, как уже было отмечено является ключом,    // по которому мы получаем доступ к состоянию    // Navigator'a, вот и все!    return Navigator(      key: navigatorKey,      // Navigator имеет параметр initialRoute,      // который указывает начальную страницу и является      // всего лишь строкой.      // Мы не будем вдаваться в подробности, но отметим,      // что по умолчанию initialRoute равен /      // initialRoute: "/",            // Navigator может сам построить наши страницы или      // мы можем переопределить метод onGenerateRoute      onGenerateRoute: (routeSettings) {        // сначала определяем текущую страницу        Widget currentPage;        if (tabItem == TabItem.POSTS) {          // пока мы будем использовать PonyListPage          currentPage = PonyListPage();        } else if (tabItem == TabItem.POSTS) {          currentPage = PonyListPage();        } else {          currentPage = PonyListPage();        }        // строим Route (страница или экран)        return MaterialPageRoute(builder: (context) => currentPage,);      },    );  }}

Также не забудьте заменить домашнюю страницу в main.dart файле:

return MaterialApp(   //...   // Наша главная страница с нижнем меню   home: HomePage(),);

Осталось только импортировать нужные классы в home_page.dart и вуаля:

Также хорошей практикой является правильная организация кода, поэтому в папке pages создадим новую папку home и перетащим туда два наших файлика:

И напоследок сделаем три страницы заглушки: PostListPage, AlbumListPage и TodoListPage:

import 'package:flutter/cupertino.dart';import 'package:flutter/material.dart';// Здесь все довольно очевидноclass PostListPage extends StatefulWidget {  @override  _PostListPageState createState() => _PostListPageState();}class _PostListPageState extends State<PostListPage> {    @override  Widget build(BuildContext context) {    return Scaffold(      appBar: AppBar(        title: Text("Post List Page"),      ),      body: Container()    );  }}

Та же структура и для двух остальных.

После этого укажим их в TabNavigator'e:

onGenerateRoute: (routeSettings) {  // сначала определяем текущую страницу  Widget currentPage;  if (tabItem == TabItem.POSTS) {  // указываем соответствующие страницы    currentPage = PostListPage();  } else if (tabItem == TabItem.ALBUMS) {    currentPage = AlbumListPage();  } else {    currentPage = TodoListPage();  }   // строим Route (страница или экран)   return MaterialPageRoute(builder: (context) => currentPage);},

Заключение

Поздравляю вас!

Искренне рад и благодарен вам за хорошие отзывы и за поддержку!

Полезные ссылки:

До скорой встречи!

Подробнее..

Основы Flutter для начинающих (Часть V)

04.06.2021 10:04:57 | Автор: admin

Наконец-то мы добрались до одной из самых важных тем, без которой идти дальше нет смысла.

План довольно простой: нам предстоит познакомиться с клиент-серверной архитектурой и реализовать получение списка постов.

В конце мы правильно организуем файлы наших страниц и вынесем элемент списка в отдельный файл.

Полетели!

Наш план
  • Часть 1- введение в разработку, первое приложение, понятие состояния;

  • Часть 2- файл pubspec.yaml и использование flutter в командной строке;

  • Часть 3- BottomNavigationBar и Navigator;

  • Часть 4 - MVC. Мы будем использовать именно этот паттерн, как один из самых простых;

  • Часть 5 (текущая статья) - http пакет. Создание Repository класса, первые запросы, вывод списка постов;

  • Часть 6 - работа с формами, текстовые поля и создание поста.

  • Часть 7 - работа с картинками, вывод картинок в виде сетки, получение картинок из сети, добавление своих в приложение;

  • Часть 8 - создание своей темы, добавление кастомных шрифтов и анимации;

  • Часть 9 - немного о тестировании;

Client и Server

Модель Client / Server лежит в основе всего Интернета и является наиболее распространенной.

В чем её суть?

Сначала разберемся что такое клиент и сервер:

  • Клиент - пользовательское устройство, которое отправляет запросы за сервер и получает ответы. Это может быть смартфон, компьютер или MacBook.

  • Сервер - специальный компьютер, который содержит данные, необходимые для пользователя.

Вся модель сводиться к примитивному принципу: клиент отправил запрос, сервер принял его, обработал и передал ответ клиенту.

Для организации взаимодействия сервера и клиента используются специальные протоколы. На текущий момент одним из самых распространенных протоколов в сети Интернет является http / https (s означает защищенный, secure).

http / https позволяет передавать почти все известные форматы данных: картинки, видео, текст.

Мы будем работать с JSON форматом.

JSON - простой и понятный формат данных, а главное легковесный, т.к. передается только текст.

Пример JSON:

[  {    "userId": 1,    "id": 1,    "title": "sunt aut facere repellat provident occaecati excepturi optio reprehenderit",    "body": "quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto"  },  {    "userId": 1,    "id": 2,    "title": "qui est esse",    "body": "est rerum tempore vitae\nsequi sint nihil reprehenderit dolor beatae ea dolores neque\nfugiat blanditiis voluptate porro vel nihil molestiae ut reiciendis\nqui aperiam non debitis possimus qui neque nisi nulla"  },  ...]  

Здесь массив постов, который мы будем получать от сервера.

Обратите внимание: квадратные скобки указывает на массив данных, а фигурные на отдельный объект.

JSON позволяет создавать глубокую вложенность объектов и массивов:

{  "total_items" : 1  "result" : [  {  "id" : 1,  "name" : "Twillight Sparkle",  "pony_type" : "alicorn",  "friends" : [  "Starlight Glimmer", "Applejack", "Rarity", "Spike"  ]}  ]}

Понятие запроса

Для обмена данными клиент должен отправлять запросы на сервер.

Т.к. интернет в большинстве случаев использует http / https то запросы называются HTTP запросами.

Структура HTTP запроса:

  • URL - уникальный адрес в Интернете, который идентифицирует сервер и его конкретный ресурс, данные которого мы собираемся получить. В нашем случае URL выглядит следующим образом: https://jsonplaceholder.typicode.com/posts. (об структуре самого URL'а можно почитать в Википедии)

  • Метод, который определяет типа запроса. GET используется только для получения данных, POST позволяет клиенту добавить свои данные на сервер, DELETE - удалить их, PUT - изменить.

  • Данные запроса обычно называются телом запроса и используются совместно с POST, PUT и DELETE методами. Для GET метода в основном используются параметры самого URL'а. Выглядит это следующим образом: https://jsonplaceholder.typicode.com/posts/1 (здесь мы обращаемся к конкретному посту по его id = 1)

Запрос и вывод списка постов

Мы будем использовать довольно мощный и простой пакет http для отправки запросов на сервер.

Сначала убедимся, что мы указали его в pubspec.yaml файле:

# блок зависимостейdependencies:  flutter:    sdk: flutter  # подключение необходимых pub-пакетов  # используется для произвольного размещения  # компонентов в виде сетки  flutter_staggered_grid_view: ^0.4.0  # мы будем использовать MVC паттерн  mvc_pattern: ^7.0.0  # http предоставляет удобный интерфейс для создания# запросов и обработки ошибок  http: ^0.13.3

Переходим к созданию классов модели.

Для этого создайте файл post.dart в папке models:

// сначала создаем объект самого постаclass Post {  // все поля являются private  // это сделано для инкапсуляции данных  final int _userId;  final int _id;  final String _title;  final String _body;    // создаем getters для наших полей  // дабы только мы могли читать их  int get userId => _userId;  int get id => _id;  String get title => _title;  String get body => _body;  // Dart позволяет создавать конструкторы с разными именами  // В данном случае Post.fromJson(json) - это конструктор  // здесь мы принимаем JSON объект поста и извлекаем его поля  // обратите внимание, что dynamic переменная   // может иметь разные типы: String, int, double и т.д.  Post.fromJson(Map<String, dynamic> json) :    this._userId = json["userId"],    this._id = json["id"],    this._title = json["title"],    this._body = json["body"];}// PostList являются оберткой для массива постовclass PostList {  final List<Post> posts = [];  PostList.fromJson(List<dynamic> jsonItems) {    for (var jsonItem in jsonItems) {      posts.add(Post.fromJson(jsonItem));    }  }}// наше представление будет получать объекты// этого класса и определять конкретный его// подтипabstract class PostResult {}// указывает на успешный запросclass PostResultSuccess extends PostResult {  final PostList postList;  PostResultSuccess(this.postList);}// произошла ошибкаclass PostResultFailure extends PostResult {  final String error;  PostResultFailure(this.error);}// загрузка данныхclass PostResultLoading extends PostResult {  PostResultLoading();}

Одной из наиболее неприятных проблем является несоответствие типов.

Если взглянуть на JSON объект поста:

{  "userId": 1,  "id": 1,  "title": "sunt aut facere repellat provident occaecati excepturi optio reprehenderit",  "body": "quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto"}

То можно заметить, что userId и id являются целыми числами, а title и body строками, поэтому в конструкторе Post.fromJson(json) мы не замарачиваемся с привидением типов.

Пришло время создать Repository класс.

Для этого создадим новую папку data и в нем файл repository.dart:

import 'dart:convert';// импортируем http пакетimport 'package:http/http.dart' as http;import 'package:json_placeholder_app/models/post.dart';// мы ещё не раз будем использовать // константу SERVERconst String SERVER = "https://jsonplaceholder.typicode.com";class Repository {  // обработку ошибок мы сделаем в контроллере  // мы возвращаем Future объект, потому что  // fetchPhotos асинхронная функция  // асинхронные функции не блокируют UI  Future<PostList> fetchPosts() async {    // сначала создаем URL, по которому    // мы будем делать запрос    final url = Uri.parse("$SERVER/posts");    // делаем GET запрос    final response = await http.get(url);// проверяем статус ответаif (response.statusCode == 200) {  // если все ок то возвращаем посты  // json.decode парсит ответ   return PostList.fromJson(json.decode(response.body));} else {  // в противном случае говорим об ошибке  throw Exception("failed request");}  }}

Вы скажите: мы могли все запихнуть в контроллер, зачем создавать ещё один класс?

Т.к. контроллеров может быть огромное количество и каждый из них будет обращаться к одному и тому же серверу, нам придеться дублировать логику.

К тому же это не очень гибко. Вдруг нам нужно будет поменять URL адрес сервера.

Реализуем PostController:

import '../data/repository.dart';import '../models/post.dart';import 'package:mvc_pattern/mvc_pattern.dart';class PostController extends ControllerMVC {  // создаем наш репозиторий  final Repository repo = new Repository();  // конструктор нашего контроллера  PostController();    // первоначальное состояние - загрузка данных  PostResult currentState = PostResultLoading();  void init() async {    try {      // получаем данные из репозитория      final postList = await repo.fetchPosts();      // если все ок то обновляем состояние на успешное      setState(() => currentState = PostResultSuccess(postList));    } catch (error) {      // в противном случае произошла ошибка      setState(() => currentState = PostResultFailure("Нет интернета"));    }  }}

Заключительная часть: подключим наш контроллер к представлению и выведем посты:

import 'package:flutter/material.dart';import '../controllers/post_controller.dart';import '../models/post.dart';import 'package:mvc_pattern/mvc_pattern.dart';class PostListPage extends StatefulWidget {  @override  _PostListPageState createState() => _PostListPageState();}// не забываем расширяться от StateMVCclass _PostListPageState extends StateMVC {  // ссылка на наш контроллер  PostController _controller;  // передаем наш контроллер StateMVC конструктору и  // получаем на него ссылку  _PostListPageState() : super(PostController()) {    _controller = controller as PostController;  }  // после инициализации состояния  // мы запрашивает данные у сервера  @override  void initState() {    super.initState();    _controller.init();  }  @override  Widget build(BuildContext context) {    return Scaffold(      appBar: AppBar(        title: Text("Post List Page"),      ),      body: _buildContent()    );  }  Widget _buildContent() {    // первым делом получаем текущее состояние    final state = _controller.currentState;    if (state is PostResultLoading) {      // загрузка      return Center(        child: CircularProgressIndicator(),      );    } else if (state is PostResultFailure) {      // ошибка      return Center(        child: Text(          state.error,          textAlign: TextAlign.center,          style: Theme.of(context).textTheme.headline4.copyWith(color: Colors.red)        ),      );    } else {      // отображаем список постов      final posts = (state as PostResultSuccess).postList.posts;      return Padding(        padding: EdgeInsets.all(10),        // ListView.builder создает элемент списка        // только когда он видим на экране        child: ListView.builder(          itemCount: posts.length,          itemBuilder: (context, index) {            return _buildPostItem(posts[index]);          },        ),      );    }  }  // элемент списка   Widget _buildPostItem(Post post) {    return Container(        decoration: BoxDecoration(            borderRadius: BorderRadius.all(Radius.circular(15)),            border: Border.all(color: Colors.grey.withOpacity(0.5), width: 0.3)        ),        margin: EdgeInsets.only(bottom: 10),        child: Column(          crossAxisAlignment: CrossAxisAlignment.stretch,          children: [            Container(              decoration: BoxDecoration(                borderRadius: BorderRadius.only(topLeft: Radius.circular(15), topRight: Radius.circular(15)),                color: Theme.of(context).primaryColor,              ),              padding: EdgeInsets.all(10),              child: Text(                post.title,                textAlign: TextAlign.left,                style: Theme.of(context).textTheme.headline5.copyWith(color: Colors.white),),            ),            Container(              child: Text(                post.body,                style: Theme.of(context).textTheme.bodyText2,              ),              padding: EdgeInsets.all(10),            ),          ],        )    );  }}

Не пугайтесь если слишком много кода.

Все сразу освоить невозможно, поэтому не спешите)

Запуск

Попробуем запустить:

Вуаля! Теперь отключим интернет:

Все работает!

Небольшая заметка

Одним из важных принципов программирования является стремление к минимизации кода и его упрощению.

Файл post_list_page.dart содержит всего 110 строк кода, это не проблема. Но если бы он был в 10 или даже в 20 раз больше!

Какой ужас был бы на глазах у того, кто взглянул бы на него.

Лучшей практикой считается выносить повторяющие фрагменты кода в отдельные файлы.

Давайте попробуем вынести функцию Widget _buildItem(post) в другой файл.

Для этого создадим для каждой группы страниц свою папку:

Затем в папке post создадим новый файл post_list_item.dart:

import 'package:flutter/material.dart';import '../../models/post.dart';// элемент спискаclass PostListItem extends StatelessWidget {    final Post post;    // элемент списка отображает один пост  PostListItem(this.post);    Widget build(BuildContext context) {    return Container(        decoration: BoxDecoration(            borderRadius: BorderRadius.all(Radius.circular(15)),            border: Border.all(color: Colors.grey.withOpacity(0.5), width: 0.3)        ),        margin: EdgeInsets.only(bottom: 10),        child: Column(          crossAxisAlignment: CrossAxisAlignment.stretch,          children: [            Container(              decoration: BoxDecoration(                borderRadius: BorderRadius.only(topLeft: Radius.circular(15), topRight: Radius.circular(15)),                color: Theme.of(context).primaryColor,              ),              padding: EdgeInsets.all(10),              child: Text(                post.title,                textAlign: TextAlign.left,                style: Theme.of(context).textTheme.headline5.copyWith(color: Colors.white),),            ),            Container(              child: Text(                post.body,                style: Theme.of(context).textTheme.bodyText2,              ),              padding: EdgeInsets.all(10),            ),          ],        )    );  }}

Не забудьте удалить ненужный код из post_list_page.dart:

import 'package:flutter/material.dart';import '../../controllers/post_controller.dart';import '../../models/post.dart';import 'post_list_item.dart';import 'package:mvc_pattern/mvc_pattern.dart';class PostListPage extends StatefulWidget {  @override  _PostListPageState createState() => _PostListPageState();}// не забываем расширяться от StateMVCclass _PostListPageState extends StateMVC {  // ссылка на наш контроллер  PostController _controller;  // передаем наш контроллер StateMVC конструктору и  // получаем на него ссылку  _PostListPageState() : super(PostController()) {    _controller = controller as PostController;  }  // после инициализации состояние  // мы запрашивает данные у сервера  @override  void initState() {    super.initState();    _controller.init();  }  @override  Widget build(BuildContext context) {    return Scaffold(      appBar: AppBar(        title: Text("Post List Page"),      ),      body: _buildContent()    );  }  Widget _buildContent() {    // первым делом получаем текущее состояние    final state = _controller.currentState;    if (state is PostResultLoading) {      // загрузка      return Center(        child: CircularProgressIndicator(),      );    } else if (state is PostResultFailure) {      // ошибка      return Center(        child: Text(          state.error,          textAlign: TextAlign.center,          style: Theme.of(context).textTheme.headline4.copyWith(color: Colors.red)        ),      );    } else {      // отображаем список постов      final posts = (state as PostResultSuccess).postList.posts;      return Padding(        padding: EdgeInsets.all(10),        // ListView.builder создает элемент списка        // только когда он видим на экране        child: ListView.builder(          itemCount: posts.length,          itemBuilder: (context, index) {            // мы вынесли элемент списка в            // отдельный виджет            return PostListItem(posts[index]);          },        ),      );    }  }  }

Заключение

В последующих частях мы ещё не раз будем сталкиваться с созданием сетевых запросов.

Я постарался кратко рассказать и показать на наглядном примере работу с сетью.

Надеюсь моя статья принесла вам пользу)

Ссылка на Github

Всем хорошего кода!

Подробнее..

Основы Flutter для начинающих (Часть VI)

05.06.2021 14:06:41 | Автор: admin

Когда вы создаете различные формы (например: регистрации или входа) на Flutter, вы не заморачиваетесь с кастомизацией компонентов, потому что вы можете изменить любое поле формы под свой стиль.

Помимо кастомизации, Flutter предоставляет возможность обработки ошибок и валидации полей формы.

И сегодня мы постараемся разобраться с этой темой на небольшом примере.

Ну что ж, погнали!

Наш план
  • Часть 1- введение в разработку, первое приложение, понятие состояния;

  • Часть 2- файл pubspec.yaml и использование flutter в командной строке;

  • Часть 3- BottomNavigationBar и Navigator;

  • Часть 4- MVC. Мы будем использовать именно этот паттерн, как один из самых простых;

  • Часть 5 - http пакет. Создание Repository класса, первые запросы, вывод списка постов;

  • Часть 6 (текущая статья) - работа с формами, текстовые поля и создание поста.

  • Часть 7 - работа с картинками, вывод картинок в виде сетки, получение картинок из сети, добавление своих в приложение;

  • Часть 8 - создание своей темы, добавление кастомных шрифтов и анимации;

  • Часть 9 - немного о тестировании;

Создание формы: добавление поста

Для начала добавим на нашу страницу HomePage кнопку по которой мы будем добавлять новый пост:

@overrideWidget build(BuildContext context) {  return Scaffold(    appBar: AppBar(      title: Text("Post List Page"),    ),    body: _buildContent(),    // в первой части мы уже рассматривали FloatingActionButton    floatingActionButton: FloatingActionButton(      child: Icon(Icons.add),      onPressed: () {      },    ),  );}

Далее создадим новую страницу в файле post_add_page.dart:

import 'package:flutter/material.dart';class PostDetailPage extends StatefulWidget {  @override  _PostDetailPageState createState() => _PostDetailPageState();}class _PostDetailPageState extends State<PostDetailPage> {    // TextEditingController'ы позволят нам получить текст из полей формы  final TextEditingController titleController = TextEditingController();  final TextEditingController contentController = TextEditingController();    // _formKey пригодится нам для валидации  final _formKey = GlobalKey<FormState>();    @override  Widget build(BuildContext context) {    return Scaffold(      appBar: AppBar(        title: Text("Post Add Page"),        actions: [          // пункт меню в AppBar          IconButton(            icon: Icon(Icons.check),            onPressed: () {              // сначала запускаем валидацию формы              if (_formKey.currentState!.validate()) {                // здесь мы будем делать запроc на сервер              }            },          )        ],      ),      body: Padding(        padding: EdgeInsets.all(15),        child: _buildContent(),      ),    );  }  Widget _buildContent() {    // построение формы    return Form(      key: _formKey,      // у нас будет два поля      child: Column(        children: [          // поля для ввода заголовка          TextFormField(            // указываем для поля границу,            // иконку и подсказку (hint)            decoration: InputDecoration(                border: OutlineInputBorder(),                prefixIcon: Icon(Icons.face),                hintText: "Заголовок"            ),            // не забываем указать TextEditingController            controller: titleController,            // параметр validator - функция которая,            // должна возвращать null при успешной проверки            // или строку при неудачной            validator: (value) {              // здесь мы для наглядности добавили 2 проверки              if (value == null || value.isEmpty) {                return "Заголовок пустой";              }              if (value.length < 3) {                return "Заголовок должен быть не короче 3 символов";              }              return null;            },          ),          // небольшой отступ между полями          SizedBox(height: 10),          // Expanded означает, что мы должны          // расширить наше поле на все доступное пространство          Expanded(            child: TextFormField(              // maxLines: null и expands: true               // указаны для расширения поля на все доступное пространство              maxLines: null,              expands: true,              textAlignVertical: TextAlignVertical.top,              decoration: InputDecoration(                  border: OutlineInputBorder(),                  hintText: "Содержание",              ),              // не забываем указать TextEditingController              controller: contentController,              // также добавляем проверку поля              validator: (value) {                if (value == null || value.isEmpty) {                  return "Содержание пустое";                }                return null;              },            ),          )        ],      ),    );  }}

Не забудьте добавить переход на страницу формы:

floatingActionButton: FloatingActionButton(   child: Icon(Icons.add),   onPressed: () {      Navigator.push(context, MaterialPageRoute(         builder: (context) => PostDetailPage()      ));   },),

Запускаем и нажимаем на кнопку:

Вуаля! Форма работает.

Небольшая заметка

У новичков могут возникнуть проблемы даже с готовым кодом. И это не издевательство, такое бывает.

Поэтому для 100%-ной работы коды постарайтесь использовать схожие версии Flutter и Dart с моими:

  • Flutter 2.0.6

  • Dart SDK version: 2.12.3

Также в комментах я обратил внимание на null safety. Это очень важно, я позабыл об этом и это мой косяк.

Я уже добавил в приложение поддержку null safety. Вы наверно обратили внимание на восклицательный знак:

// ! указывает на то, что мы 100% уверены// что currentState не содержит null значение_formKey.currentState!.validate()

О null safety и о её поддержи в Dart можно сделать целый цикл статей, а возможно и написать целую книгу.

Мы задерживаться не будем и переходим к созданию POST запроса.

POST запрос для добавления данных на сервер

POST, как уже было отмечено, является одним из HTTP методов и служит для добавления новых данных на сервер.

Для начала добавим модель для нашего результата и изменим немного класс Post:

class Post {  // все поля являются private  // это сделано для инкапсуляции данных  final int? _userId;  final int? _id;  final String? _title;  final String? _body;  // создаем getters для наших полей  // дабы только мы могли читать их  int? get userId => _userId;  int? get id => _id;  String? get title => _title;  String? get body => _body;  // добавим новый конструктор для поста  Post(this._userId, this._id, this._title, this._body);  // toJson() превращает Post в строку JSON  String toJson() {    return json.encode({      "title": _title,      "content": _body    });  }  // Dart позволяет создавать конструкторы с разными именами  // В данном случае Post.fromJson(json) - это конструктор  // здесь мы принимаем объект поста и получаем его поля  // обратите внимание, что dynamic переменная  // может иметь разные типы: String, int, double и т.д.  Post.fromJson(Map<String, dynamic> json) :    this._userId = json["userId"],    this._id = json["id"],    this._title = json["title"],    this._body = json["body"];}// у нас будут только два состоянияabstract class PostAdd {}// успешное добавлениеclass PostAddSuccess extends PostAdd {}// ошибкаclass PostAddFailure extends PostAdd {}

Затем создадим новый метод в нашем Repository:

// добавление поста на серверFuture<PostAdd> addPost(Post post) async {  final url = Uri.parse("$SERVER/posts");  // делаем POST запрос, в качестве тела  // указываем JSON строку нового поста  final response = await http.post(url, body: post.toJson());  // если пост был успешно добавлен  if (response.statusCode == 201) {    // говорим, что все ок    return PostAddSuccess();  } else {    // иначе ошибка    return PostAddFailure();  }}

Далее добавим немного кода в PostController:

// добавление поста// функция addPost будет принимать callback,// через который мы будет получать результатvoid addPost(Post post, void Function(PostAdd) callback) async {  try {    final result = await repo.addPost(post);    // сервер вернул результат    callback(result);  } catch (error) {    // произошла ошибка    callback(PostAddFailure());  }}

Ну что ж пора нам вернуться к нашему представлению PostAddPage:

class PostDetailPage extends StatefulWidget {  @override  _PostDetailPageState createState() => _PostDetailPageState();}// не забываем поменять на StateMVCclass _PostDetailPageState extends StateMVC {  // _controller может быть null  PostController? _controller;  // получаем PostController  _PostDetailPageState() : super(PostController()) {    _controller = controller as PostController;  }  // TextEditingController'ы позволят нам получить текст из полей формы  final TextEditingController titleController = TextEditingController();  final TextEditingController contentController = TextEditingController();  // _formKey нужен для валидации формы  final _formKey = GlobalKey<FormState>();  @override  Widget build(BuildContext context) {    return Scaffold(      appBar: AppBar(        title: Text("Post Add Page"),        actions: [          // пункт меню в AppBar          IconButton(            icon: Icon(Icons.check),            onPressed: () {              // сначала запускаем валидацию формы              if (_formKey.currentState!.validate()) {                // создаем пост                // получаем текст через TextEditingController'ы                final post = Post(                  -1, -1, titleController.text, contentController.text                );                // добавляем пост                _controller!.addPost(post, (status) {                  if (status is PostAddSuccess) {                    // если все успешно то возвращаемя                    // на предыдущую страницу и возвращаем                    // результат                    Navigator.pop(context, status);                  } else {                    // в противном случае сообщаем об ошибке                    // SnackBar - всплывающее сообщение                    ScaffoldMessenger.of(context).showSnackBar(                      SnackBar(content: Text("Произошла ошибка при добавлении поста"))                    );                  }                });              }            },          )        ],      ),      body: Padding(        padding: EdgeInsets.all(15),        child: _buildContent(),      ),    );  }  Widget _buildContent() {    // построение формы    return Form(      key: _formKey,      // у нас будет два поля      child: Column(        children: [          // поля для ввода заголовка          TextFormField(            // указываем для поля границу,            // иконку и подсказку (hint)            decoration: InputDecoration(                border: OutlineInputBorder(),                prefixIcon: Icon(Icons.face),                hintText: "Заголовок"            ),            // указываем TextEditingController            controller: titleController,            // параметр validator - функция которая,            // должна возвращать null при успешной проверки            // и строку при неудачной            validator: (value) {              // здесь мы для наглядности добавили 2 проверки              if (value == null || value.isEmpty) {                return "Заголовок пустой";              }              if (value.length < 3) {                return "Заголовок должен быть не короче 3 символов";              }              return null;            },          ),          // небольшой отступ между полями          SizedBox(height: 10),          // Expanded означает, что мы должны          // расширить наше поле на все доступное пространство          Expanded(            child: TextFormField(              // maxLines: null и expands: true              // указаны для расширения поля              maxLines: null,              expands: true,              textAlignVertical: TextAlignVertical.top,              decoration: InputDecoration(                  border: OutlineInputBorder(),                  hintText: "Содержание",              ),              // указываем TextEditingController              controller: contentController,              // также добавляем проверку поля              validator: (value) {                if (value == null || value.isEmpty) {                  return "Содержание пустое";                }                return null;              },            ),          )        ],      ),    );  }}

Логика работы следующая:

  1. мы нажаем добавить новый пост

  2. открывается окно с формой, вводим данные

  3. если все ок, то возвращаемся на предыдущую страницу и сообщаем об этом иначе выводим сообщение об ошибке.

Заключительный момент, добавим обработку результата в PostListPage:

floatingActionButton: FloatingActionButton(  child: Icon(Icons.add),  onPressed: () {    // then возвращает объект Future    // на который мы подписываемся и ждем результата    Navigator.push(context, MaterialPageRoute(      builder: (context) => PostDetailPage()    )).then((value) {      if (value is PostAddSuccess) {        // SnackBar - всплывающее сообщение        ScaffoldMessenger.of(context).showSnackBar(         SnackBar(content: Text("Пост был успешно добавлен"))        );      }    });  },),

Теперь тестируем:

К сожалению JSONPlaceholder на самом деле не добавляет пост и поэтому мы не сможем его увидеть среди прочих постов.

Заключение

Я надеюсь, что убедил вас в том, что работа с формами на Flutter очень проста и не требует почти никаких усилий.

Большая часть кода - это создание POST запроса на сервер и обработка ошибок.

Полезные ссылки

Всем хорошего кода)

Подробнее..

Основы Flutter для начинающих (Часть IX)

11.06.2021 16:11:37 | Автор: admin

Flutter позволяет вам писать простые и понятные тесты для разных частей приложения.

Сегодня мы попробуем написать несколько unit тестов, которые используются для тестирования классов, методов и отдельных функций.

Также мы попробуем использовать библиотеку Mockito, которая позволяет создавать фейковые реализации.

Ну что ж, приступаем к тестированию!

Наш план
  • Часть 1- введение в разработку, первое приложение, понятие состояния;

  • Часть 2- файл pubspec.yaml и использование flutter в командной строке;

  • Часть 3- BottomNavigationBar и Navigator;

  • Часть 4- MVC. Мы будем использовать именно этот паттерн, как один из самых простых;

  • Часть 5- http пакет. Создание Repository класса, первые запросы, вывод списка постов;

  • Часть 6- работа с формами, текстовые поля и создание поста.

  • Часть 7- работа с картинками, вывод картинок в виде сетки, получение картинок из сети, добавление своих в приложение;

  • Часть 8 - создание своей темы, добавление кастомных шрифтов и анимации;

  • Часть 9 (текущая статья) - немного о тестировании;

Добавления необходимых зависимостей

Нам понадобиться два дополнительных пакета mockito и build_runner, поэтому добавим их:

# зависимости для разработки# в данном случае подключено тестированиеdev_dependencies:  flutter_test:    sdk: flutter  mockito: ^5.0.10  build_runner: ^2.0.4

Теперь мы можем приступать к тестированию

Пишем первый тест

В качестве объекта тестирования будет небольшой класс Stack:

class Stack<T> {  final stack = <T>[];    void push(T t) {    stack.add(t);  }    T? pop() {    if (isEmpty) {      return null;    }    return stack.removeLast();  }    bool get isEmpty => stack.isEmpty; }

Обратите внимание: класс Stack является обобщенным.

В корневой директории нашего проекта есть папка test, которая предназначена для тестов.

Создадим в ней новый файл stack_test.dart:

import 'package:flutter_test/flutter_test.dart';import 'package:json_placeholder_app/helpers/stack.dart';void main() {  // группа тестов  group("Stack", () {    // первый тест на пустой стек    test("Stack should be empty", () {      // expect принимает текущее значение       // и сравнивает его с правильным      // если значения не совпадают, тест не пройден      expect(Stack().isEmpty, true);    });    test("Stack shouldn't be empty", () {      final stack = Stack<int>();      stack.push(5);      expect(stack.isEmpty, false);    });    test("Stack should be popped", () {      final stack = Stack<int>();      stack.push(5);      expect(stack.pop(), 5);    });    test("Stack should be work correctly", () {      final stack = Stack<int>();      stack.push(1);      stack.push(2);      stack.push(5);      expect(stack.pop(), 5);      expect(stack.pop(), 2);      expect(stack.isEmpty, false);    });  });}

Довольно просто! Не правда ли?

На самом деле, это один из типов тестирования, который называется unit (модульное).

Также Flutter поддерживает:

  • Widget тестирование

  • Интеграционное тестирование

В данной статье мы рассмотрим только unit тестирование.

Давайте выполним наши тесты командой flutter test test/stack_test.dart:

Успешно!

Тестируем получение постов

Сначала видоизменим метод fetchPosts:

Future<PostList> fetchPosts({http.Client? client}) async {  // сначала создаем URL, по которому  // мы будем делать запрос  final url = Uri.parse("$SERVER/posts");  // делаем GET запрос  final response =  (client == null) ? await http.get(url) : await client.get(url);  // проверяем статус ответа  if (response.statusCode == 200) {    // если все ок то возвращаем посты    // json.decode парсит ответ    return PostList.fromJson(json.decode(response.body));  } else {    // в противном случае вызываем исключение    throw Exception("failed request");  }}

Теперь переходим к написанию самого теста.

Мы будем использовать mockito для создания фейкового http.Client'а

Создадим файл post_test.dart в папке tests:

import 'package:flutter_test/flutter_test.dart';import 'package:http/http.dart' as http;import 'package:json_placeholder_app/data/repository.dart';import 'package:json_placeholder_app/models/post.dart';import 'package:mockito/annotations.dart';import 'package:mockito/mockito.dart';// данный файл будет сгенерированimport 'post_test.mocks.dart';// аннотация mockito@GenerateMocks([http.Client])void main() {  // создаем наш репозиторий  final repo = Repository();  group("fetchPosts", () {      test('returns posts if the http call completes successfully', () async {        // создаем фейковый клиент        final client = MockClient();        // ответ на запрос        when(client.get(Uri.parse('https://jsonplaceholder.typicode.com/posts')))            .thenAnswer((_) async => http.Response('[{"userId": 1, "id": 2, "title": "Title", "content": "Content"}]', 200));        // проверяем корректность работы fetchPosts        // при удачном выполнении        final postList = await repo.fetchPosts(client: client);        expect(postList, isA<PostList>());        expect(postList.posts.length, 1);        expect(postList.posts.first.title, "Title");      });      test('throws an exception if the http call completes with an error', () {        final client = MockClient();        // генерация ошибки        when(client.get(Uri.parse('https://jsonplaceholder.typicode.com/posts')))            .thenAnswer((_) async => http.Response('Not Found', 404));        // проверка на исключение        expect(repo.fetchPosts(client: client), throwsException);      });  });}

Перед запуском теста необходимо сгенерировать post_test.mocks.dart файл:

flutter pub run build_runner build

После этого выполняем наши тесты командой flutter test test/post_test.dart:

Вуаля!

Заключение

Мы разобрали один из самых простых и известных типов тестирования - unit (модульное).

Как уже было отмечено, Flutter позволяет отдельно тестировать виджеты, а также проводить полноценное тестирование с применением интеграционных тестов.

Полезные ссылки:

Всем хорошего кода!

Подробнее..

Когортный анализ подписок как понять, что экономика сходится?

15.06.2021 10:13:02 | Автор: admin

Когортный анализ это способ отследить плотность метрики для определенных групп пользователей. В отличие от обычных графиков, когорты показывают именно stickiness пользователей, распределенный во времени.

Звучит сложно, на деле понять проще. Возьмем конкретный пример: попробуем понять как ведет себя база подписчиков в мобильном приложении: как они платят, отписываются и как долго живут. Ответы на эти вопросы я покажу на примере нашей реализации в Adapty.

Пример когортного анализа подписокПример когортного анализа подписок

При построении когорт важно указать, как они формируются, что показывать в ячейках и что измерять. В нашем случае когорты формируются по месяцу установки, в ячейке показываем выручку, подписчиков и среднюю выручку на подписчика за каждый месяц (ARPPU).

То есть, в строчках у нас когорты (группа) пользователей, которые установили приложение в определенный месяц, а в столбцах значения метрик по каждому месяцу с месяца установки (этот месяц первый или М1). В выделенной строчке М1 это январь, М2 февраль и тд. Каждый месяц количество подписчиков в данной когорте не возрастает, даже если пользователь установил в январе, а подписался в феврале, то он будет отнесен в когорту января. Нам кажется такой способ правильным для оценки сходимости.

Идея оценивать экономику так кроется в том, как работает привлечение пользователей. При закупке рекламы разработчик так или иначе платит за установки, а не за целевые действия. Даже в CPA кампания, все будет связано со стоимостью установки (CPI). Следовательно, чтобы оценить эффективность закупки трафика надо смотреть как именно люди установившие приложение в этот период будут монетизироваться. При этом, если пользователь установил приложение, но месяц не платил, он попадет только в М2.

Видим, что когорта пользователей января принесла нам суммарно до текущего момента времени $2900 выручки от 73 подписчиков.

Далее мы видим динамику затухания когорты, то есть как быстро пользователи отписываются. В январе у нас было 67 подписчиков, в феврале уже 38, а на июнь осталось 10 (штрихованная ячейка означает, что подписчики еще могут добавиться, так как месяц не закончился).

Сойдется ли экономика?

Теперь, пусть мы заплатили за рекламу $4000 за январь, наш вопрос такой окупятся пользователи или нет?, то есть будет ли выручка с них больше чем $4000 в разумное время.

Посмотрим внимательно на динамику подписчиков когорты января.

Когорта января в разбивке по месяцам (первая колонка установки)Когорта января в разбивке по месяцам (первая колонка установки)

На текущий момент приложение заработало $2900 до комиссии Apple, или $2465 после вычета 15% (приложение находится в программе Apple для SMB). Также будем считать, что мы продаем недельные подписки в среднем по $10.

Мы видим, что количество активных подписчиков после первого месяца упало почти в 2 раза, дальше на 20%, потом всего на 10% до 24. Так как на момент написания заметок месяц еще не кончился, возьмем лучший сценарий пусть все 24 подписчика будут с нами всегда. И даже пусть они в среднем платят $15.48 дальше, то есть их ARPPU не меняется.

Чтобы добить до $4000 нужно чтобы подписчики заплатили больше $1500, даже при сохранении выручки за месяц в $372 и нулевой отписке, когорта сойдется в лучшем случае через 4-5 месяцев непрерывных платежей. На практике, учитывая предыдущую динамику, и зная, что трафик закупается равномерно, когорта вряд ли сойдется меньше, чем за пару лет, а по факту скорее всего будет в убыток. Причина такая, что недельные подписки постоянно напоминают о себе и пользователи реже остаются в долгосрочных платежах, ведь если приложение хорошее, гораздо выгоднее купить год. Но даже с подписками на месяц при такой динамики на вряд ли стоит ожидать положительной прибыли.

Сходимость экономики подписок это долгосрочный процесс, я сделал инструмент для проверки сойдется ли экономика на ваших цифрах Калькулятор Подписок. Попробуйте его, чтобы оценить как зарабатывать на мобильных приложениях.

Подробнее..

Перевод Как использовать Android Data Binding в пользовательских представлениях?

16.06.2021 20:20:54 | Автор: admin

. . .

Как вы знаете, Data Binding Library - это отличная часть библиотеки Android Jetpack, позволяющая сократить количество шаблонного кода и связать представления с данными более эффективным способом, чем это было возможно ранее. В этой статье я собираюсь объяснить, как можно использовать привязку данных в наших пользовательских представлениях.

. . .

Начало работы

Чаще всего мы создаем пользовательские представления для повышения производительности, настройки и т.д., но если вы используете привязку данных в своих проектах, или напротив не используете ее в своих пользовательских представлениях, это не очень хорошо. Возможно, это выглядит примерно так:

class MyCustomView @JvmOverloads constructor(    context: Context,    attrs: AttributeSet? = null,    defStyleAttr: Int = 0) : FrameLayout(context, attrs, defStyleAttr) {    init {        attrs?.let {            val typedArray =                context.obtainStyledAttributes(it, R.styleable.MyCustomView)            // some attr handling stuffs...            typedArray.recycle()        }    }}

Но если вы хотите использовать пользовательское представление с компонентом привязки данных, то нет необходимости обрабатывать эти атрибуты.

Автоматический выбор метода

Когда вы определяете атрибут следующим образом:

<?xml version="1.0" encoding="utf-8"?><resources>    <declare-styleable name="MyCustomView">        <attr name="currencyCode" format="string" />    </declare-styleable></resources>

библиотека привязки данных имеет возможность автоматического выбора метода, то есть для атрибута с именем currencyCode библиотека автоматически пытается найти метод setCurrencyCode(arg), принимающий в качестве аргумента совместимые типы. Пространство имен атрибута не учитывается, при поиске метода используется только имя атрибута и тип. С другой стороны, если автоматический выбор метода не работает для имени вашего атрибута или вы хотите изменить метод сеттера для вашего атрибута, вы можете использовать методы привязки.

Методы привязки

Методы привязки дают вам возможность изменить сеттер для атрибута вашего пользовательского представления. Вы можете поместить эти методы над своим классом, используя аннотацию @BindingMethods, или создать пустой класс с этой аннотацией.

class MyCustomView @JvmOverloads constructor(    context: Context,    attrs: AttributeSet? = null,    defStyleAttr: Int = 0) : FrameLayout(context, attrs, defStyleAttr) {    private val currencyFormatter = NumberFormat.getCurrencyInstance(Locale.getDefault())        //..    fun setCurrency(currencyCode: String?) {        if (currencyCode.isNullOrEmpty())            return        currencyFormatter.currency = Currency.getInstance(currencyCode)    }    //..}

определите функцию, названную как угодно, затем просто поместите ее с атрибутом в методы привязки.

@BindingMethods(    value = [        BindingMethod(            type = MyCustomView::class,            attribute = "currencyCode",            method = "setCurrency"        )    ])class BindingMethods

с помощью этого способа вы можете задать различные имена сеттеров для вашего атрибута.

Адаптеры привязки

Если у вас есть функции с несколькими параметрами в вашем пользовательском представлении, и необходимо использовать эти функции с привязкой данных, то в этом случае следует использовать адаптеры привязки.

@BindingAdapter(    value = ["paddingEnd", "paddingTop", "paddingStart", "paddingBottom"],    requireAll = false)fun MyCustomView.setPaddingRelative(    paddingEnd: Int = 0,    paddingTop: Int = 0,    paddingStart: Int = 0,    paddingBottom: Int = 0) {    this.setPaddingRelative(paddingStart, paddingTop, paddingEnd, paddingBottom)}

Для настройки отступов можно создать адаптер привязки, как показано в примере.

. . .

Вот и все, спасибо за чтение! Если вам понравилась эта история, пожалуйста, нажмите на стрелочку вверх и поделитесь ею, чтобы помочь другим!

Если у вас есть какие-либо комментарии, не стесняйтесь связаться со мной в Twitter.


Перевод материала подготовлен в рамках запуска курса "Android Developer. Professional".

Всех желающих приглашаем на двухдневный интенсив по теме: "Полный coverage. Покрываем Android приложение юнит/интеграционными/UI тестами"


Подробнее..

Перевод Android 12 лет истории дизайна ОС

20.06.2021 12:09:43 | Автор: admin
Android установлен примерно на 2,5 миллиардах активных устройств. С чего он начинался? Давайте проверим и разберёмся. Мы протестируем все версии Android, с 1.0 по 9.0, и посмотрим, как менялась система.

image

ОС Android имеет довольно долгую историю: о выпуске самого первого Android-телефона HTC Dream объявили в сентябре 2008 года. Найти этот телефон может оказаться сложно, но это нам и не нужно компания Google создала для разработчиков эмулятор каждой из версий Android. SDK для версии 1.0 можно скачать со страницы https://developer.android.com/sdk/older_releases.html, и это единственная версия, не требующая установки. Достаточно просто запустить файл tools\emulator.exe. При первом запуске мы получаем ошибку:


Создание отсутствующей папки AppData\Local\Android\SDK-1.0 позволило решить проблему, после чего мы смогли запустить эмулятор:


Эмулятор Android 1.0

На главном экране мы видим все основные компоненты, которые существуют и сегодня значки, список приложений внизу и список уведомлений наверху:


Непривычны два аспекта. Во-первых, на телефоне есть около десяти аппаратных кнопок (в том числе курсорных клавиш). Например, кнопка Menu обеспечивает доступ к некоторым функциям:


В целом, все операции можно выполнить, не касаясь экрана, при помощи только аппаратных кнопок.

Во-вторых, всё выглядит большим и контрастным, но не стоит забывать, что размер экрана был маленьким, что-то около 3,2 дюйма. Может показаться удивительным, но устройство не имело экранной клавиатуры у первого Android-телефона была физическая клавиатура:


Телефон HTC Dream

Android 1.0 работал на телефоне с 192 МБ ОЗУ, процессором на 528 МГц, аккумулятором на 1150 мАч и экраном с разрешением 320x480.

Давайте проверим компоненты системы.

Вызовы и SMS


Очевидно, что я не мог совершить телефонный звонок или отправить SMS через эмулятор, но, по крайней мере, мы видим UI:


Всё выглядит контрастным, думаю, сегодня подобный UI используется в телефонах для пожилых.

Контакты



Карты


Google Maps работают на удивление хорошо. Поиск выполняется, режим отображения карты можно менять, и так далее.


Удивительно, что Google Maps API не изменился за более чем 12 лет.

Интернет


Google Maps работают хорошо, но ситуация сильно ухудшается, если протестировать Интернет-браузер. Поиск Google работает:


Но все остальные сервисы недоступны например www.youtube.com показывает, что требуется версия не ниже Android 4.0.


Я попробовал открыть Medium.com, первая страница Get started работала (более-менее), но после нажатия на Get Started отобразилась ошибка:


На самом деле, веб-сайт www.google.com оказался единственным, который я смог открыть. Это неудивительно, ведь Android 1.0 был выпущен больше десяти лет назад, а веб-стандарты сильно изменились.

Android 4.0 (2011 год)


Было бы слишком скучно тестировать все версии Android, поэтому давайте перенесёмся на несколько лет вперёд, к Android 4.0. Типичным телефоном того времени был LG Optimum L5 или HTC Desire C: 4-дюймовый экран с разрешением 320x480, процессор на 600 МГц и аккумулятор на 1230 мАч.


HTC Desire C

Для тестирования этой версии нам понадобится AVD (Android Virtual Device), который является частью Android Studio. Эта версия предназначена для разработчиков, но для запуска эмулятора нам не нужно писать код. Компонент AVD Manager позволяет выбирать разные версии и устройства:



Как ни жаль, официального офлайн-установщика для Android Studio больше не существует во время установки он автоматически скачивает множество файлов. Возможно, это и хорошо для разработки на современных платформах, но, насколько я понимаю, после отключения старых ссылок запустить его снова будет невозможно. По крайней мере, сейчас он работает, и после скачивания всех нужных компонентов Android 4.0 мы можем приступать к работе:


Как мы видим, UI и аппаратная раскладка изменились: больше нет отдельной кнопки Menu и клавиш курсора, только три аппаратные кнопки (Home, Back и Apps List), их можно увидеть и в современном Android.

Добавлена новая функция UI Widgets:


Один из них это большая панель, которая по умолчанию включена. Она позволяет быстро включать/отключать WiFi, Bluetooth и некоторые другие сервисы.

Settings по сравнению с современными версиями не сильно изменились, однако UI и шрифты, разумеется, другие:


Contacts теперь можно сохранять локально или синхронизировать с аккаунтом Google. Contacts и Dialer (набор номера) теперь стали двумя отдельными приложениями.



Отправка SMS не особо изменилась:


Web Browser работает, но большинство страниц (google play, youtube, даже Wikipedia) не открывается:


Medium.com по-прежнему открыть нельзя, но, по крайней мере, первая страница выглядит лучше, чем на Android 1.0:


Мне удалось открыть страницу MSN (с предупреждениями), страница BBC открылась без ошибок, но UI выглядел странно, а сайт NY Times вообще не открылся:


В картах добавлена новая функция: Google Maps Navigation:


Как ни удивительно, она по-прежнему работает, карты могут находить адреса и прокладывать маршрут.

Android 6.0 (2015 год)


Четыре года долгий срок для мира технологий, и характеристики смартфонов значительно улучшились. Хорошим примером устройства с Android 6 может служить Samsung Galaxy S6: 5,1 дюймовый AMOLED-экран с разрешением 1440x2560, восьмиядерным процессором и аккумулятором на 2550 мАч:


Внизу мы видим те же три кнопки, экран определённо стал больше, и в целом такой форм-фактор популярен по сей день.

С точки зрения UI, мы видим почти такой же интерфейс, который используется сегодня:


Contacts и Dialer по-прежнему остаются двумя отдельными приложениями (и двумя значками на экране), но разница между ними не так очевидна:



Интерфейс Settings тоже не особо изменился:


Web browser работает гораздо лучше, даже видео воспроизводится корректно, однако medium.com снова не прошёл тест отображается только белая страница:



На самом деле, www.medium.com это единственный сайт, который мне не удалось открыть.

Теперь в Android добавлены Gmail и Google Photos:


Google Maps работают хорошо, но, на удивление, спустя пять лет навигация по-прежнему находится в бета-версии.


В целом, интерфейс Android 6.0 выглядит достаточно современно даже по нынешним меркам, а разница между 4.0 и 6.0 гораздо очевиднее, чем между Android 6.0 и 10.

Android 8.0 (2017 год)


Я не собирался тестировать Android 8.0, с точки зрения UI отличий было бы не так много. Но мне стало любопытно, в какой версии Android корректно откроется medium.com. Давайте проверим.

Первое забавное отличие список приложений снова можно перетаскивать снизу вверх, точно так же, как в Android 1.0 (для сравнения см. изображение в начале статьи):


Как мы видим, как отдельные приложения были добавлены Youtube, Google Drive и Google Play Music.

Давайте снова протестируем браузер на medium.com. В целом, всё стало намного лучше мне удалось добраться до первого этапа логина:


Но на этом этапе страница зависает, и постоянно появляется всплывающее окно Sign in.

Android 9.0 (2018 год)


Очевидно, в каждой новой версии Android происходило множество скрытых изменений в безопасности, API и фоновых сервисах, но с точки зрения UI эта версия не сильно изменилась по сравнению с Android 6.0 2015 года. Как мы видим, добавилась левая панель Google. Приложения можно разделить на секции популярные и все приложения:


Напоследок давайте снова проверим страницу medium.com. Вуаля, теперь она работает:


Программирование


Эта статья не задумывалась как туториал по разработке для Android, но если уж мы установили Android Studio, то легко попробовать создать новое приложение для Android.


Для начала нам нужно создать новый проект и выбрать тип приложения.


На следующем этапе нужно указать имя приложения, его уникальный идентификатор, язык программирования (Java или Kotlin) и минимальный уровень версии Android.


После нажатия на Finish будут сгенерированы исходный код и ресурсы приложения.


Теперь мы можем запустить своё приложение в эмуляторе или в реальном устройстве:


Очевидно, что это приложение не делает ничего полезного, если вас интересуют последующие шаги, то изучите туториалы на веб-сайте https://developer.android.com.

Заключение


Исследование истории Android оказалось любопытным занятием. Как обычно, я призываю заинтересовавшихся читателей установить эмулятор и самостоятельно увидеть все различия. Один из способов это Android Studio, но она выполняет образ x86 и не может запускать сторонние приложения для Android. Ещё один удобный эмулятор это Genymotion, он основан на VirtualBox и обеспечивает полную эмуляцию ARM. Кроме того, он бесплатен для личного пользования. Я пользовался Genymotion несколько лет назад, но последняя версия по неизвестным причинам не работает. Возможно, кому-то из читателей повезёт. Однако существует множество других способов запуска Android на PC, так что можете выбрать подходящий для вас.

В конце я хочу сравнить основные отличия на одном изображении.

Дизайн UI



Совместимость веб-страниц






На правах рекламы


Воплощайте любые идеи и проекты с помощью наших серверов с мгновенной активацией на Linux или Windows, на наших серверах можно установить даже Android!

Подписывайтесь на наш чат в Telegram.

Подробнее..

Категории

Последние комментарии

© 2006-2021, personeltest.ru