Complete App From Scratch: Kotlin Part 4 — Navigation, Repository and the UI

Yash Prakash
This Code
Published in
7 min readSep 17, 2020
Photo by Max Duzij on Unsplash

Welcome to Part 4 of the Remember app Series!

In this one, we’ll continue building our database for the app in a more robust way and then hop into designing the UI screens for our insert and edit functions in XML!

But first, let’s look at the things we’ve already done, very briefly :) :

  1. We’ve made a mental road-map of what exactly are the functionalities we need in our app, how the users will use our app and what are some of the basic UI components we’ll be using in our screens ( FloatingActionButtons and BottomAppBars, remember?)
  2. Next, we’ve also built our exact designs in a UI designing and prototyping tool called Figma and also defined the functionalities in (1) in a more functional, deeper way.
  3. Finally, we’ve looked into setting up the Room database for Remember, along with a helpful Paging Library for displaying our words in the lists, and wrote our basic CRUD functions for the same.

Pheeew! That’s really impressive. If you’ve come this far, pat yourself on the back because you do deserve it :)

Now, let’s continue and look at implementing the Android Jetpack Navigation library into our app.

Why do we need this in our app? Navigation component helps us manage user interactions efficiently — it helps us navigate across, into, and back out from the different pieces of content within the app.

You can understand more about it, in detail in the official documentation here. But anyway, here we’ll be diving into implementing the component very intuitively so you’ll definitely learn along the way in this series as well.

Let’s start by importing it into our app:

//navigation
def nav_version = "2.3.0-alpha05"
implementation "androidx.navigation:navigation-fragment-ktx:$nav_version"
implementation "androidx.navigation:navigation-ui-ktx:$nav_version"

Now, let’s go ahead and create the navigation directory in our res folder.

Navigate from where to where?

You guessed it right! The next step is — creating our UI screens. We’ll be going forward with Google’s new one-activity principle for Android app development, and hence, we’ll only be having the MainActivity for now, and we’ll branch out into different fragments for the different screens for different tasks — adding a word, editing a word and listing the words.

Therefore, go ahead and create the following fragments in Android Studio with these names:

  • NewWordFragment
  • ListFragment, and
  • EditWordFragment
  • SearchFragment (this will be implemented later.)

Now that we know what our fragments are, here’s the complete code for the nav_graph.xml file:

<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/nav_graph"
app:startDestination="@id/listFragment">

<fragment
android:id="@+id/listFragment"
android:name="save.newwords.vocab.remember.ui.ListFragment"
android:label="fragment_list"
tools:layout="@layout/fragment_list" >
<action
android:id="@+id/action_listFragment_to_newWordFragment"
app:destination="@id/newWordFragment"
app:launchSingleTop="false"
app:exitAnim="@anim/fade_out"
app:popEnterAnim="@anim/fade_in"
app:enterAnim="@anim/slide_in"
app:popExitAnim="@anim/slide_out"/>
<action
android:id="@+id/action_listFragment_to_optionsFragment"
app:destination="@id/optionsFragment"
app:exitAnim="@anim/fade_out"
app:popEnterAnim="@anim/fade_in"
app:enterAnim="@anim/slide_in"
app:popExitAnim="@anim/slide_out"/>
<action
android:id="@+id/action_listFragment_to_searchFragment"
app:destination="@id/searchFragment"
app:exitAnim="@anim/fade_out"
app:popEnterAnim="@anim/fade_in"
app:enterAnim="@anim/slide_in"
app:popExitAnim="@anim/slide_out"/>
<action
android:id="@+id/action_listFragment_to_editWordFragment"
app:destination="@id/editWordFragment"
app:exitAnim="@anim/fade_out"
app:popEnterAnim="@anim/fade_in"
app:enterAnim="@anim/slide_in"
app:popExitAnim="@anim/slide_out"/>


