วิธีลดความซับซ้อนของการทำงานพร้อมกันด้วยการสร้างแบบจำลองปฏิกิริยาบน Android

เผยแพร่แล้ว: 2022-03-11

การทำงานพร้อมกันและความไม่ตรงกันนั้นมีอยู่ในการเขียนโปรแกรมมือถือ

การจัดการกับภาวะพร้อมกันผ่านการเขียนโปรแกรมในรูปแบบความจำเป็น ซึ่งเป็นสิ่งที่การเขียนโปรแกรมบน Android เกี่ยวข้องโดยทั่วไป อาจเป็นสาเหตุของปัญหามากมาย การใช้ Reactive Programming กับ RxJava คุณสามารถหลีกเลี่ยงปัญหาการทำงานพร้อมกันที่อาจเกิดขึ้นได้โดยการจัดเตรียมโซลูชันที่สะอาดกว่าและมีแนวโน้มที่จะเกิดข้อผิดพลาดน้อยลง

นอกเหนือจากการลดความซับซ้อนของงานแบบอะซิงโครนัสที่เกิดขึ้นพร้อมกันแล้ว RxJava ยังให้ความสามารถในการดำเนินการรูปแบบฟังก์ชันที่เปลี่ยนรูป รวม และรวมการปล่อยก๊าซเรือนกระจกจาก Observable จนกว่าเราจะบรรลุผลตามที่ต้องการ

ด้วยการรวมกระบวนทัศน์ปฏิกิริยาของ RxJava และการดำเนินการรูปแบบการทำงาน เราสามารถสร้างแบบจำลองโครงสร้างการทำงานพร้อมกันได้หลากหลายในลักษณะปฏิกิริยา แม้แต่ในโลกที่ไม่มีปฏิกิริยาของ Android ในบทความนี้ คุณจะได้เรียนรู้วิธีดำเนินการดังกล่าว คุณจะได้เรียนรู้วิธีนำ RxJava ไปใช้กับโปรเจ็กต์ที่มีอยู่แบบค่อยเป็นค่อยไป

หากคุณยังใหม่กับ RxJava ขอแนะนำให้อ่านโพสต์ที่นี่ ซึ่งจะกล่าวถึงปัจจัยพื้นฐานบางประการของ RxJava

การเชื่อมที่ไม่เกิดปฏิกิริยากับโลกปฏิกิริยา

ความท้าทายประการหนึ่งในการเพิ่ม RxJava เป็นหนึ่งในไลบรารี่ในโครงการของคุณคือมันเปลี่ยนวิธีที่คุณให้เหตุผลเกี่ยวกับโค้ดของคุณโดยพื้นฐาน

RxJava ต้องการให้คุณคิดว่าข้อมูลถูกผลักแทนที่จะถูกดึง ในขณะที่แนวคิดนั้นเรียบง่าย การเปลี่ยน codebase แบบเต็มที่อิงตามกระบวนทัศน์การดึงอาจเป็นเรื่องที่น่ากลัวเล็กน้อย แม้ว่าความสม่ำเสมอจะเหมาะสมที่สุดเสมอ แต่คุณอาจไม่มีสิทธิ์ทำการเปลี่ยนแปลงนี้ตลอดฐานรหัสทั้งหมดของคุณในคราวเดียวเสมอไป ดังนั้นอาจจำเป็นต้องใช้วิธีการเพิ่มเติมเพิ่มเติม

พิจารณารหัสต่อไปนี้:

 /** * @return a list of users with blogs */ public List<User> getUsersWithBlogs() { final List<User> allUsers = UserCache.getAllUsers(); final List<User> usersWithBlogs = new ArrayList<>(); for (User user : allUsers) { if (user.blog != null && !user.blog.isEmpty()) { usersWithBlogs.add(user); } } Collections.sort(usersWithBlogs, (user1, user2) -> user1.name.compareTo(user2.name)); return usersWithBlogs; }

ฟังก์ชันนี้รับรายการออบเจ็กต์ User จากแคช กรองแต่ละรายการโดยพิจารณาจากว่าผู้ใช้มีบล็อกหรือไม่ จัดเรียงตามชื่อผู้ใช้ และส่งกลับไปยังผู้โทรในท้ายที่สุด เมื่อดูข้อมูลโค้ดนี้ เราสังเกตเห็นว่าการดำเนินการหลายอย่างเหล่านี้สามารถใช้ประโยชน์จากตัวดำเนินการ RxJava ได้ เช่น filter() และ sorted()

