Observable Field กับ 2 ways binding ที่หายไป

ความเดิมตอนที่แล้ว

จากบทความ มาใช้ Android Architecture Components กันเถอะ แสดงให้เห็นถึง 1 ในข้อควรระวังสำหรับการใช้งาน Android Architecture Components — คือ ความสามารถของ 2 Ways Binding ที่หายไประหว่างชั้น viewModel และ View สามารถอ่านย้อนหลังได้ตาม link ข้างล่างนะครัช

วันนี้เราจะมาทำการ Hack เพื่อเอาความสามารถเดิมกลับมา


Resource ที่จำเป็นสำหรับบทความนี้

ก่อนอื่นสำหรับผู้ที่สนใจทำตามบทความ สามารถเข้าไปโคลนโค้ดได้เลยเด้อ

ในโปรเจคมีอะไรบ้าง?

ความตั้งใจในการสร้าง Code repository สำหรับ Android Workshop คือต้องการแชร์ความรู้, ปัญหาที่เจอในการทำงานจริง และคัดย่อเอาเฉพาะส่วนย่อยๆ มาเขียนเป็นบทความ

โฟกัสที่ workshop1 ก่อนนะ

ดังนั้นเมื่อเปิดโปรเจคมา หวังว่าจะมี Module workshopXX มากขึ้นเรื่อยๆ สำหรับบทความนี้ ให้สนใจเฉพาะ workshop1

Disclaimer: โปรเจคนี้จะเขียนโดยใช้ MVPVM เป็นหลัก ส่วนทำไมต้อง MVPVM นั้น ไว้ค่อยหาโอกาสเขียนบทความอีกนะ

ขอบเขตของโปรเจค

หน้า Login อย่างง่าย โดยจะมี Input เป็น Email, Password และ ปุ่ม Sign in โดยผู้ใช้จะสามารถกดปุ่ม Sign in ได้ ก็ต่อเมื่อมีการกรอกค่า Email และ password — และเมื่อทำการกดปุ่ม Sign in จะมีการแสดง Toast เป็น email password และ เคลียร์ค่า Email, Password

ตัวอย่าง Screen shot ของ Application

เมื่อเปิดเข้าไปดูในส่วนของ workshop1 จะมีการแบ่ง Package ได้ตาม feature ของ Application จะไม่แบ่งตาม Layer ของ Architecture ดังรูปข้างล่าง

รูปแบบ file structure
Note: ชื่อ Package ไม่ควรมี prefix นะ แต่ในที่นี่เพื่อต้องการให้ package มันเรียงตามลำดับเนื้อหาที่จะกล่าวถึง เลยจำเป็นต้องใส่ prefix

ใน workshop ชุดนี้จะเป็นการ เปรียบเทียบวิธีการเขียนโค้ด เพื่อให้ได้ตามขอบเขตของโปรเจค 4 แบบด้วยกัน ได้แก่

  1. วิธีการเขียนแบบดั้งเดิม
  2. การใช้ Data Binding
  3. การใช้ Android Architecture Components
  4. การใช้ Android Architecture Components + 2 ways binding

ก่อนอื่นมารู้จัก One Way Binding กับ Two ways Binding กันก่อน

One Way Binding

คำนิยามง่ายๆ ดัง Model เปลี่ยน View ของดรอยก็เปลี่ยน จบ — แต่ One Way Binding จะมีความลำบากในการอัพเดทค่าจาก View กลับไปทาง Model สามารถดูได้จาก Diagram ข้างล่าง

One Way Binding Diagram

จาก One Way Binding Diagram จะเห็นได้ว่า Presenter จะเป็นคนคอยอัพเดท ViewModel จากนั้น ViewModel จะ Notify Change ไปยัง View เพื่อให้ View อัพเดทค่า ทุกอย่างดูสวยงาม จนกระทั้งมี User เข้ามาเกี่ยวข้อง — “555”

จากนั้น User ตัวร้ายของเราทำการอัพเดท View แต่ แต่ แต่ — แต่ View ดันต้องส่ง listener กลับไปที่ Presenter เพื่อให้ Presenter อัพเดท ViewModel อีกกลับมาอีกครั้ง เพื่อที่จะให้ ViewModel อัพเดทค่าตามที่ User ได้ตั้งใจ

