пятница, 2 августа 2019 г.

Осознание стандартных функций Kotlin: run, with, let, also и apply

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

Функции скоупа

Функции, на которых я сконцентрируюсь, это run, with, T.run, T.let, T.also и T.apply. Я называю их функциями скоупа (видимости), поскольку я вижу их основную функциональность в том, что они поставляют внутреннюю область для вызывающей стороны.

Самый простой способ проиллюстрировать определение скоупа, это функция run

fun test() {
    var mood = "I am sad"

    run {
        val mood = "I am happy"
        println(mood) // I am happy
    }
    println(mood)  // I am sad
}

Logcat:
I/System.out: I am happy
I/System.out: I am sad

Благодаря этому, внутри функции test вы можете получить отдельный изолированный скоуп, в котором mood переопределен, чтобы напечатать "I am happy".

Эта функция скоупа сама по себе кажется не очень полезной. Но есть и еще один приятный момент, помимо скоупа; он возвращает что-то, то есть последний объект в области видимости.

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

    run {
        if (firstTimeView) introView else normalView
    }.show()

3 атрибута скоуп функции

Чтобы сделать функции обзора более интересными, позвольте мне классифицировать их поведение по 3 атрибутам. Я буду использовать эти атрибуты, чтобы отличать их друг от друга.

1. Нормальная функция VS функции расширения

Если мы посмотрим на with и T.run, обе функции на самом деле очень похожи. Ниже делается одно и тоже.

with(webview.settings) {
    javaScriptEnabled = true
    databaseEnabled = true
}

// тоже самое что и
webview.settings.run {
    javaScriptEnabled = true
    databaseEnabled = true
}

Их отличает лишь то, что одна из них - нормальная функция with, тогда как другая - функция расширения T.run

В чем же преимущество каждого?

Представьте, что webview.settings может принимать значение NULL, тогда эти две функции выглядеть так:

// Ужас!
with(webview.settings) {
      this?.javaScriptEnabled = true
      this?.databaseEnabled = true
}

// Норм
webview.settings?.run {
    javaScriptEnabled = true
    databaseEnabled = true

}

В этом случае очевидно, что функция расширения T.run лучше, поскольку мы могли бы применить проверку на null перед ее использованием.


2. this VS it

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

stringVariable?.run {
      println("The length of this String is $length")
}

// тоже самое что и
stringVariable?.let {
      println("The length of this String is ${it.length}")

}

Если вы проверите сигнатуру функции T.run, вы заметите, что T.run сделан просто как функция расширения, вызывающая блок T.().
Следовательно, все в пределах скоупа, T может быть обозначено как this.
В программировании this может быть пропущено в большинстве случаев. Поэтому в нашем примере выше мы могли бы использовать $length в выражении println вместо ${this.length}. Я называю это отправкой this в качестве аргумента.

Однако для сигнатуры функции T.let вы заметите, что T.let отправляет себя в функцию, т.е. в блок: (T). Следовательно, это похоже на лямбда-аргумент. Это можно отнести к функции скоупа как есть. Поэтому я называю это отправкой в качестве аргумента.

Из вышесказанного кажется, что T.run лучше, чем T.let, так как он неявен, но есть некоторые тонкие преимущества функции T.let, как показано ниже:
  • T.let обеспечивает более четкое разграничение использования данной переменной функции/члена по сравнению с функцией/членом внешнего класса
  • Если this нельзя пропустить, например, когда оно отправляется в качестве параметра функции, ее запись короче и понятнее
  • T.let позволяет лучше именовать преобразованную используемую переменную, то есть вы можете преобразовать ее в другое имя.


stringVariable?.let {

      nonNullString ->

          println("The non null string is $nonNullString")

}


3. Возвращение this VS другие типы

Теперь давайте посмотрим на T.let и T.also, оба они идентичны, если мы посмотрим на их внутренний скоуп.

stringVariable?.let {
      println("The length of this String is ${it.length}")
}

// Точно тоже самое что и

stringVariable?.also {
      println("The length of this String is ${it.length}")
}

Однако их главное отличие - вот что они возвращают. T.let возвращает значение другого типа, в то время как T.also возвращает сам T, то есть this.

Оба полезны для цепочки функций, где помощь T.let позволяет вам расширить операцию, а T.also позволяет вам использовать ту же самую переменную (т.е. this) в следующем обработчике.

Простая иллюстрация ниже

val original = "abc"

// Обрабатываем значение и отправляем в следующий обработчик
original.let {
    println("The original String is $it") // "abc"
    it.reversed() // evolve it as parameter to send to next let
}.let {
    println("The reverse String is $it") // "cba"
    it.length  // can be evolve to other type
}.let {
    println("The length of the String is $it") // 3
}