การเขียนข้อมูลโค้ดนี้ใหม่ทำให้เรา:

 /** * @return a list of users with blogs */ public Observable<User> getUsersWithBlogs() { return Observable.fromIterable(UserCache.getAllUsers()) .filter(user -> user.blog != null && !user.blog.isEmpty()) .sorted((user1, user2) -> user1.name.compareTo(user2.name)); }

บรรทัดแรกของฟังก์ชันจะแปลง List<User> ที่ส่งคืนโดย UserCache.getAllUsers() เป็น Observable<User> ผ่าน fromIterable() นี่เป็นขั้นตอนแรกในการทำให้โค้ดของเรามีปฏิกิริยา ขณะนี้ เรากำลังดำเนินการบน Observable ซึ่งช่วยให้เราสามารถดำเนินการตัวดำเนินการ Observable ใดๆ ในชุดเครื่องมือ RxJava – filter() และ sorted sorted() ในกรณีนี้

มีจุดอื่นๆ ที่ควรทราบเกี่ยวกับการเปลี่ยนแปลงนี้

ประการแรก ลายเซ็นเมธอดจะไม่เหมือนเดิมอีกต่อไป นี่อาจไม่ใช่เรื่องใหญ่หากการเรียกใช้เมธอดนี้ใช้เพียงไม่กี่แห่ง และง่ายต่อการเผยแพร่การเปลี่ยนแปลงไปยังพื้นที่อื่นๆ ของสแต็ก อย่างไรก็ตาม ถ้ามันทำให้ไคลเอ็นต์เสียหายโดยอาศัยวิธีนี้ นั่นก็เป็นปัญหาและควรเปลี่ยนลายเซ็นของเมธอด

ประการที่สอง RxJava ได้รับการออกแบบโดยคำนึงถึงความเกียจคร้าน นั่นคือไม่ควรดำเนินการเป็นเวลานานเมื่อไม่มีสมาชิกของ Observable ด้วยการปรับเปลี่ยนนี้ สมมติฐานนั้นจะไม่เป็นจริงอีกต่อไปเนื่องจาก UserCache.getAllUsers() ถูกเรียกใช้ก่อนที่จะมีสมาชิก

ออกจากโลกปฏิกิริยา

เพื่อแก้ไขปัญหาแรกจากการเปลี่ยนแปลงของเรา เราสามารถใช้ประโยชน์จากตัวดำเนินการการบล็อกที่มีให้ Observable เช่น blockingFirst() และ blockingNext() โดยพื้นฐานแล้วโอเปอเรเตอร์ทั้งสองนี้จะบล็อกจนกว่าไอเท็มจะถูกปล่อยออกมา: blockingFirst() จะส่งคืนองค์ประกอบแรกที่ปล่อยออกมาและเสร็จสิ้น ในขณะที่ blockingNext() จะส่งคืน Iterable ซึ่งช่วยให้คุณดำเนินการ for-each วนซ้ำบนข้อมูลพื้นฐาน ( การวนซ้ำแต่ละครั้งผ่านลูปจะบล็อก)

ผลข้างเคียงของการใช้การดำเนินการบล็อกที่มีความสำคัญที่ควรทราบคือ ข้อยกเว้นจะถูกส่งต่อไปยังเธรดที่เรียก แทนที่จะถูกส่งไปยัง onError() ของผู้สังเกตการณ์

การใช้ตัวดำเนินการบล็อกเพื่อเปลี่ยนวิธีการลายเซ็นกลับเป็น List<User> ข้อมูลโค้ดของเราจะมีลักษณะดังนี้:

 /** * @return a list of users with blogs */ public List<User> getUsersWithBlogs() { return Observable.fromIterable(UserCache.getAllUsers()) .filter(user -> user.blog != null && !user.blog.isEmpty()) .sorted((user1, user2) -> user1.name.compareTo(user2.name)) .toList() .blockingGet(); }

ก่อนที่จะเรียกตัวดำเนินการบล็อก (เช่น blockingGet() ) เราต้องโยงตัวดำเนินการรวม toList() เพื่อให้สตรีมถูกแก้ไขจาก Observable<User> เป็น Single<List<User>> ( Single เป็นประเภทพิเศษ ของ Observable ที่ปล่อยค่าเดียวใน onSuccess() หรือข้อผิดพลาดผ่าน onError() )

