Computer >> 컴퓨터 >  >> 체계 >> Android

아키텍처에 관한 모든 것:다양한 아키텍처 패턴 탐색 및 앱에서 사용하는 방법

Kriptofolio 앱 시리즈 - 3부

새로운 앱 빌드를 시작할 때 가장 중점을 두는 것은 아키텍처입니다. 당신이 저지를 수 있는 가장 큰 실수는 아키텍처 스타일을 전혀 사용하지 않는 것입니다.

아키텍처 선택에 대한 주제는 최근 몇 년 동안 Android 커뮤니티에서 상당히 논쟁의 여지가 있습니다. Google도 참여하기로 결정했습니다. 2017년에는 Android Architecture Components를 출시하여 표준화된 아키텍처에 대한 자체 접근 방식을 제안했습니다. 개발자의 삶을 더 쉽게 만들기 위한 것이었습니다.

이 게시물에서는 먼저 앱을 설계해야 하는 이유에 대해 논의할 것입니다. 우리는 어떤 옵션이 있는지 다룰 것입니다. 그런 다음 우리는 그 방법을 배울 것입니다. 바퀴를 재발명하는 대신 Android 팀에서 제공하는 지침을 사용합니다.

이 글은 지식이 부족한 저에게 가장 어려운 글이었습니다. 먼저 큰 그림을 보기 위해 건축이라는 주제를 정말 잘 공부해야 했습니다. 이제 제 연구 결과를 여러분과 공유할 준비가 되었습니다.

시리즈 콘텐츠

  • 소개:2018–2019년 최신 Android 앱을 구축하기 위한 로드맵
  • 1부:SOLID 원칙 소개
  • 2부:Android 앱 빌드 시작 방법:목업, UI 및 XML 레이아웃 만들기
  • 3부:아키텍처에 관한 모든 것:다양한 아키텍처 패턴 탐색 및 앱에서 이를 사용하는 방법(현재 위치)
  • 4부:Dagger 2를 사용하여 앱에서 종속성 주입을 구현하는 방법
  • 5부:Retrofit, OkHttp, Gson, Glide 및 Coroutine을 사용하여 RESTful 웹 서비스 처리

앱 아키텍처에 관심을 가져야 하는 이유

일반적으로 Android 작업을 시작하면 대부분의 핵심 비즈니스 로직을 액티비티나 프래그먼트로 작성하게 됩니다. 이것은 나를 포함한 모든 새로운 Android 개발자에게 발생합니다. 모든 짧은 튜토리얼과 모든 샘플은 그렇게 할 것을 제안합니다. 그리고 실제로 설명을 위해 만든 작은 앱의 경우 충분합니다.

그러나 사용자의 요구에 따라 끊임없이 변화하고 새로운 기능으로 확장하는 실제 앱에서 그렇게 해보세요. 곧 코딩 경험이 점점 더 고통스러워지고 있음을 알게 될 것입니다. 모든 것은 활동이나 파편과 같은 소위 "신 클래스"에 의해 관리됩니다. 코드 줄이 너무 많아서 쉽게 길을 잃습니다.

기본적으로 모든 코드는 모든 것이 뒤섞인 스파게티처럼 보이기 시작합니다. 모든 부품은 서로 의존합니다. 그런 다음 비즈니스에서 새로운 변경이 필요할 때 전체 프로젝트를 재구축하는 것 외에는 선택의 여지가 없습니다. 또한 아키텍처 질문이 나타나기 시작하는 지점이기도 합니다.

코드를 구성하는 더 좋은 방법이 있습니까?

물론 있습니다! 고품질 코드의 핵심은 SOLID 원칙을 따르는 것입니다. 나는 이전 게시물에서 이것에 대해 이야기했습니다(이유 없이는 아님). 또한 몇 가지 아키텍처 패턴을 관심사 분리에 적용해야 합니다. 사실, 관심사의 분리가 궁극적인 목표여야 합니다. 코드 품질을 나타내는 가장 중요한 포인트입니다. 앱 아키텍처에는 몇 가지 패턴이 있습니다. 가장 잘 알려진 것은 다음과 같은 고전적인 3계층 아키텍처입니다.

  • MVC:모델-뷰-컨트롤러
  • MVP:모델-뷰-발표자
  • MVVM:모델-뷰-뷰 모델

