Kotlin new Tutorial

Build a Note Taking Application using Kotlin


In this article, we’ll be exploring different use cases for the Android Architecture Components (AAC) in building a simple Word Application.

This application will be able to take data input from the user using LiveData, save it a Local Database which we’ll be employing Room, and finally displaying this data in a recyclerview to the user screen, implementing the ViewModel.

Here’s the Demo of the app we are going to build:

Pre-requisites

  • Android Studio 3.0 or later and a fair knowledge around it.
  • An Up-to-date Android Studio
  • A device or emulator that runs API level 26
  • A Good level of familiarity with Kotlin Programming Language.

An Understanding of Basic Android Fundamentals such as the following is also required.

  • RecyclerView and Adapters
  • SQLite database and the SQLite query language
  • Threading and AsyncTask
  • It helps to be familiar with software architectural patterns that separate data from the user interface, such as MVP or MVC.

  1. Adding AAC Dependencies: We’ll begin by creating a new android studio project and adding the required dependencies for AAC to our project.
Introduction to Android Architecture Components using Kotlin

Update your Build.gradle file as follows, to include the necessary dependencies for Room, livedata and ViewModel.

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

android {
    compileSdkVersion 29
    buildToolsVersion '29.0.2'
    defaultConfig {
        applicationId "com.example.android.roomwordssample"
        minSdkVersion 20
        targetSdkVersion 29
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }

    packagingOptions {
        exclude 'META-INF/atomicfu.kotlin_module'
    }
}

repositories {
    mavenCentral()
}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'androidx.appcompat:appcompat:1.1.0'
    implementation 'androidx.constraintlayout:constraintlayout:1.1.3'

    // Room components
    implementation "androidx.room:room-runtime:$rootProject.roomVersion"
    implementation "androidx.room:room-ktx:$rootProject.roomVersion"
    kapt "androidx.room:room-compiler:$rootProject.roomVersion"
    androidTestImplementation "androidx.room:room-testing:$rootProject.roomVersion"

    // Lifecycle components
    implementation "androidx.lifecycle:lifecycle-extensions:$rootProject.archLifecycleVersion"
    kapt "androidx.lifecycle:lifecycle-compiler:$rootProject.archLifecycleVersion"
    androidTestImplementation "androidx.arch.core:core-testing:$rootProject.androidxArchVersion"

    // ViewModel Kotlin support
    implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$rootProject.archLifecycleVersion"

    // Coroutines
    api "org.jetbrains.kotlinx:kotlinx-coroutines-android:$rootProject.coroutines"

    // UI
    implementation "com.google.android.material:material:$rootProject.materialVersion"

    // Testing
    androidTestImplementation "androidx.arch.core:core-testing:$rootProject.coreTestingVersion"
    testImplementation 'junit:junit:4.12'
    androidTestImplementation ('androidx.test.espresso:espresso-core:3.1.0', {
        exclude group: 'com.android.support', module: 'support-annotations'
    })
    androidTestImplementation 'androidx.test.ext:junit:1.1.1'
}

After adding all the necessary dependencies, Update and sync your project.


2.Setting Up Room Database: There are three major concepts that are associated with room, in the new architecture components and they include the following

  • @Entity: The entity is just a POKO(Plain Old Kotlin Object) class which will basically serve as the Database. In other words, Room In order to create an entity, we simply create a Kotlin Data Class File and annotate it with the “@Entity” annotation. Additionally, you can go ahead to specify which field will be your primary key by using “@PrimaryKey” annotation and set autoGenerate to be true in parenthesis, if you want room to auto-generate the primary keys for you.
package com.example.android.roomwordssample
    
    import androidx.room.ColumnInfo
    import androidx.room.Entity
    import androidx.room.PrimaryKey
    
    /**
     * A basic class representing an entity that is a row in a one-column database table.
     *
     * @ Entity - You must annotate the class as an entity and supply a table name if not class name.
     * @ PrimaryKey - You must identify the primary key.
     * @ ColumnInfo - You must supply the column name if it is different from the variable name.
     *
     * See the documentation for the full rich set of annotations.
     * https://developer.android.com/topic/libraries/architecture/room.html
     */
    
    @Entity(tableName = "word_table")
    data class Word(@PrimaryKey @ColumnInfo(name = "word") val word: String)
    

