- PVSM.RU - https://www.pvsm.ru -
Разработка приложений на Kotlin под Android набирает популярность среди разработчиков, однако статей в русскоязычном сегменте Интернета довольно мало. Я решил немного подправить ситуацию, и написать туториал по разработке приложения на Kotlin. Мы напишем полноценное приложение с использованием всех трендовых библиотек (кроме RxJava) в мире Android-разработки. В конце у нас должно получиться расширяемое и легко тестируемое приложение (сами тесты мы писать не будем).
Наверное, некоторые из вас знают, что помимо языка программирования Kotlin JetBrains также разрабатывает библиотеку Anko [1], для создания UI приложения, в качестве замены обычным XML-файлам. Мы не будем использовать его в нашем проекте, дабы не ставить в затруднительное положение людей не знакомых с Anko.
Внимание: якори в содержании почему-то не работают. Если знаете как решить проблему, напишите в ЛС, пожалуйста.
Содержание:
Для написания приложений на языке Kotlin, Android Studio нужен специальный плагин. Инструкцию по установке плагина можно найти здесь [15]. Также не забудьте отключить функцию «Instant Run» в настройках Android Studio, т. к. на данный момент она не поддерживается плагином Kotlin.
Для корректной работы генерации кода нужно использовать версию плагина не ниже 1.0.1. Я использовал версию Kotlin 1.0.2 EAP [16]. Вот так выглядит файл 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. Урок по работе с ней можно найти по этой ссылке [17]. Для реализации Depency Injection была использована библиотека Dagger 2. На Хабре есть много статей по работе с ней. Основой всего приложения будет библиотека Moxy [18]. С ее помощью мы реализуем паттерн MVP в нашем проекте. Она полностью решает проблемы жизненного цикла, благодаря чему вы можете не переживать о пересоздании компонентов вашего приложения. Также мы воспользуемся набором расширений для языка Kotlin в Android — KAndroid [19]. Про остальные библиотеки я буду рассказывать по ходу дела.
Ниже приведен список зависимостей проекта:
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 [20]. Это плагин для Gradle, позволяющий аннотировать Kotlin-элементы.
Вот так выглядит структура нашего проекта в конечном варианте:
У заметок будет четыре поля:
Реализуем все это в коде:
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$titlen" +
"Время создания: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 [21] над нашей моделью, в которой будут происходить все операции по созданию, сохранению, обновлению и удалению заметок:
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.
Также является главным экраном приложения. На нем пользователь может добавить/удалить заметку, просмотреть информацию о заметке, отсортировать их по дате или названию, а также произвести поиск по заголовкам.
Теперь нам нужно перевести все это в код. Для начала создадим интерфейс нашей 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 [22]. Это нужно для того, чтобы активити могла восстанавливать состояние при повороте экрана.
Создадим класс презентер:
@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 [23]. 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. Поэтому мы воспользуемся своим решением:
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 можно почитать здесь [24].
В методе 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 [25]. Это связано с тем, что при использовании 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!!)
}
}
Теперь нам нужно создать экран с содержанием заметки. Здесь пользователь может просмотреть/отредактировать заголовок и текст заметки, сохранить или удалить ее, а также просмотреть информацию о заметке.
Экран содержит всего лишь три 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-канале [26]. Также вы можете почитать статьи о разработке на Kotlin здесь [27].
Исходный код проекта: Notelin [28].
Автор: ImangazalievM
Источник [29]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/android/119241
Ссылки в тексте:
[1] Anko: https://github.com/Kotlin/anko
[2] Настройка Android Studio: #ide-configuring
[3] Что будем писать?: #what-will-write
[4] Структура приложения: #application-structure
[5] Создаем Model: #creating-model
[6] Экран с заметками: #main-screen
[7] Создаем MainView и MainPresenter: #creating-main-vp
[8] Реализуем поиск по заметкам: #notes-search
[9] Реализуем сортировку заметок: #notes-sorting
[10] Экран с содержанием заметки: #note-screen
[11] Создаем NoteView и NotePresenter: #creating-note-vp
[12] Реализуем сохранение заметки: #note-saving
[13] Благодарности: #thanks
[14] Заключение: #conclusion
[15] здесь: http://java-help.ru/kotlin-introduction/
[16] Kotlin 1.0.2 EAP: https://discuss.kotlinlang.org/t/kotlin-1-0-2-eap/1581/1
[17] ссылке: http://java-help.ru/activeandroid-orm-review/
[18] Moxy: https://habrahabr.ru/post/276189/
[19] KAndroid: https://github.com/pawegio/KAndroid
[20] kapt: http://blog.jetbrains.com/kotlin/2015/06/better-annotation-processing-supporting-stubs-in-kapt/
[21] NoteWrapper: https://github.com/ImangazalievM/Notelin/blob/master/app/src/main/kotlin/imangazaliev/notelin/mvp/models/NoteWrapper.kt
[22] MvpAppCompatActivity: https://github.com/ImangazalievM/Notelin/blob/master/app/src/main/kotlin/imangazaliev/notelin/mvp/common/MvpAppCompatActivity.java
[23] FloatingActionButton: https://github.com/makovkastar/FloatingActionButton
[24] здесь: https://kotlinlang.org/docs/reference/extensions.html
[25] this@MainActivity: https://kotlinlang.org/docs/reference/this-expressions.html
[26] Slack-канале: https://kotlinlang.slack.com/
[27] здесь: http://java-help.ru/category/android-kotlin/
[28] Notelin: https://github.com/ImangazalievM/Notelin
[29] Источник: https://habrahabr.ru/post/275255/
Нажмите здесь для печати.