Created
June 9, 2019 23:16
-
-
Save enginebai/f8e664a0ecc69897b0d9c4bbade7785e to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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