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.
- 726