Modernized Android architecture - JetPack Compose Part1
Tags: android, kotlin, jetpack, compose
Published: 2021-01-15 11:59:04

_Cover Photo by PixaBay from_ [Pexels](https://www.pexels.com/photo/chords-sheet-on-piano-tiles-210764/)
จากที่เกริ่นไว้ใน Part แรกว่า JetPack Compose เป็นเรื่องที่ตั้งใจจะเขียนไว้ตอนแรกแต่เนื่องจากว่าใน Sample project มีจุดน่าสนใจหลายอย่าง เลยต้องแตกออกมาเป็นอีกหัวข้อนึง เรื่อง JetPack Compose ที่จะเขียนในวันนี้ อาจจะไม่ครอบคลุมเนื้อหาทั้งหมด เพราะตัว JetPack Compose เองก็มีเนื้อหาเยอะมาก รวมถึง API เองก็ยังไม่ได้อยู่ในสถานะ Stable เลยคิดว่าน่าจะมี Breaking change พอสมควร เนื้อหาที่จะเขียนในนี้จึงขอ Scope เนื้อหาเอาไว้คร่าวๆละ Sample App ละกัน

### เริ่มต้นสร้าง Project 
- Jetpack Compose นั้น สามารถใช้งานได้บน [Android Studio 4.2 (Preview)](https://developer.android.com/studio/preview) ขึ้นไปเท่านั้น เพราะฉะนั้นก่อนเริ่มสร้าง Project เราควรมั่นใจก่อนว่า Version ของ Android Studio เราถูกต้อง
- เมื่อกด New Project เราสามารถเลือก Empty Compose Activity ได้เลย
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/kro0jxmb3xk1f02i9xf3.jpg)
ใน `app/build.gradle` จะเห็นว่าเรามี Compose lib อยู่ข้างในด้วย
```gradle
    implementation "androidx.compose.ui:ui:$compose_version"
    implementation "androidx.ui:ui-tooling:$compose_version"
    implementation "androidx.compose.material:material:$compose_version"
```
- `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](https://dev-to-uploads.s3.amazonaws.com/i/xcon0vaqklo9tqieyoe1.jpg)

จุดที่เราสามารถสังเกตได้ชัดๆคือใน Activity ของเรานั้นไม่มีการ เรียก `setContentView(R.layout.activity_main)` อีกต่อไปแล้ว กลับกัน กับเรียก `setContent` แทนที่เป็น Extension Function ที่รับ lamda ที่มี `@Composable` ประกาศไว้ข้างหน้า
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/im3zu1j9zylo7wmx0jew.jpg)
สิ่งที่ set เข้าไปใน lamda ของ setContent ก็คือ `MyApplicationTheme` ซึ้งถ้าเราไปดู เจ้าตัว `MyApplicationTheme` นั้นก็เป็น Function ที่มี `@Composable` ไว้เช่นกัน
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/lc3gz071o32b99k278yk.jpg)
#### Composable
- `@Composable` คือ Annotation พื้นฐานสำหรับ JetPack Compose ที่ไว้ประกาศไว้หน้า Function เพื่อเป็นการบอกว่า Function นี้จะเป็น Block code ที่ไว้สำหรับ Built ด้วย Compose 
พูดง่ายๆคือเป็น UI Function ละกัน

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

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/w0ydf4t6nfdxdwo0hpu2.jpg)

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

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/y3zkzvkgmu7xij2nv9g3.jpg)

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

### Unidirectional Data Flow
 [Unidirectional Data Flow](https://en.wikipedia.org/wiki/Unidirectional_Data_Flow_(computer_science)) หรือ Single Directional Data Flow 
ย้อนกลับไปตอนที่เรายังเขียน Code เป็นแบบ [MVC Pattern](https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93controller) 
- ใน MVC เมื่อมี `Action` บางอย่างเกิดขึ้นเช่น User เปิดแอพ, User คลิกปุ่ม ฯลฯ `Action` จะถูกส่งไปให้ Controller เป็นคนจัดการ 
- Controller จะทำการจัดการกับ `Action` นั้นตามแต่ว่า Action นั้นคืออะไร เช่น User เปิดแอพ -> Controller ดึงข้อมูลจาก network เมื่อ Controller ได้ข้อมูลมาแล้ว นำข้อมูลไป parse เป็น model แล้วแสดงผลที่ View ให้ User เห็น

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/7hvplki96abolsg5isco.jpg)
Flow การทำงานนี้คอนข้างเรียบง่ายถ้าเป็นแอพขนาดเล็ก แต่ถ้าหากว่าแอพเราใหญ่ขึ้น Flow การทำงานก็จะซับซ้อนขึ้น เช่น เมื่อ User เปิดแอพ เราต้อง Fetch ข้อมูลจาก API มาจาก `n` Endpoints และแสดงผลใน View `n` Views 

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/kd0jr7pyat6nas2obcie.jpg)

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

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/zfxtf54p7qes6z7m44fv.png)
_Image from [Hacker Way: Rethinking Web App Development at Facebook](https://www.youtube.com/watch?v=nYkdrAPrdcw)_

ใครสนใจสามารถอ่านได้ที่ [Flux documentation](https://facebook.github.io/flux/docs/in-depth-overview)
สาเหตุที่จำเป็นต้องเกริ่นถึงเรื่องนี้เพราะตัว 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](https://dev-to-uploads.s3.amazonaws.com/i/49m22zsqd9j653ezb5pk.png)
- **Event** ในที่นี้อาจจะเป็น Action จากการกดปุ่ม หรือข้อมูลที่เกิดจากการ Fetch มาจาก Backend 
- **Update state** class หรือ function ที่ทำหน้าที่เปลี่ยนแปลง State
- **Display state** UI ที่ทำหน้าที่ Display ให้ User เห็น ตามแต่ละ State ที่เปลี่ยนแปลงไป

พูดอีกอย่างก็คือ เราจะแสดง UI ตาม State ที่เปลี่ยนแปลงไป ในขณะเดียวกัน State ก็อาจจะเปลี่ยนแปลงได้จากการที่ได้รับ Event บางอย่างมาจาก UI
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/j3x67v3qxwronstzlslk.png)

ยกตัวอย่าง:
```kotlin
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"
   }
}
```
Code ข้อบนชุดนี้คือ เราจะทำการ Update ค่าที่นำมาแสดงของ `helloText` เมื่อ User มีการกดปุ่มเพื่อ random โดยตัวอย่างโค๊ตข้างบนสามารถทำงานได้ถูกต้อง 100% ทีนี้ปัญหาคืออะไร? :thinking:

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

ใช้ ViewModel และ LiveData ช่วยในการจัดการ State
```kotlin
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"
       }
   }
}
```
จะเห็นว่าสิ่งที่เกิดขึ้นใน 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](https://dev-to-uploads.s3.amazonaws.com/i/j7ycfcv04iaif8stkdpx.gif)
โดยทุกๆครั้งที่ User กดปุ่ม เราจะทำการ random ข้อความขึ้นมาใหม่และแสดงที่ `Text` ตัวแอพจะแสดง Icon อยู่ทางขวาจะ random ขึ้นมาเหมือนกันแต่แค่ครั้งเดียวหลังจากที่เปิดแอพใหม่ โดยเราจะมี ViewModel อยู่แล้ว หน้าตาแบบนี้
```kotlin
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()
    }
}
```
พร้อมกับ Function สำหรับ Random Text
```kotlin
    fun randomDisplayText(): String {
        return when((Math.random() * (4 - 1) + 1).toInt()) {
            1 -> "Android"
            2 -> "JetPack"
            3 -> "Hilt"
            4 -> "People"
            else -> "World"
        }
    }
