Parcelable kotlin для чего
Возможность сериализации объектов предоставляется напрямую инфраструктурой языка Java. Однако Android также предоставляет интерфейс Parcelable , который по сути также позволяет сериализовать объекты, как и Serializable, но является более оптимизированным для Android. И подобные объекты Parcelable также можно передавать между двумя activity или использовать каким-то иным образом.
Например, в прошлой теме данные передавались между activity в виде объектов User, которые использовали сериализацию. Теперь пусть класс User применяет интерфейс Parcelable:
package com.example.viewapp; import android.os.Parcel; import android.os.Parcelable; public class User implements Parcelable < private String name; private String company; private int age; public static final CreatorCREATOR = new Creator() < @Override public User createFromParcel(Parcel source) < String name = source.readString(); String company = source.readString(); int age = source.readInt(); return new User(name, company, age); >@Override public User[] newArray(int size) < return new User[size]; >>; public User(String name, String company, int age) < this.name = name; this.company = company; this.age = age; >public String getName() < return name; >public void setName(String name) < this.name = name; >public String getCompany() < return company; >public void setCompany(String company) < this.company = company; >public int getAge() < return age; >public void setAge(int age) < this.age = age; >@Override public int describeContents() < return 0; >@Override public void writeToParcel(Parcel dest, int flags) < dest.writeString(name); dest.writeString(company); dest.writeInt(age); >>
Интерфейс android.os.Parcelable предполагает реализацию двух методов: describeContents() и writeToParcel() . Первый метод описывает контент и возвращает некторое числовое значение. Второй метод пишет в объект Parcel содержимое объекта User.
Для записи данных объекта в Parcel используется ряд методов, каждый из которых предназначен для определенного типа данных. Основные методы:
- writeString()
- writeInt()
- writeFloat()
- writeDouble()
- writeByte()
- writeLong()
- writeIntArray()
- writeValue() (записывает объект типа Object)
- writeParcelable() (записывает объект типа Parcelable)
Кроме того, объект Parcelable должен содержать статическое поле CREATOR, которое представляет объект Creator . Причем этот объект реализует два метода. Они нужны для создания их ранее сериализованных данных исходных объектов типа User.
Так, метод newArray() создает массив объект User.
Метод createFromParcel создает из Parcel новый объект типа User. То есть этот метод противоположен по действию методу writeToParcel. Для получения данных из Parcel применяются методы типа readString() , readInt() , readParcelable() и так далее — для чтения определенных типов данных.
Причем важно, что данные в createFromParcel считываются из объекта Parcel именно в том порядке, в котором они добавляются в этот объект в методе writeToParcel.
Допустим в activity, которая называется SecondActivity мы будем получать объект User:
package com.example.viewapp; import androidx.appcompat.app.AppCompatActivity; import android.os.Bundle; import android.widget.TextView; public class SecondActivity extends AppCompatActivity < @Override protected void onCreate(Bundle savedInstanceState) < super.onCreate(savedInstanceState); TextView textView = new TextView(this); textView.setTextSize(26); textView.setPadding(16, 16, 16, 16); Bundle arguments = getIntent().getExtras(); User user; if(arguments!=null)< user = arguments.getParcelable(User.class.getSimpleName()); textView.setText("Name: " + user.getName() + "\nCompany: " + user.getCompany() + "\nAge: " + String.valueOf(user.getAge())); >setContentView(textView); > >
Для получения объекта Parcelable, переданного в activity, применяется метод getParcelable() . Причем никакого приведения типов не требуется.
Для тестирования передачи Parcelable определим в файле activity_main.xml простейший интерфейс для MainActivity:
А в коде MainActivity определим передачу данных в SecondActivity:
package com.example.viewapp; import androidx.appcompat.app.AppCompatActivity; import android.content.Intent; import android.os.Bundle; import android.view.View; import android.widget.EditText; public class MainActivity extends AppCompatActivity < @Override protected void onCreate(Bundle savedInstanceState) < super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); >public void onClick(View v) < EditText nameText = findViewById(R.id.name); EditText companyText = findViewById(R.id.company); EditText ageText = findViewById(R.id.age); String name = nameText.getText().toString(); String company = companyText.getText().toString(); int age = Integer.parseInt(ageText.getText().toString()); User user = new User(name, company, age); Intent intent = new Intent(this, SecondActivity.class); intent.putExtra(User.class.getSimpleName(), user); startActivity(intent); >>
Parcelable. Передаём объекты

