RxJava를 사용해보자

 

순서는 4단계로 이루어진다.

1. Observable 생성

2. Observable에 연산자 적용

3. 작업을 수행할 Thread와 결과를 생성할 Thread를 지정

4. Observer를 Observable에 등록 후 결과를 확인

 

단일 객체와 리스트 객체를 Observable로 생성하는 코드를 비교해보자.

단일 객체
리스트 객체

예제 코드를 올리겠다.

 

Observable의 데이터 형태이다.

 

우리는 이 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
    }

}

MainActivity내의 코드이다.

 val taskObservable = Observable // create a new Observable object
            .fromIterable<Task>(DataSource().createTasksList()) // apply 'fromIterable' operator
            .subscribeOn(Schedulers.io()) // designate worker thread (background)
            .observeOn(AndroidSchedulers.mainThread()) // designate observer thread (main thread)

        taskObservable.subscribe(object : Observer<Task?> {
            override fun onComplete() {
                Log.d(TAG, "omComplete : called.")


            }

            override fun onSubscribe(d: Disposable) {
                Log.d(TAG, "onSubscribe : called.")
                disposable.add(d)
            }

            override fun onNext(t: Task) {
                Log.d(TAG, "onNext : " + Thread.currentThread().name)
                Log.d(TAG, "onNext : " + t.description)
                  //Thread.sleep(1000)
            }

            override fun onError(e: Throwable) {
                Log.e(TAG, "onError : ", e)
            }
        })

 

taskObservable이라는 Observable 객체를 생성한다.

.fromIterable() -> 이전 class에서 가져온 Data를 넣어 순서대로 돌린다.

.subscribeOn() -> 데이터를 Background에서 뿌린다(?) 

.observeOn() -> 데이터를 Main Thread에서 관찰한다.

 

 

.subscribe() -> 넘어오는 Data를 관찰할 때마다 각각의 Method가 수행된다.

처음 관찰이 진행될 때 onSubscribe()

위에서 발행된(뿌려진) Data를 하나씩 가져올 때마다 onNext() 

모든 Data를 가져왔거나 Complete가 불렸을 때 onComplete()

 

 

 

 

 

 

 

1. Reactive Programming이란?

- Observable한 Object : 다른 객체로부터 Observed 되는 객체

- 속성이 변경되면 Observer에게 알림 (LiveData와 MVVM 패턴과 같다)

 

+ RxJava / RxAndroid vs LiveData / MVVM

 Rx의 기능 중 다수(모두가 아님)를 LiveData와 MVVM 패턴으로 사용 가능하지만 Rx만의 이점이 존재

 

이점 1. Operator - Observable한 Object를 Simple하게 만든다.

       2. Threading - Thread간에 Data를 쉽게 주고받을 수 있다.(Background에서 작업실행 / Main에서 결과 검색)

(이전 포스팅에서 적었던 것 처럼 LiveData를 사용하면 Dispose를 안해도 된다. Dispose에 대해서는 후에 포스팅할 것이다.)

 

 

2. RxJava vs RxAndroid

- RxAndroid는 Android에서 RxJava를 보다 쉽게 쓰게 하는 Component를 갖는다.(Scheduler는 Threading을 쉽게 한다)

- 모든 비동기 작업을 Observable<T>로 해결한다.

 

 

 

 

 

 

 

 

 

이전 포스팅에서 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)

3초마다 새로운 온도를 생성하는 쓰레드를 생성한다.(API를 대신하는 역할)

 

 disposable = manager.updateEvent().subscribe(this::updateView)
        //subscribe하는 Item들을 updateView에서 처리하도록 하겠다 - java8식

 

TemperatureManager에는 주석으로 적어놨다.

 

 

서두는 이전 포스팅에서 썼기 때문에 날리고 예제로 바로 들어가겠다.

 

기본적으로 DataBinding과 LiveData를 사용하여 예제를 진행할 것이며, Model 역할에는 Room(DB)을 사용할 것이다.

 

DataBinding을 사용하기 위해 App단위 Gradle에

android{
    ...
    dataBinding{
        enabled=true
    }
    ...
}

또한 LiveData, Coroutine, ViewModel 를 사용하기 위해 이것들을 추가한다.

implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.2.0-alpha01"
    implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0-alpha01"

    def lifecycle_version = "2.0.0"

    // ViewModel and LiveData
    implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version"

 

 

