มาทำ Loading ที่ดีต่อใจผู้ใช้ใน Android App กันเถอะ

ทุกวันนี้แอปฯที่เหล่า Android Dev เขียนกัน จะต้องมีการทำงานบางส่วนที่ใช้เวลานาน (อย่างเช่น ดึงข้อมูลจาก Web Service) ซึ่งระหว่างที่รอการทำงานเหล่านั้นเสร็จ ก็ต้องแสดงอะไรบางอย่างบนหน้าจอเพื่อบอกให้ผู้ใช้รู้ว่า “กำลังโหลดอยู่นะ” หรือที่เรียกกันว่า Loading นั่นเอง

ว่าแต่… ทุกวันนี้ผู้ที่หลงเข้ามาอ่านเขียนคำสั่งเพื่อแสดง Loading แบบไหนกันอยู่ล่ะ?

ไม่ว่าจะเป็น Progress Dialog, Loading Dialog หรืออะไรอื่นๆที่เกี่ยวกับการ Loading เจ้าของบล็อกก็เรียกสั้นๆว่า Loading นะ

Loading นั้นสำคัญไฉน

ทุกวันนี้เวลาที่แอปถูกออกแบบและสร้างขึ้นมามักจะถูกมองข้ามในเรื่องของการแสดง Loading อยู่บ่อยครั้ง ทั้งๆที่ในแต่ละหน้าถูกออกแบบมาอย่างสวยงาม แต่ทว่าส่วนใหญ่มักจะลืมนึกถึงว่าจะแสดง Loading ยังไงนี่สิ

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

โดยการสร้าง Loading ในแอนดรอยด์นั้นสามารถทำได้หลากหลายรูปแบบ ซึ่งแต่ละแบบก็จะมีความเหมาะสมและรูปแบบในการทำงานแตกต่างกันไป

ใช้ Progress Dialog : Loading ท่ีง่ายที่สุด

ProgressDialog นั้นเป็นคลาสที่มีมาให้อยู่แล้วใน Android API ซึ่งนักพัฒนาสามารถเรียกใช้งานด้วยคำสั่งในรูปแบบง่ายๆแบบนี้

import android.app.ProgressDialog

...

// สร้างและแสดง Progress Dialog
val loadingDialog = ProgressDialog.show(context, "Fetch photos", "Loading...", true, false)

// ซ่อน Progress Dialog
loadingDialog.dismiss()

ในการสร้าง ProgressDialog จะใช้คำสั่ง show ซึ่งเป็น Static Method ของคลาสนี้ และสามารถกำหนดได้ว่าจะให้แสดงข้อความยังไง รวมไปถึงกำหนดได้ว่าจะให้ผู้ใช้กดยกเลิกได้หรือไม่

จะได้เป็น Loading ที่มาหน้าตาเป็นแบบนี้

ง่ายใช่มั้ยล่ะ? เนื่องจากคำสั่งที่เรียกใช้งานได้ง่ายมาก ดังนั้นจึงสามารถเห็นการใช้ ProgressDialog ในหลายๆแอป

แต่ทว่า ProgressDialog นั้นมีข้อเสียหลักๆก็คือมันไม่มี Support Library อย่าง AppCompat v7 จึงทำให้ Progress Dialog นั้นมีหน้าตาแปลเปลี่ยนไปตาม Theme ในแอนดรอยด์แต่ละเวอร์ชัน

นั่นหมายความว่าถ้าผู้ที่หลงเข้ามาอ่านออกแบบหน้าตาแอปเป็นแบบ Material Design (ไม่ว่าจะใช้ AppCompat v7 หรือ Custom เองก็ตาม) แต่พอเปิดแอปบนแอนดรอยด์เวอร์ชันต่ำกว่า 5.0 (API 21) ก็จะพบเจ้า ProgressDialog มีหน้าตาหลุดธีมที่วางไว้มากมาย

ใช้ Progress Dialog จาก 3rd Party Library

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

เจ้าของบล็อกก็ใช้ Library ในบางครั้งเช่นกัน ถ้าตัวที่ใช้บ่อยที่สุดก็คงจะเป็น Material Dialogs [GitHub] เพื่อทำ Dialog ที่มีหน้าตาเป็น Material Design ซึ่งรองรับต่ำสุดที่ API 14 ซึ่งใน Library ตัวนี้ก็จะมี Progress Dialog ที่เตรียมไว้ให้แล้ว

สำหรับกรณีที่เขียน Progress Dialog ขึ้นมาเอง ไม่ว่าจะทำ Custom Dialog หรือใช้ Dialog Fragment จะขอข้ามไปนะ เพราะว่าเจ้าของบล็อกเน้นไปที่รูปแบบในการแสดงผลเท่านั้น

แต่ Progress Dialog ก็ไม่ใช่วิธีที่ดีเสมอไป

การใช้ Progress Dialog นั้นมีข้อดีตรงที่เรียกใช้งานง่าย สะดวก ไม่ต้องทำอะไรมาก และไม่ต้องกังวลว่าผู้ใช้จะไปกดอะไรบนหน้าจอระหว่างกำลังรออยู่ เพราะสามารถกำหนดไม่ให้ผู้ใช้ปิด Progress Dialog (Cancellable) ได้ สั่งปิดได้ผ่านโค้ดเท่านั้น

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