// Неправильно
// Тоже самое значение в следующий обработчик (печатает неправильный ответ)
original.also {
    println("The original String is $it") // "abc"
    it.reversed() // even if we evolve it, it is useless
}.also {
    println("The reverse String is ${it}") // "abc"
    it.length  // even if we evolve it, it is useless
}.also {
    println("The length of the String is ${it}") // "abc"
}

// Исправленный вариант also (т.е. манипуляции с исходной строкой)
// Тоже самое значение отправляется в следующий обработчик 
original.also {
    println("The original String is $it") // "abc"
}.also {
    println("The reverse String is ${it.reversed()}") // "cba"
}.also {
    println("The length of the String is ${it.length}") // 3

}

T.also может показаться бессмысленным выше, так как мы могли бы легко объединить их в единый блок функций. Но если хорошо продумать, у него есть несколько хороших преимуществ:
  1. Можно очень четко разделить процесс обработки объекта на маленькие функциональные участки
  2. Подход очень мощный для манипуляций со значениями перед использованием, мы создаем цепочку Builder
Когда два действия объединяются в цепь, т.е. сначала вычисляется одно значение, другое действие ждет, это становится очень мощным подходом, например, ниже:


// Нормальный подход
fun makeDir(path: String): File  {
    val result = File(path)
    result.mkdirs()
    return result
}

// Улучшенный подход
fun makeDir(path: String) = path.let{ File(it) }.also{ it.mkdirs() }


Обзор всех свойств

Посмотрев на 3 свойства, мы в значительной степени знать поведение функции. Позвольте мне проиллюстрировать функцию T.apply, так как она не упоминалась выше. 3 атрибута T.apply, как показано ниже:

1. функция расширения
2. посылает this как аргумент
3 возвращает this (то есть само себя)

Следовательно, можно представить, что его можно использовать следующим образом:

// Нормальный подход
fun createInstance(args: Bundle) : MyFragment {
    val fragment = MyFragment()
    fragment.arguments = args
    return fragment
}

// Улучшенный подход
fun createInstance(args: Bundle) 
              = MyFragment().apply { arguments = args }

Или мы могли бы также сделать цепочку создания объектов:

// Normal approach
fun createIntent(intentData: String, intentAction: String): Intent {
    val intent = Intent()
    intent.action = intentAction
    intent.data=Uri.parse(intentData)
    return intent
}

// Improved approach, chaining
fun createIntent(intentData: String, intentAction: String) =
        Intent().apply { action = intentAction }
                .apply { data = Uri.parse(intentData) }

Выбор функций

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



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





вторник, 30 июля 2019 г.

Использование DiffUtil в RecyclerView

DiffUtil это класс-утилита который может вычислять разницу между двумя списками и выводить список операций обновления, который конвертирует первый лист во второй. Это может использоваться для вычисления обновлений адаптера RecyclerView.
В большинстве случаев наш список полностью меняется, и мы устанавливаем новый список в адаптер RecyclerView. И мы вызываем дорогостоящий notifyDataSetChanged для обновления адаптера. Класс DiffUtil решает эту проблему.

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

Я собираюсь использовать статические данные для упрощения реализации.

public class DataProvider {

    public static List getOldPersonList(){
        List persons = new ArrayList<>();
        persons.add(new Person(1, 20, "John"));
        persons.add(new Person(2, 12, "Jack"));
        persons.add(new Person(3, 8, "Michael"));
        persons.add(new Person(4, 19, "Rafael"));
        return persons;
    }

    public static List sortByAge(List oldList){
        Collections.sort(oldList, new Comparator() {
            @Override
            public int compare(Person person, Person person2) {
                return person.age - person2.age;
            }
        });
        return oldList;
    }
}

DiffUtil.Callback является абстрактным классом и имеет 4 абстрактных метода и 1 неабстрактный. Давайте их рассмотрим.


Методы DiffUtil.Callback

getOldListSize() : возвращает размер старого списка

getNewListSize() : возвращает размер нового списка

areItemsTheSame(int oldItemPosition, int newItemPosition) : Вызывается DiffUtil чтобы решить являются ли два объекта представлениями одного и того же элемента. Если ваши элементы содержат уникальные идентификаторы, этот метод должен проверить их равенство

areContentsTheSame(int oldItemPosition, int newItemPosition) : Проверяет содержат ли два элемента одинаковые данные. Вы можете изменить это поведение в зависимости от вашего UI. Этот метод вызывается DiffUtil только если areItemsTheSame возвращает true.

getChangePayload(int oldItemPosition, int newItemPosition) : Если areItemTheSame возвращает true и areContentsTheSame возвращает false, DiffUtil вызывает этот метод, чтобы получить информацию об изменениях. В моем примере я не использую изменяемые объекты, но если вы хотите это использовать, вы может посмотреть этот пример.

