08_레시피앱1(MVVM/데이터 통신/suspend/LazyVerticalGrid)

소정·2024년 7월 16일
0

Android_with_compose

목록 보기
8/17

레시피앱 실습을 통해 mvvm 패턴으로 json 파싱하여 list 목록 만들기

1. 종속성 추가

dependencies에 viewmodel과 네트워크 통신 등 프로젝트에 필요한 종속성 추가하기

	//Compose Viewmodel
    implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.6.2'
    //network call
    implementation 'com.squareup.retrofit2:retrofit:2.9.0'
    //Json loader
    implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
    //이미지 로딩
    implementation 'io.coil-kt:coil-compose:2.4.0'

참고
https://coil-kt.github.io/coil/compose/

2.데이터 class 만들기

https://www.themealdb.com/api.php 사이트에서 제공하는 레시피 db를 json으로 파싱하기 위해 data class만들기

package com.lullulalal.myrecipeapp

data class Category(
    val idCategory : String,
    val strCategory : String,
    val strCategoryThumb : String,
    val strCategoryDescription : String,
    )

data class CategoriesResponse(val categories:List<Category>)

3. api통신을 위한 Service

json 파일을 읽을 endpoint 설정하기 위한 파일
Coroutines 사용하여 접근하기, api통신을 위한 Service파일 준비

suspend와 Corutines
1. suspend?

  • API를 호출하는 suspend키워드는 비용이 많이 든다.
  • 인터넷 속도,데이터 크기 등 여러 요인에 의해서 사용자 인터페이스 UI에 데이터를 표시하는 데 평소보다 더 오래걸릴 수 있음
  • 이러한 종류의 작업은 비동기식으로 처리되는데 대기 시간이 길어지지 않기 위해 백그라운드에서 처리
  • 코루틴 api의 일부임
  • 전체 스레드는 차단하지 않고 해당 함수만 일시정지하는 키워드
    ?- Coroutines이나 다른 정지 함수에 의해서만 시전될 수 있다
  • 일시 중지된 동안 다른 작업이나 함수는 실행될 수 있음 특히 사용자 인터페이스가 있는 앱에서 리소스를 더 잘 활용하고 응답성을 향상 시킴
  1. Corutines?
  • 강력하고 가벼운 동시설 프레임워크
  • 비동기 및 비차단, 차단 작업을 처리하기 위해 특별히 설계됨
  • 네트워크 작업 같은 일은 수행하는데 자주 사용함
  • 시간이 많이 소요되는 작업을 백그라운드에서 실행, 효율적이고 반응이 빠른 사용자 인터페이스를 야기함
package com.lullulalal.myrecipeapp

import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.http.GET

//Retrofit.Builder()는 엔드포인트를 준비시키고 Json converter를 추가하는 일을 함
//그 다음 create 메소드를 제공하는데 이는 서비스 메소드에 대한 액세스 권한을 위임한다
private val retrofit = Retrofit.Builder()
    .baseUrl("https://www.themealdb.com/api/json/v1/1/")
    .addConverterFactory(GsonConverterFactory.create())
    .build()

val recepeService = retrofit.create(ApiService::class.java)
//해당 서비스가 필요하다는 것을 의미
//recepeServices는 ApiService를 외부에서 사용할 수 있도록 노출시켜주는 변수

interface ApiService {

    //List all meal categories
    //www.themealdb.com/api/json/v1/1/categories.php
    @GET("categories.php") //엔드포인트 설정
    suspend fun getCategories():CategoriesResponse

}

4. 데이터와 UI 연결다리 viewModel.kt 작성

  • 데이터를 감시하는 역할을 하는 변수와 UI에서 사용 가능하게 해주는 변수 따로둠
  • viewModelScope를 사용하여 서버 호출을 백그라운드에서 할 수 있도록함
    (메인 스레드는 ui담당이기 때문에 메인스레드에게 시킴 앱 죽어버림)
package com.lullulalal.myrecipeapp

import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.launch

class MainViewModel :ViewModel() { //ViewModel : 데이터와 UI 간의 통신 처리

    private val _categoryState = mutableStateOf(RecipeState())
    val categoriesState:State<RecipeState> = _categoryState //다른 클래스에 노출할 변수
    //State => 구성 가능한 함수를 실행하는 동안 값 속성을 참조하는 value holder는 현재의 재구성 범위가 해당
    //값의 변화에 따르게 되고 이는 기본적으로 실제로 우리의 RecipeState 객체가 변경될 떄마다 사용자 인터페이스가
    //업데이트 되기를 원한다는 것
    //RecipeState의 값이 변화된것을 알려줌

