Skip to content

Instantly share code, notes, and snippets.

@enginebai
Created June 9, 2019 23:16
Show Gist options
  • Save enginebai/f8e664a0ecc69897b0d9c4bbade7785e to your computer and use it in GitHub Desktop.
Save enginebai/f8e664a0ecc69897b0d9c4bbade7785e to your computer and use it in GitHub Desktop.
data class Location(
val name: String,
val lat: Double,
val lng: Double
)
enum class PostType {
TEXT,
PHOTO,
VIDEO,
LINK
}
@Entity(tableName = "new_post")
data class NewPost(
@PrimaryKey val id: String = UUID.randomUUID().toString(),
@ColumnInfo val caption: String? = null,
@ColumnInfo(name = "media_file") val mediaFile: File? = null,
@ColumnInfo val type: PostType? = null,
val location: Location? = null
)
@Dao
interface NewPostDao {
@Query("SELECT * FROM new_post")
fun getAll(): Observable<List<NewPost>>
@Query("SELECT * FROM new_post WHERE id = :id")
fun getPostById(id: String): Observable<NewPost>
@Insert(onConflict = OnConflictStrategy.IGNORE)
fun insertNewPost(newPost: NewPost): Long
@Update(onConflict = OnConflictStrategy.REPLACE)
fun updateNewPost(newPost: NewPost)
@Delete
fun deleteNewPost(newPost: NewPost)
@Transaction
fun upsert(newPost: NewPost) {
if (-1L == insertNewPost(newPost)) {
updateNewPost(newPost)
}
}
}
@Database(entities = [NewPost::class], version = 1)
abstract class NewPostDatabase: RoomDatabase() {
abstract fun newPostDao(): NewPostDao
companion object {
private var INSTANCE: NewPostDatabase? = null
fun getInstance(context: Context): NewPostDatabase? {
if (INSTANCE == null) {
synchronized(NewPostDatabase::class) {
INSTANCE = Room.databaseBuilder(context,
NewPostDatabase::class.java,
NewPostDatabase::class.java.simpleName).build()
}
}
return INSTANCE
}
fun destroyInstance() {
INSTANCE = null
}
}
}
object NewPostFieldConvert {
@TypeConverter
fun postTypeToStr(type: PostType?): String? = type?.name
@TypeConverter
fun strToPostType(str: String?): PostType? = str?.let { PostType.valueOf(it) }
@TypeConverter
fun fileToPath(file: File?): String? = file?.absolutePath
@TypeConverter
fun pathToFile(path: String?): File? = path?.let { File(it) }
}
interface PostRepository {
fun getNewPostList(): Observable<List<NewPost>>
}
class PostRepositoryImpl {
private val localDataSource: NewPostDao by inject()
override fun getNewPostList(): Observable<List<NewPost>> {
return localDataSource.getAll()
}
}
class PostListViewModel : ViewModel() {
private val repo: PostRepository by inject()
fun getNewPostList(): Observable<List<NewPost>> {
return repo.getNewPostList()
}
}
class PostListFragment : Fragment {
private val viewModel by viewModel<PostListViewModel>()
override fun onViewCreated() {
viewModel.getNewPostList()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe {
// update list
}
}
}
val localDataModule = module {
// 記憶體的資料庫,App關閉後資料就清除掉
single {
Room.inMemoryDatabaseBuilder(
androidApplication(),
NewPostDatabase::class.java
).build()
}
// 儲存在裝置內的資料庫,App關閉後資料依舊還在
single {
Room.databaseBuilder(
androidApplication(),
NewPostDatabase::class.java,
NewPostDatabase::class.java.simpleName
).build()
}
}
GET /feed
---
200 OK
Content-Type: application/json
Link: <https://host/feed?page=2&limit=20>; rel="next", <https://host/feed?page=5&limit=20>; rel="last"
[
{
"id": "5b726ac6d79e0ba",
"sender": "5ab5b7c9d3e0a9a8",
"caption": "This is hello world",
"media": "http://media-host/{post_id}.mp4",
"timestamp": 152488202
},
...
]
@Entity
data class Post(
@PrimaryKey
@SerializedName("id")
val id: String,
@SerializedName("sender")
@ColumnInfo
val sender: String?,
@SerializedName("caption")
@ColumnInfo
val caption: String?,
@SerializedName("media")
@ColumnInfo
val media: String?,
@SerializedName("timestamp")
@ColumnInfo
val timestamp: Long?
)
object PostDiffUtils : DiffUtil.ItemCallback<Post>() {
override fun areItemsTheSame(oldItem: Post, newItem: Post): Boolean {
return oldItem.id == oldItem.id
}
override fun areContentsTheSame(oldItem: Post, newItem: Post): Boolean {
return oldItem == newItem
}
}
interface PostApiService {
@GET("/feed")
fun getFeed(): Call<Response<List<Post>>>
}
class PostPagingDataSource : PageKeyedDataSource<String, Post>(), KoinComponent {
private val api: PostApiService by inject()
private val httpClient: OkHttpClient by inject()
private val gson: Gson by inject()
override fun loadInitial(
params: LoadInitialParams<String>,
callback: LoadInitialCallback<String, Post>
) {
val response = api.getFeed().execute()
if (response.isSuccessful) {
val nextPageUrl = parseNextPageUrl(response.headers())
val postList = response.body().body()
callback.onResult(postList, null, nextPageUrl)
} else {
// TODO: error handling
}
}
override fun loadAfter(params: LoadParams<String>, callback: LoadCallback<String, Post>) {
val url = params.key
if (url.isNotEmpty()) {
val response = httpClient.newCall(
Request.Builder()
.url(url)
.build()
).execute()
if (response.isSuccessful) {
val nextPageUrl = parseNextPageUrl(response.headers())
val listType = object : TypeToken<List<Post>>() {}.type
val postList: List<Post> = gson.fromJson(response.body()?.string(), listType)
callback.onResult(postList, nextPageUrl)
}
}
}
override fun loadBefore(params: LoadParams<String>, callback: LoadCallback<String, Post>) {
// we don't need it, leave it empty
}
private fun parseNextPageUrl(headers: Headers): String? {
// parse URL from link header
}
}
class PostPagingDataSourceFactory : DataSource.Factory<String, Post>() {
override fun create(): DataSource<String, Post> {
return PostPagingDataSource()
}
}
interface PostRepository {
fun getFeeds(): Observable<PagedList<Post>>
}
class PostRepositoryImpl : PostRepository {
override fun getFeeds(): Observable<PagedList<Post>> {
val dataSource = PostPagingDataSourceFactory()
val pagedListConfig = PagedList.Config.Builder()
.setPageSize(10)
.setPrefetchDistance(4)
.build()
return RxPagedListBuilder(dataSource, pagedListConfig)
.setFetchScheduler(Schedulers.io())
.setNotifyScheduler(AndroidSchedulers.mainThread())
.buildObservable()
}
}
class FeedViewModel : ViewModel(), KoinComponent {
private val repo: PostRepository by inject()
fun getFeeds(): Observable<PagedList<Post>> = repo.getFeeds()
}
class FeedActivity : BaseActivity() {
private lateinit var list: RecyclerView
private val adapter = FeedAdapter()
private val viewModel by viewModel<FeedViewModel>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
list.layoutManager = LinearLayoutManager(this, RecyclerView.VERTICAL, false)
list.adapter = adapter
viewModel.getFeeds()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe {
adapter.submitList(it)
}.apply { addDisposable(this) }
}
class FeedAdapter : PagedListAdapter<Post, RecyclerView.ViewHolder>(PostDiffUtils) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
// create view holder as same as you do in RecyclerView.Adapter
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
// bind data as same as you do in RecyclerView.Adapter
}
}
}
// -----------------------------------------------------------------------
interface PostApiService {
@GET("/feed")
fun getFeed(): Single<Response<List<Post>>>
}
@Dao
interface PostDao {
@Insert(onConflict = OnConflictStrategy.IGNORE)
fun insert(post: Post): Long
@Update(onConflict = OnConflictStrategy.REPLACE)
fun update(post: Post)
@Transaction
fun upsert(post: Post) {
if (-1L == insert(post))
update(post)
}
@Query("SELECT * FROM `post`")
fun getPostList(): Observable<List<Post>>
}
interface PostRepository {
fun fetchFeeds(): Completable
fun getFeeds(): Observable<List<Post>>
}
class PostRepositoryImpl : PostRepository {
private val remoteDataSource: PostApiService by inject()
private val localDataSource: PostDao by inject()
override fun fetchFeeds(): Completable {
return remoteDataSource.fetchFeeds()
.flatMapObservable {
if (it.isSuccessful)
Observable.fromIterable(it.body())
else
throw HttpException(it)
}.concatMapCompletable {
Completable.fromAction {
localDataSource.upsert(it)
}
}
}
override fun getFeeds(): Observable<List<Post>> {
return localDataSource.getPostList()
}
}
class PostBoundaryCallback : PagedList.BoundaryCallback<Post>(), KoinComponent {
private val remoteDataSource: PostApiService by inject()
private val localDataSource: PostDao by inject()
private val httpClient: OkHttpClient by inject()
private val gson: Gson by inject()
private var nextPageUrl: String? = null
override fun onZeroItemsLoaded() {
super.onZeroItemsLoaded()
val response = remoteDataSource.getFeed().execute()
if (response.isSuccessful) {
nextPageUrl = parseNextPageUrl(response.headers())
val postList: List<Post> = response.body()
upsertPostList(postList)
}
}
override fun onItemAtEndLoaded(itemAtEnd: MessageModel) {
super.onItemAtEndLoaded(itemAtEnd)
nextPageUrl?.run {
val response = httpClient.newCall(
Request.Builder()
.url(this)
.build()
).execute()
if (response.isSuccessful) {
nextPageUrl = parseNextPageUrl(response.headers())
val listType = object : TypeToken<List<Post>>() {}.type
val postList: List<Post> = gson.fromJson(response.body()?.string(), listType)
upsertPostList(postList)
}
}
}
private fun upsertPostList(postList: List<Post>) {
postList.forEach { post ->
localDataSource.upsert(post)
}
}
}
interface PostRepository {
fun getFeeds(): Observable<PagedList<Post>>
}
class PostRepositoryImpl : PostRepository {
private val remoteDataSource: PostApiService by inject()
private val localDataSource: PostDao by inject()
private val postBoundaryCallback: postBoundaryCallback by inject()
override fun getFeeds(): Observable<PagedList<Post>> {
val dataSource = localDataSource.getPostList()
val pagedListConfig = PagedList.Config.Builder()
.setPageSize(10)
.setPrefetchDistance(4)
.build()
return RxPagedListBuilder(dataSource, pagedListConfig)
.setBoundaryCallback(postBoundaryCallback)
.buildObservable()
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment