เจาะลึกไปถึงชีวิตการทำงานของ Code ใน Repository บน Architecture Component
สำหรับ Android Architecture Component ถือว่าเป็นเรื่องใหม่ และหลายๆคนกำลังศึกษาโครงสร้างนี้ ซึ่งประกอบด้วยหลายๆส่วนงาน ทั้ง View, ViewModel, Model, Data Source, Repository ซึ่งทีม Repository เป็นทีมที่น่าจับตามองอย่างยิ่ง เนื่องจากเป็นส่วนสำคัญที่ทำให้ทีมประสบความสำเร็จ และทำให้ user มีความสุขมากขึ้น ในวันนี้เรามาสอบถาม code ว่ามีแนวทางการทำงานอย่างไร ในวันที่โลกไม่หยุดนิ่ง ให้ประสบความสำเร็จคะ
//ยื่นไมค์ถาม
หลังจากที่เราได้ทำความรู้จัก Android Architecture Component กันไปแล้ว และได้รับเสียงตอบรับที่ดี ต้องขอขอบพระคุณมากๆค่ะ ณ ตรงนี้ค่ะ
แต่ยังมี details เชิงลึกอีกหลายเรื่องที่ต้องศึกษาต่อไป หลังจากเรื่อง Instant Apps ที่ต้องมีการเปลี่ยนโครงสร้างภายนอกของโปรเจกแล้ว ภายในก็อาจจะต้องเปลี่ยนตามด้วย
เรื่องที่น่าสนใจเรื่องนึง คือ Repository เนื่องจากแอปของเรามีการเชื่อมต่อกับ API ในการดึงข้อมูลมาแสดงผลในแอป ปัญหาที่เกิดขึ้น คือ โหลดหน้าแอปได้ช้าถ้าเชื่อมต่ออินเตอร์เน็ทความเร็วตํ่า นอกจากจะแก้ปัญหาในส่วน backend แล้ว ส่วน frontend ก็ต้องแก้ปัญหาเช่นกัน
ดังนั้นในส่วน Repository เป็นส่วนนึงที่น่าสนใจ เพราะแอปปิเคชั่นทั้งหลายบนโลกใบนี้ โดยส่วนใหญ่เรียกผ่าน service กันทั้งนั้น
ความเดิมตอนที่แล้ว
Room ถูกใช้ร่วมกันกับ SQLite ในการเก็บ data หรือ cache ใน local พอ offline ก็เก็บ data แล้วส่งตอน online ทีหลัง ทำให้เราไม่ต้องโหลดข้อมูลจาก API ทุกครั้ง
ในส่วน interface จะมีการทำ query ด้วย SQL ซึ่งหน้าตาจะคล้ายๆตอนเรียก API ซึ่งสามารถเรียกใช้ได้ที่ ViewModel ที่ถูกเรียกใช้ผ่าน Activity และคืนค่าเป็น LiveData
Repository จะช่วยลดความซํ้าซ้อนของโค้ดลง และสามารถทำ persisting data โดยใช้ Room เข้าช่วย ซึ่งสามารถทำ paging ได้ง่ายขึ้นด้วย
ในบล็อกนี้เราจะมาทำความรู้จักมากขึ้นในส่วนของ Repository ว่ามี code มีการทำงานร่วมกันอย่างไร
ณ บ่ายอ่อนๆ ในยามฟ้าสลัว เราก็ได้พูดคุยกับ code ถึงชีวิตการทำงาน เขาได้บอกว่าเขาทำงานในส่วนของ Repository ทำงานร่วมกันกับ Model ที่มี Room ไปติดต่อกับ SQLite และ Remote Data Source ที่ใช้หน่วยงานภายนอกอย่าง Retrofit ไปติดต่อกับ webservice เมื่อรวบรวมข้อมูลทั้งหมด ก็จะรอ ViewModel ที่เป็นคนขี้ขอ เอาไปให้ UI แสดงผล ว่าง่ายๆเขาเป็นผู้ประสานงานในเรื่องของข้อมูลนั่นเอง
แผนผังการทำงานของ Architecture Component
UI Controller (Activity/Fragment) : ส่วนของหน้าตาแอปปิเคชั่น นำข้อมูลที่ได้มาแสดงผล
ViewModel : จัดเตรียมข้อมูลในการแสดงผล ช่วยในการ config พวก data source ทำให้ตัวแอปไม่ต้องแสดงผลใหม่ เมื่อเกิด configuration change และคืนค่าที่ได้มาเก็บที่ LiveData
Repository : เป็นส่วนสำคัญในการจัดการข้อมูลจากในส่วนต่างๆ ไปที่ส่วนแสดงผล ไปให้ ViewModel
Model : ช่วยจัดการข้อมูลใน local storage
Data Source : มีหน้าที่จัดการข้อมูลจาก webservice ทั้งหลาย ไม่ว่าจะเป็น web server หรือ mock-up data ก็ตาม
LiveData : เหมือนคนนี้เป็นเบ้นะ ยังกะ messenger เลย เป็นคนส่งข้อมูลไปยัง Repository และไปที่ ViewModel ถ้ามีการเรียกใช้
จากนั้นแบ่งงานกัน ในที่นี้จะเป็นแอปแสดงสภาพอากาศประจำวัน เราจะ list ว่าใครทำอะไรบ้าง
การทำงานของ ViewModel
ต้องทวนก่อนนิดนึงว่าการทำงานของ ViewModel นั้นไซร์ มันจะต่างกับ lifecycle ปกตินะ คือมันจะทำให้แอปไม่ต้องทำงานใหม่เมื่อเกิด configuration change และมันจะใช้ onCleared() ในการเคลียร์ค่าต่างๆตอนเราออกจากแอป
ความสัมพันธ์ระหว่าง ViewModel และ LiveData
ตัว ViewModel จะรับข้อมูลจาก Repository และคืนค่ามาเป็น LiveData และตัว LiveData ก็ส่งตัวเองไปให้ทาง Activity/Fragment เพื่อนำไปแสดงผล
การทำงานของ LiveData
นางคือ ผู้ถือข้อมูล (Data Holder) ซึ่งการรักษาค่าของข้อมูลจะใช้การ Observed ซึ่งการ Observation จะใช้ตาม observer pattern คือ เมื่อ Subject มีการเปลี่ยน state จะส่งไปยัง observers ซึ่ง observer แต่ละตัวจะมี method ของมันอยู่ ในที่นี้ Subject ก็คือ LiveData นั่นเอง
การเพิ่ม LiveData
เพิ่ม MutableLiveData ที่ ViewModel นะ ซึ่งตัว MutableLiveData จะมี postValue() กับ setValue() ในที่นี้ใช้ postValue() เพื่อส่งค่าไปแสดงผลในหน้า Activity ที่มี ViewModel ต่อไป
ที่หน้า Activity/Fragment เราใส่ ViewModel เพื่อให้ observe กับ LiveData และสร้าง method ขึ้นมาเพื่อนำข้อมูลไปแสดงผลโดยเฉพาะ
มารู้จักการทำงานของ Repository ว่าเขาทำงานกันยังไง
เอาจริงๆดูงานจะหนักนะ ทีม Repository จะรับค่าจาก API มีการจัดการความแตกต่างกันของข้อมูลในแต่ละที่ เช่น ใน model (DAO), web service, cache (Room) และ update data ไปยัง ViewModel เพื่อส่งไปให้ Activity ในการแสดงผลข้อมูล
Repository ถูกนำมาใช้เนื่องจากต้องการแยกส่วนของ data layer ออกมาให้ชัดเจน ซึ่งเป็นส่วนหนึ่งในหลักการ Domain-driven design หรือ DDD
สรุปงานหลักของ Repository คือ ประสานงานระหว่างข้อมูล และViewModel นั่นเอง
มาดูแผนผังการทำงานกันดีกว่า
- Observation : Repository จะ Observe LiveData ไปยัง DataSource
- Start service : ใน Repository เองก็ check ตัวเองว่ามี data พอไหม
- และถ้าไม่พอ ตัว DataSource สร้างและเริ่มต้นใช้ Service
- Fetch : Service จะได้รับ instant จาก DataSource และเริ่มการ fetch
- ตัว DataSource เริ่ม fetch data และ update ค่า
- Save in Database : Repository ทำการ observe กับ LiveData
- Repository update ข้อมูลใน database
เมื่อเราได้รู้จักการทำงานของ Code ใน Repository คร่าวๆแล้ว ก็ให้เขาทำงานให้เราดูกันเลย โดยเราได้รับความอนุเคราะห์จากคุณ codelab ซึ่งเขาบอกตัวอย่างการ modified project ที่มีอยู่แล้ว มาทำเป็น Architecture Component
1. แก้ไข build.gradle
ก่อนอื่น เปิด build.gradle
ของโปรเจกขึ้นมา เพิ่ม repository ของ google และ update gradle อย่างคุ้นเคย
จากนั้นไปที่ build.gradle ของ module เพื่อเพิ่ม dependency ปัจจุบันเป็นเวอร์ชั่น 1.0.0-alpha9–1 (เมื่อวันที่ 21 กันยายน 2560 update ใหม่หลายอันเลย)
//Room
compile "android.arch.persistence.room:runtime:1.0.0-alpha9-1"
annotationProcessor "android.arch.persistence.room:compiler:1.0.0-alpha9-1"
//LiveData and ViewModel
compile "android.arch.lifecycle:runtime:1.0.0-alpha9-1"
compile "android.arch.lifecycle:extensions:1.0.0-alpha9-1"
annotationProcessor "android.arch.lifecycle:compiler:1.0.0-alpha9-1"
ซึ่งโปรเจกที่เรานำมาเล่นนั้น เขาบังคับให้เปิด Android Studio 3.0 ขึ้นไปแหะ
(ไม่แน่ใจว่าส่วนไหนที่ทำให้เปิดใน 2.3 ไม่ได้) และมีการเปลี่ยนแปลงหลายอย่าง เช่น สามารถทำ Instant Apps ได้, ตัว build.gradle มี syntax ที่เปลี่ยนไป ยกตัวอย่างหลักๆเลย คือ compile -> implementation ซึ่งต้องไปดูใน log ว่าควรแก้เป็นคำว่าอะไรนะ
และก็ sync gradle เป็นอันจบขั้นตอนการอัญเชิญ
2. สร้าง Entity สำหรับในส่วนของ local ซึ่งจะเก็บลง SQLite
สมมุติเราเก็บข้อมูลแบบง่ายๆ มีชื่อกับนามสกุลของ user ลง SQL Database จะมีตารางแบบนี้
และนำไปสร้างโมเดลก้อนนึง เป็นดังนี้
Entity : เป็นส่วนที่ query data ตามที่เราต้องการ หลักๆคือต้องใส่ชื่อ table และใส่ data field ไว้ด้านหลังได้
@Entity(tableName = "weather", indices = {@Index(value = {"date"}, unique = true)})
PrimaryKey : เหมือนแปลไทยเป็นใคร ก็เป็น key เฉพาะของ data แต่ละอัน เราสามารถให้มัน auto generate ให้ได้ แบบนี้
@PrimaryKey(autoGenerate = true)
Ignore : ก็ตรงตัว เป็นการยกเว้นในบางเคส ที่มีบางตัวหายไป เช่น ก้อนนี้ไม่มีรูปจาก user หรือ ก้อนนี้ขาด id ไป ดังนั้นจะมี constructor สองอันใน class เดียว ก็จะบอกให้ข้ามๆหรือไม่สนใจในความไม่เป๊ะได้
ซึ่งการสร้าง model class ก็จะเหมือนกันปกติ ยิ่งเป็น JAVA ก็อาจจะมี setter/getter ด้วย (เราแอบกัดตัวเองก่อน เผื่อ dev สาย Kotlin มาอ่านแล้วจะได้ไม่ต้องกัด ;P)
3. สร้าง DAO หรือ Database Access Object
DAO ในส่วนของ local จะใช้ SQLite ในการจัดเก็บ class DAO ของเรานั้น จะมี @Dao ไว้ด้านบน interface class
ดังนั้นใช้ basic function พวก @Query, @Insert , @Delete และ @Update ในการทำบางสิ่งบางอย่างกับข้อมูล ด้วยภาษา SQL
ตัวอย่างเช่น การ query และเพิ่มข้อมูลสภาพอากาศ เมื่อเจอข้อมูลที่มัน conflict กันก็ replace ทับไป
4. สร้าง database
เมื่อเราได้ Entity และ DAO มาแล้ว เรามาสร้าง abstract class ตัวนึงที่จัดการเรื่อง database และ extends RoomDatabase
จากนั้นใส่ @Databaseด้านบนเพื่อ annotate Entity class และใส่เวอร์ชั่นของ database แบบนี้
@Database(entities = {WeatherEntry.class}, version = 1)
ถ้า Data ของเรามีการ convert type ให้ใส่ TypeConverter ไปด้วย ซึ่งใส่ class ของ type ปลายทางด้วย
@TypeConverters(DateConverter.class)
มาดูโค้ดกันดีกว่า
5. เพิ่ม ViewModel กับ LiveData
ตอนนี้ก็ได้ทำในส่วนของ model และ room กันไปเรียบร้อยแล้ว ก็มาทำ ViewModel และ LiveData ต่อ เพื่อนำข้อมูลมาแสดงผลในส่วนหน้าแอปกัน
มาในส่วนของโค้ด แก้ไข class ของ ViewModel จาก model class ธรรมดา โดยเปลี่ยน type จาก entity class มาเป็น MytableLiveData<> ดังนี้
และเพิ่ม ViewModel บนไปใน Activity ใน onCreate
mViewModel = ViewModelProviders.of(this).get(DetailActivityViewModel.class);
ดังนั้นใส่ ViewModel ใน Activity เพื่อ observe data จาก entity class
mViewModel.getWeather().observe(this, weatherEntry -> {
if (weatherEntry != null) bindWeatherToUI(weatherEntry); });
และเพิ่มในส่วนของข้อมูล ที่ mock-up ขึ้นมา ไม่ได้ผ่าน Repository ซึ่งจริงๆไม่ควรำเนาะ แค่ลองให้แสดงผลได้ก่อน
หน้าตาภาพรวมเป็นดังนี้
6. เพิ่ม Repository
เราต้อง setup 3 สิ่ง คือ Repository, LiveData, Service
มาเริ่มที่ Repository กันก่อน ซึ่งเป็นตัวที่จัดการข้อมูล และเป็น singleton มีการสร้าง constructor และ getInstance() จากนั้นมีการ init data ที่ initializeData() ซึ่งจะมีส่วนของ Data Source อยู่
ส่วนที่จัดการ database ซึ่งจะเรียก function จาก DAO มาใช้ ก็จะแบ่งเป็นดังนี้
getCurrentWeatherForecasts()
กับgetWeatherByDate(Date date)
: รับค่าสภาพอากาศปัจจุบันในวันนั้นๆมา คืนค่าเป็น LiveDatadeleteOldData()
: รับของใหม่และลบของเก่าออกisFetchNeeded()
: เซ็ควันที่ ถ้ามีวันที่ผ่านไปแล้วหรือวันไม่เพียงพอก็ fetch ใหม่startFetchWeatherService()
: อันนี้ fetch จาก data source
จากตัวอย่าง จะมีส่วน Data Source ที่ถูกเรียกใช้ แล้วต้องสร้างเพิ่มเนอะ เป็นตัวจัดการข้อมูลที่มาจาก API ถ้าเทียบกับค่าปกติ คือเหมือนใช้ Retrofit เรียก API เลยนะ
ก่อนอื่น สร้างตัวแปร MutableLiveData โดยมีไส้ในเป็น Entry ที่เราสร้างไว้ตอนแรก และสร้างตัวเปล่าๆใน constructor จากนั้นสร้าง function ที่ fetch data เพื่อไปเรียกใช้ใน Repository และใน service ต่อไป
เพราะตามหลักเรา observe LiveData โดย Repository จากนั้นทาง Repository จะมา check ตัวเองดูว่า ข้อมูลพอไหม ไม่พอก็ไปที่ Data Source และทาง Data Source จะเริ่มสร้าง service และส่งกลับ
ดังนั้นเรามาสร้าง service กันต่อเลย ประกาศตัวแปรประเภท Data Source เพื่อเริ่มการ fetch
แต่เราขาด injection ไปนี่นา มาสร้าง injection class เพิ่ม เพื่อจัดการ class ต่างๆ
7. แก้ไขหน้า Activity เพื่อแสดงผล
จากตอนแรกที่เรา mock-up ข้อมูลเพื่อแสดงผล เราต้องแก้โค้ดให้แสดงข้อมูลที่ได้จาก Repository และ LiveData มาแสดง
ในโปรเจกนี้ที่เราเอามาเล่นนั้น หน้าแรกมันจะอยู่ที่ DetailActivity ดังนั้นไปเปลี่ยนให้ MainActivity เป็นหน้าแรกของแอป ที่ AndroidManifest
<activity
android:name=".ui.list.MainActivity"
android:label="@string/app_name"
android:launchMode="singleTop"
android:theme="@style/AppTheme.Forecast">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<activity
android:name=".ui.detail.DetailActivity"
android:label="@string/title_activity_detail"
android:parentActivityName=".ui.list.MainActivity"
android:theme="@style/AppTheme">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value=".ui.list.MainActivity"/>
</activity>
จากนั้นก็ไปที่ MainActivity โดย extends LifecycleActivity เพราะเราไม่ได้ใช้ lifecycle ปกตินะ เราทำเป็น Architecture Component และประกาศตัวแปร ViewModel มา รับค่ามาแสดงผล
ViewModel มีการประกาศตัวแปร Repository และเก็บค่าที่ได้ใส่ไปใน LiveData
เราลืมอะไรไปหรือเปล่า…. แต่ตัวที่เรียก service API จริงๆ อยู่ที่นี่
สุดท้ายหน้าตาแอปเป็นดังนี้
นี่คือตัวอย่างในกรณีปิด internet ทั้งหลายทั้งปวง ใช้งานแบบ offline
ตอนนี้ก็ได้จบการอธิบายคร่าวๆถึงการทำงานของ code ใน repository ทั้งการอธิบายแผนผังการทำงาน และตัวอย่างหน้าตาโค้ด เพื่อนำไปประยุกต์ใช้กันได้
สำหรับ code ตัวอย่างอยู่ใน codelab และสามารถ clone ตัวอย่างโค้ดไปศึกษาได้ที่ github เราลบส่วน comment ใน code เพื่อให้บรรทัดของ code น้อยลงเน้อ จะได้อ่านได้จบไวขึ้น
การประยุกต์ใช้
ถ้าเทียบกับของฟังใจ ที่แบ่งโค้ดมาอย่างดี เช่นตัว service API, gateway, model, activity/fragment ซึ่งในโปรเจกตัวอย่างก็แบ่งตาม feature ทำให้ง่ายต่อการหาโค้ดมาแก้ เข้า concept clean code เลยนะนี่
สิ่งที่เพิ่มเข้ามา น่าจะเป็น LiveData และ ViewModel ทั้งหลาย และ Room ด้วย ที่จะช่วยในการเก็บ data จาก web service มาแสดงผลเมื่อ internet ช้าลง หรือไม่มีสัญญาณ (อันนี้เหมาทั้ง 3G 4G WiFi เลยนะ) โดยเฉพาะส่วน playlist ทั้งหลาย เช่น ไม่มีหลัว มือซ้ายใต้ผ้าห่ม ผีหรักหน่อง จะไม่ได้มีการอัพเดตอะไรบ่อยเท่าพวก chart, new music, new artist หรือ trending ก็ทำให้ประหยัด data ในการโหลดข้อมูลเข้าเครื่อง user ได้ แต่คงไม่ครบทุก function อะไรงี้
สุดท้ายนี้
นอกจากโค้ดที่เราคิดว่าน่าจะทำให้ performance ภาพรวมดีขึ้นแล้ว ในการแยกส่วนการทำงานออกจากกัน ยังเหลืออีกหนึ่งเรื่อง นั่นคือเรื่องของการ testing นั่นเอง ถ้าไม่ทำ อาจจะมีอะไรบางอย่างหลุดไปได้นะ ติดตามกันตอนหน้า ณ medium ของฟังใจนะคะ :)