```

#### สร้าง 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](https://dev-to-uploads.s3.amazonaws.com/i/yavgnczqbwc6he51ja40.jpg)

สร้าง Composable Function ในที่นี้ให้ชื่อว่า MyScreen
```kotlin
@Composable
fun MyScreen() {
    
}
```
เพิ่ม `Row` โดยกำหนด Modifier ให้ แสดงผลเต็มทางแนวนอน และมี padding เป็น 16dp
```kotlin
@Composable
fun MyScreen() {
+     Row(modifier = Modifier
+         .fillMaxWidth()
+         .padding(16.dp)){
+     }
}
```
เพิ่ม `Column` โดยกำหนด weight ให้ Column = 1
```kotlin
@Composable
fun MyScreen() {
    Row(modifier = Modifier
        .fillMaxWidth()
        .padding(16.dp)) {
+         Column(modifier = Modifier.weight(1f)) {
       
+         }
    }
}
```
เพิ่ม `Icon`  ในเคสนี้เราต้องการให้ Icon ถูก random ขึ้นมา**เฉพาะครั้งแรกที่เปิดแอพเท่านั้น**
```kotlin
@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
+    }
+ }
```
เพิ่ม `Text` และ `Button` เข้าไปที่ Column
```kotlin
@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())
    }
}
```
เพิ่ม `MyScreen` เข้าไปที่ `onCreate` ของ Activity
```kotlin
override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MyAppTheme {
                Surface(color = MaterialTheme.colors.background) {
                    MyScreen(
                        text = "", {}
                    )
                }
            }
        }
    }
