An Architecture-First approach to building apps

Hi!

google.com/+CosminStefan

cosmin@silverbit.ro
@cosmin_sd

Goal for today?

Why are we discussing Architecture?

We've all done this...

Because of this...

How do we make things better?

Modular

Single responsibility

Separation of concerns

Consistency

Let's talk Architecture

MVVM

Model-View-ViewModel

A few more building blocks

Reactive Streams (RxJava)

Data Binding

Dependency Injection (Dagger)

MVVM - Basic structure

Activities

Fragments

Views

Memory

Remote APIs

Database

Layers interaction

Reactive updates flow

Model / Data layer

Repository

Obtains and manages data from multiple sources

Local and remote data sources

data class Article(val id: Long,
                   val title: String,
                   val body: String,
                   val createdAt: Instant)
// A DAO interface for use with Room
@Dao
interface ArticleDao {
    @Query("SELECT * FROM article WHERE id = :id LIMIT 1")
    fun loadArticleById(id: Long): Flowable<Article>

    @Update
    fun insertArticles(articles: List<Article>)
}

// A web service interface for use with Retrofit
interface ArticleWebService {
    @GET("/articles")
    fun fetchArticles(): Single<List<Article>>
}

Single source of truth

Repository

@Singleton
class ArticleRepository
@Inject constructor(private val articleDao: ArticleDao,
                    private val articleWebService: ArticleWebService) {
    fun article(articleId: Long): Flowable<Article> {
        return articleDao.loadArticleById(articleId)
    }
    fun syncArticles(isForcedUpdate: Boolean): Completable {
        // If the current data has not yet expired, no need to fetch anything
        if (isArticleDataStillValid(isForcedUpdate))
            return Completable.complete()

        // Fetch fresh remote data and persist it in the local data store
        return articleWebService.fetchArticles()
                .map { articles -> articleDao.insertArticles(articles) }
                .toCompletable()
                .subscribeOn(Schedulers.io())
    }

    ...
}

Things to keep in mind

Threading

Keep it consistent

Control background work inside Repositories

Outside world needs to observeOn

Error handling

Data loads should never fail

Errors are exposed via sync

View Model

Holds and manages UI data

ViewModel - Loading data

class ArticleViewModel
@Inject constructor(private val articleRepository: ArticleRepository) {

    private var article: Article?
    var articleId: Long? = null


    fun onActive() {
        assert(articleId != null, { "Article id  must be configured " })

        articleRepository.article(articleId!!)
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe({ article -> run {
                                   this.article = article
                                   this.notifyChange()
                               }
                           })
    }
    var articleId: Long? = null
    lateinit var disposable: Disposable

    fun onActive() {
        assert(articleId != null, { "Article id  must be configured " })

        disposable = articleRepository.article(articleId!!)
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe({ article -> run {
                                   this.article = article
                                   this.notifyChange()
                               }
                           })
    }

    fun onInactive() {
        disposable.dispose()
    }
    
    ...
}

ViewModel - Exposing data

    val title: String?
        get() = article?.title + " by " + author.name

    val body: String?
        get() = article?.body

    val publishDate: String?
        get() = article?.let { DATE_FORMATTER.format(it.createdAt) } 

    fun onPublishDateClicked() {
        TODO()
    }
   
    ...
}
class ArticleViewModel
@Inject constructor(private val articleRepository: ArticleRepository) {

    private var article: Article?
    private var author: Author?

View

& DataBinding

View hooks to View Model and displays exposed state

Data Binding Layout

<layout xmlns:android="http://schemas.android.com/apk/res/android">
  <data>
    <variable name="viewModel" type="com.example.ArticleViewModel" />
  </data>


























</layout>
  <LinearLayout
      android:layout_width="match_parent"
      android:layout_height="match_parent"
      android:orientation="vertical">

    <TextView
        style="@style/header_text"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@{viewModel.title}" />

    <TextView
        style="@style/body_text"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@{viewModel.body}"/>

    <TextView
        style="@style/body_text"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:onClick="@{v -> viewModel.onPublishDateClicked()}"
        android:text="@{viewModel.publishDate}" />
  </LinearLayout>
class ArticleViewModel {

    val title: String?

    val body: String?

    val publishDate: String?
    
    fun onPublishDateClicked()
}

Activity/Fragment setup

class ArticleActivity : AppCompatActivity() {

    private lateinit var viewModel: ArticleViewModel
    private lateinit var binding: ArticleScreenBinding
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        viewModel = obtainViewModel()
        binding = DataBindingUtil.setContentView(this, R.layout.article_screen)
        binding.setVariable(BR.viewModel, viewModel)
    }
    private fun obtainViewModel(): ArticleViewModel {
        // Obtain an instance of the ViewModel associated to this view, either 
        // by creating it or by re-loading it after a configuration change.
        // Possible approaches: ViewModelProviders, Dependency Injection or
        // cache persisted via onRetainCustomNonConfigurationInstance
        ...
    }

    ...
}

Activity/Fragment lifecycle

class ArticleActivity : AppCompatActivity() {

    private lateinit var viewModel: ArticleViewModel
    private lateinit var binding: ArticleScreenBinding
    override fun onStart() {
        super.onStart()
        viewModel.onActive()
    }

    override fun onStop() {
        super.onStop()
        viewModel.onInactive()
    }

    override fun onDestroy() {
        super.onDestroy()
        viewModel.onDestroy()
    }
    ...
}

Things to keep in mind

No context in View Models

class StringResources
@Inject constructor(@param:Named("Application") private val context: Context) {

    fun getString(@StringRes resId: Int): String =
            context.getString(resId)

    fun getString(@StringRes resId: Int, vararg formatArgs: Any): String =
            context.getString(resId, *formatArgs)
}
class ArticleViewModel
@Inject constructor(private val stringResources: StringResources){
    
    val author: String
        get() = stringResources.getString(R.string.author, article.author)

    @get:StringRes
    val extras: Int
        get() = if (article.isReadOnly) R.string.option_a else R.string.option_b

}

Navigation and Dialogs

sealed class ViewCommand {
    class NavToAuthorScreen(private val authorId: Long) : ViewCommand()
    class NavUp() : ViewCommand()
    class ShowPasswordDialog() : ViewCommand()
}
class ArticleViewModel {

    val commands: Subject<ViewCommand> = PublishSubject.create()

    fun onAuthorClicked() {
        commands.onNext(ViewCommand.NavToAuthorScreen(article.author))
    }
}
class ArticleActivity : AppCompatActivity() {
    
    override fun onStart() {
        super.onStart()
        ...
        viewModel.commands.subscribe(this::handleCommand)
        // TODO: Disposable cleanup omitted for brevity
    }
}

Where did we end up?

What to keep in mind?

Architecture must
be central

Adapt, but be consistent

Use base building blocks

Thank you!

An Architecture-First approach to building apps

By Cosmin Stefan

An Architecture-First approach to building apps

An opinionated view on building sturdy, testable and scalable Android apps using the right architecture, based on MVVM, RxJava and DataBinding. We'll go into the details of our tried and tested approach to creating apps, from how data gets loaded and persisted to how it's prepared for the UI and how layouts are built, with a focus on how everything fits together.

  • 732