For Instance, Here we have a data class called Word, and we’ve specified a variable to be used as the primary key, also our table name is “word_table” due to the annotation used above the class declaration block.
This class simply has just one variable, as clearly seen above.



- @Dao: DAO is an acronym for Data Access Object and is an interface. In this interface we define all our Database CRUD(create, read, update and delete) operations. Each method in DAO is annotated with a particular value ranging from “@Insert”, “@Delete” and “@Query(SELECT * FROM)” e.t.c. 


    /*
     * Copyright (C) 2017 Google Inc.
     *
     * Licensed under the Apache License, Version 2.0 (the "License");
     * you may not use this file except in compliance with the License.
     * You may obtain a copy of the License at
     *
     *      http://www.apache.org/licenses/LICENSE-2.0
     *
     * Unless required by applicable law or agreed to in writing, software
     * distributed under the License is distributed on an "AS IS" BASIS,
     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     * See the License for the specific language governing permissions and
     * limitations under the License.
     */
    
    package com.example.android.roomwordssample
    
    import androidx.lifecycle.LiveData;
    import androidx.room.Dao;
    import androidx.room.Insert;
    import androidx.room.OnConflictStrategy;
    import androidx.room.Query;
    
    
    /**
     * The Room Magic is in this file, where you map a Java method call to an SQL query.
     *
     * When you are using complex data types, such as Date, you have to also supply type converters.
     * To keep this example basic, no types that require type converters are used.
     * See the documentation at
     * https://developer.android.com/topic/libraries/architecture/room.html#type-converters
     */
    
    @Dao
    interface WordDao {
    
        // LiveData is a data holder class that can be observed within a given lifecycle.
        // Always holds/caches latest version of data. Notifies its active observers when the
        // data has changed. Since we are getting all the contents of the database,
        // we are notified whenever any of the database contents have changed.
        @Query("SELECT * from word_table ORDER BY word ASC")
        fun getAlphabetizedWords(): LiveData<List<Word>>
    
        @Insert(onConflict = OnConflictStrategy.IGNORE)
        suspend fun insert(word: Word)
    
        @Query("DELETE FROM word_table")
        suspend fun deleteAll()
    }

From the foregoing, we can see our interface name WordDao. Also, our WordDao contains 3 basic methods. The method for querying the database for all the words and deleting all words from the database, we can also see the insert method which is annotated with the @insert annotation. 

  • @Database: The Database Class is the class that houses the code for the initialization of an SQLite database and we do this by creating a class that extends from the RoomDatabase and annotate it with “@Database” 

This class ties the DAOs and Entities together. This database class also a singleton class, which means only one instance of it can run at any given point in the lifecycle of our app. A database Instance can be created at runtime by using the Room.databaseBuilder() in our Database class.

*
 * Copyright (C) 2017 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.example.android.roomwordssample

import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import androidx.sqlite.db.SupportSQLiteDatabase
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch

/**
 * This is the backend. The database. This used to be done by the OpenHelper.
 * The fact that this has very few comments emphasizes its coolness.  In a real
 * app, consider exporting the schema to help you with migrations.
 */
@Database(entities = [Word::class], version = 1, exportSchema = false)
abstract class WordRoomDatabase : RoomDatabase() {

    abstract fun wordDao(): WordDao

    companion object {
        @Volatile
        private var INSTANCE: WordRoomDatabase? = null

        fun getDatabase(
                context: Context,
                scope: CoroutineScope
        ): WordRoomDatabase {
            // if the INSTANCE is not null, then return it,
            // if it is, then create the database
            return INSTANCE ?: synchronized(this) {
                val instance = Room.databaseBuilder(
                        context.applicationContext,
                        WordRoomDatabase::class.java,
                        "word_database"
                )
                        .addCallback(WordDatabaseCallback(scope))
                        .build()
                INSTANCE = instance
                // return instance
                instance
            }
        }

        private class WordDatabaseCallback(
                private val scope: CoroutineScope
        ) : RoomDatabase.Callback() {
            /**
             * Override the onOpen method to populate the database.
             * For this sample, we clear the database every time it is created or opened.
             */
            override fun onOpen(db: SupportSQLiteDatabase) {
                super.onOpen(db)
                // If you want to keep the data through app restarts,
                // comment out the following line.
                INSTANCE?.let { database ->
                    scope.launch(Dispatchers.IO) {
                        populateDatabase(database.wordDao())
                    }
                }
            }
        }

        /**
         * Populate the database in a new coroutine.
         * If you want to start with more words, just add them.
         */
        suspend fun populateDatabase(wordDao: WordDao) {
            // Start the app with a clean database every time.
            // Not needed if you only populate on creation.
            wordDao.deleteAll()

            var word = Word("Hello")
            wordDao.insert(word)
            word = Word("World!")
            wordDao.insert(word)
        }
    }

}

