비동기 프로그래밍에 권장되는 코루틴은 멀티태스킹을 지원하고 단순히 스레드로 작업하는 것보다 레벨이 다른 추상화를 제공한다. 상태를 저장해 중단했다가 재개할 수 있다는 주요 기능이 핵심이다. 따라서 코루틴을 실행되거나 실행되지 않을 수 있다.
launch()
로 만든 작업 단위)launch()
와 async()
와 같이 새 코루틴을 생성하는 데 사용되는 코드는 CoroutineScope의 확장이다. 모든 코루틴은 범위 내에서 실행해야 하고, CoroutineScope는 하나의 이상의 관련 코루틴을 관리한다.**suspend
키워드**로 표시된다. 일시정지되거나 재개될 수 있음을 나타내기 위해서다. 함수가 이미 suspend 함수를 호출한 경우에도 자체적으로 표시해야 한다.예제로 알아보자.
class LoginViewModel(
private val loginRepository: LoginRepository
): ViewModel() {
fun login(username: String, token: String) {
// Create a new coroutine to move the execution off the UI thread
viewModelScope.launch(Dispatchers.IO) {
val jsonBody = "{ username: \"$username\", token: \"$token\"}"
loginRepository.makeLoginRequest(jsonBody)
}
}
}
viewModelScope
→ ViewModel KTX 확장 프로그램에 포함되어 사전 정의된 CoroutineScope. ViewModel 범위에서 실행된다. ViewModel이 소멸되는 경우 viewModelScope가 자동으로 취소되고, 따라서 실행 중인 모든 코루틴도 취소된다.launch
→ 코루틴을 만들고 함수 본문의 실행을 해당하는 dispatcher에 전달한다.Dispatchers.IO
→ 코루틴을 I/O 작업용 스레드에서 실행한다.다음 스레드 활용 코드.
fun main() {
val states = arrayOf("Starting", "Doing Task 1", "Doing Task 2", "Ending")
repeat(3) {
Thread {
println("${Thread.currentThread()} has started")
for (i in states) {
println("${Thread.currentThread()} - $i")
Thread.sleep(50)
}
}.start()
}
}
동일한 코드를 코루틴을 활용한 코드.
import kotlinx.coroutines.*
fun main() {
val states = arrayOf("Starting", "Doing Task 1", "Doing Task 2", "Ending")
repeat(3) {
GlobalScope.launch {
println("${Thread.currentThread()} has started")
for (i in states) {
println("${Thread.currentThread()} - $i")
delay(5000)
}
}
}
}
private val retrofit = Retrofit.Builder()
.addConverterFactory(ScalarsConverterFactory.create())
.baseUrl(BASE_URL)
.build()
addConverterFactory
→ 웹 서비스에서 얻은 데이터로 해야 할 일을 Retrofit에 알린다.ScalarsConverter
→ 문자열 및 기타 primitive 유형을 지원한다.baseUrl
→웹 서비스의 기본 URI를 추가한다.build
→ Retrofit 객체를 생성한다.cf) Json 문자열을 Kotlin 객체로 변환할 때 Json 파서인 Moshi 라이브러리를 활용한다. converter가 따로 있고, 아래와 같이 활용한다.
private val moshi = Moshi.Builder()
.add(KotlinJsonAdapterFactory())
.build()
private val retrofit = Retrofit.Builder()
.addConverterFactory(MoshiConverterFactory.create(moshi))
.baseUrl(BASE_URL)
.build()
객체의 인스턴스가 하나만 생성되도록 보장한다. 이 객체의 전역 액세스 포인트도 하나를 가진다. 객체 선언의 초기화는 스레드로부터 안전하고 첫 액세스할 때 한다.
다음은 싱글톤 객체 선언과 액세스 예시 코드. 객체 선언에는 항상 object 키워드가 붙는다.
// Object declaration
object DataProviderManager {
fun registerDataProvider(provider: DataProvider) {
// ...
}
val allDataProviders: Collection<DataProvider>
get() = // ...
}
// To refer to the object, use its name directly.
DataProviderManager.registerDataProvider(...)
Retrofit 객체에서 create() 함수를 호출하는 데는 리소스가 많이 든다. 또한 Retrofit API 인스턴스는 하나만 필요하다. 따라고 싱글톤 객체 선언을 해서 서비스를 노출시키자.
object MarsApi {
val retrofitService : MarsApiService by lazy {
retrofit.create(MarsApiService::class.java)
}
}
lazy를 활용해 초기화하면 최초 사용 시에 초기화된다. 실제로 객체가 필요할 때까지 불필요한 계산이 실행되거나 컴퓨팅 리소스가 사용되지 않도록 하기 위해 객체 생성을 의도적으로 지연하는 것이다.
view의 맞춤 속성을 위해 맞춤 setter를 만드는 데 사용되는 주석처리된 메서드다. 이게 무슨 소리인지는 다음 예시를 통해서 이해해보자.
xml에서 android:text="Sample Text"
로 속성을 설정하면, android 시스템은 setText(String: text)
메서드에서 결정되는 text 속성과 같은 이름의 setter를 찾는다. 그러니까 setText(String: text)
는 android에서 제공하는 일부 view의 setter 메서드라는 거다.
Binding Adapter 또한 이와 유사한 동작을 맞춤 설정할 수 있다! 쉽게 말해 url을 view에 붙여준다는 거다.
코드를 보며 되짚어보자.
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/photos_grid"
android:layout_width="0dp"
android:layout_height="0dp"
android:clipToPadding="false"
android:padding="6dp"
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:listData="@{viewModel.photos}"
app:spanCount="2"
tools:itemCount="16"
tools:listitem="@layout/grid_view_item" />
위 app:imageUrl
속성을 구현해 ImageView로 설정하는 메서드를 만들어야 한다.
@BindingAdapter("imageUrl")
fun bindImage(imgView: ImageView, imgUrl: String?) {
imgUrl?.let {
val imgUri = imgUrl.toUri().buildUpon().scheme("https").build()
imgView.load(imgUri) // coil 라이브러리 활용
}
}
@BindingAdpater
→ 이 주석은 속성 이름을 매개변수로 사용한다. 이 코드를 해석하면 view에 imageUrl 속성이 있을 경우 bindingAdapter에게 binding을 실행시킨다는 거다.bindImage
메서드의 첫 번째 매개변수는 타겟이 되는 view, 두 번째 매개변수는 속성에 설정되는 값이다.buildUpon.scheme("https")
→ HTTPS 스키마 사용cf) let
은 ?.와 함께 null safety 연산을 실행하는 데 사용된다. null이 아닌 경우에만 실행되고, 메서드 하나 이상을 호출하는 데 쓴다.
cf) BindingAdapter는 class 안에서 쓰지 않는다!!!
RecyclerView와 list 객체인 MarsPhoto를 인수로 사용하는 bindRecyclerView()
메서드를 추가하자.
// BindingAdapters.kt
@BindingAdapter("listData")
fun bindRecyclerView(recyclerView: RecyclerView,
data: List<MarsPhoto>?) {
}
어댑터를 할당하고, 출력할 사진 list 데이터가 포함된 adapter.submitList()
를 호출한다.
// BindingAdapters.kt
@BindingAdapter("listData")
fun bindRecyclerView(recyclerView: RecyclerView,
data: List<MarsPhoto>?) {
val adapter = recyclerView.adapter as PhotoGridAdapter
adapter.submitList(data)
}
xml의 RecyclerView에 listData 속성을 추가해 dataBinding을 활용하여 ViewModel의 데이터를 설정한다.
// fragment_overview.xml
<androidx.recyclerview.widget.RecyclerView
...
app:listData="@{viewModel.photos}"
... />
RecyclerView 어댑터를 PhotoGridAdapter 객체로 초기화한다.
// OverviewFragment.kt
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val binding = FragmentOverviewBinding.inflate(inflater)
...
binding.photosGrid.adapter = PhotoGridAdapter()
return binding.root
}
<androidx.recyclerview.widget.RecyclerView
...
tools:itemCount="16"
tools:listitem="@layout/grid_view_item" />
아래 속성을 false로 하면 스크롤 뷰가 패딩 영역 안에 그려진다.
<androidx.recyclerview.widget.RecyclerView
...
android:clipToPadding="false"
... />
인터넷을 활용해 통신을 해 데이터를 받을 때, 비행기 모드와 같은 경우에는 오류가 날 수 있다. 이러한 오류를 처리하려면 ViewModel에 웹 요청 상태를 나타내는 속성을 만들어 해결해야 한다.
위 세 가지 상태를 나타내기 위해 enum
클래스를 활용한다. kotlin에서 enum은 상수 집합을 보유할 수 있는 데이터 유형이다. 쉽게 예시로 알아보자.
// 정의
enum class Direction {
NORTH, SOUTH, WEST, EAST
}
// 사용
var direction = Direction.NORTH
다음은 실습 예제다.
class OverviewViewModel : ViewModel() {
enum class MarsApiStatus { LOADING, ERROR, DONE }
...
private fun getMarsPhotos() {
viewModelScope.launch { // 통신 시작
_status.value = MarsApiStatus.LOADING
try { // 통신 성공
_photos.value = MarsApi.retrofitService.getPhotos()
_status.value = MarsApiStatus.DONE
} catch (e: Exception) { // 통신 에러
_status.value = MarsApiStatus.ERROR
_photos.value = listOf() // RecyclerView 삭제
}
}
}
}
위 코드를 xml에 적용하려면 다음과 같이 해보자.
// BindingAdapter.kt
@BindingAdapter("marsApiStatus")
fun bindStatus(statusImageView: ImageView,
status: OverviewViewModel.MarsApiStatus?) {
when (status) {
OverviewViewModel.MarsApiStatus.LOADING -> {
statusImageView.visibility = View.VISIBLE
statusImageView.setImageResource(R.drawable.loading_animation)
}
OverviewViewModel.MarsApiStatus.ERROR -> {
statusImageView.visibility = View.VISIBLE
statusImageView.setImageResource(R.drawable.ic_connection_error)
}
OverviewViewModel.MarsApiStatus.DONE -> {
statusImageView.visibility = View.GONE
}
}
}
// fragment_overview.xml
<ImageView
android:id="@+id/status_image"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:marsApiStatus="@{viewModel.status}" />
ViewModel의 통신 상태에 따라 ImageView가 보일 수도, 안 보일 수도 있게 된다!