31

Android Architecture — Personal Best Practices

 5 years ago
source link: https://www.tuicool.com/articles/hit/r6Vvauu
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Android architecture evolves as all of our technical skills do. After a couple of years as Android Developer I found a good architecture for potentially big projects.

This by the way is the most important thing to consider: No architecture is good for every use case.

viQj2i3.png!web

But, as I am very happy with it and it has proven to be a good choice in former bigger projects I would like to share it with all of you, our great community.

This post will cover some basics about

- Kotlin Coroutines

- Architecture Components

- Repository Pattern

- Gradle modules

Project Structure

The project will be modularized per feature and contain a core module. This is insprired by another great blog post:

If you would like to check out the whole project you can find it here

Splitting the project into multiple modules is meant to keep concerns separated and to speed up the build time as the separated modules can be built simultaneously.

Keeping features separated also makes them exchangable. So if the project owner (who can also be you, of course) decides that the whole design has to be changed for one feature and this implies also changes in the ui logic, you can simply unplug the old and plug in the new module. You can even leave them side-by-side as long as you are developing.

The feature setup will more or less look like the following visualization:

fyimie7.png!web
feature based modularization

So let’s have a deeper look at the core module and a feature module .

Core Module

  • provides core dependencies
  • provides data
  • contains core logic
  • hosts extension functions

The core module is meant to host and initialize the dependencies which all modules need and propagate them to the other modules.

Apart from the common dependencies the core module also hosts all extension functions of the project. They should be logically separated into multiple files if there are many, e.g.: ImageViewExtensions.kt , FileExtensions.kt , …

If a database is used it should also be hosted in core. Any other data providing structures and SharedPreferences belong here.

Pay attention to the verbs used here: provide, contain, host.

Feature Module

  • implements module specific dependencies
  • consumes data
  • contains ui logic
  • displays data

What you consider a feature is something I would like to leave to you. Whatever it is, it belongs here. An example for a feature is the registration / login flow of your app. In case of this project it is concerned with the presentation of pokemon cards. So let’s call it card-presentation .

The verbs used for the feature module are implement, consume, contain, display.

Some text analysis (I once studied literature, sorry)

If you compare the verbs used for the modules in the previous paragraph you have a good idea what they should do. Both “contain” their proper logic, which they do not expose to anything outside their scope. But the core module “provides” whereas the feature module “consumes” and “displays”. You get the point, I guess.

Project Setup

The core module is an “Android Library” module which hosts its proper dependencies. Further, as already mentioned, it provides project-wide dependencies to all feature modules which implement the core module . Here you can see a little code snippet from the build.gradle which demonstrates this idea.

Ybqeq2V.png!web

The Kotlin dependencies on the one hand are used in every module of the app so they are transitive, which means they are provided to all implementing modules. That’s why the api keyword is used here. The retrofit dependencies on the other hand should remain private to this module so the implementation keyword is used.

As the core module uses Kotlin Coroutines and they are still marked as experimental they have to be explicitly enabled. I still don’t understand why, by the way, as I use them a lot in production. If you know, don’t tell me or my employer.

The card-presentation module is a “Phone & Tablet” module and it implements the core module

7Zze2in.png!web

As the core module already contains the Kotlin dependencies they can be used without explicitly adding them. Still, we have to enable the experimental Kotlin features. Still, hush.

Data Flow

Provide Data

Reminder: Providing data is something the core module is concerned with.

In this example, the data is consumed from a RESTful API with Retrofit:

After building the retrofit object and creating the service from the upper interface definition the data can be requested and processed. This happens in the Provider class:

The code is wrapped into a Coroutine to be able to already process the response. As soon as the request has sucessfully finished the response body is returned.

In any other case, my prefered solution for error handling is used: Logging.

The data is provided as a Deferred object, which is “a non-blocking cancellable future” as the kotlinx.coroutines reference documentation states. Just a short explanation for those who don’t know: async can return a value whereas launch can’t.

Consume Data

Reminder 2: The card-presentation module is concerned with the consumption of the data. For this purpose it contains a Repository to be able to cache the data from the Provider. For simplicity’s sake this solution uses a Singleton a.k.a Kotlin object instead of a ViewModel (the one from Architecture Components). As a singleton it lives as long as the application. For more complex situations you may need to use a ViewModel, I don’t see a huge advantage, though.

The Repository is the “data track switch” in your application. In other words: The Repository’s purpose is to decide which data source it consumes. You may have a database in your app, you may have SharedPreferences, you may keep the data in your memory or you may have to request the backend. The Repository is the “data blackbox” for the View as it does not expose from where it receives any information.

Here is how it looks like

In this scenario the data is cached in a map and therefore in the apps memory as soon as it has been once requested from the backend. A network request can still be forced by passing true for the “getAllPokemonCards()” function’s parameter “forceReload”. If data is considered out of date for example.

But the class does at least two more things:

Job

A big problem I had in one of my last projects was that I had a long running operation in the background. The network request took ages to finish and I already started fetching the data when opening the application to gain some time. The data of this first request / coroutine was something I depended on from another function. When reaching the target screen I had to know if the job was still running to not start the request again. I tried to model a similar use case for this scenario with the “getPokemonCard()” function. The function relies on the mentioned map which is populated by the first coroutine. The solution to this problem is the job object.

To be aware of the first coroutine’s state the job which the Coroutine returns is assigned to the “allCardsJob” field. In the second function “getPokemonCard()”, another Coroutine, can wait for the “allCardsJob” to finish in case it is still active by joining it.

The rest of the code is sequentially processed after the “allCardsJob” has finished.

Since Coroutine version 0.26.0 “all coroutine builders are now extensions on CoroutineScope and inherit its coroutineContext . Standalone builders are deprecated.”

See more here

More information on Coroutine Scope can be found here

I realized that while writing this article and as I couldn’t use the async Coroutine Builder any more I picked the simplest solution which was just prefixing it with the GlobalScope. There might be a better solution to this problem now.

LiveData

As the data in a list can change it can be useful to provide it in a LiveData object. If the app displays the list in a RecyclerView for example. LiveData can automatically update the RecyclerView’s Adapter everytime the Repository updates the LiveData object with “postValue()”.

To be able to do so the LiveData object should be mutable within the Repository, but immutable from outside the Repository.

More information about LiveData and Architecture Components can be found here

Display Data

Reminder 3: The card-presentation module displays the data. This is usually done in an Activity, Fragment or View.

The Observer, which is part of the Android Lifecycle Framework (see the above link for more information), is called whenever the Repository changes the LiveData. In this case our beautiful TextView is updated with the new values. If LiveData’s value is not null (LiveData.postValue has already been called once) it immediately returns it’s current value.

So finally, I guess you would like to see what this all was good for? A shiny result. I’m so proud. Here you go:

y2quYna.jpg!web

So now go and tell your project owner that you need two modules, a Retrofit Service, a Data Provider, a Repository, about 150 lines of code and at least half a day to display some strings in a TextView.

Well, we developers are so underestimated. But, I feel you. All you people out there: Keep going, build great apps, improve your coding skills.

Apart from that, I hope you liked it and there is something you can take away from this article.

Good luck and happy coding!


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK