Android Jetpack

책 검색 앱 만들기(3)- Android App Architecture 적용하기

wdadaww 2023. 1. 19. 02:23
  • AAC란?

 -  구글에서  AAC(Android Architecture Components) 라는 MVVM 패턴을 간편하게 적용할 수 있도록 테스트와 유지보수가 쉬운 앱을 디자인할 수 있도록 돕는 라이브러리를 제공한다.


->  구글이  MVVM 패턴을 변형하여 새로 만들어낸 구글 앱 아키텍처 패턴입니다.

 


Repository Pattern이란?

데이터 출처(로컬DB인지 API응답인지 등)와 관계 없이 동일 인터페이스로 데이터에 접근할 수 있도록 만드는 것을 Repository 패턴이라고 합니다.

레포지토리는 데이터 소스에 접근하는 데 필요한 논리를 캡슐화 하는 클래스 또는 구성 요소입니다.

 

Respository 클래스에서 담당하는 작업

  • 데이터 로직을 분리시킬 수 있다.
  • 데이터 변경사항을 한 곳에 집중
  • 여러 데이터 소스 간의 충돌 해결
  • 앱의 나머지 부분에 데이터 노출
  • 앱의 나머지 부분에서 데이터 소스 추상화

안드로이드에서의 Repository 패턴

View -> Presenter / Viewmodel -> Repository -> DataSource(API, LocalDB)

Repository는 ViewModel or Presenter가 요청하는 데이터를 로컬DB(Room) 또는 서버(Retrofit)로부터 가져와 전달해준다. 이를 통해 ViewModel은 누구한테 가져온 데이터인지(Local DB인지, 서버인지)에 대해 신경쓸 필요가 없어진다.

즉, ViewModel은 자신의 비즈니스 로직에만 집중하면 된다.

Room인지, Retrofit을 통한 http서비스를 통해서인지 등을  데이터의 출처를 ViewModel은 전혀 신경쓰지 않아도 된다. Repository가 처리해주기 때문이다.

단점

  • 추상레이어가 하나 추가되는 것이므로 관리해야 할 코드와 파일들이 많아진다

 


뷰모델로부터 서버랑 데이터를 가져와 두 사이를 전달해 줄 Repository를 추가할 것이다.  이를 위해 Interface로 된 Repository와 이를 구체화 할 RepositoryImpl을 만든다.

package com.example.kakaobook.ui.view.repository

import com.example.kakaobook.ui.view.data.model.SearchResponse
import retrofit2.Response

interface BookSearchRepository {

    suspend fun searchBooks(
        query: String,
        sort: String,
        page: String,
        size: Int,
    ): Response<SearchResponse>
}
package com.example.kakaobook.ui.view.repository

import com.example.kakaobook.ui.view.data.api.RetrofitInstance.api
import com.example.kakaobook.ui.view.data.model.SearchResponse
import retrofit2.Response

class BookSearchRepositoryImpl: BookSearchRepository{
    override suspend fun searchBooks(
        query: String,
        sort: String,
        page: String,
        size: Int
    ): Response<SearchResponse> {
       return api.searchBooks(query, sort, page, size)
    }

}

 

  •    ViewModel, 코루틴,Livedata를 쓰기위해 의존성을 추가한다.

 

 

수명 주기 인식 구성요소와 함께 Kotlin 코루틴 사용  |  Android 개발자  |  Android Developers

수명 주기 인식 구성요소와 함께 Kotlin 코루틴 사용 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. Kotlin 코루틴은 비동기 코드를 작성할 수 있게 하는 API를

developer.android.com


//Lifecycle
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1'
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.5.1'

//Coroutine
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4"
//Livedata
implementation  'androidx.lifecycle:lifecycle-livedata-ktx:2.5.1'

  • ViewModel 과 Coruoutines
package com.example.kakaobook.ui.view.ui.view.viewmodel

import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.example.kakaobook.ui.view.data.model.SearchResponse
import com.example.kakaobook.ui.view.repository.BookSearchRepository
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch


class BookSearchViewmodel(
    private val bookSearchRepository: BookSearchRepository
) : ViewModel() {

    //Api
    private val _searchReusult = MutableLiveData<SearchResponse>()
    val searchReuslt: LiveData<SearchResponse> get() = _searchReusult


    //코루틴 사용으로 네트워킹이나 내부 DB 접근 등 백그라운드에서 필요한 작업을 용이하게함.
    fun searchBooks(query: String) = viewModelScope.launch(Dispatchers.IO) {
        val response = bookSearchRepository.searchBooks(query, "accuracy",1 ,10)
           if(response.isSuccessful) {
               response.body()?.let {
                   _searchReusult.postValue(it)
               }
           }
    }

}
  • 코루틴(Coroutines)이란?

-> 코루틴은 비동기적으로 실행되는 코드를 간소화하기 위해 Android에서 사용할 수 있는 동시 실행 설계 패턴입니다. 


  • Repository 클래스의 네트워크 요청 방식을 확인하고, SearchBooks 함수에서 코루틴 코드를 분석해보겟습니다.  

