ในที่สุดงาน Google I/O 2017 ก็ได้เปิดตัวและจบงานไปได้ด้วยดีพร้อมกับเรื่องราวใหม่ๆ เทคโนโลยีใหม่ๆที่ผุดขึ้นมาในงานนี้อย่างมากมาย ถึงแม้ว่าในปีนี้ทาง Google จะโฟกัสไปในด้าน AI อย่างเห็นได้ชัด แต่ก็ต้องบอกเลยว่าฝั่งแอนดรอยด์ก็มีอะไรใหม่ๆเจ๋งๆเพิ่มเข้ามาเช่นกัน

และนี่คือหัวข้อต่างๆที่เกี่ยวข้องกับนักพัฒนาแอนดรอยด์ที่หยิบมาพูดถึงในบทความนี้ครับ

  • Kotlin
  • Java 8
  • Android Studio 3.0
  • Architecture Components
  • Android O
  • Android Support Library
  • Dynamic Animation
  • Google’s Maven Repository
  • Firebase
  • Instant Apps
  • Device Catalog

Welcome Kotlin

ในที่สุด Google ก็ได้ประกาศให้ Kotlin เป็น 1st Class Citizen หรือก็คือประกาศรองรับ Kotlin อย่างเป็นทางการแล้ว เรียกได้ว่าเป็นการเปิดตัวที่ทำให้ทุกคนภายในงาน (และทั่วๆโลก) ร้องเฮกันลั่นเลยทีเดียว

Kotlin เป็นภาษาโปรแกรมมิ่งที่ทาง Jetbrains (Android Studio ทุกวันนี้ก็พัฒนามาจาก IntelliJ IDEA ของ Jetbrains น่ะแหละ) พัฒนาขึ้นมาเพื่อครอบการทำงานของภาษา Java อีกทีหนึ่ง (เหมือน Groovy หรือ Scala) โดยมี Syntax ที่เรียบง่ายและสวยงามกว่า Java มาก รวมไปถึงความสามารถต่างๆที่ Java ไม่มี (หรือมีในเฉพาะ Java เวอร์ชันใหม่ๆเท่านั้น)

ขอเกริ่นถึง Kotlin แค่นี้ก็พอละกัน ที่เหลือลองไปหาอ่านกันต่อเองนะ

ซึ่งในทุกวันนี้ Kotlin ก็ถูกนำไปใช้งานแทนภาษา Java บน Platform ต่างๆอยู่บ้างพอสมควร รวมไปถึงคนที่ Google ด้วยเช่นกัน ซึ่งฝั่งแอนดรอยด์ก็มีนักพัฒนาหลายๆคนที่นำ Kotlin ไปใช้งานกันเรียบร้อยแล้วเช่นกัน

Kotlin เข้ามาเป็น 1st Class Citizen, แล้วไงต่อ?

อย่างแรกเลยก็คือ Plugin ของ Kotlin จะ Built-in ไว้ใน Android Studio (เวอร์ชัน 3.0 ขึ้นไป) ให้เลย ไม่ต้องไปนั่งติดตั้งเพิ่มเองแล้ว และจะรองรับการทำงานบน Android Studio ได้สมบูรณ์ยิ่งขึ้น อย่างเช่น สามารถสร้างโปรเจคแอนดรอยด์ที่เป็น Kotlin ได้เลย ไม่ต้องนั่งสร้างใหม่เองทุกครั้งแล้ว

ต่อมาก็คือ Google จะเข้ามาเป็น Partner กับทาง JetBrain เพื่อช่วยพัฒนา Kotlin ให้ดียิ่งขึ้น และมีแผนที่จะย้าย Kotlin เข้ามาเป็นองค์กรไม่แสวงผลกำไร (Non-profit Foundation) รวมไปถึง Documentation ใน Android Developer ก็จะมีภาษา Kotlin ด้วย

อนาคตที่สดใสรอเราอยู่~

ในที่สุดก็ได้คำตอบชัดเจนจาก Google เสียที ถึงอนาคตของการนำ Kotlin มาใช้ในการพัฒนาแอปฯ เพียงเท่านี้ผู้ที่หลงเข้ามาอ่านไม่ต้องกังวลอีกต่อไปว่า Kotlin จะเลิก Support กลางคันมั้ย หรือไม่รองรับการทำงานบางอย่างบน Android Studio มั้ย

ซึ่งถ้าผู้ที่หลงเข้ามาอ่านคนใดยังไม่เคยลองเรียนรู้ Kotlin มาก่อน ขอแนะนำว่าให้ลองดูซักครั้งครับ แล้วจะรู้ว่าตกหลุมรักความสวยงามของภาษา Kotlin แบบที่หลายๆคนเค้าพูดถึงกันนั้นเป็นอย่างไร :D

และถ้ากำลังตัดสินใจจะเอามาใช้ใน Production ก็เริ่มได้เลยครับ เพราะเอาเข้าจริง Kotlin นั้นรองรับการทำงานควบคู่ไปกับ Java ได้อยู่แล้ว รวมไปถึงรองรับการ Convert จากโค้ด Java มาเป็น Kotlin ด้วย ดังนั้นความเสี่ยงในการ Migrate จาก Java มาเป็น Kotlin จึงน้อยมาก แต่ก็ต้องเรียนรู้เพื่อให้เข้าใจอย่างถ่องแท้ด้วยนะครับ

รองรับการใช้งาน Java 8 บน Android Studio และ Android O

สามารถใช้ความสามารถของ Java 8 บน Android Studio ได้เต็มที่ ไม่ว่าจะเป็นพวก Lambda, Method Reference, Type Annotations และอื่นๆอีกมากมาย

เพื่อไม่ให้เข้าใจผิด บน Android Studio นั้นสามารถใช้งาน Java 8 ได้ก็จริง แต่ก็อย่าลืมว่าบนอุปกรณ์แอนดรอยด์นั้นจะต้องเป็น Android O ขึ้นไปเท่านั้น ถึงจะรองรับคำสั่งของ Java 8 ได้ แต่ก็มีคำสั่งบางคำสั่งของ Java 8 ที่นักพัฒนาสามารถเรียกใช้งานใน Android Studio ได้ แล้ว Compile ให้กลายเป็นคำสั่งที่รองรับกับแอนดรอยด์เวอร์ชันเก่าๆด้วย อย่างเช่น Lambda เป็นต้น

Java 8 ในมุมมองเจ้าของบล็อกนั้น รู้สึกว่ายังไม่เหมาะแก่การนำไปใช้งานอย่างเต็มรูปแบบ แต่สามารถเลือกความสามารถบางอย่างของ Java 8 ที่ทำงานเฉพาะช่วง Compile Time มาใช้งานได้ก็ดีเหมือนกันนะ ซึ่งจากที่ลองเล่นดู ตอนนี้สามารถใช้ Lambda ได้โดยที่รองรับกับแอนดรอยด์เวอร์ชันเก่าด้วย (เดี๋ยวมัน Compile ให้กลายเป็นโค้ดแบบเดิมๆให้เอง) ไม่ต้องใช้ Retrolambda อีกต่อไป

เปิดตัว Android Studio 3.0

ถ้าผู้ที่หลงเข้ามาอ่านคนใดใช้ Android Studio ใน Dev Channel ก็จะสังเกตเห็นว่าก่อนหน้านี้ทางทีมงานได้ปล่อยอัปเดตของ Android Studio 2.4 มาให้อยู่พักหนึ่ง แต่ก็ได้เปิดตัวเวอร์ชัน 3.0 ภายในงานแทนซะเลย

ซึ่ง Android Studio ในเวอร์ชัน 3.0 นี้ก็มาคู่กับ Gradle 4.0 ที่รับประกันในความเร็วที่มากกว่าเวอร์ชันก่อนๆ ซึ่งในเวอร์ชันนี้ก็ฟีเจอร์เยอะเหลือเกิน ไว้เดี๋ยวแตกหน่อเป็นบทความแยกไปอีกทีดีกว่า ขอสรุปฟีเจอร์คร่าวๆที่มีดังนี้

  • รองรับการแสดง Emoji ในโค้ด
  • รองรับ Kotlin
  • เพิ่มความสามารถและแก้ปัญหาของ ConstainLayout ในเวอร์ชันก่อนหน้า
  • ปรับปรุงการแสดง Preview Layout ใน AdapterView ให้ Preview ข้อมูลได้หลากหลายมากขึ้น รวมไปถึงรองรับการ Preview ข้อมูลจากไฟล์ JSON
  • Image Asset รองรับการภาพไอคอนสำหรับ Adaptive Icon (ฟีเจอร์ใหม่ใน Android O)
  • Device File Explorer เป็น Built-in File Explorer ที่พัฒนาขึ้นจาก File Explorer ที่เคยใช้กัน ซึ่งรองรับการ Preview ไฟล์ที่อยู่ในเครื่องโดยตรง
  • Modularization เป็นเครื่องมือที่ช่วยแยกโค้ดและไฟล์ที่มีอยู่ออกไปเป็นอีก Module หนึ่ง ซึ่งมีไว้สำหรับการทำ Instant Apps
  • APK Analyzer รองรับ Mapping.txt แล้ว
  • Android Profiler สามารถแสดงข้อมูลการทำงานได้ละเอียดมากขึ้น
  • ไม่ต้องดาวน์โหลด Repository ของ Google จาก Android SDK Manager อีกต่อไป เพราะ Google ย้ายไปไว้ใน Maven repository ของตัวเองแล้ว
  • เพิ่มความรวดเร็วในการ Build Cache
  • ปรับเปลี่ยนรูปแบบการ Build ของ Gradle เพื่อให้ Build APK ได้ไวขึ้น
  • สามารถเรียกใช้งานฟีเจอร์ของ Java 8 ที่ทำงานในช่วง Compile Time ได้
  • ปรับปรุง Dependency Management ที่เคยเป็นปัญหาวุ่นวายสำหรับโปรเจคขนาดใหญ่ที่มีหลาย Module และมี Build Variant ที่หลากหลาย
  • Android Emulator มี Google Play Store ติดตั้งมาให้ในตัวแล้ว
  • สามารถทำ Proxy ใน Android Emulator ได้แล้ว
  • หน้าต่าง Android Emulator มี Bug Report ให้ในตัว
  • เพิ่มเมนูควบคุมการหมุนของเม็ดมะยม (Rotation Input) สำหรับ Android Wear Emulator
  • Layout Inspector สามารถ Search หา Property ที่ต้องการได้
  • รองรับ WebP และเพิ่มเครื่องมือในการแปลงไฟล์ภาพอย่าง JPG/PNG ให้กลายเป็น WebP

