Разработка приложений на Kotlin под Android набирает популярность среди разработчиков, однако статей в русскоязычном сегменте Интернета довольно мало. Я решил немного подправить ситуацию, и написать туториал по разработке приложения на Kotlin. Мы напишем полноценное приложение с использованием всех трендовых библиотек (кроме RxJava) в мире Android-разработки. В конце у нас должно получиться расширяемое и легко тестируемое приложение (сами тесты мы писать не будем).

Наверное, некоторые из вас знают, что помимо языка программирования Kotlin JetBrains также разрабатывает библиотеку Anko, для создания UI приложения, в качестве замены обычным XML-файлам. Мы не будем использовать его в нашем проекте, дабы не ставить в затруднительное положение людей не знакомых с Anko.

Внимание: якори в содержании почему-то не работают. Если знаете как решить проблему, напишите в ЛС, пожалуйста.

Содержание:



Настройка Android Studio



Для написания приложений на языке Kotlin, Android Studio нужен специальный плагин. Инструкцию по установке плагина можно найти здесь. Также не забудьте отключить функцию «Instant Run» в настройках Android Studio, т. к. на данный момент она не поддерживается плагином Kotlin.

Для корректной работы генерации кода нужно использовать версию плагина не ниже 1.0.1. Я использовал версию Kotlin 1.0.2 EAP. Вот так выглядит файл build.gradle приложения в моем проекте:

apply plugin: 'com.android.application'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-android'

