Android рдореЗрдВ рдХреИрд╢рд┐рдВрдЧ рдкреЗрдЬрд┐рдиреЗрд╢рди

рдирд┐рд╢реНрдЪрд┐рдд рд░реВрдк рд╕реЗ рдкреНрд░рддреНрдпреЗрдХ Android рдбреЗрд╡рд▓рдкрд░ рдиреЗ RecyclerView рдХрд╛ рдЙрдкрдпреЛрдЧ рдХрд░рдХреЗ рд╕реВрдЪрд┐рдпреЛрдВ рдХреЗ рд╕рд╛рде рдХрд╛рдо рдХрд┐рдпрд╛ред рдФрд░ рдХрдИ рд▓реЛрдЧ рдпрд╣ рднреА рджреЗрдЦрдиреЗ рдореЗрдВ рдХрд╛рдордпрд╛рдм рд░рд╣реЗ рдХрд┐ рдПрдВрдбреНрд░реЙрдЗрдб рдЖрд░реНрдХрд┐рдЯреЗрдХреНрдЪрд░ рдХрдВрдкреЛрдиреЗрдВрдЯреНрд╕ рд╕реЗ рдкреЗрдЬрд┐рдВрдЧ рд▓рд╛рдЗрдмреНрд░реЗрд░реА рдХрд╛ рдЙрдкрдпреЛрдЧ рдХрд░рдХреЗ рд╕реВрдЪреА рдореЗрдВ рдкреЗрдЬреЗрд╢рди рдХреИрд╕реЗ рд╡реНрдпрд╡рд╕реНрдерд┐рдд рдХрд░реЗрдВред


рдпрд╣ рд╕рд░рд▓ рд╣реИ: рд╕реНрдерд┐рддрд┐-рдирд┐рд░реНрдзрд╛рд░рдг рд╕реНрд░реЛрдд рд╕реЗрдЯ рдХрд░реЗрдВ, рдХреЙрдиреНрдлрд╝рд┐рдЧрд░реЗрд╢рди рд╕реЗрдЯ рдХрд░реЗрдВ, рдкреГрд╖реНрдард╛рдВрдХрд┐рдд рдмрдирд╛рдПрдВ рдФрд░ рд╣рдорд╛рд░реЗ RecyclerView рдХреЛ рдПрдбреЗрдкреНрдЯрд░ рдФрд░ рдбрд┐рдлреНрдпреВрдЯрд┐рд▓рдХрд┐рд▓рдмреИрдХ рдХреЗ рд╕рд╛рде рд╕рднреА рдХреЛ рдЦрд┐рд▓рд╛рдПрдВред


рд▓реЗрдХрд┐рди рдХреНрдпрд╛ рд╣реЛрдЧрд╛ рдЕрдЧрд░ рд╣рдорд╛рд░реЗ рдкрд╛рд╕ рдХрдИ рдбреЗрдЯрд╛ рд╕реНрд░реЛрдд рд╣реИрдВ? рдЙрджрд╛рд╣рд░рдг рдХреЗ рд▓рд┐рдП, рд╣рдо рдХрдХреНрд╖ рдореЗрдВ рдХреИрд╢ рд░рдЦрдирд╛ рдЪрд╛рд╣рддреЗ рд╣реИрдВ рдФрд░ рдиреЗрдЯрд╡рд░реНрдХ рд╕реЗ рдбреЗрдЯрд╛ рдкреНрд░рд╛рдкреНрдд рдХрд░рддреЗ рд╣реИрдВред


рдорд╛рдорд▓рд╛ рдХрд╛рдлреА рд╣рдж рддрдХ рд╕рд╣реА рд╣реИ рдФрд░ рдЗрдВрдЯрд░рдиреЗрдЯ рдкрд░ рдЗрд╕ рд╡рд┐рд╖рдп рдкрд░ рдЬреНрдпрд╛рджрд╛ рдЬрд╛рдирдХрд╛рд░реА рдирд╣реАрдВ рд╣реИред рдореИрдВ рдЗрд╕реЗ рдареАрдХ рдХрд░рдиреЗ рдХреА рдХреЛрд╢рд┐рд╢ рдХрд░реВрдВрдЧрд╛ рдФрд░ рджрд┐рдЦрд╛рдКрдВрдЧрд╛ рдХрд┐ рдРрд╕реЗ рдорд╛рдорд▓реЗ рдХреЛ рдХреИрд╕реЗ рд╣рд▓ рдХрд┐рдпрд╛ рдЬрд╛ рд╕рдХрддрд╛ рд╣реИред


рдЫрд╡рд┐


рдпрджрд┐ рдЖрдк рдПрдХрд▓ рдбреЗрдЯрд╛ рд╕реНрд░реЛрдд рдХреЗ рд╕рд╛рде рдкреГрд╖реНрдард╛рдВрдХрди рдХреЗ рдХрд╛рд░реНрдпрд╛рдиреНрд╡рдпрди рд╕реЗ рдЕрднреА рднреА рдкрд░рд┐рдЪрд┐рдд рдирд╣реАрдВ рд╣реИрдВ, рддреЛ рдореИрдВ рдЖрдкрдХреЛ рд╕рд▓рд╛рд╣ рджреЗрддрд╛ рд╣реВрдВ рдХрд┐ рд▓реЗрдЦ рдкрдврд╝рдиреЗ рд╕реЗ рдкрд╣рд▓реЗ рдЦреБрдж рдХреЛ рдЗрд╕рд╕реЗ рдкрд░рд┐рдЪрд┐рдд рдХрд░рд╛рдПрдВред


рдкреЗрдЬрд┐рдВрдЧ рдХреЗ рдмрд┐рдирд╛ рдПрдХ рд╕рдорд╛рдзрд╛рди рдХреИрд╕рд╛ рджрд┐рдЦреЗрдЧрд╛:


  • рдХреИрд╢ рддрдХ рдкрд╣реБрдВрдЪ (рд╣рдорд╛рд░реЗ рдорд╛рдорд▓реЗ рдореЗрдВ, рдпрд╣ рдПрдХ рдбреЗрдЯрд╛рдмреЗрд╕ рд╣реИ)
  • рдпрджрд┐ рдХреИрд╢ рдЦрд╛рд▓реА рд╣реИ - рд╕рд░реНрд╡рд░ рдХреЛ рдПрдХ рдЕрдиреБрд░реЛрдз рднреЗрдЬреЗрдВ
  • рд╣рдореЗрдВ рд╕рд░реНрд╡рд░ рд╕реЗ рдбреЗрдЯрд╛ рдкреНрд░рд╛рдкреНрдд рд╣реЛрддрд╛ рд╣реИ
  • рд╣рдо рдЙрдиреНрд╣реЗрдВ рдПрдХ рд╢реАрдЯ рдореЗрдВ рдкреНрд░рджрд░реНрд╢рд┐рдд рдХрд░рддреЗ рд╣реИрдВ
  • рдХреИрд╢ рдХрд░рдиреЗ рдХреЗ рд▓рд┐рдП рд▓рд┐рдЦреЗрдВ
  • рдпрджрд┐ рдХреЛрдИ рдХреИрд╢ рд╣реИ - рд╕реВрдЪреА рдореЗрдВ рдкреНрд░рджрд░реНрд╢рд┐рдд рдХрд░реЗрдВ
  • рд╣рдореЗрдВ рд╕рд░реНрд╡рд░ рд╕реЗ рдирд╡реАрдирддрдо рдбреЗрдЯрд╛ рдорд┐рд▓рддрд╛ рд╣реИ
  • рд╣рдо рдЙрдиреНрд╣реЗрдВ рд╕реВрдЪреА рдореЗрдВ рдкреНрд░рджрд░реНрд╢рд┐рдд рдХрд░рддреЗ рд╣реИрдВ list
  • рдХреИрд╢ рдХрд░рдиреЗ рдХреЗ рд▓рд┐рдП рд▓рд┐рдЦреЗрдВ

рдЫрд╡рд┐


рдкреЗрдЬрд┐рдВрдЧ рдХреЗ рд░реВрдк рдореЗрдВ рдЗрд╕ рддрд░рд╣ рдХреА рдПрдХ рд╕реБрд╡рд┐рдзрд╛рдЬрдирдХ рдЪреАрдЬ, рдЬреЛ рдЙрдкрдпреЛрдЧрдХрд░реНрддрд╛рдУрдВ рдХреЗ рдЬреАрд╡рди рдХреЛ рд╕рд░рд▓ рдмрдирд╛рддреА рд╣реИ, рдпрд╣рд╛рдВ рдпрд╣ рд╣рдореЗрдВ рдЬрдЯрд┐рд▓ рдмрдирд╛рддреА рд╣реИред рдЖрдЗрдП рдпрд╣ рдХрд▓реНрдкрдирд╛ рдХрд░рдиреЗ рдХреА рдХреЛрд╢рд┐рд╢ рдХрд░реЗрдВ рдХрд┐ рдХрдИ рдбреЗрдЯрд╛ рд╕реНрд░реЛрддреЛрдВ рдХреЗ рд╕рд╛рде рдкреГрд╖реНрдард╛рдВрдХрд┐рдд рд╕реВрдЪреА рдХреЛ рд▓рд╛рдЧреВ рдХрд░рддреЗ рд╕рдордп рдХреНрдпрд╛ рд╕рдорд╕реНрдпрд╛рдПрдВ рд╣реЛ рд╕рдХрддреА рд╣реИрдВред