-  Repository 클래스에 있는 searchbooks suspend 함수이므로 코루틴은 여전히 필요하며, 모든 suspend 함수는 코루틴 스코프 안에서 실행되어야 합니다  response에 body()가 null이 아니라면 let 블록이 실행 되면서 _searchResult가 MutableLivedata 가변 객체 이므로  postvalue()을 통해 값을 변경할 경우 비동기적으로 처리 되어 백그라운드 스레드에서 작업이 일어난다.


- viewModelScope는 ViewModel KTX 라이브러리에 포함된 '코루틴 스코프'입니다.

 

-'코루틴 스코프'는 하나 이상의 관련 코루틴을 관리합니다.

 

-viewModelScope함수로 Viewmodel에 코루틴 을 실행하기가 더쉬워집니다.

 

- launch는 코루틴을 만들고 함수 본문의 실행을 해당 Dispatcher에 전달하는 함수입니다.

  

- Dispatchers.IO는 이 코루틴을  I/O 작업용으로 예약된 스레드에서 실행해야 함을 나타냅니다.

 

->>>모든 코루틴은 코루틴 스코프 범위 내에서 실행해야 합니다.

 

 

Android의 Kotlin 코루틴  |  Android 개발자  |  Android Developers

Android의 Kotlin 코루틴 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. 코루틴은 비동기적으로 실행되는 코드를 간소화하기 위해 Android에서 사용할 수 있는 동

developer.android.com

출처: Android의 Kotlin 코루틴  |  Android 개발자  |  Android Developers



-Dispatchers.Main

안드로이드 UI 스레드에서 코루틴을 실행하는 Dispatcher

이 Dispatcher는 UI와 상호작용하는 작업을 실행하기 위해서만 사용해야 한다.

-Dispatchers.IO

디스크 또는 네트워크I/O(Input/Output) 작업을 실행하는 데 최적화되어 있는 디스패처


-Dispatchers.Default

CPU를 많이 사용하는 작업을 UI 스레드 외부에서 실행하도록 최적화 되어있는 Dispatcher.

정렬 작업이나 JSON 파싱 작업 등에 최적화 되어있다.

 

 

package com.example.kakaobook.ui.view.ui.view.viewmodel

import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import com.example.kakaobook.ui.view.repository.BookSearchRepository

@Suppress("UNCHECKED_CAST")
class BookSearchViewModelProvideFactory(
    private val bookSearchRepository: BookSearchRepository
) : ViewModelProvider.Factory {
    override fun <T : ViewModel> create(modelClass: Class<T>): T {
        if (modelClass.isAssignableFrom(BookSearchViewmodel::class.java)) {
              return BookSearchViewmodel(bookSearchRepository) as T
            }
        throw IllegalArgumentException("ViewModel Class Not Found")
    }

}


-> 뷰모델에서는 초기값을 전달받을수없기때문에 팩토리클래스를 생성합니다.

 isAssignbleFrom에서 booksearchviewmodel이 맞으면  booksearchRepository를 담아서 viewmodel로 반환해주기 때문에 팩토리를 통해서 뷰모델이 만들어집니다.

 

  • View ( 액티비티) 
  • 뷰모델을 뷰에다가 초기화시켜주도록한다,
package com.example.kakaobook.ui.view.ui.view

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import com.example.kakaobook.R
import com.example.kakaobook.databinding.ActivityMainBinding
import com.example.kakaobook.ui.view.repository.BookSearchRepositoryImpl
import com.example.kakaobook.ui.view.ui.view.viewmodel.BookSearchViewModelProvideFactory
import com.example.kakaobook.ui.view.ui.view.viewmodel.BookSearchViewmodel

class MainActivity : AppCompatActivity() {
    private lateinit var binding: ActivityMainBinding
    private lateinit var fragmentSettingsFrgment: SettingsFragment
    private lateinit var fragmentFavoriteFrgment: FavoriteFragment
    private lateinit var fragmentSearchFragment: SearchFragment
    private lateinit var bookSearchViewmodel: BookSearchViewmodel

    override fun onCreate(savedInstanceState: Bundle?) {


        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        fragmentFavoriteFrgment = FavoriteFragment()
        fragmentSettingsFrgment = SettingsFragment()
        fragmentSearchFragment = SearchFragment()

        //처음 실행했을떄 화면을 보여준다.
        replaceFragment(fragmentSearchFragment)
        BottomNavigationMenuView()

        val bookSearchRepository = BookSearchRepositoryImpl()
        val factory = BookSearchViewModelProvideFactory(bookSearchRepository)
        //뷰모델 적용
        bookSearchViewmodel = ViewModelProvider(this,factory)[BookSearchViewmodel::class.java]
    }
    private fun BottomNavigationMenuView() {
        binding.bottomNavigationMemu.setOnItemSelectedListener {
            when (it.itemId) {
                R.id.fragment_search -> replaceFragment(fragmentSearchFragment)
                R.id.fragment_favorite -> replaceFragment(fragmentFavoriteFrgment)
                R.id.fragment_settings -> replaceFragment(fragmentSearchFragment)
            }
            true
        }
    }
    //프래그먼트 동적제어
    private fun replaceFragment(fragment: Fragment) {
        supportFragmentManager.beginTransaction()
            .apply {
                replace(binding.FrameLayout.id, fragment)
                    .commit()
            }
    }

}