Phát triển Android hiện đại với Kotlin (Phần 1)

Thật khó để tìm thấy một dự án bao gồm tất cả những công nghệ mới mẻ nhất trong phát triển Android. Trong bài này, chúng ta sẽ được tìm hiểu những điều mới mẻ đó: 0. Android Studio 3, beta 1 1. Kotlin language 2. Build Variants 3. ConstraintLayout 4. Data binding library 5. MVVM architecture + repository pattern ( with mappers) + Android Manager Wrappers 6. RxJava2 and how it helps us in architecture 7. Dagger 2.11, what is Dependency Injection, why you should use it. 8. Retrofit (with Rx Java2) 9. Room (with Rx Java2)

0. Android studio

Để cài đặt Android Studio 3 beta 6 mới nhất, có thể vào trang này. Lưu ý: Nếu bạn muốn cài đặt nó mà vẫn giữ nguyên phiên bản trước đó, trên máy Mac bạn nên đổi tên phiên bản cũ trong thư mục Application thành "Android Studio Old" chẳng hạn. Để biết thêm thông tin về máy Window và Linux, truy cập tại đây. Android Studio bây giờ đã hỗ trợ Kotlin. Chọn Create Android Project. Bạn sẽ thấy điều mới mẻ ở đây: checkbox với tên label là include Kotlin support. Mặc định nó sẽ được lựa chọn. Nhấn next hai lần và lựa chọn Empty Activity, sau đó finish. Bây giờ, bạn đã tạo một ứng dụng Android mới với Kotlin.

1. Kotlin

Quan sát nội dung file 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)
    }
}

Đuôi mở rộng .kt có nghĩa đây là file Kotlin. MainActivity : AppCompatActivity() có nghĩa là MainActivity được mở rộng từ AppCompatActivity. Hơn nữa, tất cả các phương thức đều phải có từ khoá funoverride là một từ khoá chứ không phải là một annotation như trong Java. Vậy, ý nghĩa của ?savedInstanceState: Bundle? là gì? Nó có nghĩa là savedInstanceState có thể là kiểu Bundle hoặc kiểu null. Kotlin là một ngôn ngữ null safety. Chẳng hạn, nếu bạn định nghĩa:

var a : String

bạn sẽ nhận được một lỗi compilation (lỗi biên dịch), vì biến a phải được khởi tạo và không được null. Có nghĩa là chúng ta phải khai báo như sau:

var a : String = "Init value"

Tương tự, bạn sẽ gặp lỗi biên dịch nếu bạn set giá trị:

a = null

Để khai báo biến a có thể nhận giá trị null, chúng ta phải khai báo:

var a : String?

Tại sao đây lại là một tính năng quan trọng của ngôn ngữ Kotlin. Nó giúp chúng ta tránh những lỗi NPE. Các developer Android đã cực kỳ mệt mỏi với NPE. Ngay cả người tạo ra giá trị null, sir Tony Hoare, cũng đã xin lỗi vì đã phát minh ra nó. Giả sử chúng ta có biến có thể null là nameTextView. Đoạn code sau sẽ cho ra lỗi NPE nếu biến nameTextView là null:

nameTextView.setEnabled(true)

Nhưng với Kotlin, nó sẽ không cho phép chúng ta thực hiện câu lệnh trên, sẽ có lỗi biên dịch, nó sẽ buộc chúng ta sử dụng các toán tử hoặc là ? hoặc là !!. Nếu sử dụng toán tử ?:

nameTextView?.setEnabled(true)

tức là câu lệnh setEnabled chỉ được thực hiện khi nameTextView not null. Còn trong trường hợp sử dụng toán tử !!:

nameTextView!!.setEnabled(true)

nó sẽ cho ra lỗi NPE nếu nameTextView có giá trị null.

Đây chỉ là những giới thiệu nhỏ cho Kotlin. Trong quá trình tìm hiểu tiếp theo chúng ta sẽ tiếp tục tìm hiểu những khái niệm khác của nó.

2. Build Variants

Thông thường khi phát triển ứng dụng trong một dự án, chúng ta có các môi trường khác nhau. Thông thường nhất là hai môi trường testingproduction. Các môi trường đó có thể khác nhau về server url, icon, name, target api... Tuỳ theo từng dự án cụ thể, còn có thể các môi trường khác nhau. Tuy nhiên, chung nhất chúng ta có thể thiết lập một số môi trường sau:

  • finalProduction, chúng ta dùng để public lên Google Play Store
  • demoProduction, là phiên bản có production server url với các tính năng (features) mới mà vẫn chưa có trên Google Play Store. Khách hàng của chúng ta có thể cài đặt phiên bản này bên cạnh ứng dụng từ Google Play để họ có thể test và gửi những feedback cho chúng ta.
  • demoTesting, tương tự demoProduction chỉ khác testing server url.
  • mock, rất hữu ích cho các developer hay designer. Đôi khi chúng ta đã thiết kế sẵn sàng tuy nhiên API thì lại chưa sẵn có. Chờ đợi API hoàn thiện rồi mới dev không phải là một giải pháp. Môi trường này được cung cấp với các fake data, chúng ta có sử dụng chúng để test và đưa ra các feedback. Nó thực sự hữu ích để không phải trì hoãn. Một khi API đã sẵn sàng, chúng ta sẽ di chuyển qua môi trường demoTesting để phát triển tiếp.
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"
    }
}

3. ConstraintLayout

Nếu mở class activity_main.xml, chúng ta có thể thấy layout là một ConstrainLayout. Trong iOS có một loại tương tự là AutoLayout. Chúng thậm chí còn có chung một thuật toán Cassowary.

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

Các Constraints giúp chúng ta mô tả các mối quan hệ giữa các view. Với mỗi view, chúng ta nên có 4 constraints cho mỗi bên. Trong ví dụ trên, view của chúng ta có mối quan hệ với parent theo mỗi bên. Chúng ta có thể tìm hiểu thêm ConstraintLayout về tại đây.

4. Data binding library

Khi nghe về Data binding library, chúng ta tự hỏi, ButterKnife đã thực sự hoạt động hiệu quả và tại sao chúng ta nên thay đổi. ButterKnife đã giúp những gì cho chúng ta? ButterKnife giúp chúng ta loại bỏ những nhàm chán khi làm việc với findViewById. Vì vậy, nếu có 5 view, khi không sử dụng ButterKnife chúng ta phải dùng 5 + 5 dòng code để bind với view, còn với ButterKnife chúng ta chỉ cần 5 dòng code. Đó chính là lợi ích rõ ràng nhất. ButterKnife có nhược điểm nào? ButterKnife vẫn chưa giải quyết vấn đề maintain code. Khi sử dụng ButterKnife, chúng ta sẽ thường thấy lỗi runtime exception, nguyên nhân gây ra bởi việc xoá view trong xml mà không xoá dòng code binding trong class activity/fragment. Ngoài ra, nếu thêm view vào xml, chúng ta phải binding một lần nữa. Đó là công việc nhàm chán và chúng ta đang mất thời gian cho việc maintain binding. Data Binding library có điều gì? Có rất nhiều lợi ích, với Data Binding library, chúng ta có thể binding view với chỉ 1 dòng code. Để thêm Data Binding library vào dự án:

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

Lưu ý rằng phiên bản của DataBinding Compiler sẽ giống với phiên bản của gradle trong file build.gradle:

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

Chọn Sync Now và mở file activity_main.xml:

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

Để khởi tạo biến binding, chúng ta sử dụng:

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

Chỉ cần như vậy, chúng ta đã bind được các view trong layout và thực hiện các thay đổi với view, chẳng hạn:

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

Như bạn thấy, chúng ta có thể truy cập vào tất cả các view (mà được khai báo id trong layout) thông qua biến binding. Đấy chính là lý do tại sao Databinding tốt hơn ButterKnife.

Getters và setters trong Kotlin Quan sát câu lệnh trên, chúng ta thấy không có phương thức setText(). Quan sát tiếp đoạn code sau:

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 cho phép chúng ta gọi nhiều phương thức trong một instance.

Kết luận

Trong bài viết này, chúng ta mới liệt kê và đi qua cơ bản những công nghệ mới nhất để phát triển ứng dụng Android. Ở bài viết tiếp theo, chúng ta sẽ tìm hiểu về mô hình MVVM, Repository Pattern và những công nghệ mới nhất còn lại trong danh sách lúc đầu.