Tentunya setiap pengembang Android bekerja dengan daftar menggunakan RecyclerView. Dan juga banyak yang berhasil melihat bagaimana mengatur pagination dalam daftar menggunakan Paging Library dari Android Architecture Components.
Sederhana saja: atur PositionalDataSource, atur konfigurasi, buat PagedList dan berikan semuanya dengan adaptor dan DiffUtilCallback ke RecyclerView kami.
Tetapi bagaimana jika kita memiliki beberapa sumber data? Misalnya, kami ingin memiliki cache di Kamar dan menerima data dari jaringan.
Kasing ternyata cukup khusus dan tidak ada banyak informasi tentang topik ini di Internet. Saya akan mencoba memperbaikinya dan menunjukkan bagaimana kasus seperti itu dapat diselesaikan.

Jika Anda masih belum terbiasa dengan penerapan pagination dengan satu sumber data, maka saya menyarankan Anda untuk membiasakan diri dengan hal ini sebelum membaca artikel.
Seperti apa solusi tanpa pagination:
- Akses ke cache (dalam kasus kami, ini adalah database)
- Jika cache kosong - kirim permintaan ke server
- Kami menerima data dari server
- Kami menampilkannya dalam lembaran
- Tulis ke cache
- Jika ada cache - tampilkan dalam daftar
- Kami mendapatkan data terbaru dari server
- Kami menampilkannya dalam daftar β
- Tulis ke cache

Suatu hal yang nyaman seperti pagination, yang menyederhanakan kehidupan pengguna, di sini mempersulit kita. Mari kita coba bayangkan masalah apa yang mungkin timbul ketika menerapkan daftar paginasi dengan banyak sumber data.
Algoritma ini kira-kira sebagai berikut:
- Dapatkan data cache untuk halaman pertama
- Jika cache kosong, kami mendapatkan data server, menampilkannya dalam daftar dan menulis ke database
- Jika ada cache, muat ke dalam daftar
- Jika kita sampai ke akhir basis data, maka kita meminta data dari server, menampilkannya
- dalam daftar dan tulis ke database
Dari fitur-fitur pendekatan ini, Anda dapat melihat bahwa untuk menampilkan daftar, cache pertama kali disurvei, dan sinyal untuk memuat data baru adalah akhir dari cache.

Google memikirkannya dan menciptakan solusi yang keluar dari kotak PagingLibrary - BoundaryCallback.
BoundaryCallback melaporkan ketika sumber data lokal βberakhirβ dan memberi tahu repositori untuk mengunduh data baru.

