Modernized Android architecture - JetPack Compose Part1

eddiej

Eddie Eddie

Posted on January 15, 2021

Modernized Android architecture - JetPack Compose Part1

Cover Photo by PixaBay from Pexels
จากที่เกริ่นไว้ใน Part แรกว่า JetPack Compose เป็นเรื่องที่ตั้งใจจะเขียนไว้ตอนแรกแต่เนื่องจากว่าใน Sample project มีจุดน่าสนใจหลายอย่าง เลยต้องแตกออกมาเป็นอีกหัวข้อนึง เรื่อง JetPack Compose ที่จะเขียนในวันนี้ อาจจะไม่ครอบคลุมเนื้อหาทั้งหมด เพราะตัว JetPack Compose เองก็มีเนื้อหาเยอะมาก รวมถึง API เองก็ยังไม่ได้อยู่ในสถานะ Stable เลยคิดว่าน่าจะมี Breaking change พอสมควร เนื้อหาที่จะเขียนในนี้จึงขอ Scope เนื้อหาเอาไว้คร่าวๆละ Sample App ละกัน

เริ่มต้นสร้าง Project

  • Jetpack Compose นั้น สามารถใช้งานได้บน Android Studio 4.2 (Preview) ขึ้นไปเท่านั้น เพราะฉะนั้นก่อนเริ่มสร้าง Project เราควรมั่นใจก่อนว่า Version ของ Android Studio เราถูกต้อง
  • เมื่อกด New Project เราสามารถเลือก Empty Compose Activity ได้เลย Alt Text ใน app/build.gradle จะเห็นว่าเรามี Compose lib อยู่ข้างในด้วย
    implementation "androidx.compose.ui:ui:$compose_version"
    implementation "androidx.ui:ui-tooling:$compose_version"
    implementation "androidx.compose.material:material:$compose_version"
Enter fullscreen mode Exit fullscreen mode
  • androidx.compose.ui:uiและ androidx.ui:ui-tooling คือ JetPack Compose ที่เราจะใช้
  • androidx.compose.material คือ Google Material Design system Library ที่ถูกสร้างครอบ JetPack Compose อีกที

หน้าตา Code หลังจากที่ New Project สำเร็จ
Alt Text

จุดที่เราสามารถสังเกตได้ชัดๆคือใน Activity ของเรานั้นไม่มีการ เรียก setContentView(R.layout.activity_main) อีกต่อไปแล้ว กลับกัน กับเรียก setContent แทนที่เป็น Extension Function ที่รับ lamda ที่มี @Composable ประกาศไว้ข้างหน้า
Alt Text
สิ่งที่ set เข้าไปใน lamda ของ setContent ก็คือ MyApplicationTheme ซึ้งถ้าเราไปดู เจ้าตัว MyApplicationTheme นั้นก็เป็น Function ที่มี @Composable ไว้เช่นกัน
Alt Text

Composable

  • @Composable คือ Annotation พื้นฐานสำหรับ JetPack Compose ที่ไว้ประกาศไว้หน้า Function เพื่อเป็นการบอกว่า Function นี้จะเป็น Block code ที่ไว้สำหรับ Built ด้วย Compose พูดง่ายๆคือเป็น UI Function ละกัน

Concept ของ UI เปลี่ยนไป

Alt Text

ใน JetPack Compose Concept ของ UI นั้นจะไม่เหมือนกับ Android View System ดั้งเดิม คือ ทุกๆ Component นั้นเป็น Composable function ไม่ใช่ Object ที่สืบทอดมาจาก View การเปลี่ยนแปลงการแสดงผลของ UI นั้นจะเกิดขึ้นเมื่อ Function นั้นมีการ "Re-composition" เกิดขึ้น

Alt Text

ถึงตรงนี้หลายคนอาจจะสงสัยว่าเมื่อไหร่ที่ Function จะทำการ Re-composition? โดยปกติแล้ว Composable Function จะทำการ Re-composition ก็ต่อเมื่อ State ของตัวมันเองเปลี่ยนไป โดยเรื่อง State จะกล่าวถัดไป

