SunFlower 본격 시작!!

disun.sj·2023년 6월 11일
0

Sunflower Copy

목록 보기
2/4

카피 프로젝트 링크

저번에는 gradle을 건드렸으니 이번엔 본격적으로 화면이다!

  • Application class를 만들어 준 후 @HiltAndroidApp 어노테이션 추가로 의존성 주입의 시작점을 지정하고 Application의 생명주기를 따르며, 컴파일 단계에서 DI에 필요한 구성요소들을 초기화
@HiltAndroidApp
class MyApplication :Application(){
}

Garden Activity

  • Activity를 만지려는데 studio에서 생성해준 Activity가 ComponentActivity를 상속받고 있어서 찾아봤다
  • AppCompatActivity는 FragmentActivity를 상속받고 Fragment Activity는 ComponentActivity를 상속받는다 라고 한다
  • AppCompatActivity를 사용하면 될 것 같다
  • com.google.android.material:material 을 dependencies에 추가해주자(version "1.9.0" 사용)
  • 그 후 @AndroidEntryPoint 어노테이션 추가
@AndroidEntryPoint
class GardenActivity : AppCompatActivity() {...}
  • onCreate부터 만들어 화면 구성의 틀을 잡아주자
  • WindowCompat.setDecorFitsSystemWindows(window, false) 가 onCreate에 있길래 뭔지 몰라서 찾아봤다
  • 앱의 레이아웃이 시스템 창(예: 상태 표시줄이나 내비게이션 바)과 충돌하지 않도록 조정한다고 한다
  • MdcTheme를 사용하고 있기에 "com.google.accompanist:accompanist-themeadapter-material"을 gradle에 추가해준다(version "0.28.0" 사용)

SunflowerApp

  • Composable Annotation이 붙은 SunflowerApp을 사용하고 있기에 SunflowerApp.kt를 만들어준후 fun SunflowerApp을 만들어 준다
  • @Composable은 멱등성의 보장, 위치 메모이제이션 활성화, 데이터 제공 또는 컴포저블 방출 3가지의 역할을 가진다
  • 이보다 자세한 내용의 경우 추후 Compose에 대해 정리할 예정이다
@Composable
fun SunflowerApp(
    onPageChange: (SunflowerPage) -> Unit = {},
    onAttached: (Toolbar) -> Unit = {},
    plantListViewModel: PlantListViewModel = hiltViewModel(),
) {
    val navController = rememberNavController()
    SunFlowerNavHost(
        plantListViewModel = plantListViewModel,
        navController = navController,
        onPageChange = onPageChange,
        onAttached = onAttached
    )
 }

SunflowerPage

  • SunflowerPage가 없기에 관련 파일 만든 후 SunflowerPage를 만든다
enum class SunflowerPage(
    @StringRes val titleResId : Int,
    @DrawableRes val drawableResId : Int
){
    MY_GARDEN(R.string.my_garden_title, R.drawable.ic_my_garden_active),
    PLANT_LIST(R.string.plant_list_title, R.drawable.ic_plant_list_active)
}
  • StringRes, DrawableRes Annotations은 res 폴더 아래에 정의되는 모든 res가 정말 정의되어 있는 res인지, 그리고 String resource를 정말 불러오고 있는지를 미리 검증할 수 있는 Resource Annotations
  • string res에 my_garden_title와 plant_list_title을 drawable res 에 ic_my_garden_active와 ic_plant_list_active를 추가해준다
  • Sunflower에선 string res에 translation_description가 존재하지만 지금은 필요 없어보여 제외했다

PlantListViewModel

  • 다시 SunflowerApp로 돌아와서 PlantListViewModel ViewModel이 없기에 만들어주고, Hilt를 사용하기 위해 @HiltViewModel와 @Inject internal constructor을 같이 선언해준다
@HiltViewModel
class PlantListViewModel @Inject internal constructor(
    plantRepository: PlantRepository,
    private val savedStateHandle:SavedStateHandle
):ViewModel(){
}

PlantRepository

  • PlantRepository도 같이 만들어 주면서 @Singleton와 @Inject constructor 같이 선언해준다(Hilt를 위해)
@Singleton
class PlantRepository @Inject constructor(private val plantDao : PlantDao){...}

PlantDao

  • PlantDao를 만들어준다
  • 이때 Annotations을 사용하는데 androidx.room에 속해있기에 관련 gradle을 추가해준다
  • "androidx.room:room-compiler", "androidx.room:room-ktx", "androidx.room:room-runtime" 3가지가 존재하기에 Bundle로 묶어준후 gradle에 선언해줬다(version "2.5.0" 사용)
  • Query, room에 대해서는 후에 별도로 작성할 예정, 여기서는 android 자체적으로 database를 구축하는 거다라고 생각하고 넘어가도록 하자
@Dao
interface PlantDao {
    @Query("SELECT * FROM plants ORDER BY name")
    fun getPlants(): Flow<List<Plant>>
    