Situs web resmi Android Dev memiliki tautan ke repositori dengan proyek contoh menggunakan daftar pagination dengan dua sumber data: Jaringan (Retrofit 2) + Basis Data (Kamar). Untuk lebih memahami bagaimana sistem seperti itu bekerja, mari kita coba untuk menguraikan contoh ini dan sedikit menyederhanakannya.
Mari kita mulai dengan layer data. Buat dua DataSource.
Antarmuka RedditApi.ktimport com.memebattle.pagingwithrepository.domain.model.RedditPost import retrofit2.Call import retrofit2.http.GET import retrofit2.http.Path import retrofit2.http.Query interface RedditApi { @GET("/r/{subreddit}/hot.json") fun getTop( @Path("subreddit") subreddit: String, @Query("limit") limit: Int): Call<ListingResponse>
Antarmuka ini menjelaskan permintaan ke Reddit API dan kelas model (ListingResponse, ListingData, RedditChildrenResponse) ke mana objek tanggapan API akan diciutkan.
Dan segera buat model untuk Retrofit dan Ruangan
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")
Kelas RedditDb.kt yang akan mewarisi RoomDatabase.
Redditdb.kt import androidx.room.Database import androidx.room.RoomDatabase import com.memebattle.pagingwithrepository.domain.model.RedditPost @Database( entities = [RedditPost::class], version = 1, exportSchema = false ) abstract class RedditDb : RoomDatabase() { abstract fun posts(): RedditPostDao }
Ingatlah bahwa membuat kelas RoomDatabase setiap kali untuk mengeksekusi kueri ke database sangat mahal, jadi dalam kasus nyata, buat sekali untuk seumur hidup aplikasi!
Dan kelas Dao dengan query database 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 }
Anda mungkin memperhatikan bahwa metode pengambilan posting postsBySubreddit kembali
DataSource.Factory. Ini diperlukan untuk membuat PagedList kami menggunakan
LivePagedListBuilder, di utas latar belakang. Anda dapat membaca lebih lanjut tentang ini di
pelajaran
Hebat, lapisan data sudah siap. Kita beralih ke lapisan logika bisnis.Untuk menerapkan pola Repositori, sudah lazim untuk membuat antarmuka repositori secara terpisah dari implementasinya. Oleh karena itu, kami akan membuat antarmuka RedditPostRepository.kt
RedditPostRepository.kt interface RedditPostRepository { fun postsOfSubreddit(subReddit: String, pageSize: Int): Listing<RedditPost> }
Dan segera pertanyaannya adalah - Daftar seperti apa? Ini adalah kelas tanggal yang diperlukan untuk menampilkan daftar.
Listing.kt import androidx.lifecycle.LiveData import androidx.paging.PagedList import com.memebattle.pagingwithrepository.domain.repository.network.NetworkState 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)
Buat implementasi repositori 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/")
Mari kita lihat apa yang terjadi di repositori kami.
Buat instance sumber data dan antarmuka akses data kami. Untuk basis data:
RoomDatabase dan Dao, untuk jaringan: Retrofit dan antarmuka api.
Selanjutnya, kami menerapkan metode repositori yang diperlukan
fun postsOfSubreddit(subReddit: String, pageSize: Int): Listing<RedditPost>
yang mengatur pagination:
- Buat SubRedditBoundaryCallback yang mewarisi PagedList.BoundaryCallback <>
- Kami menggunakan konstruktor dengan parameter dan meneruskan semua yang diperlukan agar BoundaryCallback berfungsi
- Buat pemicu refreshTrigger untuk memberi tahu repositori tentang perlunya memperbarui data
- Buat dan kembalikan objek Daftar
Di objek Daftar:
- livePagedList
- networkState - status jaringan
- coba lagi - panggilan balik untuk memanggil mengambil data dari server
- refresh - trigger untuk memperbarui data
- refreshState - status dari proses pembaruan
Kami menerapkan metode bantu
private fun insertResultIntoDb(subredditName: String, body: RedditApi.ListingResponse?)
untuk merekam respons jaringan dalam database. Ini akan digunakan ketika Anda perlu memperbarui daftar atau menulis data baru.
Kami menerapkan metode bantu
private fun refresh(subredditName: String): LiveData<NetworkState>
untuk pemicu penyegaran data. Semuanya cukup sederhana di sini: kami mendapatkan data dari server, membersihkan database, menulis data baru ke database.
Kami menemukan repositori. Sekarang mari kita melihat lebih dekat pada 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 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() @MainThread override fun onZeroItemsLoaded() { helper.runIfNotRunning(PagingRequestHelper.RequestType.INITIAL) { webservice.getTop( subreddit = subredditName, limit = networkPageSize) .enqueue(createWebserviceCallback(it)) } } @MainThread override fun onItemAtEndLoaded(itemAtEnd: RedditPost) { helper.runIfNotRunning(PagingRequestHelper.RequestType.AFTER) { webservice.getTopAfter( subreddit = subredditName, after = itemAtEnd.name, limit = networkPageSize) .enqueue(createWebserviceCallback(it)) } } private fun insertItemsIntoDb( response: Response<RedditApi.ListingResponse>, it: PagingRequestHelper.Request.Callback) { ioExecutor.execute { handleResponse(subredditName, response.body()) it.recordSuccess() } } override fun onItemAtFrontLoaded(itemAtFront: RedditPost) {
Ada beberapa metode yang diperlukan di kelas yang mewarisi BoundaryCallback:
override fun onZeroItemsLoaded()
Metode ini dipanggil ketika database kosong, di sini kita harus memenuhi permintaan ke server untuk mendapatkan halaman pertama.
override fun onItemAtEndLoaded(itemAtEnd: RedditPost)
Metode ini dipanggil ketika "iterator" telah mencapai "bawah" dari database, di sini kita harus meminta server untuk mendapatkan halaman berikutnya, melewati kunci yang dengannya server akan menampilkan data segera setelah catatan terakhir dari toko lokal.
override fun onItemAtFrontLoaded(itemAtFront: RedditPost)
Metode ini dipanggil ketika "iterator" telah mencapai elemen pertama dari toko kami. Untuk menerapkan kasus kami, kami dapat mengabaikan penerapan metode ini.
Tambahkan panggilan balik untuk menerima data dan mentransfernya lebih lanjut
fun createWebserviceCallback(it: PagingRequestHelper.Request.Callback) : Callback<RedditApi.ListingResponse>
Kami menambahkan metode pencatatan data yang diterima dalam database
insertItemsIntoDb( response: Response<RedditApi.ListingResponse>, it: PagingRequestHelper.Request.Callback)
Apa itu pembantu PagingRequestHelper? Ini adalah kelas SEHAT yang disediakan Google dengan ramah kepada kami dan menawarkan untuk menaruhnya di perpustakaan, tetapi kami hanya menyalinnya ke dalam paket lapisan logika.
PagingRequestHelper.kt package com.memebattle.pagingwithrepository.domain.util; 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;
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 }
Setelah lapisan logika bisnis selesai, kita bisa beralih ke mengimplementasikan tampilan.
Di lapisan presentasi, kami memiliki MVVM baru dari Google di ViewModel dan 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) } } } } }
Dalam metode onCreate, inisialisasi ViewModel, adaptor daftar, berlangganan perubahan nama langganan dan panggil repositori untuk memulai pekerjaan melalui model.
Jika Anda tidak terbiasa dengan mekanisme LiveData dan ViewModel, saya sarankan Anda membiasakan diri dengan pelajaran .
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 }
Dalam model, kami menerapkan metode yang akan menarik metode repositori: coba lagi dan refesh.
Adaptor daftar akan mewarisi PagedListAdapter. Di sini semuanya sama dengan bekerja dengan pagination dan satu sumber data.
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 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 {
Dan semua ViewHolders yang sama untuk menampilkan catatan dan status item mengunduh data dari jaringan.
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 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 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.
Itu saja. ββ , .
!