</fragment>
<fragment
android:id="@+id/newWordFragment"
android:name="save.newwords.vocab.remember.ui.NewWordFragment"
android:label="fragment_new_word"
tools:layout="@layout/fragment_new_word" >
</fragment>
<fragment
android:id="@+id/optionsFragment"
android:name="save.newwords.vocab.remember.ui.OptionsFragment"
android:label="fragment_options"
tools:layout="@layout/fragment_options" />
<fragment
android:id="@+id/searchFragment"
android:name="save.newwords.vocab.remember.ui.SearchFragment"
android:label="fragment_search"
tools:layout="@layout/fragment_search" >
<action
android:id="@+id/action_searchFragment_to_editWordFragment"
app:destination="@id/editWordFragment" />
</fragment>
<fragment
android:id="@+id/editWordFragment"
android:name="save.newwords.vocab.remember.ui.EditWordFragment"
android:label="fragment_edit_word"
tools:layout="@layout/fragment_edit_word" >
<argument
android:name="wordNameClicked"
app:argType="string" />
</fragment>

</navigation>

Take some time to study the above code. Once you’ve gone through the whole thing, you can now see how the different navigation paths are defined in between the different screens.

Basically, this describes the workflow of our entire app, which includes all our UI screens: (Don’t worry about the OptionsFragment for now, it will be our Settings screen for our app, which we will implement later in the series.)

Our start screen — the screen users will be seeing upon opening the app will be the ListFragment screen, hence this line:

app:startDestination=”@id/listFragment”

As you might have noticed, we’re also assigning a few animations — enter and exit both for the fragments. You can either omit those lines in here or keep them and just include the files in the anim folder from the GitHub repository I’ve provided at the end of this article for the app.

Looking at the Navigation paths defined, we have:

  1. From ListFragment we can go to either NewWordFragment or EditWordFragment, as you can see from the actions we’ve defined.
  2. From NewWordFragment or EditWordFragment, we can go back to ListFragment.

And with this, we’re done with the Navigation component for now! We’ll see more about using NavControllers to navigate from one part of the app to another in the later parts of the series.

Working with Kotlin Coroutines

Let’s now define some additional useful functions for the WordRepository class.

And for this, we’ll make use of Kotlin’s Coroutines. Let us import them first:

//Coroutines
def coroutine_version = "1.3.5"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutine_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutine_version"

Now for our insertion, updation and deletion operations, we’ll define the new functions such as:

  • To insert into Room database
fun saveWordToDb(word: Word){
CoroutineScope(Dispatchers.IO).launch {
database.wordDao().insertWord(word)
}
}
  • To delete word from the database
fun deleteWordFromDb(name: String) {
CoroutineScope(Dispatchers.IO).launch {
database.wordDao().deleteWord(name)
}
}
  • and finally, to update a word in the database,
fun updateWordInDb(originalName: String, word: Word){
CoroutineScope(Dispatchers.IO).launch {
database.wordDao().updateWord(originalName, word.name, word.meaning, word.audioPath)
}
}

We’ve discussed enough about the database, I think. Let us now see the designs for the the two main screens of the app: The NewWordFragment and EditWordFragment XML files!

We’ll be using Material Design UI components as well as abiding by their enforced guidelines for this app, so as a start, importing it in the app level gradle will have to be performed.

//Material Design
implementation 'com.google.android.material:material:1.2.0-alpha06'

Alright, now that’s done with, let’s see the code to build our screens!

Here’s the fragment_new_word.xml in full:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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"
xmlns:app="http://schemas.android.com/apk/res-auto"
tools:context=".ui.NewWordFragment"
android:background="@color/colorWhite">

<ScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent">

<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
>

<TextView
android:id="@+id/add_word_label"
android:layout_width="match_parent"
android:layout_height="60dp"
android:layout_marginBottom="@dimen/standard_margin"
android:fontFamily="@font/font_raleway"
android:gravity="center_vertical"
android:padding="@dimen/standard_padding"
android:text="@string/new_quote_label"
android:textSize="@dimen/standard_text_size"
android:textStyle="bold"
android:textAlignment="center"
android:background="@color/colorBack"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
tools:ignore="RtlCompat"
/>