    //받은 데이터를 표시하기 - MainViewModel 호출하면 실행되는 부분
    init {
        fetchCategories()
    }

    private fun fetchCategories() { //화면에 표시하기 위해 불러야 하는 함수
        //viewModelScope => suspend함수가 처리되도록 launch를 제공함
        //suspend => 백그라운드에서 실행
        //suspend 실행하고 싶으면 corutine 내에서 시작해야함

        viewModelScope.launch {
            try {
                val response = recepeService.getCategories()
                _categoryState.value = _categoryState.value.copy(
                    list = response.categories,
                    loading = false,
                    error = null
                )
            } catch (e:Exception) {
                _categoryState.value = _categoryState.value.copy(
                    loading = false,
                    error = "Error ${e.message}"
                )
            }
        }

    }
     data class RecipeState(
         val loading : Boolean = true,
         val list : List<Category> = emptyList(),
         val error : String? = null
     )

}

5. 사용자한테 보여질 UI 담당 파일 만들기

  • 리스트는 LazyVerticalGrid 그리드 형식의 리사이클러 리스트 사용
  • 그 리스트 안에서 레시피가 보여질 모양인 ItemView모양도 설정
  • ui를 그릴떈 데이터 로딩중/ 에러/ 성공 세가지로 나눠서 표시
package com.lullulalal.myrecipeapp

import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import coil.compose.rememberAsyncImagePainter

//사용자한테 보여질 UI 담당 파일

@Composable
fun RecipeScreen(modifier: Modifier = Modifier) {
    val recipeViewModel : MainViewModel = viewModel() //데이터 가져오는 역할
    val viewState by recipeViewModel.categoriesState
    //getValue import -> state의 값을 바로 가져올 수 있도록 해주는 애

    //ui 그리기용
    Box(modifier = Modifier.fillMaxSize()) {
        //로딩중 표시
        when {
            viewState.loading -> {
                CircularProgressIndicator(modifier.align(Alignment.Center))
            }
            viewState.error != null -> {
                Text(text = "Error")
            }
            else -> {
                //제대로 로딩됐을 때
                CategorScreen(categories = viewState.list)
            }
        }
    }
}

//리스트 항목을 생성하기 위한 view
@Composable
fun CategorScreen(categories : List<Category>) {
    //LazyVerticalGrid => 항목을 격자 무늬로 배치
    LazyVerticalGrid(columns = GridCells.Fixed(2), modifier = Modifier.fillMaxSize()) {
        //아이템과 ui 연결
        items(categories) {
            category ->
            CategoyItem(category = category)
        }
    }
}

//각 아이템 생김새 ui
@Composable
fun CategoyItem(category: Category) {
    Column(modifier = Modifier
        .padding(8.dp)
        .fillMaxSize(),
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Image(
            painter = rememberAsyncImagePainter(category.strCategoryThumb),
            contentDescription = category.strCategoryDescription,
            modifier = Modifier
                .fillMaxSize()
                .aspectRatio(1f)) // 1:1

        Text(
            text = category.strCategory,
            color = Color.Black,
            style = TextStyle(fontWeight = FontWeight.Bold),
            modifier = Modifier.padding(top=4.dp)
            )
    }
}

6. 인터넷 퍼미션

  • 인터넷을 사용하기위해 매니패스트에서 퍼미션 받기
<uses-permission android:name="android.permission.INTERNET"/>

7.main에서 실행

package com.lullulalal.myrecipeapp

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.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import com.lullulalal.myrecipeapp.ui.theme.MyRecipeAppTheme

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MyRecipeAppTheme {
                // A surface container using the 'background' color from the theme
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colorScheme.background
                ) {
                    RecipeScreen()
                }
            }
        }
    }
}

이번 파트에서 배운것

  • retrofit2를 통해 네트워크 연결
  • mvvm 패턴 사용하여 각각 파일별로 해야하는 역할 분리
  • viewmodelScope 사용
  • 이미지 비동기로 읽어주는 rememberAsyncImagePainter 사용
  • 코루틴의 종료중 하나인 suspend 사용

https://tutorials.eu/navigating-libraries-apis-and-remote-content-day-9-android-14-masterclass/

profile
보조기억장치

0개의 댓글