android {
    compileSdkVersion 23
    buildToolsVersion "23.0.2"

    defaultConfig {
        applicationId "imangazaliev.notelin"
        minSdkVersion 15
        targetSdkVersion 23
        versionCode 1
        versionName "1.0"
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
    sourceSets {
        main.java.srcDirs += 'src/main/kotlin'
        androidTest.java.srcDirs += 'src/androidTest/kotlin'
    }
}

dependencies {
    ...
}

kapt {
    generateStubs = true
}

buildscript {
    ext.kotlin_version = '1.0.2-eap-15'
    repositories {
        mavenCentral()
        maven { url 'https://dl.bintray.com/kotlin/kotlin-eap' }
    }
    dependencies {
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
    }
}
repositories {
    mavenCentral()
}



Что будем писать?



Итак, для начала нам нужно определиться что же мы будем писать? Недолго думая я остановился на приложении-заметках. Название тоже придумалось легко — Notelin. Приложение очень простое и состоит из двух экранов:

— Главный экран — содержит в себе список с заметками
— Экран заметки — здесь можно смотреть/редактировать содержание выбранной заметки

Требования к приложению небольшие:

— Добавление/просмотр/удаление заметки
— Просмотр информации о заметке
— Сортировка заметок по заголовку и по дате
— Поиск по заголовкам заметок

Используемые библиотеки



Для работы с базой данных я буду использовать библиотеку Android Active. Урок по работе с ней можно найти по этой ссылке. Для реализации Depency Injection была использована библиотека Dagger 2. На Хабре есть много статей по работе с ней. Основой всего приложения будет библиотека Moxy. С ее помощью мы реализуем паттерн MVP в нашем проекте. Она полностью решает проблемы жизненного цикла, благодаря чему вы можете не переживать о пересоздании компонентов вашего приложения. Также мы воспользуемся набором расширений для языка Kotlin в Android — KAndroid. Про остальные библиотеки я буду рассказывать по ходу дела.

Ниже приведен список зависимостей проекта:

allprojects {
    repositories {
        jcenter()
        mavenCentral()
        maven { url "https://oss.sonatype.org/content/repositories/snapshots/" }
        maven { url "https://mvn.arello-mobile.com/" }
        maven { url "https://jitpack.io" }
        maven { url 'https://dl.bintray.com/kotlin/kotlin-eap' }
    }
}


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

dependencies {
    compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
    compile "com.android.support:appcompat-v7:23.1.1"
    compile 'com.android.support:recyclerview-v7:23.1.1'
    compile 'com.android.support:cardview-v7:23.1.1'
    //дополнительные возможности для Android Kotlin
    compile 'com.pawegio.kandroid:kandroid:0.5.0@aar'
    //ActiveAndroid DB
    compile 'com.michaelpardo:activeandroid:3.1.0-SNAPSHOT'
    //FAB
    compile 'com.melnykov:floatingactionbutton:1.3.0'
    //MaterialDialog
    compile 'com.github.afollestad.material-dialogs:core:0.8.5.6@aar'
    //MVP
    compile 'com.arello-mobile:moxy:0.4.2'
    compile 'com.arello-mobile:moxy-android:0.4.2'
    kapt 'com.arello-mobile:moxy-compiler:0.4.2'
    //RX
    compile 'io.reactivex:rxjava:1.1.0'
    compile 'io.reactivex:rxandroid:1.1.0'
    //Depency Injection
    kapt 'com.google.dagger:dagger-compiler:2.0.2'
    compile 'com.google.dagger:dagger:2.0.2'
    provided 'org.glassfish:javax.annotation:10.0-b28'
    //EventBus
    compile 'org.greenrobot:eventbus:3.0.0'
}


Обратите внимание, что вместо apt я использую kapt. Это плагин для Gradle, позволяющий аннотировать Kotlin-элементы.

Структура приложения



Вот так выглядит структура нашего проекта в конечном варианте:



Создаем Model



У заметок будет четыре поля:

  • Дата создания
  • Дата изменения
  • Заголовок
  • Текст


Реализуем все это в коде:

class Note : Model {

    @Column(name = "title")
    public var title: String? = null
    @Column(name = "text")
    public var text: String? = null
    @Column(name = "create_date")
    public var createDate: Date? = null
    @Column(name = "change_date")
    public var changeDate: Date? = null

    constructor(title: String, createDate: Date, changeDate: Date) {
        this.title = title
        this.createDate = createDate
        this.changeDate = changeDate
    }

    constructor()

    fun getInfo(): String = "Название:\n$title\n" +
                                       "Время создания:\n${DateUtils.formatDate(createDate)}\n" +
                                       "Время изменения:\n${DateUtils.formatDate(changeDate)}";
}


По этой модели библиотекой ActiveAndroid будет создана БД, в которой будут храниться наши заметки. Если вы заметили, у нас есть два конструктора: пустой и с параметрами. Первый конструктор будем использовать мы, а второй — ActiveAndroid. Наша модель наследуется от класса Model, благодаря чему мы можем сохранять и удалять наши заметки просто вызывая методы save() и delete(), например:

var note = Note("Новая заметка", Date())
note.save()
...
note.delete()


Но прежде чем использовать нашу модель, нам нужно прописать кое-какие мета-данные в Manifest-файле:

<meta-data android:name="AA_DB_NAME" android:value="Notelin.db" />
<meta-data android:name="AA_DB_VERSION" android:value="1" />


Думаю, все понятно без комментариев. Осталось унаследовать класс Application от com.activeandroid.app.Application:

class NotelinApplication : Application() {
...
}


Чтобы приложение было менее зависимо от БД я создал обертку NoteWrapper над нашей моделью, в которой будут происходить все операции по созданию, сохранению, обновлению и удалению заметок:

class NoteWrapper {

    /**
     * Создает новую заметку
     */
    fun createNote(): Note {
        var note = Note("Новая заметка", Date())
        note.save()
        return note
    }

    /**
     * Сохраняет заметку в БД
     */
    fun saveNote(note: Note) : Long {
        return note.save()
    }

    /**
     * Загружает все существующие заметки и передает во View
     */
    fun loadAllNotes() : List<Note> {
        return Select().from(Note::class.java).execute<Note>()
    }

    /**
     * Ищет заметку по id и возвращает ее
     */
    fun getNoteById(noteId:Long) : Note {
        return Select().from(Note::class.java).where("id = ?", noteId).executeSingle<Note>()
    }

    /**
     * Удаляет все существующие заметки
     */
    fun deleteAllNotes()  {
        Delete().from(Note::class.java).execute<Note>();
    }

    /**
     * Удаляет заметку по id
     */
    fun deleteNote(note:Note)  {
        note.delete()
    }

}


Наверное, вы заметили, что для создания объектов мы не использовали ключевое слово new — это отличие Kotlin от Java.


Экран с заметками



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


Создаем MainView и MainPresenter





Теперь нам нужно перевести все это в код. Для начала создадим интерфейс нашей View:

@StateStrategyType(value = AddToEndSingleStrategy::class)
interface MainView : MvpView {

    fun onNotesLoaded(notes: ArrayList<Note>)

    fun updateView()

    fun onSearchResult(notes: ArrayList<Note>)

    fun onAllNotesDeleted()

    fun onNoteDeleted()

    fun showNoteInfoDialog(noteInfo: String)

    fun hideNoteInfoDialog()

    fun showNoteDeleteDialog(notePosition: Int)

    fun hideNoteDeleteDialog()

    fun showNoteContextDialog(notePosition: Int)

    fun hideNoteContextDialog()

}


Далее мы реализуем созданный интерфейс в нашей активити:

class MainActivity : MvpAppCompatActivity(), MainView {


Одной из особенностей Kotlin, является то, что наследование и реализация интерфейсов указывается через двоеточие после имени класса. Также не имеет разницы идет название родительского класса перед интерфейсами, после или даже между ними, главное, чтобы класс в списке был один. Т. е. запись выше могла бы выглядеть так:

class MainActivity : MainView, MvpAppCompatActivity() {


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

Пока оставим методы пустыми. Как видите, активити наследуется от MvpAppCompatActivity. Это нужно для того, чтобы активити могла восстанавливать состояние при повороте экрана.

Создадим класс презентер:

@InjectViewState
class MainPresenter : MvpPresenter<MainView> {

}


Презентер также наследуется от MvpPresenter, которому мы указываем с какой View мы будем работать.Осталось инжектировать нашу модель в презентер. Для этого мы создаем модуль — поставщика NoteWrapper:

@Module
class NoteWrapperModule {

    @Provides
    @Singleton
    fun provideNoteWrapper() : NoteWrapper= NoteWrapper()

}


Создадим Component для инжектирования презентера:

@Singleton
@Component(modules = arrayOf(NoteWrapperModule::class))
interface AppComponent {

    fun inject(mainPresenter : MainPresenter)

}


Теперь нам нужно создать статический экземпляр класса AppComponent в классе Application:

class NotelinApplication : Application() {

    companion object {
        lateinit var graph: AppComponent
    }

    override fun onCreate() {
        super.onCreate()

        graph = DaggerAppComponent.builder().noteWrapperModule(NoteWrapperModule()).build()
    }

}


Теперь мы можем инжектировать нашу модель в презентере:

@InjectViewState
class MainPresenter : MvpPresenter<MainView> {

    @Inject
    lateinit var mNoteWrapper: NoteWrapper

    constructor() : super() {
        NotelinApplication.graph.inject(this)
    }

}


Для взаимодействия MainView и MainPresenter нам нужно создать переменную в MainActivity:

@InjectPresenter
lateinit var mPresenter: MainPresenter


Плагин Moxy сам привяжет View к фрагменту и произведет другие необходимые действия.

Создадим разметку экрана со списком и плавающей кнопкой. Файл activity_main.xml:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".ui.activities.MainActivity">
    <TextView
        android:id="@+id/tvNotesIsEmpty"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:text="@string/notes_is_empty"
        android:gravity="center"
        />
    <android.support.v7.widget.RecyclerView
        android:id="@+id/rvNotesList"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:scrollbars="vertical"
        app:layoutManager="android.support.v7.widget.LinearLayoutManager"
        />
    <com.melnykov.fab.FloatingActionButton
        android:id="@+id/fabButton"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom|right"
        android:layout_margin="16dp"
        android:src="@mipmap/ic_add"
        app:fab_colorNormal="@color/colorPrimary"
        app:fab_colorPressed="@color/colorPrimaryDark" />
</FrameLayout>


Для реализации летающей кнопки я использовал библиотеку FloatingActionButton. Google уже добавили FAB в support-библиотеку, поэтому вы можете воспользоваться их решением.

Укажем нашей Activity, какой макет она должна показывать:

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

    }


Далее нам нужно связать FAB и список, чтобы при прокручивании списка вверх кнопка исчезала:

fabButon.attachToRecyclerView(rvNotesList)


Нам не нужно писать порядком надоевший findViewById, нужно лишь прописать одну строчку в блоке с import'ами:

import kotlinx.android.synthetic.main.activity_main.*


Как видите, последний пакет совпадает с названием нашего xml-файла. IDE автоматически инициализирует свойства (property) наших View и их имена совпадают с ID, которые мы указали в разметке.

Давайте реализуем загрузку заметок из БД. Заметки нужно загружать только один раз и использовать их в последующем. В этом нам поможет метод onFirstViewAttach класса MvpPresenter, который вызывается единожды при первой привязке View к презентеру. Далее, сколько бы мы не крутили и вертели нашу Activity, данные будут закешированы в презентере.

override fun onFirstViewAttach() {
    super.onFirstViewAttach()

    loadAllNotes()
    }

/**
* Загружает все существующие заметки и передает во View
*/
fun loadAllNotes() {
    mNotesList = mNoteWrapper.loadAllNotes() as ArrayList<Note>
    viewState.onNotesLoaded(mNotesList)
}


Создадим адаптер для нашего списка:

Код адаптера
class NotesAdapter : RecyclerView.Adapter<NotesAdapter.ViewHolder> {

    private var mNotesList: List<Note> = ArrayList()

    constructor(notesList: List<Note>) {
        mNotesList = notesList
    }

    /**
     * Создание новых View и ViewHolder элемента списка, которые впоследствии могут переиспользоваться.
     */
    override fun onCreateViewHolder(viewGroup: ViewGroup, i: Int): ViewHolder {
        var v = LayoutInflater.from(viewGroup.context).inflate(R.layout.note_item_layout, viewGroup, false);
        return ViewHolder(v);
    }

    /**
     * Заполнение виджетов View данными из элемента списка с номером i
     */
    override
    fun onBindViewHolder(viewHolder: ViewHolder, i: Int) {
        var note = mNotesList[i];
        viewHolder.mNoteTitle.text = note.title;

        viewHolder.mNoteDate.text = DateUtils.formatDate(note.changeDate);
    }

    override fun getItemCount(): Int {
        return mNotesList.size
    }

    /**
     * Реализация класса ViewHolder, хранящего ссылки на виджеты.
     */

    class ViewHolder : RecyclerView.ViewHolder {

        var mNoteTitle: TextView
        var mNoteDate: TextView

        constructor(itemView: View) : super(itemView) {
            mNoteTitle = itemView.findViewById(R.id.tvItemNoteTitle) as TextView
            mNoteDate = itemView.findViewById(R.id.tvItemNoteDate) as TextView
        }

    }

}



В адаптере мы используем класс DateUtils. Он служит для форматирования даты в строку:

class DateUtils {

    companion object {

        fun formatDate(date: Date?): String {
            var dateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm");
            return dateFormat.format(date)
        }

    }

}


Т. к. в Kotlin нет модификатора static, я поместил метод formatDate в блок companion object для того, чтобы мы могли обращаться к нему по имени класса, как к обычному статическому методу. В методе onNotesLoaded нашей Activity мы показываем наши заметки:

override fun onNotesLoaded(notes: ArrayList<Note>) {
    rvNotesList.adapter = NotesAdapter(notes)
    updateView()
}

override fun updateView() {
    rvNotesList.adapter.notifyDataSetChanged()
    if (rvNotesList.adapter.itemCount == 0) {
        rvNotesList.visibility = View.GONE
        tvNotesIsEmpty.visibility = View.VISIBLE
    } else {
        rvNotesList.visibility = View.VISIBLE
        tvNotesIsEmpty.visibility = View.GONE
    }
}


Если заметок нет, то мы показываем сообщение «Нет заметок» в TextView.

Насколько я знаю, для обработки клика по элементам RecycleView не существует «официального» OnItemClickListener. Поэтому мы воспользуемся своим решением:

ItemClickSupport.java
public class ItemClickSupport {

    private final RecyclerView mRecyclerView;
    private OnItemClickListener mOnItemClickListener;
    private OnItemLongClickListener mOnItemLongClickListener;

    private View.OnClickListener mOnClickListener = new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            if (mOnItemClickListener != null) {
                RecyclerView.ViewHolder holder = mRecyclerView.getChildViewHolder(v);
                mOnItemClickListener.onItemClicked(mRecyclerView, holder.getAdapterPosition(), v);
            }
        }
    };

    private View.OnLongClickListener mOnLongClickListener = new View.OnLongClickListener() {
        @Override
        public boolean onLongClick(View v) {
            if (mOnItemLongClickListener != null) {
                RecyclerView.ViewHolder holder = mRecyclerView.getChildViewHolder(v);
                return mOnItemLongClickListener.onItemLongClicked(mRecyclerView, holder.getAdapterPosition(), v);
            }
            return false;
        }
    };

    private RecyclerView.OnChildAttachStateChangeListener mAttachListener = new RecyclerView.OnChildAttachStateChangeListener() {
        @Override
        public void onChildViewAttachedToWindow(View view) {
            if (mOnItemClickListener != null) {
                view.setOnClickListener(mOnClickListener);
            }
            if (mOnItemLongClickListener != null) {
                view.setOnLongClickListener(mOnLongClickListener);
            }
        }

        @Override
        public void onChildViewDetachedFromWindow(View view) {

        }
    };

    private ItemClickSupport(RecyclerView recyclerView) {
        mRecyclerView = recyclerView;
        mRecyclerView.setTag(R.id.item_click_support, this);
        mRecyclerView.addOnChildAttachStateChangeListener(mAttachListener);
    }

    public static ItemClickSupport addTo(RecyclerView view) {
        ItemClickSupport support = (ItemClickSupport) view.getTag(R.id.item_click_support);
        if (support == null) {
            support = new ItemClickSupport(view);
        }
        return support;
    }

    public static ItemClickSupport removeFrom(RecyclerView view) {
        ItemClickSupport support = (ItemClickSupport) view.getTag(R.id.item_click_support);
        if (support != null) {
            support.detach(view);
        }
        return support;
    }

    public ItemClickSupport setOnItemClickListener(OnItemClickListener listener) {
        mOnItemClickListener = listener;
        return this;
    }

    public ItemClickSupport setOnItemLongClickListener(OnItemLongClickListener listener) {
        mOnItemLongClickListener = listener;
        return this;
    }

    private void detach(RecyclerView view) {
        view.removeOnChildAttachStateChangeListener(mAttachListener);
        view.setTag(R.id.item_click_support, null);
    }

    public interface OnItemClickListener {
        void onItemClicked(RecyclerView recyclerView, int position, View v);
    }

    public interface OnItemLongClickListener {
        boolean onItemLongClicked(RecyclerView recyclerView, int position, View v);
    }
}



Так как данный код написан на Java, то его можно использовать и в обычных Android-проектах. При желании можно переписать класс на Kotlin и «встроить» в RecycleView. О расширении класса в Kotlin можно почитать здесь.

В методе onCreate нашей Activity пишем:

        with(ItemClickSupport.addTo(rvNotesList)) {
            setOnItemClickListener { recyclerView, position, v -> mPresenter.openNote(this@MainActivity, position) }
            setOnItemLongClickListener { recyclerView, position, v -> mPresenter.showNoteContextDialog(position); true }
        }


Функция with позволяет не писать каждый раз имя переменной, а только лишь вызывать методы у объекта, который мы передали в нее. Обратите внимание, что для получения Activity я использовал не просто this, а this@MainActivity. Это связано с тем, что при использовании this в блоке with, возвращается объект, который мы передали в функцию with. При обычном клике по пункту мы переходим на Activity, где мы можем просмотреть текст нашей заметки. При долгом нажатии появляется контекстное меню. Если вы заметили, перед закрывающей скобкой я не написал слово return. Это не ошибка, а особенность языка Kotlin.

Вот что происходит при нажатии на пункт меню в презентере:

/**
* Открывает активити с заметкой по позиции
*/
fun openNote(activity: Activity, position: Int) {
    val intent = Intent(activity, NoteActivity::class.java)
    intent.putExtra("note_id", mNotesList[position].id)
    activity.startActivity(intent)
}


