สรุปเนื้อหา Modernize the Development of LINE Messenger for Android จากงาน LINE Developer Day 2019

สำหรับแอปอย่าง LINE ที่ยอดนิยมสุดๆในบ้านเราที่เปิดตัวมาตั้งแต่ปี 2011 ซึ่งในตอนนั้นก็ยังเป็นยุคสมัยที่แอนดรอยด์กำลังจะเปลี่ยนผ่านจาก 2.3 ไปเป็น 4.0 ถ้าลองนับจำนวนปีดูก็พบว่าตอนนั้นแอป LINE นั้นมีอายุมากถึง 8 ปีเลยทีเดียว

โดยปกติเวลาที่พูดถึงแอปที่มีอายุยาวนาน ก็มักจะไม่พ้นเรื่อง Legacy Code ที่ทำให้นักพัฒนาของแอปนั้นๆต้องหมั่นคอยดูแลและปรับปรุงแก้ไขให้ทันสมัยอยู่เสมอ (Modernize) ยิ่งในปัจจุบันนี้มีอะไรใหม่ๆเพิ่มเข้ามาเยอะมาก ซึ่งทีมพัฒนาแอป LINE ก็ได้ทำการอัพเดทให้รองรับกับสิ่งใหม่ๆอยู่เรื่อยๆ พร้อมกับเก็บเป็นเรื่องราวให้ผู้ที่หลงเข้ามาอ่านได้ฟังกันในงาน LINE Developer Day 2019 ด้วยล่ะ

ซึ่งใน Session ดังกล่าวได้หยิบ 2 ประเด็นสำคัญมาเล่าให้ฟัง นั่นก็คือเรื่องของ Kotlin และ Multi-module project

Kotlin in LINE Android App

สำหรับแอปที่มีขนาดใหญ่มากๆอย่าง LINE การจะ Migrate ทั้งโปรเจคให้ใช้ Kotlin ไม่ใช่เรื่องง่ายอย่างที่คิด แต่ทว่าทีมพัฒนาของ LINE ก็เห็นพ้องต้องกันว่าการย้ายจาก Java ไปใช้เป็น Kotlin มันมีข้อดีมากมาย จึงทำให้ในเมษายน 2017 เริ่มมีการใช้ภาษา Kotlin ในการเขียน Unit Test แล้ว และเริ่มมีการปรับโค้ดบางส่วนให้เป็น Kotlin แทน

อีก 2 เดือนต่อมาก็ได้เริ่มใช้ Kotlin ใน Production อย่างเต็มตัว โดยมีเงื่อนไขที่ทุกๆคนในทีมจะต้องทำ นั่นก็คือ

  • ฟีเจอร์ใหม่ๆหรือโค้ดใหม่ๆที่ต้องเขียนเข้าไปในโปรเจคจะต้องเป็น Kotlin เท่านั้น
  • ถ้าต้องเข้าไปเพิ่มโค้ดใหม่ๆในโค้ดเก่าที่เป็น Java จะต้องทำการเปลี่ยนโค้ด Java ทั้งหมดให้เป็น Kotlin ก่อนเสมอ

และจากข้อมูลในตุลาคม 2019 แอป LINE ได้มีสัดส่วนระหว่าง Java กับ Kotlin ดังนี้

  • จำนวนไฟล์ในโปรเจค : Java มี 6,200 ไฟล์ คิดเป็น 53% ของทั้งหมด ส่วน Kotlin มี 5,450 ไฟล์ คิดเป็น 47% ของทั้งหมด
  • จำนวนบรรทัดของโค้ด : Java มี 1,048k บรรทัด คิดเป็น 64% ของทั้งหมด ส่วน Kotlin มี 593k บรรทัด คิดเป็น 36% ของทั้งหมด

นั่นหมายความว่าในตอนนี้โปรเจคของแอป LINE ก็ยังคงมีโค้ดที่เป็น Java เกินครึ่ง ถึงแม้จะยังไม่ได้เป็น Kotlin ทั้งหมด (น่าจะใช้เวลาอีกพักใหญ่ๆเลย) แต่ก็เป็นแนวโน้มที่ดีสำหรับการ Adoption ภาษาใหม่ๆอย่าง Kotlin เข้ามาในโปรเจคของตัวเอง

Benefits of Kotlin

สำหรับข้อดีของภาษา Kotlin ที่ทำให้ทีมพัฒนาของ LINE เลือกใช้ในโปรเจค ก็ได้มีการสรุปข้อดีต่างๆไว้ดังนี้

  • สามารถทำงานร่วมกับภาษา Java ได้อย่าง 100%
  • Android Studio มีเครื่องมือช่วยแปลงภาษา Java ให้เป็น Kotlin
  • Null Safety
  • Immutable Collections
  • Property Delegation (Lazy), Extension, Data Class, Seal Class และอื่นๆอีกมากมาย
  • Coroutine

