Author: Sun Quan

Frequently asked questions during development

Writing quality software is difficult and complex, and the Snowball client team often encountered the following problems in previous development:

  • Less standard architecture to follow: Traditional development tends to result in a lot of repetitive code in the Activity/Fragment layer. In MVP mode, due to the mutual coupling between V/P layers, it is not perfect from the perspective of code layer (one-way reference between layers), and business reuse of P layer cannot be achieved.

  • Not consistent with the single responsibility principle: in the traditional MVP model, since V and P are one-to-one, if the business is very complicated, P will assume a lot of responsibility;

  • Life cycle is not easy to manage: in fact, most apps do not need to deal with complex application scenarios such as screen rotation, but even so, we often need to pay attention to whether the running network requests need to be stopped after the page is closed, whether null Pointers will be caused, or even memory leaks.

  • Bad for unit testing: Qa typically writes unit test cases for business logic, but without a separate business logic layer, it is very bad to implement;

  • Coding styles are not uniform: Without a uniform coding style, it will be very difficult for everyone to do business requirements, or help others debug code, or conduct code reviews, so it is important to have a framework that allows everyone to write code with a similar style.

Overall, the original MVP architecture is a good way to organize code, but there are still many problems when developing large software and handling complex business logic, and a highly available page architecture is needed to solve these problems.

An overview of the

Snowball Page architecture, using a set of open source libraries, combined with functional responsiveness, MVVM thinking, to achieve a reusable, testable, lifecycle safety, requirements focused development framework.

It also represents a set of good development practices that are a good way to develop any software application.

RxJava support

The implementation of the architecture idea is based on RxJava and its related technical solutions, such as RxRelay, RxLifecycle, etc.

  • RxRelay: combination of Observer and Subscriber. Provides a support base for observable data sources in MVVM;

  • RxLifecycle: The bindToLifecycle method is used to implement the RxJava streaming API how to safely bind/unbind the lifecycle and prevent problems caused by memory leaks.

In general, both RxJava and LiveData are recommended libraries for Android Architecture Components. LiveData is a relatively new library, and RxJava has more powerful features, such as support for chain operations, stream operators, and exception handling. At the same time, the whole team also has certain technical precipitation for RxJava, so we choose RxJava as the technical support of the framework.

It is also important to note that the framework focuses on specifications, and there may be multiple technical scenarios for implementation. Technology selection is a very important part, but it is not the focus of this paper.

The MVVM architecture

  • View: Direct interaction with the user;

  • ViewModel: Develop for minimal business requirements;

  • Model: A place where specific business logic is implemented, layered by business type;

  • Repository: Data source providing layer, including network data, local data, system services, etc.

Best practices

The following sections illustrate how to follow the Snowball architecture specification to implement a specific requirement through a best practice.

Note: Obviously there is no one solution that will meet all requirements. The purpose of the Snowball architecture specification is simply to provide a solution that addresses most of the requirements and keeps most of the project implementations consistent.

demand

Take the need to display details of snowball body comments as an example. The details of comments are returned through the REST API provided by the server. In addition to opening the interface, users can also load more comment information by pull-up:

Interface implementation

UI layer CommentsDetailActivity kt, corresponding layout file is activity_comments_detail. The XML. Also, assume that the comment detail POJO returned by the server is commentsdetail.kt. Prepare for these, we can create a CommentsDetailViewModel. Kt for UI layer provides data and accept user operation.

So far we have written four files:

  • CommentsDetail.kt

  • CommentsDetailActivity.kt

  • activity_comments_detail.xml

  • CommentsDetailViewModel.kt

Some code snippets are as follows:

`class CommentsDetailViewModel: XQViewModel() {` `fun loadCommentsDetail(articleId: String) {... }` `}` `class CommentsDetailActivity: Activity() {` `//... ` `var articleId: String` `var viewModel: CommentsDetailViewModel` `var refreshLayout: RefreshLayout` `override fun onCreate(savedInstanceState: Bundle?) {` ` / /... ` `articleId = getIntent().getString("ARTICLE_ID")` `viewModel = CommentsDetailViewModel()` `viewModel.loadCommentsDetail(articleId)` `refreshLayout.setOnLoadMoreListener {` `viewModel.loadCommentsDetail(articleId)` `}` `}` `}`Copy the code

The next thing to do is to connect CommentsDetailViewModel to CommentsDetailActivity: We need to write a semaphore to the CommentsDetailActivity in the ViewModel to get comment details: when the data is loaded successfully, set the value for this property; The interface layer listens for this value, and when it changes, it refreshes the interface, which is where RxRelay is needed.

RxRelay can be replaced with the RxJava native Subject, and normally the two are not significantly different. However, if an Error signal is inadvertently received due to coding negligence, using Subject will result in the subsequent signal never being received.

