23 August 2016

มารู้จักกับ RxJava และ RxAndroid กันเถอะ [ตอนที่ 3]

Updated on


        หลังจากที่หายหัวไปพักนึงเพราะ Pokemon GO ทำพิษ ในที่สุดก็ได้กลับมาเขียนต่อซักทีกับ RxJava และ RxAndroid ตอนที่ 3 ที่จะมาต่อเนื่องจากบทความตอนเก่าที่อธิบายโค้ดเบื้องต้นนะครับ และคราวนี้ก็มาดูกันว่าในการใช้งานทั่วๆไปผู้ที่หลงเข้ามาอ่านสามารถใช้งานยังไงได้บ้าง

ทบทวนความทรงจำ

        ย้อนกลับไปที่คำสั่งสุดท้ายในบทความที่แล้วจะเป็นแบบนี้

TextView tvUserId = ...

...

public String getUserId() {
    ...
}

...

Observable.fromCallable(new Callable<String>() {
            @Override
            public String call() throws Exception {
                return getUserId();
            }
        })
        .subscribeOn(Schedulers.io())
        .observeOn(AndroidSchedulers.mainThread())
        .subscribe(new Observer<String>() {
            @Override
            public void onCompleted() {
                Log.d("Rx", "onCompleted");
            }

            @Override
            public void onError(Throwable e) {
                Log.d("Rx", "onError");
                tvUserId.setText("-");
            }

            @Override
            public void onNext(String id) {
                Log.d("Rx", "onNext");
                tvUserId.setText(id);
            }
        });

        ก่อนจะเริ่มเจาะลึกไปที่ Observable, Subscriber และ Operator เจ้าของบล็อกขอนำเสนอรูปแบบการสร้างและใช้งาน Rx ก่อนดีกว่า ว่าจริงๆแล้วมันไม่จำเป็นต้องออกมาเป็นโค้ดรูปแบบนี้เสมอไปนะ ที่จะต้องมาสร้าง Observable แล้วประกาศ Subscriber ในทันที

Observable และ Subscriber สามารถสร้างแยกกันได้

        ผู้ที่หลงเข้ามาอ่านสามารถสร้าง Observable เตรียมไว้ก่อนได้เลย แล้วค่อยกำหนด Subscriber ทีหลัง

Observable<String> userIdObservable = Observable.fromCallable(new Callable<String>() {
    @Override
    public String call() throws Exception {
        return getUserId();
    }
});

        หรือจะกำหนด Operator อื่นๆเตรียมไว้เลยก็ได้เช่นกัน

Observable<String> userIdObservable = Observable.fromCallable(new Callable<String>() {
    @Override
    public String call() throws Exception {
        return getUserId();
    }
}).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread());

        พอจะใช้งานก็ค่อยเรียกจาก userIdObservable แล้วกำหนด Subscriber เข้าไปได้เลย

userIdObservable.subscribe(new Observer<String>() {
    @Override
    public void onCompleted() {
        ...
    }

    @Override
    public void onError(Throwable e) {
        ...
    }

    @Override
    public void onNext(String s) {
        ...
    }
});

        และในขณะเดียวกัน Subscriber ไม่จำเป็นต้องสร้างแบบ Annonymous เสมอไปเช่นกันสามารถสร้างเตรียมไว้ได้เลย

Observer<String> userIdObserver = new Observer<String>() {
    @Override
    public void onCompleted() {
        ...
    }

    @Override
    public void onError(Throwable e) {
        ...
    }

    @Override
    public void onNext(String s) {
        ...
    }
};

        หรือก็คือสามารถสร้าง Observable เตรียมไว้ และประกาศ Subscriber ไว้ให้พร้อม แล้วค่อยเอาค่าทั้งสองมากำหนดเข้าด้วยกันทีหลังก็ได้นั่นเอง

Observable<String> userIdObservable = Observable.fromCallable(new Callable<String>() {
    @Override
    public String call() throws Exception {
        return getUserId();
    }
}).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread());

...

Observer<String> userIdObserver = new Observer<String>() {
    @Override
    public void onCompleted() {
        ...
    }

    @Override
    public void onError(Throwable e) {
        ...
    }

    @Override
    public void onNext(String s) {
        ...
    }
};

...

userIdObservable.subscribe(userIdObserver);

        อย่าสับสนระหว่าง Observable กับ Observer เพราะชื่อมันค่อนข้างคล้ายกัน ซึ่งเจ้าของบล็อกจะเรียก Observer ว่า Subscriber แทนเพื่อลดความสับสน

        และอย่าลืมว่าการกำหนด Observable กับ Subscriber เข้าด้วยกันทีหลังเนี่ย ทั้งคู่จะต้องมีที่คลาสเหมือนกันนะ อย่างในตัวอย่างนี้จะเป็น String นั่นเอง


        นั่นหมายความว่าคุณไม่สามารถเอา Observable และ Subscriber ที่สร้างจากคนละคลาสมาเชื่อมเข้าด้วยกันได้นะ

