วิธีสร้าง Backend สำหรับแอป Android

ภาพหน้าปก Back4app Android App

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

ประเด็นสำคัญ

  • ความโดดเด่นของ Android: Android ในฐานะระบบปฏิบัติการแบบโอเพ่นซอร์ส มีการใช้งานอยู่ประมาณ 70% ของอุปกรณ์มือถือทั้งหมด และมีผู้ใช้งานมากกว่า 3 พันล้านคน
  • ตัวเลือกแบ็กเอนด์สำหรับแอป: ในการทำแบ็กเอนด์สำหรับแอปมือถือ นักพัฒนาสามารถเลือกใช้ Infrastructure as a Service (IaaS), Platform as a Service (PaaS) หรือ Backend as a Service (BaaS)
  • ขั้นตอนในการสร้างแบ็กเอนด์: เราจะอธิบายขั้นตอนโดยละเอียดเกี่ยวกับการสร้าง Android แบ็กเอนด์โดยใช้แพลตฟอร์ม BaaS และมีโค้ดตัวอย่างในตอนท้ายของบทความ

Android คืออะไร?

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

การพัฒนา Android เริ่มขึ้นในปี 2003 โดยบริษัท Android Inc. เดิมทีมีเป้าหมายที่จะสร้างระบบปฏิบัติการสำหรับกล้องดิจิทัล แต่ได้ปรับเปลี่ยนไปสู่การพัฒนาระบบปฏิบัติการมือถือเพื่อเข้าถึงตลาดที่กว้างขึ้น

ในปี 2005 บริษัทถูกซื้อกิจการโดย Google พร้อมทั้งพนักงานทั้งหมด โดย Android เวอร์ชันแรกถูกปล่อยออกมาในปี 2008

แม้ว่า Android จะเป็นโอเพ่นซอร์ส แต่บนอุปกรณ์พกพาส่วนใหญ่จะรันเวอร์ชันที่เป็นกรรมสิทธิ์ของ Google ซึ่งมาพร้อมกับซอฟต์แวร์อย่าง Google Chrome, YouTube, Google TV และ Gmail

เนื่องจาก Android สามารถปรับแต่งได้อย่างมาก หลายผู้ผลิตจึงปรับแต่งหน้าตา Android ให้สอดคล้องกับแบรนด์ของตน นั่นเป็นเหตุผลที่ Android ของ OnePlus มีหน้าตาที่ต่างจาก Android ของ Pixel

Android เป็นระบบปฏิบัติการที่ได้รับความนิยมที่สุดตั้งแต่ปี 2013 โดยมีการใช้งานประมาณ 70% ของอุปกรณ์พกพา และมีผู้ใช้งานมากกว่า 3 พันล้านคนต่อเดือน

นอกจากนี้ Google Play Store ยังมีแอปให้เลือกดาวน์โหลดมากกว่าสามล้านแอป อ่านต่อเพื่อเรียนรู้วิธีสร้างแบ็กเอนด์สำหรับแอป Android

ข้อดีของการพัฒนา Android

เรามาดูข้อดีบางประการของการพัฒนาแอปสำหรับ Android กัน

รองรับข้ามแพลตฟอร์ม

เมื่อพัฒนาแอปสำหรับ Android คุณจะสามารถเข้าถึงอุปกรณ์ได้หลากหลาย ทั้งโทรศัพท์มือถือ อุปกรณ์สวมใส่ สมาร์ททีวี เครื่องเล่นเกมคอนโซล และอื่น ๆ อีกมากมาย หากคุณมีความรู้ในการพัฒนาแอปบนมือถือ คุณก็จะพัฒนาแอปบนอุปกรณ์สวมใส่หรือสมาร์ททีวีได้ไม่ยาก

ตลาดใหญ่

อย่างที่กล่าวไปก่อนหน้านี้ Android มีส่วนแบ่งตลาดประมาณ 70% เมื่อเทียบกับ iOS ที่ 30% ดังนั้นการพัฒนาแอปบน Android จึงทำให้คุณเข้าถึงผู้ใช้ได้กว้างขวางยิ่งขึ้น

นอกจากนี้ แอปบน Android ยังมีต้นทุนการพัฒนาต่ำและให้ผลตอบแทน (ROI) ที่ค่อนข้างดี และต่างจาก iOS ตรงที่ไม่มีค่าธรรมเนียมรายปีสำหรับนักพัฒนา

ปรับแต่งได้สูง

อุปกรณ์ Android สามารถปรับแต่งได้อย่างยืดหยุ่นเมื่อเทียบกับระบบปฏิบัติการอื่น ๆ คุณสามารถปรับเปลี่ยนเกือบทุกอย่างที่คุณนึกออก

นอกจากนี้ Android ยังอนุญาตให้ผสานการทำงานกับบริการของบุคคลที่สาม (3rd-party) ได้อย่างง่ายดาย

ชุมชนขนาดใหญ่

Android ได้รับการสนับสนุนจากชุมชนโอเพ่นซอร์สขนาดใหญ่ อีกทั้งยังมาพร้อมกับเครื่องมือที่เป็นมิตรต่อเหล่านักพัฒนา เช่น Android Studio, ADB และ Logcat ซึ่งช่วยให้คุณเขียนโค้ดแอปได้สะดวก

ถ้าคุณติดปัญหาใด ๆ คุณสามารถขอความช่วยเหลือได้จากหลายแพลตฟอร์ม ไม่ว่าจะเป็น GitHub, StackOverflow, Reddit และชุมชนที่มุ่งเน้น Android อีกมากมาย

ข้อจำกัดของการพัฒนา Android

นี่คือข้อเสียบางประการของการพัฒนา Android

ความปลอดภัย

Android มีความปลอดภัยน้อยกว่า iOS และไม่ได้บังคับใช้มาตรการความปลอดภัยที่เข้มงวด เนื่องจาก Android เป็นโอเพ่นซอร์ส ทำให้มีการค้นพบช่องโหว่ความปลอดภัยบ่อยครั้ง ซึ่งเปิดโอกาสให้แฮกเกอร์เข้าถึงช่องโหว่ก่อนที่จะได้รับการแก้ไข

อุปกรณ์ Android ยังสามารถรูท (root) เพื่อให้ได้สิทธิ์ superuser ซึ่งทำให้เครื่องมีความสามารถมากขึ้น แต่ก็เสี่ยงเนื่องจากมาตรการความปลอดภัยบางอย่างอาจถูกปิดใช้งาน

ความซับซ้อน

อุปกรณ์ Android มีหลายขนาดและหลายรูปแบบ แม้ว่าจะเป็นข้อได้เปรียบ แต่ก็ทำให้เกิดความยุ่งยากในการพัฒนาแอปให้รองรับอุปกรณ์หลายประเภท

เพื่อให้แอปรองรับอุปกรณ์ได้มากที่สุด คุณจำเป็นต้องคำนึงถึงการออกแบบให้ตอบสนอง (responsive design) และลักษณะฮาร์ดแวร์ของแต่ละอุปกรณ์ เป็นต้น

แอปแบบพรีเมียม

แอปแบบพรีเมียมประสบความสำเร็จน้อยกว่าบน Google Play Store เมื่อเทียบกับ App Store เป็นที่ทราบกันดีว่าผู้ใช้ Android มักใช้จ่ายกับแอปน้อยกว่า iOS ถ้าคุณกำลังพัฒนาแอปที่เน้นโมเดลพรีเมียมเป็นหลัก Android อาจเป็นตัวเลือกที่สอง

ตัวเลือกแบ็กเอนด์สำหรับแอป Android

แล้วแบ็กเอนด์แบบไหนที่เหมาะกับแอป Android? แบ็กเอนด์สำหรับแอปมือถือสามารถโฮสต์บนโครงสร้างพื้นฐานของตนเองหรือบนบริการคลาวด์ โดยโมเดลยอดนิยมของระบบคลาวด์สำหรับแบ็กเอนด์มือถือได้แก่:

  1. Infrastructure as a Service (IaaS)
  2. Platform as a Service (PaaS)
  3. Backend as a Service (BaaS)

แต่ละตัวเลือกจะมีระดับการทำงานที่ครอบคลุมความซับซ้อต่างกัน ตามที่แสดงในภาพด้านล่าง

IaaS vs PaaS vs BaaS Abstraction Layers

มาดูแต่ละตัวเลือกแบบละเอียด เพื่อช่วยให้คุณเลือกโมเดลคลาวด์ที่เหมาะสมสำหรับแบ็กเอนด์แอปของคุณ

Infrastructure as a Service (IaaS)

Infrastructure as a Service (IaaS) เป็นโมเดลการประมวลผลแบบคลาวด์ที่มีการครอบคลุมน้อยที่สุด กล่าวคือ ผู้ให้บริการจะมอบทรัพยากรเสมือน (Virtualized Resources) เช่น เซิร์ฟเวอร์ ระบบเครือข่าย พื้นที่เก็บข้อมูล และระบบปฏิบัติการ ผ่าน API ระดับสูงหรือแดชบอร์ดที่เข้าใจง่าย

จุดเด่นของ IaaS คือผู้ใช้จะควบคุมโครงสร้างพื้นฐานได้อย่างสมบูรณ์ แม้ว่าจะยืดหยุ่นและขยายขนาดได้สูงสุด แต่ก็ต้องใช้การดูแลจัดการมากที่สุด หากคุณเลือก IaaS คุณอาจต้องมีผู้ดูแลระบบ (SysAdmin) อย่างน้อยหนึ่งคน

ตัวอย่างของ IaaS ได้แก่ AWS, Google Cloud Platform และ Azure

Platform as a Service (PaaS)

Platform as a Service (PaaS) เป็นโมเดลคลาวด์ที่ถูกออกแบบมาเพื่อช่วยนักพัฒนาสร้าง จัดการ และปรับใช้แอปพลิเคชัน

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

การใช้ PaaS ทำให้คุณสามารถโฟกัสกับซอฟต์แวร์และประสบการณ์ผู้ใช้ของคุณโดยไม่ต้องกังวลเกี่ยวกับโครงสร้างพื้นฐานเบื้องหลัง

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

แพลตฟอร์ม PaaS ที่ได้รับความนิยมที่สุด ได้แก่ Heroku, Render และ Digital Ocean App Platform

Backend as a Service (BaaS)

Backend as a Service (BaaS) เป็นบริการที่ช่วยลดภาระในการพัฒนาแบ็กเอนด์ลงอย่างมาก เพราะผู้ให้บริการจะมีแบ็กเอนด์สำเร็จรูปให้อย่างครบถ้วน