이 모든 패턴은 서로 다른 일반 레이어에 의해 분리되는 방식으로 프로젝트의 코드를 구조화하는 주요 유사한 아이디어를 나타냅니다. 모든 계층에는 고유한 책임이 있습니다. 이것이 바로 프로젝트가 모듈화되는 이유입니다. 분리된 코드 부분은 더 테스트하기 쉽고 앱은 지속적인 변경에 대해 충분히 유연합니다.

각 패턴에 대해 개별적으로 이야기하면 주제가 너무 광범위해집니다. 주요 차이점을 이해할 수 있도록 각각에 대해서만 소개하겠습니다.

모델-뷰-컨트롤러(MVC) 패턴

이 패턴은 예전에 Android 앱 아키텍처가 처음으로 반복한 것입니다. 코드를 3개의 다른 레이어로 분리하는 것이 좋습니다.

모델 — 데이터 계층. 비즈니스 로직을 처리하고 네트워크 및 데이터베이스 계층과의 통신을 담당합니다.

보기 — 사용자 인터페이스(UI) 계층입니다. 모델의 데이터를 간단하게 시각화한 것입니다.

컨트롤러 — 로직 계층, 사용자 행동에 대한 알림을 받고 필요에 따라 모델을 업데이트합니다.

아키텍처에 관한 모든 것:다양한 아키텍처 패턴 탐색 및 앱에서 사용하는 방법

이것은 MVC 스키마입니다. 그것에서 우리는 컨트롤러와 뷰가 모두 모델에 의존한다는 것을 알 수 있습니다. 컨트롤러가 데이터를 업데이트합니다. 보기는 데이터를 가져옵니다. 그러나 Model은 분리되어 UI와 독립적으로 테스트할 수 있습니다.

MVC 패턴을 적용하는 방법에는 몇 가지 접근 방식이 있습니다. 상당히 혼란스럽습니다.

하나는 액티비티와 프래그먼트가 컨트롤러처럼 작동하는 경우입니다. 데이터 처리 및 보기 업데이트를 담당합니다. 이 아키텍처 접근 방식의 문제는 활동과 조각이 상당히 커져 테스트하기가 매우 어려울 수 있다는 것입니다.

더 논리적이고 정확해 보이는 또 다른 접근 방식은 활동과 조각이 MVC 세계에서 보기여야 하는 위치입니다. 컨트롤러는 Android 클래스를 확장하거나 사용하지 않는 별도의 클래스여야 합니다. 모델도 마찬가지입니다.

어쨌든 MVC에 대해 더 자세히 조사하면 Android 프로젝트에 적용할 때 올바른 방식으로 코드 레이어가 서로 의존한다는 것을 알게 될 것입니다. 그렇기 때문에 차기 Android 앱에서는 더 이상 이 앱을 사용하지 않는 것이 좋습니다.

Model-View-Presenter(MVP) 패턴

작동하지 않는 첫 번째 접근 방식 이후, ​​Android 개발자는 계속해서 가장 인기 있는 아키텍처 패턴 중 하나인 MVP를 사용하려고 했습니다. 이 패턴은 아키텍처 선택의 두 번째 반복을 나타냅니다. 이 패턴은 널리 사용되었으며 여전히 권장되는 패턴입니다. Android 개발을 시작하는 사람이라면 누구나 쉽게 배울 수 있습니다. 3개의 개별 레이어 역할을 살펴보겠습니다.

모델 — MVC 패턴과 동일한 데이터 계층입니다. 비즈니스 로직을 처리하고 네트워크 및 데이터베이스 계층과의 통신을 담당합니다.

보기 — 사용자 인터페이스(UI) 계층입니다. 데이터를 표시하고 발표자에게 사용자 작업에 대해 알립니다.

발표자 — 모델에서 데이터를 검색하고, UI 논리를 적용하고, 보기의 상태를 관리하고, 표시할 항목을 결정하고, 보기의 사용자 입력 알림에 반응합니다. 이것은 View에 전혀 연결되지 않고 단지 인터페이스라는 점을 제외하고는 본질적으로 MVC의 컨트롤러입니다.

아키텍처에 관한 모든 것:다양한 아키텍처 패턴 탐색 및 앱에서 사용하는 방법

MVP 스키마는 View와 Presenter가 밀접하게 관련되어 있음을 보여줍니다. 서로에 대한 참조가 있어야 합니다. 이들의 관계는 Contract에 정의되어 있습니다. 인터페이스 클래스.

이 패턴에는 한 가지 중요하지만 제어 가능한 단점이 있습니다. 프리젠터는 주의를 기울이지 않고 단일 책임 원칙에 따라 코드를 손상시키지 않으면 모든 것을 알고 있는 거대한 클래스로 확장되는 경향이 있습니다. 그러나 일반적으로 말해서 MVP 패턴은 문제를 매우 잘 구분합니다. 프로젝트의 주요 선택이 될 수 있습니다.

MVVM(Model-View-ViewModel) 패턴

MVVM 패턴은 접근 방식의 세 번째 반복입니다. Android Architecture Components 출시와 함께 Android 팀에서 권장하는 아키텍처 패턴이 되었습니다. 그렇기 때문에 우리는 무엇보다도 이 패턴을 배우는 데 집중할 것입니다. 또한 "My Crypto Coins" 앱에도 사용할 것입니다. 이전과 마찬가지로 별도의 코드 레이어를 살펴보겠습니다.

모델 — 데이터 소스를 추상화합니다. ViewModel은 모델과 함께 작동하여 데이터를 가져오고 저장합니다.

View — 사용자의 작업에 대해 ViewModel에 알립니다.

ViewModel — 보기와 관련된 데이터 스트림을 노출합니다.

MVP 패턴과의 차이점은 MVVM에서 ViewModel은 Presenter와 마찬가지로 View에 대한 참조를 보유하지 않는다는 것입니다. MVVM에서 ViewModel은 다양한 View가 바인딩할 수 있는 이벤트 스트림을 노출합니다. 반면에 MVP의 경우 Presenter는 View에 표시할 내용을 직접 알려줍니다. MVVM 스키마를 살펴보겠습니다.

아키텍처에 관한 모든 것:다양한 아키텍처 패턴 탐색 및 앱에서 사용하는 방법

MVVM에서 View에는 ViewModel에 대한 참조가 있습니다. ViewModel에는 View에 대한 정보가 없습니다. View와 ViewModel 사이에는 다대일 관계가 있습니다.

MVC 대 MVP 대 MVVM 비교

다음은 제가 이야기한 모든 패턴을 요약한 표입니다.

아키텍처에 관한 모든 것:다양한 아키텍처 패턴 탐색 및 앱에서 사용하는 방법

눈치채셨겠지만 MVC는 모듈식의 테스트 가능한 최신 앱을 빌드할 때 MVP 및 MVVM에 비해 그다지 좋지 않습니다. 그러나 각 패턴에는 고유한 장점과 단점이 있습니다. 귀하의 요구 사항에 정확히 맞는 경우 좋은 선택입니다. 가치가 있으므로 이러한 모든 패턴에 대해 조사하고 자세히 알아보는 것이 좋습니다.

그동안 저는 Google에서도 추진하는 2018년 트렌드 패턴인 MVVM으로 프로젝트를 계속할 것입니다.

Android 아키텍처 구성요소

Android 애플리케이션 수명 주기에 익숙하다면 일반적으로 구성 변경 중에 나타나는 모든 데이터 흐름 문제와 지속성 및 안정성 문제를 피하는 앱을 빌드하는 것이 얼마나 골치 아픈 일인지 알 것입니다.

2017년에 Android 팀은 우리가 충분히 고군분투했다고 결정했습니다. 그들은 책임을 지고 Android Architecture Components 프레임워크를 도입했습니다. 이렇게 하면 코드를 복잡하게 하거나 해킹을 적용하지 않고도 이러한 모든 문제를 해결할 수 있습니다.