```

### Preview
เราสามารถ Preview หน้าตา UI ที่เราสร้างได้ด้วย `@Preview` ให้เราทำการสร้าง Function แยก ชื่อว่า `PreviewMyScreen` เพื่อใช้สำหรับ Preview อย่างเดียว
```kotlin
+ @Preview(showBackground = true)
+ @Composable
+ fun PreviewMyScreen() {
+    ComposeStatePlaygroundTheme {
+        MyScreen()
+    }
+ }
```
`showBackground = true` เพื่อแสดง Background ของแอพ
ทำการ Build app 1 ครั้งเพื่อ Preview
หน้าตาที่ได้:
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/ghgshmghymtb59p6r6vq.jpg)
เรายังสามารถทำการกดปุ่ม Interactive เพื่อทำการลองเล่นหน้าตา UI ของเราก่อนที่จะ Run ลงเครื่องจริงๆ ได้ด้วย
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/mhqjvbk1bo4evjfsdpt8.jpg)
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/lyy6u5vqymsrjmc66nw8.gif)

เนื่องจากเจ้า JetPack Compose นั้นเป็น Declarative UI โดยเจ้า  Declarative UI นั้นตัว Code เองคือตัวอธิบายว่าหน้าตา UI จะเป็นอย่างไร
เมื่อ User กดปุ่ม เราต้องการให้ `Text` แสดงค่า random Text จาก function ที่เราเตรียมไว้ หรือพูดง่ายๆก็คือ Update State ของ `Text` นั่นเอง
### State<T> และ MutableState<T>
เป็น Build-in Interface ของ JetPack Compose โดยปกติ Composable Function จะทำการ "subscribe" กับ `value` ของ `State<T>` เมื่อ `value` มีการเปลี่ยนแปลง Composable Function จะทำการ Recompose ตัวเองเพื่อ Update UI ใหม่
```kotlin
@Stable
interface State<T> {
    val value: T
}
```
โดย value สามารถเปลี่ยนแปลงได้ผ่านทางการเรียก `MutableState<T>` อีกที
```kotlin
@Stable
interface MutableState<T> : State<T> {
    override var value: T
    ....
}
```
เพิ่ม State ให้ Function MyScreen
```kotlin
@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())
    }
}
```
Function `mutableStateOf` จะทำการ return ค่า `MutableState` โดยในที่นี้เราจะกำหนดค่าเริ่มต้นให้กับ State เลย ด้วยการเรียก `randomDisplayText()` และเมื่อมีการกดปุ่มเราจะทำการ update ค่าของ text อีกครั้งด้วยการเรียก `text = randomDisplayText()`
อีกจุดนึงที่สังเกตเห็นได้คือมีการเรียกใช้ function `remember` ซึ่งจะขออธิบายในลำดับถัดไป
ลองรันแอพ:

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/kuwq8aarucjc85xhkgv1.gif)

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

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

ลองใส่ Log ที่ `MyScreen()` 
```kotlin
Log.d("MyComposeApp", "MyScreen is called")
```
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/19abh1cl8h0bsrzzv2i4.gif)
*สังเกตุได้ว่าค่าที่ Log ไม่ถูก Print เมื่อค่าที่ Random ได้ยังเป็นค่าเดิม*

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

### Memory ใน Function
จุดที่แตกต่างสำคัญเลยระหว่าง `randomDisplayText` `randomIconAsset` ที่ชัดเจนเลยคือ:
- `randomDisplayText` เป็น Initiate value ของ State (เราเรียก `randomDisplayText` ใน `mutableStateOf`)
- `mutableStateOf` ถูก wrap ด้วย `remember`
Composable function จะมีความสามารถในการจดจำค่าก่อนหน้าว่าเป็นค่าอะไร เมื่อเกิดการ Recomposition ขึ้น ตัว composable function จะนำค่าที่จดจำอยู่มาแสดงแทน ในกรณีของเรา เรามีการจดจำ "`State<String>`" ซึ่ง `value` ของ `State<String>` ก็คือ `text` นั่นเอง
```kotlin
var text: String by remember { mutableStateOf(randomDisplayText())}
```
สามารถเขียนในอีกแบบได้เป็น
```kotlin
var text :MutableState<String> = remember { mutableStateOf(randomDisplayText())}
...
+           Text(text = text.value)
            Button(onClick = {
+               text.value = randomDisplayText()
            }) {
                Text(text = "Click Me")
            }
