This repository contains a detailed sample app that implements MVP architecture in Kotlin using Dagger2, Room, RxJava2...
In Android we have a problem arising from the fact that activities are closely coupled to interface and data access mechanisms.
The Model-View-Presenter pattern allows to separate the presentation layer from the logic, so that everything about how the interface works is separated from how we represent it on screen.
MVP lets us to make views independent from data source and it will be easier to test each layer.
- Kotlin
- Room
- Android Architecture Components
- Android Support Libraries
- RxJava2
- Dagger 2 (2.11)
- Picasso
- Retrofit
- OkHttp
- Gson
- Timber
Dagger uses Components and Modules to define which dependencies will be injected in which objects.
This application will contain an activity (named as MainActivity) and several fragments.
So we will need three components: ApplicationComponent, ActivityComponent (where presenter resides in) and FragmentComponent (where presenter reside in).
@Singleton
@Component(modules = [AppModule::class, DataModule::class, NetworkModule::class])
interface AppComponent {
@ApplicationContext
fun context(): Context
fun getDataBaseManager(): DatabaseManager
fun getComicVineService(): ComicVineService
fun getSharedPrefs(): SharedPrefs
fun getConnectionLiveData(): ConnectionLiveData
fun comicsDao(): ComicsDao
fun issuesDao(): IssuesDao
fun inject(app: MyApplication)
}
@Module
class AppModule(private var application: Application) {
@Singleton
@Provides
@ApplicationContext
fun provideContext(): Context {
return application
}
@Provides
@Singleton
fun provideGson(): Gson {
return GsonBuilder()
.setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
.excludeFieldsWithModifiers(Modifier.FINAL, Modifier.TRANSIENT, Modifier.STATIC)
.disableHtmlEscaping()
.setPrettyPrinting()
.registerTypeAdapter(Date::class.java, DateDeserializer())
.create()
}
}
It’s quite simple. It just injects application and provides it when needed.
Let’s assume, if we want to use Calligraphy, Crashliytcs, Timber or Stetho, application module should inject those, too.
@PerActivity
@Component(dependencies = [AppComponent::class], modules = [ActivityModule::class])
interface ActivityComponent {
@ActivityFragmentManager
fun defaultFragmentManager(): FragmentManager
fun lifeCycleOwner(): LifecycleOwner
fun inject(activity: MainActivity)
fun inject(activity: FeatureActivity)
}
@PerFragment
@Component(dependencies = [AppComponent::class], modules = [FragmentModule::class])
interface FragmentComponent {
fun lifeCycleOwner(): LifecycleOwner
//inject fragment
}
@Module
class NetworkModule(private var context: Application) {
@Provides
@OkHttpNoAuth
@Singleton
internal fun provideOkHttpClientNoAuth(): OkHttpClient {
return makeOkHttpClientBuilder(context).build()
}
@Provides
@Singleton
internal fun provideGithubService(gson: Gson, @OkHttpNoAuth okHttpClient: OkHttpClient): ComicVineService {
return makeService(ComicVineService::class.java, gson, okHttpClient)
}
@Singleton
@Provides
internal fun providesConnectionLiveData(): ConnectionLiveData {
return ConnectionLiveData(context)
}
}
@Module
class DataModule(private var context: Application) {
@Singleton
@Provides
fun providesRoomDatabase(): DatabaseManager {
return Room.databaseBuilder(context, DatabaseManager::class.java, "my-database")
.fallbackToDestructiveMigration()
.allowMainThreadQueries()
.build()
}
@Provides
@Singleton
fun provideComicsDao(databaseManager: DatabaseManager): ComicsDao {
return databaseManager.comicsDao()
}
@Provides
@Singleton
fun provideIssuesDao(databaseManager: DatabaseManager): IssuesDao {
return databaseManager.issuesDao()
}
@Singleton
@Provides
internal fun providesSharePeres(): SharedPrefs {
return SharedPrefs.getInstance(context)
}
}
Why the Repository Pattern ?
-
Decouples the application from the data sources
-
Provides data from multiple sources (DB, API) without clients being concerned about this
-
Isolates the data layer
-
Single place, centralized, consistent access to data
-
Testable business logic via Unit Tests
-
Easily add new sources
So our repository now talks to the API data source and with the cache data source. We would now want to add another source for our data, a database source.
On Android, we have several options here :
-
using pure SQLite (too much boilerplate)
-
Realm ( too complex for our use case, we don’t need most of it’s features)
-
GreenDao ( a good ORM, but I think they will focus the development on objectbox in the future)
-
Room ( the newly introduced ORM from Google, good support for RXJava 2 )
I will be using for my example Room, the new library introduced by Google.
Completable.fromAction {
issuesDao.insertIgnore(entities)
issuesDao.updateIgnore(entities)
}.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(object : CompletableObserver {
override fun onComplete() {
Log.d(IssuesRepo::class.java.simpleName, "onComplete")
}
override fun onSubscribe(d: Disposable) {
Log.d(IssuesRepo::class.java.simpleName, "onSubscribe")
}
override fun onError(e: Throwable) {
Log.d(IssuesRepo::class.java.simpleName, "onError")
}
})
/**
* For single data
* @param remote
* @param onSave
* @param <T>
* @return
</T> */
fun <T> createResource(
remote: Single<Response<T>>,
onSave: PlainConsumer<T>
): Flowable<Resource<T>> {
return Flowable.create({
object : SimpleNetworkBoundSource<T>(it, true) {
override fun getRemote(): Single<Response<T>> = remote
override fun saveCallResult(data: T, isRefresh: Boolean) {
onSave.accept(data)
}
}
}, BackpressureStrategy.BUFFER)
}
fun getRepoIssues(refresh: Boolean): Flowable<Resource<BaseResponse<Issues>>> {
return createResource(refresh, comicVineService.getIssues2(
100, offset, Constants.KEY,
"json",
"cover_date: desc"
), onSave = object : OnSaveResultListener<BaseResponse<Issues>> {
override fun onSave(data: BaseResponse<Issues>, isRefresh: Boolean) {
offset = if (refresh) 0 else data.offset!! + 1
if (data.results.isNotEmpty()) {
upsert(data.results)
}
}
})
}
issuesDao.liveData().observe(getLifeCircleOwner(), Observer {
mainAdapter?.updateUi(it!!)
getView()?.size(it!!.size)
getView()?.hideProgress()
})
All pull requests are welcome, make sure to follow the contribution guidelines when you submit pull request.
Copyright 2018 LyHoangVinh.
Licensed under the the GPL-3.0 license.
See the LICENSE file for the whole license text.