ในตอนนี้ Android Studio 3.0 ยังเป็น Preview อยู่ ซึ่งต้องติดตั้งแยก (เป็นไอคอนสีเหลือง) ก็อย่าลืมดาวน์โหลดมาลองใช้งานดูครับ เนื่องจากมีฟีเจอร์หลายๆอย่างที่ช่วยให้นักพัฒนาทำงานได้สะดวกขึ้นที่อาจจะต้องใช้เวลาเรียนรู้ซักพัก แต่เชื่อเถอะว่าถ้าใช้เป็นแล้วชีวิตจะดีขึ้นมาก

Architecture Component

ระบบปฏิบัติการแอนดรอยด์นั้นมีการออกแบบขึ้นมาโดยมีการวางรูปแบบการทำงานไว้อย่างเหมาะสม ซึ่งการสร้างแอปฯที่ดี นักพัฒนาก็จะต้องเข้าใจ Component พื้นฐานของแอนดรอยด์และเขียนโค้ดเพื่อจัดการให้ถูกต้อง

อาจจะฟังดูเหมือนง่าย แต่กลับกลายเป็นว่าทุกวันนี้ยังมีแอปฯอีกมากมายที่เขียนโค้ดจัดการกับเรื่องเหล่านี้แบบผิดๆอยู่ สุดท้ายทำให้กลายเป็นแอปฯที่ทำงานได้ไม่ถูกต้อง (และเกิดบั๊กที่เหมือนๆกัน) โดยสาเหตุก็เกิดมาจากการที่นักพัฒนาส่วนใหญ่ไม่ได้เข้าใจการทำงานของ Component เหล่านั้นจริงๆ ก็เลยไม่รู้ว่าจะต้องเขียนโค้ดอย่างไรให้เหมาะสม

ปัญหานี้เป็นปัญหาเรื้อรังมานานพอสมควร (จากที่คุยมา เหมือนจะเป็นปัญหากันทุกที่) จึงทำให้ทีมแอนดรอยด์พัฒนาสิ่งที่เรียกว่า Architecture Components ขึ้นมา ซึ่งเป็น Component ที่จะช่วยให้พัฒนาแอปฯตามรูปแบบที่ถูกต้องได้ง่ายขึ้น เย้ เย

โดย Architecture Components จะมีทั้งหมด 4 กลุ่มดังนี้

  • Handling Lifecycles
  • LiveData
  • ViewModel
  • Room Persistence Library

Handling Lifecycles

นักพัฒนาแอนดรอยด์ทุกคนรู้จักกับคำว่า Lifecycle เพราะเป็นเรื่องพื้นฐานอย่างแรกๆที่นักพัฒนาจะต้องเรียนรู้ แต่ก็ยังมีโค้ดที่ทำให้เกิดบั๊กได้ เพราะว่าลำดับการทำงานของโค้ดใน Lifetcycle ไม่ถูกต้อง

ยกตัวอย่างเช่น

class MyActivity : AppCompatActivity() {
    private var locationListener: MyLocationListener? = null
    
    public override fun onCreate(savedInstanceState: Bundle?) {
        /* ... */
        locationListener = LocationListener(this, object : MyLocationListener() {
            fun onLocationChanged(location: Location?) {
                // Update UI
            }
            /* ... */
        })
    }

    public override fun onStart() {
        /* ... */
        PermissionUtil.requestLocationPermission(object : LocationPermissionCallback() {
            fun onLocationPermissionCallback(enrolled: Boolean) {
                if (enrolled) {
                    locationListener.start()
                }
            }
        })
    }

    public override fun onStop() {
        /* ... */
        locationListener.stop()
    }
}

คำสั่งข้างบนนี้จะดูคุ้นๆตากันหน่อยเนอะ เป็นคำสั่งที่เกี่ยวกับ Location ที่มีการเรียกใช้งานใน onStart() และหยุดใช้งานใน onStop() โดยมีการเช็ค Runtime Permission ด้วย ซึ่งคำสั่งดังกล่าวยังมีช่องโหว่อยู่ เพราะถ้า User ยังไม่ได้ Grant Permission แล้วหมุนหน้าจอหรือย่อแอปก็จะทำให้ locationListener.stop() ทำงานและกลายเป็นบั๊กตัวเป้งขึ้นมา เนื่องจาก locationListener.start() ยังไม่ได้ทำงาน

ทีมแอนดรอยด์จึงได้สร้างสิ่งที่เรียกว่า LifecycleOwner และ LifecycleObserver ขึ้นมา ขึ้นมาครับ เพื่อให้คลาสที่ต้องยุ่งเกี่ยวกับ Lifecycle ไปจัดการเองตาม Lifecycle ที่ต้องการซะ

class MyLocationListener(
    context: Context?,
    private val lifecycle: Lifecycle,
    private val listener: LocationListener?
) : LifecycleObserver {
    private var enabled = false

    init {
        /* ... */
        lifecycle.addObserver(this)
    }

    fun enable() {
        enabled = true
        if (lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)) {
            // Connect if not connected
        }
    }

    @OnLifecycleEvent(Lifecycle.Event.ON_START)
    fun start() {
        if (enabled) {
            // Connect
        }
    }

    @OnLifecycleEvent(Lifecycle.Event.ON_STOP)
    fun stop() {
        // Disconnect if connected
    }

    @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
    fun cleanup() {
        lifecycle.removeObserver(this)
    }
}

โดยมีเงื่อนไขคือคลาสดังกล่าวจะต้อง Implement LifecycleOwner ไว้ด้วย

MyLocationListener จะจัดการกับ Lifecycle เองได้ยังไง? ให้ดูที่ Constructor ครับ

class MyLocationListener(...) : LifecycleObserver {
    init {
        ...
        lifecycle.addObserver(this)
    }
    ...
}

จะเห็นว่ามีการโยนคลาสชื่อว่า Lifecycle เข้ามาด้วย มาจากไหนเดี๋ยวค่อยว่ากัน ซึ่งคลาสตัวนี้จะช่วยให้ MyLocationListener สามารถดัก Lifecycle Event จาก Activity/Fragment ที่เรียกใช้งานได้แล้ว โดยใช้คำสั่ง addObserver(lifecycleObserver: LifecycleObserver)

ถ้าอยากจะให้คำสั่งบางคำสั่งทำงานเมื่อ Activity/Fragment เกิด onStart() จะประกาศผ่าน Annotation แบบนี้ได้เลย

class MyLocationListener/* ... */) : LifecycleObserver {
    /* ... */
    @OnLifecycleEvent(Lifecycle.Event.ON_START)
    fun start() {
        // Do something when onStart has called
    }
}

นอกจากนี้ยังสามารถเช็คสถานะการทำงานของ Lifecycle ผ่านคลาส Lifecycle ได้เลย

class MyLocationListener(/* ... */) : LifecycleObserver {
    /* ... */
    fun enable() {
        /* ... */
        if (lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)) {
            // Do something after this method called after onStart() was called
        }
    }
}

จากตัวอย่างข้างบนคือคำสั่ง enable() จะทำงานก็ต่อเมื่อ Activity/Fragment เรียก onStart() ไปแล้วเท่านั้น

เมื่อใช้งานเสร็จแล้ว อยากให้เคลียร์ตัวเองทิ้ง ก็ลบคลาสนี้ออกจาก Observer ได้เลย

class MyLocationListener(/* ... */) : LifecycleObserver {
    /* ... */
    @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
    fun cleanup() {
        lifecycle.removeObserver(this)
    }
}

เฮ้ย! เจ๋งอ่ะ จัดการตัวเองได้ทั้งหมดเลย จากเดิมที่จะต้องใส่คำสั่งเหล่านี้ไว้ใน Activity/Fragment แต่เมื่อใช้ LifecycleOwner เข้ามาช่วย เวลาเรียกใช้งาน MyLocationListener ก็จะกลายเป็นแบบนี้แทน

class MyActivity : AppCompatActivity() {
    private var locationListener: MyLocationListener? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        locationListener = MyLocationListener(this, lifecycle, object: LocationListener {
            override fun onLocationChanged(location: Location?) {
                // Update UI
            }
            /* ... */
        }
        PermissionUtil.requestLocationPermission(object : LocationPermissionCallback() {
            fun onLocationPermissionCallback(enrolled: Boolean) {
                if (enrolled) {
                    locationListener?.enable()
                }
            }
        })
    } 
    // ลาก่อน onStart และ onStart
}