ฟีเจอร์ของ BaaS ได้แก่ การจัดการผู้ใช้ ฐานข้อมูล การยืนยันตัวตน (Authentication) การเชื่อมต่อกับโซเชียล Push Notifications, SDKs และอื่น ๆ อีกมากมาย

BaaS ให้ข้อดีทั้งหมดของ IaaS และ PaaS พร้อมทั้งฟีเจอร์เพิ่มเติม ทีมที่ใช้ BaaS มักจะปล่อยผลิตภัณฑ์ได้เร็วขึ้น ประหยัดค่าใช้จ่ายทางวิศวกรรม และพัฒนาซอฟต์แวร์ที่มีคุณภาพดีขึ้น

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

BaaS ที่เป็นตัวอย่าง ได้แก่ Back4app, AWS Amplify และ Firebase

วิธีสร้างแบ็กเอนด์สำหรับแอป Android

ในส่วนนี้ของบทความ เราจะมาดูวิธีสร้างแอปแบ็กเอนด์โดยใช้ Back4app และวิธีเชื่อมต่อกับแอป Android ของเรา

สิ่งที่ต้องมีล่วงหน้า

  • มีประสบการณ์ใช้ Kotlin และการพัฒนา Android
  • เข้าใจพื้นฐานของ Mobile Backend as a Service
  • ติดตั้ง Java และ Android Studio ไว้บนเครื่อง

Back4app คืออะไร?

Back4app เป็นแพลตฟอร์มที่ยอดเยี่ยมสำหรับการสร้างแบ็กเอนด์สำหรับแอปสมัยใหม่ และยังช่วยเร่งกระบวนการพัฒนาได้เป็นอย่างดี

มาพร้อมกับฟีเจอร์ฝั่งเซิร์ฟเวอร์ที่เป็นประโยชน์ เช่น การจัดการผู้ใช้ ฐานข้อมูลเรียลไทม์ การเชื่อมต่อโซเชียล Cloud Code, Push Notifications, APIs และอื่น ๆ อีกมากมาย

เมื่อใช้ Back4app คุณจะสามารถลดงานส่วนแบ็กเอนด์ลง และให้ความสำคัญกับฟีเจอร์หลักของแอปได้ รวมถึงช่วยเร่งการพัฒนาแบ็กเอนด์ของแอป Android ได้อีกด้วย

คุณไม่ต้องกังวลเกี่ยวกับโครงสร้างพื้นฐานของแบ็กเอนด์ เซิร์ฟเวอร์ การขยายตัว การดูแลรักษา และอื่น ๆ

ที่สำคัญ Back4app ยังมีแพ็กเกจฟรีที่เหมาะสำหรับการทดลองหรือทดสอบแพลตฟอร์ม และเมื่อแอปของคุณเติบโตขึ้น ก็สามารถอัปเกรดเป็นแพลนแบบพรีเมียมได้

แนะนำโปรเจกต์ที่จะสร้าง

ในบทความนี้ เราจะสร้างแอปจดบันทึก (notes app) อย่างง่าย โดยผู้ใช้สามารถเพิ่ม แก้ไข และลบโน้ตได้ โน้ตเหล่านั้นจะถูกเก็บไว้ในฐานข้อมูลของ Back4app และเราจะสื่อสารกับฐานข้อมูลผ่าน Parse SDK

เราจะใช้ภาษา Kotlin ในการพัฒนาแอป และสำหรับการออกแบบ UI จะใช้ Jetpack Compose ซึ่งเป็นเครื่องมือสมัยใหม่ของ Android สำหรับการสร้าง UI แบบเนทีฟ

ภาพตัวอย่าง Back4app Android Notes App

สร้างแอปบน Back4app

ขั้นตอนแรกในการพัฒนาแบ็กเอนด์สำหรับแอป Android คือคุณต้องมีบัญชี Back4app ถ้าคุณยังไม่มี ให้ ลงทะเบียน ก่อน

เมื่อคุณเข้าสู่ระบบ Back4app แล้ว คุณจะถูกนำไปยังหน้ารวมแอป คลิก “Build new app” เพื่อเริ่มการสร้างแอป

Back4app Build New App

บน Back4app มีสองโซลูชันหลักให้เลือก:

  1. Backend as a Service (BaaS) – โซลูชันแบ็กเอนด์พร้อมใช้
  2. Containers as a Service (CaaS) – แพลตฟอร์มสำหรับจัดการคอนเทนเนอร์ (โดยเฉพาะเว็บแอปพลิเคชัน)

เนื่องจากเรากำลังสร้างแบ็กเอนด์สำหรับแอปบนมือถือ เราจึงเลือก “Backend as a Service”

Back4app BaaS Solution

ตั้งชื่อแอปของคุณให้เหมาะสม เลือก “NoSQL” เป็นฐานข้อมูล และคลิก “Create” นี่เป็นขั้นตอนสำคัญในการดีพลอยแบ็กเอนด์สำหรับแอป Android

Back4app จะใช้เวลาสักครู่ในการเตรียมทุกอย่างที่จำเป็นสำหรับแอปของคุณ ซึ่งรวมถึงฐานข้อมูล การจัดการผู้ใช้ การขยายขนาด การตั้งค่า และอื่น ๆ เมื่อแอปของคุณพร้อมใช้งานแล้ว คุณจะถูกนำไปยังหน้า “Database” ของแอป

Back4app Database View

ออกแบบฐานข้อมูล

