일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
- 막내의막무가내 플러터
- 2022년 6월 일상
- 막내의막무가내 코볼 COBOL
- 막내의막무가내 일상
- 막내의막무가내 SQL
- 막내의막무가내 프로그래밍
- 부스트코스
- 막내의 막무가내 알고리즘
- 막내의막무가내
- 막내의막무가내 코틀린 안드로이드
- 막내의막무가내 안드로이드 에러 해결
- 안드로이드
- 부스트코스에이스
- 막내의막무가내 안드로이드 코틀린
- 막내의막무가내 코틀린
- Fragment
- 프로그래머스 알고리즘
- 안드로이드 Sunflower 스터디
- flutter network call
- 막내의막무가내 플러터 flutter
- 주택가 잠실새내
- 막내의 막무가내
- 막내의막무가내 rxjava
- 안드로이드 sunflower
- 막무가내
- 막내의막무가내 목표 및 회고
- 막내의막무가내 알고리즘
- 주엽역 생활맥주
- 프래그먼트
- 막내의막무가내 안드로이드
- Today
- Total
막내의 막무가내 프로그래밍 & 일상
[안드로이드] 안드로이드 아키텍처 스터디 총 정리(MVC,MVVM, AAC ViewModel, LiveData, Koin, Dagger2, RxJava2, Multi Module) Kotlin 본문
[안드로이드] 안드로이드 아키텍처 스터디 총 정리(MVC,MVVM, AAC ViewModel, LiveData, Koin, Dagger2, RxJava2, Multi Module) Kotlin
막무가내막내 2020. 3. 15. 21:48
[2021-04-13 업데이트]
참고사이트 : https://github.com/android/architecture-samples
안드로이드 스터디를 했던 내용들을 정리합니다.
스터디를 하면서 공식문서로 설명을 듣고 프로젝트에 적용하는식으로 진행했습니다.
안드로이드 개발자 공식문서가 잘 되있으므로 가장 먼저 참고하면서 공부합니다.
프로젝트 저장소
https://github.com/mtjin/android-architecture-study-movieapp
[1주차]
주제: MVC구현
- 네이버 영화검색앱 기능구현 및 깃 코드리부와 Git Flow 경험
MVC로 처음 안드로이드를 접했을때처럼 구현을 했다.
기본적인 구현은 다음 레포를 참고합니다.
https://github.com/boostcourse-connect/boostcamp_3_Android
[리뷰]
https://github.com/StudyFork/GoogryAndroidArchitectureStudy/pull/510
[원래는 검색화면만 있었는데 로그인화면과 스플래쉬 화면을 추가 구현했습니다.]
https://github.com/StudyFork/GoogryAndroidArchitectureStudy/pull/520
[2주차]
주제: Model(DataSource, Repository) 추가
- model은 단순 data class가 아니다.
Model은 다음과 같은 구조를 가진다.
-Repository
--Remote Data source -> remote get, post (Retrofit2)
--Local Data source --> sharedpref, room(sqlite), file
서버와 로컬데이터에 접근하는 DataSource가 있고 이를 Repository 에서 기능을 통합적으로 제공해준다.
- 무한스크롤과 네트워크 상태를 확인하는 기능을 추가했다.
[리뷰]
https://github.com/StudyFork/GoogryAndroidArchitectureStudy/pull/525
[3주차]
주제 : MVP (Model View Presenter)
- 기존 View에서 유효성(정규식) 검사, 요청하는 로직 등을 구현했던것을 Presenter에서 처리하도록 한다.
- 위 그림처럼 View는 뷰의 I/O(입력받고 보여지는) 역할만 하게하고 Model은 Local 또는 Server와 통신하여 데이터를 저장하고 받는 역할만 하도록 한다. 나머지 로직들은 Presenter에서 하며 View와 Model을 이어주는 매개체 역할을 한다.
- 사람으로 치면 View는 눈 , Presenter는 뇌,(매개체), Model은 행동(손) 이라고 생각하면 이해하기 쉽다.
- 로직이 분리되어 협업과 유지보수면에서 좋다. 그러나 생성해야할 클래스가 많아진다는 단점이 있다. (보일러플레이트 코드 증가)
MVP 구현 방법은 요약하면 다음과 같다.
1. Contract 만들기
2. Contract 안에 View, Presenter 만들기
3. MainPresenter 클래스 만들기
4. MainActivity 와 MainPresenter 가 Contract 에 있는 View 와 Presenter 를 impl 하기
5. MainActivity 와 MainPresenter 연결하기
실제 구현한 코드의 예이다.
[Contract Interface] 계약관계를 설계한다
package com.mtjin.androidarchitecturestudy.ui.search
import com.mtjin.androidarchitecturestudy.data.search.Movie
interface MovieSearchContract {
interface View {
fun showLoading()
fun hideLoading()
fun showToast(msg: String)
fun showEmptyQuery()
fun showWait()
fun showNetworkError()
fun showNoMovie()
fun showLastPage()
fun scrollResetState()
fun searchMovieSuccess(movieList: List<Movie>)
fun pagingMovieSuccess(movieList: List<Movie>)
}
interface Presenter {
fun requestMovie(query: String)
fun requestPagingMovie(query: String, offset: Int)
}
}
[Presenter] 로직을 담당한다.(Contract.Presenter 함수를 구현해야함) Contract.View 를 매개변수로 받는다. view로 인터페이스에서 정의한 뷰 관련 동작만 시킬 수 있다.
package com.mtjin.androidarchitecturestudy.ui.search
import android.util.Log
import com.mtjin.androidarchitecturestudy.data.search.source.MovieRepository
import retrofit2.HttpException
class MovieSearchPresenter(
private val view: MovieSearchContract.View,
private val movieRepository: MovieRepository
) :
MovieSearchContract.Presenter {
override fun requestMovie(query: String) {
if (query.isEmpty()) {
view.showEmptyQuery()
} else {
view.showWait()
view.showLoading()
view.scrollResetState()
movieRepository.getSearchMovies(query,
success = {
if (it.isEmpty()) {
view.showNoMovie()
} else {
view.searchMovieSuccess(it)
}
view.hideLoading()
},
fail = {
Log.d(TAG, it.toString())
when (it) {
is HttpException -> view.showNetworkError()
else -> view.showToast(it.message.toString())
}
view.hideLoading()
})
}
}
override fun requestPagingMovie(query: String, offset: Int) {
view.showLoading()
movieRepository.getPagingMovies(query, offset,
success = {
if (it.isEmpty()) {
view.showLastPage()
} else {
view.pagingMovieSuccess(it)
}
view.hideLoading()
},
fail = {
Log.d(TAG, it.toString())
when (it) {
is HttpException -> view.showNetworkError()
else -> view.showToast(it.message.toString())
}
view.hideLoading()
})
}
companion object {
const val TAG = "MovieSearchTAG"
}
}
[View Activity] Contract.View 관련 함수를 구현해야한다. 그리고 Pesenter 생성자에는 자신을(this) 넘겨준다.
package com.mtjin.androidarchitecturestudy.ui.search
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.view.View
import android.widget.Button
import android.widget.EditText
import android.widget.ProgressBar
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.mtjin.androidarchitecturestudy.R
import com.mtjin.androidarchitecturestudy.data.search.Movie
import com.mtjin.androidarchitecturestudy.utils.MyApplication
class MovieSearchActivity : AppCompatActivity(), MovieSearchContract.View {
private lateinit var presenter: MovieSearchContract.Presenter
private lateinit var etInput: EditText
private lateinit var btnSearch: Button
private lateinit var rvMovies: RecyclerView
private lateinit var pbLoading: ProgressBar
private lateinit var movieAdapter: MovieAdapter
private lateinit var myApplication: MyApplication
private lateinit var query: String
private lateinit var scrollListener: EndlessRecyclerViewScrollListener
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_movie_search)
initView()
initAdapter()
initListener()
presenter = MovieSearchPresenter(this, myApplication.movieRepository)
}
private fun initView() {
myApplication = application as MyApplication
etInput = findViewById(R.id.et_input)
btnSearch = findViewById(R.id.btn_search)
rvMovies = findViewById(R.id.rv_movies)
pbLoading = findViewById(R.id.pb_loading)
}
private fun initAdapter() {
movieAdapter = MovieAdapter()
val linearLayoutManager = LinearLayoutManager(this)
rvMovies.layoutManager = linearLayoutManager
scrollListener = object : EndlessRecyclerViewScrollListener(linearLayoutManager) {
override fun onLoadMore(page: Int, totalItemsCount: Int, view: RecyclerView?) {
presenter.requestPagingMovie(query, totalItemsCount + 1)
}
}
rvMovies.addOnScrollListener(scrollListener)
rvMovies.adapter = movieAdapter
}
private fun initListener() {
//어댑터 아이템 클릭리스너
movieAdapter.setItemClickListener { movie ->
Intent(Intent.ACTION_VIEW, Uri.parse(movie.link)).takeIf {
it.resolveActivity(packageManager) != null
}?.run(this::startActivity)
}
//검색버튼
btnSearch.setOnClickListener {
query = etInput.text.toString().trim()
presenter.requestMovie(query)
}
}
override fun showLoading() {
pbLoading.visibility = View.VISIBLE
}
override fun hideLoading() {
pbLoading.visibility = View.GONE
}
override fun showToast(msg: String) {
Toast.makeText(this, msg, Toast.LENGTH_SHORT).show()
}
override fun showEmptyQuery() {
Toast.makeText(this, getString(R.string.search_input_query_msg), Toast.LENGTH_SHORT).show()
}
override fun showWait() {
Toast.makeText(this, getString(R.string.wait_toast), Toast.LENGTH_SHORT).show()
}
override fun showNetworkError() {
Toast.makeText(this, getString(R.string.network_error_msg), Toast.LENGTH_SHORT).show()
}
override fun showNoMovie() {
Toast.makeText(this, getString(R.string.no_movie_error_msg), Toast.LENGTH_SHORT).show()
}
override fun showLastPage() {
Toast.makeText(this, getString(R.string.last_page_msg), Toast.LENGTH_SHORT).show()
}
override fun scrollResetState() {
scrollListener.resetState()
}
override fun searchMovieSuccess(movieList: List<Movie>) {
movieAdapter.clear()
movieAdapter.setItems(movieList)
Toast.makeText(this, getString(R.string.load_movie_success_msg), Toast.LENGTH_SHORT).show()
}
override fun pagingMovieSuccess(movieList: List<Movie>) {
movieAdapter.setItems(movieList)
Toast.makeText(this, getString(R.string.load_movie_success_msg), Toast.LENGTH_SHORT).show()
}
}
[예전에 썻던 글]
https://youngest-programming.tistory.com/111
[참고할 것]
참고 : http://www.codeplayon.com/2019/07/android-mvp-model-view-presenter-example-and-how-to-work-it/
https://github.com/android/architecture-samples/tree/todo-mvp-kotlin
[리뷰]
https://github.com/StudyFork/GoogryAndroidArchitectureStudy/pull/529
[4주차]
주제 : 데이터바인딩 (Databinding)
코틀린 코드로 하던 작업을 xml 에서 처리해 줄 수 있게 해줍니다. 코틀린 아키텍처의 필수요소 입니다.
layouts and binding expressions 및 binding adapters 등을 적용해봤습니다. two-way databinding은 뷰의 값을 보존하면서 변경로직을 구현하기위해 자주 사용된다. (아직 LiveData와 mvvm 적용전이라 해당부분은 적용되지 않았습니다.)
-참고 : 데이터바인딩 완전하게 적용하기 전입니다-
어댑터에서 executePendingBindings() 을 사용하면 더 효과적입니다.
ex) Evaluates the pending bindings, updating any Views that have expressions bound to modified variables.
fun bind(movie: Movie) {
binding.movie = movie
binding.executePendingBindings()
}
build.gradle 에 다음을 추가해서 데이터바인딩이 가능하게 해야한다.
dataBinding {
enabled = true
}
현재 2021년 기준으로는 다음과 같이 변경되었다.
android {
...
buildFeatures {
dataBinding true
}
}
적용한 예시다.
[액티비티]
xml 이름이 activity_move_search.xml 이라면 다음과 같은 이름으로 바인딩 객체를 생성할 수 있다.
private lateinit var binding: ActivityMovieSearchBinding
기존의 setContentView()를 제거해주고 데이터바인딩을 사용해 뷰를 객체화 시키도록 한다
뒤에 나올 xml의 <data> 를 주입시킬 수도 있다.
private fun initDataBinding() {
binding = DataBindingUtil.setContentView(this, R.layout.activity_movie_search)
binding.search = this
}
[XML]
<layout> 태그가 붙게되고 <data> 태그를 사용해 뷰나 모델객체, 프레젠터 등을 가져다 쓸 수도 있다.
클릭리스너도 onClick으로 설정해 줄 수 있고 액티비티에서 하던 것을 xml에서 대신 해줄 수 있는게 많다.
<?xml version="1.0" encoding="utf-8"?>
<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>
<variable
name="search"
type="com.mtjin.androidarchitecturestudy.ui.search.MovieSearchActivity" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="8dp"
tools:context=".ui.search.MovieSearchActivity">
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintGuide_percent="0.08" />
<EditText
android:id="@+id/et_input"
android:layout_width="0dp"
android:layout_height="0dp"
android:hint="@string/movie_search_input_hint"
android:inputType="text"
app:layout_constraintBottom_toTopOf="@+id/guideline"
app:layout_constraintEnd_toStartOf="@+id/btn_search"
app:layout_constraintHorizontal_weight="8"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/btn_search"
android:layout_width="0dp"
android:layout_height="0dp"
android:onClick="@{()->search.onSearchClick()}"
android:text="@string/movie_search_text"
app:layout_constraintBottom_toTopOf="@id/guideline"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_weight="2"
app:layout_constraintStart_toEndOf="@+id/et_input"
app:layout_constraintTop_toTopOf="parent" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_movies"
android:layout_width="0dp"
android:layout_height="0dp"
android:clipToPadding="false"
android:orientation="vertical"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/guideline" />
<ProgressBar
android:id="@+id/pb_loading"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
[어댑터도 비슷하게 해준다.] -> 더 자세한 사항과 구현된 예제는 다음 링크를 참고
https://youngest-programming.tistory.com/199
lateinit var binding: ItemMovieBinding
private val items: ArrayList<Movie> = ArrayList()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
binding = DataBindingUtil.inflate(
LayoutInflater.from(parent.context),
R.layout.item_movie,
parent,
false
)
val viewHolder = ViewHolder(binding)
binding.root.setOnClickListener {
itemClick(items[viewHolder.adapterPosition])
}
return viewHolder
}
[참고할 것]
https://developer.android.com/topic/libraries/data-binding?hl=eng%EF%BB%BF
https://developer.android.com/topic/libraries/data-binding
[리뷰]
mvvm 적용 전입니다. mvp에서 데이터바인딩 적용
https://github.com/StudyFork/GoogryAndroidArchitectureStudy/pull/543
[5주차]
주제 : MVVM
지난주에 데이터바인딩이 적용된 MVP 코드에서 P -> VM 으로 바꾸는 작업을 합니다. (LiveData는 아직 사용하지 않습니다. AAC 적용 X )
ViewModel 클래스는 수명 주기를 고려하여 UI 관련 데이터를 저장하고 관리하도록 설계되었습니다. ViewModel 클래스를 사용하면 화면 회전과 같이 구성을 변경할 때도 데이터를 유지할 수 있습니다.(AAC ViewModel 사용시 회전시 데이터 유지 가능 뒤에 6주차에서 다룸)
MVP 와 MVVM의 여러 차이점 중에 하나는 MVP 에서는 뷰가 프레젠터가 명령를 내리면 그때 행동하는 것 처럼 수동적이다. 허나 MVVM에서는 뷰가 능동적이다. View는 스스로 ViewModel 객체의 어떤 데이터가 필요한지 직접 관찰한다. 그래서 MVVM 에서는 옵저버 패턴이 필수적으로 사용된다.
프로젝트 구현은 다음과 같이 했다. (예시)
- 아직 AAC ViewModel 이 아니므로 ViewModel 을 상속받지는 않았고 LiveData 또한 적용 전 상태라 ObseravableField를 사용하였다.
[ViewModel]
class MovieSearchViewModel(private val movieRepository: MovieRepository) {
var query: ObservableField<String> = ObservableField("")
var movieList: ObservableField<ArrayList<Movie>> = ObservableField()
var toastMsg: ObservableField<MessageSet> = ObservableField()
var isLoading: ObservableBoolean = ObservableBoolean(false)
private var currentQuery: String = ""
fun requestMovie() {
currentQuery = query.get().toString()
if (currentQuery.isEmpty()) {
toastMsg.set(MessageSet.EMPTY_QUERY)
} else {
isLoading.set(true)
movieRepository.getSearchMovies(query.get().toString(),
success = {
if (it.isEmpty()) {
toastMsg.set(MessageSet.NO_RESULT)
} else {
movieList.set(it as ArrayList<Movie>?)
toastMsg.set(MessageSet.SUCCESS)
}
isLoading.set(false)
},
fail = {
Log.d(TAG, it.toString())
when (it) {
is HttpException -> toastMsg.set(MessageSet.NETWORK_ERROR)
else -> toastMsg.set(MessageSet.NETWORK_ERROR)
}
isLoading.set(false)
})
}
}
fun requestPagingMovie(offset: Int) {
isLoading.set(true)
movieRepository.getPagingMovies(currentQuery, offset,
success = {
if (it.isEmpty()) {
toastMsg.set(MessageSet.LAST_PAGE)
} else {
movieList.get()?.addAll(it)
movieList.notifyChange()
toastMsg.set(MessageSet.SUCCESS)
}
isLoading.set(false)
},
fail = {
Log.d(TAG, it.toString())
when (it) {
is HttpException -> toastMsg.set(MessageSet.NETWORK_ERROR)
else -> toastMsg.set(MessageSet.LAST_PAGE)
}
isLoading.set(false)
})
}
enum class MessageSet {
BASIC,
LAST_PAGE,
EMPTY_QUERY,
NETWORK_ERROR,
SUCCESS,
NO_RESULT
}
companion object {
const val TAG = "MovieSearchTAG"
}
}
[Activity]
class MovieSearchActivity : BaseActivity() {
private lateinit var binding: ActivityMovieSearchBinding
private lateinit var movieAdapter: MovieAdapter
private lateinit var myApplication: MyApplication
private lateinit var viewModel: MovieSearchViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
inject()
initViewModelCallback()
initAdapter()
}
private fun inject() {
myApplication = application as MyApplication
binding = DataBindingUtil.setContentView(this, R.layout.activity_movie_search)
viewModel = MovieSearchViewModel(myApplication.movieRepository)
binding.vm = viewModel
}
private fun initAdapter() {
movieAdapter = MovieAdapter { movie ->
Intent(Intent.ACTION_VIEW, Uri.parse(movie.link)).takeIf {
it.resolveActivity(packageManager) != null
}?.run(this::startActivity)
}
binding.rvMovies.adapter = movieAdapter
}
private fun initViewModelCallback() {
with(viewModel) {
toastMsg.addOnPropertyChangedCallback(object :
Observable.OnPropertyChangedCallback() {
override fun onPropertyChanged(sender: Observable?, propertyId: Int) {
when (toastMsg.get()) {
MovieSearchViewModel.MessageSet.LAST_PAGE -> showToast(getString(R.string.last_page_msg))
MovieSearchViewModel.MessageSet.EMPTY_QUERY -> showToast(getString(R.string.search_input_query_msg))
MovieSearchViewModel.MessageSet.NETWORK_ERROR -> showToast(getString(R.string.network_error_msg))
MovieSearchViewModel.MessageSet.SUCCESS -> showToast(getString(R.string.load_movie_success_msg))
MovieSearchViewModel.MessageSet.NO_RESULT -> showToast(getString(R.string.no_movie_error_msg))
}
toastMsg.set(MovieSearchViewModel.MessageSet.BASIC)
}
})
}
}
}
참고로 MyApplication은 DI(KOIN) 적용 전에 임의로 만든 것인데, 매번 매번 액티비티가 생성될 때마다 주입하는 것보다 Application객체에 생성해놓고 가져오는게 좋아서 그렇게 구현했다.
[Applicaton]
class MyApplication : Application() {
private lateinit var networkManager: NetworkManager
private lateinit var apiInterface: ApiInterface
lateinit var movieRepository: MovieRepository
private lateinit var movieRemoteDataSource: MovieRemoteDataSource
private lateinit var movieLocalDataSource: MovieLocalDataSource
private lateinit var movieDao: MovieDao
override fun onCreate() {
super.onCreate()
inject()
}
private fun inject() {
networkManager = NetworkManager(applicationContext)
apiInterface = ApiClient.getApiClient().create(ApiInterface::class.java)
movieDao = MovieDatabase.getInstance(this).movieDao()
movieRemoteDataSource =
MovieRemoteDataSourceImpl(
apiInterface
)
movieLocalDataSource = MovieLocalDataSourceImpl(movieDao)
movieRepository = MovieRepositoryImpl(movieRemoteDataSource, movieLocalDataSource, networkManager)
}
}
[참고할 것]
https://developer.android.com/topic/libraries/data-binding/observability
https://github.com/android/architecture-samples/tree/todo-mvvm-databinding
[리뷰]
https://github.com/StudyFork/GoogryAndroidArchitectureStudy/pull/553
[6주차]
주제 : AAC ViewModel, LiveData
ViewModel vs AAC ViewModel 의 차이점이다. ACC VIewModel은 안드로이드를 위한 거고 일반적인 ViewModel과 비슷해보이지만 내부 동작방식이 다르다. 밑 사이트에 설명이 잘되어있다.
https://thdev.tech/androiddev/2018/08/05/Android-Architecture-Components-ViewModel-Inject/
[개념]
ViewModel 클래스는 수명 주기를 고려하여 UI 관련 데이터를 저장하고 관리하도록 설계되었습니다. ViewModel 클래스를 사용하면 화면 회전과 같이 구성을 변경할 때도 데이터를 유지할 수 있습니다.
[ViewModel 주의할점]
주의: ViewModel은 뷰, Lifecycle 또는 활동 컨텍스트 참조를 포함하는 클래스를 참조해서는 안 됩니다.
ViewModel의 수명 주기
ViewModel 객체의 범위는 ViewModel을 가져올 때 ViewModelProvider에 전달되는 Lifecycle로 지정됩니다. ViewModel은 범위가 지정된 Lifecycle이 영구적으로 경과될 때까지, 즉 활동에서는 활동이 끝날 때까지 그리고 프래그먼트에서는 프래그먼트가 분리될 때까지 메모리에 남아 있습니다.
그림 1에서는 활동이 회전을 거친 후 완료될 때까지 활동의 다양한 수명 주기 상태를 보여줍니다. 또한 관련 활동 수명 주기 옆에 ViewModel의 전체 기간도 보여줍니다. 이 특정 다이어그램에서는 활동의 상태를 보여줍니다. 동일한 기본 상태가 프래그먼트의 수명 주기에 적용됩니다.
[뷰모델 생성]
val model = ViewModelProviders.of(this)[MyViewModel::class.java]
한글 문서의 위 코드를 ktx extension 을 사용해서 다음과 같이 줄일 수 있다.
val model: MyViewModel by viewModels()
이를 위해 밑 gradle 추가 필요
compileOptions {
sourceCompatibility = 1.8
targetCompatibility = 1.8
}
kotlinOptions {
jvmTarget = "1.8"
}
implementation 'androidx.core:core-ktx:1.2.0'
implementation 'androidx.fragment:fragment-ktx:1.2.4'
implementation "androidx.lifecycle:lifecycle-extensions:2.2.0"
뷰모델을 팩토리 패턴으로 생성이 가능하다. 또한 같은 뷰모델을 여러 프래그먼트에서 공유할 수 있어서 데이터 공유 및 생명주기 등의 관리도 편하다.
private val viewModel: MovieSearchViewModel by viewModels {
object : ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
return MovieSearchViewModel(myApplication.movieRepository) as T
}
}
}
[Livedata 개념]
LiveData는 식별 가능한 데이터 홀더 클래스입니다. 식별 가능한 일반 클래스와 달리 LiveData는 수명 주기를 인식합니다. 즉 활동, 프래그먼트 또는 서비스와 같은 다른 앱 구성요소의 수명 주기를 고려합니다. 이러한 수명 주기 인식을 통해 LiveData는 활성 수명 주기 상태에 있는 앱 구성요소 관찰자만 업데이트합니다.
[LiveData(전자) 와 ObserveField(후자) 사용시 observe 차이점이다. LiveData 에는 액티비티의 컨텍스트가 매개변수로 들어간다는 차이점이 있다. 즉 LifecycleOwner 인스턴스을 첫번째 매개변수로 전달해줘야한다. 이렇게 하면 이 관찰자가 소유자와 연결된 Lifecycle 개체에 결합되어 있음을 나타내며 그 의미는 다음과 같다.]
- Lifecycle 개체가 활성 상태가 아니면 값이 변경되더라도 관찰자가 호출되지 않습니다.
- Lifecycle 개체가 폐기되면 관찰자가 자동으로 삭제됩니다.
with(viewModel) { //LiveData 사용시
toastMsg.observe(this@MovieSearchActivity, Observer {
when (toastMsg.value) {
MovieSearchViewModel.MessageSet.LAST_PAGE -> showToast(getString(R.string.last_page_msg))
MovieSearchViewModel.MessageSet.EMPTY_QUERY -> showToast(getString(R.string.search_input_query_msg))
MovieSearchViewModel.MessageSet.NETWORK_ERROR -> showToast(getString(R.string.network_error_msg))
MovieSearchViewModel.MessageSet.SUCCESS -> showToast(getString(R.string.load_movie_success_msg))
MovieSearchViewModel.MessageSet.NO_RESULT -> showToast(getString(R.string.no_movie_error_msg))
}
})
// ObserverField 사용시
toastMsg.addOnPropertyChangedCallback(object :
Observable.OnPropertyChangedCallback() {
override fun onPropertyChanged(sender: Observable?, propertyId: Int) {
when (toastMsg.get()) {
MovieSearchViewModel.MessageSet.LAST_PAGE -> showToast(getString(R.string.last_page_msg))
MovieSearchViewModel.MessageSet.EMPTY_QUERY -> showToast(getString(R.string.search_input_query_msg))
MovieSearchViewModel.MessageSet.NETWORK_ERROR -> showToast(getString(R.string.network_error_msg))
MovieSearchViewModel.MessageSet.SUCCESS -> showToast(getString(R.string.load_movie_success_msg))
MovieSearchViewModel.MessageSet.NO_RESULT -> showToast(getString(R.string.no_movie_error_msg))
}
toastMsg.set(MovieSearchViewModel.MessageSet.BASIC)
}
})
하지만 프래그먼트에서는 this 가 아닌 viewLifecycleOwner 를 매개변수로 줘야한다. 이유는 다음을 참고하면 나온다. (옵저버를 중복해서 하는 문제가 발생한다.)
Pluu Dev - Fragment Lifecycle과 LiveData - http://pluu.github.io/blog/android/2020/01/25/android-fragment-lifecycle/
그리고 위와 같은 Observe 콜백 코드가 아닌 데이터바인딩으로 처리하고싶다면 데이터바인딩에도 LifecycleOwner 를 주입해줘야한다.
binding.lifecycleOwner = this
[참고할 것]
https://developer.android.com/topic/libraries/architecture/viewmodel?hl=ko
https://developer.android.com/topic/libraries/architecture/viewmodel
https://developer.android.com/topic/libraries/architecture/livedata?hl=ko
https://developer.android.com/topic/libraries/architecture/livedata
https://woovictory.github.io/2019/04/30/What-is-LiveData/
[리뷰]
https://github.com/StudyFork/GoogryAndroidArchitectureStudy/pull/564
[7주차]
주제 : DI(Dependency Injection) -KOIN-
소프트웨어 엔지니어링에서 의존성 주입은 하나의 객체가 다른 객체의 의존성을 제공하는 테크닉이다. "의존성"은 예를 들어 서비스로 사용할 수 있는 객체이다. 클라이언트가 어떤 서비스를 사용할 것인지 지정하는 대신, 클라이언트에게 무슨 서비스를 사용할 것인지를 말해주는 것이다.
코인은 안드로이드 이 의존성 주입을 쉽게 할 수 있도록 도와준다. (Dagger에 비해 러닝커브도 낮다고한다.)
대거와 코인의 차이점은 다음과 같다고한다.
[출처 : https://medium.com/harrythegreat/android-koin-%EB%A0%88%EB%B2%A8%EC%97%85-deep-drive-56b63b2e35d2]
Dagger
장점
- 순수자바
- 안정적이고 유연함
- 런타임 에러가 발생하지 않음
- 런타임시에 매우 빠름
단점
- 컴파일시 오버헤드가 발생
- 학습곡선이 상당함
Koin
장점
- Annotation 과정이 없어 컴파일이빠름
- 학습하기 쉽고 설치도 쉬움
단점
- 런타임중 에러가 발생
- Daager에 비해 런타임시 오버헤드가 있음
좋은 설명인 것 같아 이것도 가져왔다.
자세한 개념은 밑의 [참고할 것] 의 사이트를 참고한다.
gradle 추가, 이 외에도 여러개 있는데 MVVM에서 다음은 기본
// koin
implementation 'org.koin:koin-androidx-viewmodel:2.1.5'
Application 예시 , manifest application name 에 추가해야함
androidContext() 를 해줌으로 써 모듈에서 get으로 Context를 전달 받을 수 있음 androidContext() 로 안받아도 된다.
class KoinApplication : Application() {
override fun onCreate() {
super.onCreate()
startKoin {
if (BuildConfig.DEBUG) {
androidLogger()
} else {
androidLogger(Level.NONE)
}
androidContext(this@KoinApplication)
modules(
repositoryModule,
localDataModule,
remoteDataModule,
networkModule,
viewModelModule,
apiModule
)
}
}
}
Repository 모듈 예시
val repositoryModule: Module = module {
single<MovieRepository> { MovieRepositoryImpl(get(), get(), get()) }
single<LoginRepository> { LoginRepositoryImpl(get()) }
}
ViewModel 모듈 예시
val viewModelModule: Module = module {
viewModel { SplashViewModel(get()) }
viewModel { LoginViewModel(get()) }
viewModel { MovieSearchViewModel(get()) }
}
Acitivity 에서 ViewModel 모듈 가져다 쓰는거 예시 ( by viewModel() 사용)
import org.koin.androidx.viewmodel.ext.android.viewModel
class MovieSearchActivity : BaseActivity() {
private lateinit var binding: ActivityMovieSearchBinding
private lateinit var movieAdapter: MovieAdapter
private val viewModel: MovieSearchViewModel by viewModel()
Retrofit2 모듈 예시
import com.mtjin.androidarchitecturestudy.api.ApiClient
import com.mtjin.androidarchitecturestudy.api.ApiInterface
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import org.koin.core.module.Module
import org.koin.dsl.module
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
val apiModule: Module = module {
single<ApiInterface> { get<Retrofit>().create(ApiInterface::class.java) }
single<Retrofit> {
Retrofit.Builder()
.baseUrl(ApiClient.BASE_URL)
.client(get())
.addConverterFactory(get<GsonConverterFactory>())
.build()
}
single<GsonConverterFactory> { GsonConverterFactory.create() }
single<OkHttpClient> {
OkHttpClient.Builder()
.run {
addInterceptor(get<Interceptor>())
build()
}
}
single<Interceptor> {
Interceptor { chain ->
with(chain) {
val newRequest = request().newBuilder()
.addHeader("X-Naver-Client-Id", "aaa")
.addHeader("X-Naver-Client-Secret", "aaa")
.build()
proceed(newRequest)
}
}
}
}
파라미터 전달 시 예제 (각각 액티비티, 모듈) ( parmetrsOf() 사용)
class MainActivity : AppCompatActivity() {
//private val viewModel by viewModel<MainViewModel>{}
private val viewModel by viewModel<MainViewModel>{
//어떠한 파라미터를 넘기며 만들거냐
val hashId = intent.getStringExtra("Key")
parametersOf(hashId)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
}
val viewModelModule: Module = module {
//iewModel { MainViewModel(get()) }
viewModel { (hashId : String)-> MainViewModel(hashId, get())}
}
위의 방법을 사용해서 만약 해당 레포지토리가 두개가 있는 경우 구분해주는 방법 (모듈)
val repositoryModule: Module = module {
single<MainRepository> {MainRepositoryImpl()}
/*메인레포를 2개가 있는 경우 구분해주는 법,
* viewModel { (hashId : String)-> MainViewModel(hashId, get("prod))} 식으로 해주면됨
* */
//single<MainRepository>(named("prod")) {MainRepositoryImpl()}
//single<MainRepository>(named("test")) {MockRepositoryImpl()}
}
+)
val apiModule: Module = module {
single<ApiInterface>(named("tour")) { get<Retrofit>().create(ApiInterface::class.java) }
single<LocalPageRepository> { LocalPageRepositoryImpl(get(named("tour"))) }
[참고할 것]
https://gmlwjd9405.github.io/2018/11/09/dependency-injection.html
https://jungwoon.github.io/android/2019/08/21/Koin/
https://speakerdeck.com/jakewharton/dependency-injection-with-dagger-2-devoxx-2014
https://developer.android.com/training/dependency-injection
[리뷰]
https://github.com/StudyFork/GoogryAndroidArchitectureStudy/pull/580
[Multi Module]
KOIN , MVVM 등 아키텍처 패턴으로 모듈화 및 구조를 분리 했지만 모듈화를 사용하여 더 분리하여 결합도를 낮추고 응집도를 더 높일 수 있다.
data(Repository) , local, remote 로 모듈화 하는 작업을 을했고 각 모듈이 model(data class) 를 전달할 때 변환해주는 mapper 를 구현했다. app 에서는 data 모듈의 model을 사용하며 local, remote 에서 mapper 를 만들어 local, remote 의 model(data class)를 data로 mapper 로 각 알맞게 변환하여 전달하게 했다.
그래서 전체적인 구조를 보면 app은 data, local, remote를 가지게 되고 local, remote는 data 모듈을 가지게 된다.
그리고 각 모듈은 기본적으로 model, di(module), source 패키지를 가지고 있게하였다.
P.S 더 쪼개면 feature 까지 쪼갠다한다.
모듈을 만들면 settings.gradle 에 다음과 같이 모듈들이 생긴다. 만약 모듈아이콘이 안뜨고 합쳐지면 rootProject.name을 삭제해주면 된다.
이 모듈들을 불러오기 위해 자신의 gradle 에 다음과 같이 추가해주면 된다. (app 수준의 gradle)
dependencies {
//multi module
implementation project(':data')
implementation project(':remote')
implementation project(':local')
}
app, data(Repo), local, remote 모듈로 구현을 했는데
app 이 data, local, remote 모듈을 물고 있고
local, remote 모듈이 data 모듈을 물고 있다. (local, remote module 은 data 만 위 처럼 디펜던시 추가해주면 된다.)
그리고 data 모듈은 app 과 local, remote 의 model(data class)를 변환해주기 위한 mapper 를 구현한다.
ex)
local
package com.mtjin.local.mapper
import com.mtjin.local.model.search.Movie
import com.mtjin.data.model.search.Movie as dataMovie
fun mapperMovieListLocalToData(movies: List<Movie>): List<dataMovie> {
return movies.toList().map {
dataMovie(
it.actor,
it.director,
it.image,
it.link,
it.pubDate,
it.subtitle,
it.title,
it.userRating
)
}
}
fun mapperMovieListDataToLocal(movies: List<dataMovie>): List<Movie> {
return movies.toList().map {
Movie(
it.actor,
it.director,
it.image,
it.link,
it.pubDate,
it.subtitle,
it.title,
it.userRating
)
}
}
remote
package com.mtjin.remote.mapper
import com.mtjin.remote.model.search.Movie
import com.mtjin.data.model.search.Movie as dataMovie
fun mapperMovieListRemoteToData(movies: List<Movie>): List<dataMovie> {
return movies.toList().map {
dataMovie(
it.actor,
it.director,
it.image,
it.link,
it.pubDate,
it.subtitle,
it.title,
it.userRating
)
}
}
그리고 각 모듈에서 Impl 클래서에선느 외부에서 접근못하게 internal 접근자를 붙여준다. 추가로 data 모듈 에서는 LocalDataSource 와 RemoteDataSource 인터페이스를 갖고있고 remote, local 모듈에서는 이 인터페이스를 Impl 클래스로 구현해놓아야한다. 이를 통해 data의 Repository 에서 localDataSource, RemoteDataSource를 사용할 수 있게 해주고 실제 구현은 local, remote 모듈에서 해주는 형식이 된다.
그리고 app 말고 다른 모듈들에서는 필요없는 안드로이드 의존성을 제거해주도록 한다.
data 모듈 예시)
dependencies {
/* 제거
implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'androidx.core:core-ktx:1.2.0'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
*/
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
// koin
implementation 'org.koin:koin-core:2.1.5'
}
local 예시)
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
// koin
implementation 'org.koin:koin-androidx-viewmodel:2.1.5'
// Room
implementation "androidx.room:room-runtime:2.2.5"
testImplementation "androidx.room:room-testing:2.2.5"
kapt "androidx.room:room-compiler:2.2.5"
implementation 'androidx.room:room-ktx:2.2.5'
//multi module
implementation project(':data')
}
remote 예시)
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
//retrofit2
implementation 'com.squareup.retrofit2:retrofit:2.7.2'
implementation 'com.squareup.retrofit2:converter-gson:2.6.2'
// koin
implementation 'org.koin:koin-androidx-viewmodel:2.1.5'
//multi module
implementation project(':data')
}
[리뷰]
https://github.com/StudyFork/GoogryAndroidArchitectureStudy/pull/593
[RxJava2 적용]
다음은 RxJava2 공부 후 적용해본 프로젝트입니다. (네트워크 로직에만 적용했습니다.)
이에 대한 코드 설명은 다음 링크에서 다뤄봤습니다.
youngest-programming.tistory.com/288?category=925232
[Koin -> Dagger2 적용]
Dagger2 적용한 프로젝트입니다.
youngest-programming.tistory.com/351
[클린아키텍처 학습 및 적용]
https://youngest-programming.tistory.com/484
https://github.com/mtjin/mtjin-android-clean-architecture-movieapp
THE END....
댓글과 공감은 큰 힘이 됩니다. 감사합니다. !!
'안드로이드 > 코틀린 & 아키텍처 & Recent' 카테고리의 다른 글
[안드로이드] DI(Dependency Injection) 개념 및 KOIN 참고할 사이 (0) | 2020.04.15 |
---|---|
[안드로이드] 코틀린 @BindingAdapter 매개변수 두 개 이상할 경우 예시 (4) | 2020.04.05 |
[안드로이드] 코틀린 Sharedpreferences 예제 코드 기록 (0) | 2020.03.13 |
[안드로이드] 코틀린 리사이클러뷰 무한 스크롤 구현 (0) | 2020.03.13 |
[안드로이드] Android room persistent: AppDatabase_Impl does not exist (Room Database 관련 에러처리) (0) | 2020.03.09 |