คลาส MyLocationListener เรียกใช้งานใน onCreate(...) ได้เลย จากนั้นก็ปล่อยให้มันจัดการกับตัวเองตาม Lifecycle โดยอัตโนมัติซะ ในคลาส Activity/Fragment ก็ไม่ต้องมานั่งประกาศ onStart() และ onStop() แล้วยัดคำสั่งไว้ในนั้นอีกต่อไป ดีงามมากมาย

แต่จุดที่ต้องสังเกตต่อคือตอนเรียกใช้งาน MyLcationListener มีการเรียกคำสั่ง lifecycle เพื่อโยน Lifecycle ของ Activity/Fragment ตัวนั้นๆเข้าไปใน MyLocationListener

ว่าแต่… คำสั่งนี้มันมีด้วยหรอ?

คลาส Activity ที่สร้างมาจาก AppCompatActivity หรือ FragmentActivity จะมีให้โดยอัตโนมัติ​ เพราะเป็นคลาสที่อยู่ใน Android Jetpack นั่นเอง แต่ถ้าใช้ Activity ปกติของ Framework จะไม่มีให้ใช้

ไม่ยากเนอะ?

ถือว่าเป็น Component ที่เจ๋งมากๆ เพราะโค้ดที่ต้องจัดการกับ Lifecycle เป็นอะไรที่วุ่นวายมากๆ ถ้าเรียกใช้เยอะๆก็จะเห็นคำสั่งหลายๆบรรทัดอยู่ใน onResume(), onStart(), onPause(), onStop() และ onDestroy() อยู่เสมอ แต่พอใช้ LifecycleOwner และ LifecycleObserver เข้ามาช่วยก็จะทำให้โค้ดแยกออกจากกันได้มากขึ้น ไม่ต้องสนใจว่าคลาสนั้นๆจะยุ่งกับ LIfecycle ยังไง แค่เรียกใช้ในตอนแรกเท่านั้นก็พอ จบ

แต่สุดท้ายแล้ว Lifecycle ก็ยังเป็นเรื่องสำคัญที่นักพัฒนาต้องเรียนรู้และเข้าใจนะ ไม่ใช่ว่าพอมี LifecycleOwner กับ LifecyclerObserver แล้วจะไม่ต้องเรียนรู้ Lifecycle อีกต่อไป เพราะสุดท้ายแล้ว ทั้งคู่ก็เกิดมาเพื่อให้นักพัฒนาไปสร้างคลาสครอบคำสั่งต่างๆที่ต้องยุ่งกับ Lifecycle เองอยู่ดี ดังนั้นถ้าไม่เข้าใจ Lifecycle ตั้งแต่แรก ก็ไม่ช่วยให้บั๊กลดน้อยลงหรอกนะ

LiveData

LiveData เป็น Data Holder ที่ทีมแอนดรอยด์ออกแบบมาให้ทำงานตาม Lifecycle ของ Activity/Fragment ที่เรียกใช้งานได้ ซึ่งออกแบบมาให้มีลักษณะเป็น Observable ซึ่งรูปแบบการทำงานจะต่อยอดมาจากเรื่อง Handling Lifecycles อีกทีหนึ่ง

LiveData เอาไว้ใช้งานกับคลาสใดๆก็ตามที่ต้องมีการอัปเดตค่าอย่างต่อเนื่องและต้องส่งไปให้ Activity/Fragment (Location Service หรือ Accelerometer เป็นต้น) โดยปกติจะต้องใช้ Listener หรือ Delegate ในการส่งค่า แต่สำหรับ LiveData จะถูกสร้างมาให้ส่งค่าผ่าน Observable ครับ (ถ้าเคยใช้ RxJava จะคุ้นเคยกันดี)

ข้อดีหลักๆของ LiveData คือสามารถทำงานร่วมกับ Lifecycle ได้โดยอัตโนมัติ ซึ่งจะช่วยแก้ปัญหาจากเรื่อง Handling Lifecycles ถ้าลำดับการทำงานไม่ถูกต้องตาม Lifecycle โดย LiveData จะรู้ได้เองว่าถูกเรียกใช้/หยุดเรียกใช้งานตอนไหนของ Lifecycle (ไม่ต้องไปกำหนดเองแล้ว เย้)

เพื่อให้เห็นภาพง่ายขึ้น จึงขอหยิบ MyLocationListener มาทำให้เป็น LiveData กันต่อเลย

ในการเปลี่ยนให้คลาสใดๆก็ตามเป็น LiveData จะต้องเข้าใจก่อนว่าคลาสนั้นมี Data เป็นอะไร ซึ่งใน MyLocationListener จะมี Data เป็นคลาส Location เป็นข้อมูลตำแหน่งพิกัดที่ได้จาก Location Service อีกทีหนึ่ง (จะไม่โยนข้อมูลดังกล่าวออกไปตรงๆ แต่จะใช้ความสามารถของ LiveData แทน)
class MyLocationListener(context: Context) : LiveData<Location?>() {
    private val locationManager: LocationManager =
        context.getSystemService(Context.LOCATION_SERVICE) as LocationManager

    private val listener = object : LocationListenerAdapter() {
        override fun onLocationChanged(location: Location) {
            value = location
        }
    }

    @SuppressLint("MissingPermission")
    override fun onActive() {
        locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 1000, 10f, listener)
    }

    @SuppressLint("MissingPermission")
    override fun onInactive() {
        locationManager.removeUpdates(listener)
    }

    open class LocationListenerAdapter : LocationListener {
        override fun onLocationChanged(location: Location) {}
        override fun onStatusChanged(provider: String, status: Int, extras: Bundle) {}
        override fun onProviderEnabled(provider: String) {}
        override fun onProviderDisabled(provider: String) {}
    }
}

จะเห็นว่า MyLocationListener สืบทอดคลาสจาก LiveData<T> เรียบร้อยแล้ว โดยกำหนดเป็น LiveData<Location> จากนั้นให้สังเกตที่ onActive() กับ onInactive() ทั้งสองคำสั่งนี้เป็น Override Method ของ LiveData<T> ที่มีไว้กำหนดคำสั่งใดๆก็ตามเพื่อให้เริ่มทำงานและหยุดทำงานครับ

การเรียกใช้งาน LocationManager จะได้ผลลัพธ์ส่งกลับมาเป็น Listener โดยส่งค่ามาทาง Override Method ที่ชื่อว่า onLocationChanged(location: Location) ดังนั้นเจ้าของบล็อกจะต้องส่งค่านี้ออกไปให้ Activity/Fragment ด้วยคำสั่ง setValue(value: T) จะเห็นว่ามันเป็น Generic Type ซึ่งขึ้นอยู่กับที่กำหนดไว้ใน LiveData<T> นั่นเอง

เวลาเรียกใช้งานจากฝั่ง Activity/Fragment ก็จะไม่ค่อยแตกต่างอะไรจากเดิมมากนัก แต่โค้ดจะกระชับขึ้นอย่างเห็นได้ชัด

class MyActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        /* ... */
        PermissionUtil.requestLocationPermission(object : LocationPermissionCallback() {
            fun onLocationPermissionCallback(enrolled: Boolean) {
                if (enrolled) {
                    callLocationListener()
                }
            }
        })
    }

    private fun callLocationListener() {
        val locationListener = MyLocationListener(this)
        locationListener.observe(this, Observer {
            // Update UI
        })
    }
    /* ... */
}

แล้ว onActive() กับ onInactive() ทำงานตอนไหน?

สำหรับ onActive() จะถูกเรียกตอนที่ใช้คำสั่ง observe(...) ซึ่งคำสั่ง observce(...) จะเรียกใช้งานตอน onCreate(...), onStart() หรือ onResume() ก็ได้ จากในตัวอย่างถึงแม้จะเรียกใน onCreate(…) แต่เนื่องจากเรียกในคำสั่ง Runtime Permission จึงทำให้คำสั่งทำงานหลังจากที่ onResume() ทำงานเสร็จเรียบร้อยแล้ว ดังนั้น onActive() จึงถูกเรียกใน onResume()

และสำหรับ onInactive() จะมีรูปแบบการทำงานขึ้นอยู่กับ onActive() ดังนี้

  • เมื่อ onActive() ทำงานตอน onCreate() จะทำให้ onInactive() ทำงานก่อน onDestroy()
  • เมื่อ onActive() ทำงานตอน onStart() จะทำให้ onInactive() ทำงานก่อน onStop()
  • เมื่อ onActive() ทำงานตอน onResume() จะทำให้ onInactive() ทำงานก่อน onPause()

ดังนั้นจะเห็นว่านักพัฒนาไม่ต้องมานั่งเสียเวลากับเรื่อง Lifecycle มากนัก เพราะว่า LiveData ได้จัดการไว้ให้อัตโนมัติเรียบร้อยแล้ว

ให้สังเกตที่คำสั่ง observe(...) ของ LiveData ครับ

fun observe(owner: LifecycleOwner, observer: Observer<T>)

Argument ตัวแรกสุดที่โยนเข้าไปคือ LifecycleOwner นั่นเอง นั่นหมายความว่า Activity/Fragment จะเรียกใช้งาน LiveData ได้ก็ต้องเป็น LifecycleActivity/LifecycleFragment หรือเป็น Activity/Fragment ที่ Implement LifecycleRegistryOwner เท่านั้นนะ (ดูในเรื่อง Handling Lifecycles)

