SunFlower 뭔가 달라졌다??

disun.sj·2023년 9월 10일
0

Sunflower Copy

목록 보기
4/4

카피 프로젝트 링크

오랜만에 다시 시작 했는데??!!
우선 gradle, studio를 최신으로 업데이트 후 Version Catalog 버전을 최신화 했다.
git에서 pull을 받아서 GardenActivity를 켯는데 뭔가 변했다.
여기부터 다시 해야할 듯 하다.

변경 전

class GardenActivity : AppCompatActivity() {

    private val viewModel: PlantListViewModel by viewModels()

    private val menuProvider = object :MenuProvider{
        override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
            menuInflater.inflate(R.menu.menu_plant_list,menu)
        }

        override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
            return when(menuItem.itemId){
                R.id.filter_zone -> {
                    viewModel.updateData()
                    true
                }
                else -> false
            }
        }
    }
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        WindowCompat.setDecorFitsSystemWindows(window, false)

        setContent {
            MdcTheme{
                SunflowerApp(
                    onAttached = { toolbar ->
                        setSupportActionBar(toolbar)
                    },
                    onPageChange = { page ->
                        when(page){
                            SunflowerPage.MY_GARDEN -> removeMenuProvider(menuProvider)
                            SunflowerPage.PLANT_LIST -> addMenuProvider(menuProvider, this)
                        }
                    },
                    plantListViewModel = viewModel
                )
            }
        }
    }
}

변경 후

class GardenActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        WindowCompat.setDecorFitsSystemWindows(window, false)

        setContent {
            SunFlowerTheme {
                SunflowerApp()
            }
        }
    }
}
  • MdcTheme에서 SunFlowerTheme로 바뀌면서 Viewmodel, Provider 코드가 사라지고, SunflowerApp 변수가 사라졌다.

  • SunFlowerTheme도 변한게 있을까 싶어서 들어가서 확인해보니 달라졌다!!

변경 전

fun SunFlowerTheme(
    darkTheme: Boolean = isSystemInDarkTheme(),
    // Dynamic color is available on Android 12+
    dynamicColor: Boolean = true,
    content: @Composable () -> Unit
) {
    val colorScheme = when {
        dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
            val context = LocalContext.current
            if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
        }

        darkTheme -> DarkColorScheme
        else -> LightColorScheme
    }
    val view = LocalView.current
    if (!view.isInEditMode) {
        SideEffect {
            val window = (view.context as Activity).window
            window.statusBarColor = colorScheme.primary.toArgb()
            WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = darkTheme
        }
    }

    MaterialTheme(
        colorScheme = colorScheme,
        typography = Typography,
        content = content
    )
}

변경 후

fun SunFlowerTheme(
    darkTheme: Boolean = isSystemInDarkTheme(),
    // Dynamic color is available on Android 12+
    dynamicColor: Boolean = false,
    content: @Composable () -> Unit
) {
    val colorScheme = when {
        dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
            val context = LocalContext.current
            if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
        }

        darkTheme -> DarkColorScheme
        else -> LightColorScheme
    }
    val view = LocalView.current
    if (!view.isInEditMode) {
        val systemUiController = rememberSystemUiController()
        val useDarkIcons = !isSystemInDarkTheme()
        val window = (view.context as Activity).window
        WindowCompat.setDecorFitsSystemWindows(window, false)
        DisposableEffect(systemUiController, useDarkIcons ){
            systemUiController.setSystemBarsColor(color = Color.Transparent, darkIcons = useDarkIcons)
            onDispose {  }
        }
    }

    MaterialTheme(
        colorScheme = colorScheme,
        shapes = Shapes,
        typography = Typography,
        content = content
    )
}
  • rememberSystemUiController()를 사용하는데 안돼서 추적해보니 com.google.accompanist:accompanist-systemuicontroller를 사용중이다. 추가해주자
  • shapes가 MaterialTheme에 추가 되었는데 Shapes가 없다. 만들어주자
val Shapes = Shapes(
    small = RoundedCornerShape(
        topStart = 0.dp,
        topEnd = 12.dp,
        bottomStart = 12.dp,
        bottomEnd = 0.dp
    ),
    medium = RoundedCornerShape(
        topStart = 0.dp,
        topEnd = 12.dp,
        bottomStart = 12.dp,
        bottomEnd = 0.dp
    )
)
  • 다시 GardenActivity로 돌아와 달라진 SunflowerApp을 확인하러 가자. 역시나 달라졌다
    변경 전
