Deeply and Simply Understanding Rx Java Evolution Principle

Keywords: Lambda Android Programming

Summary

Rxjava and RxAndroid are becoming more and more common in Android development. The idea of functional programming and coding is still very cool. Today, this article will not introduce the use of RxJava, but how the idea of RxJava evolved from our original coding idea.

A citation

First, introduce a very good post. http://www.devtf.cn/?p=323 This post of the landlord translated the original text with his own thinking.

text

Code:

/**
 * Student
 */
public class Student {
    String name;// Name
    int age;// Age
}

/**
 * Api call
 */
public interface Api {
    /**
     * Find multiple students through a query condition
     */
    List<Student> queryStudents(String query);

    /**
     * Save a student's information and return a Uri value
     */
    Uri save(Student student);
}

/**
 * Management of Students
 */
public class StudentHelper {
    Api api;

    /**
     * Preserve the best students
     */
    public Uri saveTheBestStudent(String query) {
        // Add an exception handling
        try {
            List<Student> students = api.queryStudents(query);
            Student bestStudent = findBestStudent(students);
            return api.save(bestStudent);
        } catch (Exception e) {
            e.printStackTrace();
            return null;// Returns a default value when saving an error
        }
    }

    /**
     * Find the best students
     */
    public Student findBestStudent(List<Student> students) {
        return students.get(0);// The logic here is more arbitrary, and here I've just written one.
    }
}

Very normal code, now add demand, I want to query students and save students when adding callback operation, need to modify Api

/**
 * Api call
 */
public interface Api {
    /**
     * Inquiry for student callbacks
     */
    interface QueryStudentCallBack {
        void onStudentsReceived(List<Student> students);// The query succeeded and the result was returned

        void onQueryError(Exception e);// Query error
    }

    /**
     * Preserving student callbacks
     */
    interface SaveCallBack {
        void onStudentSaved(Uri uri);// Save success and return results

        void onSaveError(Exception e);// Save Error
    }

    /**
     * Find multiple students through a query condition
     */
    List<Student> queryStudents(String query, QueryStudentCallBack queryStudentCallBack);

    /**
     * Save a student's information and return a Uri value
     */
    Uri save(Student student, SaveCallBack saveCallBack);
}

The saveTheBest Student () method in Student Helper includes three things: querying the list of students, querying the best students, and saving the best students. Let's add a callback to save the best students, and modify the Student Helper class.

/**
 * Management of Students
 */
public class StudentHelper {
    public interface SaveTheBestStudentCallBack {
        void onBestStudentSaved(Uri uri);// Save the best student's success and return to Uri

        void onBestStudentSavedError(Exception e);// There was a mistake in preserving the best students
    }

    Api api;

    /**
     * Preserve the best students
     */
    public void saveTheBestStudent(String query, SaveTheBestStudentCallBack saveTheBestStudentCallBack) {
        api.queryStudents(query, new Api.QueryStudentCallBack() {
            @Override
            public void onStudentsReceived(List<Student> students) {
                Student bestStudent = findBestStudent(students);
                api.save(bestStudent, new Api.SaveCallBack() {
                    @Override
                    public void onStudentSaved(Uri uri) {
                        saveTheBestStudentCallBack.onBestStudentSaved(uri);
                    }

                    @Override
                    public void onSaveError(Exception e) {
                        saveTheBestStudentCallBack.onBestStudentSavedError(e);
                    }
                });
            }

            @Override
            public void onQueryError(Exception e) {
                saveTheBestStudentCallBack.onBestStudentSavedError(e);
            }
        });
    }

    /**
     * Find the best students
     */
    public Student findBestStudent(List<Student> students) {
        return students.get(0);// The logic here is more arbitrary, and here I've just written one.
    }
}