ถึงแม้จะดูเหมือนว่า LiveData ผูกการทำงานเข้ากับ LIfecycle ก็จริง แต่ตัวมันไม่ได้ผูกกับ Activity/Fragment โดยตรงเลยซักนิด (มี LifecycleOwner เป็นตัวกลาง) จึงทำให้นักพัฒนาสามารถเขียนเทส LiveData ได้ง่ายขึ้น

ViewModel

สำหรับ ViewModel ก็จะต่อยอดมาจากเรื่อง Handling Lifecycles กับ LiveData มาอีกทีน่ะแหละ เพราะ LiveData ช่วยจัดการเรื่องการ Lifecycle ก็จริง แต่ไม่ได้จัดการเรื่อง Configuration Changes ให้เลย ดังนั้นถ้าเกิด Configuration Changes ตอนที่ LiveData กำลังทำงานอยู่ ก็จะทำให้ LiveData หยุดและเริ่มทำงานใหม่อีกครั้ง

อาจจะฟังดูไม่น่าจะมีปัญหาอะไร เพราะ LiveData ก็กลับมาทำงานต่อได้ทันทีนี่นา

นั่นก็เพราะว่าในตัวอย่าง เจ้าของบล็อกได้ใช้ LiveData กับ Location Service ที่ไม่ได้ซีเรียสว่ามันจะหยุดทำงานหรือป่าว ขอแค่กลับมาทำงานต่อได้ก็พอ

แล้วถ้าเจ้าของบล็อกใช้ LiveData เพื่อเชื่อมต่อกับ Web Service ล่ะ? ผลที่ได้ก็จะกลายเป็นแบบนี้

จะเห็นว่าเมื่อแอปฯเปิดขึ้นมาจะทำงานเรียกข้อมูลจาก Web Service ทันที แต่ในระหว่างที่รอข้อมูลส่งกลับมาอยู่นั้น ผู้ใช้ดันหมุนหน้าจอซึ่งทำให้เกิด Configuration Change จึงทำให้ Activity ถูกทำลายแล้วสร้างขึ้นมาใหม่ แล้วก็ไปเรียกข้อมูลจาก Web Service ซ้ำอีกครั้ง

ไม่ค่อย Make Sense แล้วเนอะ?

ดังนั้น LiveData จึงไม่ตอบโจทย์สำหรับการทำงานแบบนี้ครับ และนั่นก็คือที่มาของ ViewModel นั่นเองงงงงงงง

ViewModel? ใช่อันที่มันอยู่ใน MVVM หรือป่าว?

ใช่ครับ ใช่เลย มันคือ ViewModel ใน MVVM นั่นแหละ หรือในอีกความหมายก็คือ Architecture Components (ในตอนนี้) จะทำให้นักพัฒนาต้องเขียนโค้ดแบบ MVVM นั่นเอง

//เจ้าของบล็อกใช้ MVP โคตรเสียใจ

ซึ่งปัญหาเดิมๆที่เกิดขึ้นระหว่างการใช้ MVVM หรือ MVP ในการพัฒนาแอปฯแอนดรอยด์ก็คือจะทำให้ ViewModel (ของ MVVM) หรือ Presenter (ของ MVP) อยู่รอดจาก Configuration Changes ได้ยังไง เพราะนี่คือคำถามยอดนิยมในการหยิบ MVVM และ MVP มาใช้งานกันครับ

ถ้าอิงจาก MVP ที่เจ้าของบล็อกใช้อยู่ ตามรูปแบบที่เจ้าของบล็อกวางไว้ก็คือปล่อยให้ Presenter แม่มตายไปสิ พอ Activity/Fragment กลับขึ้นมาทำงานใหม่ก็ค่อยให้มันสร้าง Presenter ใหม่ทีหลัง (และต้องรู้ได้ว่าเรียกข้อมูลจาก Web Service อยู่หรือป่าว ซึ่งต้องเขียนโค้ดเยอะพอสมควร)

แต่ ViewModel ของ Architecture Component นั้นไม่ตายครับ อยู่รอดจาก Configuration Changes ได้ (ขี้โกงสุดๆ) ดังนั้นจึงสามารถทำให้มันกลายเป็นแบบนี้ได้ครับ

จากเดิมที่ Activity/Fragment เรียกใช้งาน LiveData โดยตรง ก็จะย้าย LiveData ไปไว้ใน ViewModel แทน แล้วให้ Activity/Fragment เรียกใช้งานผ่าน ViewModel อีกทีครับ

class MyActivityViewModel : ViewModel() {
    private var userData: LiveData<User>? = null
    
    fun getUser(userId: String?): LiveData<User> {
        if (userData == null) {
            userData = api.getUser(userId)
        }
        return userData as LiveData<User>
    }
}

การเรียกใช้งาน ViewModel จะต้องเรียกผ่านคลาสที่ชื่อว่า ViewModelProviders แบบนี้

class MyActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        /* ... */
        val userId: String = /* ... */
        ViewModelProvider.of(this)
            .get(MyActivityViewModel::class.java)
            .getUser(userId)
            .observe(this) {
                // Update UI
            }
    }
    /* ... */
}

โดยจะเห็นว่า ViewModel ไม่มีการผูกตัวเองเข้ากับ Activity/Fragment เลยซักนิด (ใช้ประโยชน์จาก LifecycleOwner เหมือนเดิม) จึงทำให้เทสได้ง่ายไม่ต่างอะไรกับ LiveData เลย

และถึงแม้ว่า ViewModel จะรอดจาก Configuration Changes ก็จริง แต่เมื่อ Activity/Fragment นั้นๆถูกทำลาย ViewModel ก็จะหยุดทำงานไปพร้อมๆกัน

โคตรขี้โกงงงงงงงงงง

ดังนั้น ViewModel จึงกลายเป็นคำตอบสำหรับนักพัฒนาหลายๆคน แต่การใช้ ViewModel ก็จะทำให้โปรเจคต้องใช้ MVVM ไปโดยปริยาย และโค้ดจะลงตัวอย่างสวยงามมากขึ้นไปอีกถ้าใช้ควบคู่กับ Data Binding Library

Room Persistence Library

Room เป็น Object Mapping สำหรับ SQLite ที่จะแก้ปัญหาเดิมๆจากการเรียกใช้งาน SQLite ภายในแอปฯซึ่งไม่ค่อยเป็นมิตรต่อนักพัฒนามือใหม่ซักเท่าไรนัก โดยเฉพาะโค้ดตอนเรียกใช้งานที่อ่านยากและผิดพลาดได้ง่าย

SQLiteDatabase db = mDbHelper.getReadableDatabase();

// Define a projection that specifies which columns from the database
// you will actually use after this query.
String[] projection = {
        FeedEntry._ID,
        FeedEntry.COLUMN_NAME_TITLE,
        FeedEntry.COLUMN_NAME_SUBTITLE
};

// Filter results WHERE "title" = 'My Title'
String selection = FeedEntry.COLUMN_NAME_TITLE + " = ?";
String[] selectionArgs = { "My Title" };

// How you want the results sorted in the resulting Cursor
String sortOrder = FeedEntry.COLUMN_NAME_SUBTITLE + " DESC";

Cursor cursor = db.query(
        FeedEntry.TABLE_NAME,     // The table to query
        projection,               // The columns to return
        selection,                // The columns for the WHERE clause
        selectionArgs,            // The values for the WHERE clause
        null,                     // don't group the rows
        null,                     // don't filter by row groups
        sortOrder                 // The sort order
);

จากตัวอย่างข้างบนนี้จะเห็นว่ามีโค้ดที่ชวนวุ่นวายเต็มไปหมด ทั้งๆที่เป็น SQL Command ง่ายๆแบบนี้

SELECT id, name, subTitle FROM Feed 
WHERE title = 'My Title'
ORDER BY subTitle DESC

และถ้าเผลอพิมพ์เว้นวรรคใน String ผิด ก็จะทำให้คำสั่งผิดและเกิด Runtime Exception ได้ ซึ่งเจ้าของบล็อกเห็นบ่อยมากโดยเฉพาะนักพัฒนามือใหม่ ซึ่งเป็นหนึ่งในปัญหาที่เรื้อรังมานานเช่นกัน

จึงเกิดเป็น Library ที่ชื่อว่า Room ขึ้นมา เพื่อให้การเรียกใช้งาน SQLite ด้วยรูปแบบที่กระชับ เข้าใจได้ง่าย และมีการตรวจสอบ SQL Command ทุกครั้งตอน Compile (Compile Time Validation) จึงมั่นใจได้ว่าทุกครั้งที่แอปฯทำงานจะไม่เจอปัญหาจากการใส่ SQL Command ผิดอีกต่อไป

ซึ่งรูปแบบโค้ดของ Room จะมีลักษณะแบบนี้

@Entity 
class Feed(
    @PrimaryKey
    id: Int,
    title: String?,
    subTitle: String?
)

@Dao
interface FeedDao {
    @Query("SELECT id, title, subTitle FROM feed WHERE title = :keyword")
    fun loadFeed(String keyword): List<Feed>
}

@Database(entities = {Feed.class}, version = 1) 
abstract class FeedDatabase extends RoomDatabase {
    abstract FeedDao feedDao();
}

จะเห็นว่ารูปแบบขอโค้ดดูได้ง่ายกว่าแบบเดิมมาก ใช้ DAO เป็นตัวกลางในการเข้าถึง SQLite (ในที่นี้คือคลาส FeedDao) โดยผลลัพธ์ที่ได้จะกลับมาอยู่ในรูปของ POJO ที่นำไปใช้งานได้เลย (ในที่นี้คือคลาส Feed) โดยการเรียกใช้งานนั้นจะเรียกใช้งานผ่าน RoomDatabase ที่ไปครอบ Dao อีกทีหนึ่ง (ในที่นี้คือ FeedDatabase)

