우리는 이 Task라는 class의 Object를 생성하여 Observable하게 만들고 Observer가 관찰하게 할 것이다.
class Task(description : String, isComplete : Boolean, priority : Int)
{
var description = description
var isComplete = isComplete
var priority = priority
}
MVVM에서 Model 역할을 하는 class이다.
다른 예로 들자면 DB에서 Data를 가져오는 역할과 비슷하다.
class DataSource
{
//Task 생성
fun createTasksList() : List<Task>
{
var tasks = ArrayList<Task>()
tasks.add(Task("Task out the trash",true,3))
tasks.add(Task("Walk the dog",false,2))
tasks.add(Task("Make my bed",true,1))
tasks.add(Task("Upload the dishwasher",false,0))
tasks.add(Task("Make dinner",true,5))
return tasks
}
}
이전 포스팅에서 MVVM을 사용하기 위해 LiveData와 코루틴을 사용한 프로젝트를 간략히 적고 내가 느낀 점을 적었다.
더 알아보다 보니 Rxjava를 사용해야지만 MVVM의 완성이라는 사람들도 존재하는 것 같아 공부하고 내 생각을 적어본다.
기본적으로 Reactive Extension 이라는 개념에서 나왔다고 한다.
많은 Event로 하여금 좀더 유연하게/유동적으로 변경된다는 말로 이해했다.
기본적으로 끊임없이 변하는 Data의 흐름을 알기 위해 사용한다.
두 가지의 역할? 이 존재한다
Observable : Data의 Stream을 생성한다. Data를 발행한다.
Subscriber : Data Stream에서 Data를 건져 사용한다. 사용을 안할 때는 사용해제를 해줘야 함.(사용 해제는 unsubscribe(), dispose() -> dispose는 완전히 사용하지 않게 될 때)
Observable - onNext() , onCompleted() , onError() 의 3가지 메소드가 존재하며
각각 onNext() - 새로운 Data를 구독하고 있는 Subscriber에게 전달
onCompleted() - Stream을 종료
onError() - 에러 발생시
계속해서 변하는 Data를 API를 통해 들어오는 Data라고 가정해보자.
어떠한 API를 통해 Data가 지속적으로 변화되어 들어올 때 onNext()를 통해 그 바뀐 Data를 뿌려 Subscriber가 변화된 Data를 알게 하는 것이다.
(이때, Data들을 filter, map 와 같은 operator를 통해 거르고 계산하여 반환하고 할 수 있다.)
RxAndroid - Rxjava를 사용하기 위해 Android의 Rxjava? 같은 느낌이다.
가장 큰 역할은 스케줄러는 제공하는 것이다.
뒤의 코드에서 더 설명하겠다.
예제로
온도를 받아오는 API를 사용하여 그 온도를 Rxjava와 RxAndroid를 사용해 화면에 뿌리는 App을 만들어보자.
API를 사용하는 것 대신 새로운 쓰레드를 생성하여 그 쓰레드가 온도를 뿌려주는 방법으로 진행하겠다.
MainActivity
class MainActivity : AppCompatActivity() {
val TAG = "RxAndroid.MainActivity"
val manager = TemperatureManager()
var disposable : Disposable?=null
var github = GithubClient()
lateinit var binding : ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil
.setContentView<ActivityMainBinding>(this, R.layout.activity_main)
binding.currentTemperatureView.text = "받아온 온도가 없습니다."
val executorService = Executors.newSingleThreadScheduledExecutor()
executorService.scheduleAtFixedRate({
val nextTemperature = Random().nextInt(15)+10
manager.setTemperature(TemperatureManager.Temperature(nextTemperature))
}, 0L,3, TimeUnit.SECONDS)
disposable = manager.updateEvent().subscribe(this::updateView)
//subscribe하는 Item들을 updateView에서 처리하도록 하겠다 - java8식
}
override fun onDestroy() {
super.onDestroy()
disposable?.dispose()
}
fun updateView(temperature: TemperatureManager.Temperature)
{
binding.currentTemperatureView.text = "현재온도 : ${temperature.currentTemperature}"
}
}
TemperatureManager
class TemperatureManager
{
class Temperature(degree : Int)
{
var currentTemperature =degree
}
var subject : PublishSubject<Temperature> = PublishSubject.create()
//PublishSubject가 생성하는 subject는 Observable을 UI Event와 연결지어서 사용하고 싶을 때
//유저가 화면을 언제 터치할지 모를 때 -> 이럴 때 subject를 사용
//사용 하는 녀석
// 차이점 : Observable은 내부에서 Data를 emit하지만 subject는 코드처럼 외부에서 Data를 주입가능
// 아래의 함수 setTemperature()에서 onNext로 데이터를 전달하는 부분을 주목
fun setTemperature(temperature : Temperature)
{
subject.onNext(temperature)
}
//updateEvent에서 subscribeOn과 observeOn은 subscribe와 observe를 전담할 쓰레드를 지정
//UI Update는 메인쓰레드에서 실행해야하므로 AndroidSchedulers.mainThread()를 지정
fun updateEvent() : Observable<Temperature>
{
return subject.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
}
}
코드를 좀더 자세히 알아보자
binding = DataBindingUtil
.setContentView<ActivityMainBinding>(this, R.layout.activity_main)
binding.currentTemperatureView.text = "받아온 온도가 없습니다."
binding을 사용하여 layout을 관리한다.
val executorService = Executors.newSingleThreadScheduledExecutor()
executorService.scheduleAtFixedRate({
val nextTemperature = Random().nextInt(15)+10
manager.setTemperature(TemperatureManager.Temperature(nextTemperature))
}, 0L,3, TimeUnit.SECONDS)
@Entity
data class Todo(var title : String)
{
@PrimaryKey(autoGenerate = true) var id : Int = 0
}
2. Query문을 생성하는 Interface를 작성한다.
@Dao
interface TodoDao
{
@Query("SELECT * FROM Todo")
fun getAll() : LiveData<List<Todo>>
@Insert
fun insert(todo:Todo)
@Update
fun update(todo:Todo)
@Delete
fun delete(todo:Todo)
}
3. MainActivity를 작성.
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
//setContentView(R.layout.activity_main)
//setContextView를 대신한다
val binding = DataBindingUtil
.setContentView<ActivityMainBinding>(this, R.layout.activity_main)
binding.lifecycleOwner = this // LiveData를 사용하기 위해서 없으면 Observe할때마다 refresh안딤
//binding 객체는 layout의 객체로 생각한다?
//ViewModelProviders를 사용하여 ViewModel을 불러온다.
val viewModel = ViewModelProviders.of(this)[MainViewModel::class.java]
binding.viewModel = viewModel//layout의 binding 객체의 name = viewModel 에 viewModel을 초기화
/*
//LiveData 형식의 Data를 관찰하여 바뀔때마다 불린다??
viewModel.getAll().observe(this, object : Observer<List<Todo>?> {
override fun onChanged(t: List<Todo>?) {
result_text.text = t.toString()
}
})
layout에 DataBinding을 하였기 떄문에 MainViewModel의 LivewData가 변경될 떄마다
자동적으로 layout의 binding 객체를 통해 Data 전송?
*/
/*
add_button.setOnClickListener(object : View.OnClickListener {
override fun onClick(v: View?) {
//비동기 처리
//Dispatchers.IO -> Worker Thread (백그라운드 )
lifecycleScope.launch(Dispatchers.IO) {
viewModel.insert(Todo(todo_edit.text.toString()))
}
//이 코드 자체가 background에서 실행된다( 비동기 )
}
})
*/
}
}
다른 패턴에서의 Activity는 setContentView()를 사용하여 layout과 Activity를 연결하지만
Binding을 사용했기 때문에 setContentView()가 사라진 것을 볼 수 있다.
4. ViewModel 생성.
//AndroidViewModel를 쓰는이유 : context가 필요하기 때문
//ViewModel은 context를 사용할 수 없다
class MainViewModel(application: Application) : AndroidViewModel(application) {
private val db = Room.databaseBuilder(
application,
AppDatabase::class.java, "database-name"
)
.build()
var todos : LiveData<List<Todo>>
var newTodo : String?=null
init{
todos = getAll()
}
fun getAll(): LiveData<List<Todo>> {
return db.todoDao().getAll()
}
fun insert(todo: String) {
viewModelScope.launch(Dispatchers.IO) {
db.todoDao().insert(Todo(todo)) }
}
}
val binding = DataBindingUtil
.setContentView<ActivityMainBinding>(this, R.layout.activity_main)
binding.lifecycleOwner = this // LiveData를 사용하기 위해서 없으면 Observe할때마다 refresh안딤
setContentView()를 사용하지 않고 DataBinding을 사용하여 layout과 Activity를 연결시킨다.(View)
//ViewModelProviders를 사용하여 ViewModel을 불러온다.
val viewModel = ViewModelProviders.of(this)[MainViewModel::class.java]
binding.viewModel = viewModel//layout의 binding 객체의 name = viewModel 에 viewModel을 초기화
layout의 변수 viewModel 에 실제 ViewModel을 초기화하여 View 단에서 UI 변경시 ViewModel의 Data에 접근할 수 있다.
//LiveData 형식의 Data를 관찰하여 바뀔때마다 불린다??
viewModel.getAll().observe(this, object : Observer<List<Todo>?> {
override fun onChanged(t: List<Todo>?) {
result_text.text = t.toString()
}
})
layout에 DataBinding을 하였기 떄문에 MainViewModel의 LivewData가 변경될 떄마다
자동적으로 layout의 binding 객체를 통해 Data 전송?
LiveData를 사용하게 되면 이처럼 observe()를 통하여 Data가 변경될 때마다 불리우는 코드를 작성해야하지만
이미 DataBinding을 사용했기때문에 주석처리했다.
DataBinding을 사용하지 않는 LiveData를 사용할 때는 이처럼 observe()에서 UI를 변경해주어야한다.
/*
add_button.setOnClickListener(object : View.OnClickListener {
override fun onClick(v: View?) {
//비동기 처리
//Dispatchers.IO -> Worker Thread (백그라운드 )
lifecycleScope.launch(Dispatchers.IO) {
viewModel.insert(Todo(todo_edit.text.toString()))
}
//이 코드 자체가 background에서 실행된다( 비동기 )
}
})
*/
버튼을 클릭시 발생하는 Event 코드인데 Coroutine을 사용하여 BackGround에서 비동기로 처리했다.