안드로이드/정리(Android)

Android - MVVM + ROOM + RETROFIT 활용 앱

김염인 2022. 4. 26. 21:25

ㅁ Tech Stack

  • Retrofit2
  • Room
  • Mvvm(AAC)
  • RecyclerView
  • Corutine
  • webview

이 앱은 MVVM 패턴 구조와 비동기 처리를 이해하기 위해 구현을 해 보았으며 Room을 이용하여 좋아요 표시한 게시물을 앱을 껏다 키더라도 좋아요 표시가 사라지지 않게 유지하였습니다.

 

1. Retrofit Api Cal

우선 제일 중요한 Api를 불러오는 작업을 진행!

https://api.github.com/search/users?q=shop - 이 곳에 있는 sample Api를 Call하는 작업을 진행,

 

 

ModelEntity.kt

data class ModelEntity(
    @SerializedName("incomplete_results")
    val incompleteResults: Boolean?,
    @SerializedName("items")
    val items: List<User>,
    @SerializedName("total_count")
    val totalCount: Int?
)

 

User.kt

@Entity(tableName = "User")
@Parcelize
data class User (
    @SerializedName("id")@PrimaryKey()
    var id: Int,

    @SerializedName("avatar_url")
    var avatar_url : String,

    @SerializedName("login")
    var title: String,

    @SerializedName("url")
    var url: String,

    var hart : Boolean
) : Parcelable
Main화면에서 Detail화면으로 Intent시 User 객체를 한번에 넘겨주기 위해 Parcelize로 어노테이션 설정을 해줌,

 

 

RetrofitService.kt

interface RetrofitService {
    @GET("/search/users?q=shop")
    fun getAllTasks() : Call<ModelEntity>

    companion object {
        var retrofitService: RetrofitService? = null
        fun getInstance() : RetrofitService {
            if (retrofitService == null) {
                val retrofit = Retrofit.Builder()
                    .baseUrl("https://api.github.com/")
                    .addConverterFactory(GsonConverterFactory.create())
                    .build()
                retrofitService = retrofit.create(RetrofitService::class.java)
            }
            return retrofitService!!
        }
    }
}
Retrofit을 이용해 Api의 Entity를 Call 하는 과정, Companion Obeject 형식으로 Singleton 구현,

 


 

 

2. Room DB 생성

room 을 생성하는 과정

1) Entity(Model) 선언

2) DAO 데이터 액세스 개체 선언하여 DB 접근 방식 설정

3) AppDataBase를 이용하여 Room접근이 필요할 때만 Singlton으로 접근 처리

 

1) Model 선언

@Entity(tableName = "User")
@Parcelize
data class User (
    @SerializedName("id")@PrimaryKey()
    var id: Int,

    @SerializedName("avatar_url")
    var avatar_url : String,

    @SerializedName("login")
    var title: String,

    @SerializedName("url")
    var url: String,

    var hart : Boolean
) : Parcelable
 Api Call을 통해 받아오는 Model 객체이기 때문에 SerializedName으로 하였고 고유 값 id 가 따로 있기 때문에 Primarykey로 설정해주었다.

 

2) DAO 

@Dao
interface UserDao{
    @Query("SELECT * FROM User")
    fun getAllRecords(): LiveData<List<User>>

    @Query("SELECT * FROM User WHERE id = :id")
    fun selectUser(id : Int) : LiveData<User>

    @Update
    fun update(taskEntry: User)

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insertRecords(taskEntry: User)

    @Query("DELETE FROM User")
    suspend fun deleteAllRecords()
}
Mysql에서 사용하는 Select, Delete, Update 등 CRUD 명령어를 선언 해주 었음, DAO 명령으롤 통해 DB접근 가능

 

 

3) AppDataBase

@Database(entities = [User::class], version = 1)
@TypeConverters(TypeConverterOwner::class)
abstract class AppDatabase : RoomDatabase() {
    abstract fun getUserDao(): UserDao

    companion object{
        private var INSTANCE : AppDatabase ?= null

        fun getDataBase(context: Context) : AppDatabase {
            synchronized(this) {
                var instance = INSTANCE
                if (instance == null) {
                    instance = Room.databaseBuilder(
                        context.applicationContext,
                        AppDatabase::class.java,
                        "task_database"
                    ).fallbackToDestructiveMigration().build()
                    INSTANCE = instance
                }
                return instance
            }
        }
    }


}
RoomDataBase를 상속받는 Appdabase 추상 클래스를 선언, getDataBase()를 통해 Singlton 형식으로 Room DB를 접근 할 수 있다. 이렇게 선언한 추상클래스를 이용하여 ViewModel에서 DAO에 선언된 명령어에 따라 DB값을 변경 수정 삭제 할 수 있고, LiveData형식으로 보여질 수 있다.

 