จะเห็นได้ว่า การเข้ามาเกี่ยวของ User ตัวร้าย ทำให้นาย โปรแกรมเมอร์เย็นชา ต้องเขียนโค้ดเยอะขึ้น — บักก็อาจจะตามมาอีก แล้วเราจะทำยังไงดีหล่ะ?

แล้วทำไม View จะอัพเดท ViewModel ไม่ได้ ในเมื่อทั้งคู่ตัวติดกันจะตาย? — คำตอบคือ ก็ได้หน่ะสิ

Two Ways Binding

Two Ways Binding Diagram

เมื่อเทียบความปวดหัวกับ One Way Binding แล้ว ดูเหมือนว่า Two Ways Binding จะตั้ลร้ากกว่าเยอะ ขั้นตอนจาก Presenter ไปสู่ View ทุกอย่างเหมือนเดิมจ้า เว้นแต่ตอนที่ User เข้ามาเกี่ยวข้อง หลังจาก User อัพเดท View ตัว View เองก็อัพเดท ViewModel เลยจ้า — Presenter ก็สามารถ เข้าถึง ViewModel ด้วย Getter อยู่แล้ว — เห็นมะ ง่ายเข้าไปอีก

ดังนั้นนิยายง่ายๆ ของ Two Ways Binding คือ “เปลี่ยนมาเปลี่ยนกลับไม่โกง” — Model เปลี่ยน View ก็เปลี่ยน — เมื่อ View เปลี่ยน Model ก็เปลี่ยนด้วย

แต่ต้องระวังการใช้หน่อยนะ เมื่อใครก็ได้สามารถเปลี่ยน ViewModel ได้ มันก็จะ Debug ยากๆ หน่อย เราต้องเข้าใจการทำงานมันจริงๆ นะ

เกริ่นมาเยอะแล้ว งั้นเรามาเริ่ม Code กันเหอะ — เห้

วิธีการเขียนแบบดั้งเดิม — Old School Android

ไม่มีอะไรยาก God Activity เลยจ้าาา

เริ่มจาก LoginActivity.kt

บ้านๆ เลยจ้า findViewById ไปเลย ประกาศคำสั่ง Event ทุกอย่าง ในนี้ไปเลย — เยี่ยม

จากนั้น activity_login.xml

ก็แต่มี EditText สำหรับ Email และ Password แล้วก็มีปุ่ม — ทุกอย่างงดงาม

จากโค้ดชุดนี้จะเห็นได้ว่า Activity อัพเดท View รอ Event จาก View จัดการ Logic — เออ ทำมันทุกอย่างนั้นแหละ

แต่การเขียนโค้ดแบบนี้มันดีมั้ย? — ไม่ดีจ้าาาา ขอตอบเลย อันนี้ยังดีที่ Feature ไม่ได้ซับซ้อนมากนัก เรายังไล่โค้ดได้อยู่ แต่ถ้า Activity บวมล่ะก็ — เหอะๆ เตรียมตัวเจอเคล็ดวิชาโค้ด 1000 บรรทัดได้เลย

วิธีแก้ให้โค้ดดีขึ้นนั้นเหรอ — เขียนเป็น Pattern สิ จะ MVC, MVP, MVVM หรือ MVPVM ก็แล้วแต่ เอาตามความเหมาะสมเลย ในโค้ดชุดถัดๆ ไปจะใช้ MVPVM เป็นหลักนะ


การใช้ Data Binding

ต่อมาการใช้ Data Binding เมื่อพูดถึง binding แล้วสิ่งต่อตามที่ควรจะตามมาติดเลยคือ — ViewModel งั้นเราไปดูโค้ดส่วน ViewModel กันก่อนเนอะ

note: ViewModel คือ class ใดๆ ก็ตามที่เก็บข้อมูล ที่เกี่ยวข้องกับ View ข้อมูลในที่นี้รวมถึง String, Int, Boolean, Listener หรืออะไรก็ตามแต่ที่เราจะ set กลับไปที่ View ได้