Since we can return the result in the callback, we change the return type of the related method to void:
1. Public void saveTheBest Student in Student Helper (... )
2. Two methods in Api, void query students (... void save(... )

So far, because we have knocked out this code one by one, we still have a clear logic about it, but at first glance, the code is more confusing, all kinds of unknown brackets wrapped, all kinds of honey indentation, I'm not sure if I can see this code again in a few days. Now we can see the logic. If we develop more logic in our actual project, the first idea must be: What does he write? It's a nightmare to change logic. It's easy to get BUG. So let's improve it.

The Way to Improvement

There are three callbacks in the above code, but they all do two similar things:
1. Do something and return the result (onBest Student Saved, onStudents Received, onStudent Saved)
2. On Best Student Saved Error (onQuery Error, onSave Error)

When this kind of similar or repetitive operation occurs, it's time for us to refactor.

We can encapsulate it with a generic interface:

/**
 * Generic callback interface
 */
public interface CallBack<T> {
    void onResult(T result);

    void onError(Exception e);
}

After the callback interface is modified to be generic, modify Student Helper accordingly. Our previously customized Save The Best Student CallBack interface can be removed and modified to:

/**
 * Management of Students
 */
public class StudentHelper {
    Api api;

    /**
     * Preserve the best students
     */
    public void saveTheBestStudent(String query, CallBack<Uri> saveTheBestStudentCallBack) {
        api.queryStudents(query, new Api.QueryStudentCallBack() {
            @Override
            public void onStudentsReceived(List<Student> students) {
                Student bestStudent = findBestStudent(students);
                api.save(bestStudent, new Api.SaveCallBack() {
                    @Override
                    public void onStudentSaved(Uri uri) {
                        saveTheBestStudentCallBack.onResult(uri);
                    }

                    @Override
                    public void onSaveError(Exception e) {
                        saveTheBestStudentCallBack.onError(e);
                    }
                });
            }

            @Override
            public void onQueryError(Exception e) {
                saveTheBestStudentCallBack.onError(e);
            }
        });
    }

    /**
     * Find the best students
     */
    public Student findBestStudent(List<Student> students) {
        return students.get(0);// The logic here is more arbitrary. Here I thank you for one.
    }
}

Previously, we only had two call classes, Api and Student Helper. Now we add an ApiWrapper class to simplify the code. We drop the related calls of Api into ApiWrapper, then separate query Students and save, and make them to provide calls. Actual calls are called by Student Helper.

/**
 * Api Packaging classes, intermediate classes of Api and Student Helper
 */
public class ApiWrapper {
    Api api;

    public void queryStudents(String query, CallBack<List<Student>> queryStudentCallBack) {
        api.queryStudents(query, new Api.QueryStudentCallBack() {
            @Override
            public void onStudentsReceived(List<Student> students) {
                queryStudentCallBack.onResult(students);
            }

            @Override
            public void onQueryError(Exception e) {
                queryStudentCallBack.onError(e);
            }
        });
    }

    public void save(Student student, CallBack<Uri> saveCallBack) {
        api.save(student, new Api.SaveCallBack() {
            @Override
            public void onStudentSaved(Uri uri) {
                saveCallBack.onResult(uri);
            }

            @Override
            public void onSaveError(Exception e) {
                saveCallBack.onError(e);
            }
        });
    }
}

Then in the StudentHelper class, you can replace the original Api with ApiWrapper:

/**
 * Management of Students
 */
public class StudentHelper {
    ApiWrapper apiWrapper;

    /**
     * Preserve the best students
     */
    public void saveTheBestStudent(String query, CallBack<Uri> saveTheBestStudentCallBack) {
        apiWrapper.queryStudents(query, new CallBack<List<Student>>() {
            @Override
            public void onResult(List<Student> result) {
                apiWrapper.save(findBestStudent(result), saveTheBestStudentCallBack);
            }

            @Override
            public void onError(Exception e) {
                saveTheBestStudentCallBack.onError(e);
            }
        });
    }

    /**
     * Find the best students
     */
    public Student findBestStudent(List<Student> students) {
        return students.get(0);// The logic here is more arbitrary, and here I've just written one.
    }
}

With the addition of ApiWrapper, Student Helper's code is much simpler, more comfortable to look at, and more clear in logic. So far, this writing has been suitable for our needs, but don't you feel anything else? Now that we have achieved this, we will meet similar requirements next time. The parameters to be passed in are not query of String type, but query of int type or something else. Can we make this model more universal?

Above all, we make callbacks a general pattern through generics. Now, are the queryStudents method of our Api class, the save method and the saveTheBest Student method in the StudentHelper class the same pattern? It's a common parameter plus a callback parameter. Now we have written the callback parameter, so let's change the common parameter.

There is a passage in the original article that any asynchronous operation needs to carry the required conventional parameters and a callback instance object. If we try to separate these stages, each asynchronous operation will carry only one parameter object, and then return some temporary objects with callbacks.

Simply put, this is the case: the request parameter is changed into one, and the object of the callback information is passed out as the return value of the method.

Here we add an abstract class AsyncJob as the object of this CallBack information, and then put the CallBack callback in the original method into the start method of this abstract class. Because the start method is abstract, the instantiation of AsyncJob will be forced to implement the start method and try to modify it:

public abstract class AsyncJob<T> {
    public abstract void start(CallBack<T> callback);
}

Why use abstract classes here? In fact, it is also possible to use interface in this place:

public interface AsyncJob<T> {
    void start(CallBack<T> callback);
}

The reason is... There will be non-abstract methods coming in later...

Okay, then let's modify the query Students and save of saveTheBest Student, ApiWrapper. As we said earlier, these methods are actually the same pattern. Then we change the parameters of the method into one and use AsyncJob as a temporary return value.

/**
 * Api Packaging classes, intermediate classes of Api and Student Helper
 */
public class ApiWrapper {
    Api api;

    public AsyncJob<List<Student>> queryStudents(String query) {
        return new AsyncJob<List<Student>>() {
            @Override
            public void start(CallBack<List<Student>> callback) {
                api.queryStudents(query, new Api.QueryStudentCallBack() {
                    @Override
                    public void onStudentsReceived(List<Student> students) {
                        callback.onResult(students);
                    }

                    @Override
                    public void onQueryError(Exception e) {
                        callback.onError(e);
                    }
                });
            }
        };
    }

    public AsyncJob<Uri> save(Student student) {
        return new AsyncJob<Uri>() {
            @Override
            public void start(CallBack<Uri> callback) {
                api.save(student, new Api.SaveCallBack() {
                    @Override
                    public void onStudentSaved(Uri uri) {
                        callback.onResult(uri);
                    }

                    @Override
                    public void onSaveError(Exception e) {
                        callback.onError(e);
                    }
                });
            }
        };
    }
}

Then Studnet Wrapper:

/**
 * Management of Students
 */
public class StudentHelper {
    ApiWrapper apiWrapper;

    /**
     * Preserve the best students
     */
    public AsyncJob<Uri> saveTheBestStudent(String query) {
        return new AsyncJob<Uri>() {
            @Override
            public void start(CallBack<Uri> callback) {
                apiWrapper.queryStudents(query).start(new CallBack<List<Student>>() {
                    @Override
                    public void onResult(List<Student> result) {
                        apiWrapper.save(findBestStudent(result)).start(new CallBack<Uri>() {
                            @Override
                            public void onResult(Uri result) {
                                callback.onResult(result);
                            }

                            @Override
                            public void onError(Exception e) {
                                callback.onError(e);
                            }
                        });
                    }

                    @Override
                    public void onError(Exception e) {
                        callback.onError(e);
                    }
                });
            }
        };
    }

    /**
     * Find the best students
     */
    public Student findBestStudent(List<Student> students) {
        return students.get(0);// The logic here is more arbitrary, and here I've just written one.
    }
}

In this step, the change of code form is obvious. First, the original two parameters of the method are changed into one. Then the callback method in the original method parameters is called in the start method of AsyncJob. Finally, the return value of the method is changed from the original void to the current AsyncJob.

Convert Student Helper again:

/**
 * Management of Students
 */
public class StudentHelper {
    ApiWrapper apiWrapper;

    /**
     * Preserve the best students
     */
    public AsyncJob<Uri> saveTheBestStudent(String query) {
        AsyncJob<List<Student>> studentsAsync = apiWrapper.queryStudents(query);
        AsyncJob<Student> studentAsync = new AsyncJob<Student>() {
            @Override
            public void start(CallBack<Student> callback) {
                studentsAsync.start(new CallBack<List<Student>>() {
                    @Override
                    public void onResult(List<Student> result) {
                        callback.onResult(findBestStudent(result));
                    }

                    @Override
                    public void onError(Exception e) {
                        callback.onError(e);
                    }
                });
            }
        };
        AsyncJob<Uri> uriAsync = new AsyncJob<Uri>() {
            @Override
            public void start(CallBack<Uri> callback) {
                studentAsync.start(new CallBack<Student>() {
                    @Override
                    public void onResult(Student result) {
                        apiWrapper.save(result).start(new CallBack<Uri>() {
                            @Override
                            public void onResult(Uri result) {
                                callback.onResult(result);
                            }

                            @Override
                            public void onError(Exception e) {
                                callback.onError(e);
                            }
                        });
                    }

                    @Override
                    public void onError(Exception e) {
                        callback.onError(e);
                    }
                });
            }
        };
        return uriAsync;
    }

    /**
     * Find the best students
     */
    public Student findBestStudent(List<Student> students) {
        return students.get(0);// The logic here is more arbitrary. Here I thank you for one.
    }
}

Then let's look at the code in the saveTheBest Student method:

AsyncJob<List<Student>> studentsAsync = apiWrapper.queryStudents(query);
AsyncJob<Student> studentAsync = new AsyncJob<Student>() {
    @Override
    public void start(CallBack<Student> callback) {
        studentsAsync.start(new CallBack<List<Student>>() {
            @Override
            public void onResult(List<Student> result) {
                callback.onResult(findBestStudent(result));
            }
            @Override
            public void onError(Exception e) {
                callback.onError(e);
            }
        });
    }
};

Does it feel like Async Job?

public interface Func<T, R> {
    R call(T t);
}

Quite simply, the Func interface has two type members, T corresponds to the parameter type and R corresponds to the return type.

As mentioned earlier, the function of the previous code is to convert two AsynJob objects. We can probably think of this as follows: AsynJobB = AsynJobA.map() is a conversion formula, so we need to extract the common code into AsynJob:

public <R>AsyncJob<R> map(Func<T,R> func){
        final AsyncJob<T> source = this;
        return new AsyncJob<R>() {
            @Override
            public void start(CallBack<R> callback) {
                source.start(new CallBack<T>() {
                    @Override
                    public void onResult(T result) {
                        R map = func.call(result);
                        callback.onResult(map);
                    }

                    @Override
                    public void onError(Exception e) {
                        callback.onError(e);
                    }
                });
            }
        };
    }

Modify Student Helper

/**
 * Management of Students
 */
public class StudentHelper {
    ApiWrapper apiWrapper;

    /**
     * Preserve the best students
     */
    public AsyncJob<Uri> saveTheBestStudent(String query) {
        AsyncJob<List<Student>> studentsAsync = apiWrapper.queryStudents(query);
        AsyncJob<Student> studentAsync = studentsAsync.map(new Func<List<Student>, Student>() {
            @Override
            public Student call(List<Student> students) {
                return findBestStudent(students);
            }
        }) ;
        AsyncJob<Uri> uriAsync = new AsyncJob<Uri>() {
            @Override
            public void start(CallBack<Uri> callback) {
                studentAsync.start(new CallBack<Student>() {
                    @Override
                    public void onResult(Student result) {
                        apiWrapper.save(result).start(new CallBack<Uri>() {
                            @Override
                            public void onResult(Uri result) {
                                callback.onResult(result);
                            }

                            @Override
                            public void onError(Exception e) {
                                callback.onError(e);
                            }
                        });
                    }

                    @Override
                    public void onError(Exception e) {
                        callback.onError(e);
                    }
                });
            }
        };
        return uriAsync;
    }

    /**
     * Find the best students
     */
    public Student findBestStudent(List<Student> students) {
        return students.get(0);// The logic here is more arbitrary. Here I thank you for one.
    }
}

After this change, is it very cool to see that the conversion process is all internal, you just need to provide the value of the conversion, let's use Lambda expression to see.

AsyncJob<List<Student>> studentsAsync = apiWrapper.queryStudents(query);
AsyncJob<Student> studentAsync = studentsAsync.map(students -> findBestStudent(students));

God, how concise it is!

Next, let's try to modify this code.

AsyncJob<Uri> uriAsync = new AsyncJob<Uri>() {
    @Override
    public void start(CallBack<Uri> callback) {
        studentAsync.start(new CallBack<Student>() {
            @Override
            public void onResult(Student result) {
                apiWrapper.save(result).start(new CallBack<Uri>() {
                    @Override
                    public void onResult(Uri result) {
                        callback.onResult(result);
                    }
                    @Override
                    public void onError(Exception e) {
                        callback.onError(e);
                    }
                });
            }
            @Override
            public void onError(Exception e) {
                callback.onError(e);
            }
        });
    }
};

It can be changed to this way.

AsyncJob<Uri> uriAsync = studentAsync.map(
        new Func<Student, Uri>() {
            @Override
            public Uri call(Student student) {
                return apiWrapper.save(student);
            }
        }
) ;

But there will be an error because we want to return the AsyncJob type, but in fact the call method returns the Uri type, which is embarrassing. There's no way to do this. You need to add another method to AsyncJob to complete this transformation.

public abstract class AsyncJob<T> {
    public abstract void start(CallBack<T> callback);

    public <R>AsyncJob<R> map(Func<T,R> func){
        final AsyncJob<T> source = this;
        return new AsyncJob<R>() {
            @Override
            public void start(CallBack<R> callback) {
                source.start(new CallBack<T>() {
                    @Override
                    public void onResult(T result) {
                        R map = func.call(result);
                        callback.onResult(map);
                    }

                    @Override
                    public void onError(Exception e) {
                        callback.onError(e);
                    }
                });
            }
        };
    }

    public <R>AsyncJob<R>  flatMap(Func<T,AsyncJob<R>> func){
        final AsyncJob<T> source = this;
        return new AsyncJob<R>() {
            @Override
            public void start(CallBack<R> callback) {
                source.start(new CallBack<T>() {
                    @Override
                    public void onResult(T result) {
                        AsyncJob<R> mapped = func.call(result);
                        mapped.start(new CallBack<R>() {
                            @Override
                            public void onResult(R result) {
                                callback.onResult(result);
                            }

                            @Override
                            public void onError(Exception e) {
                                callback.onError(e);
                            }
                        });
                    }

                    @Override
                    public void onError(Exception e) {
                        callback.onError(e);
                    }
                });
            }
        };
    }
}

First, the type of the parameter Func of the method is determined by Func.

/**
 * Management of Students
 */
public class StudentHelper {
    ApiWrapper apiWrapper;

    /**
     * Preserve the best students
     */
    public AsyncJob<Uri> saveTheBestStudent(String query) {
        AsyncJob<List<Student>> studentsAsync = apiWrapper.queryStudents(query);
        AsyncJob<Student> studentAsync = studentsAsync.map(new Func<List<Student>, Student>() {
            @Override
            public Student call(List<Student> students) {
                return findBestStudent(students);
            }
        });
        AsyncJob<Uri> uriAsync = studentAsync.flatMap(new Func<Student, AsyncJob<Uri>>() {
            @Override
            public AsyncJob<Uri> call(Student student) {
                return apiWrapper.save(student);
            }
        });
        return uriAsync;
    }

    /**
     * Find the best students
     */
    public Student findBestStudent(List<Student> students) {
        return students.get(0);// The logic here is more arbitrary. Here I thank you for one.
    }
}

Ultimate simplified version of Lambda expression:

/**
 * Management of Students
 */
public class StudentHelper {
    ApiWrapper apiWrapper;

    /**
     * Preserve the best students
     */
    public AsyncJob<Uri> saveTheBestStudent(String query) {
        AsyncJob<List<Student>> studentsAsync = apiWrapper.queryStudents(query);
        AsyncJob<Student> studentAsync = studentsAsync.map(this::findBestStudent);
        AsyncJob<Uri> uriAsync = studentAsync.flatMap(student -> apiWrapper.save(student));
        return uriAsync;
    }

    /**
     * Find the best students
     */
    public Student findBestStudent(List<Student> students) {
        return students.get(0);// The logic here is more arbitrary. Here I thank you for one.
    }
}

At this point, we have finished the basic principles. I suggest that you follow this logic and knock it out by yourself to understand how the gods think and get such results. Next let's take a look at RxJava, where I'll quote the original text.

  • AsyncJob is actually Observable. It can not only distribute a single result, but also a sequence (it can be empty).
  • Callback is Observer, except Callback is missing the onNext (T) method. In Observer, after the onError (Throwable) method is called, onCompleted() is then called, and then Observer wraps and sends out the event stream (because it can send a sequence).
  • abstract void start(Callback callback) corresponds to Subscription subscribe(final Observer)
public class ApiWrapper {
    Api api;

    public Observable<List<Student>> queryStudents(final String query) {
        return Observable.create(new Observable.OnSubscribe<List<Student>>() {
            @Override
            public void call(final Subscriber<? super List<Student>> subscriber) {
                api.queryStudents(query, new Api.QueryStudentCallBack() {
                    @Override
                    public void onCatListReceived(List<Student> students) {
                        subscriber.onNext(students);
                    }

                    @Override
                    public void onQueryError(Exception e) {
                        subscriber.onError(e);
                    }
                });
            }
        });
    }

    public Observable<Uri> save(final Student student) {
        return Observable.create(new Observable.OnSubscribe<Uri>() {
            @Override
            public void call(final Subscriber<? super Uri> subscriber) {
                api.store(cat, new Api.SaveCallBack() {
                    @Override
                    public void onStudentSaved(Uri uri) {
                        subscriber.onNext(uri);
                    }

                    @Override
                    public void onSaveError(Exception e) {
                        subscriber.onError(e);
                    }
                });
            }
        });
    }
}

public class StudentHelper {

    ApiWrapper apiWrapper;

    public Observable<Uri> saveTheBestStudent(String query) {
        Observable<List<Student>> studentsObservable = apiWrapper.queryStudents(query);
        Observable<Student> bestStudentObservable = studentsObservable.map(new Func1<List<Student>, Student>() {
            @Override
            public Student call(List<Student> students) {
                return StudentHelper.this.findBestStudent(students);
            }
        });
        Observable<Uri> saveUriObservable = bestStudentObservable.flatMap(new Func1<Student, Observable<? extends Uri>>() {
            @Override
            public Observable<? extends Uri> call(Student student) {
                return apiWrapper.save(student);
            }
        });
        return storedUriObservable;
    }

    public Student findBestStudent(List<Student> students) {
        return students.get(0);
    }
}

This is the evolution of Rxjava's map method and flatMap method. This is only a small part of RxJava. The charm and power of Rxjava are much more than that. Let me strengthen our understanding and mastery of Rxjava through our continuous use and learning.

Posted by umer on Wed, 10 Jul 2019 17:44:43 -0700