Android 아키텍처 구성요소는 강력하고 테스트 가능하며 유지보수 가능한 앱을 설계하는 데 도움이 되는 라이브러리 모음입니다. 이 블로그 게시물을 작성하는 현재 시점에서 다음 구성 요소로 구성됩니다.

  • 데이터 바인딩 — 관찰 가능한 데이터를 UI 요소에 선언적으로 바인딩
  • 수명 주기 — 활동 및 조각 수명 주기 관리
  • LiveData — 기본 데이터베이스 변경 시 보기에 알림
  • 탐색 — 인앱 탐색에 필요한 모든 것을 처리합니다.
  • 페이징 — 데이터 소스에서 요청에 따라 점진적으로 정보 로드
  • 방 — 유창한 SQLite 데이터베이스 액세스
  • ViewModel — 수명 주기를 고려한 방식으로 UI 관련 데이터 관리
  • WorkManager — Android 백그라운드 작업 관리

Android 아키텍처 구성 요소의 도움으로 다음 다이어그램에 따라 My Crypto Coins 앱에서 MVVM 아키텍처 패턴을 구현할 것입니다.

아키텍처에 관한 모든 것:다양한 아키텍처 패턴 탐색 및 앱에서 사용하는 방법

Google에서 권장하는 아키텍처입니다. 모든 모듈이 서로 어떻게 상호 작용해야 하는지 보여줍니다. 다음으로 우리 프로젝트에서 사용할 특정 Android 아키텍처 구성요소만 다룰 것입니다.

소스 파일 구성

개발을 시작하기 전에 프로젝트의 소스 파일을 구성하는 방법을 고려해야 합니다. 나중에 이해하고 수정하기 어려운 지저분한 구조를 갖게 되므로 이 질문에 답을 남겨둘 수 없습니다.

여러 가지 방법이 있습니다. 하나는 구성 요소 범주별로 구성하는 것입니다. 예를 들어 모든 활동은 자체 폴더로 이동하고 모든 어댑터는 해당 폴더로 이동하는 식입니다.

또 다른 방법은 앱 기능별로 모든 것을 정리하는 것입니다. 예를 들어 모든 암호 화폐 목록 기능에서 암호 검색 및 추가는 자체 addsearchlist로 이동합니다. 폴더. 주요 아이디어는 모든 것을 무작위로 배치하는 대신 특정 방식으로 수행해야 한다는 것입니다. 저는 이 두 가지를 섞어서 사용합니다.

아키텍처에 관한 모든 것:다양한 아키텍처 패턴 탐색 및 앱에서 사용하는 방법
My Crypto Coins 앱 폴더 구조

프로젝트의 폴더 구조 외에도 프로젝트 파일 이름 지정에 몇 가지 규칙을 적용하는 것을 고려해야 합니다. 예를 들어 Android 클래스의 이름을 지정할 때 이름에 클래스 목적을 명확하게 정의해야 합니다.

뷰 모델

앱 아키텍처 개발을 시작하기 위해 먼저 ViewModel을 만들 것입니다. 뷰 모델은 UI 구성 요소에 대한 데이터를 제공하고 구성 변경 후에도 살아남는 개체입니다.

ViewModel을 사용하여 액티비티 또는 프래그먼트의 전체 수명 주기에 걸쳐 데이터를 유지할 수 있습니다. 활동과 조각은 수명이 짧은 개체입니다. 사용자가 앱과 상호 작용할 때 자주 생성되고 소멸됩니다. 또한 ViewModel은 데이터 조작 및 지속성뿐만 아니라 네트워크 통신과 관련된 작업을 관리하는 데 더 적합합니다.

예를 들어 이제 MainListFragment에 대한 ViewModel을 만들 수 있습니다. UI 데이터를 분리합니다.

class MainViewModel : ViewModel() {
    ...
}

그런 다음 한 줄의 코드로 ViewModel을 가져옵니다.

class MainListFragment : Fragment() {
    ...
    private lateinit var viewModel: MainViewModel
    ...
    override fun onActivityCreated(savedInstanceState: Bundle?) {

        super.onActivityCreated(savedInstanceState)

        setupList()

        // Obtain ViewModel from ViewModelProviders, using this fragment as LifecycleOwner.
        viewModel = ViewModelProviders.of(this).get(MainViewModel::class.java)
        ...
    }
    ...
}

기본적으로 끝입니다. 축하합니다! ? 계속 진행하겠습니다.

라이브 데이터