Unidirectional Data Flow

Unidirectional Data Flow หรือ Single Directional Data Flow
ย้อนกลับไปตอนที่เรายังเขียน Code เป็นแบบ MVC Pattern

  • ใน MVC เมื่อมี Action บางอย่างเกิดขึ้นเช่น User เปิดแอพ, User คลิกปุ่ม ฯลฯ Action จะถูกส่งไปให้ Controller เป็นคนจัดการ
  • Controller จะทำการจัดการกับ Action นั้นตามแต่ว่า Action นั้นคืออะไร เช่น User เปิดแอพ -> Controller ดึงข้อมูลจาก network เมื่อ Controller ได้ข้อมูลมาแล้ว นำข้อมูลไป parse เป็น model แล้วแสดงผลที่ View ให้ User เห็น

Alt Text
Flow การทำงานนี้คอนข้างเรียบง่ายถ้าเป็นแอพขนาดเล็ก แต่ถ้าหากว่าแอพเราใหญ่ขึ้น Flow การทำงานก็จะซับซ้อนขึ้น เช่น เมื่อ User เปิดแอพ เราต้อง Fetch ข้อมูลจาก API มาจาก n Endpoints และแสดงผลใน View n Views

Alt Text

ขอบคุณทีม Facebook Engineer ที่ได้ คิดใหม่และกลายเป็นที่มาของ Flux, Redux ฯลฯ ในปัจจุบันฝั่ง web App Development

Alt Text
Image from Hacker Way: Rethinking Web App Development at Facebook

ใครสนใจสามารถอ่านได้ที่ Flux documentation
สาเหตุที่จำเป็นต้องเกริ่นถึงเรื่องนี้เพราะตัว JetPack Compose เองมีสิ่งนึงที่ Build-in เข้ามาใน Library เลย นั่นก็คือ State

State

State เป็นเสมือนค่าสถานะของ Application ที่เปลี่ยนแปลงได้ตามเวลา ถ้าเราลองมองภาพกว้างๆ จะเห็นว่า Android App ของเราเองก็ทำการ แสดง State ต่างๆ ให้ User เห็นอยู่แล้ว เช่น:

  • State ที่ทำการแสดง Snackbar ให้ User เห็นเมื่อไม่มี Internet
  • State ที่ทำการแสดง Blog Post รวมถึง Comment ที่เกี่ยวข้อง
  • State ที่ทำการแสดง Ripple animation บนปุ่มเมื่อ User กด จะเห็นว่าแอพเราเองก็แสดง State ต่างๆ ให้ user เห็นผ่านทาง View อยู่แล้ว

แล้วเมื่อไหร่ที่ State ของหน้าแอพเรามันเปลี่ยนแปลงไปล่ะ?

โดยปกติ State ของแอพเรามักจะเปลี่ยนแปลงเนื่องจากมี Action หรือ Event บางอย่างมากระทำให้ State เปลี่ยนแปลงไป

Alt Text

  • Event ในที่นี้อาจจะเป็น Action จากการกดปุ่ม หรือข้อมูลที่เกิดจากการ Fetch มาจาก Backend
  • Update state class หรือ function ที่ทำหน้าที่เปลี่ยนแปลง State
  • Display state UI ที่ทำหน้าที่ Display ให้ User เห็น ตามแต่ละ State ที่เปลี่ยนแปลงไป

พูดอีกอย่างก็คือ เราจะแสดง UI ตาม State ที่เปลี่ยนแปลงไป ในขณะเดียวกัน State ก็อาจจะเปลี่ยนแปลงได้จากการที่ได้รับ Event บางอย่างมาจาก UI
Alt Text

ยกตัวอย่าง:

class MainActivity : AppCompatActivity() {

   private lateinit var binding: MainActivityBinding
   var name = ""

   override fun onCreate(savedInstanceState: Bundle?) {
       /* ... */
       binding.textUpdateButton.setOnClickListener {
           name = randomDisplayText()
           updateHello()
       }
   }

