일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 | 31 |
- 막내의 막무가내 알고리즘
- 주엽역 생활맥주
- 막내의막무가내 SQL
- 막내의막무가내 안드로이드 코틀린
- 부스트코스
- 막내의막무가내 알고리즘
- Fragment
- 막내의막무가내 코틀린 안드로이드
- 안드로이드 sunflower
- 막내의막무가내 rxjava
- flutter network call
- 막내의막무가내 플러터 flutter
- 주택가 잠실새내
- 막내의막무가내 일상
- 2022년 6월 일상
- 막내의 막무가내
- 막내의막무가내
- 막내의막무가내 프로그래밍
- 막내의막무가내 코볼 COBOL
- 안드로이드
- 막내의막무가내 안드로이드 에러 해결
- 막내의막무가내 안드로이드
- 막무가내
- 막내의막무가내 목표 및 회고
- 프로그래머스 알고리즘
- 안드로이드 Sunflower 스터디
- 프래그먼트
- 부스트코스에이스
- 막내의막무가내 코틀린
- 막내의막무가내 플러터
- Today
- Total
막내의 막무가내 프로그래밍 & 일상
[RxJava] MVVM - Model Layer 네트워크 통신에 적용 본문
RxJava 를 공부하면서 기존 코틀린 고차함수와 레트로핏의 콜백(enqueue...)형식 으로 이루어져있던 Model Layer 부분을 RxJava로 변경하는 작업을 해보았습니다. (네트워크 통신 쪽을 중점적으로 수정했습니다.)
아직 RxJava가 많이 미숙한 초보자이고 처음 적용하는거라 이상한 부분이 많을 수 도 있습니다.
간략하게 정리하고 나중에 참고하고 발전시킬려고 코드형식으로 기록합니다.
MVVM 형식입니다.
[관련 디펜던시]
//retrofit2
implementation 'com.squareup.retrofit2:retrofit:2.8.1'
implementation 'com.squareup.retrofit2:converter-gson:2.8.1'
implementation "com.squareup.retrofit2:adapter-rxjava2:2.8.1"
//glide
implementation 'com.github.bumptech.glide:glide:4.11.0'
kapt 'com.github.bumptech.glide:compiler:4.11.0'
//rxjava
implementation "io.reactivex.rxjava2:rxjava:2.2.17"
implementation "io.reactivex.rxjava2:rxandroid:2.1.1"
implementation 'com.squareup.retrofit2:adapter-rxjava2:2.8.1'
implementation "io.reactivex.rxjava2:rxkotlin:2.3.0"
// Room
implementation "androidx.room:room-runtime:2.2.5"
implementation "androidx.room:room-rxjava2: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'
[VIewModel]
BaseViewModel 을 상속받아 compositeDisposable 로 쉽게 메모리누수가 생기지 않게 관리를 해줍니다.
package com.mtjin.androidarchitecturestudy.ui.search
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.mtjin.androidarchitecturestudy.base.BaseViewModel
import com.mtjin.androidarchitecturestudy.data.search.Movie
import com.mtjin.androidarchitecturestudy.data.search.source.MovieRepository
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.schedulers.Schedulers
open class MovieSearchViewModel(private val movieRepository: MovieRepository) : BaseViewModel() {
private var currentQuery: String = ""
val query = MutableLiveData<String>()
private val _movieList = MutableLiveData<ArrayList<Movie>>()
private val _toastMsg = MutableLiveData<MessageSet>()
private val _isLoading = MutableLiveData<Boolean>(false)
val movieList: LiveData<ArrayList<Movie>> get() = _movieList
val toastMsg: LiveData<MessageSet> get() = _toastMsg
val isLoading: LiveData<Boolean> get() = _isLoading
fun requestMovie() {
currentQuery = query.value.toString().trim()
if (currentQuery.isEmpty()) {
_toastMsg.value = MessageSet.EMPTY_QUERY
} else {
compositeDisposable.add(
movieRepository.getSearchMovies(currentQuery)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doOnSubscribe { showProgress() }
.doAfterTerminate { hideProgress() }
.subscribe({ movies ->
if (movies.isEmpty()) {
_toastMsg.value = MessageSet.NO_RESULT
} else {
_movieList.value = movies as ArrayList<Movie>?
_toastMsg.value = MessageSet.SUCCESS
}
}, {
_toastMsg.value = MessageSet.NETWORK_ERROR
})
)
}
}
fun requestPagingMovie(offset: Int) {
compositeDisposable.add(
movieRepository.getRemotePagingMovies(currentQuery, offset)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doOnSubscribe { showProgress() }
.doAfterTerminate { hideProgress() }
.subscribe({ movies ->
val pagingMovieList = _movieList.value
pagingMovieList?.addAll(movies)
_movieList.value = pagingMovieList
_toastMsg.value = MessageSet.SUCCESS
}, {
when (it.message) {
"Network Error" -> _toastMsg.value = MessageSet.NETWORK_ERROR
else -> _toastMsg.value = MessageSet.LAST_PAGE
}
})
)
}
private fun showProgress() {
_isLoading.value = true
}
private fun hideProgress() {
_isLoading.value = false
}
enum class MessageSet {
LAST_PAGE,
EMPTY_QUERY,
NETWORK_ERROR,
SUCCESS,
NO_RESULT
}
}
[Repository]
인터넷연결 상태에 따른 분기처리, 데이터 캐싱 등을 구현했습니다.
참고로 map과 flatmap 의 가장 큰 차이점은 map은 그 데이터를 가공한 그대로 리턴해주지만 flatmap은 Observbale 형태로 데이터를 감싸서 리턴해줍니다.
그리고 flatmapPublisher 라는 것도 사용했는데 다음 링크와 공식문서를 참고하면, Returns a Flowable that emits items based on applying a specified function to the item emitted by the source Single, where that function returns a Publisher.
Publisher를 반환하는 곳의 Single에서 방출한 데이터에 특정한 기능을 적용(가공)하여 데이터를 내보내는 Flowable을 반환합니다.
https://stackoverflow.com/questions/49399368/what-is-flatmappublisher-in-rxjava2
package com.mtjin.androidarchitecturestudy.data.search.source
import com.mtjin.androidarchitecturestudy.data.search.Movie
import com.mtjin.androidarchitecturestudy.data.search.source.local.MovieLocalDataSource
import com.mtjin.androidarchitecturestudy.data.search.source.remote.MovieRemoteDataSource
import com.mtjin.androidarchitecturestudy.utils.NetworkManager
import io.reactivex.Flowable
import io.reactivex.Single
class MovieRepositoryImpl(
private val movieRemoteDataSource: MovieRemoteDataSource,
private val movieLocalDataSource: MovieLocalDataSource,
private val networkManager: NetworkManager
) : MovieRepository {
override fun getSearchMovies(query: String): Flowable<List<Movie>> {
if (networkManager.checkNetworkState()) {
return movieLocalDataSource.getSearchMovies(query)
.onErrorReturn { listOf() }
.flatMapPublisher { cachedMovies ->
if (cachedMovies.isEmpty()) {
getRemoteSearchMovies(query)
.toFlowable()
.onErrorReturn { listOf() }
} else {
val local = Single.just(cachedMovies)
val remote = getRemoteSearchMovies(query)
.onErrorResumeNext { local }
Single.concat(local, remote)
}
}
} else {
return movieLocalDataSource.getSearchMovies(query)
.onErrorReturn { listOf() }
.flatMapPublisher { cachedMovies ->
if (cachedMovies.isEmpty()) {
Flowable.error(IllegalStateException("Network Error"))
} else {
Flowable.just(cachedMovies)
}
}
}
}
override fun getRemoteSearchMovies(
query: String
): Single<List<Movie>> {
return movieRemoteDataSource.getSearchMovies(query)
.flatMap {
movieLocalDataSource.insertMovies(it.movies)
.andThen(Single.just(it.movies))
}
}
override fun getRemotePagingMovies(
query: String,
start: Int
): Single<List<Movie>> {
return if (networkManager.checkNetworkState()) {
movieRemoteDataSource.getSearchMovies(query, start).flatMap {
if (it.movies.isEmpty()) {
Single.error(IllegalStateException("Last Page"))
} else {
if (start != it.start) {
Single.error(IllegalStateException("Last Page"))
} else {
movieLocalDataSource.insertMovies(it.movies)
.andThen(Single.just(it.movies))
}
}
}
} else {
Single.error(IllegalStateException("Network Error"))
}
}
}
[Local]
Select 에는 Single을 insert와 delete 는 성공 OR 실패만 전달해주면 되기 때문에 Completable을 리턴타입으로 사용했습니다.
package com.mtjin.androidarchitecturestudy.data.search.source.local
import com.mtjin.androidarchitecturestudy.data.search.Movie
import io.reactivex.Completable
import io.reactivex.Single
class MovieLocalDataSourceImpl(private val movieDao: MovieDao) : MovieLocalDataSource {
override fun insertMovies(movies: List<Movie>): Completable {
return movieDao
.insertMovies(movies)
}
override fun getAllMovies(): Single<List<Movie>> {
return movieDao
.getAllMovies()
.map {
it
}
}
override fun getSearchMovies(title: String): Single<List<Movie>> {
return movieDao.getMoviesByTitle(title)
.map {
it
}
}
override fun deleteAllMovies(): Completable {
return movieDao.deleteAllMovies()
}
}
package com.mtjin.androidarchitecturestudy.data.search.source.local
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy.REPLACE
import androidx.room.Query
import com.mtjin.androidarchitecturestudy.data.search.Movie
import io.reactivex.Completable
import io.reactivex.Single
@Dao
interface MovieDao {
@Insert(onConflict = REPLACE)
fun insertMovies(movies: List<Movie>): Completable
@Query("SELECT * FROM movie")
fun getAllMovies(): Single<List<Movie>>
@Query("SELECT * FROM movie WHERE title LIKE '%' || :title || '%'")
fun getMoviesByTitle(title: String): Single<List<Movie>>
@Query("DELETE FROM movie")
fun deleteAllMovies(): Completable
}
[Remote]
네트워크 통신의 데이터 결과를 보내주기위해 Single을 사용했습니다. map은 딱히 가공하는 로직이 없으므로 생략해도 됩니다.
package com.mtjin.androidarchitecturestudy.data.search.source.remote
import com.mtjin.androidarchitecturestudy.api.ApiInterface
import com.mtjin.androidarchitecturestudy.data.search.MovieResponse
import io.reactivex.Single
class MovieRemoteDataSourceImpl(private val apiInterface: ApiInterface) :
MovieRemoteDataSource {
override fun getSearchMovies(query: String, start: Int): Single<MovieResponse> {
return apiInterface.getSearchMovie(query, start)
.map {
it
}
}
}
[Base]
그 밖에 상속해서 사용하는 BaseActivity 와 BaseViewModel 은 다음과 같습니다.
참고로 BaseViewModel 에서 onDestory() 가 아닌 onCleared() 에서 CompositeDispolsable()을 해제해주어야합니다. (CompositeDisaposable 클래스를 이용하면 생성된 모든 Observable을 안드로이드 라이프사이클에 맞춰 한 번에 모두 해체할 수 있습니다._
이유는 밑 그림의 수명주기를 보면 알 수 있습니다.
액티비티가 완전히 종료될때 뷰모델이 종료되야하기 때문에 onDestory() 시점에 일어나는 onCleared()에서 옵저버블들의 메모리누수 방지를 위해 해제해주는 작업을 합니다.
이에대한 더 자세한 내용은 다음을 참고해주세요
https://beomseok95.tistory.com/m/60
package com.mtjin.androidarchitecturestudy.base
import android.os.Bundle
import android.widget.Toast
import androidx.annotation.LayoutRes
import androidx.appcompat.app.AppCompatActivity
import androidx.databinding.DataBindingUtil
import androidx.databinding.ViewDataBinding
import io.reactivex.disposables.CompositeDisposable
abstract class BaseActivity<B : ViewDataBinding>(
@LayoutRes val layoutId: Int
) : AppCompatActivity() {
lateinit var binding: B
private val compositeDisposable = CompositeDisposable()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, layoutId)
binding.lifecycleOwner = this
}
protected fun showToast(msg: String) {
Toast.makeText(this, msg, Toast.LENGTH_SHORT).show()
}
override fun onDestroy() {
super.onDestroy()
compositeDisposable.clear()
}
}
package com.mtjin.androidarchitecturestudy.base
import androidx.lifecycle.ViewModel
import io.reactivex.disposables.CompositeDisposable
abstract class BaseViewModel : ViewModel() {
protected val compositeDisposable = CompositeDisposable()
override fun onCleared() {
compositeDisposable.dispose()
super.onCleared()
}
}
액티비티 사용 예
class MovieSearchActivity :
BaseActivity<ActivityMovieSearchBinding>(R.layout.activity_movie_search) {
댓글과 공감은 큰 힘이 됩니다. 감사합니다!
'안드로이드 > RxJava' 카테고리의 다른 글
[RxJava] RxJava 프로그래밍 책 후기 (6) | 2020.09.03 |
---|---|
[RxJava] Convert Observable to Single (Single.fromObservable) + flatMapCompletable (0) | 2020.05.21 |
[RxJava] 기초 개념 정리 사이트 (0) | 2020.05.08 |
[RxJava] Observable 클래스 (0) | 2020.04.01 |
[RxJava] RxJava 리엑티브 프로그래밍 공부 정리 (0) | 2020.03.17 |