게으른 컴공생
article thumbnail

Dessert Clicker App : ViewModel 추가

연습: ViewModel을 Dessert Clicker에 추가

 

연습: ViewModel을 Dessert Clicker에 추가  |  Android Developers

앱에 ViewModel을 추가하고 활동에서 로직을 추상화하는 방법을 연습합니다.

developer.android.com

위의 페이지를 참고하여 이전에 만들었던 Desser Clicker App에 ViewModel을 추가해보자

 

 

개요

예전에 만들었던 Dessert Clicker에 ViewModel을 이용한 앱 아키텍쳐를 적용해보자

기존에 만들어뒀던 프로젝트의 main branch로 체크아웃해서 시작하면 된다

 

build.gradle (Project: Dessert_Clicker)의 buildscript에 있는 ext안에 다음과 같은 코드를 추가하고

lifecycle_version = '2.5.1'

build.gradle (Modeul: Dessert_Clicker.app)에 다음과 같이 dependency를 추가한다

implementation "androidx.lifecycle:lifecycle-viewmodel-compose:$lifecycle_version"

 

그리고 화면 상단에 뜨는 Sync Now를 클릭해주자

 

 

GameUiState 만들기

게임 상태를 저장하는 데이터 클래스인 GameUiState를 만들자

ui.theme 경로에서 GameUiState라는 이름을 가지는 kotlin class 파일을 만들어주고

GameUiState.kt 파일의 첫 줄에 있는 package com.example.dessertclicker.ui.theme에서 theme을 빼준다

그러면 밑줄이 뜰텐데 여기서 alt+enter를 눌러서 package 경로를 바꿔주자

 

 

그리고 GameUiState 안에 DessertClickerApp 컴포저블의 rememberSaveable 변수들을 다 집어넣어주자

currentDessertIndex는 currentDessertPrice, ImageId의 값을 결정하기 위한 친구이니 그냥 빼도록 하겠다

package com.example.dessertclicker.ui

import com.example.dessertclicker.data.Datasource

data class GameUiState(
    var revenue: Int = 0,
    var dessertsSold: Int = 0,
    var currentDessertPrice: Int = Datasource.dessertList[0].price,
    var currentDessertImageId: Int = Datasource.dessertList[0].imageId
)

 

ViewModel 만들기

이제 ui package에 ViewModel.kt를 만들어주고

GameUiState를 가져온 뒤 로직도 옮겨주자

 

우선 GameUiState를 가져오자

private val _uiState =  MutableStateFlow(GameUiState())
val uiState: StateFlow<GameUiState> = _uiState.asStateFlow()

 

그리고 이미 완성되어있는 로직들을 가져온다

shareSoldDessertsInformation는 그냥 가져오면 되고

onDessertClicked에 연결되어 있는 람다는 updateState라는 메소드로 바꿔줬다

그리고 updateState에 들어있는 determineDessertToShow도 옮겨주고 다듬어줬다

package com.example.dessertclicker.ui

import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.widget.Toast
import androidx.core.content.ContextCompat
import androidx.lifecycle.ViewModel
import com.example.dessertclicker.R
import com.example.dessertclicker.model.Dessert
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import com.example.dessertclicker.data.Datasource

class GameViewModel : ViewModel() {
    private val _uiState =  MutableStateFlow(GameUiState())
    val uiState: StateFlow<GameUiState> = _uiState.asStateFlow()

    /**
     * Share desserts sold information using ACTION_SEND intent
     */
    fun shareSoldDessertsInformation(intentContext: Context, dessertsSold: Int, revenue: Int) {
        val sendIntent = Intent().apply {
            action = Intent.ACTION_SEND
            putExtra(
                Intent.EXTRA_TEXT,
                intentContext.getString(R.string.share_text, dessertsSold, revenue)
            )
            type = "text/plain"
        }

        val shareIntent = Intent.createChooser(sendIntent, null)

        try {
            ContextCompat.startActivity(intentContext, shareIntent, null)
        } catch (e: ActivityNotFoundException) {
            Toast.makeText(
                intentContext,
                intentContext.getString(R.string.sharing_not_available),
                Toast.LENGTH_LONG
            ).show()
        }
    }


    fun updateState() {
        val dessertToShow = determineDessertToShow(Datasource.dessertList, _uiState.value.dessertsSold)
        _uiState.update { currentState ->
            currentState.copy(
                revenue = currentState.revenue.plus(currentState.currentDessertPrice),
                dessertsSold = currentState.dessertsSold.inc(),
                currentDessertImageId = dessertToShow.imageId,
                currentDessertPrice = dessertToShow.price
            )
        }
    }

    /**
     * Determine which dessert to show.
     */
    private fun determineDessertToShow(
        desserts: List<Dessert>,
        dessertsSold: Int
    ): Dessert {
        var dessertToShow = desserts.first()
        for (dessert in desserts) {
            if (dessertsSold >= dessert.startProductionAmount) {
                dessertToShow = dessert
            } else {
                // The list of desserts is sorted by startProductionAmount. As you sell more desserts,
                // you'll start producing more expensive desserts as determined by startProductionAmount
                // We know to break as soon as we see a dessert who's "startProductionAmount" is greater
                // than the amount sold.
                break
            }
        }

        return dessertToShow
    }

}

 

 