   private fun randomDisplayText(): String {
    return when((Math.random() * (4 - 1) + 1).toInt()) {
                1 -> "Android"
                2 -> "JetPack"
                3 -> "Hilt"
                4 -> "People"
                else -> "World"
       }
   }

   private fun updateHello() {
       binding.helloText.text = "Hello, $name"
   }
}
Enter fullscreen mode Exit fullscreen mode

Code ข้อบนชุดนี้คือ เราจะทำการ Update ค่าที่นำมาแสดงของ helloText เมื่อ User มีการกดปุ่มเพื่อ random โดยตัวอย่างโค๊ตข้างบนสามารถทำงานได้ถูกต้อง 100% ทีนี้ปัญหาคืออะไร? 🤔

เราลองนึกถึงแอพที่ Scale ใหญ่ขึ้น หน้าจอมีความซับซ้อนมากขึ้น การเขียนโค๊ตลักษณะนี้อาจจะก่อให้เกิดปัญหาหลายอย่างเช่นการ Test เนื่องจากว่าค่าที่นำมาแสดงผลอยู่ภายใน View (State ของ helloText อยู่ใน View) การ test นั้นเป็นเรื่องค่อนข้างยาก หรือแอพมี Events มากขึ้นแล้วเราต้อง Update UI หรือ ค่า State ต่างๆ มันเป็นเรื่องง่ายมากที่เราจะลืมมาแก้ไข/เปลี่ยนแปลง Code ชุดนี้ รวมขึ้น Code Complexity ที่จะเกิดขึ้นในอนาคต

ใช้ ViewModel และ LiveData ช่วยในการจัดการ State

class MainViewModel: ViewModel() {

   // LiveData holds state which is observed by the UI
   // (state flows down from ViewModel)
   private val _name = MutableLiveData("")
   val name: LiveData<String> = _name

   // onNameChanged is an event we're defining that the UI can invoke
   // (events flow up from UI)
   fun onNameChanged() {
       _name.value = randomDisplayText()
   }

   private fun randomDisplayText(): String {
       return when((Math.random() * (4 - 1) + 1).toInt()) {
        1 -> "Android"
        2 -> "JetPack"
        3 -> "Hilt"
        4 -> "People"
        else -> "World"
       }
   }
}

class MainActivity : AppCompatActivity() {
   val helloViewModel by viewModels<MainViewModel>()

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

       binding.textUpdateButton.setOnClickListener {
           helloViewModel.onNameChanged() 
       }

       helloViewModel.name.observe(this) { name ->
           binding.helloText.text = "Hello, $name"
       }
   }
}
Enter fullscreen mode Exit fullscreen mode

จะเห็นว่าสิ่งที่เกิดขึ้นใน Code ชุดข้างบนคือ เราให้ การกดปุ่มจะไป update ค่าของ LiveData แทนที่จะ Update UI ตรงๆ ทำการ observe LiveData ตัวนั้นเพื่อทำการเปลี่ยนแปลงค่าใน helloText

  • Event/Action ในที่นี้ Event หรือ Action คือ การที่ User กดปุ่ม
  • State Updating เกิดขึ้นที่ onNameChanged เมื่อค่าของ LiveData เปลี่ยนแปลงไป เรียกว่าเราได้ทำการ Hoist State เอาไว้ที่ LiveData ของ ViewModel ของเรา (State Hoisting)
  • State Displaying เกิดขึ้น Observer เมื่อทำการเปลี่ยนแปลงค่าของ helloText

ลอง Implement Code แบบเดียวกันด้วย JetPack Compose

ผมจะลองสร้าง Sample Code ขึ้นมาให้หน้าตาและการทำงานมีลักษณะดังนี้

Alt Text
โดยทุกๆครั้งที่ User กดปุ่ม เราจะทำการ random ข้อความขึ้นมาใหม่และแสดงที่ Text ตัวแอพจะแสดง Icon อยู่ทางขวาจะ random ขึ้นมาเหมือนกันแต่แค่ครั้งเดียวหลังจากที่เปิดแอพใหม่ โดยเราจะมี ViewModel อยู่แล้ว หน้าตาแบบนี้

