[안드로이드 공부]/선플라워 디비보기

[Sunflower 디비보기] Detail View 에서 Garden 으로 아이템 담기 MVVM

코코모아 2020. 7. 29. 21:47

아래는 코모스튜디오가 직접 만든 무료 앱이에요
(한 번만 봐주세요 ^^)

01

02

03

정각알림 만들기(말하는시계)

말하는 시계 (취침, 자전거) 

말하는 타이머 음성 스톱워치 

 My Garden > Plant List > Detail

2개의 탭이 있으며,

좌측에는 나의 정원, 우측에는 식물 리스트가 있다.

여러 종류의 식물들이 있는 식물 리스트에서 식물을 하나 선택하면 그 식물의 Detail View로 간다.

DetailView에서 나의 정원으로 담기를 누를 경우 선택된 식물이 나의 정원에 담기는데, 이 일련의 과정(MVVM)들의 흐름을 알아본다.

 

View

 

layout 은 데이터 바인딩을 사용하고

  • viewModel을 통해 View를 업데이트하고,
  • callback을 등록해서 add 버튼에 바로 동작하게 한다
frament_plant_detail.xml

<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools">

    <data>
        <import type="garden.data.Plant"/>
        <variable
            name="viewModel"
            type="garden.viewmodels.PlantDetailViewModel" />
        <variable
            name="callback"
            type="garden.PlantDetailFragment.Callback" />
    </data>

callback 은 아래 Fab버튼을 눌렀을 때 반응하도록 onClick 리스너에 등록해준다

<com.google.android.material.floatingactionbutton.FloatingActionButton
            android:id="@+id/fab"
            style="@style/Widget.MaterialComponents.FloatingActionButton"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_margin="@dimen/fab_margin"
      //callback 등록
            android:onClick="@{() -> callback.add(viewModel.plant)}"
            android:tint="@android:color/white"
            app:shapeAppearance="@style/ShapeAppearance.Sunflower.FAB"
            app:isFabGone="@{viewModel.isPlanted}"
            app:layout_anchor="@id/appbar"
            app:layout_anchorGravity="bottom|end"
            app:srcCompat="@drawable/ic_plus" />

 

View -담기 버튼의 동작을 위한 작업 

 

ViewModel 생성(참고: ViewMode 주입)과 callback생성

  • Safe Argument로 받아온 plandId로 ViewModel을 생성한다.

DataBinding에 callback을 연결 

PlantDetailFragment.kt

class PlantDetailFragment : Fragment() {

    private val args: PlantDetailFragmentArgs by navArgs()

    private val plantDetailViewModel: PlantDetailViewModel by viewModels {
        InjectorUtils.providePlantDetailViewModelFactory(requireActivity(), args.plantId)
    }

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        val binding = DataBindingUtil.inflate<FragmentPlantDetailBinding>(
            inflater, R.layout.fragment_plant_detail, container, false
        ).apply {
            viewModel = plantDetailViewModel
            lifecycleOwner = viewLifecycleOwner
            //binding에 callback을 연결
            //singtone object를 생성해서 callback으로 연결한다.
            callback = object : Callback {
            //add interface 구현
                override fun add(plant: Plant?) {
                    plant?.let {
                        hideAppBarFab(fab)
                        //ViewModel에서 나의 정원으로 식물을 담는 작업을 시작한다
                        plantDetailViewModel.addPlantToGarden()
                        Snackbar.make(root, R.string.added_plant_to_garden, Snackbar.LENGTH_LONG)
                            .show()
                    }
                }
            }
            
    ...
    
 interface Callback {
    fun add(plant: Plant?)
 }

담기 버튼을 누르면 ViewModle의 addPlantToGarden()이 호출되도록 callBack을 구성하였다.

 

ViewModel -담기 버튼을 누르면

 

뷰 페이저 좌측은 담긴 식물, 우측에는 담을 식물들인데, 좌측 담긴 식물에 담기기 위해서는 정원 데이터 객체를 생성해서 삽입해야 한다.

  • 이 작업은 코루틴 launch(리턴 없음 VS async는  Deferred<T> 객체 리턴/await()) 함수에서 비동기적으로 진행되는데,
    • 이 작업 영역(launch)을 아래 appPLantToGarden() 함수 내에서 viewModelScpoe를 적용하면, ViewModel이 없어질 경우 이 작업을 자동으로 Cancel 되게 할 수도 있다.
class PlantDetailViewModel(
    plantRepository: PlantRepository,
    private val gardenPlantingRepository: GardenPlantingRepository,
    private val plantId: String
) : ViewModel() {

    val isPlanted = gardenPlantingRepository.isPlanted(plantId)
    val plant = plantRepository.getPlant(plantId)

    fun addPlantToGarden() {
        //비동기 코루틴
        viewModelScope.launch {
            gardenPlantingRepository.createGardenPlanting(plantId)
        }
    }
}

 

VM-M 정원 데이터 객체 생성과 삽입

 

Repository에서

  1. 선택된 식물(Plant Table)을 담을 정원 객체를 생성하고(foreginkey로 Plant Table과 연결),
  2. 나의 정원(GardenPlanting Table)에 담기
GardenPlantingRepository.kt
    
suspend fun createGardenPlanting(plantId: String) {
    //1
    val gardenPlanting = GardenPlanting(plantId)
    //2
    gardenPlantingDao.insertGardenPlanting(gardenPlanting)
}

1. GardenPlanting(plantId)

  • 정원(GardenPlanting Table)에 담길 식물의 정원식물 id, 심은 날짜, 마지막으로 물준날의 정보를 현재로 초기화해서 새  Data 객체를 생성
  • 나의 정원에 담을 식물의  Id(GardenPLanting Table)와 식물 리스트의 Id(Plant Table)를 foreignKey에 연결한다.
GardenPlanting.kt

@Entity(
    tableName = "garden_plantings",
    foreignKeys = [
        ForeignKey(entity = Plant::class, parentColumns = ["id"], childColumns = ["plant_id"])
    ],
    indices = [Index("plant_id")]
)
data class GardenPlanting(
    @ColumnInfo(name = "plant_id") val plantId: String,

    /**
     * Indicates when the [Plant] was planted. Used for showing notification when it's time
     * to harvest the plant.
     */
    @ColumnInfo(name = "plant_date") val plantDate: Calendar = Calendar.getInstance(),

    /**
     * Indicates when the [Plant] was last watered. Used for showing notification when it's
     * time to water the plant.
     */
    @ColumnInfo(name = "last_watering_date")
    val lastWateringDate: Calendar = Calendar.getInstance()
) {
        @PrimaryKey(autoGenerate = true)
        @ColumnInfo(name = "id")
        var gardenPlantingId: Long = 0
}
Plant.kt

@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, // how often the plant should be watered, in days
    val imageUrl: String = ""
) {

 

2. gardenPlantingDao.insertGardenPlanting(gardenPalnting)

  • 2. 새로 만든 Data 객체(GardenPlanting)를 Repository에서 실제 Room Database에 삽입한다.
GardenPlantingRepository.kt
 
suspend fun createGardenPlanting(plantId: String) {
   //1. 데이터 객체생성
   val gardenPlanting = GardenPlanting(plantId)
   //2. Room DB에 삽입
   gardenPlantingDao.insertGardenPlanting(gardenPlanting)
}

 

  • @Insert Room Database에 삽입 하기
GardenPlantingDao.kt
  
@Insert
suspend fun insertGardenPlanting(gardenPlanting: GardenPlanting): Long

 

 

Summary

 

View는 ViewModel과 데이터 바인딩으로 연결되며

  • View는 Callback으로 ViewModel과 연결되어 자동으로 Repository와 Model 간에 데이터 관리가 이루어진다.

M-V-VM에서 ViewModel과 Model 간의 연결은 싱글톤 @Volatile  Repository 가 담당을 한다.

  • Repository에서 Dao를 통해 Model에 데이터를 저장하고 꺼내온다

Room Database 관련 작업은 코루틴을 통해 비동기적으로 한다.

  • 완료된 작업은 View의 구독자에게  LiveData 및 Observe로 자동 알림이 가며, 자동으로 업데이트될 수 있다.

 

Android AAC JetPack Sunflower
이 글은 코모가 구글 안드로이드 Sunflower디비보기 한 것입니다.

 

모든 게시물은 코모스튜디오의 소유이며, 무단 복제 수정은 절대 불가입니다.
퍼가실 경우 댓글과 블로그 주소를 남기고 해당 게시물에 출처를 명확히 밝히세요.