<com.google.android.material.textfield.TextInputLayout
android:id="@+id/til_wordname"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/label_word_name"
app:counterEnabled="true"
app:counterMaxLength="26"
android:layout_marginTop="@dimen/high_margin"
android:layout_marginStart="@dimen/big_standard_margin"
android:layout_marginBottom="@dimen/big_standard_margin"
android:layout_marginEnd="@dimen/big_standard_margin"
android:padding="@dimen/standard_padding"
app:boxStrokeWidth="1dp"
app:boxBackgroundMode="outline"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
app:layout_constraintTop_toBottomOf="@id/add_word_label"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
>

<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fontFamily="@font/font_raleway"
android:cursorVisible="true"
android:maxLength="26"
android:maxLines="1"
android:inputType="text|textCapWords|textAutoCorrect"
android:textSize="@dimen/standard_text_size"
android:imeOptions="actionNext"
/>
</com.google.android.material.textfield.TextInputLayout>

<com.google.android.material.textfield.TextInputLayout
android:id="@+id/til_wordMeaning"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/label_meaning"
app:counterEnabled="true"
app:counterMaxLength="97"
app:boxBackgroundMode="outline"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
app:boxStrokeWidth="1dp"
android:layout_marginTop="@dimen/high_margin"
android:layout_marginStart="@dimen/big_standard_margin"
android:layout_marginBottom="@dimen/big_standard_margin"
android:layout_marginEnd="@dimen/big_standard_margin"
android:padding="@dimen/standard_padding"
app:layout_constraintTop_toBottomOf="@id/til_wordname"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
>

<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="@dimen/sub_standard_text_size"
android:fontFamily="@font/font_raleway"
android:cursorVisible="true"
android:gravity="top|start"
android:inputType="textMultiLine|textCapSentences|textAutoCorrect"
android:maxLength="97"
/>

</com.google.android.material.textfield.TextInputLayout>

<com.google.android.material.button.MaterialButton
android:id="@+id/btn_new_audio"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/big_standard_margin"
android:layout_marginEnd="@dimen/big_standard_margin"
android:layout_marginTop="@dimen/higher_margin"
android:layout_marginBottom="@dimen/higher_margin"
android:padding="@dimen/high_padding"
app:layout_constraintTop_toBottomOf="@id/til_wordMeaning"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:text="@string/def_edit_word_audio_btn_label"
android:fontFamily="@font/font_raleway"
android:textAppearance="@android:style/TextAppearance.Material.Medium"
style="@style/Widget.MaterialComponents.Button.OutlinedButton.Icon"
app:icon="@drawable/ic_add_black_24dp"
app:elevation="13dp"
/>

<com.google.android.material.button.MaterialButton
android:id="@+id/btn_add_delete_audio"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/standard_margin"
android:text="@string/delete_pronunciation"
android:fontFamily="@font/font_raleway"
android:layout_gravity="center_vertical"
style="@style/Widget.MaterialComponents.Button.TextButton"
app:layout_constraintTop_toBottomOf="@id/btn_new_audio"
app:layout_constraintEnd_toEndOf="parent"
android:visibility="gone"
/>


<com.google.android.material.button.MaterialButton
android:id="@+id/btn_new_add_audio"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/label_tap_to_record"
android:layout_marginStart="@dimen/big_standard_margin"
android:layout_marginEnd="@dimen/big_standard_margin"
android:layout_marginTop="@dimen/higher_margin"
android:layout_marginBottom="@dimen/higher_margin"
android:padding="@dimen/high_padding"
android:textAppearance="@android:style/TextAppearance.Material.Medium"
style="@style/Widget.MaterialComponents.Button.OutlinedButton.Icon"
app:icon="@drawable/ic_mic_black_24dp"
app:layout_constraintTop_toBottomOf="@id/btn_add_delete_audio"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:visibility="gone"
/>

<Button
android:id="@+id/btn_save"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/save"
android:layout_marginTop="@dimen/higher_margin"
android:layout_marginEnd="@dimen/high_margin"
android:layout_marginBottom="@dimen/high_margin"
android:padding="@dimen/standard_padding"
app:layout_constraintTop_toBottomOf="@id/btn_new_add_audio"
app:layout_constraintEnd_toEndOf="parent"/>