LoginDataBindingViewModel.kt

ใน Class นี้จะเก็บ ข้อมูลหลักๆ อยู่ 4 อย่าง ได้แก่ email, password, onClockLogin และ readyToLogin

จากโค้ดจะเห็นได้ว่าทุกครั้งที่มีการ Set ค่า email หรือ password จะมีการ notify ค่าของ readyToLogin กลับไปด้วย — แล้วมันไปเกี่ยวข้อง กับ View อย่างไรหล่ะ งั้นเราไปดูตัว XML กันเลย

activity_login_with_binding.xml

จากโค้ดจะเห็นได้ว่า xml ตัวนี้รอรับค่า ViewModel อยู่นะ แถมยังยัดค่าจาก ViewModel ลง Attribute ต่างๆ ของ View อีก สังเกตได้จาก…

13    android:text="@={viewModel.email}"
:
25 android:text="@={viewModel.password}"

บรรทัดที่ 13 และ 25 ของโค้ด มีการทำ Two Ways Binding — จาก Diagram ข้างบน เรารู้ได้ทันทีเลยว่า email และ password จะมีการอัพเดทจากฝั่ง User ด้วย

การใช้ Binding มันมีพลังมาก เพียงแค่ Set Attribute ด้วย ViewModel ที่เป็นตัวแปรชนิดเดียวกัน ไม่ว่าอะไรๆ ก็ Bind ได้

35    android:enabled="@{viewModel.readyToLogin}"
36 android:onClick="@{viewModel.onClickLogin}"

จากบรรทัดที่ 35 เป็นชุดคำสั่งที่คอยบอกให้ปุ่ม Enable หรือ Disable — และบรรทัดที่ 36 เป็นคนคอยตั้งค่า Listenner

จากนั้นเราก็โยนหน้าที่การจัดการต่างๆ ไปให้ Presenter เลยจ้า

LoginWithBindingPresenter.kt

Presenter ทำหน้าที่คิด logic ให้ และจัดการกับ Event ต่างๆ

จากโค้ดตอนที่ Presenter ถูก init มันจะ ตั้งค่า onClickLogin ให้ ViewModel จากนั้นรอรับเหตุการณ์จาก “Click” แล้วบอกให้ ViewInf (Interface ที่ Activity implement) performLogin — จากนั้น reset ค่าของ ViewModel — งั้น Activity ทำอะไรบ้าง ไปดู Activity กันเหอะ

LoginWithBindingActivity.kt

จากโค้ดจะเห็นว่า Activity ทำหน้าที่เพียง Init, Init แล้วก็ Init — แล้วก็ทำ Action ที่อาศัย Android Library — เพราะ Presenter ไม่ควรรู้จัก Context หรือ Toast ซึ่งเป็น Android Library ดังนั้น มันจึงให้ Activity จัดการให้ โดยเรียกผ่าน Interface ViewInf

ทุกอย่างมันดูสวยงามแล้ว ทำไมท่านถึงไม่หยุดหล่ะ?

ก็เพราะว่าวิธีนี้มันยังมีปัญหาอยู่ไงหล่ะ ไหนจะการ แชร์ค่า ViewModel ในจะการเก็บ State เมื่อหมุนจอ — Google เอง ก็ใจดีนะ ออก Library ใหม่มาให้ใช้ ก็คือ Android Architecture Components นั้นเอง ซึ่งมันก็มาพร้อมๆ กับ Class ViewModel ตัวใหม่ด้วย


การใช้ Android Architecture Components

LoginArchViewModel.kt

สิ่งที่ทำ คือ Refractor โค้ดในส่วน ViewModel ของโค้ดชุด Data Binding ให้มา extend ViewModel แทน BaseObservable และเปลี่ยนค่า Field ต่างๆ เป็น ObservableField

activity_login_arch.xml

