레시피앱 실습을 통해 mvvm 패턴으로 json 파싱하여 list 목록 만들기
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://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>)
json 파일을 읽을 endpoint 설정하기 위한 파일
Coroutines 사용하여 접근하기, api통신을 위한 Service파일 준비
suspend와 Corutines
1. suspend?
- API를 호출하는 suspend키워드는 비용이 많이 든다.
- 인터넷 속도,데이터 크기 등 여러 요인에 의해서 사용자 인터페이스 UI에 데이터를 표시하는 데 평소보다 더 오래걸릴 수 있음
- 이러한 종류의 작업은 비동기식으로 처리되는데 대기 시간이 길어지지 않기 위해 백그라운드에서 처리
- 코루틴 api의 일부임
- 전체 스레드는 차단하지 않고 해당 함수만 일시정지하는 키워드
?- Coroutines이나 다른 정지 함수에 의해서만 시전될 수 있다- 일시 중지된 동안 다른 작업이나 함수는 실행될 수 있음 특히 사용자 인터페이스가 있는 앱에서 리소스를 더 잘 활용하고 응답성을 향상 시킴
- 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
}
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
)
}
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)
)
}
}
<uses-permission android:name="android.permission.INTERNET"/>
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()
}
}
}
}
}
https://tutorials.eu/navigating-libraries-apis-and-remote-content-day-9-android-14-masterclass/