โดยรูปแบบของ SQL Command จะกำหนดไว้ใน Annotation ที่ชื่อว่า @Query ที่ให้นักพัฒนากำหนดคำสั่ง SQL ในนั้นได้โดยตรง จึงทำให้อ่านคำสั่งแล้วรู้ได้ทันทีว่าคำสั่งนั้นคืออะไร และเชื่อมกับ Argument ที่กำหนดไว้ใน Method นั้นๆได้เลย

ถ้า SQL Command ไม่ถูกต้อง ก็จะแจ้งบอกตอน Compile ให้

จะ Insert, Update หรือ Delete ก็ทำได้ง่ายๆเช่นกัน

@Entity
data class Feed(
    @PrimaryKey
    var id: String,
    var title: String?,
    var subTitle: String?
)

@Dao
interface FeedDao {
    @Query("SELECT id, title, subTitle FROM feed WHERE title = :keyword")
    fun loadFeed(keyword: String ): List<Feed>
}

@Database(entities = [Notification::class], version = 1)
abstract class FeedDatabase : RoomDatabase() {
    abstract fun feedDao(): FeedDao;
}

เวลาเรียกใช้งานก็จะมีลักษณะแบบนี้

val context: Context = /* ... */
var db = Room.databaseBuilder(context, FeedDatabase::class.java, "feed").build()
var feeds = db.feedDao().loadFeed("any_keyword")

โดยบังคับว่าจะต้องเรียกคำสั่งนี้บน Background Thread เท่านั้น ห้ามเอาไปเรียกบน Main Thread เด็ดขาด เพื่อป้องกันไม่ให้เกิด UI Blocking กับตัวแอปฯ

แต่ความเจ๋งของ Room ไม่ได้อยู่แค่นั้นครับ เพราะว่า Room นั้นสามารถใช้คู่กับ LiveData ได้ด้วย ดังนั้นเมื่อฐานข้อมูลมีการเปลี่ยนแปลงก็จะทำให้รู้ได้ทันที และ Live Data ก็จะอัปเดตให้เสมอ

@Query("SELECT * FROM feed WHERE title = :keyword")
fun loadFeed(keyword: String?): LiveData<List<Feed>>

เมื่อรองรับ LiveData นั่นก็หมายความว่า สามารถใช้ใน ViewModel ได้นั่นเอง!!

class FeedViewModel : ViewModel() {
    lateinit var feeds: LiveData<List<Feed>>
    fun FeedViewModel(feedDao: FeedDao) {
        feeds = feedDao.loadFeed("any_keyword")
    }
    /* ... */
}

เริ่มเห็นภาพมากขึ้นแล้วใช่มั้ยล่ะ ว่า Component แต่ละส่วนนั้นสัมพันธ์กันยังไง

นอกจากนี้ยังสามารถใช้กับ RxJava 2 ได้ด้วยนะเออ (ครอบคลุมดีจริง)

@Query("SELECT * FROM feed WHERE title = :keyword")
fun loadFeed(keyword: String?): Flowable<List<Feed>>

ดังนั้นผู้ที่หลงเข้ามาอ่านคนใครที่ใช้ RxJava 2 ก็สามารถเอา Room ไปใช้งานได้ทันที (แต่ถ้าใช้ RxJava 1 อยู่ ก็แนะนำให้ย้ายไปเป็น RxJava 2 แทน)

จริงๆยังมีอีกเยอะมากเกี่ยวกับ Room ที่แนะนำให้ไปลองเล่นดูเองครับ รับรองว่าถูกใจแน่นอน

Architecture Components ตอบโจทย์เรื่องการเขียนเทสด้วยนะเออ

จาก Component ทั้ง 4 ตัวที่ทางทีมแอนดรอยด์พัฒนาขึ้นมานั้น เมื่อมองภาพรวมทั้งหมดก็จะพบว่าแต่ละส่วนถูกสร้างขึ้นเพื่อใช้ใน Clean Architecture ที่อยู่ในรูปแบบของ MVVM ครับ

และที่ขาดไปไม่ได้ในยุคนี้ก็คือเรื่องของการเขียนเทส ซึ่งรูปแบบดังกล่าวถูกออกแบบมาง่ายต่อการเทสอยู่แล้ว (สังเกตดีๆว่า Component ทุกตัวใน Architecture Components ถูกออกแบบมาให้เทสได้อยู่แล้ว) โดยจะแบ่ง Layer จากบนลงล่างเพื่อทำการเขียนเทสตามแบบฉบับของ Clean Architecture

ถ้าอยากจะเทส UI Controller ก็ให้ Mock View Model แล้วเขียนเทสด้วย Espresso

ถ้าอยากจะเทส View Model ก็ให้ Mock Repository แล้วเขียนเทสด้วย Android JUnit

ถ้าอยากจะเทส Repository ก็ให้ Mock Data Sources แล้วเขียนเทสด้วย Android JUnit

ภาพรวมของ Architecture Components

ถ้าอยากจะลองใช้ Architecture Components ก็สามารถดาวน์โหลดได้จาก Maven Repository เลยครับ

// For Lifecycles, LiveData and ViewModel
implementation "android.arch.lifecycle:extensions:2.2.0"
implementation "android.arch.lifecycle:runtime:2.2.0"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0"
kapt "androidx.lifecycle:lifecycle-compiler:2.2.0"

// For Room
implementation "androidx.room:room-runtime:2.2.5"
implementation "androidx.room:room-ktx:2.2.5"
kapt "androidx.room:room-compiler:2.2.5"

เรียกได้ว่า Architecture Components นั้นเป็น Best Practice ที่ทีมแอนดรอยด์กำหนดขึ้นมาให้ก็ว่าได้ เพื่อที่ว่านักพัฒนาจะได้ไม่ต้องไปเสียเวลานั่งคิดว่าจะใช้ Pattern แบบไหนดี จะต้องเขียนโค้ดยังไงให้เป็นอิสระต่อจากกัน และจะเขียนเทสในแต่ละส่วนยังไง ซึ่งทั้งหมดนี้ทีมแอนดรอยด์คิดมาให้แล้วครับ สามารถ Follow ตาม Guideline ของ Architecture Components ได้เลย

แต่ก็ไม่ได้หมายความว่านักพัฒนาจะต้องโล๊ะโค้ดทั้งหมดเพื่อเขียนให้ถูกต้องตามที่ทีมแอนดรอยด์วาง Guideline ไว้ให้ เพราะสุดท้ายแล้ว Architecture Components ก็เป็นแค่เพียงหนึ่งทางเลือกในการนำไปใช้งานครับ (ที่เค้าคิดไว้ให้อย่างครอบคลุมแล้ว) ถ้าโปรเจคที่ทำกันอยู่ไม่มีปัญหาอะไร ทำงานได้ดี จัดการกับ Lifecycle ได้ถูกต้อง รับมือกับ Configuration Changes ได้อยู่ไม่มีปัญหา และเขียนเทสได้อยู่แล้ว ก็ทำต่อไปเถอะครับ

สำหรับรายละเอียเพิ่มเติมของ Architecture Components ลองไปดูกันต่อได้ที่ https://developer.android.com/arch

Android Architecture Components | Android Developers
App Actions

อัปเดตรายละเอียดฟีเจอร์ใน Android O

สำหรับ Android O นักพัฒนาหลายๆคนก็คงได้ลองเล่นกับ Developer Preview กันไปแล้ว ซึ่งฟีเจอร์ที่เคยเปิดตัวไปแล้วใน Developer Preview เวอร์ชันแรกจะขอข้ามไปเพื่อโฟกัสเฉพาะฟีเจอร์ใหม่ๆที่เพิ่งมีรายละเอียดเพิ่มเข้ามา สำหรับฟีเจอร์บน Android O ที่เคยเปิดตัวไปแล้วก่อนหน้านี้สามารถไปตามอ่านกันได้ที่ มาดูกันว่า Android O มีอะไรใหม่บ้าง ในแบบฉบับนักพัฒนา

NuuNeoI - มาดูกันว่า Android O มีอะไรใหม่บ้าง ในแบบฉบับนักพัฒนา
Android N ยังมีเครื่องได้ใช้กันไม่ถึง 3% ทั่วโลก คืนก่อนอยู่ดี ๆ กูเกิลก็ประกาศเปิดตัว Android O มาเป็นที่เรียบร้อยแล้ว ซึ่งถามว่าแปลกมั้ย ? จริง ๆ ไม่แปลกนะ เพราะปีที่แล้วกูเกิลก็ปล่อย Android N แบบน

ซึ่งในปีนี้ก็มีการอัปเดตข้อมูลของฟีเจอร์บน Android O เพิ่มเข้ามา นั่นก็คือ

  • Picture-in-Picture
  • Notification Dots
  • TensorFlow Lite

Picture-in-Picture

เดิมเป็นฟีเจอร์ที่มีเฉพาะใน Android TV เท่าน้ัน แต่ได้เปิดให้ใช้งานบน Phone และ Tablet ใน Android O แล้ว ซึ่งจะช่วยให้แอปฯยังคงทำงานได้ โดยแสดงผลเป็นแบบ Floating Window ในขณะที่ User ใช้งานอย่างอื่นภายในเครื่องอยู่ อาจจะฟังดูเหมือน Freeform ของ MultiWindow แต่ Picture-in-Picture นั้นจะใช้งานค่อนข้างสะดวกรวดเร็วและสำเร็จรูปมากกว่า โดยที่เน้นไปที่การแสดงวีดีโออย่างการดูหนังหรือวีดีโอคอลกับเพื่อนอยู่ แล้วต้องการทำอย่างอื่นควบคู่ไปด้วย

Notification Dots

เป็นการแสดง Notification ใน Home Screen คล้ายๆกับ Notification Badge แต่จะแตกต่างกันตรงที่ Notification Dots จะบอกแค่ว่ามี Notification ในแอปฯนั้นๆ ไม่ได้บอกจำนวนของ Notification

ผู้ใช้สามารถกดค้างที่ไอคอนแอปฯเพื่อดู Notification ของแอปฯนั้นๆแบบย่อได้ ซึ่งจะแสดงอยู่ในหน้าเดียวกับ Action Shortcut นั่นเอง โดยสีของ Notification Dots จะขึ้นอยู่กับไอคอนของแอปฯนั้นๆ

TensorFlow Lite

เป็น On-device Machine Learning ที่ทาง Google ใส่ไว้ใน Android O เพื่อให้สามารถทำ Machine Learning อย่างง่ายๆได้ โดยจะมี Library เพื่อเชื่อมต่อใช้งานผ่านแอปฯได้ด้วย (ไม่รู้ว่าตอนนี้มีแล้วหรือยัง) ซึ่งจะช่วยให้แอปฯสามารถนำข้อมูลจากการใช้งานแอปฯไปวิเคราะห์ด้วย Machine Learning เพื่อให้ได้ผลลัพธ์และนำไปใช้งานต่อได้

ยกตัวอย่างเช่น Smart Text Selection ของ Android O ที่จะวิเคราะห์ข้อความที่ผู้ใช้เลือก และ Suggest แอปฯที่เหมาะสมขึ้นมาทันที ไม่ต้องเสียเวลากด Copy แล้วสลับแอปฯเพื่อกด Paste

เห็นมั้ย AI เริ่มเข้ามามีบทบาทสำคัญมากแค่ไหน

เปิดให้ทดสอบ Android O ผ่าน Beta Program แล้ว

สำหรับเครื่องต่างๆที่รองรับการอัพเดท Android O จะสามารถเข้าร่วม Beta Program เพื่อรับอัปเดตเป็น Android O ผ่าน OTA ได้แล้ว โดยเข้าไปดูรายละเอียดได้ที่ https://android.com/beta

Android Support Library v26

ในที่สุดก็มาถึงเวอร์ชัน 26 แล้ว ซึ่งในเวอร์ชันจะตัดเวอร์ชันเก่าออกไปทำให้รองรับ Min SDK เขยิบขึ้นมาเป็น 14 แล้ว ซึ่งการตัดเวอร์ชันเก่าๆออกจะช่วยลดขนาด Library อีกทั้งยังทำให้ Method Count ลดลงไปถึงมากกว่า 1,400 Method เลยทีเดียว และล่าสุดก็ได้ย้ายไปอยู่บน Maven Repository ของ Google เรียบร้อยแล้ว

ซึ่งเจ้าของบล็อกขอหยิบเฉพาะบางส่วนมาพูดถึงเท่านั้นนะ เนื่องจากมีหลายๆอย่างยิบย่อยที่มีการเปลี่ยนแปลงและเพิ่มเข้ามาใหม่ในเวอร์ชันนี้

Font Resource Supporting

Font Resource นั้นเป็นฟีเจอร์ที่ถูกเพิ่มเข้ามาใหม่ใน Android O ซึ่งจะช่วยแก้ปัญหาเวลานักพัฒนาต้องการทำแอปฯที่ใช้ Custom Font เพราะแบบเดิมที่ทำกันอยู่คือต้องสร้าง Custom View แล้วกำหนดฟอนต์ผ่านโค้ดมาตลอด ซึ่งน่าแปลกใจตรงที่มี Support Font Resource ให้ด้วยนี่ล่ะ

ซึ่งความสามารถทั้งหมดนั้นเหมือนกับ Font Resource ที่อยู่บน Android O เลย นั่นหมายความว่าผู้ที่หลงเข้ามาอ่านสามารถเปลี่ยนมาใช้ Font Resource แทนวิธีเก่าๆได้แล้ว

สามารถดูรายละเอียดเพิ่มเติมได้ที่ Fonts in XML

Downloadable Fonts Supporting

นอกจาก Font Resource แล้ว Android Support Library เวอร์ชัน 26 นี้ ยังรองรับ Downloadable Font ที่เพิ่มเข้ามาใน Android O ด้วย นั่นหมายความว่าผู้ที่หลงเข้ามาอ่านสามารถทำแอปฯที่ผู้ใช้สามารถเปลี่ยนฟอนต์ภายในแอปฯได้ โดยที่ฟอนต์นั้นจะถูกดาวน์โหลดมาทีหลัง ไม่ได้มีอยู่ในเครื่องตั้งแต่แรก

ขอย้อนไปที่ Downloadable บน Android O ก่อนนะ ซึ่ง Android O นั้นจะมี Font Provider เพิ่มเข้ามาให้ เพื่อช่วยจัดการเรื่องการดาวน์โหลดฟอนต์มาลงในเครื่องให้ (รวมไปถึงขั้นตอนที่ผู้ใช้เลือกฟอนต์ที่จะติดตั้งลงในเครื่อง)

แล้วเวอร์ชันเก่าๆที่ต่ำกว่า Android O จะทำยังไงล่ะ? เพราะว่ามันไม่มี Font Provider ตั้งแต่แรก

คำตอบก็คือ Google Play Services v11 จะมี Font Provider มาให้ในตัวครับ

ดังนั้นเมื่อแอปฯทำงานอยู่บนเวอร์ชันที่ต่ำกว่า Android O ก็จะเรียกใช้งาน Font Provider จาก Google Play Services โดยการเรียกใช้งาน Font Provider จะต้องเปลี่ยนไปเรียกคลาสที่ชื่อว่า FontsContractCompat แทน

แต่ทว่า Google Play Services ก็ยังคงมีปัญหาที่บางเครื่องไม่ได้อัปเดตเป็นเวอร์ชันล่าสุด ซึ่งตัว FontsContractCompat ก็จะช่วยจัดการให้แล้ว โดยจะส่ง Fallback กลับมาให้แทน

สามารถดูรายละเอียดเพิ่มเติมได้ที่ Downloadable Fonts

Downloadable Fonts | Android Developers
Android 8.0 (API level 26) lets you download fonts instead of bundling them in your APK.

Emoji Compatibility Library

สำหรับบางแอปฯที่มีการใช้งาน Emoji อาจจะเคยเจอปัญหาเรื่อง Emoji บางตัวไม่มีในเวอร์ชันเก่าๆ จึงทำให้แสดงผลไม่ได้ ซึ่งใน Android Support Library ก็จะใช้ความสามารถของ Font Provider ช่วยเรื่องนี้ได้ โดยมีวิธีให้เลือกสองวิธี วิธีแรกคือให้ Font Provider ใน Google Play Services ช่วยจัดการให้ หรือจะเตรียม Emoji ไว้โดยติดตั้งผ่าน Dependency (ขนาด 7MB!!)

dependencies {
    /* ... */
    implementation "androidx.emoji:emoji-bundled:$version"
}

สามารถดูรายละเอียดเพิ่มเติมได้ที่ Emoji Compatibility

Emoji Compatibility | Android Developers
Emoji support library helps keep Android devices up to date with the latest emoji.

TextView Autosizing

TextView Autosizing ก็เป็นอีกหนึ่งในฟีเจอร์ของ Android O ที่ Backward Compatibility เหมือนกัน ซึ่งความสามารถเหมือนกับบน Android O ทั้งหมดเลย~

<TextView
    ..
    app:autoSizeTextType="uniform" 
    app:autoSizeMinTextSize="12sp"
    app:autoSizeMaxTextSize="100sp"
    app:autoSizeStepGranularity="2sp" />

Dynamic Animation Library

ไม่ได้อยู่ใน Android Support Library โดยตรง แต่เป็น Library สำหรับ Animation API ตัวใหม่ที่แยกออกมาอีกชุดหนึ่ง โดยจะเน้นไปที่ Physics-based Animation เพราะ Animation API ที่มีอยู่ตอนนี้ยังไม่ค่อยดูเป็นธรรมชาติซักเท่าไร ซึ่งจะขอพูดถึงในหัวข้อถัดไปแทน (เพราะมีรายละเอียดเยอะพอสมควร)

ทั้งหมดที่น่าสนใจก็มีประมาณนี้แหละนะ

ว่าแต่… มีนักพัฒนาคนไหนที่ไม่ได้ใช้ Android Support Library อยู่มั้ยนะ?

Dynamic Animation : Animation Library ตัวใหม่

ทุกวันนี้เวลาจะทำลูกเล่นอย่าง Animation ในแอปฯก็คงจะไม่พ้น Animator API อย่าง AnimatorSet และ ObjectAnimator เนอะ แต่ปัญหาที่ส่วนใหญ่เจอกันก็คือมันเป็นการเคลื่อนไหวที่ค่อนข้างจะทื่อๆ ไม่รู้สึกถึงความเป็นธรรมชาติ ถ้าอยากจะทำให้ดูเป็นธรรมชาติก็ต้องเขียนโค้ดหลายร้อยบรรทัด แถมไม่ใช่ง่ายๆด้วย