рдПрд▓реНрдЧреЛрд░рд┐рдереНрдо рд▓рдЧрднрдЧ рдирд┐рдореНрдирд▓рд┐рдЦрд┐рдд рд╣реИ:


  • рдкрд╣рд▓реЗ рдкреГрд╖реНрда рдХреЗ рд▓рд┐рдП рдХреИрд╢ рдбреЗрдЯрд╛ рдкреНрд░рд╛рдкреНрдд рдХрд░реЗрдВ
  • рдпрджрд┐ рдХреИрд╢ рдЦрд╛рд▓реА рд╣реИ, рддреЛ рд╣рдореЗрдВ рд╕рд░реНрд╡рд░ рдбреЗрдЯрд╛ рдорд┐рд▓рддрд╛ рд╣реИ, рдЗрд╕реЗ рд╕реВрдЪреА рдореЗрдВ рдкреНрд░рджрд░реНрд╢рд┐рдд рдХрд░реЗрдВ рдФрд░ рдбреЗрдЯрд╛рдмреЗрд╕ рдореЗрдВ рд▓рд┐рдЦреЗрдВ
  • рдпрджрд┐ рдХреЛрдИ рдХреИрд╢ рд╣реИ, рддреЛ рдЙрд╕реЗ рд╕реВрдЪреА рдореЗрдВ рд▓реЛрдб рдХрд░реЗрдВ
  • рдпрджрд┐ рд╣рдо рдбреЗрдЯрд╛рдмреЗрд╕ рдХреЗ рдЕрдВрдд рдореЗрдВ рдЖрддреЗ рд╣реИрдВ, рддреЛ рд╣рдо рд╕рд░реНрд╡рд░ рд╕реЗ рдбреЗрдЯрд╛ рдХрд╛ рдЕрдиреБрд░реЛрдз рдХрд░рддреЗ рд╣реИрдВ, рдЙрдиреНрд╣реЗрдВ рдкреНрд░рджрд░реНрд╢рд┐рдд рдХрд░рддреЗ рд╣реИрдВ
  • рд╕реВрдЪреА рдореЗрдВ рдФрд░ рдбреЗрдЯрд╛рдмреЗрд╕ рдХреЗ рд▓рд┐рдП рд▓рд┐рдЦреЗрдВ

рдЗрд╕ рджреГрд╖реНрдЯрд┐рдХреЛрдг рдХреА рд╡рд┐рд╢реЗрд╖рддрд╛рдУрдВ рдореЗрдВ рд╕реЗ, рдЖрдк рджреЗрдЦ рд╕рдХрддреЗ рд╣реИрдВ рдХрд┐ рд╕реВрдЪреА рдХреЛ рдкреНрд░рджрд░реНрд╢рд┐рдд рдХрд░рдиреЗ рдХреЗ рд▓рд┐рдП, рдХреИрд╢ рдХреЛ рдкрд╣рд▓реЗ рд╕рд░реНрд╡реЗрдХреНрд╖рдг рдХрд┐рдпрд╛ рдЧрдпрд╛ рд╣реИ, рдФрд░ рдирдП рдбреЗрдЯрд╛ рдХреЛ рд▓реЛрдб рдХрд░рдиреЗ рдХреЗ рд▓рд┐рдП рд╕рдВрдХреЗрдд рдХреИрд╢ рдХрд╛ рдЕрдВрдд рд╣реИред


рдЫрд╡рд┐


Google рдиреЗ рдЗрд╕рдХреЗ рдмрд╛рд░реЗ рдореЗрдВ рд╕реЛрдЪрд╛ рдФрд░ рдПрдХ рд╕рдорд╛рдзрд╛рди рдмрдирд╛рдпрд╛ рдЬреЛ рдкреЗрдЬрд┐рдВрдЧ рдмреЙрдХреНрд╕ рдмреЙрдиреНрдб рд╕реЗ рдмрд╛рд╣рд░ рдЖрддрд╛ рд╣реИ - рдмрд╛рдЙрдВрдбреНрд░реАрдХреЙрд▓рдмреИрдХред


рдЬрдм рд╕реНрдерд╛рдиреАрдп рдбреЗрдЯрд╛ рд╕реНрд░реЛрдд "рд╕рдорд╛рдкреНрдд" рд╣реЛрддрд╛ рд╣реИ рдФрд░ рдирдП рдбреЗрдЯрд╛ рдХреЛ рдбрд╛рдЙрдирд▓реЛрдб рдХрд░рдиреЗ рдХреЗ рд▓рд┐рдП рд░рд┐рдкреЙрдЬрд┐рдЯрд░реА рдХреЛ рд╕реВрдЪрд┐рдд рдХрд░рддрд╛ рд╣реИ рддреЛ рдмрд╛рдЙрдВрдбреНрд░реАрдХреЙрд▓рдмреИрдХ рд░рд┐рдкреЛрд░реНрдЯ рдХрд░рддрд╛ рд╣реИред


рдЫрд╡рд┐


рдЖрдзрд┐рдХрд╛рд░рд┐рдХ рдПрдВрдбреНрд░реЙрдЗрдб рджреЗрд╡ рд╡реЗрдмрд╕рд╛рдЗрдЯ рдореЗрдВ рджреЛ рдбреЗрдЯрд╛ рд╕реНрд░реЛрддреЛрдВ: рдиреЗрдЯрд╡рд░реНрдХ (рд░реЗрдЯреНрд░реЛрдлрд┐рдЯ 2) + рдбреЗрдЯрд╛рдмреЗрд╕ (рдХрдХреНрд╖) рдХреЗ рд╕рд╛рде рдкреГрд╖реНрдард╛рдВрдХрди рд╕реВрдЪреА рдХрд╛ рдЙрдкрдпреЛрдЧ рдХрд░рдХреЗ рдПрдХ рдЙрджрд╛рд╣рд░рдг рдкрд░рд┐рдпреЛрдЬрдирд╛ рдХреЗ рд╕рд╛рде рдПрдХ рд░рд┐рдкреЙрдЬрд┐рдЯрд░реА рдХрд╛ рд▓рд┐рдВрдХ рд╣реИред рдпрд╣ рд╕рдордЭрдиреЗ рдХреЗ рд▓рд┐рдП рдХрд┐ рдРрд╕реА рдкреНрд░рдгрд╛рд▓реА рдХреИрд╕реЗ рдХрд╛рдо рдХрд░рддреА рд╣реИ, рдЖрдЗрдП рдЗрд╕ рдЙрджрд╛рд╣рд░рдг рдХреЛ рдкрд╛рд░реНрд╕ рдХрд░рдиреЗ рдХреА рдХреЛрд╢рд┐рд╢ рдХрд░реЗрдВ рдФрд░ рдЗрд╕реЗ рдереЛрдбрд╝рд╛ рд╕рд░рд▓ рдХрд░реЗрдВред


рдЪрд▓рд┐рдП рдбреЗрдЯрд╛ рд▓реЗрдпрд░ рд╕реЗ рд╢реБрд░реВ рдХрд░рддреЗ рд╣реИрдВред рджреЛ рдбреЗрдЯрд╛ рд╕реНрд░реЛрдд рдмрдирд╛рдПрдБред