ขั้นต่อไป เราจะออกแบบฐานข้อมูล ซึ่งเป็นขั้นตอนที่จำเป็นในการพัฒนาแบ็กเอนด์สำหรับแอป Android

เนื่องจากเราจะสร้างแอปจดบันทึกง่าย ๆ เราจึงต้องการเพียง Class เดียว ให้คลิก “Create a class” ตั้งชื่อว่า Note เปิดสิทธิ์ “Public Read and Write” แล้วคลิก “Create class & add columns”

Back4app Create Database Class

หลังจากนั้น ให้คุณเพิ่ม 3 ฟิลด์ต่อไปนี้ลงในคลาสที่เพิ่งสร้าง:

+-------------+-------------+--------------------+----------+
| Data type   | Name        | Default value      | Required |
+-------------+-------------+--------------------+----------+
| String      | icon        | <leave blank>      | yes      |
+-------------+-------------+--------------------+----------+
| String      | title       | <leave blank>      | yes      |
+-------------+-------------+--------------------+----------+
| String      | content     | <leave blank>      | yes      |
+-------------+-------------+--------------------+----------+

สุดท้าย คลิกปุ่ม “Add row” แล้วใส่ข้อมูลตัวอย่างในฐานข้อมูลของคุณ ถ้าคุณนึกไม่ออกว่าจะใส่อะไร ก็สามารถนำเข้าไฟล์ data dump นี้ได้

Back4app Populate Database

เยี่ยม เท่านี้ส่วนแบ็กเอนด์ของเราก็พร้อมแล้ว

เขียนโค้ดฝั่ง Frontend

ในส่วนนี้ของบทความ เราจะสร้าง Android แอปตั้งแต่เริ่มต้น ตั้งค่า ViewModel ออกแบบ UI ติดตั้งและตั้งค่า Parse SDK และสุดท้ายจะดึงข้อมูลจากฐานข้อมูลเรียลไทม์บน Back4app

สร้างโปรเจกต์

ตามที่กล่าวไว้ในหัวข้อก่อนหน้า คุณต้องติดตั้ง Android Studio เพื่อทำตามขั้นตอนต่อไป ถ้าคุณยังไม่ได้ติดตั้ง ให้ ดาวน์โหลด ได้เลย

เริ่มจากเปิด Android Studio และคลิกปุ่ม “New Project”

Android Studio Index

จากนั้นให้เลือก “Empty Activity” เป็นโครงโปรเจกต์ แล้วคลิก “Next”

เพื่อสร้างโปรเจกต์ คุณต้องตั้งชื่อโปรเจกต์และชื่อแพ็กเกจ แนะนำให้ตั้งชื่อโปรเจกต์เป็น AndroidApp และใช้รูปแบบ reverse domain name เป็นชื่อแพ็กเกจ

หลังจากที่คุณตั้งค่าทุกอย่างเรียบร้อยแล้ว ให้คลิก “Finish” เพื่อสร้างโปรเจกต์

Android Studio Project Settings

Android Studio จะใช้เวลาประมาณสองนาทีในการเตรียมไฟล์และติดตั้ง Gradle รวมถึงการติดตั้งไลบรารีต่าง ๆ

เมื่อโปรเจกต์พร้อมแล้ว ให้คุณเปิดไฟล์ MainActivity.kt และแทนที่เนื้อหาทั้งหมดด้วยโค้ดด้านล่าง:

// app/src/main/java.<your_package_name>/MainActivity.kt

package <your.package.name>

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.sp

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MaterialTheme {
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colorScheme.background,
                ) {
                    Text(text = "Back4app rocks!", fontSize = 18.sp)
                }
            }
        }
    }
}

อย่าลืมแทนที่ <your_package_name> ด้วยชื่อแพ็กเกจจริง ๆ ของคุณ

สุดท้าย ลองรันแอปด้วยการคลิกปุ่ม play สีเขียว หรือกด Shift + F10 ถ้าทุกอย่างถูกต้อง เครื่องจำลอง (emulator) จะรันขึ้นมาและแสดงข้อความ “Back4app rocks” บนหน้าจอ

Android First App

ViewModel

เพื่อจัดการ State แบบ Global ของแอป เราจะใช้ ViewModel แต่ก่อนอื่นเราต้องสร้างคลาส Note สำหรับเก็บข้อมูลที่มีโครงสร้างเหมือนกับในฐานข้อมูลของ Back4app

ให้สร้างไฟล์ดาต้าคลาสชื่อว่า Note:

// app/src/main/java.<your_package_name>/Note.kt

package <your.package.name>

data class Note(
    var objectId: String? = null,
    var icon: String,
    var title: String,
    var content: String,
) {
    companion object {
        fun generateObjectId(): String {
            val chars = ('a'..'z') + ('A'..'Z') + ('0'..'9')
            return (1..10).map { chars.random() }.joinToString("")
        }
    }
}
  1. เราได้กำหนดเมธอด generateObjectId() ไว้เพื่อสร้าง Object ID แบบ Parse
  2. ฟิลด์ objectId ถูกกำหนดให้เป็น null ได้ (nullable) เพราะถ้า objectId == null แสดงว่ายังไม่ได้ถูกบันทึกลงในฐานข้อมูล

หลังจากนั้น สร้างคลาส AppViewModel โดยใส่โค้ดดังนี้:

// app/src/main/java.<your_package_name>/AppViewModel.kt