Очень часто программисту приходится передавать данные из одной активности в другую. Когда речь идёт о простых типах, то используется метод intent.putExtra() и ему подобные. Данный способ годится для типов String, int, long или массивов. А вот объекты таким образом передать не получится.
В Java существует специальный интерфейс Serializable, но он оказался слишком неповоротлив для мобильных устройств и пользоваться им настоятельно не рекомендуется. Для передачи объектов следует использовать другой интерфейс Parcelable.
В интерфейсе Parcelable используются два метода describeContents() и writeToParcel():
@Override public int describeContents() < >@Override public void writeToParcel(Parcel dest, int flags)
Метод describeContents() описывает различного рода специальные объекты, описывающие интерфейс.
Метод writeToParcel() упаковывает объект для передачи.
Также необходимо реализовать статическое поле Parcelable.Creator CREATOR, которое генерирует объект класса-передатчика.
Напишем пример. Попробуем передать объект «Мои документы». Как известно, признаком настоящего документа являются Усы, лапы, хвост (я ещё добавил бы имя). Создадим новый класс DocumentInfo, который реализует интерфейс Parcelable:
package ru.alexanderklimov.parcelable; import android.os.Parcel; import android.os.Parcelable; public class DocumentInfo implements Parcelable < private String mName; // имя кота private String mWhiskers; // усы private String mPaws; // лапы private String mTail; // хвост public DocumentInfo(String name, String whiskers, String paws, String tail) < mName = name; mWhiskers = whiskers; mPaws = paws; mTail = tail; >public DocumentInfo(Parcel in) < String[] data = new String[4]; in.readStringArray(data); mName = data[0]; mWhiskers = data[1]; mPaws = data[2]; mTail = data[3]; >public void setCatName(String name) < mName = name; >public String getCatName() < return mName; >public void setWhiskers(String whiskers) < mWhiskers = whiskers; >public String getWhiskers() < return mWhiskers; >public String getPaws() < return mPaws; >public String getTail() < return mTail; >@Override public int describeContents() < return 0; >@Override public void writeToParcel(Parcel dest, int flags) < dest.writeStringArray(new String[] < mName, mWhiskers, mPaws, mTail >); > public static final Parcelable.Creator CREATOR = new Parcelable.Creator() < @Override public DocumentInfo createFromParcel(Parcel source) < return new DocumentInfo(source); >@Override public DocumentInfo[] newArray(int size) < return new DocumentInfo[size]; >>; >
Теперь мы может передать объект через Intent. Создадим две активности. В первой активности напишем код для отправки объекта:
public void onClick(View view)
Вторая активность должна принять данные от первой активности:
@Override protected void onCreate(Bundle savedInstanceState)
Получив объект из первой активности, мы можем извлечь необходимые данные и разложить по полочкам.