ดังนั้นทีมแอนดรอยด์จึงพัฒนา Library ตัวนี้ขึ้นมาโดยใช้หลักทางเคลื่อนที่แบบฟิสิกส์ (Physics-based Animation) เพื่อให้การเคลื่อนไหวดูสมจริงมากขึ้น มีแรงทางธรรมชาติเข้ามาเกี่ยวข้อง

โดย Animator API จะมีปัญหาอย่างหนึ่งที่ขัดใจเจ้าของบล็อกมากๆก็คือความเร็วในการเคลื่อนที่ของวัตถุนั้นขึ้นอยู่กับระยะเวลา (Duration) ดังนั้นความเร็วในการเคลื่อนที่จะสัมพันธ์กับเวลาและระยะทาง จึงทำให้ไม่สามารถกำหนดความเร็วในการเคลื่อนที่ได้

แต่สำหรับ Dynamic Animation จะใช้ความเร็วในการเคลื่อนที่ (Velocity) เป็นตัวแปรหลักในการทำงาน จึงทำให้ระยะเวลาที่ใช้ใน Animation ขึ้นอยู่กับค่าความเร็วและระยะทางแทน

dependencies {
    /* ... */
    implementation "androidx.dynamicanimation:dynamicanimation:$version"
}

Library ดังกล่าวนั้นรองรับเวอร์ชันต่ำสุดที่ API 16 โดยตอนนี้มี Animation ให้ใช้งานอยู่ 2 คลาสด้วยกัน คือ

  • FlingAnimation
  • SpringAnimation

Spring Animation

Spring Animation เป็นการเคลื่อนที่แบบสปริง (Spring-based Animation) ซึ่งใช้ Concept ในการเคลื่อนที่ด้วยแรงสปริง ดังนั้นอัตราเร็วและแรงเสียดทานในการเคลื่อนที่ของวัตถุจะใช้รูปแบบการเคลื่อนที่ของสปริง

ภาพข้างบนนี้คือตัวอย่างการเคลื่อนที่ของ Spring Animation ที่วัตถุอยู่ซ้ายมือ แล้วถูกลากไปทางขวามือ เมื่อปล่อยนิ้วออกจะทำให้วัตถุวิ่งกลับไปตำแหน่งเดิมโดยมีความยืดหยุ่นเหมือนกับมีสปริงอยู่จริงๆ

val springAnimation = SpringAnimation(view, DynamicAnimation.TRANSLATION_Y, 0f)
    .setStartValue(200f)
    .setStartVelocity(3000f)
    .start()

จะเห็นว่ารูปแบบคำสั่งคล้ายกับ ObjectAnimator ที่สามารถกำหนดค่าต่างๆและกำหนด View ที่ต้องการให้เล่น Animation ได้เลย (แต่จากที่ส่องโค้ดข้างใน พบว่าไม่ได้สร้างขึ้นมาจาก ObjectAnimator แต่อย่างใด แค่ทำให้ใช้งานคล้ายๆกันเฉยๆ)

ในการกำหนดรูปแบบการเคลื่อนไหวของสปริงจะมีคลาสที่ชื่อว่า SpringForce ที่สามารถกำหนดค่า Damping และ Stiffenss ได้ ซึ่งจะทำให้รูปแบบการเคลื่อนที่แตกต่างออกไปตามต้องการ

val springForce = SpringForce(0f)
    .setDampingRatio(0.4f)
    .setStiffness(500f)
val springAnimation = SpringAnimation(view, DynamicAnimation.TRANSLATION_Y, 0f)
        .setStartValue(200f)
        .setStartVelocity(3000f)
        .setSpring(springForce)
        .start()

ถ้าไม่อยากกำหนดเป็นตัวเลขให้วุ่นวาย ก็มีตัวแปรคงที่ให้เรียกใช้งานได้ทันที

// Stiffness Constant Value
SpringForce.STIFFNESS_VERY_LOW
SpringForce.STIFFNESS_LOW
SpringForce.STIFFNESS_MEDIUM
SpringForce.STIFFNESS_HIGH

// Damping Ratio Constant Value
SpringForce.DAMPING_RATIO_NO_BOUNCY
SpringForce.DAMPING_RATIO_MEDIUM_BOUNCY
SpringForce.DAMPING_RATIO_LOW_BOUNCY
SpringForce.DAMPING_RATIO_HIGH_BOUNCY

Fling Animation

Fling Animation เป็นการเคลื่อนที่แบบมีแรงเสียดทานเข้ามาเกี่ยวข้อง (Fling-based Animation) ซึ่งใช้ Concept เป็นการเคลื่อนที่ของวัตถุที่อยู่บนพื้นผิวที่มีแรงเสียดทาน

จากภาพจะเป็นการปัดนิ้วบน View ที่ Scroll ได้ เมื่อออกแรงปัดมาก ก็จะทำให้การ Scroll ไปได้ไกล และจะช้าลงเรื่อยๆจนกระทั่งหยุดนิ่ง

val flingAnimation = FlingAnimation(view, DynamicAnimation.TRANSLATION_Y)
    .setFriction(0.5f)
    .setStartValue(200f)
    .setStartVelocity(3000f)
    .start()

สำหรับ FlingAnimation จะเหมือนกับ SpringAnimation เลย แต่จะไม่มีคลาส SpringForce เข้ามาเกี่ยวข้อง ดังนั้นจึงขอจบเรื่อง FlingAnimation ไปอย่างรวดเร็วเลยนะ

ส่วน Event Listener ของ DynamicAnimation ก็จะมีลักษณะแบบนี้

val flingAnimation: FlingAnimation = ...
flingAnimation.addUpdateListener(OnAnimationUpdateListener { animation, value, velocity ->
    // Do something when animation updated
})
flingAnimation.addEndListener(OnAnimationEndListener { animation, canceled, value, velocity ->
    // Do something when animation ended
})

และที่น่าสนใจสำหรับ DynamicAnimation อีกอย่างหนึ่งก็คือมี Scroll Animation สำหรับใช้งานกับ ScrollView, ListView หรือ RecyclerView ได้ด้วย

DynamicAnimation.ALPHA
DynamicAnimation.ROTATION
DynamicAnimation.ROTATION_X
DynamicAnimation.ROTATION_Y
DynamicAnimation.SCALE_X
DynamicAnimation.SCALE_Y
DynamicAnimation.SCROLL_X
DynamicAnimation.SCROLL_Y
DynamicAnimation.TRANSLATION_X
DynamicAnimation.TRANSLATION_Y
DynamicAnimation.TRANSLATION_Z

นอกจากนี้ SpringAnimation และ FlingAnimation สามารถเอาไปใช้งานแบบ ValueAnimator ได้เหมือนกันนะเออ

จากความสามารถโดยรวมของ DynamicAnimation ถือว่าเป็นอีกเรื่องที่เจ้าของบล็อกสนใจมากๆ เพราะเป็น Library ที่ทำให้นักพัฒนาสามารถสร้าง Animation ที่ดูเป็นธรรมชาติและสวยงามมากขึ้น เพราะ Animation ภายในแอปฯจะช่วยดึงดูดความสนใจผู้ใช้ได้เป็นอย่างดี รวมไปถึงทำให้การใช้งานแอปฯดูลื่นไหลสวยงาม

ไว้จะหาเวลาเขียนเรื่องแบบนี้ละเอียดๆให้อ่านกันทีหลังครับ

ไม่ต้องดาวน์โหลด Plugin และ Library ผ่าน Android SDK Manager แล้ว

เดิมทีนั้น Plugin และ Library ของทีมแอนดรอยด์แทบทั้งหมดจะอยู่ใน Android SDK Manager ทำให้เวลาอัปเดตเวอร์ชันใหม่ๆก็จะต้องเปิด Android SDK Manager แล้วกดดาวน์โหลด ซึ่งเจ้าของบล็อกก็รู้สึกว่ามันเป็นอะไรที่น่าเบื่อเช่นกัน

ล่าสุด Plugin และ Library เหล่านี้ได้ถูกย้ายไปไว้บน Maven Repository ของ Google แล้ว จึงทำให้ไม่ต้องมานั่งกดอัปเดตเองอีกต่อไป เพียงแค่ระบุ URL ของ Maven Repository ของ Google ไว้ใน build.gradle ด้วย (Android Studio 3.0 ใส่ไว้เป็น Default แล้ว)

// build.gradle
buildscript {
    repositories {
        /* ... */
        google()
    }
    /* ... */
}

allprojects {
    repositories {
        /* ... */
        google()
    }
}
/* ... */

Plugin และ Library ที่ถูกย้ายไป Maven Repository ก็จะมีดังนี้

  • Gradle Plugin
  • Support Libraries
  • Data-Binding Library
  • Constrain Layout Library
  • Instant Apps Library
  • Architecture Components
  • Android Testing Libraries
  • Espresso

น่าจะทำแบบนี้ได้ตั้งนานแล้วนะ…

Firebase

ขอยกเนื้อหาสำหรับการเปลี่ยนแปลงและความสามารถใหม่ๆของ Firebase ให้กับบทความ มีอะไรใหม่จากทีม Firebase ที่งาน Google I/O 2017 แทนนะครับ

มีอะไรใหม่จากทีม Firebase ที่งาน Google I/O 2017
ครบขวบปีตั้งแต่การเปิดตัว Firebase ที่งาน Google I/O ปีที่แล้ว จนวันนี้ Firebase ก็มีโปรเจคที่นักพัฒนาทั่วโลกสร้างเกิน 1 ล้านโปรเจคไปแล้ว เหตุผลที่ Firebase ได้รับความนิยมอย่างมากมายนั่น ก็เพราะ…