package <your.package.name>

import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel

class AppViewModel : ViewModel() {
    val notes: MutableState<Map<String, Note>> = mutableStateOf(mapOf(
        "7IggsqFAKt" to Note(
            objectId = "7IggsqFAKt",
            icon = "\uD83D\uDE80",
            title = "Deploy app",
            content = "Go ahead and deploy your backend to Back4app.",
        ),
        "eFRNm0hTat" to Note(
            objectId = "eFRNm0hTat",
            icon = "\uD83C\uDFA8",
            title = "Design website",
            content = "Design the website for the conference.",
        ),
        "uC7hTQmG5F" to Note(
            objectId = "uC7hTQmG5F",
            icon = "\uD83D\uDC42",
            title = "Attend meeting",
            content = "Attend meeting with the team to discuss the conference.",
        ),
    ))

    companion object {
        @Volatile
        private var instance: AppViewModel? = null

        fun getInstance(): AppViewModel {
            return instance ?: synchronized(this) {
                instance ?: AppViewModel().also { instance = it }
            }
        }
    }
}
  1. เราใช้ MutableState จาก Compose เพื่อให้ UI อัปเดตเมื่อข้อมูลเปลี่ยน
  2. MutableState เก็บ Map ที่แมปจาก objectId ไปเป็น Note พร้อมใส่ข้อมูลตัวอย่างไว้
  3. เพื่อให้มั่นใจว่าจะมีแค่ AppViewModel อินสแตนซ์เดียว เราใช้ Singleton Pattern

เราสามารถเข้าถึง AppViewModel ได้ใน Activity ต่าง ๆ ผ่าน AppViewModel.getInstance()

Main Activity

ต่อมา ให้แก้ไข MainActivity.kt ด้วยโค้ดต่อไปนี้เพื่อแสดงโน้ต:

// app/src/main/java.<your_package_name>/MainActivity.kt
package <your.package.name>
import android.content.Context
import android.content.Intent
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.viewModels
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material3.ExtendedFloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
class MainActivity : ComponentActivity() {
    private val viewModel = AppViewModel.getInstance()
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val context = this as Context
        setContent {
            MainActivityContent(
                viewModel = viewModel,
                onNoteListItemClick = {
                    // TODO: Open note edit form
                },
                onNoteAddClick = {
                    // TODO: Open note create form
                },
            )
        }
    }
}
@Composable
fun MainActivityContent(
    viewModel: AppViewModel,
    onNoteListItemClick: (note: Note) -> Unit,
    onNoteAddClick: () -> Unit,
) {
    val notes = viewModel.notes.value
    MaterialTheme {
        Scaffold(
            topBar = { TopAppBar(title = { Text("My Notes") }) },
            floatingActionButton = {
                ExtendedFloatingActionButton(
                    onClick = { onNoteAddClick() },
                    icon = { Icon(Icons.Filled.Add, contentDescription = "Add") },
                    text = { Text("Add") }
                )
            },
        ) { contentPadding ->
            Box(modifier = Modifier.padding(contentPadding)) {
                NoteList(notes, onNoteListItemClick = { onNoteListItemClick(it) })
            }
        }
    }
}
@Composable
fun NoteListItem(note: Note, onNoteListItemClick: (note: Note) -> Unit) {
    Row(
        modifier = Modifier
            .fillMaxWidth()
            .clickable(onClick = { onNoteListItemClick(note) })
            .padding(16.dp),
        verticalAlignment = Alignment.CenterVertically,
    ) {
        Text(text = note.icon, fontSize = 32.sp, modifier = Modifier.size(48.dp))
        Spacer(modifier = Modifier.width(8.dp))
        Column {
            Text(text = note.title, fontSize = 18.sp)
            Spacer(modifier = Modifier.height(4.dp))
            Text(
                text = note.content,
                fontSize = 14.sp,
                maxLines = 1,
                overflow = TextOverflow.Ellipsis,
            )
        }
    }
}
@Composable
fun NoteList(notes: Map, onNoteListItemClick: (note: Note) -> Unit) {
    LazyColumn {
        items(notes.entries.toList()) { (_, note) ->
            NoteListItem(note = note, onNoteListItemClick = onNoteListItemClick)
        }
    }
}
  1. notes ถูกนำมาจาก AppViewModel
  2. เราไม่กำหนด event คลิกไว้ใน Composable โดยตรง แต่ส่งผ่านเป็น argument แทน
  3. เราใช้ Compose ในการออกแบบ UI โดยใช้ MaterialTheme ร่วมกับเลย์เอาต์ Scaffold เพื่อให้ UI ดูสวยงามขึ้น

ถ้าคุณรันแอปตอนนี้ คุณจะเห็นรายการโน้ตที่เรากำหนดแบบฮาร์ดโค้ดไว้แสดงบนหน้าจอ

Back4app Notes App List

Note Form Activity

ในส่วนนี้ เราจะสร้าง Activity ใหม่เพื่อให้ผู้ใช้สามารถเพิ่มและแก้ไขโน้ตได้

เริ่มจากสร้างคลาสชื่อ FormActivity และใส่โค้ดต่อไปนี้:

// app/src/main/java.<your_package_name>/FormActivity.kt

package <your.package.name>

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material3.Button
import androidx.compose.material3.ExtendedFloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.unit.dp