ในส่วนของ XML เปลี่ยนไม่มากนัก เพียงแค่แก้เครื่องหมาย “@={” เป็น “@{” เพื่อเป็นการแสดงออกทางสัญลักษณ์ว่าต่อไปนี้ View จะไม่อัพเดท ViewModel แล้วนะ — ต่อให้ยังใช้เครื่องหมายเดิม “@={” มันก็ไม่สามารถที่จะอัพเดทค่ากลับมาได้

ทำไมทำ 2 ways binding ไม่ได้แล้ว? เพราะตัวแปร email และ password กลายเป็น ObservableField แล้ว มันไม่ใช่ String อีกต่อไป มันจึงเอาแค่ String มายัดใส่ ObservableField ไม่ได้ — อ่าว เห้ย มันเป็น ObservableField แต่ทำไมมันแสดงผลได้หล่ะ ผมก็เกิดความสงสัยนี้เช่นกัน เลยลองไปแกะโค้ด Binding ที่ถูก Generate ขึ้นมา ก็ถึง บางอ้อเลยจ้าาา

โค้ดในส่วนการนำค่าไปแสดงผล

นั้นไง เต็มๆ เลย เพราะมัน เรียก method.get() หน่ะสิ มันเลยได้ Sting กลับไปใช้งาน แหมทำมาขนาดนี้แล้ว ไม่ Provide setter มาบ้าง — แต่ชีวิตเรายังต้องไปต่อ งั้นเราก็ด้นสดสิ ตาม Diagram One way binding ข้างบนหน่ะ แน่นอนว่า Presenter เราต้องรอรับ Event OnChange ดังนั้นเราก็ Implement method รอสิ จะช้าทำไม

LoginArchPresenter.kt

ที่สำคัญ ตอนนี้เรา Notify ให้ data ของ readyToLogin ใน ViewModel เปลี่ยนไม่ได้ เราจึงต้องให้ Presenter จัดการให้ คือทุกครั้งที่มีการอัพเดทค่า email หรือ password ให้อัพเดทค่า readyToLogin ด้วย — โอ้ พระสงฆ์! เพียงแค่ 2 ways binding ที่หายไป Presenter ต้องรับศึกหนักขึ้นเยอะเบย

แน่นอนว่า Presenter ไม่สามารถรู้ได้ว่า View มีการเปลี่ยน นอกจากตัวของ View เอง — ดังนั้น Activity ก็รับศึกหนัก ไม่แพ้กัน

LoginArchActivity.kt

สิ่งที่เพิ่มเติมขึ้นมาคือ InitEvent ที่ต้องรอรับ Listener จาก TextWatcher จากนั้น ให้ Presenter อัพเดทค่าต่างๆ ให้ เห็นมั้ยว่าโค้ดมัน ไม่น่ารักเลย ถ้าเป็นหน้า Form เยอะๆ เช่นการกรอกโปรไฟล์เนี่ย แค่คิดจะเขียนก็ร้องแล้ว — งั้นเรามาเอา 2 ways binding กลับมาเถอะ


การใช้ Android Architecture Components + 2 ways binding

จากที่กล่าวไปก่อนหน้า ViewModel ถืออะไรก็ได้ ที่ยัดใส่ View ได้ — งั้นเรามายอมรับความจริงง่ายๆ ข้อนึงก่อน ก็คือ EditText เป็น View → true, EditText สามารถ addTextChangedListener ได้ → true, addTextChangedListener รับ Parameter เป็น TextWatcher → true; ดังนั้น ViewModel ก็ถือ TextWatcher แล้ว set กลับไปที่ View ได้สิ — ก็ได้หนะสิ

แต่เราจะประกาศ TextWatcher หลายๆ ตัว เหมือน method initEvent() ของ LoginArchActivity.kt หรือป่าว — อย่า!! ใครทำเดี่ยวตีมือเลย มันก็แค่ย้ายโค้ด รกๆ จากที่นึงไปอีกที่นึงเอง ไม่ได้ทำอะไรเลย — แล้วเราจะสร้าง TextWatcher แบบ โค้ดไม่รกยังไงดี — แท่นแท๊น สร้าง Class ที่ implement TextWatcher สิ

TextWatcherAdapter.kt

class นี้ก็แค่ class นึงที่ Implement TextWatcher มา แต่ข้อสังเกตของ class จะอยู่ตรง Constructor ตรง Object type ที่แปลกๆ นั้นแหละ

1   private var field: (String) -> Unit 

จากบรรทัดที่ 1ไม่ต้องตกใจ หน้าตามัน อาจจะแปลกๆ หน่อย แต่จริงๆ แล้วมันคือ parameter ที่รับ Action เมื่อมีการ Invoke String นั้นเอง

19    field.invoke(s)

เมื่อได้ค่ากลับไป จะมีการ Invoke String กลับไปทาง ViewModel แล้วให้มันจัดการค่า String ต่อไป — งั้นเราลองไปดู ViewModel กัน

LoginArch2WaysBindingViewModel.kt

ข้อสังเกต จะเห็นว่า ViewModel มีค่าของ emailOnChange และ passwordOnChange อยู่ด้วยนะเออ

6    var usernameOnChange = TextWatcherAdapter({s ->
7 username!!.set(s)
8 setCanLoginButton()
9 })
10 var passwordOnChange = TextWatcherAdapter({s ->
11 password!!.set(s)
12 setCanLoginButton()
13 })

จากบรรทัดที่ 6 และ 10 เมื่อ TextWatcher Invoke String กลับมามันก็จะกลับมาอัพเดทค่าของ email และ password ตามลำดับ

อ่าวแล้ว ตัว XML ต้องทำอย่างไรถึงจะตั้งค่า TextWatcherAdapter ทั้ง 2 ได้ งั้น เราไปดู XML กันเหอะ

activity_login_arch_2_ways_binding.xml

ข้อสังเกต

8     android:addTextChangedListener="@{viewModel.emailOnChange}"
.
.
.
21 android:addTextChangedListener="@{viewModel.passwordOnChange}"

ก็แค่ยัด addTextChangedListener ลงไปโต้งๆ นั้นแหละ — ส่วน Presenter ก็เหมือนเดิมเลย

LoginArch2WaysBindingPresenter.kt

LoginArch2WaysBindingActivity.kt

แทบจะไม่ต่างกับวิธีการเขียนของ Data binding เลย แค่ init, init แล้วก็ init

เห็นมั้ยโค้ดน่ารักขึ้นเยอะ, มาเหอะ จะเขียน EditText สัก 20 ตัวก็จัดมา ไม่กลัวแล้ว จะเห็นได้ว่าการเอาความสารถ 2 Ways Binding กลับมา มันช่วยลด lines of code ได้จริงๆ นะ


สรุป

จากโค้ดต่างๆ ที่นำเสนอข้างต้น ตั้งแต่การเขียน แบบดั้งเดิม จนกระทั้ง เขียนโดยใช้ Android Architecture Components + 2 ways binding ล้วนเกิดปัญหาต่างๆ ขึ้นในแต่ละวิธี เราจึงต้องคิดเทคนิค วิธีการ เข้ามาแก้ปัญหาจนได้วิธีการแก้ปัญหาที่หลากหลาย แต่แน่นอนว่าหนึ่งปัญหาย่อมมีหลายทางออก วิธีนี้อาจจะไม่ใช้วิธีที่ดีที่สุด ยังไงก็ตามใครมีความคิดเห็นใดๆ แลกเปลี่ยนกัน ก็แบ่งปันกันได้นะ

ในส่วนของ Final code

จากบทความ มาใช้ Android Architecture Components กันเถอะ ได้อธิบายข้อดีต่างๆ มากมายของตัว Architecture Components เอง แต่ก็มีข้อจำกัดเรื่อง 2 ways binding — ในโปรเจคที่ทำอยู่ ก็เจอปัญหานี้เช่นกัน เพราะ 2 ways binding ช่วยลดโค้ดลงเยอะมาก ไม่ต้องโยน data ไปๆ มาๆ ก็เลยมีความพยายามที่จะ Hack ความสามารถ 2 ways binding กลับมา ผลก็เป็นอย่างโค้ดที่เห็นหล่ะครับ

ขอบคุณที่อ่านจนถึงบรรทัดนี้นะครับ