class MainViewModel: ViewModel() {

    // LiveData holds state which is observed by the UI
    // (state flows down from ViewModel)
    private val _name = MutableLiveData("")
    val name: LiveData<String> = _name

    // onNameChanged is an event we're defining that the UI can invoke
    // (events flow up from UI)
    fun onNameChanged() {
        _name.value = randomDisplayText()
    }
}
Enter fullscreen mode Exit fullscreen mode

พร้อมกับ Function สำหรับ Random Text

    fun randomDisplayText(): String {
        return when((Math.random() * (4 - 1) + 1).toInt()) {
            1 -> "Android"
            2 -> "JetPack"
            3 -> "Hilt"
            4 -> "People"
            else -> "World"
        }
    }
Enter fullscreen mode Exit fullscreen mode

สร้าง UI ด้วย Column และ Row

Column และ Row เป็น Layout พื้นฐานของ Compose โดย

  • Column จะแสดง Child เรียงเป็นแนวตั้ง คล้ายกับ LinearLayout ที่มี orientation="vertical"
  • Row จะแสดง Child เรียงเป็นแนวนอน คล้ายกับ LinearLayout ที่มี orientation="horizontal"

เราจะให้ Text และ Button อยู่ใน Column (กรอบสีแดง)เนื่องจากเรียงจากบนลงล่างและให้ตัว Column และ Icon อยู่ภายใน Row (กรอบสีน้ำเงิน)
Alt Text

สร้าง Composable Function ในที่นี้ให้ชื่อว่า MyScreen

@Composable
fun MyScreen() {

}
Enter fullscreen mode Exit fullscreen mode

เพิ่ม Row โดยกำหนด Modifier ให้ แสดงผลเต็มทางแนวนอน และมี padding เป็น 16dp

@Composable
fun MyScreen() {
+     Row(modifier = Modifier
+         .fillMaxWidth()
+         .padding(16.dp)){
+     }
}
Enter fullscreen mode Exit fullscreen mode

เพิ่ม Column โดยกำหนด weight ให้ Column = 1

@Composable
fun MyScreen() {
    Row(modifier = Modifier
        .fillMaxWidth()
        .padding(16.dp)) {
+         Column(modifier = Modifier.weight(1f)) {

+         }
    }
}
Enter fullscreen mode Exit fullscreen mode

เพิ่ม Icon ในเคสนี้เราต้องการให้ Icon ถูก random ขึ้นมาเฉพาะครั้งแรกที่เปิดแอพเท่านั้น

@Composable
fun MyScreen() {
    Row(modifier = Modifier
        .fillMaxWidth()
        .padding(16.dp)) {
         Column(modifier = Modifier.weight(1f)) {

         }
        + MyRandomIcon(asset = randomIconAsset())
    }
}

+ @Composable
+ fun MyRandomIcon(asset: VectorAsset) = Icon(asset = asset)

+ fun randomIconAsset(): VectorAsset {
+    return when((Math.random() * (14 - 1) + 1).toInt()) {
+                1 -> Icons.Default.Cloud
+                2 -> Icons.Default.CloudQueue
+                3 -> Icons.Default.CloudUpload
+                4 -> Icons.Default.CloudOff
+                5 -> Icons.Default.CloudDone
+                6 -> Icons.Default.CloudDownload
+                7 -> Icons.Default.WbCloudy
+                8 -> Icons.Default.Wifi
+                9 -> Icons.Default.WifiCalling
+                10 -> Icons.Default.WifiLock
+                11 -> Icons.Default.WifiProtectedSetup
+                12 -> Icons.Default.WifiOff
+                13 -> Icons.Default.WifiTethering
+                14 -> Icons.Default.PortableWifiOff
+                else -> Icons.Default.FavoriteBorder
+    }
+ }
Enter fullscreen mode Exit fullscreen mode

เพิ่ม Text และ Button เข้าไปที่ Column

@Composable
fun MyScreen() {
    Row(modifier = Modifier
        .fillMaxWidth()
        .padding(16.dp)) {
           Column(modifier = Modifier.weight(1f)) {
+             Text("")
+             Button(onClick = {}) {
+                Text(text = "Click Me")
+             }
           }
           MyRandomIcon(asset = randomIconAsset())
    }
}
Enter fullscreen mode Exit fullscreen mode

เพิ่ม MyScreen เข้าไปที่ onCreate ของ Activity

override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MyAppTheme {
                Surface(color = MaterialTheme.colors.background) {
                    MyScreen(
                        text = "", {}
                    )
                }
            }
        }
    }
Enter fullscreen mode Exit fullscreen mode

Preview

เราสามารถ Preview หน้าตา UI ที่เราสร้างได้ด้วย @Preview ให้เราทำการสร้าง Function แยก ชื่อว่า PreviewMyScreen เพื่อใช้สำหรับ Preview อย่างเดียว

+ @Preview(showBackground = true)
+ @Composable
+ fun PreviewMyScreen() {
+    ComposeStatePlaygroundTheme {
+        MyScreen()
+    }
+ }
Enter fullscreen mode Exit fullscreen mode

showBackground = true เพื่อแสดง Background ของแอพ
ทำการ Build app 1 ครั้งเพื่อ Preview
หน้าตาที่ได้:
Alt Text
เรายังสามารถทำการกดปุ่ม Interactive เพื่อทำการลองเล่นหน้าตา UI ของเราก่อนที่จะ Run ลงเครื่องจริงๆ ได้ด้วย
Alt Text
Alt Text

เนื่องจากเจ้า JetPack Compose นั้นเป็น Declarative UI โดยเจ้า Declarative UI นั้นตัว Code เองคือตัวอธิบายว่าหน้าตา UI จะเป็นอย่างไร
เมื่อ User กดปุ่ม เราต้องการให้ Text แสดงค่า random Text จาก function ที่เราเตรียมไว้ หรือพูดง่ายๆก็คือ Update State ของ Text นั่นเอง

State และ MutableState

เป็น Build-in Interface ของ JetPack Compose โดยปกติ Composable Function จะทำการ "subscribe" กับ value ของ State<T> เมื่อ value มีการเปลี่ยนแปลง Composable Function จะทำการ Recompose ตัวเองเพื่อ Update UI ใหม่

@Stable
interface State<T> {
    val value: T
}
Enter fullscreen mode Exit fullscreen mode

โดย value สามารถเปลี่ยนแปลงได้ผ่านทางการเรียก MutableState<T> อีกที

@Stable
interface MutableState<T> : State<T> {
    override var value: T
    ....
}
Enter fullscreen mode Exit fullscreen mode

เพิ่ม State ให้ Function MyScreen

@Composable
fun MyScreen() {
+ var text: String by remember { mutableStateOf(randomDisplayText())}
    Row(modifier = Modifier.fillMaxWidth().padding(16.dp)) {
           Column(modifier = Modifier.weight(1f)) {
              Text(text)
              Button(onClick = {
+                  text = randomDisplayText()
             }) {
                 Text(text = "Click Me")
               }
           }
           MyRandomIcon(asset = randomIconAsset())
    }
}
Enter fullscreen mode Exit fullscreen mode

Function mutableStateOf จะทำการ return ค่า MutableState โดยในที่นี้เราจะกำหนดค่าเริ่มต้นให้กับ State เลย ด้วยการเรียก randomDisplayText() และเมื่อมีการกดปุ่มเราจะทำการ update ค่าของ text อีกครั้งด้วยการเรียก text = randomDisplayText()
อีกจุดนึงที่สังเกตเห็นได้คือมีการเรียกใช้ function remember ซึ่งจะขออธิบายในลำดับถัดไป
ลองรันแอพ:

Alt Text