class FormActivity : ComponentActivity() {
    private val viewModel = AppViewModel.getInstance()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val objectId = intent.getStringExtra("objectId")
        val note = if (objectId !== null) viewModel.notes.value[objectId] else null

        setContent {
            FormActivityContent(
                note,
                onNoteAddClick = { icon: String, title: String, content: String ->
                    if (note !== null) return@FormActivityContent
                    val newNote = Note(
                        icon = icon,
                        title = title,
                        content = content,
                    )
                    newNote.objectId = Note.generateObjectId()
                    viewModel.notes.value += (newNote.objectId!! to newNote)
                    finish()
                },
                onNoteSaveClick = { icon: String, title: String, content: String ->
                    if (note === null) return@FormActivityContent
                    val updatedNote = note.copy()
                    updatedNote.icon = icon
                    updatedNote.title = title
                    updatedNote.content = content
                    viewModel.notes.value += (updatedNote.objectId!! to updatedNote)
                    finish()
                },
                onNoteDeleteClick = {
                    if (note === null) return@FormActivityContent
                    viewModel.notes.value = viewModel.notes.value.filter {
                        it.value.objectId != note.objectId
                    }
                    finish()
                },
            )
        }
    }
}

@Composable
fun FormActivityContent(
    note: Note?,
    onNoteAddClick: (icon: String, title: String, content: String) -> Unit,
    onNoteSaveClick: (icon: String, title: String, content: String) -> Unit,
    onNoteDeleteClick: () -> Unit,
) {
    MaterialTheme {
        Scaffold(
            topBar = {
                TopAppBar(title = { Text(note?.let { "Edit Note" } ?: ("Add Note")) })
            },
            floatingActionButton = {
                if (note !== null) {
                    ExtendedFloatingActionButton(
                        onClick = { onNoteDeleteClick() },
                        icon = { Icon(Icons.Filled.Delete, "Delete") },
                        text = { Text("Delete") },
                    )
                }
            },
        ) { contentPadding ->
            Box(modifier = Modifier.padding(contentPadding)) {
                NoteForm(note = note, onNoteSave = { icon, title, content ->
                    if (note === null) {
                        onNoteAddClick(icon, title, content)
                    } else {
                        onNoteSaveClick(icon, title, content)
                    }
                })
            }
        }
    }
}

@Composable
fun NoteForm(
    note: Note?,
    onNoteSave: (icon: String, title: String, content: String) -> Unit
) {
    var icon by remember { mutableStateOf(TextFieldValue(note?.icon ?: "")) }
    var title by remember { mutableStateOf(TextFieldValue(note?.title ?: "")) }
    var content by remember { mutableStateOf(TextFieldValue(note?.content ?: "")) }
    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(16.dp)
    ) {
        TextField(
            label = { Text(text = "Icon") },
            value = icon,
            onValueChange = { icon = it },
            modifier = Modifier.fillMaxWidth()
        )
        Spacer(modifier = Modifier.height(16.dp))
        TextField(
            label = { Text(text = "Title") },
            value = title,
            onValueChange = { title = it },
            modifier = Modifier.fillMaxWidth()
        )
        Spacer(modifier = Modifier.height(16.dp))
        TextField(
            label = { Text(text = "Content") },
            value = content,
            onValueChange = { content = it },
            modifier = Modifier.fillMaxWidth()
        )
        Spacer(modifier = Modifier.height(16.dp))
        Button(
            onClick = { onNoteSave(icon.text, title.text, content.text) },
            Modifier.fillMaxWidth()
        ) {
            Text(text = "Save")
        }
    }
}
  1. ฟอร์มนี้ใช้ได้ทั้งการเพิ่มและแก้ไขโน้ต โดยดูจาก objectId ถ้า objectId == null แสดงว่าเป็นการเพิ่มโน้ต แต่ถ้ามี objectId แสดงว่าเป็นการแก้ไข
  2. ถ้า objectId !== null เราจะดึง note จาก State มาตั้งค่าเริ่มต้นในฟอร์ม
  3. เพื่อให้ทำงานได้กับ MutableState เราต้องปฏิบัติต่อข้อมูลใน State เหมือนเป็น Immutable จึงมีการ copy() แทนการแก้ไขโดยตรง
  4. เราใช้ mutableStateOf() เพื่อจัดการ State ของฟอร์มใน Compose

ต่อไป ให้ลงทะเบียน FormActivity ในส่วน application ของไฟล์ AndroidManifest.xml:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <application>
        <!-- ... -->
        <activity android:name=".FormActivity"/>
    </application>

</manifest>

จากนั้น ไปแก้ไขเมธอด onNoteListItemClick และ onNoteAddClick ใน MainActivity.kt เพื่อเรียก Intent ในการเปิด FormActivity:

// app/src/main/java.<your_package_name>/MainActivity.kt

class MainActivity : ComponentActivity() {
    // ...

    override fun onCreate(savedInstanceState: Bundle?) {
        // ...

        setContent {
            MainActivityContent(
                viewModel = viewModel,
                onNoteListItemClick = {
                    // triggers the edit action, since `objectId` is provided
                    val intent = Intent(context, FormActivity::class.java)
                    intent.putExtra("objectId", it.objectId)
                    context.startActivity(intent)
                },
                onNoteAddClick = {
                    // triggers the add action, since no `objectId` is present
                    val intent = Intent(context, FormActivity::class.java)
                    context.startActivity(intent)
                },
            )
        }
    }
}