Мы еще не создали класс NoteActivity, поэтому компилятор будет выдавать ошибку. Для решения этой проблемы можно создать класс NoteActivity или вовсе закомментировать код внутри метода openNote. Запись NoteActivity::class.java аналогична NoteActivity.class в Java. Также заметьте, что мы обращаемся к ArrayList не через метод get(position), а через квадратные скобки, как к обычному массиву.

При использовании MVP-библиотеки Moxy в своем приложении, нам нужно привыкать, что все действия с View, такие как показ/закрытие диалога и другие, должны проходить через презентер. Изначально это не очень привычно и неудобно, но пользы от этого гораздо больше, т. к. мы можем быть уверены, что при пересоздании Activity наше диалоговое окно никуда не пропадет.

/**
* Показывает контекстное меню заметки
*/
fun showNoteContextDialog(position: Int) {
    viewState.showNoteContextDialog(position)
}

/**
 * Прячет контекстное меню заметки
*/
fun hideNoteContextDialog() {
    viewState.hideNoteContextDialog()
}


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

При нажатии на FAB должна создаваться новая заметка:

fabButton.setOnClickListener {
            mPresenter.openNewNote(this)
}


Для создания новой заметки мы вызываем у презентера функция openNewNote:

fun openNewNote(activity: Activity) {
        val newNote = mNoteWrapper.createNote()
        mNotesList.add(newNote)
        sortNotesBy(getCurrentSortMethod())
        openNote(activity, mNotesList.indexOf(newNote))
}