fun SunflowerApp(
    onPageChange : (SunflowerPage) -> Unit = {},
    onAttached: (Toolbar) -> Unit = {},
    plantListViewModel : PlantListViewModel = hiltViewModel()
){
    val navController = rememberNavController()
    SunFlowerNavHost(
        plantListViewModel = plantListViewModel,
        navController = navController,
        onPageChange = onPageChange,
        onAttached = onAttached
    )
}

변경 후

fun SunflowerApp(){
    val navController = rememberNavController()
    SunFlowerNavHost(
        navController = navController
    )
}
  • SunFlowerNavHost의 경우 지난번에 틀만 빌드에 문제가 없을 정도로만 작성해놨었는데, 변한부분 + 나머지 부분 작성해 주도록 하자
  • HomeScreen()이 필요하니 작성해 주도록 하자
  • 작성 중 rememberPagerState()이 필요하여 추적해보니 package androidx.compose.foundation.pager을 에서 사용중 인데 androidx.compose.foundation:foundation, androidx.compose.foundation:foundation-layout 두가지가 존재하길래 둘 다 추가해줫다.
  • Sunflower Project는 rememberPagerState() 인데 안돼서 보니 코드가 미묘하게 다르다. 버전의 문제일까??
    -compose bom이 프로젝트 만들 때 추가된걸 그냥 쓰고 있었는데, 여기에도 foundation이 포함된다.
  • 그런데 version 최신화 하면서 1.5.0 버전이 포함된 버전으로 올라가 버리면서 코드가 변한것 같다
  • 최신화 해주고 싶지만 우선은 카피 프로젝트이고, 관련 코드 이해도가 부족한 상황이니 bom의 버전을 2023.06.01로 맞춘후 진행하자
  • HomeTopAppBar, HomePagerScreen를 추가해주자

HomePagerScreen

@OptIn(ExperimentalFoundationApi::class)
@Composable
fun HomePagerScreen(
    onPlantClick: (Plant) -> Unit,
    pagerState: PagerState,
    modifier: Modifier = Modifier,
    pages: Array<SunflowerPage> = SunflowerPage.values()
){
    Column(modifier) {
        val coroutineScope = rememberCoroutineScope()

        TabRow(selectedTabIndex = pagerState.currentPage) {
            pages.forEachIndexed{index, page ->
                val title = stringResource(id = page.titleResId)
                Tab(
                    selected = pagerState.currentPage == index,
                    onClick = { coroutineScope.launch { pagerState.animateScrollToPage(index) }},
                    text = { Text(text = title)},
                    icon = {
                        Icon(painter = painterResource(id = page.drawableResId), contentDescription = title)
                    },
                    unselectedContentColor = MaterialTheme.colorScheme.secondary
                )
            }
        }

        HorizontalPager(
            modifier = Modifier.background(MaterialTheme.colorScheme.background),
            pageCount = pages.size,
            state = pagerState,
            verticalAlignment = Alignment.Top
        ) { index ->
            when (pages[index]) {
                SunflowerPage.MY_GARDEN -> {
                    GardenScreen(
                        Modifier.fillMaxSize(),
                        onAddPlantClick = {
                            coroutineScope.launch {
                                pagerState.scrollToPage(SunflowerPage.PLANT_LIST.ordinal)
                            }
                        },
                        onPlantClick = {
                            onPlantClick(it.plant)
                        })
                }

                SunflowerPage.PLANT_LIST -> {
                    PlantListScreen(
                        onPlantClick = onPlantClick,
                        modifier = Modifier.fillMaxSize(),
                    )
                }
            }
        }
    }
}

HomeTopAppBar