ถ้านึกไม่ออก ให้ลองสมมติว่า Facebook ที่ใช้กันอยู่ทุกวันนี้เกิดมี Progress Dialog ขึ้นมาดูครับ

ผู้ใช้เข้ามาหน้า Feed หรือหน้า Profile ของใครซักคน ก็ต้องเจอ Progress Dialog ทุกครั้ง และก็ต้องรอจนกว่าจะโหลดหน้านั้นเสร็จทั้งหมด ซึ่งบอกเลยว่า ไม่ใช่ทุกคนหรอกที่จะยอมนั่งรอแบบนี้

ดังนั้นในกรณีที่แย่ที่สุด (และเกิดขึ้นได้บ่อย) ก็คือ "กดปุ่ม Recent App" > "ปิดแอปจากหน้า Recent App ทิ้ง" > "เปิดแอปใหม่"

ฟังดูแย่ใช่มั้ยล่ะ?

ดังนั้นการใช้ Progress Dialog สำหรับเจ้าของบล็อกถือว่าควรใช้ในกรณีที่จำเป็นจริงๆเท่านั้น (อย่างเช่น รอการทำการชำระเงินเสร็จ หรือ กด Submit ข้อมูลในฟอร์มสมัครใช้บริการบางอย่าง เป็นต้น) และถ้าเป็นไปได้ก็ควรเลี่ยงไปใช้วิธีอื่นแทน

แสดง Loading ที่ Content ไปเลย

เพื่อไม่ให้ผู้ใช้อารมณ์เสียเพราะกดอะไรไม่ได้ระหว่างที่ Loading กำลังทำงานอยู่ จึงทำให้หลายๆแอปเลือกที่จะแสดง Loading ใน Content นั้นๆไปเลย

AirBNB ก็เป็นหนึ่งในแอปที่ใช้วิธีการแสดง Loading แบบนี้ โดยจะซ่อน Content และแสดง Loading เมื่อรอการทำงานบางอย่างจนเสร็จ แล้วค่อยสลับการแสดงผลเพื่อโชว์ Content

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

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <FrameLayout
        android:id="@+id/layoutContentContainer"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <!-- Content -->

    </FrameLayout>

    <FrameLayout
        android:id="@+id/layoutLoadingContainer"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <!-- Loading -->
        
    </FrameLayout>
</FrameLayout>

อยากจะแสดงแสดง Loading หรือ Content ก็ใช้วิธีกำหนด Visibility หรือจะทำ Transition อย่าง Fade In/Out ก็ได้ จะเขียนแบบไหนก็แล้วแต่ผู้ที่หลงเข้ามาอ่านเลย

class MainActivity : AppCompatActivity() {
    ...
    fun showLoading() {
        layoutContentContainer.visibility = View.GONE
        layoutLoadingContainer.visibility = View.VISIBLE
    }

    fun hideLoading() {
        layoutContentContainer.visibility = View.VISIBLE
        layoutLoadingContainer.visibility = View.GONE
    }
    ...
}

แต่ทว่าการเขียนโค้ดด้วยวิธีแบบนี้จะเหนื่อยพอสมควร เพราะว่าเจ้าของบล็อกต้องสร้าง Frame Layout 3 ตัว (1 Parent Layout และ 2 Child Layout) เพื่อครอบ Layout ส่วนที่เป็น Content แล้วเพิ่ม Layout ของ Loading เข้าไป

ซึ่งในการใช้งานจริง ก็ไม่ได้มีแค่ Content เดียวที่จะทำ Loading ดังนั้นเจ้าของบล็อกก็ต้องไปนั่งไล่แปะใน Layout XML แล้วเพิ่มโค้ดเพื่อกำหนด Visibility ให้ครบ

ใช้ 3rd Party Library ให้ชีวิตง่ายขึ้นเถอะ

เพื่อลดขั้นตอนการสร้าง Layout เพื่อทำ Loading เจ้าของบล็อกแนะนำว่าให้หา Library มาใช้ดีกว่า ซึ่ง Library ที่เจ้าของบล็อกหยิบมาใช้มีชื่อว่า EmptyView [GitHub] ที่ทำ Loading ในลักษณะของ Layout มาให้แล้ว สามารถเอาไปครอบ Content ที่ต้องการได้เลย

<com.santalu.emptyview.EmptyView
    android:id="@+id/layoutContentContainer"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <!-- Content -->

</com.santalu.emptyview.EmptyView>

นอกจากการแสดง Loading และ Content แล้ว ยังสามารถแสดง Layout สำหรับกรณี Empty และ Error ได้ด้วยนะ ถือว่าเป็น Library ที่สะดวกสบายพอสมควรเลยล่ะ

layoutContentContainer.showContent()
layoutContentContainer.showLoading()
layoutContentContainer.showError()
layoutContentContainer.showEmpty()

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

แสดง Loading ผ่าน Snack Bar