Если класс слишком большой, то вручную его переделывать утомительно. Существует онлайн-сервис parcelabler а также плагин для Android Studio mcharmas/android-parcelable-intellij-plugin: IntelliJ Plugin for Android Parcelable boilerplate code generation.
Урок 69. Передаем Parcelable объекты с помощью Intent
С Parcel мы немного поработали на прошлом уроке. Этих знаний хватит, чтобы понять, как реализовать в своем объекте интерфейс Parcelable. Создадим свой объект, реализуем в нем интерфейс Parcelable и попробуем передать в другое Activity через Intent.
Project name: P0691_Parcelable
Build Target: Android 2.3.3
Application name: Parcelable
Package name: ru.startandroid.develop.p0691parcelable
Create Activity: MainActivity
В strings.xml пропишем тексты:
MainActivity SecondActivity Send Parcelable
В main.xml нарисуем кнопку:
Перед тем как кодить MainActivity.java, создадим свой объект для передачи MyObject.java:
package ru.startandroid.develop.p0691parcelable; import android.os.Parcel; import android.os.Parcelable; import android.util.Log; public class MyObject implements Parcelable < final static String LOG_TAG = "myLogs"; public String s; public int i; // обычный конструктор public MyObject(String _s, int _i) < Log.d(LOG_TAG, "MyObject(String _s, int _i)"); s = _s; i = _i; >public int describeContents() < return 0; >// упаковываем объект в Parcel public void writeToParcel(Parcel parcel, int flags) < Log.d(LOG_TAG, "writeToParcel"); parcel.writeString(s); parcel.writeInt(i); >public static final Parcelable.Creator CREATOR = new Parcelable.Creator() < // распаковываем объект из Parcel public MyObject createFromParcel(Parcel in) < Log.d(LOG_TAG, "createFromParcel"); return new MyObject(in); >public MyObject[] newArray(int size) < return new MyObject[size]; >>; // конструктор, считывающий данные из Parcel private MyObject(Parcel parcel) < Log.d(LOG_TAG, "MyObject(Parcel parcel)"); s = parcel.readString(); i = parcel.readInt(); >>
Объект сам по себе несложный: пара переменных s и i, и конструктор. Все остальное используется для реализации Parcelable. Давайте смотреть.
Про метод describeContents ничего сказать не могу. Я не понял, зачем он нужен.
В методе writeToParcel мы получаем на вход Parcel и упаковываем в него наш объект. Т.е., в нашем случае, помещаем туда переменные s и i. flags не используем.
Для этого используется его метод createFromParcel, который мы должны реализовать. На вход нам дается Parcel, а вернуть мы должны готовый MyObject. В нашем примере мы используем здесь конструктор MyObject(Parcel parcel), который реализован чуть дальше.
Смысл метода newArray остался для меня непонятен.
Конструктор MyObject(Parcel parcel) принимает на вход Parcel и заполняет объект данными из него. Этот метод использовался нами чуть ранее в CREATOR.createFromParcel.
Создадим второе Activity, в которое будем передавать объект.
Сначала создаем экран second.xml:
Кодим SecondActivity.java:
package ru.startandroid.develop.p0691parcelable; import android.app.Activity; import android.os.Bundle; import android.util.Log; public class SecondActivity extends Activity < final String LOG_TAG = "myLogs"; protected void onCreate(Bundle savedInstanceState) < super.onCreate(savedInstanceState); setContentView(R.layout.second); Log.d(LOG_TAG, "getParcelableExtra"); MyObject myObj = (MyObject) getIntent().getParcelableExtra( MyObject.class.getCanonicalName()); Log.d(LOG_TAG, "myObj: " + myObj.s + ", " + myObj.i); >>
Мы вытаскиваем наш MyObject-объект из Intent и в лог выводим значения s и i.
Кодим MainActivity.java:
package ru.startandroid.develop.p0691parcelable; import android.app.Activity; import android.content.Intent; import android.os.Bundle; import android.util.Log; import android.view.View; public class MainActivity extends Activity < final String LOG_TAG = "myLogs"; /** Called when the activity is first created. */ public void onCreate(Bundle savedInstanceState) < super.onCreate(savedInstanceState); setContentView(R.layout.main); >public void onclick(View v) < MyObject myObj = new MyObject("text", 1); Intent intent = new Intent(this, SecondActivity.class); intent.putExtra(MyObject.class.getCanonicalName(), myObj); Log.d(LOG_TAG, "startActivity"); startActivity(intent); >>
Создаем Intent, помещаем туда объект MyObject. В качестве ключа используем его имя класса (разумеется, это необязательно, вы можете свое имя использовать). И отправляем Intent с вызовом SecondActivity.
Все сохраним и запустим приложение.
Жмем Send, Intent уходит в SecondActivity
MyObject(String _s, int _i)
startActivity
writeToParcel
getParcelableExtra
createFromParcel
MyObject(Parcel parcel)
myObj: text, 1
Сначала вызвался конструктор MyObject(String _s, int _i) – это мы создали myObj.
startActivity – начинаем вызов Activity
writeToParcel — мы поместили объект в Intent, и похоже, что при отправке он упаковался в Parcel. Т.к. сам Parcel не знает, как именно упаковать объект, он вызвал метод writeToParcel, где мы реализовали упаковку.
getParcelableExtra – извлекаем объект из Intent
createFromParcel – это был вызван метод CREATOR.createFromParcel, которому на вход дали Parcel, а он должен вернуть MyObject. Этот метод в свою очередь для создания MyObject использует конструктор MyObject(Parcel parcel), в котором мы расписали, как надо читать Parcel и заполнить объект.
myObj: text, 1 – вывели в лог значения объекта.
Итак. Чтобы нам передать объект через Intent, нам надо реализовать в нем интерфейс Parcelable. В этом случае Intent без проблем запакует, передаст и распакует наш объект. И я так подозреваю, что делает он это с помощью Parcel. Т.е. в реализации интерфейса Parcelable мы полностью описываем алгоритм упаковки и распаковки объекта, а Parcel эти алгоритмы потом использует. Т.к. сам он не может знать, как правильно распаковать и создать передаваемый объект.
Если кто разберется, зачем нужны непонятые мною методы – пишите на форуме в ветке этого урока. Я добавлю вашу инфу в урок.
На следующем уроке:
— сохраняем данные при повороте экрана
Присоединяйтесь к нам в Telegram:
— в канале StartAndroid публикуются ссылки на новые статьи с сайта startandroid.ru и интересные материалы с хабра, medium.com и т.п.
— в чатах решаем возникающие вопросы и проблемы по различным темам: Android, Compose, Kotlin, RxJava, Dagger, Тестирование, Performance
— ну и если просто хочется поговорить с коллегами по разработке, то есть чат Флудильня
Пишем плагин Parcelize для компилятора Kotlin под iOS

В этой статье описан мой опыт по написанию плагина для компилятора Kotlin. Моей главной целью было создание плагина под iOS (Kotlin/Native), аналогичного kotlin-parcelize под Android. Дело в том, что в iOS, как и в Android, приложения тоже могут быть убиты системой, а значит, может возникнуть необходимость сохранять стек навигации и другие данные. В результате работы над этой задачей получился kotlin-parcelize-darwin. Подробности о его создании и применении — под катом.
Parcelize в Android
Хотя в статье будет описана разработка под iOS, давайте вспомним, что собой представляют интерфейс Parcelable и плагин kotlin-parcelize под Android. Интерфейс Parcelable позволяет нам сериализовать в Parcel реализующий класс, чтобы он был представлен в виде массива байтов. Также он позволяет десериализовать класс из Parcel для восстановления всех данных. Эта возможность широко используется для записи и восстановления состояний экрана, например когда приостановленное приложение сначала убивается системой из-за нехватки памяти, а затем снова активируется.
Реализовать интерфейс Parcelable не сложно. Нужно написать два основных метода:
- writeToParcel(Parcel, …) — пишет данные в Parcel,
- createFromParcel(Parcel) — читает из Parcel.
Необходимо записывать информацию поле за полем, а затем считывать в том же порядке. Звучит довольно просто, но писать шаблонный код надоедает. Кроме того, можно наделать ошибок, так что в идеале нужно писать ещё и тесты для классов Parcelable.
К счастью, для компилятора Kotlin есть плагин kotlin-parcelize. Если его включить, то достаточно лишь пометить класс Parcelable с помощью аннотации @Parcelize — и плагин автоматически сгенерирует реализацию. Это избавляет нас от написания соответствующего шаблонного кода и гарантирует корректность реализации на этапе компилирования.
Применение Parcelize в iOS
Поскольку iOS-приложения тоже имеют вышеупомянутые особенности, есть аналогичные способы сохранения состояния приложения. Один из них заключается в использовании протокола NSCoding, который очень похож на интерфейс Parcelable в Android. Классы тоже должны реализовать два метода:
- encode(with: NSCoder)— кодирует объект в NSCoder,
- init?(coder: NSCoder)— декодирует объект из NSCoder.
Kotlin Native под iOS
Kotlin не ограничен Android — его можно использовать для написания Kotlin Native-фреймворков под iOS и даже мультиплатформенного кода. А поскольку iOS-приложения тоже могут быть убиты системой, то с ними возникают те же проблемы. Kotlin Native под iOS предлагает двунаправленную совместимость с Objective-C, то есть мы можем использовать NSCoding и NSCoder.
Очень простой класс данных может выглядеть так:

Добавим реализацию протокола NSCoding:

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

Попробуем расширить класс NSObject с помощью класса данных User:

И опять не компилируется!