3. MVVM 패턴 구조화

MVVM 은 기존 MVC 에서 Controller 에게 막중한 역할을 부여하기보다,  동작 자체를 분리하여 동작의 흐름을 더욱 체계적으로 만들어주고 유지보수를 편리하게 할 수 있도록 해주는 디자인 패턴이다. MVVM 은 Model, View, ViewModel 로 이루어져 아래와 같은 동

작을 한다.

 

 

View

  1. Activity / Fragment  View 역할을 함
  2. 사용자의 Action 을 받음 (텍스트 입력, 버튼 터치 등)
  3. ViewModel 의 데이터를 관찰하여 UI 갱신ViewModel
  1. View 가 요청한 데이터 Model 로 요청
  2. Model 로부터 요청한 데이터를 받음

Model

  1. ViewModel  요청한 데이터를 반환
  2. Room, Realm 과 같은 DB 사용이나 Retrofit 을 통한 백엔드 API 호출 (네트워킹) 이 보편적

 

결국 View 가 필요로 하는 데이터 ViewModel 이 쥐고 있고,
View 는 그것을 필요로 하기 때문에 ViewModel 이 쥐고 있는 데이터를 관찰 (Observing) 한다.

때문에 MVC 패턴과 다르게, View 가 DB 에 직접 접근하는 것이 아닌 UI 업데이트에만 집중한다.
또한 관찰하고 있는 만큼 데이터 변화에 더욱 능동적으로 움직이게 된다.

따라서 MVVM 패턴은 다음과 같은 장점을 가진다.

  • View 가 ViewModel 의 Data 를 관찰하고 있으므로 UI 업데이트가 간편
  • ViewModel 이 데이터를 홀드하고 있으므로 Memory Leak 발생 가능성 배제
    (View  직접 Model 에 접근하지 않아 Activity / Fragment 라이프 사이클에 의존하지 않기 때문)
  • 기능별 모듈화가 잘 되어 유지 보수에 용이 (e.g. ViewModel 재사용 및 DB 교체 등의 작업이 편리함)

 

 

 

구글 Android JetPack에서는 MVVM 패턴 구조화를 위해 AAC를 제공해준다.

이러한 구조로 앱을 만들기로 목표로 하며 제작하였다.

 

 

 

 

1) ViewModel 선언

class MainViewModel constructor(private val mainRepository: MainRepository) : ViewModel() {
    fun getAllTasks(): LiveData<List<User>> {
        return mainRepository.getAllRecords()
    }

    suspend fun getUpdateTakst(user: User){
        return mainRepository.updateRecord(user)
    }

    fun makeApiCall() {
        mainRepository.makeApiCall()
    }

    fun selectUser(id : Int) : LiveData<User>{
        return mainRepository.selectUser(id)
    }
    
}
API를 불러오는 작업과 Room을 이용한 DB접근 작업을 ViewModel을 거쳐 View와 Model이 상호 작용 할 수 있도록 설정 해 주었다.
먼저 여기서 중요한 것은 LIveData 형식으로 받는 다는 점이다. ViewModel의 특징은 Activity 생명주기와 상관없이 지속적으로 실행되고 있다는 점이다. 그래서 화면 회전과 같이 activity를 다시그려야 하는 작업에도 activity의 모습을 변하지 않는다. 그이유는 LiveData로 구성된 Viewmodel의 Model이 보여지기 때문이다.

 

 

 

2) Repository 설정