Observable กับการใช้งานจริง

        เนื่องจาก Observable และ Subscriber ไม่จำเป็นต้องสร้างขึ้นมาพร้อมๆกัน ดังนั้นนักพัฒนาส่วนใหญ่จึงสร้าง Method สำหรับสร้าง Observable ขึ้นมาโดยเฉพาะ เพื่อให้โค้ดมีความกระชับมากขึ้นในเวลาเรียกใช้งาน

public Observable<String> getUserIdObservable() {
    return Observable.fromCallable(new Callable<String>() {
                @Override
                public String call() throws Exception {
                    return getUserId();
                }
            })
            .subscribeOn(Schedulers.io())
            .observeOn(AndroidSchedulers.mainThread());
}

        พอเรียกใช้งานก็จะเหลือแค่นี้

Observer<String> userIdObserver = new Observer<String>() {
    @Override
    public void onCompleted() {
        ...
    }

    @Override
    public void onError(Throwable e) {
        ...
    }

    @Override
    public void onNext(String s) {
        ...
    }
};

...

getUserIdObservable().subscribe(userIdObserver);

ทีนี้มาดูกันต่อที่ Subscriber กันบ้าง 

        จากตัวอย่างที่เจ้าของบล็อกอธิบายตั้งแต่แรกจนถึงตอนนี้ เวลาสร้าง Subscriber ข้ึนมานั้นจะอยู่ในรูปของคลาส Observer ที่จะประกาศไปด้วย Method จำนวน 3 ตัวด้วยกัน คือ onNext, onCompleted และ onError


        ถึงแม้รูปแบบของ Observer จะยืดหยุ่นก็ตาม แต่บ่อยครั้งเจ้าของบล็อกก็รู้สึกไม่ค่อยชอบซักเท่าไรนัก เพราะเวลาสร้าง Observer ขึ้นมาทีไร ต้องประกาศ onNext, onCompleted และ onError ให้ครบทุกครั้ง ทั้งๆที่บางครั้งก็อยากจะใช้แค่ตัวใดตัวหนึ่งเท่านั้น ก็เลยรู้สึกว่าทำแบบนี้แล้วมันสิ้นเปลืองโค้ดไปหลายบรรทัดโดยใช้เหตุ

        อย่างที่บอกในตอนแรกสุดนู้นนนนนน ว่า Rx มีรูปแบบการใช้งานที่ค่อนข้างยืดหยุ่นมาก ดังนั้น Subscriber ก็ไม่ได้มีแค่คลาส Observer เท่านั้นหรอกนะ

รู้จักกับคลาส Action0 และ Action1 (มี Action2 ด้วย แต่ยังไม่พูดถึง)

        แต่นอกเหนือจากคลาส Observer แล้ว ยังมีคลาสที่น่าสนใจอยู่อีก 2 ตัวด้วยกัน นั่นก็คือคลาส Action0 และ Action1 ที่จะมาแทนที่ Observer ได้ เพราะ Method ทั้ง 3 ตัวของ Observer จะมาถูกแทนที่ด้วย Action0 หรือ Action1 นั่นเอง

        ก่อนจะเริ่มทำความเข้าใจ อยากจะให้ดูก่อนว่าเวลาสร้าง Action0 และ Action1 ขึ้นมาแล้วจะเป็นยังไง

Action0 action0 = new Action0() {
    @Override
    public void call() {
        ...
    }
};

Action1<String> action1 = new Action1<String>() {
    @Override
    public void call(String s) {
        ...
    }
};

        Action0 จะเป็นคลาสเปล่าๆที่ข้างในมี Method ที่ชื่อว่า call ในขณะที่ Action1 สามารถกำหนดคลาสที่ต้องการได้ (ในตัวอย่างกำหนดเป็น String) เมื่อสร้างคลาส Action1 ขึ้นมาก็จะมี Method ที่ชื่อว่า call อยู่ในนั้นเช่นกัน แต่จะมี Parameter ที่ส่งเข้ามาใน call เป็น String

        ซึ่งหัวใจสำคัญของ Action0 และ Action1 อยู่ที่ Parameter ใน Method ที่ชื่อว่า call นี่แหละ

        ดังนั้น Action0 ก็คือ Action ที่ไม่มี Parameter ใดๆนั่นเอง ส่วน Action1 ก็คือ Action ที่มี Parameter 1 ตัว โดยสามารถกำหนดได้ตามต้องการ

        ซึ่ง onNext, onCompleted และ onError ใน Observer ก็คือ Action นั่นเอง ดังนั้นใน Observer จะมีทั้งหมด 3 Action แตกต่างกันออกไป โดยมีความสัมพันธ์กันแบบนี้ครับ


        onCompleted ไม่มี Parameter อะไร ดังนั้นจึงเป็น Action0

        onError มี Parameter เป็นคลาส Throwable ดังนั้นจึงเป็น Action1

        onNext มี Parameter เป็นคลาส List<String> (ตามที่กำหนดไว้) ดังนั้นจึงเป็น Action1