ทุกๆครั้งที่เรากดปุ่ม Text ของเราอัพเดทถูกต้อง แต่จะเห็นว่า Icon ก็โดนเปลี่ยนไปด้วย???? 🤨🤨🤨

อย่างที่บอกไว้ก่อนหน้านั้น เนื่องจากว่า Composable function จะ "subscribe" ตัวเองกับ State เมื่อ State เปลี่ยน (ในที่นี้คือ text) function จะมีการ Recompose เพื่อแสดงผลใหม่ ทำให้ ตัว Function เองโดนเรียกซ้ำทำให้ function randomIconAsset โดนเรียกซ้ำอีกที

ลองใส่ Log ที่ MyScreen()

Log.d("MyComposeApp", "MyScreen is called")
Enter fullscreen mode Exit fullscreen mode

Alt Text
สังเกตุได้ว่าค่าที่ Log ไม่ถูก Print เมื่อค่าที่ Random ได้ยังเป็นค่าเดิม

เอ๊ะแล้วแบบนี้มันแตกต่างกับ randomDisplayText() ยังไง 🤔??

Memory ใน Function

จุดที่แตกต่างสำคัญเลยระหว่าง randomDisplayText randomIconAsset ที่ชัดเจนเลยคือ:

  • randomDisplayText เป็น Initiate value ของ State (เราเรียก randomDisplayText ใน mutableStateOf)
  • mutableStateOf ถูก wrap ด้วย remember Composable function จะมีความสามารถในการจดจำค่าก่อนหน้าว่าเป็นค่าอะไร เมื่อเกิดการ Recomposition ขึ้น ตัว composable function จะนำค่าที่จดจำอยู่มาแสดงแทน ในกรณีของเรา เรามีการจดจำ "State<String>" ซึ่ง value ของ State<String> ก็คือ text นั่นเอง
var text: String by remember { mutableStateOf(randomDisplayText())}
Enter fullscreen mode Exit fullscreen mode

สามารถเขียนในอีกแบบได้เป็น

var text :MutableState<String> = remember { mutableStateOf(randomDisplayText())}
...
+           Text(text = text.value)
            Button(onClick = {
+               text.value = randomDisplayText()
            }) {
                Text(text = "Click Me")
            }
Enter fullscreen mode Exit fullscreen mode
แก้ไม่ให้ Icon Update ทุกครั้ง

วิธีการแก้ในเคสนี้เราสามารถทำได้สองแบบ
แบบที่ 1:
เราสามารถใช้เจ้า remember กับ randomIconAsset ได้ เพื่อให้ค่าที่ถูก random มาครั้งแรก ถูกจดจำไว้:

val asset by remember { randomIconAsset() }
MyRandomIcon(asset = asset)
Enter fullscreen mode Exit fullscreen mode

แบบที่ 2:
เราสามารถทำให้ MyRandomIcon ไม่มีการ Share State กันระหว่างตัวมันเองกับ MyScreen โดยเราสามารถทำได้โดยย้าย randomIconAsset ไปไว้ใน MyRandomIcon แทน

@Composable
fun MyRandomIcon() {
    Icon(asset = randomIconAsset())
}
Enter fullscreen mode Exit fullscreen mode

State hoisting

จาก Code ชุดข้างบน เราจะเห็นว่าตัว Function MyScreen เองนั้นเป็นคนถือ State เอาไว้ (Stateful) ในเคสนี้นั้นเราต้องการให้ ViewModel เป็นคนถือ State แทน เรียกว่า hoist/lift State ไปไว้ที่ LiveData ของ ViewModel แทน
เมื่อ User กดปุ่ม เราจะ Update value ใน LiveData ที่อยู่ใน ViewModel เท่ากับเราต้องทำการ

  • Bind ค่าที่อยู่ใน LiveData กับ Text
  • Bind action ของการกดปุ่มเข้ากับ onNameChanged function ของ ViewModel

ทำการเพิ่ม Parameter ใน Function เป็น String และ lambda สำหรับการแสดงผลและการกดปุ่มและนำ State ของ Function ออก