@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class)
@Composable
private fun HomeTopAppBar(
    pagerState: PagerState,
    onFilterClick: () -> Unit,
    scrollBehavior: TopAppBarScrollBehavior,
    modifier: Modifier = Modifier
) {
    TopAppBar(
        title = {
            Row(
                Modifier.fillMaxWidth(),
                horizontalArrangement = Arrangement.Center,
            ) {
                Text(
                    text = stringResource(id = R.string.app_name),
                    style = MaterialTheme.typography.displaySmall
                )
            }
        },
        modifier = modifier.statusBarsPadding(),
        actions = {
            if (pagerState.currentPage == SunflowerPage.PLANT_LIST.ordinal) {
                IconButton(onClick = onFilterClick) {
                    Icon(
                        painter = painterResource(id = R.drawable.ic_filter_list_24dp),
                        contentDescription = stringResource(
                            id = R.string.menu_filter_by_grow_zone
                        )
                    )
                }
            }
        },
        scrollBehavior = scrollBehavior
    )
}
  • GardenScreen과 PlantListScreen을 추가해 줘야한다.
  • PlantListScreen 부터 추가해주는데 지난번에 만든 PlantListViewModel에 아래 코드를 추가해준다.
  • asLiveData을 사용하기위해 androidx.lifecycle:lifecycle-livedata-ktx을 추가해줄건데, androidx.lifecycle:lifecycle-viewmodel-compose, androidx.lifecycle:lifecycle-viewmodel-ktx도 있길래 미리 같이 추가해주기로 했다.
  • 다시 PlantListScreen으로 돌아와 observeAsStae를 위해 androidx.compose.runtime:runtime-livedata를 추가한다.
    val plants: LiveData<List<Plant>> = growZone.flatMapLatest { zone ->
        if (zone == NO_GROW_ZONE) {
            plantRepository.getPlants()
        } else {
            plantRepository.getPlantsWithGrowZoneNumber(zone)
        }
    }.asLiveData()
  • PlantListScreen에 아래코드를 추가한다
  • PlantListItem을 추가해야 하기 때문에 PlantListItemView를 추가해준다
fun PlantListScreen(
    plants: List<Plant>,
    modifier: Modifier = Modifier,
    onPlantClick: (Plant) -> Unit = {},
) {
    LazyVerticalGrid(
        columns = GridCells.Fixed(2),
        modifier = modifier.testTag("plant_list"),
        contentPadding = PaddingValues(
            horizontal = dimensionResource(id = R.dimen.card_side_margin),
            vertical = dimensionResource(id = R.dimen.header_margin)
        )
    ) {
        items(
            items = plants,
            key = { it.plantId }
        ) { plant ->
            PlantListItem(plant = plant) {
                onPlantClick(plant)
            }
        }
    }
}

PlantListItemView

@Composable
fun PlantListItem(plant: Plant, onClick: () -> Unit) {
    ImageListItem(name = plant.name, imageUrl = plant.imageUrl, onClick = onClick)
}
  • ImageListItem이 필요해서 PlantListItemView에 작성중, SunflowerImage()코드가 필요해 먼저 작성한다
  • ExperimentalGlideComposeApi를 OptIn 해줘야 하는데 Import가 안돼서 찾아보니 Glide에 속해있는 것 같다
  • com.github.bumptech.glide:compose를 추가해준다

SunflowerImage

@OptIn(ExperimentalGlideComposeApi::class)
@Composable
fun SunflowerImage(
    model : Any?,
    contentDescription : String,
    modifier: Modifier = Modifier,
    alignment: Alignment = Alignment.Center,
    contentScale: ContentScale = ContentScale.Fit,
    alpha : Float = DefaultAlpha,
    colorFilter: ColorFilter? = null,
    requestBuilderTransform: RequestBuilderTransform<Drawable> ={it}
){
    if(LocalInspectionMode.current){
        Box(modifier = modifier.background(Color.Magenta))
        return
    }
    
    GlideImage(model = model, contentDescription = contentDescription, modifier = modifier, alignment = alignment, contentScale = contentScale, alpha = alpha, colorFilter = colorFilter, requestBuilderTransform =  requestBuilderTransform,
        loading = placeholder{
            Box(modifier.fillMaxWidth(), contentAlignment = Alignment.Center){
                CircularProgressIndicator(Modifier.size(40.dp))
            }
        }
    )
}

ImageListItem

@OptIn(ExperimentalMaterial3Api::class)
    @Composable
    fun ImageListItem(name: String, imageUrl:String, onClick: () -> Unit){
        Card(
            onClick = onClick,
            colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.secondaryContainer),
            modifier = Modifier
                .padding(horizontal = dimensionResource(id = R.dimen.card_side_margin))
                .padding(bottom = dimensionResource(id = R.dimen.card_bottom_margin))
        ){
            Column(Modifier.fillMaxHeight()) {
                SunflowerImage(model = imageUrl, contentDescription = stringResource(id = R.string.a11y_plant_item_image),
                    Modifier
                        .fillMaxWidth()
                        .height(dimensionResource(id = R.dimen.plant_item_image_height)),
                    contentScale = ContentScale.Crop)
                Text(text = name, textAlign = TextAlign.Center, maxLines = 1, style = MaterialTheme.typography.titleMedium, modifier = Modifier
                    .fillMaxWidth()
                    .padding(
                        vertical = dimensionResource(id = R.dimen.margin_normal))
                    .wrapContentWidth(Alignment.CenterHorizontally))
            }
        }
    }
  • PlantListScreen 작성이 모두 끝났다. GardenScreen 작성을 시작한다