หลังจากนั้น เราสามารถเรียกตัวดำเนิน blockingGet() ซึ่งเปิด Single และส่งคืน List<User>

แม้ว่า RxJava จะสนับสนุนสิ่งนี้ แต่ควรหลีกเลี่ยงสิ่งนี้ให้มากที่สุดเท่าที่จะเป็นไปได้เนื่องจากนี่ไม่ใช่การเขียนโปรแกรมเชิงโต้ตอบที่มีสำนวน เมื่อจำเป็นจริงๆ ตัวดำเนินการบล็อกเป็นวิธีเริ่มต้นที่ดีในการก้าวออกจากโลกแห่งปฏิกิริยา

วิธีขี้เกียจ

ดังที่ได้กล่าวไว้ก่อนหน้านี้ RxJava ได้รับการออกแบบโดยคำนึงถึงความเกียจคร้าน กล่าวคือ การดำเนินการระยะยาวควรล่าช้าให้นานที่สุด (เช่น จนกว่าจะมีการเรียกใช้การสมัครรับข้อมูลบน Observable ) เพื่อให้การแก้ปัญหาของเราขี้เกียจ เราใช้ตัวดำเนินการ defer()

defer() ใช้ในโรงงาน ObservableSource ซึ่งสร้าง Observable สำหรับผู้สังเกตการณ์ใหม่แต่ละคนที่สมัครรับข้อมูล ในกรณีของเรา เราต้องการส่งคืน Observable.fromIterable(UserCache.getAllUser()) ทุกครั้งที่ผู้สังเกตการณ์สมัครรับข้อมูล

 /** * @return a list of users with blogs */ public Observable<User> getUsersWithBlogs() { return Observable.defer(() -> Observable.fromIterable(UserCache.getAllUsers())) .filter(user -> user.blog != null && !user.blog.isEmpty()) .sorted((user1, user2) -> user1.name.compareTo(user2.name)); }

ตอนนี้การดำเนินการที่ใช้เวลานานถูกรวมไว้ใน defer() เราก็มีการควบคุมอย่างเต็มที่ว่าเธรดใดที่ควรรันในเธรดนี้โดยการระบุตัว Scheduler ที่เหมาะสมใน subscribeOn() การเปลี่ยนแปลงนี้ทำให้โค้ดของเรามีปฏิกิริยาตอบสนองโดยสมบูรณ์ และการสมัครรับข้อมูลจะเกิดขึ้นในเวลาที่จำเป็นต้องใช้ข้อมูลเท่านั้น

 /** * @return a list of users with blogs */ public Observable<User> getUsersWithBlogs() { return Observable.defer(() -> Observable.fromIterable(UserCache.getAllUsers())) .filter(user -> user.blog != null && !user.blog.isEmpty()) .sorted((user1, user2) -> user1.name.compareTo(user2.name)) .subscribeOn(Schedulers.io()); }

โอเปอเรเตอร์ที่มีประโยชน์อีกตัวหนึ่งสำหรับการเลื่อนการคำนวณคือ fromCallable() ซึ่งแตกต่างจาก defer() ซึ่งคาดว่า Observable จะถูกส่งกลับในฟังก์ชันแลมบ์ดาและในทางกลับกัน "แผ่" Observable ที่ส่งคืน fromCallable() จะเรียกใช้แลมบ์ดาและคืนค่าดาวน์สตรีม

 /** * @return a list of users with blogs */ public Observable<User> getUsersWithBlogs() { final Observable<List<User>> usersObservable = Observable.fromCallable(() -> UserCache.getAllUsers()); final Observable<User> userObservable = usersObservable.flatMap(users -> Observable.fromIterable(users)); return userObservable.filter(user -> user.blog != null && !user.blog.isEmpty()) .sorted((user1, user2) -> user1.name.compareTo(user2.name)); }

เดี่ยวโดยใช้ fromCallable() ในรายการจะส่งคืน Observable<List<User>> เราจำเป็นต้องทำให้รายการนี้เรียบขึ้นโดยใช้ flatMap()

รีแอคทีฟ-ทุกอย่าง