LiveData는 관찰 가능한 데이터 홀더 클래스입니다. 관찰자 패턴을 따릅니다. LiveData는 수명 주기를 인식합니다. 즉, 활성 수명 주기 상태에 있는 앱 구성 요소(액티비티, 프래그먼트 등) 관찰자만 업데이트합니다.

LiveData 클래스는 데이터의 최신 값을 반환합니다. 데이터가 변경되면 업데이트된 값을 반환합니다. LiveData는 ViewModel에 가장 적합합니다.

다음과 같이 ViewModel과 함께 LiveData를 사용합니다.

...
class MainViewModel : ViewModel() {

    private val liveData = MutableLiveData<ArrayList<Cryptocurrency>>()
    val data: LiveData<ArrayList<Cryptocurrency>>
        get() = liveData

    init {
        val tempData = ArrayList<Cryptocurrency>()

        val btc:Cryptocurrency = Cryptocurrency("Bitcoin", 1, 0.56822348, "BTC", 8328.77, 4732.60, 0.19, -10.60, 0.44, 20.82)
        val eth:Cryptocurrency = Cryptocurrency("Etherium", 2, 6.0, "ETH", 702.99, 4217.94, 0.13, -7.38, 0.79, 33.32)

        tempData.add(btc)
        tempData.add(eth)

        liveData.value = tempData
    }
}

LiveData로 노출된 ViewModel의 데이터 관찰:

...
class MainListFragment : Fragment() {

    private lateinit var recyclerView: RecyclerView
    private lateinit var recyclerAdapter: MainRecyclerViewAdapter

    private lateinit var viewModel: MainViewModel

    ...

    override fun onActivityCreated(savedInstanceState: Bundle?) {

        super.onActivityCreated(savedInstanceState)

        setupList()

        // Obtain ViewModel from ViewModelProviders, using this fragment as LifecycleOwner.
        viewModel = ViewModelProviders.of(this).get(MainViewModel::class.java)

        // Observe data on the ViewModel, exposed as a LiveData
        viewModel.data.observe(this, Observer { data ->
            // Set the data exposed by the LiveData
            if (data != null) {
                recyclerAdapter.setData(data)
            }
        })
    }
    ...
}

여기에서 히스토리의 이 시점에서 저장소를 찾아보십시오.

데이터 바인딩

데이터 바인딩 라이브러리는 XML 레이아웃에 연결하는 데 필요한 상용구 코드를 제거하기 위해 생성되었습니다.

Kotlin 프로젝트에서 데이터 바인딩을 사용하려면 kapt 컴파일러 플러그인으로 주석 프로세서 지원을 켜야 합니다. 또한 Android 구성 gradle 파일에 데이터 바인딩 블록을 추가합니다.

...
apply plugin: 'kotlin-kapt'

android {
    ...
    dataBinding {
        enabled = true
    }
}
...

데이터 바인딩 생성 클래스를 사용하려면 모든 보기 코드를 <layo에 넣어야 합니다. ut> 태그. 데이터 바인딩의 가장 강력한 개념은 일부 데이터 클래스를 xml 레이아웃에 바인딩하고 항목 속성을 필드에 직접 바인딩할 수 있다는 것입니다.

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

    <data>

        <variable
            name="cryptocurrency"
            type="com.baruckis.mycryptocoins.data.Cryptocurrency" />
    </data>
  
    ...      

            <android.support.v7.widget.AppCompatTextView
                android:id="@+id/item_name"
                style="@style/MainListItemPrimeText"
                android:layout_marginEnd="@dimen/main_cardview_list_item_text_between_margin"
                android:layout_marginStart="@dimen/main_cardview_list_item_inner_margin"
                android:text="@{cryptocurrency.name}"
                android:textAlignment="viewStart"
                app:layout_constraintBottom_toTopOf="@+id/item_amount_symbol"
                app:layout_constraintEnd_toStartOf="@+id/guideline1_percent"
                app:layout_constraintStart_toEndOf="@+id/item_image_icon"
                app:layout_constraintTop_toTopOf="parent"
                app:layout_constraintVertical_chainStyle="spread"
                tools:text="@string/sample_text_item_name" />

     ...
</layout>

데이터 입찰 기능이 있는 RecyclerView 어댑터는 다음과 같습니다.

class MainRecyclerViewAdapter() : RecyclerView.Adapter<MainRecyclerViewAdapter.BindingViewHolder>() {

    private lateinit var dataList: ArrayList<Cryptocurrency>

    fun setData(newDataList: ArrayList<Cryptocurrency>) {
        dataList = newDataList
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingViewHolder {
        val inflater = LayoutInflater.from(parent.context)
        val binding = FragmentMainListItemBinding.inflate(inflater, parent, false)

        return BindingViewHolder(binding)
    }

    override fun onBindViewHolder(holder: BindingViewHolder, position: Int) = holder.bind(dataList[position])

    override fun getItemCount(): Int = dataList.size

    ...

    inner class BindingViewHolder(var binding: FragmentMainListItemBinding) : RecyclerView.ViewHolder(binding.root) {
        fun bind(cryptocurrency: Cryptocurrency) {
            binding.cryptocurrency = cryptocurrency

            binding.itemRanking.text = String.format("${cryptocurrency.rank}")
            ...
            binding.executePendingBindings()
        }
    }
}

마침내 더 이상 findViewById를 작성하지 않습니다. ? 여기에서 히스토리의 이 시점에서 저장소를 찾아보십시오.

우리 앱은 사용자가 보유한 다양한 암호화폐의 영구 데이터를 저장해야 합니다. 이것은 Android 기기 내부에 비공개로 보관되는 로컬 데이터베이스에 저장되어야 합니다.

개인 데이터베이스에 구조화된 데이터를 저장하기 위해 우리는 SQLite 데이터베이스를 사용할 것입니다. 이것은 종종 최선의 선택입니다.

앱용 SQLite 데이터베이스를 생성하기 위해 Room을 사용할 것입니다. Room은 SQLite 위의 래퍼인 Android 팀에서 만든 지속성 라이브러리입니다. SQLite와 상호 작용하는 데 필요한 상용구 코드의 대부분을 제거하는 추상화 계층입니다. 또한 SQL 쿼리의 컴파일 시간 검사를 추가합니다.

그것을 생각하는 가장 좋은 방법은 개체 인스턴스와 데이터베이스의 행 사이를 매핑하기 위해 자동으로 글루 코드를 생성하도록 설계된 ORM(Object Relational Mapper) 도구입니다.

Room에는 기본적으로 3가지 주요 구성 요소가 있습니다.

  1. Entity — 이 구성 요소는 데이터베이스 행을 보유하는 클래스를 나타냅니다. 각 항목에 대해 항목을 보관할 데이터베이스 테이블이 생성됩니다.
  2. DAO(Data Access Object) — 데이터베이스에 액세스하는 방법을 정의하는 주요 구성 요소입니다.
  3. 데이터베이스 — 주석을 사용하여 엔티티 목록, DAO 목록 및 데이터베이스 버전을 정의하고 기본 연결에 대한 기본 액세스 지점 역할을 하는 홀더 클래스인 구성 요소입니다.
아키텍처에 관한 모든 것:다양한 아키텍처 패턴 탐색 및 앱에서 사용하는 방법

다음의 간단한 단계에 따라 My Crypto Coins 앱에서 Room을 설정해 보겠습니다.

  1. 엔티티를 생성합니다.
@Entity
data class Cryptocurrency(val name: String,
                          val rank: Short,
                          val amount: Double,
                          @PrimaryKey
                          val symbol: String,
                          val price: Double,
                          val amountFiat: Double,
                          val pricePercentChange1h: Double,
                          val pricePercentChange7d: Double,
                          val pricePercentChange24h: Double,
                          val amountFiatChange24h: Double)

데이터베이스의 구조에 대해 Room에 알리기 위해 몇 가지 추가 정보를 추가합니다.

2. DAO를 생성합니다.

@Dao
interface MyCryptocurrencyDao {

    @Query("SELECT * FROM Cryptocurrency")
    fun getMyCryptocurrencyLiveDataList(): LiveData<List<Cryptocurrency>>