Метод openNewNote использует созданный нами ранее openNote, в который мы передаем Context и позицию заметки в списке.


Реализуем поиск по заметкам





Давайте добавим поиск по заметкам. Создайте в папке res/menu файл main.xml:

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <item
        android:id="@+id/action_search"
        android:icon="@android:drawable/ic_menu_search"
        android:title="@string/search"
        app:actionViewClass="android.support.v7.widget.SearchView"
        app:showAsAction="always" />

</menu>


В MainActivity пишем:

override fun onCreateOptionsMenu(menu: Menu): Boolean {
    menuInflater.inflate(R.menu.main, menu)

    initSearchView(menu)
    return true
}

private fun initSearchView(menu: Menu) {
    var searchViewMenuItem = menu.findItem(R.id.action_search);
    var searchView = searchViewMenuItem.actionView as SearchView;
    searchView.onQueryChange { query -> mPresenter.search(query) }
    searchView.setOnCloseListener { mPresenter.search(""); false }
}


При изменении текста в поле поиска мы передаем строку из поля в презентер, после чего показываем результаты в списке. На самом деле, у SearchView нет метода onQueryChange, его добавила библиотека KAndroid.

Реализуем поиск в презентере:

/**
* Ищет заметку по имени
*/
fun search(query: String) {
    if (query.equals("")) {
        viewState.onSearchResult(mNotesList)
    } else {
        val searchResults = mNotesList.filter { note -> note.title!!.toLowerCase().startsWith(query.toLowerCase()) }
        viewState.onSearchResult(searchResults as ArrayList<Note>)
    }
}


Обратите внимание, как красиво, в одну строчку мы реализовали поиск по ArrayList с помощью метода filter и лямбд. В Java тот же функционал занял бы 6-7строк. Осталось отобразить результаты поиска:

override fun onSearchResult(notes: ArrayList<Note>) {
    rvNotesList.adapter = NotesAdapter(notes)
}



Реализуем сортировку заметок



И последний этап в создании главного экрана, это сортировка заметок. Добавим в res/menu/main.xml следующие строки:

<item android:title="@string/sort_by">
    <menu>
        <item
            android:id="@+id/menuSortByName"
            android:title="@string/sort_by_title" />
        <item
            android:id="@+id/menuSortByDate"
            android:title="@string/sort_by_date" />
    </menu>
</item>


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

override fun onOptionsItemSelected(item: MenuItem): Boolean {
        when (item.itemId) {
            R.id.menuSortByName -> mPresenter.sortNotesBy(MainPresenter.SortNotesBy.NAME)
            R.id.menuSortByDate -> mPresenter.sortNotesBy(MainPresenter.SortNotesBy.DATE)
        }
    return super.onOptionsItemSelected(item)
}


Оператор when является более функциональным аналогом switch-case в Java. Код сортировки в MainPresenter:
/**
* Сортирует заметки
*/
fun sortNotesBy(sortMethod: SortNotesBy) {
    mNotesList.sortWith(getSortComparator(sortMethod))
    viewState.updateView()
}

fun getSortComparator(sortMethod: SortNotesBy): Comparator<Note> {
    when (sortMethod) {
        SortNotesBy.NAME -> return SortName()
        SortNotesBy.DATE -> return SortDate()
    }
}


А вот код самих Comparator'ов:

/**
 * Cортировка заметок по дате
 */
class SortDate : Comparator<Note> {
    override fun compare(note1: Note, note2: Note): Int {
        return  note1.changeDate!!.compareTo(note2.changeDate!!)
    }

}

/**
 * Cортировка заметок по имени
 */
class SortName : Comparator<Note> {
    override fun compare(note1: Note, note2: Note): Int {
        return  note1.title!!.compareTo(note2.title!!)
    }

}



Экран с содержанием заметки



Теперь нам нужно создать экран с содержанием заметки. Здесь пользователь может просмотреть/отредактировать заголовок и текст заметки, сохранить или удалить ее, а также просмотреть информацию о заметке.


Создаем NoteView и NotePresenter





Экран содержит всего лишь три View:

-Заголовок
-Дата последнего изменения
-Текст заметки

А вот и сама разметка:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">


    <EditText
        android:id="@+id/etTitle"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:singleLine="true"
        android:background="#EEEEEE"
        android:textColor="#212121"
        android:paddingLeft="10dp"
        android:paddingTop="10dp"
        android:paddingBottom="5dp"
        android:hint="Заголовок"
        />

    <TextView
        android:id="@+id/tvNoteDate"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:singleLine="true"
        android:background="#EEEEEE"
        android:textColor="#212121"
        android:paddingLeft="10dp"
        android:paddingBottom="10dp"
        />

    <EditText
        android:id="@+id/etText"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:textColor="#000000"
        android:gravity="start"
        android:hint="Текст"
        android:background="@null"
        android:paddingLeft="10dp"
        />

</LinearLayout>


В начале статьи я мельком упомянул об Anko. Библиотека позволяет существенно сократить код, не теряя при этом в удобочитаемости. Вот так, например, выглядела бы наша разметка при использовании Anko:

class MyActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        MyActivityUI().setContentView(this)
    }
}

class MyActivityUI : AnkoComponent {

    companion object {
        val ET_TITLE_ID = View.generateViewId()
        val TV_NOTE_DATE_ID = View.generateViewId()
        val ET_TEXT_ID = View.generateViewId()
    }
    
    override fun createView(ui: AnkoContext<MainActivit>) = with(ui) {
        verticalLayout { 
            editText { 
                id = ET_TITLE_ID
                singleLine = true
                backgroundColor = 0xEE.gray.opaque
                textColor = 0x21.gray.opaque
                leftPadding = dip(10)
                topPadding = dip(10)
                bottomPadding = dip(5)
                hint = "Заголовок"
            }.lparams(matchParent, wrapContent)
            
            textView {
                id = TV_NOTE_DATE_ID
                singleLine = true
                backgroundColor = 0xEE.gray.opaque
                textColor = 0x21.gray.opaque
                leftPadding = dip(10)
                bottomPadding = dip(10)
            }.lparams(matchParent, wrapContent)
            
            editText { 
                id = ET_TEXT_ID
                textColor = Color.BLACK
                gravity = Gravity.START
                hint = "Текст"
                background = null
                leftPadding = dip(10)
            }
        }
    }
}


