The foreword 0.

I had a chat with my colleague last week. My colleague said that their company’s current app architecture mode is MVP mode, but it is not forced to use generics and inheritance and other methods. It all depends on developers to create a presenter in an Activity or Fragment to handle it. It all depends on the developer. This brings me to a thinking, application of architecture or design patterns, in addition to the traditional low coupling and high cohesion, business separation, I think there is a more important point is used to constraint developers, although use patterns or architecture may not save the amount of code, some may even increase coding, but let developers in certain rules for development, Ensuring consistency, especially if the project is large and requires teamwork, is extremely important. Some time ago, Google announced JetPack, which aims to help developers build an app faster. Based on this, I wrote this project template for some encapsulation, to provide a support for my own app writing in the future.

1. What is MVVM

The MVVM design pattern is very similar to that of MVP, except that Presenter is a ViewModel, and viewModels are bound to views.

MVP

MVVM

I did not use this standard bidirectional binding MVVM in my project. Instead, I used a single bound MVVM to update the UI by listening for changes in the data, and then changing the UI by changing the data when needed. Refer to Google’s official documentation for the specific App architecture

2. Framework combination

The entire template uses a combination of Retrofit for network requests, ViewModel for data storage for reuse, and LiveData for notification of UI data changes. This article assumes that you are already familiar with ViewModels and LiveData.

3. Key code analysis

3.1 the Retrofit

First of all, we use Retrofit for network requests, and Retrofit returns Call by default, but because we want changes in data to be observable and UI aware, we need to wrap the data with LiveData, which I won’t explain in detail here. Just remember that it is a data structure that can be observed to change during the Activity or Fragment life cycle. As you know Retrofit uses an adapter to determine whether a network request will return a Call or something else, so we need to write the adapter that returns the result first to return a LiveData, right

class LiveDataCallAdapterFactory : CallAdapter.Factory() {

    override fun get(
        returnType: Type,
        annotations: Array<Annotation>,
        retrofit: Retrofit
    ): CallAdapter<*, *>? {
        if (CallAdapter.Factory.getRawType(returnType) ! = LiveData::class.java) {return null
        }
        val observableType = CallAdapter.Factory.getParameterUpperBound(0, returnType as ParameterizedType)
        val rawObservableType = CallAdapter.Factory.getRawType(observableType)
        if(rawObservableType ! = ApiResponse::class.java) { throw IllegalArgumentException("type must be a resource")}if(observableType ! is ParameterizedType) { throw IllegalArgumentException("resource must be parameterized")
        }
        val bodyType = CallAdapter.Factory.getParameterUpperBound(0, observableType)
        return LiveDataCallAdapter<Any>(bodyType)
    }
}

class LiveDataCallAdapter<R>(private val responseType: Type) : CallAdapter<R, LiveData<ApiResponse<R>>> {


    override fun responseType() = responseType

    override fun adapt(call: Call<R>): LiveData<ApiResponse<R>> {
        return object : LiveData<ApiResponse<R>>() {
            private var started = AtomicBoolean(false)
            override fun onActive() {
                super.onActive()
                if (started.compareAndSet(false.true)) {
                    call.enqueue(object : Callback<R> {
                        override fun onResponse(call: Call<R>, response: Response<R>) {
                            postValue(ApiResponse.create(response))
                        }

                        override fun onFailure(call: Call<R>, throwable: Throwable) {
                            postValue(ApiResponse.create(throwable))
                        }
                    })
                }
            }
        }
    }
}
Copy the code

First look at the LiveDataCallAdapter, here in the adat method we return a LiveData<ApiResponse>, the ApiResponse is a layer of encapsulation of the returned result. Because we may return to a network error, or some special circumstances special processing, these can be done inside the ApiResponse again, then watch LiveDataCallAdapterFactory, returns a LiveDataCallAdapter, It also forces your interface to define a network request that returns a LiveData<ApiResponse> structure. When used

.object GitHubApi {

    var gitHubService: GitHubService = Retrofit.Builder()
        .baseUrl("https://api.github.com/")
        .addCallAdapterFactory(LiveDataCallAdapterFactory())
        .addConverterFactory(GsonConverterFactory.create())
        .build().create(GitHubService::class.java)


}

