일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- 동영상
- Kotlin
- mysql
- list
- mvvm
- MotionLayout
- Android
- rx
- MediaSession
- databinding
- Koin
- GCP
- 테스트 자동화
- node.js
- android13
- PagingLib
- google play
- 인앱결제
- SwiftUI Tutorial
- Animation
- SWIFTUI
- node
- RxKotlin
- Android 13
- junit
- liveData
- paging
- Reactive
- Observable
- MediaPlayer
- Today
- Total
봄날은 갔다. 이제 그 정신으로 공부하자
MVVM + Koin 최소 샘플 앱 개발 - part 1 본문
해당 글은 koin, DataBinding, Rxjava, retrofit, Coroutine, PagingLibrary을 사용하여. MVVM 구조로 된
github 사용자 검색 앱을 개발하는 과정을 기술합니다.
해당 글을 작성하는 이유는 위 기술이 적용된 최소 샘플 앱을 만들어 놓고 필요 시 참고하기 위함입니다.
실제 서비스 로직이 적용된 앱의 경우 앱에 적용된 서비스 로직으로 인해 해당 기술의 최소 적용 기준을 파악하기 어렵고
각각의 기술을 설명한 부분은 독립적이라 앱에 적용하기 위해서는 별도의 노력이 필요한 부분이 있습니다.
자주 사용하는 기술을 최소 샘플앱으로 만들어 신규앱 개발 시에 필요한 기술들을 편하게 참고 하기 위함입니다.
무엇을 만들 것인가?
github에서 사용자를 검색해 리스트로 보여주는 앱을 만들 예정 입니다.
화면은 Activity + Fragment 구성됩니다.
Activity에서 검색할 사용자 이름을 입력하면 Activity에 검색된 사용자 수가 보여지고 Fragment에 검색된 사용자 목록이 보여집니다.
시작 및 종속성 추가
Android Studio에서 Activity가 1개인 앱을 생성합니다.
App 레벨의 build.gradle 파일에 종속성을 추가합니다.
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'kotlin-android-extensions'
id 'kotlin-kapt'
}
...
dataBinding {
enabled = true
}
dependencies {
...
// swiperefreshlayout
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.0.0'
// rxjava
implementation "io.reactivex.rxjava3:rxjava:$rxjava_version"
implementation "io.reactivex.rxjava3:rxandroid:$rxjava_version2"
// koin
implementation "org.koin:koin-core:$koin_version"
implementation "org.koin:koin-android:$koin_version"
implementation "org.koin:koin-androidx-viewmodel:$koin_version"
// retrofit
implementation "com.squareup.retrofit2:retrofit:$retrofit_version"
implementation "com.squareup.retrofit2:converter-gson:$retrofit_version"
implementation "com.squareup.retrofit2:adapter-rxjava2:$retrofit_version"
// fastjson
implementation "org.ligboy.retrofit2:converter-fastjson-android:$fastjson_version"
// coroutines
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
// paging lib
implementation "androidx.paging:paging-runtime:$paging_version" // For Kotlin use paging-runtime-ktx
testImplementation "androidx.paging:paging-common:$paging_version" // For Kotlin use paging-common-ktx
implementation "androidx.paging:paging-rxjava2:$paging_version" // For Kotlin use paging-rxjava2-ktx
// multidex
implementation "androidx.multidex:multidex:$multidex_version"
// rx EventBus
implementation "org.greenrobot:eventbus:$eventbus_version"
// permission utility
implementation "gun0912.ted:tedpermission:$tedpermission_version"
}
projet의 build.gradle 파일에 각 라이브러리별 버전을 추가해줍니다.
이와 같이 버전을 분리해서 명시하는 이유는 프로젝트에 여러개의 앱이 존재할 경우 라이브러리의 버전 관리를 일관성있게 지원하기 위합입니다.
buildscript {
ext {
rxjava_version = '3.0.6'
koin_version= '2.0.1'
retrofit_version = '2.9.0'
fastjson_version = '2.2.0'
kotlin_version = '1.3.71'
coroutines_version = '1.3.0'
paging_version = '2.1.2'
multidex_version = "2.0.1"
eventbus_version = '3.0.0'
tedpermission_version = '2.2.0'
}
….
}
기본 구조 만들기 (MVVM)
View 폴더에 MainActivity.kt 파일을 이동 시킵니다.
같은 레벨에 공통으로 사용할 class들을 모아놓을 common 폴더를 추가해줍니다.
View 만들기
만들려는 앱이 Activity + Fragment 구조 이므로 view 폴더의 기본 제공 파일인 MainActivity.kt파일을 Activity + Fragment 구조로 변경합니다.
여러개의 Activity와 Fragment를 만들어야 하므로 아래와 같이 BaseActivity와 BaseFragment를 만들어 MainActivity가 상속받도록 합니다.
abstract class BaseActivity<T : ViewDataBinding> : AppCompatActivity() {
lateinit var viewDataBinding: T
abstract val layoutResourceId: Int
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewDataBinding = DataBindingUtil.setContentView(this, layoutResourceId)
}
}
abstract class BaseFragment<T : ViewDataBinding> : Fragment(){
lateinit var viewDataBinding: T
abstract val layoutResourceId: Int
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
viewDataBinding = DataBindingUtil.inflate(inflater, layoutResourceId, container, false)
return viewDataBinding.root
}
}
MainActivity가 BaseActivity를 상속받도록 아래와 같이 수정해줍니다.
// MainActivity.kt
class MainActivity : BaseActivity<ActivityMainBinding>() {
override val layoutResourceId: Int get() = R.layout.activity_main
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewDataBinding.vmMain = getViewModel()
viewDataBinding.lifecycleOwner = this
}
}
아직 작업이 안되서 에러가 발생합니다.
일단은 무시하고 사용자 목록을 보여줄 UserListFragment 파일을 아래와 같이 만들어줍니다.
// UserListFragment.kt
class UserListFragment: BaseFragment<FragmentUserListBinding>(){
override val layoutResourceId: Int get() = R.layout.fragment_user_list
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewDataBinding.vmUserList = getViewModel()
}
}
ViewModel 만들기
공통으로 사용할 BaseViewModel 부터 아래와같이 만들어줍니다.
// BaseViewModel.kt
abstract class BaseViewModel : ViewModel() {
private val compositeDisposable = CompositeDisposable()
fun addDisposable(disposable : Disposable){
compositeDisposable.add(disposable)
}
override fun onCleared() {
compositeDisposable.clear()
super.onCleared()
}
}
이제 MainActivity와 연결될 MainViewModel class를 아래와 같이 만들어줍니다.
class MainViewModel: BaseViewModel() {
}
Main View에 필요한 부분은 사용자 검색 EditBox와 검색 Button 그리고 검색된 사용자 수를 보여줄 TextView 이렇게 3개가 필요하므로 layout xml과 Binding 시켜줄 LiveData를 선언 및 정의하고 검색 버튼 클릭 시 연동할 함수를 아래와 같이 추가합니다.
val keyword = NotNullMutableLiveData<String>("")
val count = NotNullMutableLiveData<Int>(0)
fun onClickSearch(view: View){
}
NotNullMutableLiveData 데이터는 null check의 편의를 위해 common 폴더에 생성한 base LiveData class로 코드는 아래와 같습니다.
class NotNullMutableLiveData<T : Any>(_defaultValue: T) : MutableLiveData<T>() {
init {
value = _defaultValue
}
override fun getValue() = super.getValue()!!
}
이제 사용자 리스트를 보여줄 UserListViewModel class를 만들어 줍니다.
class UserListViewModel: BaseViewModel() {
}
Layout xml 편집하기 (ViewModel과 layout xml 연동하기)
위 항목에서 만들어진 viewmodel을 layout xml에 연동하기 위해서는 layout xml을 layout tag를 root로 선언한뒤 아래와 같이 <data> tag 안에 mainViewModel을 선언합니다.
<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="vmMain"
type="com.kiljae.mygitsample.viewmodel.MainViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"
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>
검색어를 입력하고 결과를 받은 control을 배치하고 MainViewModel의 LiveData와 연결합니다.
아래 코드에서 눈여겨 보아야 할 부분은 EditText에서 입력된 값을 가져오기 위해 Twoway binding을 사용한 부분과 검색 카운드를 보여주는 TextView에서 직접 Int Value를 넣으면 에러가 발생하므로 String으로 값을 변경해서 넣어주는 부분입니다.
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<EditText
android:id="@+id/edtKeyword"
android:layout_width="0dp"
android:layout_height="60dp"
android:layout_marginTop="20dp"
android:layout_marginStart="20dp"
android:text="@={vmMain.keyword}"
android:textSize="12dp"
android:hint="@string/search_keyword_hint"
android:inputType="phone"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@+id/btnSearch"
app:layout_constraintTop_toTopOf="parent"/>
<Button
android:id="@+id/btnSearch"
android:layout_width="120dp"
android:layout_height="60dp"
android:layout_marginTop="20dp"
android:layout_marginStart="10dp"
android:layout_marginEnd="20dp"
android:text="@string/search"
android:textSize="16dp"
android:background="@color/colorTomato"
android:textColor="@color/colorWhite"
android:onClick="@{vmMain::onClickSearch}"
app:layout_constraintStart_toEndOf="@+id/edtKeyword"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
<TextView
android:id="@+id/txvCount"
android:layout_width="0dp"
android:layout_height="60dp"
android:layout_margin="10dp"
android:textSize="16dp"
android:text="@{String.valueOf(vmMain.count)}"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/btnSearch"/>
<androidx.viewpager.widget.ViewPager
android:id="@+id/vpUserList"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/txvCount"
app:layout_constraintBottom_toBottomOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
이제 UserList layout xml을 만들어줍니다.
이것도 위와 동일하게 layout tag를 root로 해서 아래와 같이 작성해줍니다.
<?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="vmUserList"
type="com.kiljae.mygitsample.viewmodel.UserListViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/colorOrange">
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/swpRefresh"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rcvUserList"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="20dp"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"/>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
종속성 주입하기(koin으로 View와 ViewModel 연동하기)
common 폴더에 종속성 주입을 위한 MyDi.kt 파일을 만들어줍니다.
그 다음에 해당 파일에 viewModel module을 아래와 같이 만들어줍니다.
var viewModelpart = module{
viewModel{
MainViewModel()
}
viewModel{
UserListViewModel()
}
}
var myDiModule = listOf(
viewModelpart
)
여기까지 하면 모든 에러가 사라지고 앱이 빌드 됩니다.
Activity와 Fragment 연결하기
MainActiviy에 UserListFragment를 연결하기 전에 우선 ViewPager를 아래와 같이 만들어줍니다.
class PagerAdapterUserList(fragmentManager: FragmentManager) : FragmentStatePagerAdapter(fragmentManager){
override fun getItem(position: Int): Fragment {
return UserListFragment()
}
override fun getCount(): Int {
return 1
}
}
이렇게 만들어진 PagerAdapter를 사용해 Activity와 Fragment를 아래와 같이 연결해줍니다.
vpUserList.adapter = PagerAdapterUserList(supportFragmentManager)
빌드해서 실행해보면 아래와 같은 화면이 보입니다.
Activity 영역은 배경이 흰색이고 Fragment 영역은 오렌지 색인 것을 확인할 수 있습니다.
다음 글에서는 Github 사용자 검색 API를 호출하는 부분을 만들어보도록 하겠습니다.
여기까지 작업된 소스는 아래 경로에 업로드 되어 있습니다.
https://github.com/lee-kil-jae/MyGitSample/tree/dev_first
'Android jetpack' 카테고리의 다른 글
MVVM + koin 최소 샘플 앱 개발 - part3 (1) | 2020.12.07 |
---|---|
MVVM + Koin 최소 샘플 앱 개발 - part 2 (0) | 2020.12.05 |
Paging Library - Display paged lists (0) | 2020.11.24 |
Paging Library - Gather Paged Data (0) | 2020.11.24 |
Paging Library - Overview (0) | 2020.11.24 |