จากตัวอย่างก่อนหน้านี้ เราพบว่าเราสามารถรวมออบเจ็กต์ใดๆ ไว้ใน Observable และข้ามไปมาระหว่างสถานะที่ไม่เกิดปฏิกิริยาและปฏิกิริยาได้โดยใช้การดำเนินการบล็อกและ defer() / fromCallable() เมื่อใช้โครงสร้างเหล่านี้ เราสามารถเริ่มแปลงพื้นที่ของแอพ Android ให้เป็นรีแอกทีฟได้

การดำเนินงานที่ยาวนาน

จุดเริ่มต้นที่ดีในการคิดที่จะใช้ RxJava คือเมื่อใดก็ตามที่คุณมีกระบวนการที่ต้องใช้เวลาดำเนินการ เช่น การโทรเครือข่าย (ดูตัวอย่างในโพสต์ก่อนหน้า) การอ่านและเขียนดิสก์ ฯลฯ ตัวอย่างต่อไปนี้แสดงให้เห็นฟังก์ชันง่ายๆ ที่ จะเขียนข้อความไปยังระบบไฟล์:

 /** * Writes {@code text} to the file system. * * @param context a Context * @param filename the name of the file * @param text the text to write * @return true if the text was successfully written, otherwise, false */ public boolean writeTextToFile(Context context, String filename, String text) { FileOutputStream outputStream; try { outputStream = context.openFileOutput(filename, Context.MODE_PRIVATE); outputStream.write(text.getBytes()); outputStream.close(); return true; } catch (Exception e) { e.printStackTrace(); return false; } }

เมื่อเรียกใช้ฟังก์ชันนี้ เราต้องตรวจสอบให้แน่ใจว่าทำงานบนเธรดที่แยกต่างหาก เนื่องจากการดำเนินการนี้ถูกบล็อก การกำหนดข้อจำกัดดังกล่าวสำหรับผู้โทรทำให้สิ่งต่างๆ ซับซ้อนสำหรับนักพัฒนา ซึ่งเพิ่มโอกาสของข้อบกพร่องและอาจทำให้การพัฒนาช้าลง

การเพิ่มความคิดเห็นในฟังก์ชันจะช่วยหลีกเลี่ยงข้อผิดพลาดของผู้โทรได้อย่างแน่นอน แต่ก็ยังห่างไกลจากการกันกระสุน

อย่างไรก็ตาม การใช้ RxJava ทำให้เรารวมสิ่งนี้เข้ากับ Observable ได้อย่างง่ายดายและระบุตัว Scheduler ที่ควรเรียกใช้ ด้วยวิธีนี้ ผู้เรียกไม่จำเป็นต้องกังวลกับการเรียกใช้ฟังก์ชันในเธรดแยกต่างหาก ฟังก์ชันนี้จะดูแลเอง

 /** * Writes {@code text} to the filesystem. * * @param context a Context * @param filename the name of the file * @param text the text to write * @return An Observable emitting a boolean indicating whether or not the text was successfully written. */ public Observable<Boolean> writeTextToFile(Context context, String filename, String text) { return Observable.fromCallable(() -> { FileOutputStream outputStream; outputStream = context.openFileOutput(filename, Context.MODE_PRIVATE); outputStream.write(text.getBytes()); outputStream.close(); return true; }).subscribeOn(Schedulers.io()); }

การใช้ fromCallable() การเขียนข้อความไปยังไฟล์จะถูกเลื่อนออกไปจนกว่าจะถึงเวลาสมัคร

เนื่องจากข้อยกเว้นเป็นอ็อบเจ็กต์ระดับเฟิร์สคลาสใน RxJava ข้อดีอีกประการหนึ่งของการเปลี่ยนแปลงของเราคือเราไม่จำเป็นต้องห่อการดำเนินการในบล็อก try/catch อีกต่อไป ข้อยกเว้นจะแพร่กระจายไปตามกระแสน้ำแทนที่จะถูกกลืนกิน สิ่งนี้ทำให้ผู้โทรสามารถจัดการกับข้อยกเว้นที่เขา/เธอเห็นว่าเหมาะสม (เช่น แสดงข้อผิดพลาดให้ผู้ใช้เห็นโดยขึ้นอยู่กับข้อยกเว้นที่ถูกส่งออกไป เป็นต้น)