3. LiveData: This is an Android class for observing data changes and it is from the lifecycle library. It is a data holder class for observable data, and since it’s a lifecycle aware component, it is going to update the component in its active lifecycle state.

// Room executes all queries on a separate thread.
// Observed LiveData will notify the observer when the data has changed.
val allWords: LiveData<List<Word>> = wordDao.getAlphabetizedWords()

4. Creating the Repo: The task of this class is to determine where to fetch data from. It chooses between local database or the API.


/*
 * Copyright (C) 2017 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.example.android.roomwordssample

import androidx.annotation.WorkerThread
import androidx.lifecycle.LiveData

/**
 * Abstracted Repository as promoted by the Architecture Guide.
 * https://developer.android.com/topic/libraries/architecture/guide.html
 */
class WordRepository(private val wordDao: WordDao) {

    // Room executes all queries on a separate thread.
    // Observed LiveData will notify the observer when the data has changed.
    val allWords: LiveData<List<Word>> = wordDao.getAlphabetizedWords()

    suspend fun insert(word: Word) {
        wordDao.insert(word)
    }
}

For Instance in our Repo Class, we add a method to fetch data in our application. And as a note, data queries will be done on a separate thread and not on the main thread, as a result of wrapping it with the LiveData type.              

5. ViewModel: The ViewModel is also part of the lifecycle library, and it is tasked with the job of providing data between the repository and UI. The ViewModel is saved during data configuration changes and gets the current ViewModel to reconnect with the new instance of the owner, thereby preserving our on-screen-data. 

/*
 * Copyright (C) 2017 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.example.android.roomwordssample

import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.launch

/**
 * View Model to keep a reference to the word repository and
 * an up-to-date list of all words.
 */
class WordViewModel(application: Application) : AndroidViewModel(application) {

    // The ViewModel maintains a reference to the repository to get data.
    private val repository: WordRepository
    // LiveData gives us updated words when they change.
    val allWords: LiveData<List<Word>>

    init {
        // Gets reference to WordDao from WordRoomDatabase to construct
        // the correct WordRepository.
        val wordsDao = WordRoomDatabase.getDatabase(application, viewModelScope).wordDao()
        repository = WordRepository(wordsDao)
        allWords = repository.allWords
    }

    /**
     * The implementation of insert() in the database is completely hidden from the UI.
     * Room ensures that you're not doing any long running operations on the mainthread, blocking
     * the UI, so we don't need to handle changing Dispatchers.
     * ViewModels have a coroutine scope based on their lifecycle called viewModelScope which we
     * can use here.
     */
    fun insert(word: Word) = viewModelScope.launch {
        repository.insert(word)
    }
}

There are several reasons why the ViewModel are applied and one of those includes the fact that it survives configuration changes, thereby reducing the bulk of work involved in writing codes to manage our lifecycle. Also, it has good integration with LiveData(note that RxJava can be used in the place of LiveData).

6. Create an Adapter and Add a RecyclerView: We create a layout and name it “recyclerview_item” Our recyclerview layout just contains a textview, wrapped in a CardView as the parent View. Since we’ve already added dependencies for our recyclerview and cardView, we can go ahead to create our Adapter Class, which will be responsible for displaying data on our screen.

/*
* Copyright (C) 2017 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.example.android.roomwordssample

import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView


class WordListAdapter internal constructor(
        context: Context
) : RecyclerView.Adapter<WordListAdapter.WordViewHolder>() {

    private val inflater: LayoutInflater = LayoutInflater.from(context)
    private var words = emptyList<Word>() // Cached copy of words

    inner class WordViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        val wordItemView: TextView = itemView.findViewById(R.id.textView)
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): WordViewHolder {
        val itemView = inflater.inflate(R.layout.recyclerview_item, parent, false)
        return WordViewHolder(itemView)
    }

    override fun onBindViewHolder(holder: WordViewHolder, position: Int) {
        val current = words[position]
        holder.wordItemView.text = current.word
    }

    internal fun setWords(words: List<Word>) {
        this.words = words
        notifyDataSetChanged()
    }

    override fun getItemCount() = words.size
}

7. Populate the Database: Whenever we open our Application, by default, we are populating our database with a few words and deleting previous entries into the database. This has already been done in the Database class, but let us take a look at the code for it.

 /**
         * Populate the database in a new coroutine.
         * If you want to start with more words, just add them.
         */
        suspend fun populateDatabase(wordDao: WordDao) {
            // Start the app with a clean database every time.
            // Not needed if you only populate on creation.
            wordDao.deleteAll()

            var word = Word("Hello")
            wordDao.insert(word)
            word = Word("World!")
            wordDao.insert(word)
        }

