เจาะลึกไปถึงชีวิตการทำงานของ Code ใน Repository บน Architecture Component

สำหรับ Android Architecture Component ถือว่าเป็นเรื่องใหม่ และหลายๆคนกำลังศึกษาโครงสร้างนี้ ซึ่งประกอบด้วยหลายๆส่วนงาน ทั้ง View, ViewModel, Model, Data Source, Repository ซึ่งทีม Repository เป็นทีมที่น่าจับตามองอย่างยิ่ง เนื่องจากเป็นส่วนสำคัญที่ทำให้ทีมประสบความสำเร็จ และทำให้ user มีความสุขมากขึ้น ในวันนี้เรามาสอบถาม code ว่ามีแนวทางการทำงานอย่างไร ในวันที่โลกไม่หยุดนิ่ง ให้ประสบความสำเร็จคะ 
//ยื่นไมค์ถาม

ตามติดชีวิตของคุณโค้ด credit: https://pixabay.com/en/architecture-building-businessman-1850732/

หลังจากที่เราได้ทำความรู้จัก 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 แสดงผล ว่าง่ายๆเขาเป็นผู้ประสานงานในเรื่องของข้อมูลนั่นเอง

credit : https://codelabs.developers.google.com/codelabs/build-app-with-arch-components/index.html?index=..%2F..%2Findex#2

แผนผังการทำงานของ 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 ว่าใครทำอะไรบ้าง

credit : https://codelabs.developers.google.com/codelabs/build-app-with-arch-components/#3

การทำงานของ 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 ในรูปนิดหน่อย ;credit : https://codelabs.developers.google.com/codelabs/build-app-with-arch-components/#8

การเพิ่ม LiveData

เพิ่ม MutableLiveData ที่ ViewModel นะ ซึ่งตัว MutableLiveData จะมี postValue() กับ setValue() ในที่นี้ใช้ postValue() เพื่อส่งค่าไปแสดงผลในหน้า Activity ที่มี ViewModel ต่อไป

ที่หน้า Activity/Fragment เราใส่ ViewModel เพื่อให้ observe กับ LiveData และสร้าง method ขึ้นมาเพื่อนำข้อมูลไปแสดงผลโดยเฉพาะ

credit: https://codelabs.developers.google.com/codelabs/build-app-with-arch-components/#8

มารู้จักการทำงานของ Repository ว่าเขาทำงานกันยังไง

เอาจริงๆดูงานจะหนักนะ ทีม Repository จะรับค่าจาก API มีการจัดการความแตกต่างกันของข้อมูลในแต่ละที่ เช่น ใน model (DAO), web service, cache (Room) และ update data ไปยัง ViewModel เพื่อส่งไปให้ Activity ในการแสดงผลข้อมูล

Repository ถูกนำมาใช้เนื่องจากต้องการแยกส่วนของ data layer ออกมาให้ชัดเจน ซึ่งเป็นส่วนหนึ่งในหลักการ Domain-driven design หรือ DDD

สรุปงานหลักของ Repository คือ ประสานงานระหว่างข้อมูล และViewModel นั่นเอง

มาดูแผนผังการทำงานกันดีกว่า

อันนี้วาดเองนะ ลอกจาก https://codelabs.developers.google.com/codelabs/build-app-with-arch-components/#9 ให้เหลือแผ่นเดียว ใครเอาไปใช้ใครเครดิตเราด้วยเน้อ
  1. Observation : Repository จะ Observe LiveData ไปยัง DataSource
  2. Start service : ใน Repository เองก็ check ตัวเองว่ามี data พอไหม
  3. และถ้าไม่พอ ตัว DataSource สร้างและเริ่มต้นใช้ Service
  4. Fetch : Service จะได้รับ instant จาก DataSource และเริ่มการ fetch
  5. ตัว DataSource เริ่ม fetch data และ update ค่า
  6. Save in Database : Repository ทำการ observe กับ LiveData
  7. 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 จะมีตารางแบบนี้

ตารางเก็บข้อมูล user แบบ SQL

และนำไปสร้างโมเดลก้อนนึง เป็นดังนี้

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) : รับค่าสภาพอากาศปัจจุบันในวันนั้นๆมา คืนค่าเป็น LiveData
  • deleteOldData() : รับของใหม่และลบของเก่าออก
  • 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 ของฟังใจนะคะ :)

Like what you read? Give Minseo Chayabanjonglerd a round of applause.

From a quick cheer to a standing ovation, clap to show how much you enjoyed this story.