    @Insert
    fun insertDataToMyCryptocurrencyList(data: List<Cryptocurrency>)
}

먼저 Entity로 만든 테이블에서 레코드를 검색하고 일부 샘플 데이터를 삽입할 수 있는 DAO를 만들 것입니다.

3. 데이터베이스 생성 및 설정.

데이터베이스 인스턴스는 이상적으로는 세션당 한 번만 빌드되어야 한다고 말하는 것이 중요합니다. 이를 달성하는 한 가지 방법은 싱글톤 패턴을 사용하는 것입니다.

@Database(entities = [Cryptocurrency::class], version = 1, exportSchema = false)
abstract class AppDatabase : RoomDatabase() {

    abstract fun myCryptocurrencyDao(): MyCryptocurrencyDao


    // The AppDatabase a singleton to prevent having multiple instances of the database opened at the same time.
    companion object {

        // Marks the JVM backing field of the annotated property as volatile, meaning that writes to this field are immediately made visible to other threads.
        @Volatile
        private var instance: AppDatabase? = null

        // For Singleton instantiation.
        fun getInstance(context: Context): AppDatabase {
            return instance ?: synchronized(this) {
                instance ?: buildDatabase(context).also { instance = it }
            }
        }

        // Creates and pre-populates the database.
        private fun buildDatabase(context: Context): AppDatabase {
            return Room.databaseBuilder(context, AppDatabase::class.java, DATABASE_NAME)
                    // Prepopulate the database after onCreate was called.
                    .addCallback(object : Callback() {
                        override fun onCreate(db: SupportSQLiteDatabase) {
                            super.onCreate(db)
                            // Insert the data on the IO Thread.
                            ioThread {
                                getInstance(context).myCryptocurrencyDao().insertDataToMyCryptocurrencyList(PREPOPULATE_DATA)
                            }
                        }
                    })
                    .build()
        }

        // Sample data.
        val btc: Cryptocurrency = Cryptocurrency("Bitcoin", 1, 0.56822348, "BTC", 8328.77, 4732.60, 0.19, -10.60, 0.44, 20.82)
        val eth: Cryptocurrency = Cryptocurrency("Etherium", 2, 6.0, "ETH", 702.99, 4217.94, 0.13, -7.38, 0.79, 33.32)

        val PREPOPULATE_DATA = listOf(btc, eth)

    }

}
private val IO_EXECUTOR = Executors.newSingleThreadExecutor()

// Utility method to run blocks on a dedicated background thread, used for io/database work.
fun ioThread(f : () -> Unit) {
    IO_EXECUTOR.execute(f)
}

초기 실행에서 볼 수 있듯이 데이터베이스는 테스트 목적으로만 일부 샘플 데이터로 미리 채워집니다.

4. 추가 단계 저장소를 만듭니다.

저장소는 아키텍처 구성 요소 라이브러리의 일부가 아닙니다. 코드 분리 및 아키텍처에 대한 권장 모범 사례입니다.

아키텍처에 관한 모든 것:다양한 아키텍처 패턴 탐색 및 앱에서 사용하는 방법

여러 데이터 소스를 관리해야 하는 경우에 대비하여 모든 앱 데이터에 대한 단일 소스의 역할을 합니다.

class MyCryptocurrencyRepository private constructor(
        private val myCryptocurrencyDao: MyCryptocurrencyDao
) {

    fun getMyCryptocurrencyLiveDataList(): LiveData<List<Cryptocurrency>> {
        return myCryptocurrencyDao.getMyCryptocurrencyLiveDataList()
    }

    companion object {

        // Marks the JVM backing field of the annotated property as volatile, meaning that writes to this field are immediately made visible to other threads.
        @Volatile
        private var instance: MyCryptocurrencyRepository? = null

        // For Singleton instantiation.
        fun getInstance(myCryptocurrencyDao: MyCryptocurrencyDao) =
                instance ?: synchronized(this) {
                    instance
                            ?: MyCryptocurrencyRepository(myCryptocurrencyDao).also { instance = it }
                }
    }
}

ViewModel에서 이 저장소를 사용할 것입니다.

class MainViewModel(myCryptocurrencyRepository: MyCryptocurrencyRepository) : ViewModel() {

    val liveData = myCryptocurrencyRepository.getMyCryptocurrencyLiveDataList()
}

프래그먼트 코드도 진화합니다.

class MainListFragment : Fragment() {

    ...

    private lateinit var viewModel: MainViewModel