<Button
android:id="@+id/btn_delete"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/cancel"
android:layout_marginTop="@dimen/higher_margin"
android:layout_marginEnd="@dimen/high_margin"
android:layout_marginBottom="@dimen/high_margin"
android:padding="@dimen/standard_padding"
style="@style/Widget.MaterialComponents.Button.TextButton"
app:layout_constraintTop_toBottomOf="@id/btn_new_add_audio"
app:layout_constraintEnd_toStartOf="@id/btn_save"/>


</androidx.constraintlayout.widget.ConstraintLayout>

</ScrollView>

</androidx.constraintlayout.widget.ConstraintLayout>

One important thing to note here is this — You can either use MaterialButton or Button in the XML, both will render the same material design button component if you’ve already defined the material design style in your styles.xml file.

I’ve done it for this app, and you can follow the same through this snippet of code:

<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.MaterialComponents.Light.NoActionBar">
<!-- Customize your theme here. -->
...
...
...
</style>

Also, since the main components of our EditWordFragment will be similar, our code for its XML will also look very much alike.

Below lies the code in its entirety.

<?xml version="1.0" encoding="utf-8"?>
<ScrollView 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"
xmlns:app="http://schemas.android.com/apk/res-auto"
tools:context=".ui.EditWordFragment"
android:animateLayoutChanges="true">

<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
>

<TextView
android:id="@+id/edit_word_label"
android:layout_width="match_parent"
android:layout_height="60dp"
android:textSize="@dimen/standard_text_size"
android:text="@string/edit_word"
android:fontFamily="@font/font_raleway"
android:padding="@dimen/standard_padding"
android:textAlignment="center"
android:gravity="center_vertical"
android:background="@color/colorBack"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
tools:ignore="RtlCompat"
/>

<com.google.android.material.textfield.TextInputLayout
android:id="@+id/til_edit_word_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/high_margin"
android:layout_marginStart="@dimen/big_standard_margin"
android:layout_marginBottom="@dimen/big_standard_margin"
android:layout_marginEnd="@dimen/big_standard_margin"
android:padding="@dimen/standard_padding"
app:counterEnabled="true"
app:counterMaxLength="26"
app:boxStrokeWidth="1dp"
app:boxBackgroundMode="outline"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
app:layout_constraintTop_toBottomOf="@id/edit_word_label"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
>
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fontFamily="@font/font_raleway"
android:cursorVisible="true"
android:maxLength="26"
android:maxLines="1"
android:inputType="text|textCapWords|textAutoCorrect"
android:textSize="@dimen/standard_text_size"
android:imeOptions="actionNext"
/>
</com.google.android.material.textfield.TextInputLayout>

<com.google.android.material.textfield.TextInputLayout
android:id="@+id/til_edit_word_meaning"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/high_margin"
android:layout_marginStart="@dimen/big_standard_margin"
android:layout_marginBottom="@dimen/big_standard_margin"
android:layout_marginEnd="@dimen/big_standard_margin"
android:padding="@dimen/standard_padding"
app:counterEnabled="true"
app:counterMaxLength="97"
app:boxBackgroundMode="outline"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
app:boxStrokeWidth="1dp"
app:layout_constraintTop_toBottomOf="@id/til_edit_word_name"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
>
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="@dimen/sub_standard_text_size"
android:fontFamily="@font/font_raleway"
android:cursorVisible="true"
android:gravity="top|start"
android:inputType="textMultiLine|textCapSentences|textAutoCorrect"
android:maxLength="97"
android:imeOptions="actionDone"
/>
</com.google.android.material.textfield.TextInputLayout>


<com.google.android.material.button.MaterialButton
android:id="@+id/btn_edit_audio"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/big_standard_margin"
android:layout_marginEnd="@dimen/big_standard_margin"
android:layout_marginTop="@dimen/higher_margin"
android:layout_marginBottom="@dimen/higher_margin"
android:padding="@dimen/high_padding"
app:layout_constraintTop_toBottomOf="@id/til_edit_word_meaning"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:text="@string/def_edit_word_audio_btn_label"
android:fontFamily="@font/font_raleway"
android:textAppearance="@android:style/TextAppearance.Material.Medium"
style="@style/Widget.MaterialComponents.Button.OutlinedButton.Icon"
app:icon="@drawable/ic_add_black_24dp"
app:elevation="13dp"
/>