การปรับให้เหมาะสมอีกอย่างหนึ่งที่เราสามารถทำได้คือการส่งคืน Completable แทนที่จะเป็น Observable Completable นั้นเป็น Observable ชนิดพิเศษซึ่งคล้ายกับ Single ซึ่งบ่งชี้ว่าการคำนวณสำเร็จผ่าน onComplete() หรือล้มเหลวผ่าน onError() การส่งคืน Completable ดูเหมือนจะสมเหตุสมผลมากกว่าในกรณีนี้ เนื่องจากดูเหมือนโง่ที่จะคืนค่า True เดียวในสตรีม Observable

 /** * Writes {@code text} to the filesystem. * * @param context a context * @param filename the name of the file * @param text the text to write * @return A Completable */ public Completable writeTextToFile(Context context, String filename, String text) { return Completable.fromAction(() -> { FileOutputStream outputStream; outputStream = context.openFileOutput(filename, Context.MODE_PRIVATE); outputStream.write(text.getBytes()); outputStream.close(); }).subscribeOn(Schedulers.io()); }

เพื่อให้การดำเนินการเสร็จสมบูรณ์ เราใช้การดำเนินการ fromAction() ของ Completable เนื่องจากค่าที่ส่งกลับไม่เป็นที่สนใจของเราอีกต่อไป หากจำเป็น เช่น Observable Completable ก็รองรับ fromCallable() และ defer()

การแทนที่การโทรกลับ

จนถึงตอนนี้ ตัวอย่างทั้งหมดที่เราดูได้ปล่อยค่าใดค่าหนึ่ง (เช่น สามารถจำลองเป็น Single ได้) หรือบอกเราว่าการดำเนินการสำเร็จหรือล้มเหลว (เช่น สามารถจำลองเป็น Completable ได้)

แม้ว่าเราจะแปลงพื้นที่ในแอปของเราที่ได้รับการอัปเดตหรือเหตุการณ์อย่างต่อเนื่อง (เช่น การอัปเดตตำแหน่ง ดูเหตุการณ์การคลิก เหตุการณ์เซ็นเซอร์ และอื่นๆ) ได้อย่างไร

เราจะดูสองวิธีในการทำเช่นนี้โดยใช้ create() และการใช้ Subjects

create() ช่วยให้เราสามารถเรียกใช้ onNext() | . ของผู้สังเกตการณ์ได้อย่างชัดเจน onComplete() | onError() เมธอดในขณะที่เราได้รับการอัปเดตจากแหล่งข้อมูลของเรา ในการใช้ create() เราส่ง ObservableOnSubscribe ซึ่งรับ ObservableEmitter ทุกครั้งที่ผู้สังเกตการณ์สมัครรับข้อมูล ด้วยการใช้อีซีแอลที่ได้รับ เราสามารถดำเนินการเรียกตั้งค่าที่จำเป็นทั้งหมดเพื่อเริ่มรับการอัปเดต จากนั้นเรียกใช้เหตุการณ์ Emitter ซีแอลที่เหมาะสม