Но не будем отвлекаться и приступим к написанию кода. Первым делом нам нужно создать View:

interface NoteView : MvpView {

    fun showNote(note: Note)

    fun onNoteSaved()

    fun onNoteDeleted()

    fun showNoteInfoDialog(noteInfo: String)

    fun hideNoteInfoDialog()

    fun showNoteDeleteDialog()

    fun hideNoteDeleteDialog()

}


Имплементируем NoteView в NoteActivity:

class NoteActivity : MvpAppCompatActivity(), NoteView {

    @InjectPresenter
    lateinit var mPresenter: NotePresenter

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_note)

        val noteId = intent.extras.getLong("note_id", -1)
        mPresenter.showNote(noteId)
    }

}


В onCreate мы извлекаем id заметки, чтобы презентер достал заметку из БД и передал данные во View. Создадим презентер:

@InjectViewState
class NotePresenter : MvpPresenter<NoteView> {

    @Inject
    lateinit var mNoteWrapper: NoteWrapper
    lateinit var mNote: Note

    constructor() : super() {
        NotelinApplication.graph.inject(this)
    }

    fun showNote(noteId: Long) {
        mNote = mNoteWrapper.getNoteById(noteId)
        viewState.showNote(mNote)
    }

}


Не забудьте добавить в класс AppComponent строку:

fun inject(notePresenter: NotePresenter)


Покажем нашу заметку:

override fun showNote(note: Note) {
    tvNoteDate.text = DateUtils.formatDate(note.changeDate)
    etTitle.setText(note.title)
    etText.setText(note.text)
}



Реализуем сохранение заметки



Для сохранения заметки нам нужно выбрать соответствующий пункт в меню. Создайте файл res/menu/note.xml:

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    >
    <item
        android:id="@+id/menuSaveNote"
        android:title="@string/save"
        app:showAsAction="never"
        />
</menu>


override fun onCreateOptionsMenu(menu: Menu): Boolean {
    menuInflater.inflate(R.menu.note, menu)
    return true
}

Покажем меню в Activity:

override fun onOptionsItemSelected(item: MenuItem): Boolean {
    when (item.itemId) {
        R.id.menuSaveNote -> mPresenter.saveNote(etTitle.text.toString(), etText.text.toString())
    }
    return super.onOptionsItemSelected(item)
}


Опять же, я не стал приводить код удаления и вывода информации о заметке. При просмотре исходного кода, вы можете заметить, что помимо идентификатора заметки я передал в NoteActivity позицию заметки в списке. Это нужно для того, чтобы при удалении заметки на экране просмотра заметки, она также удалялась из списка. Для реализации этого функционала я использовал EventBus. И опять, я не стал приводить код.

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


Благодарности



Конечно же, нельзя забывать о людях, которые помогли мне при написании статьи. Хотел бы выразить благодарность хабраюзерам Юрию Шмакову (@senneco) за помощь с его библиотекой Moxy и за помощь по другим вопросам. Также, хочу сказать спаcибо сотруднику JetBrains Роману Белову (@belovrv) за ревью статьи и за предоставленный код на Anko.


Заключение



Надеюсь, эта статья смогла убедить вас в том, что писать приложения на Kotlin не трудно, а может даже и легче, чем на Java. Конечно же, могут встречаться и баги, которые сотрудники JetBrains достаточно быстро фиксят. Если у вас появились какие-либо вопросы, вы можете задать их напрямую разработчикам на Slack-канале. Также вы можете почитать статьи о разработке на Kotlin здесь.

Исходный код проекта: Notelin.
А вы используете Kotlin в своих проектах?

Проголосовало 155 человек. Воздержалось 89 человек.

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