<com.google.android.material.button.MaterialButton
android:id="@+id/btn_edit_delete_audio"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/standard_margin"
android:text="@string/delete_pronunciation"
app:layout_constraintTop_toBottomOf="@id/btn_edit_audio"
app:layout_constraintEnd_toEndOf="parent"
style="@style/Widget.MaterialComponents.Button.TextButton"
android:visibility="gone"
/>

<com.google.android.material.button.MaterialButton
android:id="@+id/btn_edit_add_audio"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/label_tap_to_record"
android:layout_marginStart="@dimen/big_standard_margin"
android:layout_marginEnd="@dimen/big_standard_margin"
android:layout_marginTop="@dimen/higher_margin"
android:layout_marginBottom="@dimen/higher_margin"
android:padding="@dimen/high_padding"
app:layout_constraintTop_toBottomOf="@id/btn_edit_delete_audio"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:textAppearance="@android:style/TextAppearance.Material.Medium"
style="@style/Widget.MaterialComponents.Button.OutlinedButton.Icon"
app:icon="@drawable/ic_mic_black_24dp"
android:visibility="gone"/>



<Button
android:id="@+id/btn_edit_save"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/save"
android:layout_marginTop="@dimen/higher_margin"
android:layout_marginEnd="@dimen/high_margin"
android:layout_marginBottom="@dimen/high_margin"
android:padding="@dimen/standard_padding"
app:layout_constraintTop_toBottomOf="@id/btn_edit_add_audio"
app:layout_constraintEnd_toEndOf="parent"
/>

<Button
android:id="@+id/btn_edit_cancel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/cancel"
android:layout_marginTop="@dimen/higher_margin"
android:layout_marginEnd="@dimen/high_margin"
android:layout_marginBottom="@dimen/high_margin"
android:padding="@dimen/standard_padding"
style="@style/Widget.MaterialComponents.Button.TextButton"
app:layout_constraintTop_toBottomOf="@id/btn_edit_add_audio"
app:layout_constraintEnd_toStartOf="@id/btn_edit_save"
/>



</androidx.constraintlayout.widget.ConstraintLayout>


</ScrollView>

Also, if you want to follow along, here are the colours I’m using with my theme:

<resources>
<color name="colorPrimary">#6200EE</color>
<color name="colorPrimaryDark">#3700B3</color>
<color name="colorAccent">#F08867</color>
<color name="colorBack">#BFA4EE</color>
<color name="colorWhite">#FFFFFF</color>
<color name="colorBlack">#131212</color>
<color name="colorPressed">#B6AAA9</color>
<color name="appbarback">#CFF08867</color>
</resources>

You can choose your own and use a different set of combinations, but since we’ve seen the designs for the app in Part Two of the series, I thought why not let you know the exact hex codes for them as well :)

Now that we’ve seen all that we’ve wanted to for this particular articles, let me quickly link some resources I think will be useful for some of the concepts I’ve mentioned earlier. If you choose to go through them once, you’ll have a clearer idea of how they work and you’ll be more confident in working with them in this app and later, in your app development journey too.

So, here they are:

  • For a more detailed overview of the Navigation component, please do read this article on the MindOrks blog. They explain it very well, along with a clear example.
  • This is a codelab from Google, a thorough tutorial to help you get started with using Kotlin Coroutines and understand their different aspects of usage in apps.

For your convenience, I’ve already uploaded the complete, working code for Remember in my GitHub, so please go ahead and check it out to see the complete app in action for yourself.

With these series of articles and the complete code at your disposal, understanding all the concepts involved in building this app will be a walk in the park! :)

Here’s the link to the repo:

Thank you for reading, and if you find this article helpful, please be sure to leave a few claps, and I’ll see you in the next one! ❤

Do you want to get one free, clean email from me every week or two weeks containing the best of my curated articles and tutorials that I publish? Join my Codecast!

Connect with me on Twitter and LinkedIn!

--

--

Yash Prakash
This Code

Software engineer → Solopreneur ⦿ Scaling my own 1-person business model ⦿ Writing for busy founders and business owners.