    ...

    override fun onActivityCreated(savedInstanceState: Bundle?) {

        super.onActivityCreated(savedInstanceState)

        setupList()
        subscribeUi()
    }

    ...

    private fun subscribeUi() {

        val factory = InjectorUtils.provideMainViewModelFactory(requireContext())
        // Obtain ViewModel from ViewModelProviders, using this fragment as LifecycleOwner.
        viewModel = ViewModelProviders.of(this, factory).get(MainViewModel::class.java)

        // Update the list when the data changes by observing data on the ViewModel, exposed as a LiveData.
        viewModel.liveData.observe(this, Observer<List<Cryptocurrency>> { data ->
            if (data != null && data.isNotEmpty()) {
                emptyListView.visibility = View.GONE
                recyclerView.visibility = View.VISIBLE
                recyclerAdapter.setData(data)
            } else {
                recyclerView.visibility = View.GONE
                emptyListView.visibility = View.VISIBLE
            }
        })

    }

}

이제 ViewModel 클래스에 더 이상 비어 있지 않은 생성자가 있으므로 공급자 팩토리 패턴을 구현해야 합니다. 이것은 ViewModelProviders.of()로 전달됩니다. 메소드를 두 번째 매개변수로 사용합니다.

object InjectorUtils {

    fun provideMainViewModelFactory(
            context: Context
    ): MainViewModelFactory {
        val repository = getMyCryptocurrencyRepository(context)
        return MainViewModelFactory(repository)
    }

    private fun getMyCryptocurrencyRepository(context: Context): MyCryptocurrencyRepository {
        return MyCryptocurrencyRepository.getInstance(
                AppDatabase.getInstance(context).myCryptocurrencyDao())
    }
}
class MainViewModelFactory(private val repository: MyCryptocurrencyRepository) : ViewModelProvider.NewInstanceFactory() {

    override fun <T : ViewModel?> create(modelClass: Class<T>): T {
        return MainViewModel(repository) as T
    }
}

여기에서 히스토리의 이 시점에서 저장소를 찾아보십시오.

최종 생각

이 부분에서 논의한 디자인 아키텍처는 정보에 입각한 지침으로 사용되어야 하지만 엄격한 규칙이 아닙니다. 각 주제에 대해 너무 자세히 설명하고 싶지 않았습니다. Android Architecture Components를 사용하여 코딩 프로세스를 살펴보았습니다. 각 구성 요소에 대해 개별적으로 더 많은 것을 배울 수 있다는 점을 염두에 두십시오. 그렇게 하는 것이 좋습니다.

이미 만들 수 있는 모든 것을 요약해 보겠습니다.

  • My Crypto Coins 앱에서 모든 개별 화면에는 자체 ViewModel이 있습니다. 이렇게 하면 구성 변경을 견뎌내고 데이터 손실로부터 사용자를 보호할 수 있습니다.
  • 앱의 사용자 인터페이스는 반응형입니다. 즉, 백엔드에서 데이터가 변경되면 즉시 업데이트됩니다. 이는 LiveData의 도움으로 이루어집니다.
  • 데이터 바인딩을 사용하여 직접 코드의 변수에 바인딩하므로 프로젝트의 코드가 더 적습니다.
  • 마지막으로 우리 앱은 사용자 데이터를 SQLite 데이터베이스로 기기 내부에 로컬로 저장합니다. 데이터베이스는 Room 컴포넌트로 편리하게 생성되었습니다. 앱의 코드는 기능별로 구성되어 있으며 모든 프로젝트 아키텍처는 Android 팀에서 권장하는 패턴인 MVVM입니다.

저장소

이제 "Kriptofolio"(이전의 "My Crypto Coins") 앱이 실제로 모양을 갖추기 시작했습니다. 이 파트 3에 대한 최신 저장소 커밋을 사용하면 총 보유 포트폴리오 값이 올바르게 계산된 사용자에 대해 미리 채워진 데이터베이스 데이터를 멋지게 표시하는 것을 찾을 수 있습니다.

GitHub에서 소스 보기

아츄! 읽어 주셔서 감사합니다! 저는 원래 2018년 8월 22일 개인 블로그 www.baruckis.com에 이 게시물을 게시했습니다.