แล้ว Action0 และ Action1 มันช่วยให้เกิดประโยชน์อะไร?

        ถ้าดูจากการสร้างคลาส Action0 และ Action1 แล้ว กลายเป็นว่ามันยิ่งเปลืองบรรทัดเข้าไปใหญ่เลย จนรู้สึกว่าใช้คลาส Observer ก็จบแล้วใช่มั้ยล่ะ

        แต่จริงๆแล้ว Action ใช้งานกันแบบนี้ครับ

public void doSomething() {
    getUserIdObservable().doOnNext(getUserIdAction)
                         .doOnError(errorAction)
                         .subscribe();

    getUserProfileObservable().doOnNext(getUserProfileAction)
                              .doOnError(errorAction)
                              .subscribe();
}

private Action1<String> getUserIdAction = new Action1<String>() {
    @Override
    public void call(String userId) {
        // On Next
    }
};

private Action1<Profile> getUserProfileAction = new Action1<Profile>() {
    @Override
    public void call(Profile profile) {
        // On Next
    }
};

private Action1<Throwable> errorAction = new Action1<Throwable>() {
    @Override
    public void call(Throwable e) {
        // On Error
    }
};

        ให้ดูใน Method ที่ชื่อว่า doSomething จะเห็นว่าเจ้าของบล็อกสามารถสร้าง Action ต่างๆเพื่อใช้งานกับ Observable ได้เลย แต่จะแตกต่างจากเดิมตรงนี้การกำหนด Action จะใช้คำสั่งจำพวก doOnNext, doOnCompleted และ doOnError แทน

        เนื่องจากเจ้าของบล็อกรู้สึกว่า onCompleted ไม่จำเป็นซักเท่าไรกับโค้ด ดังนั้นก็ไม่ต้องสร้าง Action0 และไม่ต้องประกาศ doOnCompleted เลย ที่น่าสนใจมากที่สุดก็คือตรง doOnError ที่สามารถกำหนด Action ตัวเดียวกันได้เลย ในกรณีที่ Handle Error เหมือนๆกัน จึงทำให้โค้ดรู้สึกกระชับทำงานเป็นสัดเป็นส่วนอย่างชัดเจน

        เวลาที่ใช้พวก doOnNext, doOnCompleted หรือ doOnError จะต้องจบท้ายด้วยคำสั่ง subscribe ด้วยทุกครั้ง ไม่งั้น Observable จะไม่ทำงาน

        จริงๆจะประกาศแบบนี้ก็ได้นะ แต่ไม่แนะนำซักเท่าไร

getUserIdObservable().doOnNext(new Action1<String>() {
    @Override
    public void call(String s) {
        // On Next
    }
}).doOnCompleted(new Action0() {
    @Override
    public void call() {
        // On Completed
    }
}).doOnError(new Action1<Throwable>() {
    @Override
    public void call(Throwable throwable) {
        // On Error
    }
}).subscribe();

        นอกจาก doOnNext, doOnCompleted และ doOnError แล้วยังมีอีกหลายๆคำสั่งให้ใช้งานด้วยนะเออ เรียกได้ว่ามีเป็น Life Cycle เลยทีเดียว


        และในกรณีที่เรียกใช้คำสั่ง subscribe ก็สามารถโยน Action1 เข้าไปได้เลยนะ แต่จะทำงานแทน onNext เท่านั้น (ถ้าอยากได้ onCompleted กับ onError ด้วย ก็ให้ไปกำหนดที่ doOnCompleted กับ doOnError แทน)

getUserIdObservable().subscribe(new Action1<String>() {
    @Override
    public void call(String s) {
        // On Next
    }
});

        เพิ่มเติม - Subscriber ทุกตัวควรกำหนด onError หรือ doOnError ไว้ด้วยครับ เพื่อที่ว่าเวลาเกิด Crash ใดๆในขณะที่ Obserable ทำงานอยู่ จะได้วิ่งเข้า onError แทน ไม่เช่นนั้นจะ Crash แทน - ขอบคุณ Non Vassanon สำหรับคำแนะนำนี้

