Работа с файловой системой
Работа с настройками уровня activity и приложения позволяет сохранить небольшие данные отдельных типов (string, int), но для работы с большими массивами данных, такими как графически файлы, файлы мультимедиа и т.д., нам придется обращаться к файловой системе.
ОС Android построена на основе Linux. Этот факт находит свое отражение в работе с файлами. Так, в путях к файлам в качестве разграничителя в Linux использует слеш «/», а не обратный слеш «\» (как в Windows). А все названия файлов и каталогов являются регистрозависимыми, то есть «data» это не то же самое, что и «Data».
Приложение Android сохраняет свои данные в каталоге /data/data// и, как правило, относительно этого каталога будет идти работа.
Для работы с файлами абстрактный класс android.content.Context определяет ряд методов:
- boolean deleteFile (String name) : удаляет определенный файл
- String[] fileList () : получает все файлы, которые содержатся в подкаталоге /files в каталоге приложения
- File getCacheDir() : получает ссылку на подкаталог cache в каталоге приложения
- File getDir(String dirName, int mode) : получает ссылку на подкаталог в каталоге приложения, если такого подкаталога нет, то он создается
- File getExternalCacheDir() : получает ссылку на папку /cache внешней файловой системы устройства
- File getExternalFilesDir(String type) : получает ссылку на каталог /files внешней файловой системы устройства
- File getFileStreamPath(String filename) : возвращает абсолютный путь к файлу в файловой системе
- FileInputStream openFileInput(String filename) : открывает файл для чтения
- FileOutputStream openFileOutput (String name, int mode) : открывает файл для записи
Все файлы, которые создаются и редактируются в приложении, как правило, хранятся в подкаталоге /files в каталоге приложения.
Для непосредственного чтения и записи файлов применяются также стандартные классы Java из пакета java.io.
Итак, применим функционал чтения-записи файлов в приложении. Пусть у нас будет следующая примитивная разметка layout:
Поле EditText предназначено для ввода текста, а TextView — для вывода ранее сохраненного текста. Для сохранения и восстановления текста добавлены две кнопки.
Теперь в коде Activity пропишем обработчики кнопок с сохранением и чтением файла:
package com.example.filesapp; import androidx.appcompat.app.AppCompatActivity; import android.os.Bundle; import android.view.View; import android.widget.EditText; import android.widget.TextView; import android.widget.Toast; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; public class MainActivity extends AppCompatActivity < private final static String FILE_NAME = "content.txt"; @Override protected void onCreate(Bundle savedInstanceState) < super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); >// сохранение файла public void saveText(View view) < FileOutputStream fos = null; try < EditText textBox = findViewById(R.id.editor); String text = textBox.getText().toString(); fos = openFileOutput(FILE_NAME, MODE_PRIVATE); fos.write(text.getBytes()); Toast.makeText(this, "Файл сохранен", Toast.LENGTH_SHORT).show(); >catch(IOException ex) < Toast.makeText(this, ex.getMessage(), Toast.LENGTH_SHORT).show(); >finally < try< if(fos!=null) fos.close(); >catch(IOException ex) < Toast.makeText(this, ex.getMessage(), Toast.LENGTH_SHORT).show(); >> > // открытие файла public void openText(View view) < FileInputStream fin = null; TextView textView = findViewById(R.id.text); try < fin = openFileInput(FILE_NAME); byte[] bytes = new byte[fin.available()]; fin.read(bytes); String text = new String (bytes); textView.setText(text); >catch(IOException ex) < Toast.makeText(this, ex.getMessage(), Toast.LENGTH_SHORT).show(); >finally < try< if(fin!=null) fin.close(); >catch(IOException ex) < Toast.makeText(this, ex.getMessage(), Toast.LENGTH_SHORT).show(); >> > >
При нажатии на кнопку сохранения будет создаваться поток вывода FileOutputStream fos = openFileOutput(FILE_NAME, MODE_PRIVATE)
В данном случае введенный текст будет сохраняться в файл «content.txt». При этом будет использоваться режим MODE_PRIVATE
Система позволяет создавать файлы с двумя разными режимами:
- MODE_PRIVATE : файлы могут быть доступны только владельцу приложения (режим по умолчанию)
- MODE_APPEND : данные могут быть добавлены в конец файла
Поэтому в данном случае если файл «content.txt» уже существует, то он будет перезаписан. Если же нам надо было дописать файл, тогда надо было бы использовать режим MODE_APPEND:
FileOutputStream fos = openFileOutput(FILE_NAME, MODE_APPEND);
Для чтения файла применяется поток ввода FileInputStream :
FileInputStream fin = openFileInput(FILE_NAME);
Подробнее про использование потоков ввода-вывода можно прочитать в руководстве по Java: https://metanit.com/java/tutorial/6.3.php
В итоге после нажатия кнопки сохранения весь текст будет сохранен в файле /data/data/название_пакета/files/content.txt

Где физически находится созданный файл? Чтобы увидеть его на подключенном устройстве перейдем в Android Stud в меню к пункту View -> Tool Windows -> Device File Explorer

После этого откроектся окно Device File Explorer для просмотра файловой системы устройства. И в папке data/data/[название_пакета_приложения]/files мы сможем найти сохраненный файл.
Функциональное программирование
Одним из строительных блоков программы являются функции. Функция определяет некоторое действие. В Kotlin функция объявляется с помощью ключевого слова fun , после которого идет название функции. Затем после названия в скобках указывается список параметров. Если функция возвращает какое-либо значение, то после списка параметров через запятую можно указать тип возвращаемого значения. И далее в фигурных скобках идет тело функции.
fun имя_функции (параметры) : возвращаемый_тип
Например, определим и вызовем функцию, которая просто выводит некоторую строку на консоль:
fun main() < hello() // вызов функции hello hello() // вызов функции hello hello() // вызов функции hello >// определение функции hello fun hello()
Функции можно определять в файле вне других функций или классов, сами по себе, как например, определяется функция main. Такие функции еще называют функциями верхнего уровня (top-level functions).
Здесь кроме главной функции main также определена функция hello, которая не принимает никаких параметров и ничего не возвращает. Она просто выводит строку на консоль.
Функция hello (и любая другая определенная функция, кроме main) сама по себе не выполняется. Чтобы ее выполнить, ее надо вызвать. Для вызова функции указывается ее имя (в данном случае «hello»), после которого идут пустые скобки.
Таким образом, если необходимо в разных частях программы выполнить одни и те же действия, то можно эти действия вынести в функцию, и затем вызывать эту функцию.
Предача параметров
Через параметры функция может получать некоторые значения извне. Параметры указываются после имени функции в скобках через запятую в формате имя_параметра : тип_параметра . Например, определим функцию, которая просто выводит сообшение на консоль:
fun main() < showMessage("Hello Kotlin") showMessage("Привет Kotlin") showMessage("Salut Kotlin") >fun showMessage(message: String)
Функция showMessage() принимает один параметр типа String . Поэтому при вызове функции в скобках необходимо передать значение для этого параметра: showMessage(«Hello Kotlin») . Причем это значение должно представлять тип String, то есть строку. Значения, которые передаются параметрам функции, еще назвают аргументами.
Консольный вывод программы:
Hello Kotlin Привет Kotlin Salut Kotlin
Другой пример — функция, которая выводит данные о пользователе на консоль:
fun main() < displayUser("Tom", 23) displayUser("Alice", 19) displayUser("Kate", 25) >fun displayUser(name: String, age: Int)
Функция displayUser() принимает два параметра — name и age. При вызове функции в скобках ей передаются значения для этих параметров. При этом значения передаются параметрам по позиции и должны соответствовать параметрам по типу. Так как вначале идет параметр типа String , а потом параметр типа Int , то при вызове функции в скобках вначале передается строка, а потом число.
Аргументы по умолчанию
В примере выше при вызове функций showMessage и displayUser мы обязательно должны предоставить для каждого их параметра какое-то определенное значение, которое соответствует типу параметра. Мы не можем, к примеру, вызвать функцию displayUser, не передав ей аргументы для параметров, это будет ошибка:
displayUser()
Однако мы можем определить какие-то параметры функции как необязательные и установить для них значения по умолчанию:
fun displayUser(name: String, age: Int = 18, position: String=»unemployed») < println("Name: $name Age: $age Position: $position") >fun main()
В данном случае функция displayUser имеет три параметра для передачи имени, возраста и должности. Для первого параметр name значение по умолчанию не установлено, поэтому для него значение по-прежнему обязательно передавать значение. Два последующих — age и position являются необязательными, и для них установлено значение по умолчанию. Если для этих параметров не передаются значения, тогда параметры используют значения по умолчанию. Поэтому для этих параметров в принципе нам необязательно передавать аргументы. Но если для какого-то параметра определено значение по умолчанию, то для всех последующих параметров тоже должно быть установлено значение по умолчанию.
Консольный вывод программы
Name: Tom Age: 23 Position: Manager Name: Alice Age: 21 Position: unemployed Name: Kate Age: 18 Position: unemployed
Именованные аргументы
По умолчанию значения передаются параметрам по позиции: первое значение — первому параметру, второе значение — второму параметру и так далее. Однако, используя именованные аргументы, мы можем переопределить порядок их передачи параметрам:
fun main()
При вызове функции в скобках мы можем указать название параметра и с помощью знака равно передать ему нужное значение.
При этом, как видно из последнего случае, необязательно все аргументы передавать по имени. Часть аргументов могут передаваться параметрам по позиции. Но если какой-то аргумент передан по имени, то остальные аргументы после него также должны передаваться по имени соответствующих параметров.
Также если до обязательного параметра функции идут необязательные параметры, то для обязательного параметра значение передается по имени:
fun displayUser(age: Int = 18, name: String) < println("Name: $name Age: $age") >fun main()
Изменение параметров
По умолчанию все параметры функции равносильны val-переменным, поэтому их значение нельзя изменить. Например, в случае следующей функции при компиляции мы получим ошибку:
fun double(n: Int) < n = n * 2 // !Ошибка - значение параметра нельзя изменить println("Значение в функции double: $n") >
Однако если параметр предствляет какой-то сложный объект, то можно изменять отдельные значения в этом объекте. Например, возьмем функцию, которая в качестве параметра принимает массив:
fun double(numbers: IntArray)< numbers[0] = numbers[0] * 2 println("Значение в функции double: $") > fun main() < var nums = intArrayOf(4, 5, 6) double(nums) println("Значение в функции main: $") >
Здесь функция double принимает числовой массив и увеличивает значение его первого элемента в два раза. Причем изменение элемента массива внутри функции приведет к тому, что также будет изменено значение элемента в том массиве, который передается в качестве аргумента в функцию, так как этот один и тот же массив. Консольный вывод:
Значение в функции double: 8 Значение в функции main: 8
Работа с коллекциями в Kotlin и Java
В этой статье хотелось бы сделать шпаргалку для тех, кто только начинает осваивать Kotlin. Этот язык предлагает широкий набор методов для работы с коллекциями. Многие начинают осваивать его, уже имея за плечами опыт на Java, поэтому мне хотелось бы привести также варианты кода и на Java.
Создаём класс-сущность
Для того, чтобы нам было от чего отталкиваться, создадим класс-сущность. Это класс, предназначенный для хранения данных. Часто в приложениях поля таких классов мапятся один к одному на поля таблицы в базе данных. Для примера рассмотрим сущность «Город», у которой есть два атрибута: название и количество проживающих в нём людей.
На старых версиях Java такой класс выглядел бы следующим образом:
public class City <
<>
private final String name;
private final int population;
public City(String name, int population) this .name = name;
this .population = population;
>
public String getName() return name;
>
public int getPopulation() return population;
>
@Override
public String toString() return «City «name='» + name + ‘\» +
«, population=» + population +
‘>’ ;
>
>
Обратите внимание, что класс неизменяемый, то есть значения его полей можно установить только в момент создания объекта, а затем они доступны лишь для чтения. Это особенно важно при работе с коллекциями в функциональном стиле.
С появлением Java 16 для этих целей мы можем использовать класс типа record.
public record City(
String name,
int population
) >
Компилятор автоматически сгенерит для него конструктор, стандартные методы equals(), hashCode(), toString() и get-методы для каждого поля. record тоже является неизменямым. Значения полей можно установить только через конструктор в момент инициализации.
На kotlin аналогичный класс объявляется следующим образом:
data class City(
val name: String,
val population: Int
)
Все классы на kotlin полностью совместимы на уровне байт-кода с Java-классами.
Создание коллекции
Создадим список (List) из четырёх объектов типа City. На Java, начиная с 9-ой версии, мы можем сделать это с помощью метода List.of():
List
new City( «Париж» , 2_148_327 ),
new City( «Москва» , 12_678_079 ),
new City( «Берлин» , 3_644_826 ),
new City( «Мадрид» , 3_266_126 )
);
А на kotlin вот так (ключевое слово new не требуется):
val cities = listOf(
City( «Париж» , 2_148_327 ),
City( «Москва» , 12_678_079 ),
City( «Берлин» , 3_644_826 ),
City( «Мадрид» , 3_266_126 )
)
Метод расширения listOf() возвращает объект класса List. Причём это опять-таки неизменяемый объект, то есть в него нельзя добавлять или удалять элементы. Если же нам нужен изменяемый список, то мы могли бы воспользоваться методом mutableListOf(), который вернёт нам объект класса MutableList.
И Java, и kotlin позволяют разделять десятичные разряды в числах при помощи символа нижнего подчёркивания. Делается это исключительно для удобства чтения и на сами значения никак не влияет.
Перебор всех элементов коллекции
Помимо традиционного перебора элементов через цикл foreach (например, в целях логирования), Java позволяет сделать это в функциональном стиле через метод forEach():
cities.forEach(c -> System.out.println(c.name()));
Тут мы при помощи лямбда-выражения выводим в консоль название каждого города. В kotlin это делается аналогично:
cities.forEach < println(it.name) >
Имя переменной it представляет собой имя по умолчанию для единственного параметра ламбда-выражения. Вы также можете явно задать имя по аналогии с Java. Но если у вас окажется несколько вложенных лямбда-выражений, тогда явное именование их параметров становится обязательным.
Если вам помимо самого элемента нужен ещё и его индекс в коллекции, то используйте метод forEachIndexed(). Тогда в лямбде нужно будет в явном виде указывать два параметра вместо одного дефолтного it: индекс и элемент.
cities.forEachIndexed < index, city ->println( «$index: $
Здесь мы выводим индекс элемента, а затем после двоеточия название города. В kotlin символ доллара позволяет подставлять значение переменной прямо в строку. Если нужно встроить не объект целиком, а одно из его полей, тогда помимо доллара выражение нужно взять в фигурные скобки.
Преобразование элементов списка
Давайте теперь преобразуем созданный нами список городов в список их названий с сохранением порядка следования. На Java нам помогут стримы:
List
.map(City::name)
.toList();
Тут мы сначала преобразуем нашу коллекцию в стрим, затем делаем преобразование с помощью метода map() и затем преобразуем стрим в новый список с помощью метода toList(). На kotlin такое же преобразование выглядт следующим образом:
// более краткая запись: cities.map < it.name >
val cityNames = cities.map < c ->c.name >
Тут метод также называется map().
Фильтрация элементов списка
Давайте теперь создадим новый список, в котором будут только города с населением более трёх миллионов человек. Код на Java:
cities.stream()
.filter(c -> c.population() >= 3_000_000 )
.toList();
cities.filter < it.population >= 3_000_000 >
Как видите, в обоих случаях ключевым является метод filter(). Только в kotlin он уже возвращает готовую коллекцию, тогда как на Java возвращается стрим, который затем мы преобразуем с помощью toList().
Первый и последний элемент списка
По аналогии с фильтрацией можно сделать поиск первого элемента из списка. В Java 21 нам поможет метод getFirst(). Если же список пустой, то этот метод кидает исключение NoSuchElementException. Поэтому можно использовать более универсальный findFirst() из стримов:
// если список не пустой, начиная с Java 21
City first = cities.getFirst();
// список может быть пустой
Optional first = cities.stream().findFirst();
Он возвращает объект типа Optional, т.к. коллекция может быть пустой и тогда Optional также будет пустым. В kotlin нет Optional, а вместо этого используется nullable-тип, т.е. компилятор контролирует, может ли ваш тип когда-либо принять значение null или не может. Признаком допустимости null является знак вопроса после имени соответствующего типа. Ниже для наглядности тип возвращаемого значения указан в явном виде после двоеточия. Обычно компилятор kotlin сам выводит тип, поэтому явное указание излишне.
// кидает исключение для пустого списка
val nonNullableFirst: City = cities.first()
// возвращает null в случае пустого списка
val nullableFirst: City? = cities.firstOrNull()
В случае пустого списка firstOrNull() вернёт null, тогда как метод first() выбросит исключение. Поэтому метод firstOrNull() в общем случае использовать предпочтительнее.
Помимо получения самого первого элемента списка вы можете выбрать первый элемент, удовлетворяющий определённым условиям. Например, первый город, имя которого начинается на букву «М». На Java мы просто скомбинируем два уже известных нам метода:
cities.stream()
.filter(c -> c.name().startsWith( «М» ))
.findFirst();
А на kotlin метод firstOrNull() и другие ему подобные принимают в качестве параметра предикат (условие фильтрации):
cities.firstOrNull < it.name.startsWith( "М" ) >
Опять же, код получился более компактным.
Kotlin также предоставляет методы last() и lastOrNull(), которые возвращают не первый, а последний элемент списка. В остальном принцип их работы такой же, как и у выше рассмотренных методов.
Начиная с Java 21 нам также доступен метод getLast(), возвращающий последний элемент, если список не пустой.
Прямая и обратная сортировка элементов
Теперь давайте отсортируем наши города по названию в обратном алфавитном порядке. То есть от «Я» до «А». Код на Java:
List
.sorted(Comparator.comparing(City::name, Comparator.reverseOrder()))
.toList(); // Париж, Москва, Мадрид, Берлин
В метод sorted() мы передаём компаратор, который содержит в себе информацию, по какому полю сортируем (поле name) и каков порядок сортировки (обратный). Для прямой сортировки нужно использовать Comparator.naturalOrder() или же вообще не указывать второй параметр. Затем создаём новый отсортированный список. То же самое на kotlin записывается более компактно:
// Париж, Москва, Мадрид, Берлин
val sortedCities = cities.sortedByDescending < it.name >
Как видите, для удобства есть специальный метод расширения sortedByDescending(). В виде лямбды в него нужно лишь передать поле, по которому производится сортировка. Для прямой сортировки используйте метод sortedBy().
Объединение нескольких строк в одну
Очень полезно бывает объединить несколько строк в одну, поместив между ними запятую. Давайте объединим названия городов. В Java для этого есть метод String.join().
// «Париж, Москва, Берлин, Мадрид»
String citiesString = String.join( «, » , cityNames);
В kotlin есть похожий метод joinToString():
// «Париж, Москва, Берлин, Мадрид»
val citiesString = cityNames.joinToString(separator = «, » )
В параметрах можно указывать не только разделитель, но и префикс, постфикс, ограничение на максимальное количество элементов, которые будут включены в строку, а также специальную строку, которая будет сигнализировать о том, что в результат поместились не все элементы (например, многоточие). По умолчанию все эти параметры имеют подходящие значения, поэтому в явном виде их вообще можно не указывать.
Поиск максимальных и минимальных значений
Давайте найдём город с самым большим населением. Нетрудно догадаться, что среди наших исходных данных это Москва. В Java мы воспользуемся методом max() и методом Comparator.comparing() для указания того поля, по которому происходит сравнение:
Optional
.max(Comparator.comparing(City::population)); // Москва
В kotlin такая же выборка делается c помощью метода maxByOrNull():
val mostPopulatedCity = cities.maxByOrNull < it.population >// Москва
Для поиска минимального элемента в обоих случаях нужно поменять «max» на «min», то есть воспользоваться методами min() и minByOrNull() соответственно. Тогда в нашей выборке самым маленьким городом окажется Париж.
Вычисление суммы и среднего значения
Теперь давайте посчитаем сколько всего людей живёт в наших городах вместе взятых. То есть посчитаем сумму по полю population. В Java нам нужно будет получить IntStream при помощи метода mapToInt():
int totalPopulation = cities.stream()
.mapToInt(City::population)
.sum();
Метод IntStream.sum() всегда возвращает целочисленное значение, т.к. даже в случае пустой коллекции сумма просто будет равна нулю.
Теперь давайте вычислим среднее значение населения. Тут уже чуть сложнее, потому что во-первых, среднее значение имеет тип Double, а во-вторых, его может и не быть в случае, если коллекция пуста:
OptionalDouble averagePopulation = cities.stream()
.mapToInt(City::population)
.average();
Также класс IntStream предлагает метод summaryStatistics(), который позволяет получить все основные статистические параметры разом:
IntSummaryStatistics statistics = cities.stream()
.mapToInt(City::population).summaryStatistics();
// count=4, sum=21737358, min=2148327, average=5434339.500000, max=12678079
Объект IntSummaryStatistics содержит пять полей:
- количество элементов
- сумма
- минимум
- максимум
- среднее значение.
Теперь давайте найдём вычислим суммарное и среднее население с использованием kotlin:
val totalPopulation = cities.sumOf < it.population >// или cities.map < it.population >.sum()
val averagePopulation = cities.map < it.population >.average() // среднее значение
Метод sumOf() принимает в качестве параметра поле, по которому нужно выполнить суммирование и возвращает целочисленную сумму. Этот метод равносилен комбинации методов map() и sum(). Метод average() возвращает среднее значение в виде Double, причём в случае пустого списка метод вернёт специальное значение Double.NAN («not a number»).
Преобразование List в Map
Теперь давайте превратим наш список городов в мапу, где ключом будет название города. После этого мы сможем быстро находить нужный город в нашей коллекции по его имени. На Java это делается через метод Collectors.toMap():
Map
.collect(Collectors.toMap(City::name, Function.identity()));
В первом параметре мы указываем, какое поле должно стать ключом, а во втором – что брать в качестве значения. Function.identity() возвращает элемент списка целиком. На kotlin для этого есть специальный метод associateBy():
val citiesByName = cities.associateBy < it.name >
Если же мы хотим, чтобы ключом мапы было название города, а значением – кол-во его жителей, тогда на Java код будет выглядеть так:
Map
.collect(Collectors.toMap(City::name, City::population));
А на kotlin вот так:
val nameToPopulation = cities.associate < it.name to it.population >
Обратите внимание, что to в данном случае не ключевое слово языка, а лишь обычная функция, помеченная ключевым словом infix. Этот модификатор позволяет записывать вызов функций в таком красивом виде, а по факту данный вызов полностью эвивалентен вызову it.name.to(it.population). Механизм инфиксной записи открывает довольно широкие возможности для создания синтаксисов, ориентированных на конкретную предметную область.
В итоге функция to() возвращает объект Pair, который состоит из двух полей first и second. Затем метод associate() преобразует список таких объектов в мапу.
Группировка элементов
Теперь давайте создадим не просто мапу, а такую мапу, в которой ключом будет первая буква имени города, а значением – список всех городов, начинающихся на эту букву. Иными словами, сделаем группировку по первой букве имени города. В наших исходных данных есть два города, начинающихся на одну букву, следовательно по ключу «М» у нас должен быть список из двух элементов.
Map
.collect(Collectors.groupingBy(c -> c.name().charAt( 0 )));
В Java мы используем метод Collectors.groupingBy(), в который указываем правило формирования ключа будущей мапы. Логика его заключается в том, чтобы брать первый символ в названии города. На kotlin это записывается с помощью метода groupBy():
val citiesByFirstLetter = cities.groupBy < it.name.first() >
Уже знакомый нам метод first(), применённый к строке, возвращает её первый символ.
Преобразование двумерного списка в одномерный
Теперь выполним обратную задачу: объединим несколько списков разного размера в один общий список. Предположим, у нас имеется список, каждый элемент которого является списком строк. Тогда мы можем получить один «плоский» список с помощью метода flatMap(). Код на Java:
List letterLists = List.of(
List.of( «a» , «b» , «c» ),
List.of( «d» ),
List.of( «e» , «f» )
);
List plainLetters = letterLists.stream()
.flatMap(Collection::stream)
.toList(); // [a, b, c, d, e, f]
Каждый элемент, являющийся вложенным списком, мы преобразуем в стрим. Эти стримы объединяются в один общий и затем результирующий стрим преобразуется в новый список.
В kotlin также есть метод flatMap(), но если нам не требуется выполнять дополнительных преобразований над элементами, то воспользуемся его более кратким эквивалентом flatten():
val letterLists = listOf(
listOf( «a» , «b» , «c» ),
listOf( «d» ),
listOf( «e» , «f» )
)
val plainLetters = letterLists.flatten() // [a, b, c, d, e, f]
Преобразование List в Set
Коллекция типа «множество» (set) отличается от простого списка тем, что в нём содержатся только уникальные значения. Если мы сначала создадим список с дублями, а затем преобразуем его в Set, то в результате получим новую коллекцию с меньшим количеством элементов и все они будут уникальными.
Давайте создадим сначала список из дней недели, в котором будут дубли. Затем преобразуем его в множество с уникальными элементами (множество дней недели, которые считаются выходными). В Java это можно сделать так:
// список с дублями
List days = List.of( «суббота» , «воскресенье» , «суббота» );
// быстрый поиск
Set fastHolidays = new HashSet<>(days);
// сохранение порядка элементов
Set orderedHolidays = new LinkedHashSet<>(days);
Как я рассказывал в статье Коллекции: list, set, map, в Java (а, значит, и в Kotlin) есть несколько реализаций интерфейса Set. В данном случае я привёл две из них: HashSet и LinkedHashSet. Первую следует использовать тогда, когда нам требуется искать в ней элементы и порядок нам не важен, а вторую – когда мы будем где-либо показывать это множество, т.к. в нём сохранится порядок следования элементов из исходной коллекции.
На kotlin создание этих множеств будет выглядеть следующим образом:
val days = listOf( «суббота» , «воскресенье» , «суббота» ) // список с дублями
val fastHolidays = days.toHashSet() // быстрый поиск
val orderedHolidays = days.toSet() // сохранение порядка элементов
Метод toHashSet() ожидаемо возвращает именно HashSet, оптимизированный для поиска значений. А метод toSet() по факту возвращает именно LinkedHashSet, если в исходной коллекции больше одного элемента. То есть он возвращает множество, которое сохранит порядок следования элементов из исходной коллекции.
Поиск элементов в коллекции
Поиск элементов по значению можно выполнять в любом виде коллекций (list, set, map), но лучше всего для этого подходят именно множества (set). Сказанное далее технически применимо к любой коллекции, но быстрее всего будет работать именно в HashSet. Поэтому возьмём коллекцию fastHolidays из предыдущего примера и проверим, является ли понедельник выходным днём.
В Java для таких проверок используется метод contains():
boolean isHoliday = fastHolidays.contains( «понедельник» ); // false
В результате мы ожидаемо получим false, т.к. «понедельник – день тяжёлый» и выходным не является. В kotlin эквивалентная проверка выглядит следующим образом:
val isHoliday = fastHolidays.contains( «понедельник» )
// или более краткая форма
val isHoliday = «понедельник» in fastHolidays
Вторая форма использует выражение in, но это не более чем синтаксический сахар, т.к. благодаря соглашениям об именовании методов kotlin всегда будет вызывать метод contains(), когда встречает выражение in. На мой взгляд, второй вариант гораздо более читаемый и понятный.
Групповая проверка условий
Иногда бывает необходимо проверить какое-то условие над всеми элементами коллекции сразу. Например, у нас есть множество целых чисел и мы хотим узнать, есть ли среди них положительные числа? Все ли они положительны? И наконец, мы хотим убедиться, что среди них нет нуля. Пример на Java:
Set
boolean hasPositive = numbers.stream().anyMatch(n -> n > 0 ); // true
boolean allPositive = numbers.stream().allMatch(n -> n > 0 ); // false
boolean withoutZero = numbers.stream().noneMatch(n -> n == 0 ); // true
В этом нам помогают методы стримов anyMatch(), allMatch() и noneMatch(), которые принимают условия проверки в виде лямбды. В результате этих проверок мы узнаём, что среди наших чисел есть положительные, однако не все, и среди них действительно нет нуля.
То же самое можно написать и на kotlin с использованием методов any(), all() и none() соответственно:
val numbers = setOf(- 2 , — 1 , 3 , 4 , 5 )
val hasPositive = numbers.any < it >0 > // true
val allPositive = numbers.all < it >0 > // false
val withoutZero = numbers.none < it == 0 >// true
Важно также отметить, что и в случае метода allMatch(), и в случае метода all(), вызов на пустой коллекции с любым предикатом всегда вернёт true.
Выводы
Как видите, с точки зрения синтаксиса, операции над коллекциями в kotlin всегда более компактны и читаемы, нежели на Java. Важно заметить, что при этом нет накладных расходов в плане производительности, т.к. в kotlin используются те же классы коллекций из Java, но при этом они дополнены большим количеством вспомогательных методов расширения. В результате работать с коллекциями в kotlin всегда легко и приятно.
Kotlin как изменить имя читаемого файла
Android использует файловую систему, которая аналогична дисковым файловым системам на других платформах. Эта лекция описывает, как работать с файловой системой Android для чтения и записи файлов с помощью File API (перевод документации [1]).
Объект File подходит для чтения или записи больших объемов данных в порядке от начала до конца, без пропусков. Например, это хорошо подходит для файлов картинок или для различных обменов данными через сеть. Здесь будет показано, как выполнять базовые файловые операции в Вашем приложении. Подразумевается, что Вы знакомы с файловой системой Linux и стандартной системой ввода/вывода файлов в (standard file input/output API) в java.io.
[Выбор между внутренним и внешним хранилищем (Internal Storage, External Storage)]
Все устройства Android имеют две области хранения файлов (file storage area): «internal» и «external» storage. Эти имена появились на первых стадиях развития Android, когда большинство устройств поставлялись со встроенной энергонезависимой памятью FLASH (internal storage) плюс извлекаемый носитель памяти, такой как micro SD card (external storage). Некоторые устройства делили постоянную область хранения (permanent storage space) на разделы «internal» и «external», так что даже без наличия внешнего извлекаемого хранилища (removable storage medium) всегда имеется 2 пространства хранения, и поведение API всегда одинаково — независимо от того, есть в наличии внешнее хранилище или нет. Следующие списки подводят общую черту под фактическими различиями каждого пространства хранения.
| Internal storage (внутреннее, неизвлекаемое хранилище): | External storage (внешнее, извлекаемое хранилище): |
| • Оно всегда доступно. • Файлы, которые сохранены здесь, по умолчанию доступны только Вашего приложения. • Не требуется запрашивать разрешения на доступ к internal storage для Вашего приложения. • Когда пользователь деинсталлирует Ваше приложение, то система также удалит все файлы приложения с internal storage. |
Совет: несмотря на то, что приложения по умолчанию устанавливаются в internal storage, Вы можете указать атрибут android:installLocation в файле манифеста, после чего Ваше приложение может быть установлено и на external storage. Пользователи ценят эту опцию, когда размер APK очень велик, и размер external storage space больше, чем internal storage. Дополнительную информацию см. в документации App Install Location [2].
[Получение разрешения для приложения на доступ к External Storage]
Чтобы иметь возможность записи в external storage, Вы должны запросить в файле манифеста разрешение WRITE_EXTERNAL_STORAGE :
< manifest. > < uses-permissionandroid:name="android.permission.WRITE_EXTERNAL_STORAGE" /> . < /manifest >
Внимание: в настоящее время приложения имеют возможность чтения external storage без специального на то разрешения. Однако это изменится в будущих релизах системы Android. Если Ваше приложение требует чтения external storage (но не записывает в него), то Вам нужно декларировать разрешение READ_EXTERNAL_STORAGE . Чтобы обеспечить будущую работу Вашего приложения так, как это ожидалось, Вы должны декларировать это разрешение уже сейчас, до того как изменения вступят в реальную силу.
< manifest. > < uses-permissionandroid:name="android.permission.READ_EXTERNAL_STORAGE" /> . < /manifest >
Но если Ваше приложение использует разрешение WRITE_EXTERNAL_STORAGE, то это неявно дает ему также разрешение использовать и чтение external storage.
Вам не нужно получать никаких разрешений на сохранение файлов в internal storage. Ваше приложение всегда имеет разрешение на чтение и запись файлов в свой внутренний каталог на internal storage.
[Сохранение файла в Internal Storage]
Когда сохраняется файл в internal storage, Вы можете запросить подходящую директорию для объекта файла File вызовом одного из двух методов:
getFilesDir() возвращает объект File, представляющий внутренний каталог Вашего приложения.
getCacheDir() возвращает объект File, представляющий внутренний каталог временных файлов кэша Вашего приложения. Обязательно удаляйте оттуда каждый файл, когда он больше не нужен, и реализуйте разумный предел размера для объема памяти, который используете в любой момент времени, такой как предел в 1 мегабайт. Если система Android обнаружит, что на внутреннем хранилище недостаточно места, то она может удалить Ваши файлы кэша без предупреждения.
Чтобы создать новый файл в одной из этих директорий, Вы можете использовать конструктор File(), передав ему File, предоставленный одним из этих методов, которые укажут каталог на internal storage. Пример:
File file = new File(context.getFilesDir(), filename);
Альтернативно Вы можете вызвать openFileOutput(), чтобы получить FileOutputStream, который записывает файл в Вашей внутренней директории. Например, здесь показано, как записать некий текст в файл:
String filename = "myfile"; String string = "Hello world!"; FileOutputStream outputStream;
try outputStream = openFileOutput(filename, Context.MODE_PRIVATE); outputStream.write(string.getBytes()); outputStream.close(); > catch (Exception e) e.printStackTrace(); >
Или, если Вам нужно кэшировать некоторые файлы, Вы должны вместо этого использовать createTempFile() . Например, следующий метод вытаскивает имя файла из URL и создает файл с таким именем во внутренней директории для кэша Вашего приложения:
public File getTempFile(Context context, String url) File file; try String fileName = Uri.parse(url).getLastPathSegment(); file = File.createTempFile(fileName, null, context.getCacheDir()); > catch (IOException e) // Ошибка при создании файла . > return file; >
Примечание: каталог internal storage Вашего приложения указывается на основе имени пакета приложения в специальном месте файловой системы Android. Технически другое приложение может прочитать Ваши внутренние файлы, если Вы установите файловый режим с разрешенным чтением. Однако для этого другое приложение должно также знать имя пакета Вашего приложения и имена используемых Вашим приложением файлов. Другие приложения не могут просматривать Ваши внутренние директории, и не могут получить доступ на чтение или запись, за исключением случая, когда Вы явно установите файл как читаемый и/или записываемый. Таким образом, пока Вы используете MODE_PRIVATE для Ваших файлов на internal storage, то они никогда не будут доступны для других приложений.
[Сохранение файла в External Storage]
Поскольку external storage иногда может быть недоступно (когда пользователь смонтировал его как внешний USB-носитель на PC, или когда вытащил карту SD из телефона), то перед доступом к тому Вы должны всегда проверить, что он есть в наличии. Вы можете запросить состояние external storage вызовом getExternalStorageState() . Если возвращенное состояние External Storage равно MEDIA_MOUNTED , то Вы можете читать и записывать на него свои файлы. Например, следующие методы полезны для определения доступности устройства хранения:
/* Проверяет, доступно ли external storage для чтения и записи */ public boolean isExternalStorageWritable() String state = Environment.getExternalStorageState(); if (Environment.MEDIA_MOUNTED.equals(state)) return true; > return false; > /* Проверяет, доступно ли external storage как минимум для чтения */ public boolean isExternalStorageReadable() String state = Environment.getExternalStorageState(); if (Environment.MEDIA_MOUNTED.equals(state) || Environment.MEDIA_MOUNTED_READ_ONLY.equals(state)) return true; > return false; >
Несмотря на то, что external storage может быть модифицировано пользователем и другими приложениями, есть две категории файлов, которые могут быть сохранены здесь:
Публичные файлы (Public files) — это файлы, которые должны быть свободно доступны для других приложений и пользователя. Когда пользователь деинсталлирует Ваше приложение, эти файлы должны остаться доступными для пользователя. Например, такими файлами могут быть фотографии, созданные другими приложениями или другие файлы, загруженные через сеть.
Частные файлы (Private files) — это файлы, полные права на которые принадлежат Вашему приложению, и которые должны быть удалены при деинсталляции Вашего приложения пользователем. Несмотря на то, что эти файлы технически доступны для пользователя и других приложений, поскольку они находятся на внешнем извлекаемом хранилище (external storage), эти файлы не имеют в реальности особого значения для пользователя вне Вашего приложения. Когда пользователь деинсталлирует Ваше приложение, система удалит все файлы в Вашем частном каталоге на внешнем хранилище. Примером таких файлов могут быть дополнительные ресурсы, загруженные Вашим приложением или временные медиафайлы.
Если Вы хотите сохранить public-файлы на external storage, используйте метод getExternalStoragePublicDirectory() для получения экземпляра File , предоставляющего подходящую директорию на external storage. Метод принимает аргумент, указывающий тип файла, который Вы хотите сохранить, так чтобы типы файла были логически организованы с другими public-файлами, такими как DIRECTORY_MUSIC или DIRECTORY_PICTURES . Пример:
public File getAlbumStorageDir(String albumName) // Получение каталога для публичного каталога картинок пользователя. File file = new File(Environment.getExternalStoragePublicDirectory( Environment.DIRECTORY_PICTURES), albumName); if (!file.mkdirs()) Log.e(LOG_TAG, "Директория не создана"); > return file; >
Если Вы хотите сохранить файлы, которые являются частными (private) для Вашего приложения, Вы можете получить подходящую директорию вызовом метода getExternalFilesDir() и передачей ему имени, указывающего тип директории, который Вам нужен. Каждая директория, созданная таким способом, будет добавлена к родительской директории, в которой инкапсулированы все файлы внешнего хранилища Вашего приложения, которые система удалит, когда пользователь деинсталлирует Ваше приложение. Например, вот метод, которым Вы можете создать директорию индивидуального фотоальбома:
public File getAlbumStorageDir(Context context, String albumName) // Получение каталога для приватного каталога картинок приложения. File file = new File(context.getExternalFilesDir( Environment.DIRECTORY_PICTURES), albumName); if (!file.mkdirs()) Log.e(LOG_TAG, "Директория не создана"); > return file; >
Если ни одно из предварительно определенных имен поддиректорий не подходит для Ваших файлов, то Вы можете вместо этого вызвать getExternalFilesDir() и передать null. Это возвратит корневую частную директорию для Вашего приложения на external storage.
Помните, что getExternalFilesDir() создает директорию внутри директории, которая будет удалена при деинсталляции Вашего приложения. Если файлы, которые Вы сохраняете, должны оставаться доступными после того, как пользователь деинсталлирует Ваше приложение — как например если Ваше приложение работает с фотокамерой, и пользователь хотел бы сохранить сделанные фотографии — Вы должны вместо этого использовать getExternalStoragePublicDirectory() .
Независимо от того, используете ли Вы для публичных файлов getExternalStoragePublicDirectory() или getExternalFilesDir() для частных файлов приложения, важно иметь в виду, что Вы используете имена директорий, предоставленные константами API наподобие DIRECTORY_PICTURES . Эти имена директорий гарантируют, что система будет правильно рассматривать эти файлы. Например файлы, сохраненные в DIRECTORY_RINGTONES , будут рассортированы медиасканером системы как рингтоны вместо музыки.
[Опрос количества свободного места]
Если Вы знаете заранее, сколько файлов сохраняете, то можете без получения ошибок IOException узнать, сколько места осталось путем вызова getFreeSpace() или getTotalSpace() . Эти методы предоставляют соответственно текущее доступное пространство и общее пространство на томе хранения. Эта информация также полезна, чтобы избежать переполнения тома хранения свыше определенного порога.
Однако система не гарантирует, что Вы можете записать столько байт, сколько показывает вызов getFreeSpace(). Если возвращенное количество всего на несколько мегабайт больше, чем Вам нужно сохранить, или если файловая система уже заполнена меньше, чем на 90%, то вероятно сохранение будет безопасным. Иначе возможно, что записать данные в хранилище не получится.
Внимание: Вам не обязательно проверять количество свободного места перед сохранения файла. Вместо этого Вы можете попробовать записать файл сразу же, и затем перехватить исключение IOException, если оно произойдет. Вы возможно, должны так поступить, когда не знаете, сколько места Вам нужно. Например, если Вы меняете способ кодирования файла перед его сохранением, преобразовывая картинку PNG в JPEG, то Вы не будете знать размер файла заранее.
[Удаление файла]
Вы всегда должны удалять файлы, которые Вам больше не нужны. Самый прямой способ удаления файла состоит в том, чтобы иметь этот файл открытым и вызвать delete() для самого себя.
myFile.delete();
Если файл сохранен на internal storage, Вы можете также запросить Context, чтобы найти и удалить файл вызовом deleteFile():
myContext.deleteFile(fileName);
Внимание: когда пользователь деинсталлирует Ваше приложение, система Android удалит следующее:
• Все файлы, сохраненные Вашим приложением на internal storage.
• Все файлы, сохраненные Вашим приложением с использованием getExternalFilesDir().
Однако Вы должны регулярно удалять все кэшируемые файлы, создаваемые с getCacheDir(), и также регулярно удалять файлы, которые Вам больше не нужны.
[Пример записи файла на sdcard0]
Предположим, что необходимо записать какой-нибудь тестовый файл (с именем myFile.txt) в папку myFolder на внешний носитель, который виден в системе Android как sdcard0. Т. е. полный путь должен выглядеть примерно так:
Базовый путь до External Storage
Проблема тут состоит в том, чтобы узнать часть пути basePath, поскольку на разных системах Android этот путь будет разным, в зависимости от версии и внутреннего аппаратного устройства. В моем телефоне Samsung Galaxy Note этот basePath = /storage/sdcard0 , но это еще не значит, что на Вашем телефона этот путь будет именно таким. Чтобы получить basePath, используйте вызов функции getExternalStorageDirectory :
//Вызов getExternalStorageDirectory вернет путь до "внешнего" (External) // носителя, например /storage/sdcard0 String basePath = Environment.getExternalStorageDirectory().getAbsolutePath();
Проверка доступности носителя данных в External Storage
Вторая проблема состоит в доступности на запись носителя данных. Дело в том, что записать на носитель можно не всегда, например если он смонтирован как флешка USB (когда Ваш телефон подключен к компьютеру в режиме Mass Storage Device, USB MSD). Проверить доступность носителя можно следующей функцией:
/* Проверяет, доступно ли external storage для чтения и записи */ public boolean isExternalStorageWritable() String state = Environment.getExternalStorageState(); if (Environment.MEDIA_MOUNTED.equals(state)) return true; > return false; >
Разрешение доступа к носителю данных в файле манифеста
Как уже упоминалось, необходимо в файле манифеста запросить разрешение WRITE_EXTERNAL_STORAGE . Вот пример такого файла манифеста:
< manifestxmlns:android="http://schemas.android.com/apk/res/android" package="com.findusbdevbr" android:versionCode="1" android:versionName="1.0" > < uses-featureandroid:name="android.hardware.usb.host" /> < uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> < uses-sdkandroid:minSdkVersion="13" android:targetSdkVersion="19" /> < applicationandroid:allowBackup="true" android:icon="@drawable/ic_launcher" android:label="@string/app_name" android:theme="@style/AppTheme" > < activityandroid:name="com.findusbdevbr.MainActivity" android:label="@string/app_name" > < actionandroid:name="android.intent.action.MAIN" /> < categoryandroid:name="android.intent.category.LAUNCHER" /> < /intent-filter > < /activity > < /application > < /manifest >
Функция, которая сохраняет файл, принимая полный путь до файла filePath и сохраняемый текст FileContent:
public void SaveFile (String filePath, String FileContent) //Создание объекта файла. File fhandle = new File(filePath); try //Если нет директорий в пути, то они будут созданы: if (!fhandle.getParentFile().exists()) fhandle.getParentFile().mkdirs(); //Если файл существует, то он будет перезаписан: fhandle.createNewFile(); FileOutputStream fOut = new FileOutputStream(fhandle); OutputStreamWriter myOutWriter = new OutputStreamWriter(fOut); myOutWriter.write(FileContent); myOutWriter.close(); fOut.close(); > catch (IOException e) //e.printStackTrace(); textInfo.setText("Path " + filePath + ", " + e.toString()); > >
Вызов функции SaveFile, который выполняет задачу сохранения файла в External-носителе:
public void btnClick(View v) String fullpath, foldername, filename; foldername = "temp/myFolder"; filename = "myFile.txt"; //Сохранение файла на External Storage: fullpath = Environment.getExternalStorageDirectory().getAbsolutePath() + "/" + foldername + "/" + filename; if (isExternalStorageWritable()) SaveFile(fullpath, "Этот текст сохранен на External Storage"); > >
[Пример записи файла на extSdCard]
Получение полного корневого пути до извлекаемой карты SD не так прост, как до External Storage, поскольку в API Android для этого почему-то не предусмотрены специальные простые функции. Приходится получать путь окольными путями, через имена системных папок. Вот код функции, которая получает путь до извлекаемой карты SD:
//Функция определяет путь до внешней извлекаемой карты // наподобие /storage/extSdCard private String getSDcardPath() String exts = Environment.getExternalStorageDirectory().getPath(); String sdCardPath = null; try FileReader fr = new FileReader(new File("/proc/mounts")); BufferedReader br = new BufferedReader(fr); String line; while((line = br.readLine())!=null) if(line.contains("secure") || line.contains("asec")) continue; if(line.contains("fat")) String[] pars = line.split("\\s"); if(pars.length <2)continue; if(pars[1].equals(exts)) continue; sdCardPath =pars[1]; break; > > fr.close(); br.close(); return sdCardPath; > catch (Exception e) //e.printStackTrace(); textInfo.setText(e.toString()); > return sdCardPath; >
Вызов функции SaveFile, который выполняет задачу сохранения файла на извлекаемой карте SD:
public void btnCheckUsbDevClick(View v) String fullpath, foldername, filename; foldername = "temp/myFolder"; filename = "myFile.txt"; //Сохранение файла на карту SD: fullpath = getSDcardPath() + "/" + foldername + "/" + filename; SaveFile(fullpath, "Этот текст сохранен на карту SD"); >
[Сохранение бинарного файла (массива byte[])]
В предыдущих примерах мы рассматривали класс OutputStreamWriter , который позволяет записать строку String или массив символов char[]. Но как быть, если нужно записать массив байт byte[]? Для этого подойдет класс DataOutputStream . Пример:
public static void SaveBin (byte[] buf, int count) String foldername = "temp/myfolder"; String filename = "myfile.bin"; String fullpath = fileutil.getSDcardPath() + "/" + foldername + "/" + filename; //Создание объекта файла. File fhandle = new File(fullpath); try //Если нет директорий в пути, то они будут созданы: if (!fhandle.getParentFile().exists()) fhandle.getParentFile().mkdirs(); //Если файл существует, то он будет перезаписан: fhandle.createNewFile(); FileOutputStream fOut = new FileOutputStream(fhandle); DataOutputStream myOutWriter = new DataOutputStream(fOut); myOutWriter.write(buf, 0, count); myOutWriter.flush(); myOutWriter.close(); fOut.close(); > catch (IOException e) Toast.makeText(context, "SaveFile: path " + fullpath + ", " + e.toString(), Toast.LENGTH_LONG).show(); > >
Примеры вызовов getAbsolutePath:
| Вызов | Результат вызова |
| Environment.getRootDirectory.getAbsolutePath() | /system |
| Environment.getExternalStorageDirectory().getAbsolutePath() | /storage/sdcard0 |
| Environment.getExternalStoragePublicDirectory(null).getAbsolutePath() | завершится с ошибкой |
| Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_ALARMS).getAbsolutePath() | /storage/sdcard0/Alarms |
[Ссылки]
1. Saving Files site:developer.android.com .
2. App Install Location site:developer.android.com .