Интересно. Похоже, компилятор пытается переопределить и сгенерировать метод toString, но для классов, наследующихся от NSObject, нам нужно переопределить метод description. Кроме того, нам, вероятно, вообще не стоит использовать наследование, потому что это может помешать пользователям расширять их собственные классы Kotlin (из-за невозможности множественного наследования).
Parcelable для iOS
Нам нужно другое решение, без использования наследования. Определим интерфейс Parcelable:

Всё просто. Классы Parcelable будут иметь только метод coding, который возвращает экземпляр NSCodingProtocol. Остальное будет обработано реализацией протокола.
Теперь давайте изменим класс User таким образом, чтобы он реализовал интерфейс Parcelable:

Мы создали вложенный класс CodingImpl, который реализует протокол NSCoding. Метод encodeWithCoder остался неизменным, а вот с initWithCoder ситуация чуть сложнее. Он должен возвращать экземпляр протокола NSCoding, но класс User теперь им не является. Нам нужно какое-то обходное решение, промежуточный класс:

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

Тестирование
Давайте напишем тест, чтобы проверить, всё ли работает правильно.
- Создаём экземпляр класса User с какими-нибудь данными.
- Кодируем его с помощью NSKeyedArchiver, в качестве результата получаем NSData.
- Декодируем NSData с помощью NSKeyedUnarchiver.
- Убеждаемся, что декодированный объект аналогичен исходному.
Пишем плагин для компилятора
Мы определили интерфейс Parcelable под iOS, попробовали его в работе с помощью класса User и протестировали код. Теперь можно автоматизировать реализацию Parcelable, чтобы код генерировался автоматически, как при использовании kotlin-parcelize под Android.
Мы не можем использовать Kotlin Symbol Processing (KSP), потому что это не позволит нам менять существующие классы, а только генерировать новые. Так что единственным решением будет написать плагин для компилятора Kotlin. Но это не так просто, в основном потому, что документации до сих пор нет, API работает нестабильно и т. д. Если вы всё же соберётесь писать плагин для компилятора, то рекомендую обратиться к этим источникам:
- «Волшебство расширений для компилятора» — выступление Андрея Шикова,
- “Writing Your Second Kotlin Compiler Plugin” — статья Брайана Нормана.
Плагин работает так же, как kotlin-parcelize. Классы должны реализовывать интерфейс Parcelable и быть помечены с помощью аннотации @Parcelize. При компилировании плагин генерирует реализации Parcelable. Классы Parcelable выглядят так:

Название плагина
Плагин называется kotlin-parcelize-darwin. Часть “-darwin” означает, что плагин должен работать со всеми платформами Darwin (Apple), но сейчас нас интересует только iOS.
Gradle-модули
- Kotlin-parcelize-darwin — первый модуль, который нам понадобится. Он содержит плагин для Gradle, который регистрирует плагин для компилятора, и ссылается на два артефакта: один — для плагина компилятора Kotlin/Native, второй — для плагина компилятора под все другие платформы.
- kotlin-parcelize-darwin-compiler — модуль плагина для компилятора Kotlin/Native.
- kotlin-parcelize-darwin-compiler-j — модуль плагина для ненативного компилятора. Он необходим, потому что является обязательным и на него ссылается Gradle-плагин. Хотя на самом деле этот модуль пустой, поскольку нам ничего не нужно из ненативного варианта.
- kotlin-parcelize-darwin-runtime — содержит зависимости времени выполнения (runtime) для компиляторного плагина. Например, здесь находятся интерфейс Parcelable и аннотация @Parcelize.
- tests — содержит тесты для компиляторного плагина, добавляет в плагин модули в виде included builds.
Процесс установки плагина
В корневом файле build.gradle:

В файле build.gradle проекта:

Реализация
Генерирование кода в Parcelable состоит из двух основных этапов. Нам нужно:
- Сделать код компилируемым с помощью добавления синтетических заглушек для отсутствующих методов fun coding(): NSCodingProtocol из интерфейса Parcelable.
- Сгенерировать реализации для заглушек, добавленных на предыдущем этапе.
Добавление заглушек
Это делается с помощью класса ParcelizeResolveExtension, который реализует интерфейс SyntheticResolveExtension. Всё очень просто: класс реализует методы getSyntheticFunctionNames и generateSyntheticMethods, которые вызываются при компилировании для каждого класса.

