📜 ⬆️ ⬇️

Modern Android development on Kotlin. Part 1

This article is a translation of the article from Mladen Rakonjac

It is very difficult to find one project that would cover everything new in the development for Android in Android Studio 3.0, so I decided to write it. In this article we will sort the following:


')
  1. Android Studio 3
  2. Kotlin programming language
  3. Assembly options
  4. ConstraintLayout
  5. Data Binding Library
  6. MVVM architecture + repository pattern (with mappers) + Android Manager Wrappers
  7. RxJava2 and how it helps us in architecture
  8. Dagger 2.11, what is dependency injection, why should you use it.
  9. Retrofit (Rx Java2)
  10. Room (Rx Java2)

What will our application be?


Our application will be the simplest, which covers all of the above things: it will have only one function that extracts all the googlesamples user repositories from GitHub, stores this data in a local database and displays it to the user.

I will try to explain as many lines of code as possible. You can always see the code I posted on GitHub .

Android Studio


To install Android Studio 3, go to this page.

Android Studio 3 supports Kotlin. Open Create Android Project . There you will see a new checkbox labeled Include Kotlin support . It is selected by default. Double-click the Next button and select Empty Activity , then click Finish .

Congratulations! You made the first Android application on Kotlin :)

Kotlin


You can see MainActivity.kt :

package me.fleka.modernandroidapp import android.support.v7.app.AppCompatActivity import android.os.Bundle class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) } } 

The .kt extension means that the file is a Kotlin file.

MainActivity: AppCompatActivity () means that we extend AppCompatActivity .

In addition, all methods must have the keyword fun and in Kotlin you do not need to use ; but you can if you want. You must use the override keyword, not an annotation, as in Java.

So what does it mean ? in savedInstanceState: Bundle? ? This means that the savedInstanceState can be of type Bundle or of type null . Kotlin is a null safe language. If you have:

 var a : String 

you will get a compilation error, because a must be initialized and it cannot be null . This means that you should write:

 var a : String = "Init value" 

In addition, you will get a compilation error if you do this:

 a = null 

To make a nullable, you must write:

 var a : String? 

Why this important feature of Kotlin language? This helps us avoid NPE . Android developers are already tired of NPE. Even the creator of the null, Sir Tony Hoar, apologized for the invention. Suppose we have a nullable nameTextView . If the variable is null, then in the following code we get an NPE:

 nameTextView.setEnabled(true) 

But Kotlin, in fact, is good, he will not allow us to do even that. Does he make us use the operator ? or operator !! . If we use the operator ? :

 nameTextView?.setEnabled(true) 

The string will only be executed if nameTextView is not null. Otherwise, if you use the operator !! :

 nameTextView!!.setEnabled(true) 


We will get NPE if nameTextView is null. This is for adventurers :).
It was a small introduction to Kotlin. When we continue, I’ll stop to describe another specific code on Kotlin.

2. Build Variants



In development, you often have different environments. The most standard is the test and production environment. These environments may differ in server URLs, icon, name, target API, etc. On fleka in every project you have:



In this application we will use all of them. They will have different applicationId and names. In gradle 3.0.0, there is a new flavorDimension API that allows you to mix product varieties , for example, you can mix demo and minApi23 . In our application, we will use only “default” flavorDimension. Go to build.gradle for the application and paste this code inside android {}

 flavorDimensions "default" productFlavors { finalProduction { dimension "default" applicationId "me.fleka.modernandroidapp" resValue "string", "app_name", "Modern App" } demoProduction { dimension "default" applicationId "me.fleka.modernandroidapp.demoproduction" resValue "string", "app_name", "Modern App Demo P" } demoTesting { dimension "default" applicationId "me.fleka.modernandroidapp.demotesting" resValue "string", "app_name", "Modern App Demo T" } mock { dimension "default" applicationId "me.fleka.modernandroidapp.mock" resValue "string", "app_name", "Modern App Mock" } } 

Go to strings.xml and delete the app_name string so that we do not have conflicts. Then click Sync Now . If you go to Build Variants to the left of the screen, you will see 4 build options, each of which has two types of build: Debug and Release . Go to the demoProduction build option and run it. Then switch to another one and launch it. You should see two applications with different names.



3. ConstraintLayout