Instant Apps

Instant App เป็นฟีเจอร์ที่ทาง Google ได้เปิดตัวขึ้นในปีที่แล้ว ซึ่งจะทำให้ผู้ใช้สามารถใช้งานแอปฯได้ทันทีโดยไม่ต้องติดตั้งลงในเครื่อง โดยจะอยู่ในรูปแบบของ URL ที่กดแล้วจะเปิดหน้าแอปฯตัวนั้นขึ้นมาได้ทันที แต่ว่าไม่ใช่การเปิดหน้าเว็ปหน้า เป็นการเปิดแอปฯที่ทำงานแบบ Native โดยสมบูรณ์

ในการเปิดใช้งานครั้งแรก ตัวเครื่องจะทำการดาวน์โหลดข้อมูลของแอพมาเก็บไว้ในเครื่องให้โดยอัตโนมัติ จากนั้นก็จะทำงานได้เหมือนกับแอปฯทั่วๆไป (แต่ไม่ต้องผ่านขั้นตอนการติดตั้ง) โดยมี Google Play เป็นหัวใจสำคัญในการทำงาน

ซึ่งการสร้าง Instant App นั้นจะทำผ่าน Android Studio ได้เลย โดยสร้างแอปฯแยกออกมาจากตัวเดิม โค้ดที่ใช้จะเป็นการหยิบโค้ดที่มีอยู่แล้วมาทำเป็น Instant App แทน ซึ่งจะต้องใช้วิธีที่เรียกว่า Modularization เข้ามาช่วย หรือพูดง่ายๆก็คือการแยกโค้ดตามฟีเจอร์ออกมาเป็นแต่ละ Module แทนที่จะรวมอยู่ใน Module เดียวกัน และ APK สำหรับ Instant App จะต้องมีขนาดไม่เกิน 4MB

จากรูปแบบดังกล่าวจะเห็นว่าโค้ดข้างในก็ยังคงเป็นเหมือนเดิม เพิ่มเติมคือเปลี่ยนรูปแบบของโครงสร้างโค้ด ทำให้ได้โค้ดที่สามารถ Build เป็น Instant App หรือ Installable App (แบบปกติ) ก็ได้

ซึ่งการทำ Modularization ที่ว่าก็คือการย้ายโค้ดออกไปไว้ใน Module น่ะแหละ โดย Android Studio จะย้ายโค้ดไปไว้ที่อีก Module ให้ โดยมีการตรวจสอบว่ามีไฟล์ไหนที่เกี่ยวข้องกับไฟล์นั้นๆหรือไม่ จะได้ย้ายไปทั้งยวงเลย ดังนั้นหัวใจสำคัญของการทำ Instant App คือต้องวางโครงสร้างโค้ดที่ยืดหยุ่นและสามารถทำงานแยกจากกันได้อย่างอิสระ ถ้าทีมพัฒนาทีมไหนไม่ได้วางไว้ตั้งแต่แรกก็อาจจะต้องไล่ปรับใหม่ทั้งยวงเลยก็ว่าได้ (หรือไม่ทำ Instant App ไปเลย ฮาๆ)

Instant App ถือว่าเป็นฟีเจอร์ที่แปลกใหม่และสร้างความว้าวได้พอสมควร แต่ก็ไม่ได้รู้สึกถึงความจำเป็นมากขนาดนั้น เมื่อเทียบกับการเขียน Web Apps ไปเลย จึงเป็นแค่หนึ่งทางเลือกในการเพิ่มโอกาสเข้าถึงผู้ใช้ให้มากขึ้นที่ดูเหมือนจะทับซ้อนกับทางเลือกอื่นๆอยู่พอสมควร ก็ตัดสินใจกันตามความเหมาะสมเนอะว่าจะทำมั้ย คุ้มมั้ย เมื่อเทียบกับทางเลือกอื่น

อ่านรายละเอียดเพิ่มเติมของ Instant Apps ได้ที่ g.co/InstantApps

Google Play Instant | Android Developers
<!-- hide description -->

ดูรายละเอียดของอุปกรณ์แอนดรอยด์ผ่าน Device Catalogue บน Google Play Developer Console

จริงๆแล้วบน Google Play Developer Console นั้นมีอัปเดตอะไรหลายๆอย่างมาก ซึ่งที่เจ้าของบล็อกชอบมากที่สุดก็คือ Device Catalogue ที่จะแสดงรายละเอียดของอุปกรณ์แอนดรอยด์แต่ละรุ่นที่เป็นประโยชน์ต่อนักพัฒนา

โดยเข้าไปดูได้ที่ https://play.google.com/apps/publish/ (ต้องเคยสมัครเป็น Developer ก่อน) โดยจะอยู่ในเมนูที่ชื่อว่า Device catalogue

สามารถกดดูทุกเครื่องที่มีในฐานข้อมูลของ Google Play Store ได้ โดยอุปกรณ์แต่ละเครื่องจะมีบอกข้อมูลสำคัญต่างๆไม่ว่าจะเป็น CPU, RAM, GPU, Screen Size, Screen Density และ API Version

สรุปแนวโน้มของ Android Development

เรื่องแอนดรอยด์ที่พีคที่สุดของปีนี้ก็คงไม่พ้นเรื่อง Kotlin ซึ่งเป็นเสมือนสัญญาณบอกว่าถึงเวลาแล้วที่จะต้องเรียนรู้ Kotlin ติดตัวไว้ด้วย ในด้านการเรียนการศึกษาอาจจะยังไม่สำคัญมากนัก ไม่จำเป็นต้องเปลี่ยนเนื้อหาไปเป็น Kotlin ก็ได้ เพราะสุดท้ายแล้วควรให้ผู้เรียนโฟกัสไปที่ Java เป็นหลักก่อน เมื่อเข้าใจ Java มากพอสมควรแล้ว การจะเรียนรู้ Kotlin เพิ่มเติมก็ไม่ใช่เรื่องยากนัก

แต่สำหรับสายงาน Android Developer นั้นถือว่าจำเป็นต้องรู้แล้วครับ อาจจะไม่จำเป็นต้องรู้แบบทะลุปรุโปร่ง แต่อย่างน้อยก็ต้องอ่านโค้ด Kotlin ให้เป็นครับ เพราะในอีกไม่นานก็จะได้เห็นแอปฯต่างๆพากันใช้ Kotlin กันแน่นอน (เพราะที่ผ่านมายังลังเลกันอยู่) ไม่ควรยึดติดกับ Java อีกต่อไปแล้ว ในเมื่อมี Kotlin เป็นทางเลือก ก็ควรเรียนรู้เพิ่มเติมเพื่อเพิ่มโอกาสให้กับ Career Path ให้มากขึ้นครับ

เรื่องที่พีครองลงมาก็คงจะเป็น Architecture Components นี่แหละ เพราะว่าเรื่อง Code Structure บนแอนดรอยด์นั้นเป็นอะไรที่ถกเถียงกันมานานแล้ว โดยเฉพาะการนำ Pattern ใหม่ๆอย่าง MVVM หรือ MVP ไปใช้งานกับแอนดรอยด์ที่ถูกวิธี (หลายๆคนก็คงมองเห็นถึงปัญหานี้กันอยู่แล้ว) และนั่นก็ทำให้ทีมแอนดรอยด์ได้ออกแบบ Pattern ที่ปลอดภัย มีประสิทธิภาพ ยืดหยุ่น และเขียนเทสได้ (อันนี้สำคัญ) โดยที่นักพัฒนาไม่จำเป็นต้องคิดเอง ลองเอง เจ็บเอง สามารถทำความเข้าใจและหยิบไปใช้งานได้เลย

แต่ก็อย่างที่บอกไปในตอนแรกครับว่าถ้าคิดว่า Pattern ที่ใช้กันอยู่ภายในทีมนั้นโอเคแล้ว ไม่มีปัญหาอะไร ก็ใช้ต่อไปได้ครับ แต่ถ้ากำลังมองหาวิธีหรือ Pattern ใหม่ๆที่จะมาแก้ปัญหาในที่มีอยู่ในโปรเจค ขอแนะนำ Architecture Components เลยครับ

ในปีนี้ Sundar Pichai ได้กล่าวไว้ชัดเจนว่า "Mobile first to AI first" ซึ่งก็คือ Google จะเน้นโฟกัสไปที่ AI มากขึ้นอย่างเห็นได้ชัด แต่นั่นก็ไม่ได้หมายความว่า Mobile จะกลายเป็นสิ่งที่ล้าสมัยหรือตายลง (ใครที่กังวล Career Path ด้านนี้ก็อย่าคิดมากไป) เพราะสุดท้ายแล้วการที่ AI พัฒนาตัวเองได้นั้นจะต้องมีสิ่งที่เรียกว่า "ข้อมูล" ในปริมาณที่เยอะมาก และการจะหาข้อมูลเหล่านั้นมาใช้พัฒนา AI ได้ ส่วนหนึ่งก็มาจาก Mobile ที่ใช้กันอยู่ทุกวันนี้นั่นเอง ดังนั้นต่อไปก็คงจะเข้าสู่ AI แน่นอนครับ แต่ในนั้นก็จะมี Mobile เป็นหัวใจสำคัญเช่นกัน โอเคเนอะ

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

แล้วเจอกันอีกทีตอน Google I/O 2018 ครับ