public class MyDiffCallback extends DiffUtil.Callback {

    List oldPersons;
    List newPersons;

    public MyDiffCallback(List newPersons, List oldPersons) {
        this.newPersons = newPersons;
        this.oldPersons = oldPersons;
    }

    @Override
    public int getOldListSize() {
        return oldPersons.size();
    }

    @Override
    public int getNewListSize() {
        return newPersons.size();
    }

    @Override
    public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
        return oldPersons.get(oldItemPosition).id == newPersons.get(newItemPosition).id;
    }

    @Override
    public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
        return oldPersons.get(oldItemPosition).equals(newPersons.get(newItemPosition));
    }

    @Nullable
    @Override
    public Object getChangePayload(int oldItemPosition, int newItemPosition) {
        // вы можете вернуть определенное поле для измененного элемента
        return super.getChangePayload(oldItemPosition, newItemPosition);
    }
}

Метод обновления данных в моем RecyclerViewAdapter

public void updateList(ArrayList newList) {
    DiffUtil.DiffResult diffResult = 
            DiffUtil.calculateDiff(new MyDiffCallback(this.persons, newList));
    diffResult.dispatchUpdatesTo(this);
}

Это все. Мы вызываем метод dispactUpdatesTo (RecyclerView.Adapter), и адаптер будет уведомлен об изменении.

четверг, 25 июля 2019 г.

Kotlin: Деструктуризация

Если вы Android разработчик, которые переходит с Java на Kotlin, одной из наиболее интересных функций, с которой придется столкнуться, является деструктуризация.

Деструктуризация позволяет разбить объект на несколько переменных. Например, у вас может быть класс Person, который состоит из имени и поля возраста:

data class Person (val name: String, val age: Int)

Деструктуризация экземпляров этого класса позволяет эффективно инициализировать переменные внутренними полями объекта Person, как показано ниже:

(1) val person = Person("Thomas", 40)(2) val (name, age) = person // деструктуризация(3) println("$name is $age years old")



>> Tomas is 40 years old


Деструктуризация здесь происходит во второй строке. За сценой происходит следующее:

val name = person.component1()
val age = person.component2()


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

Для ответа на этот вопрос посмотрим на функционал типичного приложения, допустим нам необходимо иметь дело с данными о местоположении.
Допустим, нам необходимо обрабатывать коллекцию истории объекта Location. Мы по возможности упростим пример, наш Project Manager просто попросил чтобы мы писали широту и долготу каждой позиции в лог.


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

fun processLocations(locations: List) {
    for (location in locations) {
        Log.d("TAG", "Lat: ${location.latitude}, Lon: ${location.longitude}")
    }
}


Ну или что-то вроде этого:
fun processLocations(locations: List) { 
    locations.forEach { 
        location -> Log.d("TAG", "Lat: ${location.latitude}, Lon: ${location.longitude}") 
    }
}

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

Сначала мы создадим методы расширения для существующего класса Android Location, чтобы мы могли использовать деструктуризацию:

operator fun Location.component1() = latitude
operator fun Location.component2() = longitude

Теперь, с этими доработками, мы можем изменить наш пример чтобы использовать деструктуризацию. Вот один из примеров:

fun processLocations(locations: List) {
    for ((lat, lon) in locations) {
        Log.d("TAG", "Lat: $lat, Lon: $lon")
    }
}

Или даже еще более краткий:

fun processLocations(locations: List) {
    locations.forEach { (lat, lon) -> println("Lat: $lat, Lon: $lon") }
}

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

















понедельник, 30 января 2017 г.

Отличное видео по потокам Android


Ссылки по теме:
Библиотека Chronos...: https://habrahabr.ru/company/redmadrobot/blog/263111/ 

понедельник, 17 марта 2014 г.

Поиск оптимальных курсов обмена валют в Москве

Вот и воспользовался первый раз своим приложением "Валютные обменники Москвы". Нужно было лезть в свои заначки и менять евро на рубли.
Google Play - Валютные обменники Москвы
Google Play - Валютные обменники Санкт-Петербурга

Искал обменники недалеко от 2-й Брестской:



































Обращаем внимание на красные цифры под курсом обмена - это разница с наилучшим курсом по всем обменникам (не курсом ЦБ, а именно реальным) а также расстояние до местоположения обменника. Видим, что ближайший нормальный обменник располагается совсем рядом, в 400 метрах.

А вот он на карте:
 

Что ж, осталось только добежать до этого места чтобы убедиться в актуальности информации. А еще можно никуда не бегать, а для начала просто позвонить, т.к. все телефоны у нас имеются, а звонить можно прямо из программы, нажав на номер телефона:

По-моему, полезная штука