23 March 2014

ทำไมต้อง findViewById ? คิดเล่นๆ แต่ได้อะไรมากกว่าที่คิด !

Updated on

        คำถามนี้ถามง่ายๆเลย แต่เจ้าของบล็อกเชื่อว่ามีผู้ที่หลงเข้ามาอ่านหลายคนที่ตอบกันไม่ได้ บอกได้แค่ว่าเขียนๆตามกันมา แต่เคยสงสัยกันมั้ยมันมีที่มาอย่างไร? บทความนี้อาจจะดูเหมือนไม่สำคัญอะไร แต่ทว่าถ้าได้อ่านก็อาจจะร้องอ๋อก็เป็นได้

        ไม่ว่าจะเป็น Button TextView EditText ListView หรือ View ต่างๆที่อยู่ในหน้า Layout XML ล้วนจะต้องใช้ findViewById ทั้งนั้น ซึ่งผู้ที่หลงเข้ามาอ่านคงจะรู้ดี

Button button = (Button)findViewById(R.id.button);

        ทั้งๆที่ View เหล่านี้มันก็เป็น Class ทั้งนั้นนี่นา โดยปกติการประกาศคลาสใดๆผู้ที่หลงเข้ามาอ่านมักจะคุ้นเคยในรูปแบบนี้

Object object = new Object();

        อันนี้ยกตัวอย่างเป็นคลาส Object ที่ประกาศชื่อเป็นobjr แล้วกำหนดค่าโดยสร้าง Constructor ขึ้นมาใหม่

        แล้วทำไม View ถึงไม่ได้ประกาศแบบนั้นล่ะ?

        จริงๆแล้วก็ไม่ใช่ว่าจะทำไม่ได้หรอกนะ เพราะผู้ที่หลงเข้ามาอ่านก็สามารถประกาศ View แบบนี้ได้เช่นกัน

Button button = new Button();

        แต่ทว่า button ตัวนี้ จะกำหนดให้มันไปเชื่อมกับ Button ที่สร้างไว้ใน XML ได้อย่างไรล่ะ?
        .
        .
        .
        .
        .
        หยุดไว้ที่ตรงนี้ก่อนละกัน แล้วเปลี่ยนมาถามคำถามใหม่ว่า

        จะรู้ได้ไงว่า Activity ตัวนั้นๆใช้ Layout ไฟล์ไหน? 

        แน่นอนอยู่แล้วว่าไม่ได้ดูจากชื่อที่เหมือนกัน เพราะว่า Activity จะชื่ออะไรก็ได้ และ Layout จะชื่ออะไรก็ได้เช่นกัน เพียงแต่ว่าเวล่ากำหนดจะกำหนดด้วยคำสั่ง

setContentView(R.layout.activity_main);

        คำสั่งนี้จะค้องเรียกใช้ทุกครั้งเมื่อเข้าสู่ onCreate ซึ่งก็คือการกำหนดว่า Activity นี้ใช้ Layout อะไรนั่นเอง ถ้ายกตัวอย่างเป็นภาพก็จะได้ประมาณนี้


        เมื่อใช้คำสั่งนี้ก็จะทำให้ Activity นั้นๆ กำหนดทันทีว่าใช้กับ Layout ไหนอยู่ แต่อย่าลืมว่าใน Layout ได้มีการสร้าง View ต่างๆไว้อย่างพวก Button หรือ TextView เป็นต้น ดังนั้น Layout ก็จะมีลักษณะแบบนี้



        ถ้ามองภาพรวมในตอนนี้ก็จะได้ออกมาเป็นแบบนี้



        จะเห็นว่า Layout ถูกกำหนดให้กับ Activity เรียบร้อยแล้ว ดังนั้น View ที่อยู่ใน Layout ตัวนั้นก็จะตามไปโดยปริยาย

        และเวลาที่ต้องการเรียกใช้งานก็จะเรียกผ่านคำสั่ง findViewById ก็คือการดึง View นั้นๆออกมาใช้งานนั่นเอง



        ดังนั้นถ้าลองมองโค๊ดกับภาพประกอบรวมกันก็จะเป็นลำดับขั้นตอนดังนี้



        ถ้าไม่ได้กำหนด Layout แล้วไปดึง View มาใช้เลยล่ะ?


        มันก็จะหาไม่เจอเพราะตัว Activity เองยังไม่รู้เลยว่าตัวเองใช้ Layout ไหนอยู่ ดังนั้นกำหนด View ที่จะดึงไปก็เท่านั้น มันก็จะหาไม่เจอแล้วขึ้นเออเรอเมื่อนำ mButton ไปใช้งานทันที

        และมีผู้ที่หลงเข้ามาอ่านคนหนึ่งเคยใช้คำสั่งแบบนี้

protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.layout1);
        setContentView(R.layout.layout2);

        ...
}

        จะเห็นว่ามีการใช้คำสั่งกำหนด Layout ถึงสองครั้งด้วยกัน เนื่องจากเข้าใจว่าถ้าอยากทำแอปพลิเคชันสองหน้าก็ใช้คำสั่งสองครั้ง เจ้าของบล็อกก็เลยเอามาเป็นตัวอย่างให้คิดดูเล่นๆกันว่า แล้ว Activity จะใช้ Layout ไหน?

        ให้สังเกตที่คำว่า set นั่นก็หมายความว่ากำหนดค่าลงไปโดยไม่สนว่าค่าเก่าจะเป็นอะไร ดังนั้นในsetContentView บรรทัดแรกจะกำหนดให้ Activity ใช้ Layout เป็น layout1


        ต่อมาในบรรทัดที่ 2 Layout ก็จะถูกแทนที่ด้วย layout2 ของเดิมก็จะหายไป



        ดังนั้นในกรณีนี้ Activity ก็จะใช้ Layout เป็น layout2 ไปโดยปริยาย คำสั่งแรกจึงไร้ประโยชน์ไป


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

        แล้ว findViewById มันเรียกได้ยังไงล่ะ?

        นั่นก็เพราะว่าคำสั่งนี้เป็น Method หนึ่งของคลาส Activity และ Activity ที่สร้างขึ้นมานั้นสืบทอดมาจากคลาส Activity โดยตรงนั่นเอง

public class MainActivity extends Activity {
        ...
}

        ถ้าไม่ได้สืบทอดคลาสจาก Activity ล่ะ?

        ก็แน่นอนอยู่แล้วว่าไม่สามารถเรียกใช้งานได้ แต่ก็ไม่ได้หมายความว่าต้องสืบทอดมาจากคลาส Activity เท่านั้นนะ เพราะอย่าลืมว่าก็มีคลาสต่างๆที่สืบทอดมาจาก Activity เช่นกัน ดังนั้นถ้าสืบทอดจากคลาสเหล่านี้อีกทีก็จะสามารถใช้คำสั่ง findViewById ได้เช่นกัน


        เจ้าของบล็อกอ้างอิงจาก Activity [Android Developer]

        นอกจากนี้ถ้านำคลาสอื่นๆมาใช้แล้วคลาสนั้นๆสืบทอดมาจาก Activity ก็สามารถเรียกใช้ findViewById เช่นกัน


        ไหนๆก็อ้างอิงจาก Reference กันแล้ว ก็แปะภาพโครงสร้างของคำสั่งนี้เสียหน่อย


     
        จะเห็นว่าคำสั่งนี้ Return ค่ากลับมาเป็นคลาส View ดังนั้นเวลาใช้คำสั่งก็ควรจะเป็นแบบนี้

View view = findViewById(R.id.view);

        เอ....แต่ที่คุ้นเคยนี่มันไม่ใช่ View นี่นา ถ้าสมมติว่าเป็น Button ล่ะ?

Button button = findViewById(R.id.button);

        ปัญหาคือเออเรอร์แน่ๆเพราะว่าคำสั่ง findViewById มัน Return กลับมาเป็น View


        สิ่งที่ทำได้ก็จะมีแค่ Cast Class เท่านั้น หรือก็คือแปลงคลาส View ให้กลายเป็น Button ซะ

Button button = (Button)findViewById(R.id.button);

        เท่านี้ก็กลับสู่จุดเริ่มต้นแล้วว่าทำไมถึงต้องใช้คำสั่งแบบนี้


        แต่ทว่า? อยู่ดีๆจะแปลง View มากลายเป็น Button เลยเนี่ยนะ? ทำไมถึงทำแบบนี้ได้เลยล่ะ?

        นั่นก็เพราะว่า..



        เพราะว่า Button สืบทอดมาจากคลาส View นั่นเอง ซึ่งการ Cast Class สามารถกระทำจากคลาสแม่ไปยังคลาสลูกได้อยู่แล้ว ดังนั้นจึงไม่ได้หมายความว่าจะแปลงคลาสไปมาได้มั่วซั่ว ของแบบนี้มีที่มาเสมอ

        สำหรับคำสั่ง findViewById จะมองแบบนี้ก็ได้

View view = findViewById(R.id.button);
Button button = (Button)view;

        ซึ่งก็คือที่พิมพ์กันอยู่บ่อยๆนั่นเอง

Button button = (Button)findViewById(R.id.button);

        จบแล้วววววว พิมพ์เพลินกว่าที่คิด