If you open activity_main.xml , you will see that this layout is ConstrainLayout . If you have ever written an iOS application, you know about AutoLayout . ConstraintLayout really looks like him. They even use the same Cassowary algorithm.

 <?xml version="1.0" encoding="utf-8"?> <android.support.constraint.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="me.fleka.modernandroidapp.MainActivity"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Hello World!" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent" /> </android.support.constraint.ConstraintLayout> 

Constraint helps us describe the connections between the View . For each View you should have 4 Constraint , one for each side. In this case, our View is limited to a parent on each side.

If you move TextView “Hello World” up a little in the Design tab, a new line will appear in the Text tab:

 app:layout_constraintVertical_bias="0.28" 



Design and Text tabs are synchronized. Our changes in the Design tab affect the xml in the Text tab and vice versa. Vertical_bias describes the vertical trend of view of its Constraint . If you want to center vertically, use:

 app:layout_constraintVertical_bias="0.28" 

Let's make our Activity show only one repository. It will have the name of the repository, the number of stars, the owner, and it will show whether the repository has any issues or not.



To get this layout, the xml should look like this:

 <?xml version="1.0" encoding="utf-8"?> <android.support.constraint.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="me.fleka.modernandroidapp.MainActivity"> <TextView android:id="@+id/repository_name" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginEnd="16dp" android:layout_marginStart="16dp" android:textSize="20sp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintHorizontal_bias="0.0" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent" app:layout_constraintVertical_bias="0.083" tools:text="Modern Android app" /> <TextView android:id="@+id/repository_has_issues" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginEnd="16dp" android:layout_marginStart="16dp" android:layout_marginTop="8dp" android:text="@string/has_issues" android:textStyle="bold" app:layout_constraintBottom_toBottomOf="@+id/repository_name" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintHorizontal_bias="1.0" app:layout_constraintStart_toEndOf="@+id/repository_name" app:layout_constraintTop_toTopOf="@+id/repository_name" app:layout_constraintVertical_bias="1.0" /> <TextView android:id="@+id/repository_owner" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginBottom="8dp" android:layout_marginEnd="16dp" android:layout_marginStart="16dp" android:layout_marginTop="8dp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/repository_name" app:layout_constraintVertical_bias="0.0" tools:text="Mladen Rakonjac" /> <TextView android:id="@+id/number_of_starts" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginBottom="8dp" android:layout_marginEnd="16dp" android:layout_marginStart="16dp" android:layout_marginTop="8dp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintHorizontal_bias="1" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/repository_owner" app:layout_constraintVertical_bias="0.0" tools:text="0 stars" /> </android.support.constraint.ConstraintLayout> 

Let tools: text not confuse you. It simply helps us to see a good preview of the layout.

You may notice that our layout is flat, smooth. There are no nested layouts. You should use nested layouts as rarely as possible, as this may affect performance. More information about this can be found here . In addition, ConstraintLayout works great with different screen sizes:



and it seems to me that I can achieve the desired result very quickly.
This was a small introduction to ConstraintLayout . You can find the Google code lab here , and the ConstraintLayout documentation on GitHub .

4. Library Data Binding


When I heard about the data binding library, the first question I asked myself was: " ButterKnife works very well for me. In addition, I use a plugin that helps me get a View from xml. Why should I change it?". As soon as I learned more about data binding, I had the same feeling that I had when I first used ButterKnife .

How does ButterKnife help us?


ButterKnife helps us get rid of the boring findViewById . So, if you have 5 View, without Butterknife you have 5 + 5 lines to link your View. With ButterKnife you have 5 lines. That's all.

What is bad in ButterKnife?


ButterKnife still does not solve the problem of code support. When I used ButterKnife, I often get an exception at runtime, because I deleted the View in xml, and did not delete the binding code in the Activity / Fragment class. In addition, if you want to add a View in xml, you need to bind again. This is very boring. You lose time keeping in touch.

What about the data binding library?


There are many benefits! With the data binding library you can link your View with just one line of code! Let me show you how it works. Let's add the Data Binding library to our project:

 // at the top of file apply plugin: 'kotlin-kapt' android { //other things that we already used dataBinding.enabled = true } dependencies { //other dependencies that we used kapt "com.android.databinding:compiler:3.0.0-beta1" } 

Please note that the version of the Data Binding compiler must match the version of gradle in the project's build.gradle file:

 classpath 'com.android.tools.build:gradle:3.0.0-beta1' 

