บอกลา findViewById ด้วยเทคนิคการหา View อัตโนมัติโดยใช้ Data Binding Library

Posted on 28 Jun 2016 13:51 | 19941 reads | 0 shares
 

การ findViewById เป็นวิธีการหา View ในหน้าต่างๆของแอปแอนดรอยด์ที่มีมาช้านานตั้งแต่แอนดรอยด์ถือกำเนิดมา แต่ถ้าใครเขียนแอนดรอยด์เยอะๆก็จะเริ่มรู้สึกรำคาญ นี่จะ findViewById อะไรนักหนากันนะ ถ้ามี View สัก 30 อันในหน้านั้นก็หากันน้ำตานองเลย พิมพ์ผิดนิดหน่อยก็อาจจะทำให้แอปมีปัญหาได้

ด้วยเหตุนี้ก็เลยมีคนพยายามทำไลบรารี่เพื่อช่วยลดโค้ดตรงนี้ลง ที่ได้รับความนิยมสูงมากก็คงจะเป็น Butter Knife ที่ใช้ Annotation เข้ามาช่วยในการ Map ระหว่างตัวแปรและ ID ใน Layout

class ExampleActivity extends Activity {
    @BindView(R.id.title) TextView title;
    @BindView(R.id.subtitle) TextView subtitle;
    @BindView(R.id.footer) TextView footer;

    @Override public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.simple_activity);
        ButterKnife.bind(this);
        // TODO Use fields...
    }
}

อย่างไรก็ตาม ถึงโดยรวมจะดีขึ้นมากแต่โค้ดก็ยังไม่ได้อยู่ในจุดที่สั้นที่สุดอยู่ดี เพราะเรายังต้องประกาศตัวแปรและต้องมานั่ง @BindView ทีละตัวอีกด้วย

มาวันนี้มีวิธีที่สามารถกำจัดโค้ดเหล่านี้ให้หายวับไปอย่างสิ้นเชิงแล้วโดยใช้ความสามารถของไลบรารี่ตัวเทพอย่าง Data Binding Library ที่พัฒนามาถึงจุดที่ช่วยให้ชีวิตดีขึ้นมากมาย มาลองกัน!

เตรียมเครื่องมือ

การเปิดใช้ Data Binding Library จะต้องใช้ Android Studio 1.5 เป็นต้นไป ซึ่งตอนนี้ทุกคนน่าจะเป็น 2.0 กันหมดแล้ว ไม่น่ามีปัญหาอะไร

จากนั้นให้เปิดไฟล์ build.gradle ของโมดูลที่ต้องการจะใช้งาน Data Binding ยกตัวอย่างเช่น app/build.gradle แล้วเพิ่มโค้ดส่วนนี้เข้าไปในช่องของ android

android {
    ...
    dataBinding {
        enabled true
    }
}

จากนั้นก็ Sync Gradle ให้เรียบร้อย เพียงเท่านั้นเราก็สามารถใช้งาน Data Binding ได้แล้ว ง่ายใช่ม้า

เปลี่ยนโค้ดให้ใช้ Data Binding แทนการ Inflate ปกติ

ในการใช้งาน Data Binding เราจะต้องเปลี่ยนวิธีการ Inflate ให้ไปใช้วิธีของ Data Binding แทน อย่างแรกที่ต้องทำคือเปลี่ยนโครงสร้างไฟล์ layout จากเดิมที่วาง Layout ไปเลยแบบนี้

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout 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/activity_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.inthecheesefactory.lab.databinding.MainActivity">

    <TextView
        android:id="@+id/tvHello"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        android:text="Hello World!" />

</RelativeLayout>

ให้เพิ่ม tag <layout>...</layout> เข้าไปครอบเป็น Root Element ซ้อนตัวเดิมไปอีกชั้นนึง

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

    <RelativeLayout
        android:id="@+id/activity_main"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context="com.inthecheesefactory.lab.databinding.MainActivity">

        <TextView
            android:id="@+id/tvHello"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerInParent="true"
            android:text="Hello World!" />

    </RelativeLayout>
</layout>

จากนั้นให้กด Build Project ก่อนทีนึงเพื่อให้ Data Binding Library สร้างโค้ดที่จำเป็นขึ้นมาให้เราโดยอัตโนมัติ

พอ Build เสร็จก็ให้เข้าไปแก้ไขไฟล์ Activity เปลี่ยนคำสั่ง setContentView จากเดิม

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }

}

ให้กลายเป็น

public class MainActivity extends AppCompatActivity {

    ActivityMainBinding binding;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
    }

}

โดยคลาส ActivityMainBinding เป็นคลาสที่ถูกสร้างโดยอัตโนมัติ (Auto Generated) จาก Data Binding Library นั่นเอง ซึ่งชื่อคลาสก็มาจากชื่อไฟล์ activity_main.xml ถูกเปลี่ยนเป็น Upper Camelcase ก่อนจะต่อท้ายด้วย Binding สุดท้ายเลยกลายเป็น ActivityMainBinding และนี่เป็นวิธีการแปลงชื่อไฟล์เป็นชื่อคลาสครับ ใช้กับทุกกรณีของไลบรารี่ตัวนี้

จากนั้นเราจะสามารถเข้าถึงตัวแปรต่างๆใน activity_main.xml ผ่านตัวแปร binding ได้ทันที! ยกตัวอย่างเช่นหากอยากเข้าถึง TextView ที่ประกาศไว้ก็สั่งตามนี้ได้เลย

binding.tvHello.setText("Hello from Data Binding");

ผลการทำงานเป็นแบบนี้

ก็จะสังเกตดู จะเห็นว่าโค้ดสั้นลงมาก และต่อให้มี View เพิ่มอีก 100 ตัว โค้ดส่วนนี้ก็ยังเท่าเดิม สะดวกขึ้นมากๆ

การ Inflate ลง Custom ViewGroup

ด้านบนเราสอนวิธีการ Inflate ลง Activity กันแล้ว แล้วถ้าจะ Inflate ลง Custom ViewGroup เพื่อสร้าง View ของตัวเองหละ ทำได้มั้ย? คำตอบคือทำได้ครับ ง่ายไม่ต่างกันเลย เริ่มแรกก็เช่นเคย สร้าง Layout ขึ้นมาก่อนในรูปแบบเดิมคือใช้ <layout>...</layout> ครอบไว้เป็น Root Element ยกตัวอย่างเช่น

item_bloglist.xml

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        android:padding="16dp">

        <TextView
            android:id="@+id/tvTitle"
            style="@style/TextAppearance.AppCompat.Title"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Title" />

        <TextView
            android:id="@+id/tvCaption"
            style="@style/TextAppearance.AppCompat.Caption"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Caption" />

    </LinearLayout>

</layout>

ส่วนโค้ดการ Inflate เราจะใช้คำสั่ง

ItemBloglistBinding binding = ItemBloglistBinding.inflate(layoutInflater, root, attachToRoot);

ซึ่งหน้าตาจะเหมือนการ inflate ปกติที่เราเคยทำเลย เพียงแต่จะใช้คลาสที่ถูกสร้างขึ้นมาโดยอัตโนมัติอย่าง ItemBloglistBinding ในการ inflate ให้เราแทน ซึ่งชื่อคลาสนี้ตัวก็ถูกแปลงมาจากชื่อไฟล์ item_bloglist.xml นั่นเอง

ส่วนอันนี้เป็นตัวอย่างโค้ดเต็มของการ Inflate ลง FrameLayout ครับ

package com.inthecheesefactory.lab.databinding;

import android.annotation.TargetApi;
import android.content.Context;
import android.os.Build;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.widget.FrameLayout;

import com.inthecheesefactory.lab.databinding.databinding.ItemBloglistBinding;

/**
 * Created by nuuneoi on 6/28/2016.
 */

public class BlogListItem extends FrameLayout {

    ItemBloglistBinding binding;

    public BlogListItem(Context context) {
        super(context);
        initInflate();
        initInstances();
    }

    public BlogListItem(Context context, AttributeSet attrs) {
        super(context, attrs);
        initInflate();
        initInstances();
    }

    public BlogListItem(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initInflate();
        initInstances();
    }

    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    public BlogListItem(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        initInflate();
        initInstances();
    }

    private void initInflate() {
        LayoutInflater inflater = (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        binding = ItemBloglistBinding.inflate(inflater, this, true);
    }

    private void initInstances() {

    }

}

เพียงเท่านี้เราก็จะได้ Custom ViewGroup ที่มี Layout ของ item_bloglist.xml แปะอยู่แล้ว และหากต้องการเข้าถึงตัวแปรใน Layout ก็เข้าถึงผ่านตัวแปร binding ได้เลยเช่นเดิม

    private void initInstances() {
        binding.tvTitle.setText("I am the Title");
    }

อันนี้ลองเอาเจ้า BlogListItem ไปแปะลงใน activity_main.xml ตามนี้

<?xml version="1.0" encoding="utf-8"?>
<layout
    ...>

    <RelativeLayout
        ...>

        ...

        <com.inthecheesefactory.lab.databinding.BlogListItem
            android:layout_width="match_parent"
            android:layout_height="wrap_content" />

    </RelativeLayout>
</layout>

และนี่คือผลที่ได้ครับ

โค้ดสั้นลงไปเยอะมาก ชิว =)

รูปแบบการแปลง ID เป็นชื่อตัวแปร

ก็จะเห็นว่า @+id ในไฟล์ XML จะถูกแปลงเป็นชื่อตัวแปรในคลาส XXXBinding โดยอัตโนมัติ ซึ่งเราควรทำความเข้าใจวิธีการแปลงชื่อตัวแปรสักเล็กน้อยเพื่อไม่ให้เกิดปัญหาในอนาคต

กฏการแปลงชื่อตัวแปรนั้นค่อนข้างตรงไปตรงมาคือ จะแปลง ID ทุกตัวให้กลายเป็น camelCase

ยกตัวอย่างเช่น

@+id/tvHello

อันนี้เป็น camelCase อยู่แล้ว ก็จะได้เป็นตัวแปรชื่อ tvHello ในจาวาทันที

@+id/tv_hello

อันนี้อยู่ในรูปแบบ Underscores ก็จะถูกแปลงเป็น camelCase ก่อนเช่นกัน กลายเป็นชื่อตัวแปรเดียวกัน tvHello ครับ

แล้วจะเกิดอะไรขึ้นหาก id สองตัวนี้ปรากฎอยู่ในไฟล์เดียวกัน? เช่น

        <TextView
            android:id="@+id/tvHello"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerInParent="true"
            android:text="Hello World!" />

        <TextView
            android:id="@+id/tv_hello"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerInParent="true"
            android:text="Hello World!" />

ไม่ต้องกลัวว่าจะมีปัญหาครับ Data Binding Library ฉลาดพอที่จะแยกว่าสองตัวนี้เป็นคนละตัวกัน ผลสุดท้ายจะได้ตัวแปรมาสองตัวด้วยกันได้แก่ tvHello และ tvHello1 ซึ่งเอาไว้อ้างอิง View ทั้งสองตัวตามลำดับ

อย่างไรก็ตาม ถึงจะสามารถใช้งานได้แต่หากดูชื่อตัวแปรแล้วก็จะเห็นว่าอาจจะทำให้เกิดการสับสนและการทำงานผิดพลาดในอนาคตได้ ดังนั้นคำแนะนำคือให้ตั้งชื่อ @+id เป็นรูปแบบใดรูปแบบหนึ่งเท่านั้นไม่ว่าจะเป็น camelCase หรือ Underscores ก็ตาม แล้วก็จะไม่เกิดปัญหาการซ้ำซ้อนของชื่อตัวแปรขึ้นแน่นอนครับ

สรุป

สำหรับ Data Binding Library ยังทำอะไรได้อีกเยอะมาก แต่สำหรับฟีเจอร์นี้ขอยกให้เป็นหนึ่งใน Killer Feature ซึ่งจะทำให้ชีวิตการเป็นนักพัฒนาแอปแอนดรอยด์ของคุณเปลี่ยนไปมากทีเดียว ลองใช้ดูครับแล้วจะติดใจ =)

ของแถม: Kotlin Android Extensions

ใครมาสาย Kotlin ก็น่าจะคุ้นเคยกับคอนเซปต์ที่ผมยกมาในบล็อกนี้ดีเพราะมันมีสิ่งที่เรียกว่า Kotlin Android Extensions อยู่แล้ว วิธีคล้ายๆกัน ต่างกันตรงที่ไม่จำเป็นต้องเปลี่ยนแปลง Layout อะไร ยังไงใครใช้ Kotlin ลองไปอ่านกันดูได้ครับ

ผู้เขียน: nuuneoi (Android GDE, CTO & CEO at The Cheese Factory)
นักพัฒนาแบบ Full-Stack ที่มีประสบการณ์ในการพัฒนาแอพฯแอนดรอยด์มากว่า 6 ปีและอยู่ในวงการพัฒนาแอพฯมือถือมากว่า 12 ปี มีความสนใจทางด้าน Infrastucture, Service Side, Design, UI&UX, Hardware, Optimization, Cooking, Photographing, Blogging, Training, Public Speaking และรักที่จะแชร์เรื่องราวให้ผู้คนได้อ่านได้ฟังกันผ่าน Blog