This reverts commit b37cf2203e
.
15 KiB
Create a Clean-Code App with Kotlin Coroutines and Android Architecture Components
Full demo weather app included.
Android development is evolving fast. A lot of developers and companies are trying to address common problems and create some great tools or libraries that can totally change the way we structure our apps.
We get excited by the new possibilities, but it’s difficult to find time to rewrite our app to really benefit from a new programming style. But what if we actually start a new project? Which of those breakthrough ideas to employ? Which solutions are stable enough? Should we use RxJava extensively and structure our app with reactive-first mindset?
The Cycle.js library (by André Staltz) contains a great explanation of reactive-first mindset: Cycle.js — Streams.
Rx is highly composable and it has great potential, but it’s so different from regular object-oriented programming style, that it will be really hard to understand for any developer without RxJava experience.
There are more questions to ask before starting a new project. For example:
-
Should we use Kotlin instead of Java? (actually here the answer is simple: YES)
-
Should we use experimental Kotlin Coroutines? (which, again, promote totally new programming style)
-
Should we use the new experimental library from Google: Android Architecture Components?
It’s necessary to try it all first in a small app to really make an informed decision. This is exactly what I did, getting some useful insights in the process. If you want to find out what I learned, read on!
About The App
The aim of the experiment was to create an app that downloads weather data for cities selected by user and then displays forecasts with graphical charts (and some fancy animations). It’s simple, yet it contains most of the typical features of Android projects.
It turns out that coroutines and architecture components play really well together and give us clean app architecture with good separation of concerns. Coroutines allow to express ideas in a natural and concise way. Suspendable functions are great if you want to code line-by-line the exact logic you have in mind — even if you need to make some asynchronous calls in between.
Also: no more jumping between callbacks. In this example app, coroutines also completely removed the need of using RxJava. Functions with suspendable points are easier to read and understand than some RxJava operator chains — these chains can quickly become too _functional. _ ;-)
Having said that, I don’t think that RxJava can be replaced with coroutines in every use case. Observables give us a different kind of expressiveness that can not be mapped one to one to suspendable functions. In particular once constructed observable operator chain allow many events to flow through it, while a suspendable point resumes only once per invocation.
Back to our weather app: You can watch it in action below — but beware, I’m not a designer. :-) Chart animations show how easily you can implement them arbitrarily by hand with simple coroutine — without any ObjectAnimators, Interpolators, Evaluators, PropertyValuesHolders, etc.
** 此处有Canvas,请手动处理 **
** 此处有iframe,请手动处理 **
The most important source code snippets are displayed below. However, if you’d like to see the full project, it’s available on GitHub.
https://github.com/elpassion/crweather
There is not a lot of code and it should be easy to go through.
I will present the app structure starting from the network layer. Then I will move to the business logic (in the MainModel.kt file) which is (almost) not Android-specific. And finish with the UI part (which obviously is Android-specific).
Here is the general architecture diagram with text reference numbers added for your convenience. I will especially focus on green elements — suspendable functions and actors (an actor is a really useful kind of coroutine builder ).
The actor model in general is a mathematical model of concurrent computation — more about it in my next blog post.
01 Weather Service
This service downloads weather forecasts for a given city from Open Weather Map REST API.
I use simple but powerful library from Square called Retrofit. I guess by now every Android developer knows it, but in case you never used it: it’s the most popular HTTP client on Android. It makes network calls and parses responses to POJO. Nothing fancy here — just a typical Retrofit configuration. I plug in the Moshi converter to convert JSON responses to data classes.
https://github.com/elpassion/crweather/…/OpenWeatherMapApi.kt
One important thing to note here is that I set a return types of functions generated by Retrofit to: Call.
I use Call.enqueue(Callback) to actually make a call to Open Weather Map. I don’t use any call adapter provided by Retrofit, because I wrap the Call object in the suspendable function myself.
02 Utils
This is where we enter the (brave new) coroutines world: we want to create a generic suspendable function that wraps a Call object.
I assume you know at least the very basics of coroutines. Please read the first chapter of Coroutines Guide (written by Roman Elizarov) if you don’t.
It will be an extension function: suspend fun Call.await() that invokes the Call.enqueue(…) (to actually make a network call), then suspends and later resumes (when the response comes back).
** 此处有Canvas,请手动处理 **
https://github.com/elpassion/crweather/…/CommonUtils.kt
To turn any asynchronous computation into a suspendable function, we use the suspendCoroutine function from The Kotlin Standard Library. It gives us a Continuation object which is kind of a universal callback. We just have to call its resume method (or resumeWithException) anytime we want our new suspendable function to resume (normally or by throwing an exception).
The next step will be to use our new suspend fun Call.await() function to convert asynchronous functions generated by Retrofit into convenient suspendable functions .
03 Repository
The Repository object is a source of the data (charts) displayed in our app.
https://github.com/elpassion/crweather/…/Repository.kt
Here we have some private suspendable functions created by applying our suspend fun Call.await() extension to weather service functions. This way all of them return ready to use data like Forecast instead of Call. Then we use it in our one public suspendable function : suspend fun getCityCharts(city: String): List. It converts the data from api to a ready to display list of charts. I use some custom extension properties on List to actually convert the data to List. Important note: only suspendable functions can call other suspendable functions .
We have the appid hardcoded here for simplicity. Please generate new appid hereif you want to test the app — this hardcoded one will be automatically blocked for 24h if it is used too frequently by too many people.
In the next step we will create the main app model (implementing the Android ViewModel architecture component), that uses an actor (coroutine builder) to implement the application logic.
04 Model
In this app we only have one simple model: MainModel : ViewModel used by our one activity: MainActivity.
https://github.com/elpassion/crweather/…/MainModel.kt
This class represents the app itself. It will be instantiated by our activity (actually by the Android system ViewModelProvider), but it will survive configuration changes such as a screen rotation — new activity instance will get the same model instance. We don’t have to worry about activity lifecycle here at all. Instead of implementing all those activity lifecycle related methods (onCreate, onDestroy, …), we have just one onCleared() method called when the user exits the app.
To be precise onCleared method is called when the activity is finished.
Even though we are not tightly coupled to activity lifecycle anymore, we still have to somehow publish current state of our app model to display it somewhere (in the activity). This is where the LiveData comes into play.
The LiveData is like RxJava BehaviorSubject reinvented once again… It holds a mutable value that is observable. The most important difference is how we subscribe to it and we will see it later in the MainActivity.
Also LiveData doesn’t have all those powerful composable operators Observable has. There are only some simple Transformations.
Another difference is that LiveData is Android-specific and RxJava subjects are not, so they can be easily tested with regular non-android JUnit tests.
Yet another difference is that LiveData is “lifecycle aware” — more about it in my next posts, where I present the MainActivity class.
In here we are actually using the MutableLiveData : LiveData objects that allow to push new values into it freely. The app state is represented by four LiveData objects: city, charts, loading, and message. The most important of these is the charts: LiveData<List> object which represents current list of charts to display.
All the work of changing the app state and reacting to user actions is performed by an ACTOR .
Actors are awesome and will be explained in my next blog post :-)
Summary
We have already prepared everything for our main actor . And if you look at the actor code itself — you can (kind of) see how it works even without knowing coroutines or actors theory. Even though it has only a few lines, it actually contains all important business logic of this app. The magic is where we call suspendable functions (marked by gray arrows with green line). One suspendable point is the iteration over user actions and second is the network call. Thanks to coroutines it looks like synchronous blocking code but it doesn’t block the thread at all.
Stay tuned for my next post, where I will explain actors (and channels ) in detail.
作者:Marek Langiewicz 译者:译者ID 校对:校对者ID