아무 class 파일에다가 Room을 적게 되면 Alt+Enter를 사용해 import가 가능하다.

import가 정상적으로 진행되면 이러한 dependencies가 자동으로 추가된다.

    implementation 'androidx.room:room-runtime:2.2.3'
    kapt 'androidx.room:room-compiler:2.2.3'

1. import를 진행한 후 Room에 저장한 Data의 Schema를 만들어보자

@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)) }
    }

}

 

5. layout을 생성

<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="viewModel"
        type="com.example.practice_nexterz.MainViewModel"/>
        <!-- Int String 같은것도 됨 -->

</data>
    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">

        <Button
            android:id="@+id/add_button"
            android:onClick="@{() -> viewModel.insert(viewModel.newTodo)}"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="16dp"
            android:layout_marginEnd="16dp"
            android:text="추가"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintTop_toTopOf="parent">

        </Button>

        <EditText
            android:id="@+id/todo_edit"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:ems="10"
            android:hint="할 일"
            android:text="@={viewModel.newTodo}"
            android:inputType="textPersonName"
            app:layout_constraintEnd_toStartOf="@+id/add_button"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent">

        </EditText>

        <TextView
            android:id="@+id/result_text"
            android:text="@{viewModel.todos.toString()}"
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:layout_marginStart="8dp"
            android:layout_marginTop="8dp"
            android:layout_marginEnd="8dp"
            android:layout_marginBottom="16dp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/todo_edit"></TextView>

    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

 


val binding = DataBindingUtil
                .setContentView<ActivityMainBinding>(this, R.layout.activity_main)
        binding.lifecycleOwner = this // LiveData를 사용하기 위해서 없으면 Observe할때마다 refresh안딤
<data>
    <variable
        name="viewModel"
        type="com.example.practice_nexterz.MainViewModel"/>
        <!-- Int String 같은것도 됨 -->

</data>

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에서 비동기로 처리했다.

이 또한 DataBinding을 사용하였기 때문에 주석처리했다.

이를 사용하는 건 layout 내에서 사용.

 

 

 


Layout

<data>
    <variable
        name="viewModel"
        type="com.example.practice_nexterz.MainViewModel"/>
        <!-- Int String 같은것도 됨 -->

</data>

Layout내에서 DataBinding을 사용하여 쓰고싶은 변수를 선언

 

<Button
            android:id="@+id/add_button"
            android:onClick="@{() -> viewModel.insert(viewModel.newTodo)}"

위의 onclickListener()를 주석처리한 이유

클릭 시 -> viewModel의 insert()를 호출하는데 매개변수로는 viewModel.newTodo를 사용

 

 

<EditText
            ...
            android:text="@={viewModel.newTodo}"
           ...

        </EditText>

양방향 DataBinding이다.

단방향으로써는 ViewModel의 Data를 View가 사용하는 것이고

양방향은 View에서 어떠한 Resource가 변경될 때마다 VIewModel의 Data에 갱신한다.

 

 <TextView
            android:id="@+id/result_text"
            android:text="@{viewModel.todos.toString()}"

단방향 DataBinding의 한 예이다.

ViewModel의 Data를 가져와 text를 변경한다.

 

 


아직까지 제대로 이해할 수 없는 디자인 패턴이다. 

간략히 이해한 바로는 View는 layout과 Activity등이 존재하고 setContentView()가 아닌 DataBinding을 통해 ViewModel과 Layout을 연결한다.

LiveData를 사용하여 실시간으로 Data의 변경을 체크할 수 있다.

 

View에서는 UI적인 코드들만 작성

ViewModel에서 Data와 관련된 것들을 작성(LOGIC)

View는 직접 ViewModel에 Data를 요구하기보다는 LiveData와 DataBinding을 통해 변경을 체크하여 Resource를 변경한다.

 

좀더 연구를 해봐야겠지만 이 예제를 통해 막막한 내 머리를 조금은 뚫어준 것 같다.

 

예제를 자세히 따라하고 싶다면

https://www.youtube.com/watch?v=pG6OkJ3rSjg&list=PLxTmPHxRH3VXHOBnaGQcbSGslbAjr8obc&index=1

에서 따라하는 게 좋다. 굉장히 잘 설명하는 강의이다.

 

+ Recent posts