เป็นอีกหนึ่งวิธีที่น่าสนใจและเริ่มถูกใช้งานในแอปหลายๆตัว โดยให้แสดงเป็น Snack Bar อยู่ที่ข้างล่างหน้าจอเพื่อให้ผู้ใช้รับรู้

เหมาะกับการทำงานที่ใช้ระยะเวลาที่ไม่นานมากนัก และแสดงผลตอนที่เปิดใช้งานแอปอยู่เท่านั้น โดยที่ผู้ใช้ก็ยังสามารถใช้งานได้โดยไม่ต้องรอการทำงานนั้นๆจนเสร็จ

ซึ่งการใช้ Snack Bar มาทำเป็น Loading ก็ไม่ได้มีอะไรซับซ้อนมากนัก เพราะเรียกใช้งานง่ายอยู่แล้ว

// Start
val snackbar: Snackbar = Snackbar.make(view, "Loading...", Snackbar.LENGTH_INDEFINITE)
snackbar.show()

// Done
snackbar.dismiss()

แต่การใช้ Snack Bar ก็จะมีปัญหาอยู่นิดหน่อยตรงที่มันถูกออกแบบมาให้แสดงเฉพาะตอนเปิดแอปอยู่เท่านั้น จึงไม่รองรับกรณีที่ผู้ใช้ออกจากหน้าจอซักเท่าไร จึงทำให้ Snack Bar เหมาะกับเฉพาะบางกรณีเท่านั้นจริงๆ

แสดง Loading ผ่าน Progress Notification

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

ซึ่งมันก็คือการสร้าง Foreground Service (Service + UI) นั่นเอง ด้วยวิธีนี้คำสั่งที่ต้องใช้เวลาทำงานนานๆก็จะไปทำงานอยู่บน Service แทนที่จะอยู่บน Activity หรือ Fragment

ยกตัวอย่างเช่น เจ้าของบล็อกต้องการเขียนโค้ดเพื่อสมัครสมาชิกผ่าน Web Service หรือ API ตัวหนึ่ง โดยใช้ Progress Notification

Activity/Fragment จะไปสั่งให้ Foreground Service ทำงาน ซึ่ง Service ดังกล่าวก็จะทำการเรียก Web Service พร้อมกับแสดง Progress Notification เพื่อให้ผู้ใช้สามารถรับรู้ได้ว่า “กำลังสมัครอยู่นะ รอแปปนึงนะ”

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

ซึ่ง Flow การทำงานของ Progress Notification จะยุ่งยากนิดหน่อย ผู้ใช้สามารถทำอะไรก็ได้ในขณะที่ Progress Notification กำลังทำงานอยู่ ไม่ว่าจะเป็น

  • เปิดหน้านั้นทิ้งไว้
  • สลับไปแอปอื่นๆ
  • ปิดแอป
  • สลับไปที่หน้าอื่นๆภายในแอป
  • ปิดหน้าจอ

ดังนั้นถ้าผู้ที่หลงเข้ามาอ่านอยากจะใช้ Progress Notification ก็จะต้องออกแบบ Flow ให้รองรับกับความเป็นไปได้ทุกแบบด้วย

ในการออกแบบ Flow แบบที่ง่ายที่สุดนั้นจะมีลักษณะเป็นแบบนี้

จะเห็นว่าการทำ Progress Notification นั้นมี Test Case เกิดขึ้นได้หลายแบบมาก และยังส่งผลต่อ Screen Flow ด้วย เพราะส่วนใหญ่ชอบออกแบบ Screen Flow โดยสมมติว่า User ใช้งานแอปตลอดเวลาตั้งแต่ต้นจนจบขั้นตอน ไม่ได้มีกรณีที่ User หยุดใช้งานชั่วคราว แล้วค่อยกลับมาใช้งานต่อ ดังนั้นถ้ามีการใช้ Progress Notification ก็แนะนำว่าให้เช็คให้ดีว่าจะไปขัดอะไรกับ Screen Flow เดิมที่ออกแบบไว้หรือป่าว

สำหรับส่วนของโค้ดก็จะเน้นไปที่การทำงานของ Foreground Service, Notification และการส่งข้อมูลจาก Foreground Service กลับมายัง Activity เพื่อทำงานต่อ ซึ่งสามารถดูรูปแบบของโค้ดได้ที่ Progress Notification [GitHub]

akexorcist/ProgressNotification
[Android] Example of progress notification when do some asynchronous things - akexorcist/ProgressNotification

สรุป

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

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

ใช้ Progress Dialog ทำเป็น Loading

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

แสดง Loading ที่ Content ไปเลย

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

แต่ถึงกระนั้นก็จะมีข้อเสียก็ตรงที่ต้องเข้าไปจัดการในส่วนของ Layout XML ด้วย ถ้าหน้านั้นๆมี Loading หลายๆส่วนก็จะรู้สึกเปลือง Layout เพื่อทำ Loading พอสมควร (รวมไปถึงโค้ดที่ต้องเขียนสั่งงานด้วย)

ใช้ Snack Bar ทำเป็น Loading

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

ใช้ Progress Notification ทำเป็น Loading

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

เลือกใช้ตามความเหมาะสมละกัน

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