рдЗрдВрдЯрд░рдлрд╝реЗрд╕ RedditApi.kt
import com.memebattle.pagingwithrepository.domain.model.RedditPost import retrofit2.Call import retrofit2.http.GET import retrofit2.http.Path import retrofit2.http.Query /** * API communication setup */ interface RedditApi { @GET("/r/{subreddit}/hot.json") fun getTop( @Path("subreddit") subreddit: String, @Query("limit") limit: Int): Call<ListingResponse> // for after/before param, either get from RedditDataResponse.after/before, // or pass RedditNewsDataResponse.name (though this is technically incorrect) @GET("/r/{subreddit}/hot.json") fun getTopAfter( @Path("subreddit") subreddit: String, @Query("after") after: String, @Query("limit") limit: Int): Call<ListingResponse> @GET("/r/{subreddit}/hot.json") fun getTopBefore( @Path("subreddit") subreddit: String, @Query("before") before: String, @Query("limit") limit: Int): Call<ListingResponse> class ListingResponse(val data: ListingData) class ListingData( val children: List<RedditChildrenResponse>, val after: String?, val before: String? ) data class RedditChildrenResponse(val data: RedditPost) } 

рдпрд╣ рдЗрдВрдЯрд░рдлрд╝реЗрд╕ Reddit API рдФрд░ рдореЙрдбрд▓ рдХреНрд▓рд╛рд╕реЗрд╕ (ListingResponse, ListingData, RedditChildrenResponse) рдХреЗ рдЙрди рдЕрдиреБрд░реЛрдзреЛрдВ рдХрд╛ рд╡рд░реНрдгрди рдХрд░рддрд╛ рд╣реИ рдЬрд┐рдирдореЗрдВ API рдкреНрд░рддрд┐рдХреНрд░рд┐рдпрд╛рдУрдВ рдХреЛ рдзреНрд╡рд╕реНрдд рдХрд┐рдпрд╛ рдЬрд╛рдПрдЧрд╛ред


рдФрд░ рддреБрд░рдВрдд рд░реЗрдЯреНрд░реЛрдлрд┐рдЯ рдФрд░ рд░реВрдо рдХреЗ рд▓рд┐рдП рдПрдХ рдореЙрдбрд▓ рдмрдирд╛рдПрдВ


RedditPost.kt
 import androidx.room.ColumnInfo import androidx.room.Entity import androidx.room.Index import androidx.room.PrimaryKey import com.google.gson.annotations.SerializedName @Entity(tableName = "posts", indices = [Index(value = ["subreddit"], unique = false)]) data class RedditPost( @PrimaryKey @SerializedName("name") val name: String, @SerializedName("title") val title: String, @SerializedName("score") val score: Int, @SerializedName("author") val author: String, @SerializedName("subreddit") // this seems mutable but fine for a demo @ColumnInfo(collate = ColumnInfo.NOCASE) val subreddit: String, @SerializedName("num_comments") val num_comments: Int, @SerializedName("created_utc") val created: Long, val thumbnail: String?, val url: String?) { // to be consistent w/ changing backend order, we need to keep a data like this var indexInResponse: Int = -1 } 

RedditDb.kt рд╡рд░реНрдЧ рдЬреЛ RoomDatabase рдХреЛ рдЗрдирд╣реЗрд░рд┐рдЯ рдХрд░реЗрдЧрд╛ред


RedditDb.kt
 import androidx.room.Database import androidx.room.RoomDatabase import com.memebattle.pagingwithrepository.domain.model.RedditPost /** * Database schema used by the DbRedditPostRepository */ @Database( entities = [RedditPost::class], version = 1, exportSchema = false ) abstract class RedditDb : RoomDatabase() { abstract fun posts(): RedditPostDao } 

рдпрд╛рдж рд░рдЦреЗрдВ рдХрд┐ рдбреЗрдЯрд╛рдмреЗрд╕ рдореЗрдВ рдХрд┐рд╕реА рдХреНрд╡реЗрд░реА рдХреЛ рдирд┐рд╖реНрдкрд╛рджрд┐рдд рдХрд░рдиреЗ рдХреЗ рд▓рд┐рдП рд╣рд░ рдмрд╛рд░ рд░реВрдордбреЗрдЯрд╛рдмреЗрд╕ рдХреНрд▓рд╛рд╕ рдмрдирд╛рдирд╛ рдмрд╣реБрдд рдорд╣рдВрдЧрд╛ рд╣реИ, рдЗрд╕рд▓рд┐рдП рдПрдХ рд╡рд╛рд╕реНрддрд╡рд┐рдХ рдорд╛рдорд▓реЗ рдореЗрдВ, рдПрдкреНрд▓рд┐рдХреЗрд╢рди рдХреЗ рдкреВрд░реЗ рдЬреАрд╡рди рдХреЗ рд▓рд┐рдП рдЗрд╕реЗ рдПрдХ рдмрд╛рд░ рдмрдирд╛рдПрдВ!


рдФрд░ рдбреЗрдЯрд╛рдмреЗрд╕ рдкреНрд░рд╢реНрдиреЛрдВ RedditPostDao.kt рдХреЗ рд╕рд╛рде рджрд╛рдУ рд╡рд░реНрдЧ


RedditPostDao.kt
 import androidx.paging.DataSource import androidx.room.Dao import androidx.room.Insert import androidx.room.OnConflictStrategy import androidx.room.Query import com.memebattle.pagingwithrepository.domain.model.RedditPost @Dao interface RedditPostDao { @Insert(onConflict = OnConflictStrategy.REPLACE) fun insert(posts : List<RedditPost>) @Query("SELECT * FROM posts WHERE subreddit = :subreddit ORDER BY indexInResponse ASC") fun postsBySubreddit(subreddit : String) : DataSource.Factory<Int, RedditPost> @Query("DELETE FROM posts WHERE subreddit = :subreddit") fun deleteBySubreddit(subreddit: String) @Query("SELECT MAX(indexInResponse) + 1 FROM posts WHERE subreddit = :subreddit") fun getNextIndexInSubreddit(subreddit: String) : Int } 

рдЖрдкрдиреЗ рд╢рд╛рдпрдж рджреЗрдЦрд╛ рдХрд┐ рдкреЛрд╕реНрдЯрд╕рдмреНрд░реЗрдбреНрд░реЗрдбрд┐рдЯ рдкреЛрд╕реНрдЯ рд░рд┐рдЯреНрд░реАрд╡рд▓ рд╡рд┐рдзрд┐ рд░рд┐рдЯрд░реНрди рдХрд░рддреА рд╣реИ
DataSource.Factoryред рдЗрд╕рдХрд╛ рдЙрдкрдпреЛрдЧ рдХрд░рдХреЗ рд╣рдорд╛рд░реЗ PagedList рдХреЛ рдмрдирд╛рдирд╛ рдЖрд╡рд╢реНрдпрдХ рд╣реИ
LivePagedListBuilder, рдкреГрд╖реНрдарднреВрдорд┐ рдереНрд░реЗрдб рдореЗрдВред рдЖрдк рдЗрд╕рдХреЗ рдмрд╛рд░реЗ рдореЗрдВ рдЕрдзрд┐рдХ рдкрдврд╝ рд╕рдХрддреЗ рд╣реИрдВ
рд╕рдмрдХ ред


рдорд╣рд╛рди, рдбреЗрдЯрд╛ рд▓реЗрдпрд░ рддреИрдпрд╛рд░ рд╣реИред рд╣рдо рд╡реНрдпрд╛рдкрд╛рд░ рддрд░реНрдХ рдкрд░рдд рдХреА рдУрд░ рдореБрдбрд╝рддреЗ рд╣реИрдВред рд░рд┐рдкреЙрдЬрд┐рдЯрд░реА рдкреИрдЯрд░реНрди рдХреЛ рд▓рд╛рдЧреВ рдХрд░рдиреЗ рдХреЗ рд▓рд┐рдП, рдЗрд╕рдХреЗ рдХрд╛рд░реНрдпрд╛рдиреНрд╡рдпрди рд╕реЗ рдЕрд▓рдЧ рдПрдХ рд░рд┐рдкреЙрдЬрд┐рдЯрд░реА рдЗрдВрдЯрд░рдлрд╝реЗрд╕ рдмрдирд╛рдиреЗ рдХреЗ рд▓рд┐рдП рдкреНрд░рдерд╛рдЧрдд рд╣реИред рдЗрд╕рд▓рд┐рдП, рд╣рдо рдЗрдВрдЯрд░рдлрд╝реЗрд╕ RedditPostRepository.kt рдмрдирд╛рдПрдВрдЧреЗ


RedditPostRepository.kt
 interface RedditPostRepository { fun postsOfSubreddit(subReddit: String, pageSize: Int): Listing<RedditPost> } 

рдФрд░ рддреБрд░рдВрдд рд╕рд╡рд╛рд▓ - рдХрд┐рд╕ рддрд░рд╣ рдХреА рд▓рд┐рд╕реНрдЯрд┐рдВрдЧ? рдпрд╣ рд╕реВрдЪреА рдкреНрд░рджрд░реНрд╢рд┐рдд рдХрд░рдиреЗ рдХреЗ рд▓рд┐рдП рдЖрд╡рд╢реНрдпрдХ рджрд┐рдирд╛рдВрдХ рд╡рд░реНрдЧ рд╣реИред


Listing.kt
 import androidx.lifecycle.LiveData import androidx.paging.PagedList import com.memebattle.pagingwithrepository.domain.repository.network.NetworkState /** * Data class that is necessary for a UI to show a listing and interact w/ the rest of the system */ data class Listing<T>( // the LiveData of paged lists for the UI to observe val pagedList: LiveData<PagedList<T>>, // represents the network request status to show to the user val networkState: LiveData<NetworkState>, // represents the refresh status to show to the user. Separate from networkState, this // value is importantly only when refresh is requested. val refreshState: LiveData<NetworkState>, // refreshes the whole data and fetches it from scratch. val refresh: () -> Unit, // retries any failed requests. val retry: () -> Unit) 

MainRepository.kt рд░рд┐рдкреЙрдЬрд┐рдЯрд░реА рдХрд╛ рдХрд╛рд░реНрдпрд╛рдиреНрд╡рдпрди рдмрдирд╛рдПрдБ


MainRepository.kt
 import android.content.Context import androidx.annotation.MainThread import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.Transformations import com.android.example.paging.pagingwithnetwork.reddit.api.RedditApi import retrofit2.Retrofit import retrofit2.converter.gson.GsonConverterFactory import androidx.room.Room import com.android.example.paging.pagingwithnetwork.reddit.db.RedditDb import com.android.example.paging.pagingwithnetwork.reddit.db.RedditPostDao import com.memebattle.pagingwithrepository.domain.model.RedditPost import retrofit2.Call import retrofit2.Callback import retrofit2.Response import java.util.concurrent.Executors import androidx.paging.LivePagedListBuilder import com.memebattle.pagingwithrepository.domain.repository.core.Listing import com.memebattle.pagingwithrepository.domain.repository.boundary.SubredditBoundaryCallback import com.memebattle.pagingwithrepository.domain.repository.network.NetworkState import com.memebattle.pagingwithrepository.domain.repository.core.RedditPostRepository class MainRepository(context: Context) : RedditPostRepository { private var retrofit: Retrofit = Retrofit.Builder() .baseUrl("https://www.reddit.com/") //   .addConverterFactory(GsonConverterFactory.create()) //,    JSON'   .build() var db = Room.databaseBuilder(context, RedditDb::class.java, "database").build() private var redditApi: RedditApi private var dao: RedditPostDao val ioExecutor = Executors.newSingleThreadExecutor() init { redditApi = retrofit.create(RedditApi::class.java) // ,       dao = db.posts() } /** * Inserts the response into the database while also assigning position indices to items. */ private fun insertResultIntoDb(subredditName: String, body: RedditApi.ListingResponse?) { body!!.data.children.let { posts -> db.runInTransaction { val start = db.posts().getNextIndexInSubreddit(subredditName) val items = posts.mapIndexed { index, child -> child.data.indexInResponse = start + index child.data } db.posts().insert(items) } } } /** * When refresh is called, we simply run a fresh network request and when it arrives, clear * the database table and insert all new items in a transaction. * <p> * Since the PagedList already uses a database bound data source, it will automatically be * updated after the database transaction is finished. */ @MainThread private fun refresh(subredditName: String): LiveData<NetworkState> { val networkState = MutableLiveData<NetworkState>() networkState.value = NetworkState.LOADING redditApi.getTop(subredditName, 10).enqueue( object : Callback<RedditApi.ListingResponse> { override fun onFailure(call: Call<RedditApi.ListingResponse>, t: Throwable) { // retrofit calls this on main thread so safe to call set value networkState.value = NetworkState.error(t.message) } override fun onResponse(call: Call<RedditApi.ListingResponse>, response: Response<RedditApi.ListingResponse>) { ioExecutor.execute { db.runInTransaction { db.posts().deleteBySubreddit(subredditName) insertResultIntoDb(subredditName, response.body()) } // since we are in bg thread now, post the result. networkState.postValue(NetworkState.LOADED) } } } ) return networkState } /** * Returns a Listing for the given subreddit. */ override fun postsOfSubreddit(subReddit: String, pageSize: Int): Listing<RedditPost> { // create a boundary callback which will observe when the user reaches to the edges of // the list and update the database with extra data. val boundaryCallback = SubredditBoundaryCallback( webservice = redditApi, subredditName = subReddit, handleResponse = this::insertResultIntoDb, ioExecutor = ioExecutor, networkPageSize = pageSize) // we are using a mutable live data to trigger refresh requests which eventually calls // refresh method and gets a new live data. Each refresh request by the user becomes a newly // dispatched data in refreshTrigger val refreshTrigger = MutableLiveData<Unit>() val refreshState = Transformations.switchMap(refreshTrigger) { refresh(subReddit) } // We use toLiveData Kotlin extension function here, you could also use LivePagedListBuilder val livePagedList = LivePagedListBuilder(db.posts().postsBySubreddit(subReddit), pageSize) .setBoundaryCallback(boundaryCallback) .build() return Listing( pagedList = livePagedList, networkState = boundaryCallback.networkState, retry = { boundaryCallback.helper.retryAllFailed() }, refresh = { refreshTrigger.value = null }, refreshState = refreshState ) } } 

рдЖрдЗрдП рджреЗрдЦреЗрдВ рдХрд┐ рд╣рдорд╛рд░реЗ рднрдВрдбрд╛рд░ рдореЗрдВ рдХреНрдпрд╛ рд╣реЛрддрд╛ рд╣реИред


рд╣рдорд╛рд░реЗ рдбреЗрдЯрд╛ рд╕реНрд░реЛрдд рдФрд░ рдбреЗрдЯрд╛ рдПрдХреНрд╕реЗрд╕ рдЗрдВрдЯрд░рдлреЗрд╕ рдХреЗ рдЙрджрд╛рд╣рд░рдг рдмрдирд╛рдПрдБред рдбреЗрдЯрд╛рдмреЗрд╕ рдХреЗ рд▓рд┐рдП:


RoomDatabase рдФрд░ Dao, рдиреЗрдЯрд╡рд░реНрдХ рдХреЗ рд▓рд┐рдП: Retrofit рдФрд░ api рдЗрдВрдЯрд░рдлрд╝реЗрд╕ред


рдЕрдЧрд▓рд╛, рд╣рдо рдЖрд╡рд╢реНрдпрдХ рд░рд┐рдкреЙрдЬрд┐рдЯрд░реА рд╡рд┐рдзрд┐ рдХреЛ рд▓рд╛рдЧреВ рдХрд░рддреЗ рд╣реИрдВ


 fun postsOfSubreddit(subReddit: String, pageSize: Int): Listing<RedditPost> 

рдЬреЛ рд╕реЗрдЯ рдЕрдк рдХрд░рддрд╛ рд╣реИ:


  • рдПрдХ SubRedditBoundaryCallback рдмрдирд╛рдПрдБ рдЬреЛ PagedList рдХреЛ рд╡рд┐рд░рд╛рд╕рдд рдореЗрдВ рдорд┐рд▓рд╛ рд╣реИред BoundaryCallback <>
  • рд╣рдо рдХрдВрд╕реНрдЯреНрд░рдХреНрдЯрд░ рдХрд╛ рдЙрдкрдпреЛрдЧ рдорд╛рдкрджрдВрдбреЛрдВ рдХреЗ рд╕рд╛рде рдХрд░рддреЗ рд╣реИрдВ рдФрд░ рдХрд╛рдо рдХреЗ рд▓рд┐рдП рдмрд╛рдЙрдВрдбреНрд░реАрдХреЙрд▓рдмреИрдХ рдХреЗ рд▓рд┐рдП рдЖрд╡рд╢реНрдпрдХ рд╕рднреА рдЪреАрдЬреЛрдВ рдХреЛ рдкрд╛рд╕ рдХрд░рддреЗ рд╣реИрдВ
  • рдбреЗрдЯрд╛ рдЕрдкрдбреЗрдЯ рдХрд░рдиреЗ рдХреА рдЖрд╡рд╢реНрдпрдХрддрд╛ рдХреЗ рдмрд╛рд░реЗ рдореЗрдВ рд░рд┐рдкреЙрдЬрд┐рдЯрд░реА рдХреЛ рд╕реВрдЪрд┐рдд рдХрд░рдиреЗ рдХреЗ рд▓рд┐рдП рдПрдХ рд░рд┐рдлреНрд░реИрд╕реНрдЯрд┐рдЧрд░ рдЯреНрд░рд┐рдЧрд░ рдмрдирд╛рдПрдБ
  • рдПрдХ рд╕реВрдЪреАрдмрджреНрдз рд╡рд╕реНрддреБ рдмрдирд╛рдПрдБ рдФрд░ рд╡рд╛рдкрд╕ рд▓реМрдЯрд╛рдПрдБ

рд▓рд┐рд╕реНрдЯрд┐рдВрдЧ рд╡рд╕реНрддреБ рдореЗрдВ:


  • livePagedList
  • networkState - рдиреЗрдЯрд╡рд░реНрдХ рд╕реНрдерд┐рддрд┐
  • рд░рд┐рдЯреНрд░реА - рдХреЙрд▓рдмреИрдХ рдЯреВ рдХреЙрд▓ рд╕рд░реНрд╡рд░ рд╕реЗ рдбреЗрдЯрд╛ рдкреБрдирдГ рдкреНрд░рд╛рдкреНрдд рдХрд░рдиреЗ рдХреЗ рд▓рд┐рдП
  • рддрд╛рдЬрд╝рд╛ рдХрд░реЗрдВ - рдбреЗрдЯрд╛ рдЕрдкрдбреЗрдЯ рдХрд░рдиреЗ рдХреЗ рд▓рд┐рдП рдЯреНрд░рд┐рдЧрд░
  • рддрд╛рдЬрд╝рд╛ рдХрд░реЗрдВ - рдЕрджреНрдпрддрди рдкреНрд░рдХреНрд░рд┐рдпрд╛ рдХреА рд╕реНрдерд┐рддрд┐

рд╣рдо рдПрдХ рд╕рд╣рд╛рдпрдХ рд╡рд┐рдзрд┐ рд▓рд╛рдЧреВ рдХрд░рддреЗ рд╣реИрдВ


 private fun insertResultIntoDb(subredditName: String, body: RedditApi.ListingResponse?) 

рдбреЗрдЯрд╛рдмреЗрд╕ рдореЗрдВ рдиреЗрдЯрд╡рд░реНрдХ рдкреНрд░рддрд┐рдХреНрд░рд┐рдпрд╛ рджрд░реНрдЬ рдХрд░рдиреЗ рдХреЗ рд▓рд┐рдПред рдЗрд╕рдХрд╛ рдЙрдкрдпреЛрдЧ рддрдм рдХрд┐рдпрд╛ рдЬрд╛рдПрдЧрд╛ рдЬрдм рдЖрдкрдХреЛ рд╕реВрдЪреА рдХреЛ рдЕрдкрдбреЗрдЯ рдХрд░рдиреЗ рдпрд╛ рдбреЗрдЯрд╛ рдХрд╛ рдПрдХ рдирдпрд╛ рдЯреБрдХрдбрд╝рд╛ рд▓рд┐рдЦрдиреЗ рдХреА рдЖрд╡рд╢реНрдпрдХрддрд╛ рд╣реЛрдЧреАред


рд╣рдо рдПрдХ рд╕рд╣рд╛рдпрдХ рд╡рд┐рдзрд┐ рд▓рд╛рдЧреВ рдХрд░рддреЗ рд╣реИрдВ


 private fun refresh(subredditName: String): LiveData<NetworkState> 

рдбреЗрдЯрд╛ рд░рд┐рдлреНрд░реЗрд╢ рдЯреНрд░рд┐рдЧрд░ рдХреЗ рд▓рд┐рдПред рдпрд╣рд╛рдВ рд╕рдм рдХреБрдЫ рдХрд╛рдлреА рд╕рд░рд▓ рд╣реИ: рд╣рдо рд╕рд░реНрд╡рд░ рд╕реЗ рдбреЗрдЯрд╛ рдкреНрд░рд╛рдкреНрдд рдХрд░рддреЗ рд╣реИрдВ, рдбреЗрдЯрд╛рдмреЗрд╕ рдХреЛ рд╕рд╛рдл рдХрд░рддреЗ рд╣реИрдВ, рдбреЗрдЯрд╛рдмреЗрд╕ рдХреЛ рдирдпрд╛ рдбреЗрдЯрд╛ рд▓рд┐рдЦрддреЗ рд╣реИрдВред


рд╣рдордиреЗ рднрдВрдбрд╛рд░ рдХрд╛ рдкрддрд╛ рд▓рдЧрд╛рдпрд╛ред рдЕрдм SubredditBoundaryCallback рдкрд░ рдХрд░реАрдм рд╕реЗ рдирдЬрд╝рд░ рдбрд╛рд▓рддреЗ рд╣реИрдВред


SubredditBoundaryCallback.kt
 import androidx.paging.PagedList import androidx.annotation.MainThread import com.android.example.paging.pagingwithnetwork.reddit.api.RedditApi import com.memebattle.pagingwithrepository.domain.model.RedditPost import retrofit2.Call import retrofit2.Callback import retrofit2.Response import java.util.concurrent.Executor import com.memebattle.pagingwithrepository.domain.util.PagingRequestHelper import com.memebattle.pagingwithrepository.domain.repository.network.createStatusLiveData /** * This boundary callback gets notified when user reaches to the edges of the list such that the * database cannot provide any more data. * <p> * The boundary callback might be called multiple times for the same direction so it does its own * rate limiting using the com.memebattle.pagingwithrepository.domain.util.PagingRequestHelper class. */ class SubredditBoundaryCallback( private val subredditName: String, private val webservice: RedditApi, private val handleResponse: (String, RedditApi.ListingResponse?) -> Unit, private val ioExecutor: Executor, private val networkPageSize: Int) : PagedList.BoundaryCallback<RedditPost>() { val helper = PagingRequestHelper(ioExecutor) val networkState = helper.createStatusLiveData() /** * Database returned 0 items. We should query the backend for more items. */ @MainThread override fun onZeroItemsLoaded() { helper.runIfNotRunning(PagingRequestHelper.RequestType.INITIAL) { webservice.getTop( subreddit = subredditName, limit = networkPageSize) .enqueue(createWebserviceCallback(it)) } } /** * User reached to the end of the list. */ @MainThread override fun onItemAtEndLoaded(itemAtEnd: RedditPost) { helper.runIfNotRunning(PagingRequestHelper.RequestType.AFTER) { webservice.getTopAfter( subreddit = subredditName, after = itemAtEnd.name, limit = networkPageSize) .enqueue(createWebserviceCallback(it)) } } /** * every time it gets new items, boundary callback simply inserts them into the database and * paging library takes care of refreshing the list if necessary. */ private fun insertItemsIntoDb( response: Response<RedditApi.ListingResponse>, it: PagingRequestHelper.Request.Callback) { ioExecutor.execute { handleResponse(subredditName, response.body()) it.recordSuccess() } } override fun onItemAtFrontLoaded(itemAtFront: RedditPost) { // ignored, since we only ever append to what's in the DB } private fun createWebserviceCallback(it: PagingRequestHelper.Request.Callback) : Callback<RedditApi.ListingResponse> { return object : Callback<RedditApi.ListingResponse> { override fun onFailure(call: Call<RedditApi.ListingResponse>, t: Throwable) { it.recordFailure(t) } override fun onResponse( call: Call<RedditApi.ListingResponse>, response: Response<RedditApi.ListingResponse>) { insertItemsIntoDb(response, it) } } } } 

рд╡рд░реНрдЧ рдореЗрдВ рдХрдИ рдЖрд╡рд╢реНрдпрдХ рд╡рд┐рдзрд┐рдпрд╛рдБ рд╣реИрдВ рдЬреЛ рд╕реАрдорд╛рдмрджреНрдзрддрд╛ рдХреЛ рд╡рд┐рд░рд╛рд╕рдд рдореЗрдВ рджреЗрддреА рд╣реИрдВ:


 override fun onZeroItemsLoaded() 

рдбреЗрдЯрд╛рдмреЗрд╕ рдЦрд╛рд▓реА рд╣реЛрдиреЗ рдкрд░ рд╡рд┐рдзрд┐ рдХреЛ рдХрд╣рд╛ рдЬрд╛рддрд╛ рд╣реИ, рдпрд╣рд╛рдВ рд╣рдореЗрдВ рдкрд╣рд▓реЗ рдкреГрд╖реНрда рдХреЛ рдкреНрд░рд╛рдкреНрдд рдХрд░рдиреЗ рдХреЗ рд▓рд┐рдП рд╕рд░реНрд╡рд░ рд╕реЗ рдЕрдиреБрд░реЛрдз рдкреВрд░рд╛ рдХрд░рдирд╛ рд╣реЛрдЧрд╛ред


 override fun onItemAtEndLoaded(itemAtEnd: RedditPost) 

рдЙрд╕ рд╡рд┐рдзрд┐ рдХреЛ рдХрд╣рд╛ рдЬрд╛рддрд╛ рд╣реИ рдЬрдм рдбреЗрдЯрд╛рдмреЗрд╕ рдХрд╛ "рдиреАрдЪреЗ" рдкрд╣реБрдВрдЪ рдЧрдпрд╛ рд╣реЛрддрд╛ рд╣реИ, рддреЛ рд╣рдореЗрдВ рдЕрдЧрд▓реЗ рдкреГрд╖реНрда рдХреЛ рдкреНрд░рд╛рдкреНрдд рдХрд░рдиреЗ рдХреЗ рд▓рд┐рдП рд╕рд░реНрд╡рд░ рдХреЛ рдХреНрд╡реЗрд░реА рдХрд░рдирд╛ рдкрдбрд╝рддрд╛ рд╣реИ, рдЬрд┐рд╕рдХреЗ рд╕рд╛рде рд╕рд░реНрд╡рд░ рд╕реНрдерд╛рдиреАрдп рд╕реНрдЯреЛрд░ рдХреЗ рдкрд┐рдЫрд▓реЗ рд░рд┐рдХреЙрд░реНрдб рдХреЗ рддреБрд░рдВрдд рдмрд╛рдж рдбреЗрдЯрд╛ рдХреЛ рдЖрдЙрдЯрдкреБрдЯ рдХрд░реЗрдЧрд╛ред


 override fun onItemAtFrontLoaded(itemAtFront: RedditPost) 

рдЙрд╕ рд╡рд┐рдзрд┐ рдХреЛ рдХрд╣рд╛ рдЬрд╛рддрд╛ рд╣реИ рдЬрдм рд╣рдорд╛рд░реЗ рд╕реНрдЯреЛрд░ рдХреЗ рдкрд╣рд▓реЗ рддрддреНрд╡ рддрдХ "рдкреБрдирд░рд╛рд╡реГрддреНрддрд┐рдХрд░реНрддрд╛" рдкрд╣реБрдБрдЪ рдЧрдпрд╛ рд╣реИред рд╣рдорд╛рд░реЗ рдорд╛рдорд▓реЗ рдХреЛ рд▓рд╛рдЧреВ рдХрд░рдиреЗ рдХреЗ рд▓рд┐рдП, рд╣рдо рдЗрд╕ рдкрджреНрдзрддрд┐ рдХреЗ рдХрд╛рд░реНрдпрд╛рдиреНрд╡рдпрди рдХреА рдЙрдкреЗрдХреНрд╖рд╛ рдХрд░ рд╕рдХрддреЗ рд╣реИрдВред


рдбреЗрдЯрд╛ рдкреНрд░рд╛рдкреНрдд рдХрд░рдиреЗ рдФрд░ рдЗрд╕реЗ рдЖрдЧреЗ рд╕реНрдерд╛рдирд╛рдВрддрд░рд┐рдд рдХрд░рдиреЗ рдХреЗ рд▓рд┐рдП рдХреЙрд▓рдмреИрдХ рдЬреЛрдбрд╝реЗрдВ


 fun createWebserviceCallback(it: PagingRequestHelper.Request.Callback) : Callback<RedditApi.ListingResponse> 

рд╣рдо рдбреЗрдЯрд╛рдмреЗрд╕ рдореЗрдВ рдкреНрд░рд╛рдкреНрдд рдбреЗрдЯрд╛ рдХреЛ рд░рд┐рдХреЙрд░реНрдб рдХрд░рдиреЗ рдХреА рд╡рд┐рдзрд┐ рдЬреЛрдбрд╝рддреЗ рд╣реИрдВ


 insertItemsIntoDb( response: Response<RedditApi.ListingResponse>, it: PagingRequestHelper.Request.Callback) 

PagingRequestHelper рд╕рд╣рд╛рдпрдХ рдХреНрдпрд╛ рд╣реИ? рдпрд╣ рдПрдХ рд╕реНрд╡рд╛рд╕реНрдереНрдпрд╡рд░реНрдзрдХ рд╡рд░реНрдЧ рд╣реИ рдЬрд┐рд╕реЗ Google рдиреЗ рд╣рдореЗрдВ рдкреНрд░рджрд╛рди рдХрд┐рдпрд╛ рд╣реИ рдФрд░ рдЗрд╕реЗ рдкреБрд╕реНрддрдХрд╛рд▓рдп рдореЗрдВ рд░рдЦрдиреЗ рдХреА рдкреЗрд╢рдХрд╢ рдХрд░рддрд╛ рд╣реИ, рд▓реЗрдХрд┐рди рд╣рдо рдЗрд╕реЗ рддрд░реНрдХрд╢рд╛рд╕реНрддреНрд░ рдкреИрдХреЗрдЬ рдореЗрдВ рдХреЙрдкреА рдХрд░рддреЗ рд╣реИрдВред


PagingRequestHelper.kt
 package com.memebattle.pagingwithrepository.domain.util;/* * Copyright 2017 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import java.util.Arrays; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.Executor; import java.util.concurrent.atomic.AtomicBoolean; import androidx.annotation.AnyThread; import androidx.annotation.GuardedBy; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import androidx.paging.DataSource; /** * A helper class for {@link androidx.paging.PagedList.BoundaryCallback BoundaryCallback}s and * {@link DataSource}s to help with tracking network requests. * <p> * It is designed to support 3 types of requests, {@link RequestType#INITIAL INITIAL}, * {@link RequestType#BEFORE BEFORE} and {@link RequestType#AFTER AFTER} and runs only 1 request * for each of them via {@link #runIfNotRunning(RequestType, Request)}. * <p> * It tracks a {@link Status} and an {@code error} for each {@link RequestType}. * <p> * A sample usage of this class to limit requests looks like this: * <pre> * class PagingBoundaryCallback extends PagedList.BoundaryCallback&lt;MyItem> { * // TODO replace with an executor from your application * Executor executor = Executors.newSingleThreadExecutor(); * com.memebattle.pagingwithrepository.domain.util.PagingRequestHelper helper = new com.memebattle.pagingwithrepository.domain.util.PagingRequestHelper(executor); * // imaginary API service, using Retrofit * MyApi api; * * {@literal @}Override * public void onItemAtFrontLoaded({@literal @}NonNull MyItem itemAtFront) { * helper.runIfNotRunning(com.memebattle.pagingwithrepository.domain.util.PagingRequestHelper.RequestType.BEFORE, * helperCallback -> api.getTopBefore(itemAtFront.getName(), 10).enqueue( * new Callback&lt;ApiResponse>() { * {@literal @}Override * public void onResponse(Call&lt;ApiResponse> call, * Response&lt;ApiResponse> response) { * // TODO insert new records into database * helperCallback.recordSuccess(); * } * * {@literal @}Override * public void onFailure(Call&lt;ApiResponse> call, Throwable t) { * helperCallback.recordFailure(t); * } * })); * } * * {@literal @}Override * public void onItemAtEndLoaded({@literal @}NonNull MyItem itemAtEnd) { * helper.runIfNotRunning(com.memebattle.pagingwithrepository.domain.util.PagingRequestHelper.RequestType.AFTER, * helperCallback -> api.getTopBefore(itemAtEnd.getName(), 10).enqueue( * new Callback&lt;ApiResponse>() { * {@literal @}Override * public void onResponse(Call&lt;ApiResponse> call, * Response&lt;ApiResponse> response) { * // TODO insert new records into database * helperCallback.recordSuccess(); * } * * {@literal @}Override * public void onFailure(Call&lt;ApiResponse> call, Throwable t) { * helperCallback.recordFailure(t); * } * })); * } * } * </pre> * <p> * The helper provides an API to observe combined request status, which can be reported back to the * application based on your business rules. * <pre> * MutableLiveData&lt;com.memebattle.pagingwithrepository.domain.util.PagingRequestHelper.Status> combined = new MutableLiveData&lt;>(); * helper.addListener(status -> { * // merge multiple states per request type into one, or dispatch separately depending on * // your application logic. * if (status.hasRunning()) { * combined.postValue(com.memebattle.pagingwithrepository.domain.util.PagingRequestHelper.Status.RUNNING); * } else if (status.hasError()) { * // can also obtain the error via {@link StatusReport#getErrorFor(RequestType)} * combined.postValue(com.memebattle.pagingwithrepository.domain.util.PagingRequestHelper.Status.FAILED); * } else { * combined.postValue(com.memebattle.pagingwithrepository.domain.util.PagingRequestHelper.Status.SUCCESS); * } * }); * </pre> */ // THIS class is likely to be moved into the library in a future release. Feel free to copy it // from this sample. public class PagingRequestHelper { private final Object mLock = new Object(); private final Executor mRetryService; @GuardedBy("mLock") private final RequestQueue[] mRequestQueues = new RequestQueue[] {new RequestQueue(RequestType.INITIAL), new RequestQueue(RequestType.BEFORE), new RequestQueue(RequestType.AFTER)}; @NonNull final CopyOnWriteArrayList<Listener> mListeners = new CopyOnWriteArrayList<>(); /** * Creates a new com.memebattle.pagingwithrepository.domain.util.PagingRequestHelper with the given {@link Executor} which is used to run * retry actions. * * @param retryService The {@link Executor} that can run the retry actions. */ public PagingRequestHelper(@NonNull Executor retryService) { mRetryService = retryService; } /** * Adds a new listener that will be notified when any request changes {@link Status state}. * * @param listener The listener that will be notified each time a request's status changes. * @return True if it is added, false otherwise (eg it already exists in the list). */ @AnyThread public boolean addListener(@NonNull Listener listener) { return mListeners.add(listener); } /** * Removes the given listener from the listeners list. * * @param listener The listener that will be removed. * @return True if the listener is removed, false otherwise (eg it never existed) */ public boolean removeListener(@NonNull Listener listener) { return mListeners.remove(listener); } /** * Runs the given {@link Request} if no other requests in the given request type is already * running. * <p> * If run, the request will be run in the current thread. * * @param type The type of the request. * @param request The request to run. * @return True if the request is run, false otherwise. */ @SuppressWarnings("WeakerAccess") @AnyThread public boolean runIfNotRunning(@NonNull RequestType type, @NonNull Request request) { boolean hasListeners = !mListeners.isEmpty(); StatusReport report = null; synchronized (mLock) { RequestQueue queue = mRequestQueues[type.ordinal()]; if (queue.mRunning != null) { return false; } queue.mRunning = request; queue.mStatus = Status.RUNNING; queue.mFailed = null; queue.mLastError = null; if (hasListeners) { report = prepareStatusReportLocked(); } } if (report != null) { dispatchReport(report); } final RequestWrapper wrapper = new RequestWrapper(request, this, type); wrapper.run(); return true; } @GuardedBy("mLock") private StatusReport prepareStatusReportLocked() { Throwable[] errors = new Throwable[]{ mRequestQueues[0].mLastError, mRequestQueues[1].mLastError, mRequestQueues[2].mLastError }; return new StatusReport( getStatusForLocked(RequestType.INITIAL), getStatusForLocked(RequestType.BEFORE), getStatusForLocked(RequestType.AFTER), errors ); } @GuardedBy("mLock") private Status getStatusForLocked(RequestType type) { return mRequestQueues[type.ordinal()].mStatus; } @AnyThread @VisibleForTesting void recordResult(@NonNull RequestWrapper wrapper, @Nullable Throwable throwable) { StatusReport report = null; final boolean success = throwable == null; boolean hasListeners = !mListeners.isEmpty(); synchronized (mLock) { RequestQueue queue = mRequestQueues[wrapper.mType.ordinal()]; queue.mRunning = null; queue.mLastError = throwable; if (success) { queue.mFailed = null; queue.mStatus = Status.SUCCESS; } else { queue.mFailed = wrapper; queue.mStatus = Status.FAILED; } if (hasListeners) { report = prepareStatusReportLocked(); } } if (report != null) { dispatchReport(report); } } private void dispatchReport(StatusReport report) { for (Listener listener : mListeners) { listener.onStatusChange(report); } } /** * Retries all failed requests. * * @return True if any request is retried, false otherwise. */ public boolean retryAllFailed() { final RequestWrapper[] toBeRetried = new RequestWrapper[RequestType.values().length]; boolean retried = false; synchronized (mLock) { for (int i = 0; i < RequestType.values().length; i++) { toBeRetried[i] = mRequestQueues[i].mFailed; mRequestQueues[i].mFailed = null; } } for (RequestWrapper failed : toBeRetried) { if (failed != null) { failed.retry(mRetryService); retried = true; } } return retried; } static class RequestWrapper implements Runnable { @NonNull final Request mRequest; @NonNull final PagingRequestHelper mHelper; @NonNull final RequestType mType; RequestWrapper(@NonNull Request request, @NonNull PagingRequestHelper helper, @NonNull RequestType type) { mRequest = request; mHelper = helper; mType = type; } @Override public void run() { mRequest.run(new Request.Callback(this, mHelper)); } void retry(Executor service) { service.execute(new Runnable() { @Override public void run() { mHelper.runIfNotRunning(mType, mRequest); } }); } } /** * Runner class that runs a request tracked by the {@link PagingRequestHelper}. * <p> * When a request is invoked, it must call one of {@link Callback#recordFailure(Throwable)} * or {@link Callback#recordSuccess()} once and only once. This call * can be made any time. Until that method call is made, {@link PagingRequestHelper} will * consider the request is running. */ @FunctionalInterface public interface Request { /** * Should run the request and call the given {@link Callback} with the result of the * request. * * @param callback The callback that should be invoked with the result. */ void run(Callback callback); /** * Callback class provided to the {@link #run(Callback)} method to report the result. */ class Callback { private final AtomicBoolean mCalled = new AtomicBoolean(); private final RequestWrapper mWrapper; private final PagingRequestHelper mHelper; Callback(RequestWrapper wrapper, PagingRequestHelper helper) { mWrapper = wrapper; mHelper = helper; } /** * Call this method when the request succeeds and new data is fetched. */ @SuppressWarnings("unused") public final void recordSuccess() { if (mCalled.compareAndSet(false, true)) { mHelper.recordResult(mWrapper, null); } else { throw new IllegalStateException( "already called recordSuccess or recordFailure"); } } /** * Call this method with the failure message and the request can be retried via * {@link #retryAllFailed()}. * * @param throwable The error that occured while carrying out the request. */ @SuppressWarnings("unused") public final void recordFailure(@NonNull Throwable throwable) { //noinspection ConstantConditions if (throwable == null) { throw new IllegalArgumentException("You must provide a throwable describing" + " the error to record the failure"); } if (mCalled.compareAndSet(false, true)) { mHelper.recordResult(mWrapper, throwable); } else { throw new IllegalStateException( "already called recordSuccess or recordFailure"); } } } } /** * Data class that holds the information about the current status of the ongoing requests * using this helper. */ public static final class StatusReport { /** * Status of the latest request that were submitted with {@link RequestType#INITIAL}. */ @NonNull public final Status initial; /** * Status of the latest request that were submitted with {@link RequestType#BEFORE}. */ @NonNull public final Status before; /** * Status of the latest request that were submitted with {@link RequestType#AFTER}. */ @NonNull public final Status after; @NonNull private final Throwable[] mErrors; StatusReport(@NonNull Status initial, @NonNull Status before, @NonNull Status after, @NonNull Throwable[] errors) { this.initial = initial; this.before = before; this.after = after; this.mErrors = errors; } /** * Convenience method to check if there are any running requests. * * @return True if there are any running requests, false otherwise. */ public boolean hasRunning() { return initial == Status.RUNNING || before == Status.RUNNING || after == Status.RUNNING; } /** * Convenience method to check if there are any requests that resulted in an error. * * @return True if there are any requests that finished with error, false otherwise. */ public boolean hasError() { return initial == Status.FAILED || before == Status.FAILED || after == Status.FAILED; } /** * Returns the error for the given request type. * * @param type The request type for which the error should be returned. * @return The {@link Throwable} returned by the failing request with the given type or * {@code null} if the request for the given type did not fail. */ @Nullable public Throwable getErrorFor(@NonNull RequestType type) { return mErrors[type.ordinal()]; } @Override public String toString() { return "StatusReport{" + "initial=" + initial + ", before=" + before + ", after=" + after + ", mErrors=" + Arrays.toString(mErrors) + '}'; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; StatusReport that = (StatusReport) o; if (initial != that.initial) return false; if (before != that.before) return false; if (after != that.after) return false; // Probably incorrect - comparing Object[] arrays with Arrays.equals return Arrays.equals(mErrors, that.mErrors); } @Override public int hashCode() { int result = initial.hashCode(); result = 31 * result + before.hashCode(); result = 31 * result + after.hashCode(); result = 31 * result + Arrays.hashCode(mErrors); return result; } } /** * Listener interface to get notified by request status changes. */ public interface Listener { /** * Called when the status for any of the requests has changed. * * @param report The current status report that has all the information about the requests. */ void onStatusChange(@NonNull StatusReport report); } /** * Represents the status of a Request for each {@link RequestType}. */ public enum Status { /** * There is current a running request. */ RUNNING, /** * The last request has succeeded or no such requests have ever been run. */ SUCCESS, /** * The last request has failed. */ FAILED } /** * Available request types. */ public enum RequestType { /** * Corresponds to an initial request made to a {@link DataSource} or the empty state for * a {@link androidx.paging.PagedList.BoundaryCallback BoundaryCallback}. */ INITIAL, /** * Corresponds to the {@code loadBefore} calls in {@link DataSource} or * {@code onItemAtFrontLoaded} in * {@link androidx.paging.PagedList.BoundaryCallback BoundaryCallback}. */ BEFORE, /** * Corresponds to the {@code loadAfter} calls in {@link DataSource} or * {@code onItemAtEndLoaded} in * {@link androidx.paging.PagedList.BoundaryCallback BoundaryCallback}. */ AFTER } class RequestQueue { @NonNull final RequestType mRequestType; @Nullable RequestWrapper mFailed; @Nullable Request mRunning; @Nullable Throwable mLastError; @NonNull Status mStatus = Status.SUCCESS; RequestQueue(@NonNull RequestType requestType) { mRequestType = requestType; } } } 

PagingRequestHelperExt.kt
 import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import com.memebattle.pagingwithrepository.domain.util.PagingRequestHelper private fun getErrorMessage(report: PagingRequestHelper.StatusReport): String { return PagingRequestHelper.RequestType.values().mapNotNull { report.getErrorFor(it)?.message }.first() } fun PagingRequestHelper.createStatusLiveData(): LiveData<NetworkState> { val liveData = MutableLiveData<NetworkState>() addListener { report -> when { report.hasRunning() -> liveData.postValue(NetworkState.LOADING) report.hasError() -> liveData.postValue( NetworkState.error(getErrorMessage(report))) else -> liveData.postValue(NetworkState.LOADED) } } return liveData } 

, .
MVVM Google ViewModel LiveData.


MainActivity.kt
 import android.os.Bundle import android.view.KeyEvent import android.view.inputmethod.EditorInfo import androidx.lifecycle.Observer import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProviders import androidx.paging.PagedList import com.memebattle.pagingwithrepository.R import com.memebattle.pagingwithrepository.domain.model.RedditPost import com.memebattle.pagingwithrepository.domain.repository.MainRepository import com.memebattle.pagingwithrepository.domain.repository.network.NetworkState import com.memebattle.pagingwithrepository.presentation.recycler.PostsAdapter import kotlinx.android.synthetic.main.activity_main.* class MainActivity : AppCompatActivity() { companion object { const val KEY_SUBREDDIT = "subreddit" const val DEFAULT_SUBREDDIT = "androiddev" } lateinit var model: MainViewModel override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) model = getViewModel() initAdapter() initSwipeToRefresh() initSearch() val subreddit = savedInstanceState?.getString(KEY_SUBREDDIT) ?: DEFAULT_SUBREDDIT model.showSubReddit(subreddit) } private fun getViewModel(): MainViewModel { return ViewModelProviders.of(this, object : ViewModelProvider.Factory { override fun <T : ViewModel?> create(modelClass: Class<T>): T { val repo = MainRepository(this@MainActivity) @Suppress("UNCHECKED_CAST") return MainViewModel(repo) as T } })[MainViewModel::class.java] } private fun initAdapter() { val adapter = PostsAdapter { model.retry() } list.adapter = adapter model.posts.observe(this, Observer<PagedList<RedditPost>> { adapter.submitList(it) }) model.networkState.observe(this, Observer { adapter.setNetworkState(it) }) } private fun initSwipeToRefresh() { model.refreshState.observe(this, Observer { swipe_refresh.isRefreshing = it == NetworkState.LOADING }) swipe_refresh.setOnRefreshListener { model.refresh() } } override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) outState.putString(KEY_SUBREDDIT, model.currentSubreddit()) } private fun initSearch() { input.setOnEditorActionListener { _, actionId, _ -> if (actionId == EditorInfo.IME_ACTION_GO) { updatedSubredditFromInput() true } else { false } } input.setOnKeyListener { _, keyCode, event -> if (event.action == KeyEvent.ACTION_DOWN && keyCode == KeyEvent.KEYCODE_ENTER) { updatedSubredditFromInput() true } else { false } } } private fun updatedSubredditFromInput() { input.text.trim().toString().let { if (it.isNotEmpty()) { if (model.showSubReddit(it)) { list.scrollToPosition(0) (list.adapter as? PostsAdapter)?.submitList(null) } } } } } 

onCreate ViewModel, , .


LiveData ViewModel, .


MainViewModel.kt
 import androidx.lifecycle.MutableLiveData import androidx.lifecycle.Transformations import androidx.lifecycle.ViewModel import com.memebattle.pagingwithrepository.domain.repository.core.RedditPostRepository class MainViewModel(private val repository: RedditPostRepository) : ViewModel() { private val subredditName = MutableLiveData<String>() private val repoResult = Transformations.map(subredditName) { repository.postsOfSubreddit(it, 10) } val posts = Transformations.switchMap(repoResult) { it.pagedList }!! val networkState = Transformations.switchMap(repoResult) { it.networkState }!! val refreshState = Transformations.switchMap(repoResult) { it.refreshState }!! fun refresh() { repoResult.value?.refresh?.invoke() } fun showSubReddit(subreddit: String): Boolean { if (subredditName.value == subreddit) { return false } subredditName.value = subreddit return true } fun retry() { val listing = repoResult?.value listing?.retry?.invoke() } fun currentSubreddit(): String? = subredditName.value } 

, : retry refesh.


PagedListAdapter. .


PostAdapter.kt
 import androidx.paging.PagedListAdapter import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView import android.view.ViewGroup import com.memebattle.pagingwithrepository.R import com.memebattle.pagingwithrepository.domain.model.RedditPost import com.memebattle.pagingwithrepository.domain.repository.network.NetworkState import com.memebattle.pagingwithrepository.presentation.recycler.viewholder.NetworkStateItemViewHolder import com.memebattle.pagingwithrepository.presentation.recycler.viewholder.RedditPostViewHolder /** * A simple adapter implementation that shows Reddit posts. */ class PostsAdapter( private val retryCallback: () -> Unit) : PagedListAdapter<RedditPost, RecyclerView.ViewHolder>(POST_COMPARATOR) { private var networkState: NetworkState? = null override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { when (getItemViewType(position)) { R.layout.reddit_post_item -> (holder as RedditPostViewHolder).bind(getItem(position)) R.layout.network_state_item -> (holder as NetworkStateItemViewHolder).bindTo( networkState) } } override fun onBindViewHolder( holder: RecyclerView.ViewHolder, position: Int, payloads: MutableList<Any>) { if (payloads.isNotEmpty()) { val item = getItem(position) (holder as RedditPostViewHolder).updateScore(item) } else { onBindViewHolder(holder, position) } } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { return when (viewType) { R.layout.reddit_post_item -> RedditPostViewHolder.create(parent) R.layout.network_state_item -> NetworkStateItemViewHolder.create(parent, retryCallback) else -> throw IllegalArgumentException("unknown view type $viewType") } } private fun hasExtraRow() = networkState != null && networkState != NetworkState.LOADED override fun getItemViewType(position: Int): Int { return if (hasExtraRow() && position == itemCount - 1) { R.layout.network_state_item } else { R.layout.reddit_post_item } } override fun getItemCount(): Int { return super.getItemCount() + if (hasExtraRow()) 1 else 0 } fun setNetworkState(newNetworkState: NetworkState?) { val previousState = this.networkState val hadExtraRow = hasExtraRow() this.networkState = newNetworkState val hasExtraRow = hasExtraRow() if (hadExtraRow != hasExtraRow) { if (hadExtraRow) { notifyItemRemoved(super.getItemCount()) } else { notifyItemInserted(super.getItemCount()) } } else if (hasExtraRow && previousState != newNetworkState) { notifyItemChanged(itemCount - 1) } } companion object { private val PAYLOAD_SCORE = Any() val POST_COMPARATOR = object : DiffUtil.ItemCallback<RedditPost>() { override fun areContentsTheSame(oldItem: RedditPost, newItem: RedditPost): Boolean = oldItem == newItem override fun areItemsTheSame(oldItem: RedditPost, newItem: RedditPost): Boolean = oldItem.name == newItem.name override fun getChangePayload(oldItem: RedditPost, newItem: RedditPost): Any? { return if (sameExceptScore(oldItem, newItem)) { PAYLOAD_SCORE } else { null } } } private fun sameExceptScore(oldItem: RedditPost, newItem: RedditPost): Boolean { // DON'T do this copy in a real app, it is just convenient here for the demo :) // because reddit randomizes scores, we want to pass it as a payload to minimize // UI updates between refreshes return oldItem.copy(score = newItem.score) == newItem } } } 

ViewHolder .


RedditPostViewHolder.kt
 import android.content.Intent import android.net.Uri import androidx.recyclerview.widget.RecyclerView import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.ImageView import android.widget.TextView import com.memebattle.pagingwithrepository.R import com.memebattle.pagingwithrepository.domain.model.RedditPost /** * A RecyclerView ViewHolder that displays a reddit post. */ class RedditPostViewHolder(view: View) : RecyclerView.ViewHolder(view) { private val title: TextView = view.findViewById(R.id.title) private val subtitle: TextView = view.findViewById(R.id.subtitle) private val score: TextView = view.findViewById(R.id.score) private var post : RedditPost? = null init { view.setOnClickListener { post?.url?.let { url -> val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url)) view.context.startActivity(intent) } } } fun bind(post: RedditPost?) { this.post = post title.text = post?.title ?: "loading" subtitle.text = itemView.context.resources.getString(R.string.post_subtitle, post?.author ?: "unknown") score.text = "${post?.score ?: 0}" } companion object { fun create(parent: ViewGroup): RedditPostViewHolder { val view = LayoutInflater.from(parent.context) .inflate(R.layout.reddit_post_item, parent, false) return RedditPostViewHolder(view) } } fun updateScore(item: RedditPost?) { post = item score.text = "${item?.score ?: 0}" } } 

NetworkStateItemViewHolder.kt
 import androidx.recyclerview.widget.RecyclerView import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.Button import android.widget.ProgressBar import android.widget.TextView import com.memebattle.pagingwithrepository.R import com.memebattle.pagingwithrepository.domain.repository.network.NetworkState import com.memebattle.pagingwithrepository.domain.repository.network.Status /** * A View Holder that can display a loading or have click action. * It is used to show the network state of paging. */ class NetworkStateItemViewHolder(view: View, private val retryCallback: () -> Unit) : RecyclerView.ViewHolder(view) { private val progressBar = view.findViewById<ProgressBar>(R.id.progress_bar) private val retry = view.findViewById<Button>(R.id.retry_button) private val errorMsg = view.findViewById<TextView>(R.id.error_msg) init { retry.setOnClickListener { retryCallback() } } fun bindTo(networkState: NetworkState?) { progressBar.visibility = toVisibility(networkState?.status == Status.RUNNING) retry.visibility = toVisibility(networkState?.status == Status.FAILED) errorMsg.visibility = toVisibility(networkState?.msg != null) errorMsg.text = networkState?.msg } companion object { fun create(parent: ViewGroup, retryCallback: () -> Unit): NetworkStateItemViewHolder { val view = LayoutInflater.from(parent.context) .inflate(R.layout.network_state_item, parent, false) return NetworkStateItemViewHolder(view, retryCallback) } fun toVisibility(constraint : Boolean): Int { return if (constraint) { View.VISIBLE } else { View.GONE } } } } 

, , Reddit androiddev. , .


рдЫрд╡рд┐


, !


, Google.


рд╡рд╣ рд╕рдм рд╣реИред тАЬтАЭ , .


!

Source: https://habr.com/ru/post/hi431212/


All Articles