เมื่อ Observable เริ่มทำงาน จะต้องมีสิ่งที่เรียกว่า Subscription

        เมื่อเรียกคำสั่ง subscribe ไม่ว่าจะแบบใดก็ตาม จะเป็นการสั่งให้ Observable เริ่มทำงาน ซึ่งคำสั่งนี้จะส่งคลาส Subscription ออกมาด้วย เพื่อเก็บไว้เช็คได้ว่า Observable ตัวนั้นๆกำลังทำงานอยู่หรือป่าว หรือจะสั่งให้หยุดทำงานก็ได้

Subscription getUserIdSubscription = getUserIdObservable()
                .doOnNext(getUserIdAction)
                .doOnError(errorAction)
                .subscribe();

        ในกรณีที่ต้องการให้ Observable หยุดทำงาน (Unsubscribe) สามารถใช้คำสั่งแบบนี้ได้เลย

Subscription getUserIdSubscription = getUserIdObservable().doOnNext(getUserIdAction)
                .doOnError(errorAction)
                .subscribe();

...

if (getUserIdSubscription != null && !getUserIdSubscription.isUnsubscribed()) {
    getUserIdSubscription.unsubscribe();
}

        ก็แค่เช็คว่าเป็น Null มั้ย และ Unsubscribe แล้วหรือยัง ถ้ายังก็สั่ง Unsubscribe เลย แต่ในกรณีที่ Observable ทำงานซับซ้อน มีการใช้คำสั่งที่ใช้ระยะเวลานานหรือทำงานวนลูปตลอด เมื่อสั่ง Unsubscribe ก็จะไม่ได้หยุดทำงานในทันที ซึ่งอันนี้ไว้เดี๋ยวพูดถึงในบทความหน้าครับ

โค้ดกระชับมากขึ้นเมื่อใช้กับ Retrolambda

        เนื่องจาก Lambda จะลดรูปของ Class ใดๆก็ตามที่มี Method อยู่ในนั้นแค่ 1 ตัวให้สั้นลงอย่างน่าใจหาย ดังนั้นถ้าเอามา Combo เข้ากับ Rx ด้วยล่ะก็ โคตรบันเทิงงงงงงงงงง

getUserIdObservable().doOnNext(getUserIdAction) 
                     .doOnError(errorAction) 
                     .subscribe();

...

public Action1<String> getUserIdAction = userId -> {
    // On Next
};

public Action1<Throwable> errorAction = e -> {
    // On Error
};

        หรือแบบนี้ก็ได้เช่นกัน

getUserIdObservable().doOnNext(s -> {
    // On Next
}).doOnCompleted(() -> {
    // On Completed
}).doOnError(throwable -> {
    // On Error
}).subscribe();

        เพิ่มเติม : Lambda ไม่ได้แทน Class นะครับ จริงๆแล้ว Lambda ทำงานคู่กับ Functional Interface หรือที่เดิมเรียกว่า SAM( Single Abstract Method) หรือ SAM-type ซึ่งก็คือ Interface ที่มีแค่หนึ่ง Method เท่านั้น - ขอบคุณ Poohdish Rattanavijai หรือพี่กอล์ฟแห่ง Kaidee ที่มาช่วยเพิ่มเติมให้ครับ

สรุป

        ในบทความนี้เจ้าของบล็อกก็จะแสดงให้เห็นถึงการทำงานของ Observable กับ Subscriber ว่าจริงๆแล้วมันสามารถแยกออกจากกันได้นะ แล้วค่อยเรียกใช้งานร่วมกันทีหลังได้ แต่มีเงื่อนไขว่าต้องเป็น Observable และ Subscribe ที่สร้างมาจากคลาสตัวเดียวกันเท่านั้นถึงจะใช้งานร่วมกันได้

        ซึ่งการแยกโค้ดระหว่าง Observable และ Subscriber ออกจากกันก็จะช่วยให้ผู้ที่หลงเข้ามาอ่านสามารถจัดโค้ดได้เป็นกลุ่มมากขึ้น ทำให้โค้ดที่ได้มีความชัดเจน ดูเข้าใจง่าย ด้วย Pattern ตามแบบฉบับที่ Rx กำหนดไว้นั่นเอง

        และจะเห็นว่าทำไมเจ้าของบล็อกถึงเรียก Observer ว่า Subscriber เพราะจริงๆแล้ว Observer เป็นแค่คลาสตัวหนึ่งเท่านั้นที่ใช้เป็น Subscriber แต่ในความเป็นจริงแล้วยังมี Subscriber ที่น่าสนใจอย่าง Action0 และ Action1 อยู่ ที่จะช่วยให้สามารถจัดโค้ดเป็นกลุ่มก้อนที่ชัดเจน รู้สึกสะอาดตามากขึ้น

        และในบทความ <กำลังเขียนอยู่> เจ้าของบล็อกก็จะพาไปลงรายละเอียดไปกับ Observable กันต่อครับ