    @Query("SELECT * FROM plants WHERE growZoneNumber = :growZoneNumber ORDER BY name")
    fun getPlantsWithGrowZoneNumber(growZoneNumber:Int): Flow<List<Plant>>

    @Query("SELECT * FROM plants WHERE id = :plantId")
    fun getPlant(plantId: String): Flow<Plant>

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insertAll(plants: List<Plant>)
}

Plant

  • 만들고 보니 Plant가 필요해서 만들어 주도록 하자
@Entity(tableName = "plants")
data class Plant(
    @PrimaryKey @ColumnInfo(name = "id") val plantId: String,
    val name: String,
    val description:String,
    val growZoneNumber : Int,
    val wateringInterval : Int = 7,
    val imageUrl : String = ""
){
    fun shouldBeWatered(since : Calendar, lastWateringDate : Calendar) = since > lastWateringDate.apply { add(DAY_OF_YEAR, wateringInterval) }
    override fun toString() = name    
}

PlantRepository 내부 코드

  • 다시 PlantRepository로 돌아와 안에 내용을 작성한다
fun getPlants() = plantDao.getPlants()
    fun getPlant(plantId: String) = plantDao.getPlant(plantId)
    fun getPlantsWithGrowZoneNumber(growZoneNumber: Int) =
        plantDao.getPlantsWithGrowZoneNumber(growZoneNumber)
    companion object {
        @Volatile private var instance: PlantRepository? = null

        fun getInstance(plantDao: PlantDao) =
            instance ?: synchronized(this) {
                instance ?: PlantRepository(plantDao).also { instance = it }
            }
    }

SunFlowerNavHost

  • SunflowerApp으로 돌아와 SunFlowerNavHost가 없으니 SunflowerApp.kt에 만들어 주도록 하자
@Composable
fun SunFlowerNavHost(
    navController: NavHostController,
    onPageChange: (SunflowerPage) -> Unit = {},
    onAttached: (Toolbar) -> Unit = {},
    plantListViewModel: PlantListViewModel = hiltViewModel(),
) {
    val activity = (LocalContext.current as Activity)
    NavHost(navController = navController, startDestination = "home") {
        composable("home") {}
    }
}
  • compose의 navigator은 이미 선언해 놨기때문에 넘어가도록 한다(기존 navigator와의 차이점은 사용하며 익혀보도록 하자, 추후 별도 정리 예정)
  • NavHost(navController = navController, startDestination = "home"){} 을 통해 NavHost를 만들어주며 시작경로를 "home"으로 설정해준다
  • 경로의 경우 composable()을 통해 추가해준다
  • "home"에 대한 자세한 내용은 추후 작성하기로 하고 다시 Activity로 돌아와서 onCreate의 setContent를 마저 작성한다

Garden Activity onCreate

setContent {
            MdcTheme {
                SunflowerApp(
                    onAttached = { toolbar ->
                        setSupportActionBar(toolbar)
                    },
                    onPageChange = { page ->
                        when (page) {
                            SunflowerPage.MY_GARDEN -> removeMenuProvider(menuProvider)
                            SunflowerPage.PLANT_LIST -> addMenuProvider(menuProvider, this)
                        }
                    },
                    plantListViewModel = viewModel,
                )
            }
        }
  • private val viewModel: PlantListViewModel by viewModels() 를 Activity에 추가해준다
  • MenuProvider는 app bar에 메뉴 아이템을 보여주기 위해 필요한 인터페이스라고 한다
  • MenuProvider를 만들어 주도록하자
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
            }
        }
    }
  • menu_plant_list를 res의 Menu에 만들어준다
  • PlantListViewModel에 updateData()를 만들어줘야 하고 관련코드도 같이 추가해준다

PlantListViewModel 코드 추가

    private val growZone: MutableStateFlow<Int> = MutableStateFlow(
        savedStateHandle[GROW_ZONE_SAVED_STATE_KEY] ?: NO_GROW_ZONE
    )

    companion object {
        private const val NO_GROW_ZONE = -1
        private const val GROW_ZONE_SAVED_STATE_KEY = "GROW_ZONE_SAVED_STATE_KEY"
    }

    fun updateData() {
        if (isFiltered()) {
            clearGrowZoneNumber()
        } else {
            setGrowZoneNumber(9)
        }
    }

    private fun isFiltered() = growZone.value != NO_GROW_ZONE

    private fun setGrowZoneNumber(num: Int) {
        growZone.value = num
    }

    private fun clearGrowZoneNumber() {
        growZone.value = NO_GROW_ZONE
    }

이로써 GardenActivity 코드는 모두 작성하였고, 이와 연결된 코드들을 일부 구현하였다
Build시 현재 PlantDao쪽에서 inject와 관련된 에러가 발생하고 있다.
다음 작업시에는 이부분을 수정해보도록 하자

0개의 댓글