Day 10 : Android Annotations

Day 10 : Android Annotations Cover Image

สวัสดีครับ บทความนี้เป็นบทความที่ 10 แล้วนะครับ ที่ผมจะมาเขียน ในซีรีย์ Learn 30 Android Libraries in 30 days

สำหรับบทความทั้งหมด อ่านได้จากด้านล่างครับ

สำหรับวันนี้ขอนำเสนอเรื่อง Android Annotations ครับ

Android Annotations คืออะไร?

Android Annotations มันคือ Android Library ที่จะช่วยให้คุณพัฒนาแอพ Android ได้เร็วขึ้น อ่านง่ายขึ้น โค๊ดเป็นระเบียบ ตรงตามหลักการ Dependency Injection เลย โดยปกติแล้ว เวลาเราเขียนโค๊ด Android เบื่อมั้ยที่ต้องมี View เยอะๆ แล้วต้องมาทำ findViewById() ทุกๆครั้งๆ มี Button ที่ต้องมานั่ง setOnClickListener() แล้วถ้ามันมีวิธีที่สะดวกสบายกว่านั้น แถมโค๊ดสะอาดตา ฟังดูน่าสนใจใช่มั้ยครับ?

นั่นแหละ จึงเป็นสาเหตุว่า ทำไมถึงมี Android Annotations ขึ้นมา อ่านรายละเอียดเพิ่มเติมได้ที่ Android Annotations Wiki

Installation

สำหรับวิธีการติดตั้ง ผมจะพูดถึงการติดตั้งด้วย build.gradle บน Android Studio นะครับ หากเป็น Eclipse คิดว่าน่าจะแค่ดาวน์โหลด androidannotations-bundle-3.0.1.zip จากนั้นก็อปไฟล์ jar มาวาง แล้วก็ Add Built Path น่าจะได้แล้ว

วิธีการ config กับ Android Studio เปิดไฟล์ build.gradle

เพิ่ม โค๊ดนี้ลงไป เพื่อให้มันรู้จัก Plugin ชื่อ android-apt

buildscript {
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath 'com.neenbedankt.gradle.plugins:android-apt:1.+'
    }
}

จากนั้นเพิ่ม apply plugin: 'android-apt' ต่อท้าย com.androdi.application ดังนี้

apply plugin: 'com.android.application'
apply plugin: 'android-apt'

ต่อมาเพิ่มโค๊ดนี้ลงไป

apt {
    arguments {
        androidManifestFile variant.processResources.manifestFile
        resourcePackageName "com.devahoy.learn30androidlibraries"
    }
}

เพื่อให้ android annotation ทำการ gen ไฟล์ที่อยู่ใน package name ของเรา (สิ่งที่ต้องเปลี่ยนคือเปลี่ยน resourcePackageName เป็นชื่อ package name ของคุณเองนะครับ)

ต่อมาเพิ่ม dependencies ของ Android Annotations ลงไป

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    compile 'com.android.support:appcompat-v7:20.+'

    apt "org.androidannotations:androidannotations:3.0+"
    compile "org.androidannotations:androidannotations-api:3.0+"

}

ไฟล์ build.gradle จะได้ดังนี้

buildscript {
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath 'com.neenbedankt.gradle.plugins:android-apt:1.+'
    }
}

apply plugin: 'com.android.application'
apply plugin: 'android-apt'

android {
    compileSdkVersion 20
    buildToolsVersion "20.0.0"

    defaultConfig {
        applicationId "com.devahoy.learn30androidlibraries"
        minSdkVersion 11
        targetSdkVersion 20
        versionCode 1
        versionName "1.0"
    }
    buildTypes {
        release {
            runProguard true
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

apt {
    arguments {
        androidManifestFile variant.processResources.manifestFile
        resourcePackageName "com.devahoy.learn30androidlibraries"
    }
}

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    compile 'com.android.support:appcompat-v7:20.+'

    apt "org.androidannotations:androidannotations:3.0+"
    compile "org.androidannotations:androidannotations-api:3.0+"

}

กด Sync Gradle เพื่อทำการโหลด dependencies ทั้งหมด

สุดท้าย ลืมไม่ได้ เปิดไฟล์ AndroidManifest.xml แล้วเปลี่ยนชื่อ Activity แรกที่จะใช้เป็น Activity หลักเมื่อเปิดแอพขึ้นมา โดยการเพิ่ม underscore(_) ต่อท้ายชื่อ เช่น

<application>
    <activity
        android:name=".day10.AnnotationsActivity"
        android:label="@string/app_name" >
        <intent-filter>
            <action android:name="android.intent.action.MAIN" />

            <category android:name="android.intent.category.LAUNCHER" />
        </intent-filter>
    </activity>

</application>

ให้กลายเป็น

<application>
    <activity
        android:name=".day10.AnnotationsActivity_"
        android:label="@string/app_name" >
        <intent-filter>
            <action android:name="android.intent.action.MAIN" />

            <category android:name="android.intent.category.LAUNCHER" />
        </intent-filter>
    </activity>

</application>

เท่านี้ก็เรียบร้อย หากใครเพิ่ม underscore(_) แล้วมันยัง error ว่าหา class นี้ไม่เจอ แสดงว่าท่านยัง setup ไม่ถูกนะครับ ลองไปดูไฟล์ build.gradle ใหม่ หรือหากอยากลองเปรียบเทียบกับไฟล์ build.gradle ของผมก็นี้ครับ หรือหากใครคิดว่า setup ยาก ก็ลองโหลดไฟล์ jar มาเลยก็ได้ครับ (ส่วนนี้ผมไม่ได้ลองแฮะ ว่ามันง่ายจริงไหม)

Getting Started

การใช้งาน Android Annotations หลังจากลองเล่นดู รู้สึกว่าคล้ายๆกับ Butter Knife ที่ได้ลองใช้ไปเมื่อวานเลยครับ คอนเซปคล้ายๆกัน แต่รู้สึกว่าตัวนี้จะมีลูกเล่นให้เล่นได้เยอะกว่า ก่อนที่จะเริ่มสร้างโปรเจ็ค เรามาดูกันก่อนว่า Android Annotations มันมีหลักการทำงานอย่างไร ไฟล์ที่จะใช้ มี 2 ไฟล์เช่นเคย AnnotationsActivity.java และ day10_activity_annotations.xml

สำหรับไฟล์ day10_activity_annotations.xml จะใช้เลเอาท์อันเดียวกันกับ Day 9 : Butter Knife จะได้ดังนี้

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:padding="16dp"
              android:layout_width="match_parent"
              android:layout_height="match_parent">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentTop="true"
        android:layout_alignParentLeft="true"
        android:id="@+id/greeting"/>

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="16dp"
        android:layout_below="@id/greeting"
        android:id="@+id/name"/>

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/button_click"
        android:paddingLeft="32dp"
        android:paddingRight="32dp"
        android:layout_alignParentBottom="true"
        android:layout_centerHorizontal="true"
        android:id="@+id/button_click"/>
</RelativeLayout>

@EActivity

ตัวแรกเลยคือ @Eactivity เป็น annotation ที่เอาไว้ generate ตัว Activity ให้เป็นชื่อเดียวกัน แต่ว่ามี underscore(_) เพิ่มมาต่อท้ายที่ชื่อด้วย เช่น

package com.devahoy.learn30androidlibraries.day10;

@EActivity(R.layout.day10_activity_annotations)
public class AnnotationsActivity extends ActionBarActivity {
    ...
}

มันจะถูก generate ไปเป็นชื่อเดิม + (_) และถูกเก็บไว้อีกโฟลเดอร์นึง ไปเป็น

package com.devahoy.learn30androidlibraries.day10;

@EActivity(R.layout.day10_activity_annotations)
public class AnnotationsActivity_ extends ActionBarActivity {
    ...
}

ทีนี้ก็หายสงสัยหรือยังครับ ว่าทำไมเราต้องกำหนด Activity เริ่มต้น เป็นคลาสที่เติม underscore ด้วย :D

<activity
    android:name=".day10.AnnotationsActivity_"
    android:label="@string/app_name" >
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />

        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
</activity>

ทีนี้ลองมาเปรียบเทียบ การใช้ @EActivity กับการเขียนแบบธรรมดา

หากใช้ @EActivity โค๊ดเราก็จะเหมือนแบบด้านบน คือเป็นแบบนี้

package com.devahoy.learn30androidlibraries.day10;

@EActivity(R.layout.day10_activity_annotations)
public class AnnotationsActivity extends ActionBarActivity {
    ...
}

เปรียบเทียบได้กับการเขียนธรรมดาแบบนี้

package com.devahoy.learn30androidlibraries.day10;

public class AnnotationsActivity extends ActionBarActivity {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.day10_activity_annotations);
    }
}

สังเกตว่า @EActivity() มันคือการ setContentView() นั่นเอง

@ViewById

@ViewById เป็น annotation สำหรับ Inject View ครับ โดยปกติเราต้องทำการ findViewById() ให้มัน แบบนี้

public class AnnotationsActivity extends ActionBarActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        setContentView(R.layout.day10_activity_annotations);

        TextView greeting = (TextView) findViewById(R.id.greeting);
        TextView name = (TextView) findViewById(R.id.name);
        Button buttonClick = (Button) findViewById(R.id.button_click);
        buttonClick.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {

            }
        });
    }

}

แต่ถ้าหากใช้ Android Annotation ด้วย @ViewById ก็จะได้แบบนี้

@EActivity(R.layout.day10_activity_annotations)
public class AnnotationsActivity extends ActionBarActivity {

    @ViewById(R.id.name);
    TextView name;

    @ViewById
    TextView greeting;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
    }

    @Click(R.id.button_click)
    void onClick() {
    }
}

จะสังเกตได้ว่า เราใช้ @ViewById() โดยรับ parameter เป็น id ของ TextView ที่เราตั้ง แต่ในขณะเดียวกัน เราไม่ต้องใส่ parameter ให้กับ @ViewById ก็ได้ แต่ว่าชื่อที่ตั้งในโค๊ด และใน layout ทั้งสองต้องตรงกัน เช่น TextView ในเลเอาท์ มี id ชื่อ greeting ในโค๊ดเราก็ต้องใช้ชื่อ greeting

ส่วน @Click() ตามด้วย parameter เป็นชื่อ id ของ Button คือการ binding และทำการ setOnClickListener() ให้กับ Button โดยเราไม่ต้อง setListerner เอง ไม่ต้องสร้าง inner class เองให้ยุ่งยาก ลดความผิดพลาดลงไปได้เยอะครับ

ข้อควรระวัง อย่าทำการ setText() ให้กับ View เด็ดขาด ไม่เชื่อคุณลอง ทำแบบนี้ โดยพเพิ่ม greeting.setText("Hello I'm John Doe"); ลงไปในเมธอด onCreate()

@EActivity(R.layout.day10_activity_annotations)
public class AnnotationsActivity extends ActionBarActivity {

    @ViewById(R.id.name);
    TextView name;

    @ViewById
    TextView greeting;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        greeting.setText("Hello I'm John Doe");
    }

    @Click(R.id.button_click)
    void onClick() {
    }
}

ลองรันดู ว่าจะมี error มั้ย?

แน่นอนครับ มันจะเกิด NullPointerException เนื่องจากว่า มันยังไม่ถูก Inject ครับ ซึ่งโดยปกติแล้ว Android Annotation มันจะ generate ไฟล์ต่างๆ และทำการ Inject ในช่วง Compile Time นะครับ

อ้าว แล้วงี้เราจะ setText() ยังไงละเนี่ย? วิธีแก้ อ่านต่อด้านล่างครับ :D

@AfterView

@AfterView คือ annotation ที่เอาไว้บอก เมธอดนั้นๆว่า เอ้ย! จะถูก call หลังจาก binding View เรียบร้อยแล้วนะ ( setContentView() และทำการ findViewById() เรียบร้อยแล้ว) ฉะนั้น วิธีที่จะ setText ที่ถูกต้อง ต้องเป็นแบบนี้

package com.devahoy.learn30androidlibraries.day10;

import android.os.Bundle;
import android.support.v7.app.ActionBarActivity;
import android.widget.TextView;
import android.widget.Toast;

import com.devahoy.learn30androidlibraries.R;

import org.androidannotations.annotations.AfterViews;
import org.androidannotations.annotations.Click;
import org.androidannotations.annotations.EActivity;
import org.androidannotations.annotations.ViewById;

@EActivity(R.layout.day10_activity_annotations)
public class AnnotationsActivity extends ActionBarActivity {

    @ViewById(R.id.greeting)
    TextView greeting;

    @ViewById
    TextView name;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
    }

    @Click(R.id.button_click)
    void onClick() {
        Toast.makeText(this, "Hello " ,Toast.LENGTH_LONG).show();
    }

    @AfterViews
    void setGreeting() {
        greeting.setText("Hello I'm John Doe");
    }
}

Features อื่นๆ

ตัว Android Annotation มันไม่ได้ทำได้แค่ ด้านบนนะครับ มันยังสามารถที่จะ Inject System, Service รวมถึงไฟล์ Resource ต่างๆ ทำ UI Thread ทำ Background Thread เรียกได้ว่าครอบคลุมเลยก็ว่าได้ ดูเพิ่มเติม

หลักจากที่ผมได้ลองเล่น Annotation ทั้งสองตัว ทั้ง Butter Knife และ Android Annotation แล้วพบว่า โค๊ดมันแลสวยงาม สบายตาขึ้น ง่ายต่อการแก้ไข ปรับปรุงมากครับ คิดว่าโปรเจ็คในอนาคต จะได้ลองพวก Inject View แน่นอน ส่วนเรื่อง Perfomance อันนี้ก็ไม่รู้นะครับ ว่าจะมีผลแค่ไหน เนื่องจากผมก็เป็นแค่ผู้ใช้ธรรมดาคนหนึ่ง ก็อาศัยอ่านจาก Docs เอาแหละครับ

สุดท้ายอยากจะบอกว่า ใน Wiki ยังมีรายละเอียดให้ศึกษาอีกเยอะมากๆครับ สำหรับบทความนี้ก็เป็นแค่การแนะนำ เบื้องต้นเท่านั้นครับ เพราะว่าผมเพิ่งลองใช้งานเมื่อกี้เอง เรียกได้ว่า ทำไป เขียนรีวิวไป มากกว่า :D

Chai

Chai Phonbopit : Developer แห่งหนึ่ง • ผู้ชายธรรมดาๆ ที่ชื่นชอบ Node.js, JavaScript และ Open Source มีงานอดิเรกเป็น Acoustic Guitar และ Football

บทความล่าสุด