We are writing an MVP application on Kotlin for Android

Published on April 26, 2016

We are writing an MVP application on Kotlin for Android



    Application development on Kotlin for Android is gaining popularity among developers, but there are few articles in the Russian-language segment of the Internet. I decided to fix the situation a bit and write a tutorial on developing an application on Kotlin. We will write a complete application using all trend libraries (except RxJava) in the Android development world. In the end, we should get an extensible and easily testable application (we won’t write tests ourselves).

    Attention! This article describes how to create an application version 1.0 . The current code in the repository may differ from that described in the article.

    Probably some of you know that in addition to the programming language Kotlin, JetBrains is also developing the Anko library to create a UI application, as a replacement for ordinary XML files. We will not use it in our project, so as not to put people not familiar with Anko in a difficult position.

    Content:



    Android Studio setup



    To write applications in the Kotlin language, Android Studio needs a special plugin. Instructions for installing the plugin can be found here . Also, do not forget to disable the "Instant Run" function in the settings of Android Studio, because at the moment it is not supported by the Kotlin plugin.

    For the correct generation of the code, you need to use the plugin version no lower than 1.0.1. I used Kotlin version 1.0.2 EAP . This is what the build.gradle file of the application looks like in my project:

    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()
    }
    



    What will we write?



    So, for a start we need to decide what we will write? Without thinking twice, I settled on the notes application. The name was also thought up easily - Notelin. The application is very simple and consists of two screens:

    - The main screen - contains a list with notes
    - Note screen - here you can view / edit the contents of the selected note

    Application requirements are small:

    - Add / view / delete notes
    - View note information
    - Sort notes by title and by date
    - Search by notes title

    Libraries used



    To work with the database, I will use the Android Active library . A lesson on working with it can be found at this link . The Dagger 2 library was used to implement Depency Injection. There are many articles on Habré on Habré. The basis of the entire application will be the Moxy library . With its help, we implement the MVP pattern in our project. It completely solves the problems of the life cycle, so you can not worry about re-creating the components of your application. We will also use a set of extensions for the Kotlin language in Android - KAndroid . I’ll talk about the rest of the libraries along the way.

    The following is a list of project dependencies:

    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' }
        }
    }
    


    And here is the list of application dependencies:

    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'
    }
    


    Note that I use kapt instead of apt . This is a plugin for Gradle that allows you to annotate Kotlin elements.

    Application structure



    Here is the structure of our project in the final version:



    Create Model



    Notes will have four fields:

    • date of creation
    • Date of change
    • Headline
    • Text


    We implement all this in the code:

    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${formatDate(createDate)}\n" +
                                           "Время изменения:\n${formatDate(changeDate)}";
    }
    


    According to this model, the ActiveAndroid library will create a database in which our notes will be stored. If you notice, we have two constructors: empty and with parameters. We will use the first constructor, and the second - ActiveAndroid. Our model is inherited from the Model class, so we can save and delete our notes simply by calling the save () and delete () methods , for example:

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


    But before using our model, we need to write some meta data in the Manifest file:

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


    I think everything is clear without comment. It remains to inherit the Application class from com.activeandroid.app.Application:

    class NotelinApplication : Application() {
    ...
    }
    


    To make the application less dependent on the database, I created a NoteDao wrapper on our model, in which all operations for creating, saving, updating and deleting notes will take place:

    class NoteDao {
        /**
         * Создает новую заметку
         */
        fun createNote(): Note {
            var note = Note("Новая заметка", Date())
            note.save()
            return note
        }
        /**
         * Сохраняет заметку в БД
         */
        fun saveNote(note: Note) = note.save()
        /**
         * Загружает все существующие заметки и передает во View
         */
        fun loadAllNotes() = Select().from(Note::class.java).execute<Note>()
        /**
         * Ищет заметку по id и возвращает ее
         */
        fun getNoteById(noteId: Long) = 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()
        }
    }
    


    You probably noticed that we did not use the new keyword to create objects - this is the difference between Kotlin and Java.


    Notes screen



    It is also the main screen of the application. On it, the user can add / delete a note, view information about the note, sort them by date or name, and also search by title.


    Create MainView and MainPresenter





    Now we need to translate all this into code. First, create the interface of our View:

    @StateStrategyType(value = AddToEndSingleStrategy::class)
    interface MainView : MvpView {
        fun onNotesLoaded(notes: List<Note>)
        fun updateView()
        fun onSearchResult(notes: List<Note>)
        fun onAllNotesDeleted()
        fun onNoteDeleted()
        fun showNoteInfoDialog(noteInfo: String)
        fun hideNoteInfoDialog()
        fun showNoteDeleteDialog(notePosition: Int)
        fun hideNoteDeleteDialog()
        fun showNoteContextDialog(notePosition: Int)
        fun hideNoteContextDialog()
    }
    


    Next, we implement the created interface in our activity:

    class MainActivity : MvpAppCompatActivity(), MainView {
    


    One of the features of Kotlin is that the inheritance and implementation of interfaces is indicated by a colon after the class name. Also, the name of the parent class in front of the interfaces does not matter, after or even between them, the main thing is that there is only one class in the list. That is, the entry above could look like this:

    class MainActivity : MainView, MvpAppCompatActivity() {
    


    If you try to add another class name, separated by a comma, the IDE will throw an error and underline the name of the class that goes second with a red line.

    For now, leave the methods empty. As you can see, activity is inherited from MvpAppCompatActivity . This is necessary so that the activity can restore state when the screen is rotated.

    Create a presenter class:

    @InjectViewState
    class MainPresenter: MvpPresenter<MainView>() {
    }
    


    The presenter is also inherited from MvpPresenter, which we indicate which View we will work with. It remains to inject our model into the presenter. To do this, we create a module - supplier NoteDao:

    @Module
    class NoteDaoModule {
        @Provides
        @Singleton
        fun provideNoteDao() : NoteDao= NoteDao()
    }
    


    Create a Component for injecting a presenter:

    @Singleton
    @Component(modules = arrayOf(NoteDaoModule::class))
    interface AppComponent {
        fun inject(mainPresenter : MainPresenter)
    }
    


    Now we need to create a static instance of the AppComponent class in the Application class:

    class NotelinApplication : Application() {
        companion object {
            lateinit var graph: AppComponent
        }
        override fun onCreate() {
            super.onCreate()
            graph = DaggerAppComponent.builder().noteDaoModule(NoteDaoModule()).build()
        }
    }
    


    Now we can inject our model in a presenter:

    @InjectViewState
    class MainPresenter : MvpPresenter<MainView>() {
        @Inject
        lateinit var mNoteDao: NoteDao
        init {
            NotelinApplication.graph.inject(this)
        }
    }
    


    For the interaction between MainView and MainPresenter, we need to create a variable in MainActivity:

    @InjectPresenter
    lateinit var mPresenter: MainPresenter
    


    The Moxy plugin itself will bind the View to the fragment and perform other necessary actions.

    Create a screen layout with a list and a floating button. File 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>
    


    To implement the flying button, I used the FloatingActionButton library . Google has already added FAB to the support library, so you can use their solution.

    We indicate to our Activity which layout it should show:

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


    Next, we need to link the FAB and the list so that when the list scrolls up, the button disappears:

    fabButon.attachToRecyclerView(rvNotesList)
    


    We do not need to write the boring findViewById in order , we only need to write one line in the block with import:

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


    As you can see, the last package matches the name of our xml file. The IDE automatically initializes the properties of our View and their names match the IDs that we specified in the markup.

    Let's load notes from the database. Notes need to be downloaded only once and used later. The onFirstViewAttach method of the MvpPresenter class will help us with this, which is called once when the View is first bound to the presenter. Further, no matter how much we spin and twirl our Activity, the data will be cached in the presenter.

    override fun onFirstViewAttach() {
        super.onFirstViewAttach()
        loadAllNotes()
    }
    /**
    * Загружает все существующие заметки и передает во View
    */
    fun loadAllNotes() {
        mNotesList = mNoteDao.loadAllNotes()
        viewState.onNotesLoaded(mNotesList)
    }
    


    Create an adapter for our list:

    Adapter code
    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 = 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
            }
        }
    }
    



    In the adapter, we use the formatDate method. It serves to format the date into a string:

    @file:JvmName("DateUtils")
    package imangazaliev.notelin.utils
    import java.text.SimpleDateFormat
    import java.util.*
    fun formatDate(date: Date?): String {
        var dateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm");
        return dateFormat.format(date)
    }
    


    This method is located in the DateUtils.kt file and we can use it as a regular static method. The difference from the static method here is that the method does not belong to the class, but to the package and we do not need to write the class name before the method name. In the annotation, we indicate the name of the class through which we will access the method from Java. For example, in Java, a melon method will be called like this:

    String formattedDate = DateUtils.formatDate(date);
    


    In the onNotesLoaded method of our Activity, we show our notes:

    override fun onNotesLoaded(notes: List<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
        }
    }
    


    If there are no notes, then we display the message “No notes” in the TextView.

    As far as I know, there is no “official” OnItemClickListener to handle clicks on RecycleView elements. Therefore, we will use our solution:

    ItemClickSupport.kt
    class ItemClickSupport private constructor(private val recyclerView: RecyclerView) {
        companion object {
            fun addTo(view: RecyclerView) =
                    view.getTag(R.id.item_click_support) as? ItemClickSupport ?: ItemClickSupport(view)
            fun removeFrom(view: RecyclerView): ItemClickSupport? {
                val support = view.getTag(R.id.item_click_support) as? ItemClickSupport
                support?.detach(view)
                return support
            }
        }
        interface ItemClickListener {
            fun onClick(recyclerView: RecyclerView, position: Int, view: View)
        }
        interface ItemLongClickListener {
            fun onLongClick(recyclerView: RecyclerView, position: Int, view: View): Boolean
        }
        private var itemClickListener: ItemClickListener? = null
        private var itemLongClickListener: ItemLongClickListener? = null
        private val onClickListener = View.OnClickListener {
            val holder = recyclerView.getChildViewHolder(it)
            itemClickListener?.onClick(recyclerView, holder.adapterPosition, it)
        }
        private val onLongClickListener = View.OnLongClickListener listener@{
            val position = recyclerView.getChildViewHolder(it).adapterPosition
            return@listener itemLongClickListener?.onLongClick(recyclerView, position, it) ?: false
        }
        private val attachListener = object : RecyclerView.OnChildAttachStateChangeListener {
            override fun onChildViewAttachedToWindow(view: View) {
                if (itemClickListener != null) {
                    view.setOnClickListener(onClickListener)
                }
                if (itemLongClickListener != null) {
                    view.setOnLongClickListener(onLongClickListener)
                }
            }
            override fun onChildViewDetachedFromWindow(view: View) = Unit
        }
        init {
            recyclerView.setTag(R.id.item_click_support, this)
            recyclerView.addOnChildAttachStateChangeListener(attachListener)
        }
        fun setOnItemClickListener(listener: ItemClickListener): ItemClickSupport {
            itemClickListener = listener
            return this
        }
        fun setOnItemClickListener(listener: (RecyclerView, Int, View) -> Unit) =
                setOnItemClickListener(object : ItemClickListener {
                    override fun onClick(recyclerView: RecyclerView, position: Int, view: View) {
                        listener(recyclerView, position, view)
                    }
                })
        fun setOnItemLongClickListener(listener: ItemLongClickListener): ItemClickSupport {
            itemLongClickListener = listener
            return this
        }
        fun setOnItemLongClickListener(block: (RecyclerView, Int, View) -> Boolean): ItemClickSupport =
                setOnItemLongClickListener(object : ItemLongClickListener {
                    override fun onLongClick(recyclerView: RecyclerView, position: Int, view: View): Boolean {
                        return block (recyclerView, position, view)
                    }
                })
        private fun detach(view: RecyclerView) {
            view.removeOnChildAttachStateChangeListener(attachListener)
            view.setTag(R.id.item_click_support, null)
        }
    }
    



    In the onCreate method of our Activity, we write:

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


    The with function allows you not to write the name of the variable each time, but only to call methods on the object that we passed to it. Please note that to get an Activity, I used not just this , but this @ MainActivity . This is due to the fact that when using this in the with block, the object that we passed to the with function is returned. With a normal click on an item, we go to Activity, where we can view the text of our note. With a long press, a context menu appears. If you notice, before the closing bracket I did not write the word return . This is not a mistake, but a feature of the Kotlin language.

    Here's what happens when you click on a menu item in the presenter:

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


    We have not yet created the NoteActivity class, so the compiler will throw an error. To solve this problem, you can create a NoteActivity class or even comment out the code inside the openNote method. The NoteActivity :: class.java entry is similar to NoteActivity.class in Java. Also notice that we are accessing the list not through the get (position) method, but through square brackets, like a regular array.

    When using the Moxy MVP library in our application, we need to get used to the fact that all actions with the View, such as showing / closing the dialog and others, must go through the presenter. Initially, this is not very familiar and inconvenient, but the benefits are much greater, because we can be sure that when you recreate the Activity, our dialog box will not disappear.

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


    I will not show the code for the context menu, deleting and displaying information about the note since the article is very large. But, I think, you caught the general meaning. It should also be noted that the hideNoteContextDialog method on the presenter should be called even when the dialog is closed via the back button or when clicking on the area outside the dialog.

    Pressing FAB should create a new note:

    fabButton.setOnClickListener {
                mPresenter.openNewNote(this)
    }
    


    To create a new note, we call the openNewNote function on the presenter:

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


    The openNewNote method uses the openNote we created earlier, to which we pass Context and the position of the note in the list.


    We implement search by notes





    Let's add a note search. Create the main.xml file in the res / menu folder:

    <?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>
    


    In MainActivity we write:

    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 }
    }
    


    When changing the text in the search field, we transfer the string from the field to the presenter, and then show the results in the list. In fact, SearchView does not have an onQueryChange method, it was added by the KAndroid library.

    We implement a search in the presenter:

    /**
    * Ищет заметку по имени
    */
    fun search(query: String) {
        if (query.equals("")) {
            viewState.onSearchResult(mNotesList)
        } else {
            val searchResults = mNotesList.filter { it.title!!.startsWith(query, ignoreCase = true) }
            viewState.onSearchResult(searchResults)
        }
    }
    


    Note how beautiful, in one line, we implemented a list search using the filter method and lambdas. In Java, the same functionality would take 6-7 lines. It remains to display the search results:

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



    We implement sorting notes



    And the last step in creating the main screen is sorting notes. Add the following lines to 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>
    


    Now we need to handle clicking on the menu items:

    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)
    }
    


    The when statement is a more functional analogue of switch-case in Java. Sort code in MainPresenter:
    /**
    * Сортирует заметки
    */
    fun sortNotesBy(sortMethod: SortNotesBy) {
        mNotesList.sortWith(sortMethod)
        viewState.updateView()
    }
    



    Screen with note content



    Now we need to create a screen with the contents of the note. Here the user can view / edit the title and text of the note, save or delete it, and also view information about the note.


    Create NoteView and NotePresenter





    The screen contains only three View:

    -Title
    -Date of last change
    -Text of note

    And here is the markup itself:

    <?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>
    


    At the beginning of the article, I briefly mentioned Anko. The library allows you to significantly reduce the code without losing readability. Here, for example, our markup would look like when using 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)
                }
            }
        }
    }
    


    But let's not get distracted and start writing code. First of all, we need to create a View:

    interface NoteView : MvpView {
        fun showNote(note: Note)
        fun onNoteSaved()
        fun onNoteDeleted()
        fun showNoteInfoDialog(noteInfo: String)
        fun hideNoteInfoDialog()
        fun showNoteDeleteDialog()
        fun hideNoteDeleteDialog()
    }
    


    Implementing NoteView in 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)
        }
    }
    


    In onCreate, we retrieve the id of the note so that the presenter retrieves the note from the database and transfers the data to the View. Create a presenter:

    @InjectViewState
    class NotePresenter : MvpPresenter<NoteView>() {
        @Inject
        lateinit var mNoteDao: NoteDao
        lateinit var mNote: Note
        init {
            NotelinApplication.graph.inject(this)
        }
        fun showNote(noteId: Long) {
            mNote = mNoteDao.getNoteById(noteId)
            viewState.showNote(mNote)
        }
    }
    


    Remember to add the line to the AppComponent class:

    fun inject(notePresenter: NotePresenter)
    


    Show our note:

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



    Implement saving notes



    To save a note, we need to select the appropriate item in the menu. Create a res / menu / note.xml file :

    <?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)
    }
    


    Again, I did not cite the code for deleting and displaying information about the note. When viewing the source code, you may notice that in addition to the note identifier, I passed the position of the note in the list to NoteActivity. This is necessary so that when you delete a note on the note viewing screen, it is also removed from the list. To implement this functionality, I used EventBus. And again, I did not cite the code.

    That's all: notes are added, edited and deleted. We can also search and sort notes. Be sure to look at the full source code, the link to which I provided at the end of the article, to better understand how everything works.


    Acknowledgments



    Of course, we should not forget about the people who helped me in writing the article. I would like to express my gratitude to the habrayuzers Yuri Shmakov (@senneco) for help with his Moxy library and for help on other issues. Also, I want to say thank you to JetBrains employee Roman Belov (@belovrv) for reviewing the article and for the code provided on Anko.

    UPD: I also wanted to say thanks to Sirikid for the EPIC COMMIT, thanks to which I redid the impressive part of the code using Kotlin's features.


    Conclusion



    I hope this article was able to convince you that writing applications in Kotlin is not difficult, or even easier, than in Java. Of course, there may also be bugs that JetBrains employees quickly fix. If you have any questions, you can ask them directly to the developers on the Slack channel . You can also read articles about development on Kotlin here .

    Project source code: Notelin .

    Only registered users can participate in the survey. Please come in.

    Do you use Kotlin in your projects?

    • 30.4% Yes 50
    • 31.7% No, but now I’ll start 52
    • 37.8% No, and I will not 62