RxRelay provides various types of Relay (most of the time using PublishRelay to solve the problem) that are both producers and consumers and can be used as an implementation of MVVM semaphores based on this feature.

The code for CommentsDetailViewModel then becomes:

class CommentsDetailViewModelXQViewModel(a){
  val commentsDetail = XQSignal.create<CommentsDetail>()
 
  fun loadCommentsDetail(articleId: String) {
  commentsModel.loadCommentsDetail(articleId)
  .subscribe{comments -> commentsDetail.call(comments)}
  }
}

Copy the code

Where commentsModel is what we call the business logic layer, loadCommentsDetail is responsible for loading the comment data. The code for CommentsDetailActivity becomes:

class CommentsDetailActivityActivity(a){
 / /...
 override fun onCreate(savedInstanceStatus: Bundle?) {
 // ...
 
 bindViewModel()
  viewModel.loadCommentsDetail(articleId)
  refreshLayout.setOnLoadMoreListener {
     viewModel.loadCommentsDetail(articleId)
  }
 }
 
 private fun bindViewModel(a) {
 viewModel.commentsDetail.subscribe{comments -> updateCommentsUI(comments)}
 }
}

Copy the code

Next, the loading process is likely to be a network failure or a business exception due to various permission-related issues, such as users not having permission to view certain big-V comments.

As with the logic for loading comments, we design the semaphore for these exceptions:

`class CommentsDetailViewModel: XQViewModel() {` `val commentsDetail = XQSignal<CommentsDetail>.create()` `val loadingError = XQSignal<String>.create()`  `fun loadCommentsDetail(articleId: String) {` `commentsModel.loadCommentsDetail(articleId).subscribe(` `{comments -> commentsDetail.modify(comments)},` '{throwable ->' if(throwable is ApiException) loadingError.modify(throwable.getMessage()) else Loadingerror. modify(" network exception ")}) ' '} ' '} ' 'class CommentsDetailActivity: Activity() {' //... ` `private fun bindViewModel() {` `viewModel.commentsDetail.subscribe{comments -> updateCommentsUI(comments)}` `viewModel.loadingError.subscribe{errorMessage -> showErrorMessage(errorMessage)}` `}` `}`Copy the code

If different exceptions require different exception presentations, such as network load failure using Toast to display copywriting, but no permissions may need to close the page, then design more error semaphores:

`class CommentsDetailViewModel: XQViewModel() {` `val commentsDetail = XQSignal<CommentsDetail>.create()` `val loadingError = XQSignal<String>.create()`  `val loadingErrorNoPermission = XQSignal<String>.create()` `fun loadCommentsDetail(articleId: String) {` `commentsModel.loadCommentsDetail(articleId).subscribe(` `{comments -> commentsDetail.modify(comments)},` `{throwable ->` `if(throwable is ApiNoPermissionException) loadingErrorNoPermission.modify(throwable.getMessage())// No access else if(throwable is ApiException) loadingError.modify(throwable.getMessage()) else Loadingerror. modify(" network error ")}) ' '} ' ' '} ' ' '} 'Copy the code

The key to understanding here is to treat the “exception” itself as a “normal” business logic:

`class CommentsDetailActivity: Activity() {` `//... ` `private fun bindViewModel() {` `viewModel.commentsDetail.subscribe{comments -> updateCommentsUI(comments)}` `viewModel.loadingError.subscribe{errorMessage -> showErrorMessage(errorMessage)}` `viewModel.loadingErrorNoPermission.subscribe{errorMessage -> showErrorMessage(errorMessage)}` `}` `}`Copy the code

This completes the View and ViewModel layers.

Business logic implementation

So far, we’ve combined the View and ViewModel pretty well. Let’s look at the implementation inside CommentsModel.

Most server-side interfaces are currently compatible with Restful design principles, so Retrofit is recommended for handling network requests:

`interface ApiRepository {`
 `@GET(/article/{articleId})`
 `fun loadCommentsDetail(@Path("articleId") articleId: String): Observable<CommentsDetail>` 
`}`

Copy the code

Model is mainly responsible for the implementation of business logic, such as data caching.

One idea mentioned in the ViewModel is to treat the “exception” itself as a “normal” business logic. It’s the model layer’s job to translate all the “normal” or “exceptions” into a “business logic” :

`class CommentsModel: XQModel {`
 `val apiRepo = ApiRepository.getInstance()`
 `val cacheDao = CommentsCacheDao.getInstance()`
 `fun loadCommentsDetail(articleId: String): Observable<CommentsDetail>` 
 `= cacheDao.getComments(articleId).concatWith(apiRepo.loadCommentsDetail(articleId))`
`}`

Copy the code

The life cycle

Since the View(Activity/Fragment) is coupled with the ViewModel using RxRelay, we can use RxLifecycle to decoupled the View when needed (close/rotate the screen, etc.) :

`class CommentsDetailActivity: RxActivity() {` `//... ` `private fun bindViewModel() {` `viewModel.commentsDetail` `.compose(bindToLifecycle())` `.subscribe{comments -> updateCommentsUI(comments)}` `viewModel.loadingError` `.compose(bindToLifecycle())` `.subscribe{errorMessage -> showErrorMessage(errorMessage)}` `viewModel.loadingErrorNoPermission` `.compose(bindToLifecycle())` `.subscribe{errorMessage -> showErrorMessage(errorMessage)}` `}` `}`Copy the code

Second requirement

In snowball page architecture, a View can be flexibly connected to multiple ViewModels.

There was a requirement to add a “like comment” feature in an iteration:

At this point we recommend creating a new FabulousViewModel to handle this requirement, in CommentsDetailActivity:

`class CommentsDetailActivity: RxActivity() {` `var commentsDetailViewModel: CommentsDetailViewModel` `var fabulousViewModel: FabulousViewModel` `var btnFabulousButton: Button` `override fun onCreate(savedInstanceStatus: Bundle?) {` ` / /... ` `bindViewModel()` `btnFabulousButton.setOnClickListener {` `fabulousViewModel.fabulousComment(commentId)` `}` `//... ` `}` `private fun bindViewModel() {` `commentsDetailViewModel.commentsDetail.subscribe{comments -> updateCommentsUI(comments)}` `commentsDetailViewModel.loadingError.subscribe{errorMessage -> showErrorMessage(errorMessage)}` `commentsDetailViewModel.loadingErrorNoPermission.subscribe{errorMessage -> showErrorMessage(errorMessage)}` `fabulousViewModel.fabulousSuccess.subscribe{success -> showMessage(success.result)}` `fabulousViewModel.fabulousFailure.subscribe{errorMessage -> fabulousError(success.result)}` `}` `}`Copy the code

There are some obvious benefits to writing different functions in different ViewModels:

  • Single responsibility (SRP principle) : clearer logic;

  • Reuse: The same functionality can be reused in a variety of places. This example is a very common scenario (APP’s comment list, favorites list, text and so on will use the “like” function; There is also APP check and update function).

The final architecture

The following image shows the modules of the Snowball page architecture and how they interact:

Looking at the overall architecture from another perspective (see figure below), the point is not to use a few rings, but rather the principle of dependency, where code dependencies are from the outside in, and the code in the inner layer doesn’t know anything in the outer layer. In other words: The more stable the inner layer, the smaller the change:

Snowball’s page architecture is also called Onion architecture because it looks like an “onion” shape, adhering to the idea that “separation is for better combination”.

Xueqiu – onion framework

In order to facilitate business use and avoid direct operation of complex RxJava operators, Snowball Android team abstract RxJava and the overall architecture, developed xueqiw-Onion framework, simplify the use of costs, so that developers pay more attention to the business itself. The framework mainly includes the following contents:

  • Thread switching: custom Transformer, including IO thread, CPU intensive calculation thread, UI thread, etc.

  • Event source subscription: unified processing of service abnormalities and normal subscriber. The developer only needs to send semaphores in the corresponding subscriber.

  • Life cycle binding: Lets developers not worry about ViewModel life cycle, memory leaks, etc.

  • Semaphore: secondary encapsulation for RxRelay;

  • DI container: Use the DI container to create viewModels and models.

  • .

summary

Content Review:

  • Some common problems in client development;

  • Snowball architecture specification overview and related technology introduction (RxJava, MVVM, etc.);

  • Demonstrate through a best practice how to follow the snowball architecture specification to implement specific requirements;

  • The architecture diagram shows how each module of snowball Onion framework interacts with each other.

  • This paper introduces xueqiu-Onion framework based on snowball architecture specification.

Snowball client team, through the transformation of the page architecture, has greatly improved the existing project code confusion, unclear hierarchy, poor code reuse and scalability, and is flexible enough to adapt to the ever-changing project and requirements. This is where the Snowball Onion architecture comes in. It represents a set of good best practices that are a good choice in any software development.

Of course, no architecture is “once and for all”, and we need to continue to explore and optimize it on the road of architecture evolution.

reference

Google’s official App Development architecture Guide

RxJava official documentation

Domain-Driven Design with Onion Architecture

One more thing

Snowball business is developing by leaps and bounds, and the engineer team is looking forward to joining us. If you are interested in “being the premier online wealth management platform for Chinese people”, we hope you can join us. Click “Read the article” to check out the hot jobs.

Hot positions: Big front-end architect, Android/iOS/FE engineer, recommendation algorithm engineer, Java development engineer.