```

##### แก้ไม่ให้ Icon Update ทุกครั้ง
วิธีการแก้ในเคสนี้เราสามารถทำได้สองแบบ 
**แบบที่ 1:**
เราสามารถใช้เจ้า `remember` กับ `randomIconAsset` ได้ เพื่อให้ค่าที่ถูก random มาครั้งแรก ถูกจดจำไว้:
```kotlin
val asset by remember { randomIconAsset() }
MyRandomIcon(asset = asset)
```
**แบบที่ 2:**
เราสามารถทำให้ `MyRandomIcon` ไม่มีการ Share State กันระหว่างตัวมันเองกับ MyScreen โดยเราสามารถทำได้โดยย้าย `randomIconAsset` ไปไว้ใน `MyRandomIcon` แทน
```kotlin
@Composable
fun MyRandomIcon() {
    Icon(asset = randomIconAsset())
}
```
### 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 ออก
```kotlin
@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") {} 
   }
}
```
จาก Code ชุดข้างบนจะทำให้ function MyScreen ไม่มีการถือ State เอาไว้ สำหรับเปลี่ยนแปลงค่า displaying Text (Stateless)

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

ใน `onCreate` ทำการ convert LiveData ใน ViewModel ให้เป็น State
```kotlin
setContent {
            MyAppTheme {
                Surface(color = MaterialTheme.colors.background) {
+                     val text by viewModel.name.observeAsState()
                    MyScreen(
+                     text = text, 
+                     onButtonClick = viewModel::onNameChanged)
                }
            }
        }
```
ลองรันแอพอีกครั้ง
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/mylnts1qcpw8g4vdat7g.gif)
แน่นอนว่าการทำงานยังเหมือนเดิม สิ่งที่แตกต่างคือ State นั้นอยู่ที่ LiveData ของ ViewModel แทน
จริงๆเรายังสามารถให้ State อยู่ที่ ViewModel ได้โดยตรงด้วยโดย:

เอา LiveData ออก
```kotlin
-     private val _name = MutableLiveData("")
-     val name: LiveData<String> = _name
```
เปลี่ยนเป็น State แทน
```kotlin
+ 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

```
แก้ไข function `onNameChanged`
```kotlin
fun onNameChanged() {
        - _name.value = randomDisplayText()
+         name = randomDisplayText()
    }
```
ใส่ `name` เข้าไปตรงๆที่ MyScreen:
```kotlin
Surface(color = MaterialTheme.colors.background) {
- val text by viewModel.name.observeAsState()
   MyScreen(
   - text,
+    viewModel.name,
   viewModel::onNameChanged)
                }
```
ผลลัพธ์ที่ได้ก็ยังคงเหมือนเดิม
*เราสามารถเรียกใช้ `mutableStateOf` ใน ViewModel ได้เมื่อเรามั่นใจว่า View นั้นเป็น Compose แต่ถ้าเรายังต้องใช้ ViewModel contact กับ Android View System เดิม ใช้ `LiveData` หรือ `State/Share Flow` น่าจะเหมาะสมกว่า*
สำหรับคนที่สนใจ แนะนำว่าสามารถศึกษาเพิ่มเติมได้จาก [JetPack Compose - Pathway](https://developer.android.com/courses/pathways/compose) ในบทความหน้าจะลองยกตัวอย่าง Compose UI ที่ซับซ้อนขึ้นเช่นการใช้ `LazyColumnFor` (`RecycelerView` ในโลกของ JetPack Compose) รวถึงการใช้ `State/ShareFlow` หรือหากท่านใดสนใจ สามารถดู source code ตัวอย่างได้จากเจ้าของ Blog [ทางนี้เลย](https://github.com/jutikorn/ComposePlayground)

### Reference:
- [State and Jetpack Compose](https://developer.android.com/jetpack/compose/state)
- [Android Codelab - State in Compose](https://developer.android.com/codelabs/jetpack-compose-state)
- [JetPack Compose - Pathway](https://developer.android.com/courses/pathways/compose)