Jetpack is an Android library collection that incorporates best practices and provides backward compatibility in your Android apps.
The Jetpack app architecture guide gives you an overview of best practices and recommended architecture to think about when you design your Android app.
In this tutorial I will show you how to build modern Android applications using Kotlin and Jetpack libraries:
To complete this tutorial, you will need to:
Download and install Android Studio (version 4.0 or higher)
Create a new Kotlin Android project and call it Post App. Open the project’s build.gradle file and add the Google() repository as shown below:
1
2
3
4
5
6
allprojects {
repositories {
google()
jcenter()
}
}
Project overview
In this article, we are going to build an Android application that fetches a list of posts from jsonplaceholder API. The application will make use of;
Kotlin as the programming language
Retrofit to connect to the post API
Kotlin coroutine for background processes
Data binding to bind views to a data source
LiveData and ViewModel
Navigation component
By the end of this article, we should have an Android application that looks like this:
We need to add the following dependency to our project. Open the project’s build.gradle file and add the following inside the build script.
1 2 3 4 5 6 7 8
ext { kotlin_version = '1.3.41' version_retrofit = "2.6.1" version_moshi = "1.8.0" version_kotlin_coroutines = "1.1.0" navigationVersion = "1.0.0" lifecycle_version "2.1.0" }
Also in the app gradle, edit the dependency section to look like this.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation 'androidx.appcompat:appcompat:1.1.0' implementation 'androidx.core:core-ktx:1.1.0' implementation 'androidx.constraintlayout:constraintlayout:1.1.3' //Retrofit implementation "com.squareup.retrofit2:retrofit:$version_retrofit" implementation "com.squareup.retrofit2:converter-moshi:$version_retrofit" //Moshi implementation "com.squareup.moshi:moshi:$version_moshi" implementation "com.squareup.moshi:moshi-kotlin:$version_moshi" // Kotin coroutine implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$version_kotlin_coroutines" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$version_kotlin_coroutines" //Navigation component implementation "android.arch.navigation:navigation-fragment-ktx:$navigationVersion" implementation "android.arch.navigation:navigation-ui-ktx:$navigationVersion" // LiveData and viewmodel implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version" implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0-alpha04" implementation 'androidx.legacy:legacy-support-v4:1.0.0' implementation 'androidx.lifecycle:lifecycle-extensions:2.1.0' implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.1.0' testImplementation 'junit:junit:4.12' androidTestImplementation 'androidx.test:runner:1.2.0' androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' }
Since the app will need internet connection, don’t forget to add the internet permission in the manifest.
<uses-permission android:name="android.permission.INTERNET" />
Create a “network” package inside the app folder. Create a Kotlin data class for the post object to look like this:
1
2
3
4
5
data class Post(
val userId: Int,
val id: Int,
val title: String,
val body: String)
Take note that the post class properties map to the ones from the jsonplaceholder API. Also note that we use data class here to take advantage of all the functionalities Kotlin data class provides.
Now create a PostApiService.kt file that will contain the mechanism by which the app will connect to the server using retrofit. The file content should look like this:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
private
const val BASE_POST_URL = "https://jsonplaceholder.typicode.com"
private val moshi = Moshi.Builder()
.add(KotlinJsonAdapterFactory())
.build()
private val retrofit = Retrofit.Builder()
.addConverterFactory(MoshiConverterFactory.create(moshi))
.baseUrl(BASE_POST_URL)
.build()
// Interface that defines how Retrofit talks to the web server using HTTP requests
interface PostApiService {
@GET("posts")
suspend fun getPosts(): List < Post >
}
object PostApi {
val retrofitService: PostApiService by lazy {
retrofit.create(PostApiService::class.java)
}
}
We first create the instance of moshi with the builder pattern, which is used in converting the JSON string into Kotlin objects. Then, create a retrofit instance with the builder pattern and add a converterFactory and our base url. Then, finally, we call the build method.
Next, we create an interface that defines how retrofit talks to the web server using HTTP requests and returns a suspend function from the getPosts() method. Note that returning a suspend function is only possible from retrofit 2.6.0 and above.
Then finally we use the Kotlin object class to provide a single instance of the PostApiService and lazily initialize it (i.e initializing on-demand).
We are now done with the network layer and will move to the overview screen.
Before we start working with the overview screen, let’s first enable data binding. Open the app-level build.gradle and enable the data-binding under the Android section as demonstrated below.
1 2 3 4
android { dataBinding { enabled = true }
Create a package called “post_overview” and create a fragment called “PostsOverviewFragment” also create a view model for the fragment and call it “PostsOverviewViewModel”. Then edit your “posts_overview_fragment.xml” to look like this.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
<?xml version="1.0" encoding="utf-8"?> <layout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" xmlns:app="http://schemas.android.com/apk/res-auto"> <data> <variable name="viewModel" type="posts_overview.PostsOverviewViewModel" /> </data> <androidx.constraintlayout.widget.ConstraintLayout android:layout_width="match_parent" android:layout_height="match_parent" tools:context="posts_overview.PostsOverviewFragment"> <androidx.recyclerview.widget.RecyclerView android:id="@+id/posts_list" android:padding="6dp" android:layout_width="match_parent" android:layout_height="match_parent" app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" tools:listitem="@layout/post_item_list" /> </androidx.constraintlayout.widget.ConstraintLayout> </layout>
Now create a PostListAdapter class adapter that looks like this:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
class PostListAdapter: RecyclerView.Adapter < PostListAdapter.ViewHolder > () {
var data = listOf < Post > ()
set(value) {
field = value
notifyDataSetChanged()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder.from(parent)
}
override fun getItemCount() = data.size
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val post = data[position]
holder.bind(post)
}
class ViewHolder private constructor(view: View): RecyclerView.ViewHolder(view) {
private val title: TextView = view.findViewById(R.id.title)
private val body: TextView = view.findViewById(R.id.body)
companion object {
fun from(parent: ViewGroup): ViewHolder {
val layoutInflater = LayoutInflater.from(parent.context)
val view = layoutInflater.inflate(R.layout.post_item_list, parent, false)
return ViewHolder(view)
}
}
fun bind(post: Post) {
title.text = post.title
body.text = post.body
}
}
}
The adapter has field data which holds a list of posts and we override the setter to call notifyDataSetChanged() whenever this list is assigned. As a contract for extending the RecyclerView.Adapter, we need to override the:
OnCreateViewHolder
getItemCount
onBindViewHolder
Next, we implement our PostsOverviewViewModel class as follows:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class PostsOverviewViewModel: ViewModel() {
private val _posts = MutableLiveData < List < Post >> ()
val post: LiveData < List < Post >>
get() = _posts
init {
getPosts()
}
private fun getPosts() {
viewModelScope.launch {
val getDeferredPosts = PostApi.retrofitService.getPosts()
try {
val listResult = getDeferredPosts
if (listResult.isNotEmpty()) {
_posts.value = listResult
}
} catch (e: Exception) {
_posts.value = ArrayList()
}
}
}
}
PostsOverviewViewModel extends the ViewModel class and has a private field _posts which holds a MutableLiveData of List<Post>
. In order to provide the list of posts to outside classes, we declare other public property posts with exposes _posts as a non mutable LiveData. We then call the getPosts()
method in the init{}
block. This method is responsible for launching the coroutine within the viewmodelScope, calling the suspend function from the PostApi.retrofitService.getPosts()
and assigning the listResult to the _post.value.
Now it’s about time we moved to the PostsOverviewFragment class. The class should look like this.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class PostsOverviewFragment: Fragment() {
lateinit
var binding: PostsOverviewFragmentBinding
private val postViewModel: PostsOverviewViewModel by lazy {
ViewModelProviders.of(this)
.get(PostsOverviewViewModel::class.java)
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup ? ,
savedInstanceState : Bundle ?
): View ? {
binding = PostsOverviewFragmentBinding.inflate(inflater)
binding.setLifecycleOwner(this)
val postListAdapter = PostListAdapter()
binding.viewModel = postViewModel
binding.postsList.adapter = postListAdapter
postViewModel.post.observe(this, Observer {
it?.let {
postListAdapter.data = it
}
})
return binding.root
}
}
We create a lateinit var binding to hold the PostsOverviewFragmentBinding class which was automatically created for us by the data binding. We also create a postViewModel by lazy from ViewModelProviders, then we set the lifecycle owner to the fragment with the call binding.setLifecycleOwner(this)
, we create a PostListAdapter and assign it to the binding.postsList.adapter.
We finally set an observer to observe the LiveData<List<Post>>
from the view model and assign emitted lists of posts to the postListAdapter.data
Up to this point, if you run the app it should still display the “Hello world” template. This is because we have not set up the navigation.
Create a navigation resource file inside the navigation folder in the res folder as shown below.
The navigation.xml will probably display an error because it needs a NavHostFragment to be associated with. We will resolve this error now.
Open your activity_main.xml, delete the Hello world TextView and add the <fragment/>
layout as shown below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
<?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout 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:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> <fragment android:id="@+id/myNavHostFragment" android:name="androidx.navigation.fragment.NavHostFragment" android:layout_width="match_parent" android:layout_height="match_parent" app:defaultNavHost="true" app:navGraph = "@navigation/navigation"/> </androidx.constraintlayout.widget.ConstraintLayout>
Note that the Android:name attribute of the fragment is set to androidx.navigation.fragment.NavHostFragment and the app:navGraph = “@navigation/navigation” then the defaultNavHost is set to true.
Go back to the naviagtion.xml file and notice that the error is gone. Click on “New Destination ‘’ to add a new destination to your app, search for postsOverviewFragment and click ok. Since this is your only destination, it is automatically selected as your startDestination. When you are done your navigation file should look like this.
1 2 3 4 5 6 7 8 9 10 11 12 13
<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/navigation" app:startDestination="@id/postsOverviewFragment"> <fragment android:id="@+id/postsOverviewFragment" android:name="posts_overview.PostsOverviewFragment" android:label="PostsOverviewFragment" tools:layout="@layout/posts_overview_fragment" /> </navigation>
Although the navigation component seems very trivial in this scenario, it is very powerful when it comes to the concept of Single Activity applications.
Congratulations, you are done building the post-app and learning the basics of Android architecture components and how they work together.
Github link: https://github.com/De-Morgan/Post-App