Reactive Streams (RxJava)
Data Binding
Dependency Injection (Dagger)
Activities
Fragments
Views
Memory
Remote APIs
Database
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>>
}
@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())
}
...
}
Keep it consistent
Control background work inside Repositories
Outside world needs to observeOn
Data loads should never fail
Errors are exposed via sync
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()
}
...
}
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?
<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()
}
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
...
}
...
}
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()
}
...
}
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
}
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
}
}