class MainRepository constructor(
    private val retrofitService: RetrofitService,
    private val userDao: UserDao
) {
    fun selectUser(id: Int): LiveData<User> {
        return userDao.selectUser(id)
    }

    fun getAllRecords(): LiveData<List<User>> {
        return userDao.getAllRecords()
    }

    fun updateRecord(repositoryData: User) {
        userDao.update(repositoryData)
    }

    suspend fun insertRecord(repositoryData: User) {
        userDao.insertRecords(repositoryData)
    }


    fun makeApiCall() {
        val call: Call<ModelEntity> = retrofitService.getAllTasks()
        call?.enqueue(object : Callback<ModelEntity> {
            override fun onResponse(
                call: Call<ModelEntity>,
                response: Response<ModelEntity>
            ) {
                if (response.isSuccessful) {
                    CoroutineScope(Dispatchers.IO).launch {
                        userDao.deleteAllRecords()
                        response.body()?.items?.forEach {
                            insertRecord(it)
                        }
                    }
                }
            }

            override fun onFailure(call: Call<ModelEntity>, t: Throwable) {
                Log.d("this", "Error : ${t} ")
            }

        })
    }

}
repository에 viewmodel의 Room 접근 이나 APi접근을 구현하 였다. 여기서 makeApiCall() 함수를 통해 Retrofit으로 구성한 부분의 api data를 불러 올 수 있고, response가 sucessful 할 시에 CoroutineScope를 이용하여 User Model을 Room DB에 Insert 해준다. repository는 곧 ViewModel 과 데이터를 주고받기 위해, 데이터 API 를 포함하는 클래스다. 사용자 동작에 따라 필요한 데이터나 외부 백엔드 서버 등에서 데이터를 가져오게 된다. Repository 의 존재 덕분에 ViewModel 이 데이터를 관리할 필요가 없게 된다.

여기서 생각 할것이 있었는데 매번 앱 실행시마다 makeApiCall을 해버리면 계속 Room DB가 새로 덮어씌어 지는 현상이 발생하였다. 그래서 MainActivity에서 makeApiCall을 할 시 viewmodel의 livedata를 observe()하며 livedata가 빈값일 경우에만 makeApiCall를 실시시키도록 하였다. 그 과정은 아래와 같다.

 

 

 

3) MainAcitivty

class MainActivity : AppCompatActivity(), OnItemClick {
    lateinit var adapter: RecyclerViewAdapter
    lateinit var viewModel: MainViewModel
    lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        initRecyclerView()

        val db = AppDatabase.getDataBase(this)
        val userDao = db.getUserDao()

        Log.d("this", "asdsd : ${userDao.getAllRecords().value}")


        val retrofitService = RetrofitService.getInstance()
        val mainRepository = MainRepository(retrofitService, userDao)

        viewModel  = ViewModelProvider(this, MyViewModelFactory(mainRepository)).get(MainViewModel::class.java)


        viewModel.getAllTasks().observe(this, Observer<List<User>>{
            adapter.setList(it)
            adapter.notifyDataSetChanged()
            if (it.size == 0){
                viewModel.makeApiCall()
            }
        })

    }
ViewModel을 Factory형태로 인자값을 전달시켜줄 필요가 있기 때문에 전달해주었고 Provider를 통해 선언해 주었다. 이 때 observe()를 통해 observer패턴을 viewmodel에 적용시켜 주었고 it.size == 0 즉 model이 없을 경우는 makeApiCall을 실행시켜 주었다. 그러면 DB가 존재 하지 않는 처음에만 MakeApiCall이 실행이 된다는 장점이 있다.

 

 


 

4. DetailActivity

마지막으로 Model 클릭 시 DetailView 화면으로 넘어가 실행되는 Method를 설정해 주었다.

클릭 전
클릭 후

우측 하트 버튼을 누르면 LiveData로 연동된 DB값이 변경 되므로 MainActivity의 하트의 색상 또한 바뀌게 된다. viewmodel로 작동 시킨 동작이기 때문에 화면 회전시에도 변동 없이 유지되고 있다. 그리고 하단부 URL Link를 클릭시 webview 형태로 아래의 명령어를 통해 이동 가능하게 해주었다.

binding.webTextView.setOnClickListener { var intent = Intent(Intent.ACTION_VIEW, Uri.parse("${userData?.url}")) startActivity(intent)}

userData의 Url을 parse하여 webview동작을 간단하게 설정해 줄 수 있다.

 

 

 

마무리

mvvm과 ACC패턴 구조화는 분명 같은 것은 아니지만 동작방식에 있어 비슷하다고 생각하고 MVC패턴의 한계를 두개 모두다 극복시켜주는 좋은 패턴구조화 라고 생각한다. 요즘 많이 쓰이고 대세론 적인 패턴구조화여서 학습하기 흥미로웠고 Room과 Retrofit api를 둘다 접근 해보면서 비동기 처리 형식, Singlton 패턴, observer패턴 을 학습할 수 있었던 경험이었던것 같다. 이 앱을 구성하기 위해 혼자 4일 동안 짬짬이 만들고 나니 패턴구조 이해하는데 큰 학습이 됐다.