GardenScreen

@Composable
fun GardenScreen(
    modifier: Modifier = Modifier,
    viewModel: GardenPlantingListViewModel = hiltViewModel(),
    onAddPlantClick: () -> Unit,
    onPlantClick: (PlantAndGardenPlantings) -> Unit
) {
    val gardenPlants by viewModel.plantAndGardenPlantings.collectAsState(initial = emptyList())
    GardenScreen(
        gardenPlants = gardenPlants,
        modifier = modifier,
        onAddPlantClick = onAddPlantClick,
        onPlantClick = onPlantClick
    )
}
  • GardenPlantingListViewModel이 없기때문에 추가해줘야한다
  • GardenPlantingRepository 를 같이 추가해준다
  • GardenPlantingRepository에는 GardenPlantingDao가 필요하기 때문에 추가한다
  • GardenPlantingRepository에서 getPlantedGardens만 이번에 사용하기 때문에 관련된 코드만 우선 추가하자
  • PlantAndGardenPlantings Data Class가 필요하다 추가하자
  • @Embedded, @Relation에 대해 모르겠어서 찾아본다
  • 각각 아래와 같다고한다. 하지만 자세히 이해가 되지 않아, 추후 ROOM을 정리할 때 같이 정리하도록 하자

@Embedded
Room에서 obejct를 표현하기 위한 어노테이션입니다.

@Relation
부모와 자식 관계를 정의하기 위한 어노테이션입니다.

PlantAndGardenPlantings

data class PlantAndGardenPlantings(
    @Embedded
    val plant:Plant,

    @Relation(parentColumn = "id", entityColumn = "plant_id")
    val gardenPlantings: List<GardenPlanting> = emptyList()
)

GardenPlantingDao

@Dao
interface GardenPlantingDao {
    @Transaction
    @Query("SELECT * FROM plants WHERE id IN (SELECT DISTINCT(plant_id) FROM garden_plantings)")
    fun getPlantedGardens() : Flow<List<PlantAndGardenPlantings>>
}

GardenPlantingRepository

@Singleton
class GardenPlantingRepository @Inject constructor(
    private val gardenPlantingDao : GardenPlantingDao
){

    fun getPlantedGardens() = gardenPlantingDao.getPlantedGardens()
}

GardenPlantingListViewModel

@HiltViewModel
class GardenPlantingListViewModel @Inject internal constructor(
    gardenPlantingRepository: GardenPlantingRepository
) : ViewModel() {
    val plantAndGardenPlantings: Flow<List<PlantAndGardenPlantings>> =
        gardenPlantingRepository.getPlantedGardens()
}
  • GardenScreen을 작성해준다

GardenScreen

@Composable
fun GardenScreen(
    modifier: Modifier = Modifier,
    viewModel: GardenPlantingListViewModel = hiltViewModel(),
    onAddPlantClick: () -> Unit,
    onPlantClick: (PlantAndGardenPlantings) -> Unit
) {
    val gardenPlants by viewModel.plantAndGardenPlantings.collectAsState(initial = emptyList())
    GardenScreen(
        gardenPlants = gardenPlants,
        modifier = modifier,
        onAddPlantClick = onAddPlantClick,
        onPlantClick = onPlantClick
    )
}

@Composable
fun GardenScreen(
    gardenPlants: List<PlantAndGardenPlantings>,
    modifier: Modifier = Modifier,
    onAddPlantClick: () -> Unit = {},
    onPlantClick: (PlantAndGardenPlantings) -> Unit = {}
) {
    if (gardenPlants.isEmpty()) {
        EmptyGarden(onAddPlantClick, modifier)
    } else {
        GardenList(gardenPlants = gardenPlants, onPlantClick = onPlantClick, modifier = modifier)
    }
}

@Composable
private fun GardenList(
    gardenPlants: List<PlantAndGardenPlantings>,
    onPlantClick: (PlantAndGardenPlantings) -> Unit,
    modifier: Modifier = Modifier,
) {
    // Call reportFullyDrawn when the garden list has been rendered
    val gridState = rememberLazyGridState()
    ReportDrawnWhen { gridState.layoutInfo.totalItemsCount > 0 }
    LazyVerticalGrid(
        columns = GridCells.Fixed(2),
        modifier,
        state = gridState,
        contentPadding = PaddingValues(
            horizontal = dimensionResource(id = R.dimen.card_side_margin),
            vertical = dimensionResource(id = R.dimen.margin_normal)
        )
    ) {
        items(
            items = gardenPlants,
            key = { it.plant.plantId }
        ) {
            GardenListItem(plant = it, onPlantClick = onPlantClick)
        }
    }
}