ViewModel 연결하기

이제 얼추 완성(?)된 ViewModel을 MainActivity에 연결해주자

나는 GameScreen.kt 파일을 ui 경로에 만들어서 빼주고

MainActivity가 GameScreen을 호출하는 방식으로 진행해주겠다

 

GameScreen.kt

package com.example.dessertclicker.ui

import androidx.annotation.DrawableRes
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Share
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.example.dessertclicker.R

@Composable
fun GameScreen(
    gameViewModel: GameViewModel = viewModel()
) {
    val gameUiState by gameViewModel.uiState.collectAsState()

    Scaffold(
        topBar = {
            val intentContext = LocalContext.current
            AppBar(
                onShareButtonClicked = {
                    gameViewModel.shareSoldDessertsInformation(
                        intentContext = intentContext,
                        dessertsSold = gameUiState.dessertsSold,
                        revenue = gameUiState.revenue
                    )
                }
            )
        }
    ) { contentPadding ->
        DessertClickerScreen(
            revenue = gameUiState.revenue,
            dessertsSold = gameUiState.dessertsSold,
            dessertImageId = gameUiState.currentDessertImageId,
            onDessertClicked = { gameViewModel.updateState() },
            modifier = Modifier.padding(contentPadding)
        )
    }
}

@Composable
private fun AppBar(
    onShareButtonClicked: () -> Unit,
    modifier: Modifier = Modifier
) {
    Row(
        modifier = modifier
            .fillMaxWidth()
            .background(MaterialTheme.colors.primary),
        horizontalArrangement = Arrangement.SpaceBetween,
        verticalAlignment = Alignment.CenterVertically,
    ) {
        Text(
            text = stringResource(R.string.app_name),
            modifier = Modifier.padding(start = 16.dp),
            color = MaterialTheme.colors.onPrimary,
            style = MaterialTheme.typography.h6,
        )
        IconButton(
            onClick = onShareButtonClicked,
            modifier = Modifier.padding(end = 16.dp),
        ) {
            Icon(
                imageVector = Icons.Filled.Share,
                contentDescription = stringResource(R.string.share),
                tint = MaterialTheme.colors.onPrimary
            )
        }
    }
}


@Composable
fun DessertClickerScreen(
    revenue: Int,
    dessertsSold: Int,
    @DrawableRes dessertImageId: Int,
    onDessertClicked: () -> Unit,
    modifier: Modifier = Modifier
) {
    Box(modifier = modifier) {
        Image(
            painter = painterResource(R.drawable.bakery_back),
            contentDescription = null,
            contentScale = ContentScale.Crop
        )
        Column {
            Box(
                modifier = Modifier
                    .weight(1f)
                    .fillMaxWidth(),
            ) {
                Image(
                    painter = painterResource(dessertImageId),
                    contentDescription = null,
                    modifier = Modifier
                        .width(150.dp)
                        .height(150.dp)
                        .align(Alignment.Center)
                        .clickable { onDessertClicked() },
                    contentScale = ContentScale.Crop,
                )
            }
            TransactionInfo(revenue = revenue, dessertsSold = dessertsSold)
        }
    }
}

@Composable
private fun TransactionInfo(
    revenue: Int,
    dessertsSold: Int,
    modifier: Modifier = Modifier
) {
    Column(
        modifier = modifier
            .background(Color.White),
    ) {
        DessertsSoldInfo(dessertsSold)
        RevenueInfo(revenue)
    }
}

@Composable
private fun RevenueInfo(revenue: Int, modifier: Modifier = Modifier) {
    Row(
        modifier = modifier
            .fillMaxWidth()
            .padding(16.dp),
        horizontalArrangement = Arrangement.SpaceBetween,
    ) {
        Text(
            text = stringResource(R.string.total_revenue),
            style = MaterialTheme.typography.h4
        )
        Text(
            text = "$${revenue}",
            textAlign = TextAlign.Right,
            style = MaterialTheme.typography.h4
        )
    }
}

@Composable
private fun DessertsSoldInfo(dessertsSold: Int, modifier: Modifier = Modifier) {
    Row(
        modifier = modifier
            .fillMaxWidth()
            .padding(16.dp),
        horizontalArrangement = Arrangement.SpaceBetween,
    ) {
        Text(
            text = stringResource(R.string.dessert_sold),
            style = MaterialTheme.typography.h6
        )
        Text(
            text = dessertsSold.toString(),
            style = MaterialTheme.typography.h6
        )
    }
}

 

 

이제 실행을 해보면 DessertClicker가 잘 작동하는 것을 볼 수 있다

profile

게으른 컴공생

@노나니노나

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!

검색 태그