8. Connect UI and Data: In order to display data from the database, we will make use of an observer that will observe the data changes and notify the necessary subscribers. The ViewModel will do this for us.

Go ahead to connect the ViewModel with the ViewModelProvider, and subsequently, in the onChange() method, we will get the observed data and update the screen with it. 

/*
 * Copyright (C) 2017 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.example.android.roomwordssample

import android.app.Activity
import android.content.Intent
import android.os.Bundle
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.Toolbar
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.floatingactionbutton.FloatingActionButton

class MainActivity : AppCompatActivity() {

    private val newWordActivityRequestCode = 1
    private lateinit var wordViewModel: WordViewModel

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


        val recyclerView = findViewById<RecyclerView>(R.id.recyclerview)
        val adapter = WordListAdapter(this)
        recyclerView.adapter = adapter
        recyclerView.layoutManager = LinearLayoutManager(this)

        // Get a new or existing ViewModel from the ViewModelProvider.
        wordViewModel = ViewModelProvider(this).get(WordViewModel::class.java)

        // Add an observer on the LiveData returned by getAlphabetizedWords.
        // The onChanged() method fires when the observed data changes and the activity is
        // in the foreground.
        wordViewModel.allWords.observe(this, Observer { words ->
            // Update the cached copy of the words in the adapter.
            words?.let { adapter.setWords(it) }
        })

        val fab = findViewById<LinearLayout>(R.id.new_note)
        fab.setOnClickListener {
            val intent = Intent(this@MainActivity, NewWordActivity::class.java)
            startActivityForResult(intent, newWordActivityRequestCode)
        }
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, intentData: Intent?) {
        super.onActivityResult(requestCode, resultCode, intentData)

        if (requestCode == newWordActivityRequestCode && resultCode == Activity.RESULT_OK) {
            intentData?.getStringExtra(NewWordActivity.EXTRA_REPLY)?.let {
                val word = Word(it)
                wordViewModel.insert(word)
                Unit
            }
        } else {
            Toast.makeText(
                    applicationContext,
                    R.string.empty_not_saved,
                    Toast.LENGTH_LONG
            ).show()
        }
    }
}

In our MainActivity, we’ve created a button at the bottom of our page, which takes us to a new activity to add a new word to our Application.  

9. Create a NewWord Activity: Here is the activity that handles user data input. Here we use an edittext to get the user input from the user and this class has a method that checks to ensure that the input fields are not empty before saving it whenever the user clicks the back button

/*
 * Copyright (C) 2017 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.example.android.roomwordssample

import android.app.Activity
import android.content.Intent
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import android.text.TextUtils
import android.widget.Button
import android.widget.EditText

/**
 * Activity for entering a word.
 */

class NewWordActivity : AppCompatActivity() {

    private lateinit var editWordView: EditText

    public override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_new_word)
        editWordView = findViewById(R.id.linedEditText)

//        val button = findViewById<Button>(R.id.button_save)
//        button.setOnClickListener {
//
//        }
    }

    fun checkAndSave(){
        val replyIntent = Intent()
        if (TextUtils.isEmpty(editWordView.text)) {
            setResult(Activity.RESULT_CANCELED, replyIntent)
        } else {
            val word = editWordView.text.toString()
            replyIntent.putExtra(EXTRA_REPLY, word)
            setResult(Activity.RESULT_OK, replyIntent)
        }
        finish()
    }


    override fun onPause() {
        super.onPause()

        checkAndSave()
    }

    companion object {
        const val EXTRA_REPLY = "com.example.android.wordlistsql.REPLY"
    }
}

That’s it. Now you have a Note Application. You can find the source code for this project here. And in subsequent posts, we’ll be adding other features to this application. 


Share on social media

//