Как видите, сначала нужно проверить, можно ли применять логику Parcelize к текущему классу. Для этого используется функция isValidForParcelize:

Мы обрабатываем только те классы, у которых есть аннотация @Parcelize и которые реализуют интерфейс Parcelable.
Генерирование реализаций заглушек
Как вы могли догадаться, этот этап создания плагина значительно сложнее. За него отвечает класс ParcelizeGenerationExtension, который реализует интерфейс IrGenerationExtension. Он содержит всего один метод:

Нам необходимо пройтись по всем классам, содержащимся в предоставленном нам IrModuleFragment. Для этого используется класс ParcelizeClassLoweringPass, который наследует ClassLoweringPass. Класс ParcelizeClassLoweringPass переопределяет только один метод:

Проходить по классам не сложно:

Помимо этих основных, в процессе генерации кода есть ещё несколько этапов. Я не буду описывать все подробности реализации, потому что тогда придётся привести слишком много кода. Вместо этого я в общих чертах расскажу об основных вызовах и покажу, как выглядел бы сгенерированный код, если бы мы писали его вручную. Думаю, в контексте статьи это более полезная информация. Но если вам хочется узнать больше, то подробности реализации вы найдёте здесь.
Итак, сначала нам снова нужно проверить, можно ли применять логику Parcelize к текущему классу (irClass):

Затем добавим в irClass вложенный класс CodingImpl, определим его супертипы (NSObject и NSCoding) и пометим аннотацией @ExportObjCClass (чтобы класс был доступен при поиске во время выполнения):

Теперь добавим в класс CodingImpl первичный конструктор. У него должен быть только один аргумент — data: TheClass, поэтому нам также надо сгенерировать поле (field) data, свойство (property) и метод считывания (getter).

Вот что у нас получается:

Добавим реализацию протокола NSCoding:

Теперь сгенерированный класс выглядит так:

Наконец нам нужно сгенерировать тело метода coding(), просто создав экземпляр класса CodingImpl:


Использование плагина
Плагин задействуется, когда мы пишем на Kotlin классы Parcelable. Обычно его используют для сохранения состояний экрана. Плагин позволяет восстанавливать исходное состояние приложения после того, как оно было убито iOS. Другой сценарий использования — сохранение стека навигации в тех случаях, когда она реализована на Kotlin.
Вот обобщённый пример использования Parcelable в Kotlin, который демонстрирует, как можно сохранить и восстановить данные:

А вот пример того, как мы можем кодировать и декодировать классы Parcelable в iOS-приложении:

Parcelize в Kotlin Multiplatform
Теперь у нас есть два плагина: kotlin-parcelize для Android и kotlin-parcelize-darwin — для iOS. И мы можем применить их оба и использовать @Parcelize в общем коде!
Файл build.gradle нашего общего модуля будет выглядеть так:

Теперь у нас в наборах androidMain и iosMain есть доступ к интерфейсам Parcelable и аннотациям @Parcelize. Чтобы использовать их в commonMain, нам нужно вручную определить их с помощью expect/actual.
Напишем в commonMain:



Во всех остальных наборах:

Теперь мы можем использовать Parcelize как обычно в commonMain. При сборке под Android код будет обработан плагином kotlin-parcelize, а при сборке под iOS — плагином kotlin-parcelize-darwin. В случае с другими платформами ничего не будет сделано, потому что интерфейс Parcelable будет пуст, а аннотация будет отсутствовать.
Заключение
Мы рассмотрели компиляторный плагин kotlin-parcelize-darwin. Исследовали его структуру и принцип работы, узнали, как его можно применять в Kotlin Native, как подружить его с Android-плагином kotlin-parcelize в Kotlin Multiplatform, а также как использовать Parcelable на стороне iOS.
Исходный код лежит на GitHub. Плагин ещё не опубликован, но вы уже можете с ним экспериментировать, опубликовав в локальном Maven-репозитории или с помощью Gradle Composite builds.
В репозитории лежит очень простой пример проекта, в котором есть общий модуль, а также Android- и iOS-приложения. Спасибо, что дочитали, и не забудьте подписаться на меня в Twitter!
- Блог компании Badoo
- Программирование
- Разработка под iOS
- Разработка под Android
- Kotlin