และสิ่งหนึ่งใน Kotlin ที่ดูเหมือนทีมพัฒนาจะชอบกันเป็นอย่างมากก็คือ Coroutine ด้วยเหตุผลที่ว่า

  • ลดการทำงานของ Asynchronous Task ได้ เช่น สามารถใช้ใน Procedural-way ค่อนข้างมากกว่า Functional-way (เช่น RxJava) และแก้ปัญหา Callback-hell
  • มีความคล้ายกับ async/await ของ C# และ JavaScript
  • สามารถควบคุมและจัดการเรื่อง Concurrency ได้ง่าย รวมไปถึงตอน Cancellation ด้วย

แต่ปัญหาของ Coroutine ที่ทีมพัฒนาของ LINE เจอในตอนนั้นคือยังเป็น Experiment อยู่ กว่าจะปล่อย Coroutines 1.0.0 มาก็ปาไปตุลาคม 2018 ซึ่งทีมพัฒนาใช้ Kotlin มาปีกว่าๆแล้ว

ถึงแม้ว่าในช่วงเวลาก่อนหน้านั้นจะยังไม่มี Coroutine ให้ใช้งาน แต่ทีมพัฒนาของ LINE ก็พบปัญหาว่าในโปรเจคมีวิธีการจัดการกับ Asynchronous Code ค่อนข้างหลากหลายเกินไป ไม่ว่าจะเป็น, AsyncTask, Executor + Callback, RxJava 1, RxJava 2 รวมไปถึง Library สำหรับ Asynchronous ที่คนในทีมเขียนขึ้นมาใช้งานกันเอง

จึงทำให้ทีมพัฒนาตัดสินใจแก้ปัญหาดังกล่าวก่อนเพื่อตอนที่ Coroutine เปิดตัวออกมาจะได้มีความพร้อมในการย้ายไปใช้งานในทันที

Transition Plan to Kotlin Coroutines

โดยมีจุดประสงค์คือต้องการรวบรวม Asynchronous Code ที่มีอยู่ในตอนนั้นเป็นรูปแบบเดียวกันทั้งหมดก่อน และรูปแบบที่ว่านั้นก็คือ RxJava 2 นั่นเอง เหตุผลส่วนหนึ่งก็คือความสามารถของ RxJava 2 ที่หลากหลายและยืดหยุ่น และนอกจากนี้ Coroutine ยังมี Utility Library ที่สามารถทำให้โค้ดของ Coroutine และ RxJava 2 ทำงานร่วมกันได้ด้วย

ยกตัวอย่างเช่น จากโค้ดเดิมของ RxJava 2 ที่เขียนไว้ในลักษณะแบบนี้

fun fetchJsonAsync(url: String): Single<Json> = ...
fun getFooDataFor(json: Json): Single<FooData> = ...
fun showFooData(fooData: FooData) = ...

val disposable = fetchJsonAsync(url)
    .flatMap { json ->
        getFooDataFor(json)
    }
    .observeOn(AndroidSchedulers.Main())
    .subcribe { fooData -> 
        showFooData(fooData)
    }

แต่เมื่อจะต้องย้ายมาใช้เป็น Coroutine ก็สามารถเปลี่ยนมาใช้คำสั่งแบบนี้แทนได้เลย

mainScope.launch {
    val json = fetchJsonAsync(url).await()
    val fooData = getFooDataFor(json).await()
    showFooData(fooData)
}

เมื่อเปลี่ยนมาใช้ Coroutine ครบหมดแล้ว ก็ค่อยๆทยอยเปลี่ยนโค้ดข้างในทั้งหมดจาก Async Function ให้กลายเป็น Suspending Function ซะ

// Async Function
fun fetchJsonAsync(url: Url): Single<Json> = 
    Single.fromCallable {
        fetchJsonBlocking(url)
    }.subscribeOn(Schedulers.io())

// Suspending Function
suspend fun fetchJson(url: Url): Json =
    withContext(Dispatcher.IO) {
        fetchJsonBlocking(url)
    }

สำหรับการ Cancellation จากเดิมที่ใช้ของ RxJava 2 ก็ต้องย้ายมาใช้เป็น Coroutine ด้วยเช่นกัน ซึ่งทีมพัฒนาได้มีการสร้าง Utility Class ไว้เพื่อช่วยเรื่องนี้ด้วย โดยมีชื่อว่า AutoResetLifecycleScope

Multi-module Project for LINE Android App

เรื่องต่อมาที่ทีมพัฒนาของ LINE ได้พูดถึงก็คือการทำ Modularization ที่ทีมพัฒนาในปรับมาใช้ในแอป LINE ด้วยเหตุผลดังนี้

  • ต้องการแยกโค้ดระหว่างแต่ละฟีเจอร์ให้อิสระต่อจากกัน แบ่งหน้าที่กันอย่างชัดเจน สามารถเขียนเพิ่มในอนาคตได้ง่าย
  • เพิ่มความเร็วสำหรับ Build Time เพราะโปรเจคของแอป LINE นั่นมีโค้ดมากถึง 1.6 ล้านบรรทัด
  • ลดขนาดของ APK ด้วยการใช้ Dynamic Feature Modules และ On-demand Delivery ของ Google Play