Комментарии (24)


  1. kostya_bakay
    26.04.2016 18:44
    +3

    Молодец, продолжай в том же духе.


  1. Tiberal
    26.04.2016 19:01
    +15

    Котлин ради Котлина. Смысл писать на Котлине, как на Java? Ни одна из особенностей языка в проекте толком не применена. Ни дата классы, ни делегаты, ни функции расширения, ни null safe. Хотя все это в вашем проекте можно с успехом применять. А писать на Котлине, только для того, чтоб юзать лямбды слегка глуповато.

    З.Ы. Нормальный layout делать через Anko это Ад.
    З.З.Ы. Статья скорее «отпугивает» от изучения языка


    1. ExplosiveZ
      26.04.2016 20:47
      -2

      Лямбды и в java есть уже давно.


      1. Anatol_s
        26.04.2016 21:24
        +2

        В Java 8, а в Android используется Java 7.


        1. Tiberal
          26.04.2016 21:33

          в Android N появятся


          1. kostya_bakay
            27.04.2016 14:30

            Уже вроде как появились.


            1. procks
              27.04.2016 14:51

              Сегодня вышла стабильная версия Android Studio 2.1 с поддержкой лямбда-выражений и другими фичами java 8 для API 23 и ниже.


            1. Tiberal
              27.04.2016 14:51

              ага, вчера выкатили.


        1. ndc
          27.04.2016 02:06

          С retrolambda лямбды можно и в 7 использовать


        1. NikitaBulygin
          27.04.2016 02:06

          C retrolambda можно и не ждать Android N, а статья я думаю хорошая, по крайней мере для Andoid разработчиков которые не использовали Kotlin.


    1. UngVas
      26.04.2016 21:27
      +1

      Думаю приложение изначально было написанно на Java и потом переведено на Kotlin.


    1. x2bool
      27.04.2016 09:37

      Вы не поверите, но в реальном мире почти всегда такой код и получается. Дело в том, что Kotlin не существует в вакууме, а взаимодействует с фреймворками Android, которые написаны на Java. Чтобы быть абсолютно null-safe нужно очень постараться (что не всегда оправданно). Остальные фичи довольно редко применяются. Смысла обсуждать эти фичи довольно мало: для «hello world» приложения нет необходимости использовать все особенности языка.


      1. Tiberal
        27.04.2016 13:10
        +2

        Вы правы не поверю! Видимо вы так же отдаленно знакомы с языком как и автор.Те «остальные фичи», которые редко применяются, в нормальных проектах применяются на каждом шагу. В java любой объект может быть null, так что не вижу препятствий в применении котлиновской модели null-safe при использовании sdk. Смысл текущей статьи заинтересовать разработчика использовать Котлин вместо Java, при этом ни одна из ключевых концепций языка не использована. Более того включен кусок кода на Java, что как бы может намекать, что без нее все равно не обойтись, и отдельные компоненты придется писать на Java.


        1. x2bool
          27.04.2016 15:13

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


          1. Tiberal
            27.04.2016 16:02

            Я хочу увидеть что то типа «на Java для лист итема мы создавали модель вот так(создаем класс, переопределяем equals и hashCode бла — бла, но на Котлине мы можем это сделать в одну строчку вот так)». И я такой сижу и думаю: «Ого, как круто, интересно какие там еще штуки есть»


            1. lexxpavlov
              05.05.2016 16:06

              Вот если бы кто-нибудь сделает похожую статью, но «правильно», было бы супер. Когда кто-то делает статью-ответ, то это лучшее, что есть в хабре.


  1. dimasikstd
    27.04.2016 03:40
    +5

    Может я проглядел, но помойму не раскрыт основной тезис — в чём приемущество котлина?


  1. furyon
    27.04.2016 03:41
    +1

    Сам потихоньку перевожу свое приложение на Kotlin, вместе с анко кол-во строк сокращается в 2-3 раза. Kotlin для меня поприятней Джавы, единственное меня пугают злые языки, которые говорят что скоро на андройде будет Swift основным ЯП, что мне потом с моим котлином делать?


    1. UngVas
      27.04.2016 09:32

      Один плюс будет, быстрее привыкнешь к новому языку потому что синтаксис Kotlin-а похож на Swift.


      1. furyon
        27.04.2016 09:36
        -3

        Наверняка Google сделает мигратор с java на swift, а с котлина его скорей всего не будет, это будет проблемой.


    1. molchanoviv
      27.04.2016 10:39

      Вряд-ли. Скорее ограничатся поддержкой еще одного языка, как сделали с C++ и прочими. Максимум сделают его вторым приоритетным. Но с Джавы они точно никуда не уйдут. Это по сути нужно будет выкинуть все что было сделано за все время существования андроид. Слишком дорого и слишком рискованно. Так-что не волнуйся и спокойно пиши.


    1. belovrv
      27.04.2016 11:02
      +3

      Вот мне действительно интересно, неужели кто-то всерьез думает, что свифт от эппл может стать ключевой технологией для гугла?


  1. codeZfox
    27.04.2016 03:41

    Зачем добавлять iml файлы?


  1. eld0727
    28.04.2016 10:19
    +1

    Чтобы приложение было менее зависимо от БД я создал обертку NoteWrapper над нашей моделью, в которой будут происходить все операции по созданию, сохранению, обновлению и удалению заметок

    Кажется это называется DAO, Wrapper — это совершенно другой паттерн