@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun GardenListItem(
    plant: PlantAndGardenPlantings,
    onPlantClick: (PlantAndGardenPlantings) -> Unit
) {
    val vm = PlantAndGardenPlantingsViewModel(plant)

    // Dimensions
    val cardSideMargin = dimensionResource(id = R.dimen.card_side_margin)
    val marginNormal = dimensionResource(id = R.dimen.margin_normal)

    ElevatedCard(
        onClick = { onPlantClick(plant) },
        modifier = Modifier.padding(
            start = cardSideMargin,
            end = cardSideMargin,
            bottom = dimensionResource(id = R.dimen.card_bottom_margin)
        ),
        colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.secondaryContainer)
    ) {
        Column(Modifier.fillMaxWidth()) {
            SunflowerImage(
                model = vm.imageUrl,
                contentDescription = plant.plant.description,
                Modifier
                    .fillMaxWidth()
                    .height(dimensionResource(id = R.dimen.plant_item_image_height)),
                contentScale = ContentScale.Crop,
            )

            // Plant name
            Text(
                text = vm.plantName,
                Modifier
                    .padding(vertical = marginNormal)
                    .align(Alignment.CenterHorizontally),
                style = MaterialTheme.typography.titleMedium,
            )

            // Planted date
            Text(
                text = stringResource(id = R.string.plant_date_header),
                Modifier.align(Alignment.CenterHorizontally),
                style = MaterialTheme.typography.titleSmall
            )
            Text(
                text = vm.plantDateString,
                Modifier.align(Alignment.CenterHorizontally),
                style = MaterialTheme.typography.labelSmall
            )

            // Last Watered
            Text(
                text = stringResource(id = R.string.watered_date_header),
                Modifier
                    .align(Alignment.CenterHorizontally)
                    .padding(top = marginNormal),
                style = MaterialTheme.typography.titleSmall
            )
            Text(
                text = vm.waterDateString,
                Modifier.align(Alignment.CenterHorizontally),
                style = MaterialTheme.typography.labelSmall
            )
            Text(
                text = pluralStringResource(
                    id = R.plurals.watering_next,
                    count = vm.wateringInterval,
                    vm.wateringInterval
                ),
                Modifier
                    .align(Alignment.CenterHorizontally)
                    .padding(bottom = marginNormal),
                style = MaterialTheme.typography.labelSmall
            )
        }
    }
}

@Composable
private fun EmptyGarden(onAddPlantClick: () -> Unit, modifier: Modifier = Modifier) {
    // Calls reportFullyDrawn when this composable is composed.
    ReportDrawn()

    Column(
        modifier,
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center
    ) {
        Text(
            text = stringResource(id = R.string.garden_empty),
            style = MaterialTheme.typography.headlineSmall
        )
        Button(
            shape = MaterialTheme.shapes.medium,
            onClick = onAddPlantClick
        ) {
            Text(
                text = stringResource(id = R.string.add_plant),
                style = MaterialTheme.typography.titleSmall
            )
        }
    }
}
  • 작성중 R.plurals.watering_next라는 녀석을 발견했다. dimen, string과 같은 values들은 사용해 봤는데 처음보는 values라 찾아봤다
  • 문자열의 단수/복수 일 경우 구분해서 작성해주는 코드라고 한다
  • 지금까진 사용해본 적 없고, 앞으로도 쓸 일이 생길진 모르겠지만 알아둬서 나쁠건 없는 것 같다
  • 여기까지 진행시 error: [Dagger/MissingBinding] com.practice.sunflower.data.dao.GardenPlantingDao cannot be provided without an @Provides-annotated method.
    public abstract static class SingletonC implements MyApplication_GeneratedInjector,에러가 발생한다
  • DatabaseModule에 아래 코드를 추가해준 후, AppDatabase에
    abstract fun gardenPlantingDao(): GardenPlantingDao를 추가한다
    @Provides
    fun provideGardenPlantingDao(appDatabase: AppDatabase): GardenPlantingDao {
        return appDatabase.gardenPlantingDao()
    }

GardenActivity에서 이어지는 ui, viewmodel 등을 모두 작성한듯 하다.
지금까지는 단순 Build만 했고, 다음부터는 Run시키며 문제점 수정 및 추가 진행 해보도록 하자

0개의 댓글