ในกรณีของการอัปเดตตำแหน่ง เราสามารถลงทะเบียนเพื่อรับการอัปเดตในสถานที่นี้และปล่อยการอัปเดตตำแหน่งตามที่ได้รับ

 public class LocationManager { /** * Call to receive device location updates. * @return An Observable emitting location updates */ public Observable<Location> observeLocation() { return Observable.create(emitter -> { // Make sure that the following conditions apply and if not, call the emitter's onError() method // (1) googleApiClient is connected // (2) location permission is granted final LocationRequest locationRequest = new LocationRequest(); locationRequest.setInterval(1000); locationRequest.setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY); LocationServices.FusedLocationApi.requestLocationUpdates(googleApiClient, locationRequest, new LocationListener() { @Override public void onLocationChanged(Location location) { if (!emitter.isDisposed()) { emitter.onNext(location); } } }); }); } }

ฟังก์ชันภายในการเรียก create() จะร้องขอการอัปเดตตำแหน่งและส่งผ่านในการเรียกกลับที่จะถูกเรียกใช้เมื่อตำแหน่งของอุปกรณ์เปลี่ยนแปลง ดังที่เราเห็นที่นี่ โดยพื้นฐานแล้วเราจะแทนที่อินเทอร์เฟซแบบเรียกกลับและปล่อยตำแหน่งที่ได้รับในสตรีม Observable ที่สร้างขึ้นแทน (เพื่อจุดประสงค์ด้านการศึกษา ฉันข้ามรายละเอียดบางส่วนด้วยการสร้างคำขอตำแหน่ง หากคุณต้องการเจาะลึก สามารถอ่านรายละเอียดเพิ่มเติมได้ที่นี่)

อีกสิ่งหนึ่งที่ควรทราบเกี่ยวกับ create() คือเมื่อใดก็ตามที่มีการเรียก subscribe() จะมีการจัดเตรียมอีซีแอลใหม่ กล่าวอีกนัยหนึ่ง create() ส่งคืน Observable แบบเย็น ซึ่งหมายความว่าในฟังก์ชันด้านบน เราอาจร้องขอการอัปเดตตำแหน่งหลายครั้ง ซึ่งไม่ใช่สิ่งที่เราต้องการ

เพื่อแก้ไขปัญหานี้ เราต้องการเปลี่ยนฟังก์ชันเพื่อคืนค่า Observable ที่ร้อนแรงด้วยความช่วยเหลือของ Subjects

ใส่หัวเรื่อง

Subject ขยาย Observable และใช้งาน Observer ในเวลาเดียวกัน สิ่งนี้มีประโยชน์อย่างยิ่งเมื่อใดก็ตามที่เราต้องการส่งหรือส่งกิจกรรมเดียวกันไปยังสมาชิกหลายคนพร้อมกัน ในแง่การปฏิบัติ เราต้องการเปิดเผย Subject เป็น Observable ให้กับลูกค้า ในขณะที่เก็บไว้เป็น Subject สำหรับผู้ให้บริการ

 public class LocationManager { private Subject<Location> locationSubject = PublishSubject.create(); /** * Invoke this method when this LocationManager should start listening to location updates. */ public void connect() { final LocationRequest locationRequest = new LocationRequest(); locationRequest.setInterval(1000); locationRequest.setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY); LocationServices.FusedLocationApi.requestLocationUpdates(googleApiClient, locationRequest, new LocationListener() { @Override public void onLocationChanged(Location location) { locationSubject.onNext(location); } }); } /** * Call to receive device location updates. * @return An Observable emitting location updates */ public Observable<Location> observeLocation() { return locationSubject; } }

ในการใช้งานใหม่นี้ มีการใช้ประเภทย่อย PublishSubject ซึ่งจะส่งเหตุการณ์เมื่อมาถึงโดยเริ่มจากเวลาที่สมัครใช้งาน ดังนั้น หากมีการสมัครรับข้อมูล ณ จุดที่ปล่อยการอัปเดตตำแหน่งแล้ว ผู้สังเกตการณ์จะไม่ได้รับการปล่อยมลพิษในอดีต เฉพาะการปล่อยที่ตามมาเท่านั้น หากไม่ต้องการพฤติกรรมนี้ มีประเภทย่อย Subject อื่นๆ สองสามประเภทในชุดเครื่องมือ RxJava ที่สามารถใช้ได้

นอกจากนี้ เรายังได้สร้างฟังก์ชันการ connect() แยกต่างหากซึ่งเริ่มคำขอเพื่อรับการอัปเดตตำแหน่ง observeLocation() ยังคงทำการเรียก connect() ได้ แต่เราได้ทำการปรับโครงสร้างใหม่ออกจากฟังก์ชันเพื่อความชัดเจน/ความเรียบง่าย

สรุป

เราได้พิจารณากลไกและเทคนิคต่างๆ ดังนี้

  • defer() และตัวแปรเพื่อชะลอการประมวลผลการคำนวณจนถึงการสมัครสมาชิก
  • Cold Observables ที่สร้างขึ้นผ่าน create()
  • Observables ร้อนโดยใช้ Subjects
  • การดำเนินการ blockingX เมื่อเราต้องการออกจากโลกปฏิกิริยา

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

หากคุณสนใจที่จะเรียนรู้เพิ่มเติมเกี่ยวกับ RxJava ฉันกำลังดำเนินการเกี่ยวกับหนังสือเชิงลึกที่อธิบายวิธีดูปัญหาด้วยวิธีตอบสนองโดยใช้ตัวอย่าง Android หากคุณต้องการรับข้อมูลอัปเดต โปรดสมัครรับข่าวสารที่นี่