interface GitHubService {

    @GET("users/{login}")
    fun getUser(@Path("login") login: String): LiveData<ApiResponse<User>>

}

Copy the code

3.2 Processing of ApiResponse

The returned result is processed using NetWorkResource, and the data is converted to Resource and packaged into LiveData for outgoing.

abstract class NetWorkResource<ResultType, RequestType>(val executor: AppExecutors) {

    private val result = MediatorLiveData<Resource<ResultType>>()

    init {
        result.value = Resource.loading(null)
        val dbData=loadFromDb()
        if (shouldFetch(dbData)) {
            fetchFromNetWork()
        }
        else{
            setValue(Resource.success(dbData))
        }
    }

    private fun setValue(resource: Resource<ResultType>) {

        if(result.value ! = resource) { result.value = resource } } private funfetchFromNetWork() {
        val networkLiveData = createCall()

        result.addSource(networkLiveData, Observer {

            when (it) {

                is ApiSuccessResponse -> {
                    executor.diskIO().execute {
                        val data = processResponse(it)
                        executor.mainThread().execute {
                            result.value = Resource.success(data)
                        }
                    }
                }

                is ApiEmptyResponse -> {
                    executor.diskIO().execute {
                        executor.mainThread().execute {
                            result.value = Resource.success(null)
                        }
                    }
                }

                is ApiErrorResponse -> {
                    onFetchFailed()
                    result.value = Resource.error(it.errorMessage, null)
                }

            }

        })

    }

    fun asLiveData() = result as LiveData<Resource<ResultType>>

    abstract fun onFetchFailed()

    abstract fun createCall(): LiveData<ApiResponse<RequestType>>

    abstract fun processResponse(response: ApiSuccessResponse<RequestType>): ResultType

    abstract fun shouldFetch(type: ResultType?) : Boolean abstract fun loadFromDb(): ResultType? }Copy the code

This is an abstract class. Notice its abstract methods, which determine whether to use cached data or network requests and how to handle the results of network requests. The AppExecutor is used to update LiveData in the main thread and process network request results in the child thread. You can then simply return an anonymous inner class in Repository and duplicate the corresponding abstract method.

class UserRepository {

    private val executor = AppExecutors()

    fun getUser(userId: String): LiveData<Resource<User>> {

        return object : NetWorkResource<User, User>(executor) {
            override fun shouldFetch(type: User?) : Boolean {return true
            }

            override fun loadFromDb(): User? {
                return null
            }

            override fun onFetchFailed() {

            }

            override fun createCall(): LiveData<ApiResponse<User>> = GitHubApi.gitHubService.getUser(userId)

            override fun processResponse(response: ApiSuccessResponse<User>): User {
                return response.body
            }

        }.asLiveData()
    }

}
Copy the code

3.3 Simple ENCAPSULATION of UI

abstract class VMActivity<T : BaseViewModel> : BaseActivity() {

    protected lateinit var mViewModel: T

    abstract fun loadViewModel(): T

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        mViewModel = loadViewModel()
        lifecycle.addObserver(mViewModel)
    }
}
Copy the code

By using integration and generics, you force the developer to return a ViewMode when inheriting the class. In use as follows.

class MainActivity : VMActivity<MainViewModel>() {
    override fun loadViewModel(): MainViewModel {
        returnMainViewModel() } override fun getLayoutId(): Int = R.layout.activity_main override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) mViewModel.loginResponseLiveData.observe(this, Observer { when (it? .status) { Status.SUCCESS -> { contentTV.text = it.data? .reposUrl } Status.ERROR -> { contentTV.text ="error"
                }

                Status.LOADING -> {
                    contentTV.text = "loading"
                }

            }


        })
        loginBtn.setOnClickListener {
            mViewModel.login("skateboard1991")}}}Copy the code

4. Making the address

The whole Github project is a simple demo of Git to obtain user information. There are still many deficiencies, which will be gradually improved in the subsequent application process.

Follow my official account

Reference 5.

Github.com/googlesampl…