โดยได้มีการยกตัวอย่างว่าโครงสร้างของโปรเจคเดิมทีมีความซับซ้อนและยุ่งยากต่อการจัดการ จึงเริ่มจากการจัดกลุ่มของโค้ดแต่ละส่วนก่อนว่าอยู่ในฟีเจอร์ใด

เมื่อโค้ดจากที่อื่นๆที่ต้องการเรียกใช้งานโค้ดที่อยู่ภายในฟีเจอร์ที่ชื่อว่า Foo ก็จะทำการสร้าง Facade ขึ้นมาเพื่อเป็นตัวกลางในโค้ดอื่นๆมาเรียกใช้งาน

interface FooFacade {
    fun launchFooActivity(messageId: String)
}

class FooFacadeImpl : FooFacade {
    override fun launchFooActivity(messageId: String) = /* ... */
}

โดยโค้ดภายนอกจะมองเห็นแค่ Interface ของ Facade (และ Parameter Class บางตัว) เพื่อบังคับให้การเรียกใช้งานต้องทำผ่าน Facade เท่านั้น

ด้วยวิธีแบบนี้ก็จะช่วยให้ทีมพัฒนาสามารถแยกโค้ดแต่ละฟีเจอร์ออกมาเป็น Module ต่างๆได้นั่นเอง

และสำหรับการทำให้ FooFacade รู้ว่าต้องไปเรียก Instance ของ FooFacadeImpl โดยปกติแล้วผู้ที่หลงเข้ามาอ่านก็จะนึกถึง Dependency Injection อย่าง Dagger 2 กันใช่มั้ยล่ะ?

แต่ทีมพัฒนาตัดสินใจที่จะไม่ใช้ Dagger 2 เพราะการแยกโปรเจคออกเป็นหลายๆ Module แบบนี้ การใช้ Dagger 2 จะค่อนข้างซับซ้อนและยุ่งยาก จึงทำให้ทีมพัฒนาเลือกที่จะใช้ Service Locator แทน โดยพัฒนาขึ้นมาเองและมีชื่อเรียกว่า Lich Component

line/lich
A library collection that enhances the development of Android apps. - line/lich

จากนั้นก็ใช้รูปแบบนี้กับโค้ดทั้งหมด จนแบ่งโค้ดทั้งหมดของแต่ละฟีเจอร์แยกออกมาเป็น Module อิสระได้

เมื่อสรุปขั้นตอนของการทำ Modularization ที่ทีมพัฒนาของ LINE ทำทั้งหมด ก็จะมีดังนี้

  • สร้าง Facade Interface สำหรับแต่ละ Feature
  • เปลี่ยนให้ Module ต่างๆเรียกใช้งานผ่าน Facade แทนการเรียกคำสั่งในฟีเจอร์นั้นโดยตรง
  • แยกโค้ดแต่ละฟีเจอร์ออกมาเป็น Module แต่ละตัว
  • ใช้ Lich Component เพื่อเรียก Facade ของแต่ละ Module

สรุป

ใน Session นี้ถือว่าเป็นเรื่องที่น่าสนใจมากตั้งแต่การที่ทีมพัฒนาของ LINE เลือกที่จะตัดสินใจใช้ Kotlin และเลือกที่จะทำ Modularization ให้กับแอป โดยจะเห็นกันแล้วว่าการ Adoption บางอย่างให้กับสิ่งที่มีอยู่แล้ว มันไม่ได้ง่ายอย่างที่คิด และสิ่งที่ขาดไปไม่ได้เลยก็คือการวางแผนและขั้นตอน เพื่อทำให้โค้ดที่มีอยู่นั้นสามารถ Migrate ได้ง่ายๆ

โดยการเปลี่ยนไปใช้ Kotlin ก็เริ่มจากการทำในสิ่งที่ง่ายที่สุดอย่างการเขียน Unit Test ก่อน เพื่อดูความเป็นไปได้ จากนั้นจึงเริ่มนำไปใช้งานใน Production ด้วยเงื่อนไขที่บังคับให้ต้องเขียน Kotlin ลงไปในโค้ดใหม่ๆ

สำหรับ Coroutine ก็เริ่มจากการปรับโค้ดที่มีปัญหาอยู่ให้ไปใช้เป็น RxJava 2 ก่อน เพราะสามารถ Migrate ไปใช้เป็น Coroutine ในภายหลังได้ง่ายที่สุด

และสุดท้ายการเลือกใช้ Facade เพื่อช่วยให้โค้ดที่มีอยู่สามารถแยกออกมาเป็น Module ที่อิสระต่อจากกันได้นั่นเอง

ทั้งหมดทั้งมวลนี้ ขั้นตอนและไอเดียในแต่ละส่วนของทีมพัฒนาของ LINE ก็เป็นแนวทางและตัวอย่างที่ดี สำหรับนักพัฒนาแอปต่างๆที่กำลังอยากจะ Adoption ความสามารถใหม่ๆเข้ามาในโปรเจคที่กำลังพัฒนาอยู่นั่นเอง