Click Sync Now . Go to activity_main.xml and wrap ConstraintLayout with the layout tag:

 <?xml version="1.0" encoding="utf-8"?> <layout 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.support.constraint.ConstraintLayout android:layout_width="match_parent" android:layout_height="match_parent" tools:context="me.fleka.modernandroidapp.MainActivity"> <TextView android:id="@+id/repository_name" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginEnd="16dp" android:layout_marginStart="16dp" android:textSize="20sp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintHorizontal_bias="0.0" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent" app:layout_constraintVertical_bias="0.083" tools:text="Modern Android app" /> <TextView android:id="@+id/repository_has_issues" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginEnd="16dp" android:layout_marginStart="16dp" android:layout_marginTop="8dp" android:text="@string/has_issues" android:textStyle="bold" app:layout_constraintBottom_toBottomOf="@+id/repository_name" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintHorizontal_bias="1.0" app:layout_constraintStart_toEndOf="@+id/repository_name" app:layout_constraintTop_toTopOf="@+id/repository_name" app:layout_constraintVertical_bias="1.0" /> <TextView android:id="@+id/repository_owner" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginBottom="8dp" android:layout_marginEnd="16dp" android:layout_marginStart="16dp" android:layout_marginTop="8dp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/repository_name" app:layout_constraintVertical_bias="0.0" tools:text="Mladen Rakonjac" /> <TextView android:id="@+id/number_of_starts" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginBottom="8dp" android:layout_marginEnd="16dp" android:layout_marginStart="16dp" android:layout_marginTop="8dp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintHorizontal_bias="1" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/repository_owner" app:layout_constraintVertical_bias="0.0" tools:text="0 stars" /> </android.support.constraint.ConstraintLayout> </layout> 

Note that you need to move all xmlns to the layout tag. Then click the Build icon or use the keyboard shortcut Ctrl + F9 ( Cmd + F9 on Mac). We need to build a project so that the Data Binding library can generate the ActivityMainBinding class, which we will use in our MainActivity class.



If you do not build the project, you will not see the ActivityMainBinding class, because it is generated at compile time. We still haven't finished binding, we just said that we have a non-zero variable of type ActivityMainBinding . In addition, as you can see, I did not specify ? at the end of the ActivityMainBinding type, and I haven't initialized it. How is this possible? The lateinit modifier allows us to have nonzero variables awaiting initialization. Like ButterKnife , binding initialization should be done in the onCreate method when your Activity is ready. In addition, you should not declare a binding in the onCreate method, because you probably use it outside the scope of the onCreate method. Our binding should not be null, so we use lateinit . Using the lateinit modifier, we do not need to check the binding of a variable every time we access it.

Let's initialize our binding variable. You must replace:

 setContentView(R.layout.activity_main) 

on:

 binding = DataBindingUtil.setContentView(this, R.layout.activity_main) 

That's all! You have successfully linked your View. Now you can access and apply the changes. For example, let's change the repository name to “Modern Android Habrahabr Article”:

 binding.repositoryName.text = "Modern Android Habrahabr Article" 

As you can see, we can access all View (which have an id , of course) from activity_main.xml through the binding variable. That is why Data Binding is better than ButterKnife .

Getters and Setters in Kotlin


You may have already noticed that we do not have a .setText () method, as in Java. I would like to stop here to explain how getters and setters work in Kotlin compared to Java.

First, you need to know why we use setters and getters. We use them to hide class variables and allow access only through methods, so that we can hide class members from class clients and prevent the same clients from directly changing our class. Suppose we have a Square class in Java:

 public class Square { private int a; Square(){ a = 1; } public void setA(int a){ this.a = Math.abs(a); } public int getA(){ return this.a; } } 

Using the setA () method, we prohibit class clients from setting a negative value to the side of a square; it should not be negative. Using this approach, we need to make a private, so it cannot be installed directly. It also means that the client of our class cannot get a directly, so we have to provide getter. This getter returns a . If you have 10 variables with similar requirements, you need to provide 10 getters. Writing such lines is a boring thing, in which we usually do not use our mind.

Kotlin makes life easier for our developer. If you call

 var side: Int = square.a 

this does not mean that you access a directly. This is the same as

 int side = square.getA(); 

in java. The reason is that Kotlin automatically generates getters and setters by default. In Kotlin, you must specify a special setter or getter, only if you have one. Otherwise, Kotlin autogenerates it for you:

 var a = 1 set(value) { field = Math.abs(value) } 

field ? What is it? To be clear, let's look at this code:

 var a = 1 set(value) { a = Math.abs(value) } 

This means that you call the set method inside the set method, because there is no direct access to a property in the Kotlin world. This will create endless recursion. When you call a = something , it automatically calls the set method.
I hope it’s clear now why you should use the field keyword and how setters and getters work.

Let's return to our code. I would like to show you another great feature of the Kotlin language, apply :

 class MainActivity : AppCompatActivity() { lateinit var binding: ActivityMainBinding override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = DataBindingUtil.setContentView(this, R.layout.activity_main) binding.apply { repositoryName.text = "Medium Android Repository Article" repositoryOwner.text = "Fleka" numberOfStarts.text = "1000 stars" } } } 

apply allows you to call multiple methods on a single instance.

We still haven't finished data binding, there is still a lot to do. Let's create a user interface model class for the repository (this user interface model class for the GitHub repository stores the data that should be displayed, do not confuse them with the Repository pattern). To make a Kotlin class, you must go to New -> Kotlin File / Class :

 class Repository(var repositoryName: String?,var repositoryOwner: String?,var numberOfStars: Int? ,var hasIssues: Boolean = false) 


In Kotlin, the primary constructor is part of the class header. If you do not want to provide a second constructor, that's all! Your class creation work is completed here. There are no constructor parameters for field assignments, no getters and setters. A whole class on one line!

Return to the MainActivity.kt class and create an instance of the Repository class:

 var repository = Repository("Habrahabr Android Repository Article", "Fleka", 1000, true) 

As you can see, to build an object, you do not need the keyword new .

Now let's move on to activity_main.xml and add a data tag:

 <data> <variable name="repository" type="me.fleka.modernandroidapp.uimodels.Repository" /> </data> 

We can access the repository variable, which is the Repository type in our layout. For example, we can do the following in a TextView with the id repository_name :

 android:text="@{repository.repositoryName}" 

The TextView repository_name will display the text obtained from the repositoryName property of the repository variable. It remains only to associate the repository variable from xml to repository from MainActivity.kt .
Click Build to generate the data binding library to create the required classes, go back to MainActivity and add two lines:

 binding.repository = repository binding.executePendingBindings() 

If you run the app, you will see that the Habrahabr Android Repository Article appears in the TextView . Good feature, yes? :)

But what happens if we do the following:

 Handler().postDelayed({repository.repositoryName="New Name"}, 2000) 

Will the new text appear after 2 seconds? No, it will not be displayed. You must reinstall the repository value. Something like this will work:

 Handler().postDelayed({repository.repositoryName="New Name" binding.repository = repository binding.executePendingBindings()}, 2000) 

But it's boring if you need to do it every time we change a property. There is a better solution called Property Observer .
Let's first describe what the Observer pattern is, we'll need it in the rxJava section:

You may have heard about androidweekly.net . This is a weekly newsletter about Android development. If you want to receive it, you need to subscribe to it by entering your email address. Later, if you want, you can stop unsubscribing from your site.

This is one example of the Observer / Observable pattern. In this case, Android Weekly is Observable , it releases newsletters every week. Readers are Observers , they subscribe to it, wait for new issues, and as soon as they receive it, they read it, and if some of them decide that they don’t like it, he / she can stop following.

Property Observer , in our case, is an XML layout that will listen for changes in the Repository instance. Thus, the Repository is observable . For example, as soon as the name property of the Repository class changes in an instance of the class, the xml should be updated without calling:

 binding.repository = repository binding.executePendingBindings() 


How to do this using the data binding library? The data binding library provides us with the BaseObservable class, which must be implemented in the Repository class:

 class Repository(repositoryName : String, var repositoryOwner: String?, var numberOfStars: Int? , var hasIssues: Boolean = false) : BaseObservable(){ @get:Bindable var repositoryName : String = "" set(value) { field = value notifyPropertyChanged(BR.repositoryName) } } 

BR is a class that is automatically generated once when the Bindable annotation is used . As you can see, as soon as the new value is set, we will know about it. Now you can run the application, and you will see that the repository name will be changed after 2 seconds without calling the function executePendingBindings () again .

For this part, that's all. In the next part, I'll write about the MVVM pattern, the Repository pattern, and Android Wrapper Managers . You can find all the code here . This article covers the code to this commit.

Source: https://habr.com/ru/post/341602/


All Articles