อย่าลืมนำเข้า Intent ด้านบนไฟล์ด้วย:

import android.content.Intent

ถ้าคุณรันแอปตอนนี้ คุณจะได้แอปที่สามารถเพิ่ม แก้ไข และลบโน้ตได้ แต่ถ้าปิดและเปิดแอปใหม่ ข้อมูลจะกลับไปเป็นค่าที่ฮาร์ดโค้ดไว้

ในหัวข้อต่อไป เราจะเชื่อมแอปกับแบ็กเอนด์บน Back4app เพื่อให้ข้อมูลถูกเก็บและซิงก์ระหว่างอุปกรณ์หลายเครื่อง

Android Notes App Edit Preview

ติดตั้ง Parse SDK

อย่างที่หลายคนทราบ Back4app พัฒนาบน Parse platform ดังนั้นหากเราต้องการติดต่อกับฐานข้อมูลหรือบริการของ Back4app เราต้องติดตั้ง Parse SDK

เริ่มจากเพิ่ม JitPack repository ไปที่ไฟล์ settings.gradle:

// settings.gradle

pluginManagement {
    repositories {
        // ...
        maven { url "https://jitpack.io" }
    }
}
dependencyResolutionManagement {
    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
    repositories {
        // ...
        maven { url "https://jitpack.io" }
    }
}

จากนั้นเพิ่ม Android Parse SDK ลงใน build.gradle ของโมดูลแอป:

// app/build.gradle

dependencies {
    // ...
    implementation "com.github.parse-community.Parse-SDK-Android:parse:4.2.0"
}

จากนั้นซิงค์ Gradle และตรวจสอบว่าไม่มีเออเรอร์

ตั้งค่า Parse SDK

เพื่อเชื่อมต่อกับแบ็กเอนด์ Back4app คุณต้องระบุ Application ID และ Client Key ให้กับ Parse SDK ในการรับค่าดังกล่าว ให้เข้าแอปของคุณใน Back4app แล้วเลือกเมนู “App Settings > Security & Keys”

จากนั้นนำค่าที่ได้มาใส่ในไฟล์ strings.xml แบบนี้:

<!-- app/src/main/res/values/strings.xml -->

<resources>
    <string name="app_name">back4app-android-app</string>
    <string name="back4app_server_url">https://parseapi.back4app.com/</string>
    <string name="back4app_app_id">your_parse_app_id</string>
    <string name="back4app_client_key">your_parse_client_key</string>
</resources>

อย่าลืมแทนที่ your_parse_app_id และ your_parse_client_key ด้วยค่าจริงของคุณ

จากนั้นแก้ไขไฟล์ AndroidManifest.xml เพื่ออนุญาตการเข้าถึงอินเทอร์เน็ตและเพิ่ม Metadata สำหรับ Parse:

<!-- app/src/main/AndroidManifest.xml -->

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    <uses-permission android:name="android.permission.INTERNET" />

    <application>
        <!-- ... -->

        <meta-data
            android:name="com.parse.SERVER_URL"
            android:value="@string/back4app_server_url" />
        <meta-data
            android:name="com.parse.APPLICATION_ID"
            android:value="@string/back4app_app_id" />
        <meta-data
            android:name="com.parse.CLIENT_KEY"
            android:value="@string/back4app_client_key" />
    </application>

</manifest>

สุดท้าย เราต้อง Initialize Parse ก่อนที่ Activity ใด ๆ จะถูกเรียกใช้ เราจึงสร้างคลาสใหม่ชื่อ App ซึ่งสืบทอดจากคลาส Application

สร้างไฟล์ App.kt และกำหนดค่า Parse ดังนี้:

// app/src/main/java/<your_package_name>/App.kt

package <your.package.name>

import android.app.Application
import com.parse.Parse

class App : Application() {
    override fun onCreate() {
        super.onCreate()

        Parse.initialize(
            Parse.Configuration.Builder(this)
                .applicationId(getString(R.string.back4app_app_id))
                .clientKey(getString(R.string.back4app_client_key))
                .server(getString(R.string.back4app_server_url))
                .build()
        )
    }
}

จากนั้นลงทะเบียน App ใน AndroidManifest.xml:

<!-- app/src/main/AndroidManifest.xml -->

<application
    android:name=".App"
    ...
>
    <!-- ... -->
</application>

ลองรันแอปอีกครั้ง และสังเกตใน Logcat ว่าไม่มีเออเรอร์ใด ๆ หากไม่พบข้อผิดพลาด แสดงว่าแอปได้เชื่อมต่อกับ Back4app เรียบร้อยแล้ว

โหลดโน้ตจาก Back4app

ขั้นตอนสุดท้ายก่อนที่แอปของเราจะสมบูรณ์คือการโหลดโน้ตจากฐานข้อมูล Back4app และศึกษาการใช้งาน Parse ในทางปฏิบัติ

เริ่มจากไปที่คลาส AppViewModel ลบข้อมูลตัวอย่างที่ใส่ใน map ออก แล้วเพิ่ม init block ดังนี้:

// app/src/main/java/<your_package_name>/AppViewModel.kt

package <your.package.name>

import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel

class AppViewModel : ViewModel() {
    val notes: MutableState<Map<String, Note>> = mutableStateOf(mapOf())

    init {
        val query = com.parse.ParseQuery.getQuery<com.parse.ParseObject>("Note")
        query.orderByDescending("createdAt")
        query.findInBackground { notes, e ->
            if (e == null) {
                for (parseNote in notes) {
                    val note = Note(
                        objectId = parseNote.objectId,
                        icon = parseNote.getString("icon")!!,
                        title = parseNote.getString("title")!!,
                        content = parseNote.getString("content")!!,
                    )
                    this.notes.value += (note.objectId!! to note)
                }
            } else {
                println("Error: ${e.message}")
            }
        }
    }

    companion object {
        // ...
    }
}

โค้ดดังกล่าวจะใช้ Parse SDK ดึงโน้ตจากฐานข้อมูล แปลงเป็น Note และนำไปใส่ใน Map ของ State

จากนั้น เพิ่มเมธอดสามตัวต่อไปนี้ใน Note:

// app/src/main/java/<your_package_name>/Note.kt

package <your.package.name>

import com.parse.ParseObject
import com.parse.ParseQuery

data class Note(
    var objectId: String? = null,
    var icon: String,
    var title: String,
    var content: String,
) {
    fun addToParse(callback: (objectId: String) -> Unit) {
        if (objectId !== null) throw Exception("Note is already saved to Parse!")

        val parseNote = ParseObject("Note")
        parseNote.put("icon", icon)
        parseNote.put("title", title)
        parseNote.put("content", content)

        parseNote.saveInBackground {
            if (it !== null) throw Exception("Error: ${it.message}")
            objectId = parseNote.objectId
            callback(parseNote.objectId)
        }
    }

    fun updateToParse(callback: (objectId: String) -> Unit) {
        if (objectId === null) throw Exception("Note hasn't been saved to Parse yet!")

        val query = ParseQuery.getQuery<ParseObject>("Note")
        val parseNote = query.get(objectId)
        parseNote.put("icon", icon)
        parseNote.put("title", title)
        parseNote.put("content", content)

        parseNote.saveInBackground {
            if (it !== null) throw Exception("Error: ${it.message}")
            callback(parseNote.objectId)
        }
    }

    fun deleteFromParse(callback: () -> Unit) {
        if (objectId === null) throw Exception("Note hasn't been saved to Parse yet!")

        val query = ParseQuery.getQuery<ParseObject>("Note")
        val parseNote = query.get(objectId)
        parseNote.deleteInBackground {
            if (it !== null) throw Exception("Error: ${it.message}")
            callback()
        }
    }
}
  1. addToParse() ใช้เพื่อเพิ่มโน้ตใหม่ลง Parse Server พร้อมข้อมูลที่เกี่ยวข้อง ถ้าสำเร็จจะอัปเดต objectId และแจ้ง callback เมื่อเสร็จ
  2. updateToParse() ใช้เพื่ออัปเดตข้อมูลโน้ตบน Parse Server ถ้าสำเร็จจะแจ้ง callback เมื่อเสร็จ
  3. deleteFromParse() ใช้เพื่อลบโน้ตออกจาก Parse Server ถ้าสำเร็จจะแจ้ง callback เช่นกัน

สุดท้าย ให้แก้ไขเมธอดคลิกใน FormActivity เพื่อเรียกเมธอดใหม่ที่เราสร้างไว้:

// app/src/main/java/<your_package_name>/NoteFormActivity.kt

class FormActivity : ComponentActivity() {
    // ...

    override fun onCreate(savedInstanceState: Bundle?) {
        // ...

        setContent {
            FormActivityContent(
                note,
                onNoteAddClick = { icon: String, title: String, content: String ->
                    if (note !== null) return@FormActivityContent
                    val newNote = Note(
                        icon = icon,
                        title = title,
                        content = content,
                    )
                    newNote.addToParse {
                        viewModel.notes.value += (it to newNote)
                        finish()
                    }
                },
                onNoteSaveClick = { icon: String, title: String, content: String ->
                    if (note === null) return@FormActivityContent
                    val updatedNote = note.copy()
                    updatedNote.icon = icon
                    updatedNote.title = title
                    updatedNote.content = content
                    updatedNote.updateToParse {
                        viewModel.notes.value += (it to updatedNote)
                        finish()
                    }
                },
                onNoteDeleteClick = {
                    if (note === null) return@FormActivityContent
                    viewModel.notes.value = viewModel.notes.value.filter {
                        it.value.objectId != note.objectId
                    }
                    note.deleteFromParse {
                        finish()
                    }
                },
            )
        }
    }
}

เรียบร้อย!

ตอนนี้แอปของเราทำงานได้สมบูรณ์ และซิงก์กับแบ็กเอนด์บน Back4app แล้ว ลองแก้ไขข้อมูลในแอป และตรวจสอบในหน้า Database ของ Back4app ดูว่าข้อมูลเปลี่ยนตามหรือไม่

สรุป

ในบทความนี้ เราได้สร้างแบ็กเอนด์สำหรับแอปมือถือและเชื่อมต่อกับแอป Android โดยใช้บริการ BaaS จาก Back4app และใช้ Kotlin ร่วมกับ Jetpack Compose ฝั่ง Frontend

ตอนนี้คุณน่าจะเข้าใจหลักการทำงานของแบ็กเอนด์สำหรับแอปมือถือ และสามารถสร้างแบ็กเอนด์ของคุณเองได้ สามารถดูซอร์สโค้ดฉบับสมบูรณ์ได้ที่ back4app-android-app repo


Leave a reply

Your email address will not be published.