\ No newline at end of file
diff --git a/app/build.gradle b/app/build.gradle
index a1b9519..0ed3b31 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -1,11 +1,12 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
+apply plugin: 'kotlin-kapt'
android {
compileSdkVersion 27
defaultConfig {
- applicationId "com.levibostian.driverexample"
+ applicationId "com.levibostian.tellerexample"
minSdkVersion 16
targetSdkVersion 27
versionCode 1
@@ -18,13 +19,38 @@ android {
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
+ kapt {
+ arguments {
+ arg("room.schemaLocation", "$projectDir/schemas".toString())
+ }
+ }
}
dependencies {
- implementation fileTree(dir: 'libs', include: ['*.jar'])
- implementation"org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
- implementation 'com.android.support:appcompat-v7:27.1.1'
- implementation 'com.android.support.constraint:constraint-layout:1.1.0'
+ def lifecycle_version = "1.1.1"
+ def room_version = "1.1.0"
+ def retrofit_version = "2.4.0"
+ def support_lib_version = "27.1.1"
+ implementation fileTree(include: ['*.jar'], dir: 'libs')
+ implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
+ implementation "com.android.support:appcompat-v7:$support_lib_version"
+ implementation "com.android.support:design:$support_lib_version"
+ implementation "com.android.support:recyclerview-v7:$support_lib_version"
+ implementation project(':teller')
+ implementation "android.arch.lifecycle:extensions:$lifecycle_version"
+ kapt "android.arch.lifecycle:compiler:$lifecycle_version"
+ implementation "android.arch.lifecycle:reactivestreams:$lifecycle_version"
+ implementation "android.arch.persistence.room:runtime:$room_version"
+ kapt "android.arch.persistence.room:compiler:$room_version"
+ implementation "android.arch.persistence.room:rxjava2:$room_version"
+ implementation 'io.reactivex.rxjava2:rxandroid:2.0.2'
+ implementation "io.reactivex.rxjava2:rxjava:2.1.13"
+ implementation 'com.google.code.gson:gson:2.8.4'
+ implementation "com.squareup.retrofit2:retrofit:$retrofit_version"
+ implementation "com.squareup.retrofit2:converter-gson:$retrofit_version"
+ implementation "com.squareup.retrofit2:adapter-rxjava2:$retrofit_version"
+ implementation "com.squareup.okhttp3:logging-interceptor:3.10.0"
+ implementation 'com.f2prateek.rx.preferences2:rx-preferences:2.0.0'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'com.android.support.test:runner:1.0.2'
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro
index f1b4245..e860e90 100644
--- a/app/proguard-rules.pro
+++ b/app/proguard-rules.pro
@@ -19,3 +19,13 @@
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
+
+### Retrofit
+# Platform calls Class.forName on types which do not exist on Android to determine platform.
+-dontnote retrofit2.Platform
+# Platform used when running on Java 8 VMs. Will not be used at runtime.
+-dontwarn retrofit2.Platform$Java8
+# Retain generic type information for use by reflection by converters and adapters.
+-keepattributes Signature
+# Retain declared checked exceptions for use by a Proxy instance.
+-keepattributes Exceptions
\ No newline at end of file
diff --git a/app/schemas/com.levibostian.tellerexample.model.AppDatabase/1.json b/app/schemas/com.levibostian.tellerexample.model.AppDatabase/1.json
new file mode 100644
index 0000000..ba910f3
--- /dev/null
+++ b/app/schemas/com.levibostian.tellerexample.model.AppDatabase/1.json
@@ -0,0 +1,45 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 1,
+ "identityHash": "835ceb8fbe667ba300129c091c2dba14",
+ "entities": [
+ {
+ "tableName": "repo",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `repo_owner_name` TEXT NOT NULL, PRIMARY KEY(`id`))",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "owner.name",
+ "columnName": "repo_owner_name",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": []
+ }
+ ],
+ "setupQueries": [
+ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"835ceb8fbe667ba300129c091c2dba14\")"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/app/src/androidTest/java/com/levibostian/driverexample/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/levibostian/tellerexample/ExampleInstrumentedTest.kt
similarity index 94%
rename from app/src/androidTest/java/com/levibostian/driverexample/ExampleInstrumentedTest.kt
rename to app/src/androidTest/java/com/levibostian/tellerexample/ExampleInstrumentedTest.kt
index d288a9f..30ec6c2 100644
--- a/app/src/androidTest/java/com/levibostian/driverexample/ExampleInstrumentedTest.kt
+++ b/app/src/androidTest/java/com/levibostian/tellerexample/ExampleInstrumentedTest.kt
@@ -1,4 +1,4 @@
-package com.levibostian.driverexample
+package com.levibostian.tellerexample
import android.support.test.InstrumentationRegistry
import android.support.test.runner.AndroidJUnit4
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 948d480..f2d5103 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -1,15 +1,18 @@
+ package="com.levibostian.tellerexample">
+
+
-
+
diff --git a/app/src/main/java/com/levibostian/driverexample/MainActivity.kt b/app/src/main/java/com/levibostian/driverexample/MainActivity.kt
deleted file mode 100644
index c2df733..0000000
--- a/app/src/main/java/com/levibostian/driverexample/MainActivity.kt
+++ /dev/null
@@ -1,12 +0,0 @@
-package com.levibostian.driverexample
-
-import android.support.v7.app.AppCompatActivity
-import android.os.Bundle
-
-class MainActivity : AppCompatActivity() {
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- setContentView(R.layout.activity_main)
- }
-}
diff --git a/app/src/main/java/com/levibostian/tellerexample/MainApplication.kt b/app/src/main/java/com/levibostian/tellerexample/MainApplication.kt
new file mode 100644
index 0000000..f52d505
--- /dev/null
+++ b/app/src/main/java/com/levibostian/tellerexample/MainApplication.kt
@@ -0,0 +1,14 @@
+package com.levibostian.tellerexample
+
+import android.app.Application
+import com.levibostian.teller.Teller
+
+class MainApplication: Application() {
+
+ override fun onCreate() {
+ super.onCreate()
+
+ Teller.init(this)
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/levibostian/tellerexample/activity/MainActivity.kt b/app/src/main/java/com/levibostian/tellerexample/activity/MainActivity.kt
new file mode 100644
index 0000000..98525f7
--- /dev/null
+++ b/app/src/main/java/com/levibostian/tellerexample/activity/MainActivity.kt
@@ -0,0 +1,175 @@
+package com.levibostian.tellerexample.activity
+
+import android.arch.lifecycle.Observer
+import android.arch.lifecycle.ViewModelProviders
+import android.support.v7.app.AppCompatActivity
+import android.os.Bundle
+import com.levibostian.tellerexample.R
+import com.levibostian.tellerexample.model.AppDatabase
+import com.levibostian.tellerexample.service.GitHubService
+import com.levibostian.tellerexample.viewmodel.ReposViewModel
+import retrofit2.Retrofit
+import android.arch.persistence.room.Room
+import android.content.Context
+import android.os.Handler
+import android.support.design.widget.Snackbar
+import android.support.v7.widget.LinearLayoutManager
+import android.text.format.DateUtils
+import android.view.View
+import android.widget.TextView
+import com.levibostian.teller.datastate.LocalDataStateListener
+import com.levibostian.teller.datastate.OnlineDataStateListener
+import com.levibostian.tellerexample.adapter.RepoRecyclerViewAdapter
+import com.levibostian.tellerexample.model.RepoModel
+import com.levibostian.tellerexample.viewmodel.GitHubUsernameViewModel
+import io.reactivex.schedulers.Schedulers
+import kotlinx.android.synthetic.main.activity_main.*
+import okhttp3.OkHttpClient
+import okhttp3.logging.HttpLoggingInterceptor
+import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory
+import retrofit2.converter.gson.GsonConverterFactory
+import java.util.*
+import android.content.Context.INPUT_METHOD_SERVICE
+import android.content.DialogInterface
+import android.support.v7.app.AlertDialog
+import android.view.inputmethod.InputMethodManager
+import com.levibostian.tellerexample.extensions.closeKeyboard
+
+
+class MainActivity : AppCompatActivity() {
+
+ private lateinit var reposViewModel: ReposViewModel
+ private lateinit var gitHubUsernameViewModel: GitHubUsernameViewModel
+ private lateinit var service: GitHubService
+ private lateinit var db: AppDatabase
+
+ private var fetchingSnackbar: Snackbar? = null
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ setContentView(R.layout.activity_main)
+ showEmptyView()
+
+ initialize()
+ }
+
+ private fun initialize() {
+ val httpLoggingInterceptor = HttpLoggingInterceptor()
+ httpLoggingInterceptor.level = HttpLoggingInterceptor.Level.BODY
+ val client = OkHttpClient.Builder()
+ .addInterceptor(httpLoggingInterceptor)
+ .build()
+ service = Retrofit.Builder()
+ .client(client)
+ .baseUrl("https://api.github.com/")
+ .addCallAdapterFactory(RxJava2CallAdapterFactory.createWithScheduler(Schedulers.io()))
+ .addConverterFactory(GsonConverterFactory.create())
+ .build()
+ .create(GitHubService::class.java)
+ db = Room.databaseBuilder(applicationContext, AppDatabase::class.java, "teller-example").build()
+
+ reposViewModel = ViewModelProviders.of(this).get(ReposViewModel::class.java)
+ gitHubUsernameViewModel = ViewModelProviders.of(this).get(GitHubUsernameViewModel::class.java)
+ reposViewModel.init(service, db)
+ gitHubUsernameViewModel.init(this)
+
+ reposViewModel.observeRepos()
+ .observe(this, Observer { reposState ->
+ reposState?.deliver(object : OnlineDataStateListener> {
+ override fun firstFetchOfData() {
+ showLoadingView()
+ }
+ override fun isEmpty() {
+ showEmptyView()
+ }
+ override fun data(data: List, fetched: Date) {
+ showDataView()
+ data_age_textview.text = "Data last synced ${DateUtils.getRelativeTimeSpanString(fetched.time)}"
+
+ repos_recyclerview.apply {
+ layoutManager = LinearLayoutManager(this@MainActivity)
+ adapter = RepoRecyclerViewAdapter(data)
+ setHasFixedSize(true)
+ }
+ }
+ override fun error(error: Throwable) {
+ //showErrorView()
+ AlertDialog.Builder(this@MainActivity)
+ .setTitle("Error")
+ .setMessage(error.message?: "Unknown error. Please, try again.")
+ .setPositiveButton("Ok") { dialog, _ ->
+ dialog.dismiss()
+ }
+ .create()
+ .show()
+ }
+ override fun fetchingFreshData() {
+ fetchingSnackbar = Snackbar.make(parent_view, "Updating repos list...", Snackbar.LENGTH_LONG)
+ fetchingSnackbar?.show()
+ }
+ override fun finishedFetchingFreshData(errorDuringFetch: Throwable?) {
+ Handler().postDelayed({
+ fetchingSnackbar?.setText(errorDuringFetch?.message ?: "Done fetching repos!")
+
+ Handler().postDelayed({
+ fetchingSnackbar?.dismiss()
+ }, 3000)
+ }, 1500)
+ }
+ })
+ })
+ gitHubUsernameViewModel.observeUsername()
+ .observe(this, Observer { username ->
+ username?.deliver(object : LocalDataStateListener {
+ override fun isEmpty() {
+ username_edittext.setText("", TextView.BufferType.EDITABLE)
+ }
+ override fun data(data: String) {
+ username_edittext.setText(data, TextView.BufferType.EDITABLE)
+ reposViewModel.setUsername(data)
+ }
+ override fun error(error: Throwable) {
+ showErrorView(error.message ?: "Unknown error occurred. Please, try again.")
+ }
+ })
+ })
+
+ go_button.setOnClickListener {
+ if (username_edittext.text.isBlank()) {
+ username_edittext.error = "Enter a GitHub username"
+ } else {
+ gitHubUsernameViewModel.setUsername(username_edittext.text.toString())
+
+ closeKeyboard()
+ }
+ }
+ }
+
+ private fun showLoadingView() {
+ loading_view.visibility = View.VISIBLE
+ empty_view.visibility = View.GONE
+ repos_recyclerview.visibility = View.GONE
+ }
+
+ private fun showEmptyView() {
+ loading_view.visibility = View.GONE
+ empty_view.visibility = View.VISIBLE
+ repos_recyclerview.visibility = View.GONE
+
+ empty_view_textview.text = "There are no repos."
+ }
+
+ private fun showDataView() {
+ loading_view.visibility = View.GONE
+ empty_view.visibility = View.GONE
+ repos_recyclerview.visibility = View.VISIBLE
+ }
+
+ private fun showErrorView(message: String) {
+ showEmptyView()
+
+ empty_view_textview.text = message
+ }
+
+}
diff --git a/app/src/main/java/com/levibostian/tellerexample/adapter/RepoRecyclerViewAdapter.kt b/app/src/main/java/com/levibostian/tellerexample/adapter/RepoRecyclerViewAdapter.kt
new file mode 100644
index 0000000..f4d85d2
--- /dev/null
+++ b/app/src/main/java/com/levibostian/tellerexample/adapter/RepoRecyclerViewAdapter.kt
@@ -0,0 +1,30 @@
+package com.levibostian.tellerexample.adapter
+
+import android.support.v7.widget.RecyclerView
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.TextView
+import com.levibostian.tellerexample.R
+import com.levibostian.tellerexample.model.RepoModel
+
+class RepoRecyclerViewAdapter(private val repos: List): RecyclerView.Adapter() {
+
+ override fun getItemCount(): Int = repos.size
+
+ class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
+ val nameTextView: TextView = view.findViewById(R.id.repo_name_textview)
+ }
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RepoRecyclerViewAdapter.ViewHolder {
+ val view = LayoutInflater.from(parent.context).inflate(R.layout.adapter_repo_recyclerview, parent, false)
+ return ViewHolder(view)
+ }
+
+ override fun onBindViewHolder(holder: ViewHolder, position: Int) {
+ val repo = repos[position]
+
+ holder.nameTextView.text = repo.name
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/levibostian/tellerexample/dao/ReposDao.kt b/app/src/main/java/com/levibostian/tellerexample/dao/ReposDao.kt
new file mode 100644
index 0000000..e47535e
--- /dev/null
+++ b/app/src/main/java/com/levibostian/tellerexample/dao/ReposDao.kt
@@ -0,0 +1,20 @@
+package com.levibostian.tellerexample.dao
+
+import android.arch.persistence.room.Dao
+import android.arch.persistence.room.Insert
+import android.arch.persistence.room.Query
+import com.levibostian.tellerexample.model.RepoModel
+import com.levibostian.tellerexample.model.RepoOwnerModel
+import io.reactivex.Flowable
+import io.reactivex.Observable
+
+@Dao
+interface ReposDao {
+
+ @Query("SELECT * FROM repo WHERE repo_owner_name = :name")
+ fun observeReposForUser(name: String): Flowable>
+
+ @Insert
+ fun insertRepos(repos: List)
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/levibostian/tellerexample/extensions/ActivityExtensions.kt b/app/src/main/java/com/levibostian/tellerexample/extensions/ActivityExtensions.kt
new file mode 100644
index 0000000..eddb089
--- /dev/null
+++ b/app/src/main/java/com/levibostian/tellerexample/extensions/ActivityExtensions.kt
@@ -0,0 +1,12 @@
+package com.levibostian.tellerexample.extensions
+
+import android.app.Activity
+import android.content.Context
+import android.view.inputmethod.InputMethodManager
+
+fun Activity.closeKeyboard() {
+ this.currentFocus?.let { view ->
+ val imm = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
+ imm.hideSoftInputFromWindow(view.windowToken, 0)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/levibostian/tellerexample/model/AppDatabase.kt b/app/src/main/java/com/levibostian/tellerexample/model/AppDatabase.kt
new file mode 100644
index 0000000..a3ce53d
--- /dev/null
+++ b/app/src/main/java/com/levibostian/tellerexample/model/AppDatabase.kt
@@ -0,0 +1,10 @@
+package com.levibostian.tellerexample.model
+
+import android.arch.persistence.room.RoomDatabase
+import android.arch.persistence.room.Database
+import com.levibostian.tellerexample.dao.ReposDao
+
+@Database(entities = [RepoModel::class], version = 1, exportSchema = true)
+abstract class AppDatabase: RoomDatabase() {
+ abstract fun reposDao(): ReposDao
+}
diff --git a/app/src/main/java/com/levibostian/tellerexample/model/RepoModel.kt b/app/src/main/java/com/levibostian/tellerexample/model/RepoModel.kt
new file mode 100644
index 0000000..67b1027
--- /dev/null
+++ b/app/src/main/java/com/levibostian/tellerexample/model/RepoModel.kt
@@ -0,0 +1,8 @@
+package com.levibostian.tellerexample.model
+
+import android.arch.persistence.room.*
+
+@Entity(tableName = "repo")
+class RepoModel(@PrimaryKey var id: Long = 0,
+ var name: String = "",
+ @Embedded(prefix = "repo_owner_") var owner: RepoOwnerModel = RepoOwnerModel())
\ No newline at end of file
diff --git a/app/src/main/java/com/levibostian/tellerexample/model/RepoOwnerModel.kt b/app/src/main/java/com/levibostian/tellerexample/model/RepoOwnerModel.kt
new file mode 100644
index 0000000..458d84e
--- /dev/null
+++ b/app/src/main/java/com/levibostian/tellerexample/model/RepoOwnerModel.kt
@@ -0,0 +1,8 @@
+package com.levibostian.tellerexample.model
+
+import android.arch.persistence.room.ColumnInfo
+import android.arch.persistence.room.Entity
+import android.arch.persistence.room.PrimaryKey
+import com.google.gson.annotations.SerializedName
+
+class RepoOwnerModel(@SerializedName("login") var name: String = "")
\ No newline at end of file
diff --git a/app/src/main/java/com/levibostian/tellerexample/repository/GitHubUsernameRepository.kt b/app/src/main/java/com/levibostian/tellerexample/repository/GitHubUsernameRepository.kt
new file mode 100644
index 0000000..a39bb64
--- /dev/null
+++ b/app/src/main/java/com/levibostian/tellerexample/repository/GitHubUsernameRepository.kt
@@ -0,0 +1,32 @@
+package com.levibostian.tellerexample.repository
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.preference.PreferenceManager
+import com.f2prateek.rx.preferences2.RxSharedPreferences
+import com.levibostian.teller.repository.LocalRepository
+import io.reactivex.Completable
+import io.reactivex.Observable
+import android.content.SharedPreferences
+import io.reactivex.Scheduler
+import io.reactivex.schedulers.Schedulers
+
+class GitHubUsernameRepository(private val context: Context): LocalRepository() {
+
+ private val githubUsernameSharedPrefsKey = "${this::class.java.simpleName}_githubUsername_key"
+ private val rxSharedPreferences: RxSharedPreferences = RxSharedPreferences.create(PreferenceManager.getDefaultSharedPreferences(context))
+
+ override fun saveData(data: String) {
+ PreferenceManager.getDefaultSharedPreferences(context).edit().putString(githubUsernameSharedPrefsKey, data).apply()
+ }
+
+ override fun observeData(): Observable {
+ return rxSharedPreferences.getString(githubUsernameSharedPrefsKey, "")
+ .asObservable()
+ .filter { it.isNotBlank() }
+ .subscribeOn(Schedulers.io())
+ }
+
+ override fun isDataEmpty(data: String): Boolean = data.isBlank()
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/levibostian/tellerexample/repository/ReposRepository.kt b/app/src/main/java/com/levibostian/tellerexample/repository/ReposRepository.kt
new file mode 100644
index 0000000..e0760d3
--- /dev/null
+++ b/app/src/main/java/com/levibostian/tellerexample/repository/ReposRepository.kt
@@ -0,0 +1,58 @@
+package com.levibostian.tellerexample.repository
+
+import com.levibostian.teller.repository.OnlineRepository
+import com.levibostian.teller.type.AgeOfData
+import com.levibostian.tellerexample.model.AppDatabase
+import com.levibostian.tellerexample.model.RepoModel
+import com.levibostian.tellerexample.service.GitHubService
+import io.reactivex.Completable
+import io.reactivex.Observable
+import io.reactivex.Single
+import io.reactivex.schedulers.Schedulers
+import retrofit2.Retrofit
+
+class ReposRepository(private val service: GitHubService,
+ private val db: AppDatabase): OnlineRepository, ReposRepository.GetRequirements, List>() {
+
+ override var maxAgeOfData: AgeOfData = AgeOfData(1, AgeOfData.Unit.HOURS)
+
+ override fun fetchFreshData(requirements: GetRequirements): Single>> {
+ return service.listRepos(requirements.username)
+ .map { response ->
+ val fetchResponse: FetchResponse>
+ if (!response.isSuccessful) {
+ fetchResponse = when (response.code()) {
+ in 500..600 -> {
+ FetchResponse.fail("The GitHub API is down. Please, try again later.")
+ }
+ 404 -> {
+ FetchResponse.fail("The username ${requirements.username} does not exist. Try another one.")
+ }
+ else -> {
+ // I do not like when apps say, "Unknown error. Please try again". It's terrible to do. But if it ever happens, that means you need to handle more HTTP status codes. Above are the only ones that I know GitHub will return. They don't document the rest of them, I don't think?
+ FetchResponse.fail("Unknown error. Please, try again.")
+ }
+ }
+ } else {
+ fetchResponse = FetchResponse.success(response.body()!!)
+ }
+
+ fetchResponse
+ }
+ }
+
+ override fun saveData(data: List) {
+ db.reposDao().insertRepos(data)
+ }
+
+ override fun observeCachedData(requirements: GetRequirements): Observable> {
+ return db.reposDao().observeReposForUser(requirements.username).toObservable()
+ }
+
+ override fun isDataEmpty(data: List): Boolean = data.isEmpty()
+
+ class GetRequirements(val username: String): GetDataRequirements {
+ override var tag: String = "${this::class.java.simpleName}_$username"
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/levibostian/tellerexample/service/GitHubService.kt b/app/src/main/java/com/levibostian/tellerexample/service/GitHubService.kt
new file mode 100644
index 0000000..e9acf96
--- /dev/null
+++ b/app/src/main/java/com/levibostian/tellerexample/service/GitHubService.kt
@@ -0,0 +1,14 @@
+package com.levibostian.tellerexample.service
+
+import com.levibostian.tellerexample.model.RepoModel
+import io.reactivex.Single
+import retrofit2.Response
+import retrofit2.http.GET
+import retrofit2.http.Path
+
+interface GitHubService {
+
+ @GET("users/{user}/repos")
+ fun listRepos(@Path("user") user: String): Single>>
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/levibostian/tellerexample/viewmodel/GitHubUsernameViewModel.kt b/app/src/main/java/com/levibostian/tellerexample/viewmodel/GitHubUsernameViewModel.kt
new file mode 100644
index 0000000..a3a4b7d
--- /dev/null
+++ b/app/src/main/java/com/levibostian/tellerexample/viewmodel/GitHubUsernameViewModel.kt
@@ -0,0 +1,33 @@
+package com.levibostian.tellerexample.viewmodel
+
+import android.arch.lifecycle.MutableLiveData
+import android.arch.lifecycle.LiveData
+import android.arch.lifecycle.LiveDataReactiveStreams
+import android.arch.lifecycle.ViewModel
+import android.content.Context
+import com.levibostian.teller.datastate.LocalDataState
+import com.levibostian.teller.datastate.OnlineDataState
+import com.levibostian.tellerexample.model.AppDatabase
+import com.levibostian.tellerexample.model.RepoModel
+import com.levibostian.tellerexample.repository.GitHubUsernameRepository
+import com.levibostian.tellerexample.repository.ReposRepository
+import com.levibostian.tellerexample.service.GitHubService
+import io.reactivex.BackpressureStrategy
+
+class GitHubUsernameViewModel: ViewModel() {
+
+ private lateinit var repository: GitHubUsernameRepository
+
+ fun init(context: Context) {
+ repository = GitHubUsernameRepository(context)
+ }
+
+ fun setUsername(username: String) {
+ repository.saveData(username)
+ }
+
+ fun observeUsername(): LiveData> {
+ return LiveDataReactiveStreams.fromPublisher(repository.observe().toFlowable(BackpressureStrategy.LATEST))
+ }
+
+}
diff --git a/app/src/main/java/com/levibostian/tellerexample/viewmodel/ReposViewModel.kt b/app/src/main/java/com/levibostian/tellerexample/viewmodel/ReposViewModel.kt
new file mode 100644
index 0000000..102179f
--- /dev/null
+++ b/app/src/main/java/com/levibostian/tellerexample/viewmodel/ReposViewModel.kt
@@ -0,0 +1,30 @@
+package com.levibostian.tellerexample.viewmodel
+
+import android.arch.lifecycle.MutableLiveData
+import android.arch.lifecycle.LiveData
+import android.arch.lifecycle.LiveDataReactiveStreams
+import android.arch.lifecycle.ViewModel
+import com.levibostian.teller.datastate.OnlineDataState
+import com.levibostian.tellerexample.model.AppDatabase
+import com.levibostian.tellerexample.model.RepoModel
+import com.levibostian.tellerexample.repository.ReposRepository
+import com.levibostian.tellerexample.service.GitHubService
+import io.reactivex.BackpressureStrategy
+
+class ReposViewModel: ViewModel() {
+
+ private lateinit var reposRepository: ReposRepository
+
+ fun init(service: GitHubService, db: AppDatabase) {
+ reposRepository = ReposRepository(service, db)
+ }
+
+ fun setUsername(username: String) {
+ reposRepository.loadDataRequirements = ReposRepository.GetRequirements(username)
+ }
+
+ fun observeRepos(): LiveData>> {
+ return LiveDataReactiveStreams.fromPublisher(reposRepository.observe().toFlowable(BackpressureStrategy.LATEST))
+ }
+
+}
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
index 7539a01..e956163 100644
--- a/app/src/main/res/layout/activity_main.xml
+++ b/app/src/main/res/layout/activity_main.xml
@@ -1,19 +1,74 @@
-
+ android:orientation="vertical"
+ android:id="@+id/parent_view"
+ tools:context=".activity.MainActivity">
+
+
+
+
+
+
+
+ tools:text="Data last synced 5 minutes ago"
+ android:gravity="end"/>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/adapter_repo_recyclerview.xml b/app/src/main/res/layout/adapter_repo_recyclerview.xml
new file mode 100644
index 0000000..5e2fc7b
--- /dev/null
+++ b/app/src/main/res/layout/adapter_repo_recyclerview.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 4b382d4..77089b6 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -1,3 +1,3 @@
- DriverExample
+ Teller example
diff --git a/app/src/test/java/com/levibostian/driverexample/ExampleUnitTest.kt b/app/src/test/java/com/levibostian/tellerexample/ExampleUnitTest.kt
similarity index 89%
rename from app/src/test/java/com/levibostian/driverexample/ExampleUnitTest.kt
rename to app/src/test/java/com/levibostian/tellerexample/ExampleUnitTest.kt
index 8d3ea47..99b0c80 100644
--- a/app/src/test/java/com/levibostian/driverexample/ExampleUnitTest.kt
+++ b/app/src/test/java/com/levibostian/tellerexample/ExampleUnitTest.kt
@@ -1,4 +1,4 @@
-package com.levibostian.driverexample
+package com.levibostian.tellerexample
import org.junit.Test
diff --git a/gradlew b/gradlew
old mode 100755
new mode 100644
diff --git a/local.properties b/local.properties
index e94f746..80144dd 100644
--- a/local.properties
+++ b/local.properties
@@ -1,10 +1,8 @@
-## This file is automatically generated by Android Studio.
-# Do not modify this file -- YOUR CHANGES WILL BE ERASED!
-#
-# This file should *NOT* be checked into Version Control Systems,
+## This file must *NOT* be checked into Version Control Systems,
# as it contains information specific to your local configuration.
#
# Location of the SDK. This is only used by Gradle.
# For customization when using a Version Control System, please read the
# header note.
-sdk.dir=/Users/levibostian/Library/Android/sdk
\ No newline at end of file
+#Wed May 16 17:40:04 CDT 2018
+sdk.dir=C\:\\Users\\Levi\\AppData\\Local\\Android\\Sdk
diff --git a/teller/src/main/java/com/levibostian/teller/Teller.kt b/teller/src/main/java/com/levibostian/teller/Teller.kt
index 822e556..95f9c3b 100644
--- a/teller/src/main/java/com/levibostian/teller/Teller.kt
+++ b/teller/src/main/java/com/levibostian/teller/Teller.kt
@@ -4,7 +4,6 @@ import android.app.Application
import android.content.Context
import android.content.SharedPreferences
import android.preference.PreferenceManager
-import com.levibostian.teller.repository.GetDataRequirements
import com.levibostian.teller.repository.OnlineRepository
import io.reactivex.Completable
import io.reactivex.Scheduler
@@ -28,7 +27,7 @@ class Teller private constructor(private val context: Context) {
* @throws RuntimeException If you have not called [Teller.Companion.init] yet to initialize singleton instance.
*/
@JvmStatic fun sharedInstance(): Teller {
- if (instance == null) throw RuntimeException("Sorry, you must call Teller.init() first.")
+ if (instance == null) throw RuntimeException("Oh, no! You forgot to call Teller.init()")
return instance!!
}
diff --git a/teller/src/main/java/com/levibostian/teller/repository/GetDataRequirements.kt b/teller/src/main/java/com/levibostian/teller/repository/GetDataRequirements.kt
deleted file mode 100644
index 4ef49ec..0000000
--- a/teller/src/main/java/com/levibostian/teller/repository/GetDataRequirements.kt
+++ /dev/null
@@ -1,10 +0,0 @@
-package com.levibostian.teller.repository
-
-/**
- * Data object that are the requirements to fetch, get cached data. It could contain a query term to search for data. It could contain a user id to fetch a user from an API.
- *
- * @property tag Unique tag that drives the behavior of a [OnlineDataSource]. If the [tag] is ever changed, [OnlineDataSource] will trigger a new data fetch so that you can get new data.
- */
-interface GetDataRequirements {
- var tag: String
-}
\ No newline at end of file
diff --git a/teller/src/main/java/com/levibostian/teller/repository/LocalRepository.kt b/teller/src/main/java/com/levibostian/teller/repository/LocalRepository.kt
index 5c53533..eb8b15e 100644
--- a/teller/src/main/java/com/levibostian/teller/repository/LocalRepository.kt
+++ b/teller/src/main/java/com/levibostian/teller/repository/LocalRepository.kt
@@ -25,7 +25,6 @@ abstract class LocalRepository {
compositeDisposable.add(
observeData()
- .subscribeOn(Schedulers.io())
.subscribe({ cachedData ->
if (cachedData == null || isDataEmpty(cachedData)) {
stateOfDate!!.onNextEmpty()
@@ -37,21 +36,24 @@ abstract class LocalRepository {
})
)
}
- return stateOfDate!!.asObservable()
+ return stateOfDate!!.asObservable().doOnDispose {
+ compositeDisposable.dispose()
+ }
}
/**
* Save the data to whatever storage method Repository chooses.
*
* It is up to you to call [saveData] when you have new data to save. A good place to do this is in a ViewModel.
+ *
+ * *Note:* It is up to you to run this function from a background thread. This is not done by default for you.
*/
-
- abstract fun saveData(data: DATA?): Completable
+ abstract fun saveData(data: DATA)
/**
* This function should be setup to trigger anytime there is a data change. So if you were to call [saveData], anyone observing the [Observable] returned here will get notified of a new update.
*/
- abstract fun observeData(): Observable
+ abstract fun observeData(): Observable
/**
* DataType determines if data is empty or not. Because data can be of `Any` type, the DataType must determine when data is empty or not.
diff --git a/teller/src/main/java/com/levibostian/teller/repository/OnlineRepository.kt b/teller/src/main/java/com/levibostian/teller/repository/OnlineRepository.kt
index da3e7f4..6b72027 100644
--- a/teller/src/main/java/com/levibostian/teller/repository/OnlineRepository.kt
+++ b/teller/src/main/java/com/levibostian/teller/repository/OnlineRepository.kt
@@ -10,10 +10,11 @@ import io.reactivex.Completable
import io.reactivex.Observable
import io.reactivex.Single
import io.reactivex.disposables.CompositeDisposable
+import io.reactivex.disposables.Disposable
import io.reactivex.schedulers.Schedulers
import java.util.*
-abstract class OnlineRepository {
+abstract class OnlineRepository {
private val forceSyncNextTimeFetchKey by lazy {
"${ConstantsUtil.PREFIX}_forceSyncNextTimeFetch_dataSource_${this::class.java.simpleName}_key"
@@ -24,6 +25,7 @@ abstract class OnlineRepository? = null
/**
@@ -38,8 +40,9 @@ abstract class OnlineRepository
fun initializeObservingCachedData() {
- compositeDisposable.add(
- this.observeCachedData(getDataRequirements)
- .subscribe({ cachedData ->
- val needsToFetchFreshData = this.doSyncNextTimeFetched() || this.isDataTooOld()
-
- if (cachedData == null || isDataEmpty(cachedData)) {
- stateOfDate?.onNextEmpty(needsToFetchFreshData)
- } else {
- stateOfDate?.onNextData(cachedData, lastTimeFetchedFreshData!!, needsToFetchFreshData)
- }
-
- if (needsToFetchFreshData) {
- this._sync(getDataRequirements, {
- stateOfDate?.onNextDoneFetchingFreshData(null)
- }, { error ->
- stateOfDate?.onNextDoneFetchingFreshData(error)
- })
- }
- }, { error ->
- this.stateOfDate?.onNextError(error)
- })
- )
+ if (observeCachedDataDisposable == null || !observeCachedDataDisposable!!.isDisposed) {
+ observeCachedDataDisposable = this.observeCachedData(getDataRequirements)
+ .subscribe({ cachedData ->
+ val needsToFetchFreshData = this.doSyncNextTimeFetched() || this.isDataTooOld()
+
+ if (cachedData == null || isDataEmpty(cachedData)) {
+ stateOfDate?.onNextEmpty(needsToFetchFreshData)
+ } else {
+ stateOfDate?.onNextData(cachedData, lastTimeFetchedFreshData!!, needsToFetchFreshData)
+ }
+
+ if (needsToFetchFreshData) {
+ this._sync(getDataRequirements, {
+ stateOfDate?.onNextDoneFetchingFreshData(null)
+ }, { error ->
+ stateOfDate?.onNextDoneFetchingFreshData(error)
+ })
+ }
+ }, { error ->
+ this.stateOfDate?.onNextError(error)
+ })
+ compositeDisposable.add(observeCachedDataDisposable!!)
+ }
}
if (!hasEverFetchedData()) {
@@ -85,7 +89,7 @@ abstract class OnlineRepository
+ }, { _ ->
// Note: Even if there is an error, we want to start observing cached data so we can transition to an empty state instead of infinite loading state for the UI for the user.
initializeObservingCachedData()
})
@@ -128,28 +132,31 @@ abstract class OnlineRepository Unit, onError: (Throwable) -> Unit) {
+ fun processFailedFetchResult(error: Throwable) {
+ this.resetForceSyncNextTimeFetched()
+ // Before 5-14-18:
+ // Note: We need to set the last updated time here or else we could run an infinite loop if the api call errors.
+ // The way that I handle errors now: if an error occurs (network error, status code error, any other error) I tell the rxswift subscriber that the error has occurred so you can tell the user. From there, you have the option to ask the user to retry the network call and perform the retry by calling datasource set data query requirements with force param true.
+ //
+ // Update 5-14-18:
+ // I am commenting out line below because of this example. What if I am building a GitHub app where I take a username and I call API for a list of repos for that username. What if that username does not exist and we get a 404 back from GitHub? If we call `updateLastTimeFreshDataFetched()`, we will set the data to empty the next time the user tries to use that github username again in the app. We don't want that because the data is not empty. It's non-existing. So after some thought, I am going to try and comment this out because I should only update the last time fetched fresh data when the data was actually fetched successfully, right? I say this because in the UI I want to show how old data is. I don't want to show "5 minutes ago" for a failed API request because then users will think that data is 5 minutes old. We want to show to the user the age of the data, not when it was last synced. If I do want to show both, I need to store both individually.
+ // this.updateLastTimeFreshDataFetched(getDataRequirements)
+ this.stateOfDate?.onNextError(error)
+ onError(error)
+ }
+
this.fetchFreshData(getDataRequirements)
.subscribe({ freshData ->
- this.resetForceSyncNextTimeFetched()
- this.updateLastTimeFreshDataFetched(getDataRequirements)
- this.saveData(freshData)
- .subscribe({
- onComplete()
- }, { error ->
- this.stateOfDate?.onNextError(error)
- onError(error)
- })
+ if (freshData.isSuccessful()) {
+ this.saveData(freshData.data!!)
+ this.resetForceSyncNextTimeFetched()
+ this.updateLastTimeFreshDataFetched(getDataRequirements)
+ onComplete()
+ } else {
+ processFailedFetchResult(freshData.failure!!)
+ }
}, { error ->
- this.resetForceSyncNextTimeFetched()
- // Before 5-14-18:
- // Note: We need to set the last updated time here or else we could run an infinite loop if the api call errors.
- // The way that I handle errors now: if an error occurs (network error, status code error, any other error) I tell the rxswift subscriber that the error has occurred so you can tell the user. From there, you have the option to ask the user to retry the network call and perform the retry by calling datasource set data query requirements with force param true.
- //
- // Update 5-14-18:
- // I am commenting out line below because of this example. What if I am building a GitHub app where I take a username and I call API for a list of repos for that username. What if that username does not exist and we get a 404 back from GitHub? If we call `updateLastTimeFreshDataFetched()`, we will set the data to empty the next time the user tries to use that github username again in the app. We don't want that because the data is not empty. It's non-existing. So after some thought, I am going to try and comment this out because I should only update the last time fetched fresh data when the data was actually fetched successfully, right? I say this because in the UI I want to show how old data is. I don't want to show "5 minutes ago" for a failed API request because then users will think that data is 5 minutes old. We want to show to the user the age of the data, not when it was last synced. If I do want to show both, I need to store both individually.
- // this.updateLastTimeFreshDataFetched(getDataRequirements)
- this.stateOfDate?.onNextError(error)
- onError(error)
+ processFailedFetchResult(error)
})
}
@@ -197,23 +204,57 @@ abstract class OnlineRepository
+ abstract fun fetchFreshData(requirements: GET_DATA_REQUIREMENTS): Single>
/**
* Save the data to whatever storage method Repository chooses.
*
* It is up to you to call [saveData] when you have new data to save. A good place to do this is in a ViewModel.
+ *
+ * *Note:* It is up to you to run this function from a background thread. This is not done by default for you.
*/
- abstract fun saveData(data: REQUEST?): Completable
+ abstract fun saveData(data: REQUEST)
/**
* Get existing cached data saved to the device if it exists. Return nil is data does not exist or is empty.
*/
- abstract fun observeCachedData(requirements: GET_DATA_REQUIREMENTS): Observable
+ abstract fun observeCachedData(requirements: GET_DATA_REQUIREMENTS): Observable
/**
* DataType determines if data is empty or not. Because data can be of `Any` type, the DataType must determine when data is empty or not.
*/
abstract fun isDataEmpty(data: RESULT): Boolean
+ /**
+ * Data object that are the requirements to fetch, get cached data. It could contain a query term to search for data. It could contain a user id to fetch a user from an API.
+ *
+ * @property tag Unique tag that drives the behavior of a [OnlineDataSource]. If the [tag] is ever changed, [OnlineDataSource] will trigger a new data fetch so that you can get new data.
+ */
+ interface GetDataRequirements {
+ var tag: String
+ }
+
+ class FetchResponse private constructor(val data: DATA? = null,
+ val failure: Throwable? = null) {
+ companion object {
+ fun success(data: DATA): FetchResponse {
+ return FetchResponse(data = data)
+ }
+
+ fun fail(message: String): FetchResponse {
+ return FetchResponse(failure = ResponseFail(message))
+ }
+
+ fun fail(throwable: Throwable): FetchResponse {
+ return FetchResponse(failure = throwable)
+ }
+ }
+
+ fun isSuccessful(): Boolean = data != null
+
+ fun isFailure(): Boolean = failure != null
+
+ class ResponseFail(message: String): Throwable(message)
+ }
+
}
\ No newline at end of file