@Composable
fun MyScreen(
+ text: String, 
+ onButtonClick: () -> Unit
) {
- var text : MutableState<String> = remember { mutableStateOf(randomDisplayText()) }
    Row(modifier = Modifier
        .fillMaxWidth()
        .padding(16.dp)) {
           Column(modifier = Modifier.weight(1f)) {
+             Text(text)
+             Button(onClick = onButtonClick) {
                Text(text = "Click Me")
             }
           }
           val asset by remember { randomIconAsset() }
           MyRandomIcon(asset = asset)
    }
}

@Preview(showBackground = true)
@Composable
fun PreviewMyScreen() {
   ComposeStatePlaygroundTheme {
+       MyScreen("Hello Android") {} 
   }
}
Enter fullscreen mode Exit fullscreen mode

จาก Code ชุดข้างบนจะทำให้ function MyScreen ไม่มีการถือ State เอาไว้ สำหรับเปลี่ยนแปลงค่า displaying Text (Stateless)

observeAsState

JetPack Compose ได้สร้าง extension function มาให้เราแล้วชื่อ observeAsState เพื่อทำการ Convert LiveData ให้เป็น State

ใน onCreate ทำการ convert LiveData ใน ViewModel ให้เป็น State

setContent {
            MyAppTheme {
                Surface(color = MaterialTheme.colors.background) {
+                     val text by viewModel.name.observeAsState()
                    MyScreen(
+                     text = text, 
+                     onButtonClick = viewModel::onNameChanged)
                }
            }
        }
Enter fullscreen mode Exit fullscreen mode

ลองรันแอพอีกครั้ง
Alt Text
แน่นอนว่าการทำงานยังเหมือนเดิม สิ่งที่แตกต่างคือ State นั้นอยู่ที่ LiveData ของ ViewModel แทน
จริงๆเรายังสามารถให้ State อยู่ที่ ViewModel ได้โดยตรงด้วยโดย:

เอา LiveData ออก

-     private val _name = MutableLiveData("")
-     val name: LiveData<String> = _name
Enter fullscreen mode Exit fullscreen mode

เปลี่ยนเป็น State แทน

+ import androidx.compose.runtime.mutableStateOf
+ import androidx.compose.runtime.setValue
+ import androidx.compose.runtime.getValue

class MainViewModel: ViewModel() {
...
+     var name: String by mutableStateOf("")
+         private set

Enter fullscreen mode Exit fullscreen mode

แก้ไข function onNameChanged

fun onNameChanged() {
        - _name.value = randomDisplayText()
+         name = randomDisplayText()
    }
Enter fullscreen mode Exit fullscreen mode

ใส่ name เข้าไปตรงๆที่ MyScreen:

Surface(color = MaterialTheme.colors.background) {
- val text by viewModel.name.observeAsState()
   MyScreen(
   - text,
+    viewModel.name,
   viewModel::onNameChanged)
                }
Enter fullscreen mode Exit fullscreen mode

ผลลัพธ์ที่ได้ก็ยังคงเหมือนเดิม
เราสามารถเรียกใช้ mutableStateOf ใน ViewModel ได้เมื่อเรามั่นใจว่า View นั้นเป็น Compose แต่ถ้าเรายังต้องใช้ ViewModel contact กับ Android View System เดิม ใช้ LiveData หรือ State/Share Flow น่าจะเหมาะสมกว่า
สำหรับคนที่สนใจ แนะนำว่าสามารถศึกษาเพิ่มเติมได้จาก JetPack Compose - Pathway ในบทความหน้าจะลองยกตัวอย่าง Compose UI ที่ซับซ้อนขึ้นเช่นการใช้ LazyColumnFor (RecycelerView ในโลกของ JetPack Compose) รวถึงการใช้ State/ShareFlow หรือหากท่านใดสนใจ สามารถดู source code ตัวอย่างได้จากเจ้าของ Blog ทางนี้เลย

Reference:

💖 💪 🙅 🚩
eddiej
Eddie Eddie

Posted on January 15, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related