最近のアプリの主流の構造
典型的なフィードストリームの作業モデル
フィードストリームの構造は、一般的に3つの主要なブロック、すなわち、ヘッダー領域、コンテンツエリアとフッター領域に分割され、その役割は、表示し、ドロップダウンのロード、情報コンテンツの表示、および表示の過程でより多くのコンテンツをロードするためにスライドアップのプロセスを促すことです。フッター領域は、一般的に最も単純であり、唯一のフィードの最後に挿入する必要があり、あなたが一番下にスライドして、より多くのデータをロードするとき、ヘッダー領域は、ロードと通知の2つの機能に分けることができます。ヘッダー領域は、ローディングと通知2つの機能に分けることができ、ローディングとフッターは、ユーザーが結果をロードするために待つことを意味し、ドロップダウンリフレッシュショーで、に対応しています。そして、通知は、ドロップダウンがロードされたように、主に役割のリマインダーですどのように多くのデータ、嫌い、操作のヒントなどの結果を削除します。コンテンツエリアの真ん中にメインコンテンツの表示領域は、サーバーから返されたデータの各部分の種類に応じて、対応するViewのレイアウトを見つける、データの各部分は、UIコントロールにレンダリングされます。
フィードの作業モードは、次の場合に分かれています。
- ロードの初期化
- スクロールダウンして更新とプロンプトが表示されます。
- スライディングムーブメント
- 下にスライドさせると、より多くのデータが読み込まれ、フッターが表示されます。
- コンテンツのアイテムが操作されると、リストを更新します。
ここでまた、無限フィードストリームと呼ばれる作業モードを言及する必要があります、つまり、ユーザーがn個のアイテムの下部からの距離にスライドさせると、ロードモアをトリガする必要があり、ユーザーがスライドを続行すると、それはフィードストリームの中断の現象から出てくるのではなく、全体のフィードストリームが無限に長く感じられるように、ユーザー体験の程度を向上させるように。
RecyclerViewに基づくフィードストリームの実装プロセス
RecyclerViewは、様々なアイテムのレイアウトと大きなフィードリストデータのため、フィードに適しています。RecyclerViewは、FeedListの上に2層のキャッシュを追加し、開発者がより柔軟にレイアウトの再利用に対処できるようにし、Viewの作成プロセスとデータバインディングプロセスを分離することで、より細かい粒度でフィードのパフォーマンスを最適化します。フィードのパフォーマンスをより細かい単位で最適化します。 レイアウトの再利用を基本としているので、 さまざまな環境のレイアウトでフィード全体のパフォーマンスを維持することができます。
フィードストリームのロードプロセスは、一般的に初期化、すなわち、データのロード、ビューの初期化、およびデータの初期レンダリングに分かれています。もう少し詳しく説明すると、ネットワークリクエスト/ローカルクエリの結果は解析され、データはリストにリフレッシュされ、リストの状態が設定されます。初期化されていないロード処理では、データが解析された後、データがリストにリフレッシュされる前に、重複排除/嫌いな処理が行われます。ドロップダウンのリフレッシュには、既存のデータをすべて削除して、新しく要求されたデータをリストの先頭にロードする方法と、RecyclerViewのローカル・リフレッシュapiを直接使用することで実現できる、リストの末尾にデータをロードするuploadmoreの2つの戦略があります。
Paging
まずページングアーキテクチャ図上でJetpackは、ページングを介してリストの無限のロードを達成するためにMVVMを使用することをお勧めします、ビュー層としてページング、PagedListのデータを保存するViewModel層では、PagedListのデータを作成し、ViewModelにそれを更新するリポジトリは、ここでリポジトリは、モデル層に相当する、ビューは、PagedListの変更に耳を傾けるPagedListAdapterは、リストを更新するために通知するDataSourceの作成でPagedListが渡されながら、PagedListAdapterは、より多くのデータのロードの呼び出し後にロードするDataSourceのPagedListAdapter は DataSource の loadAfter メソッドを呼び出してデータをロードし、 onResult メソッドを使用して PagedList にデータを格納してリストを更新します。
ここにもページングの概念が含まれ、フロントエンドのページング表示と同じ形式ですが、 フロントエンドは現在のページのデータを次のページのデータに置き換えるのに対し、 ページングの次のページは現在のページの下にロードモアが接続された形になっています。より正確には、ページングはページングのロード方法であり、唯一の1ページが毎回ロードされ、ユーザーが左の事前定義されたn個の項目の下部の位置をスライドさせると、それは次のページをロードするアクションをトリガされますので、無限リストを達成するために。
ページング機能を公開するPagedListAdapterを提供するために、PagedListは、データストレージオブジェクトであるだけでなく、ページの読み込み設定オブジェクト、PagedListAdapterはRecyclerを使用することは推奨されません。PagedListAdapterは、Recycler.Adapterの部分的なリフレッシュのAPIを使用することをお勧めしませんが、submitListメソッドを使用して、このメソッドは、差分操作を使用して、つまり、各位置のデータ、比較のためのリフレッシュ前後のデータとなり、この差分は、インターフェイスであり、ユーザーは、IOスレッドのセットでdiff.Pagingの差分操作の粒度を制御することができますリフレッシュプロセスは、メインスレッドです。
無限リストを実装するためのページング
ドロップダウンリフレッシュとloadmore機能を備えた無制限のリスト、およびloadmoreはページングを使用することです自動loadmoreが付属していますので、実際には一般的な状況では、フッターを実装する必要はありませんので、loadmoreは、フッターの下部に到達していない表示されることはありません、ここでは最も簡素化の原則に従ってデモ。もちろん、また、フッターを追加することができ、フッターを削除するには、データを挿入する前に、毎回追加してから追加します。ドロップダウンリフレッシュは、ローディングアニメーションを表示するかどうかを制御するためにsetRefreshing(boolean)を実装するためにSwipeRefreshLayoutを使用しています。
ページングの機能」のアーキテクチャ図によると、すべてのロジックとUI表示の軸はViewModelであるため、まず無限リストを持つViewModelを実装する必要があります。このViewModelは、RefreshStateとPagedListという2つのLiveDataを保持する必要があり、UIを更新するために、データ要求の前とRefreshStateの完了時にPagedListを更新する必要があります。UIは、PagedList LiveDataに作成し、更新する初期化とドロップダウンアクションでPagedListは、リストを更新するアダプタに送信するPagedListへの変更で見つかったオブザーバに耳を傾けるように、操作のリストを更新する初期化とドロップダウンのリフレッシュプロセスを実現します。
class ListViewModel: ViewModel() {
companion object {
const val STATE_INIT = 0
const val STATE_REFRESHING = 1
const val STATE_REFRESHED = 2
const val STATE_LOADING = 3
const val STATE_LOADED = 4
}
val refreshState = MutableLiveData<Int>().apply { postValue(STATE_INIT) }
val repository = ListDataRepository(refreshState)
val pagedList = MutableLiveData<PagedList<Item>>().apply { postValue(repository.getPagedList()) }
fun refresh() {
pagedList.postValue(repository.getPagedList())
}
}
refreshStateはリフレッシュ状態のLiveDataを表し、pagedListはPagedListのLiveDataを表し、repositoryはPagedListの生成を担当するデータセンターを表します。refresh メソッドは、pagedList の値を更新するために、ドロップダウン アクション リスナーで呼び出されます。
次に、アクティビティーのレイアウトからUIを定義します。
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout xmlns:android="http://..com/apk/res/android"
android:id="@+id/swipeRefreshLayout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView xmlns:app="http://..com/apk/res-auto"
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scrollbars="vertical"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://..com/apk/res/android"
android:orientation="horizontal" android:layout_width="match_parent"
android:layout_height="70dp"
android:gravity="center"
>
<TextView
android:id="@+id/title"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:textSize="20sp"
/>
<TextView
android:id="@+id/summary"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="2"
android:textSize="15sp"
/>
</LinearLayout>
次に、アクティビティ内のUIの初期化とLiveDataのリスニングです。
class PagingDemoActivity: AppCompatActivity() {
companion object {
class ListViewHolder(itemView: View): RecyclerView.ViewHolder(itemView) {
val title = itemView.findViewById<TextView>(R.id.title)
val summary = itemView.findViewById<TextView>(R.id.summary)
}
val diff = object: DiffUtil.ItemCallback<Item>() {
override fun areItemsTheSame(oldItem: Item, newItem: Item): Boolean {
return oldItem == newItem
}
override fun areContentsTheSame(oldItem: Item, newItem: Item): Boolean {
return oldItem.id == newItem.id
}
}
class ListAdapter: PagedListAdapter<Item, ListViewHolder>(diff) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ListViewHolder {
val viewHolder = ListViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.item_demo_paging, parent, false))
return viewHolder
}
override fun onBindViewHolder(holder: ListViewHolder, position: Int) {
holder.title.text = currentList?.get(position)?.title
holder.summary.text = currentList?.get(position)?.summary
getItem(position)
}
}
}
lateinit var viewModel: ListViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_demo_paging)
viewModel = ViewModelProvider(this, object : ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
return ListViewModel() as T
}
})[ListViewModel::class.java]
val adapter = ListAdapter()
viewModel.pagedList.observe(this, Observer {
adapter.submitList(it)
})
viewModel.refreshState.observe(this, Observer {
when(it) {
ListViewModel.STATE_REFRESHING -> swipeRefreshLayout.isRefreshing = true
ListViewModel.STATE_REFRESHED -> swipeRefreshLayout.isRefreshing = false
ListViewModel.STATE_LOADING -> Toast.makeText(this, "loading", Toast.LENGTH_SHORT).show()
ListViewModel.STATE_LOADED -> Toast.makeText(this, "load success", Toast.LENGTH_SHORT).show()
}
})
recyclerView.adapter = adapter
swipeRefreshLayout.setOnRefreshListener {
viewModel.refresh()
}
}
}
class ListDataRepository(val refreshState: MutableLiveData<Int>) {
companion object {
val mainHandler = Handler(Looper.getMainLooper())
}
val dataSourceFactory = ListDataSourceFactory(refreshState)
val config = PagedList.Config.Builder()
.setEnablePlaceholders(true)
.setPrefetchDistance(3)
.setInitialLoadSizeHint(30)
.setPageSize(10)
.build()
fun getPagedList(): PagedList<Item> {
return PagedList.Builder<Int, Item>(dataSourceFactory.create(), config)
.setFetchExecutor {
GlobalScope.launch { it.run() }
}.setNotifyExecutor {
mainHandler.post(it)
}.build()
}
}
class ListDataSourceFactory(val refreshState: MutableLiveData<Int>): DataSource.Factory<Int, Item>() {
override fun create(): DataSource<Int, Item> {
return ListDataSource(refreshState)
}
}
/**
*
*/
class ListDataSource(val refreshState: MutableLiveData<Int>): ItemKeyedDataSource<Int, Item>() {
companion object {
var idCreator = 0
fun getId(): Int {
return idCreator++
}
fun createList(params: LoadInitialParams<Int>): ArrayList<Item> {
val size = params.requestedLoadSize
val list = ArrayList<Item>()
for (i in 0..size) {
val id = getId()
list.add(Item(id, "title-$id", "summary-$id"))
}
return list
}
}
fun createList(params: LoadParams<Int>): ArrayList<Item> {
val size = params.requestedLoadSize
val list = ArrayList<Item>()
for (i in 0..size) {
val id = getId()
list.add(Item(id, "title-$id", "summary-$id"))
}
return list
}
override fun loadInitial(params: LoadInitialParams<Int>, callback: LoadInitialCallback<Item>) {
refreshState.postValue(ListViewModel.STATE_REFRESHING)
GlobalScope.launch {
delay(1000L)
callback.onResult(createList(params))
refreshState.postValue(ListViewModel.STATE_REFRESHED)
}
}
override fun loadAfter(params: LoadParams<Int>, callback: LoadCallback<Item>) {
refreshState.postValue(ListViewModel.STATE_LOADING)
GlobalScope.launch {
delay(500L)
callback.onResult(createList(params))
refreshState.postValue(ListViewModel.STATE_LOADED)
}
}
override fun loadBefore(params: LoadParams<Int>, callback: LoadCallback<Item>) {
}
override fun getKey(item: Item): Int {
return item.id
}
}
ここではidジェネレータ上のデモの簡素化のために、タイトルと要約もid生成に基づいています。主なものは、loadInitial loadAfter 2つのメソッドは、要求の解決に時間がかかるシミュレートするために、ここではコンコーダンスとそれぞれ1000ミリ秒と500ミリ秒遅延を使用しています。をアクティビティに通知して、SwipeRefreshLayoutのドロップダウン・アニメーションをトリガーし、さらにロードするときにトーストをポップアップし、リクエスト終了後にコールバックを使用してデータをアダプタに戻してリストを更新し、同時にrefreshStateの値をSTATE_REFRESHEDとSTATE_LOADED に変更してアクティビティに通知し、ドロップダウンのアニメーションを停止してロードモアをロードし、ロードモア成功のトーストプロンプトをポップアップします。DataSource も getKey メソッドを実装していますが、これはここで ItemKeyDataSource を使用しているためで、ItemKeyDataSource のこれは、ItemKeyDataSourceがここで使用されているためで、ItemKeyDataSourceは、loadmoreのためにスライドアップしている場合は、loadAfterは最後のアイテムの次のキーをロードし、loadBeforeをトリガするためにプルダウンしている場合は、最初のアイテムの前のキーをロードすることを意味するので、各アイテムがキーを持っていることを確認するためにgetKeyを実装する必要があります。
最終的な効果としては、一番上までスクロールダウンするとローディングアニメーションが発生し、1000ms後にリストが更新されてローディングアニメーションが消えます。一番下までスライドアップすると、一番下に到達する前にloadmoreが起動し、"loading "トーストがポップアップし、500ms後にリストが長くなっていることがわかり、"load success "トーストがポップアップします。無限のリスト
要求失敗時のページング
ドロップダウンのリフレッシュのリクエストが失敗した場合、ドロップダウンのリフレッシュはユーザによって手動でトリガされるため、コールバックを直接破棄することができます。ユーザが再度リフレッシュするためにドロップダウンすると、loadInitialメソッドが再度トリガされ、コールバックオブジェクトが再生成されます。
しかし、loadmoreでは、自動的にloadmoreをトリガするのと同じようなもので、底部からの距離がn個の項目があるときにloadmoreをトリガし、その後、ユーザがちょうど底部にスライドした場合、下にこれ以上の項目がない、スライドプロセスが再び自動的にloadmoreをトリガすることはできません、新しいコールバックを渡すloadAfterを呼び出すことはできません。この場合、リクエストに失敗したときだけコールバックを保存し、RecyclerViewのスライドをリッスンし、スライドリスナーにデータをリクエストし、リクエストが成功したら、保存したコールバックを使用してonResultメソッドを実行し、データをアダプタに戻してリストを更新し、さらにデータをロードすることで、ユーザは再び新しいアイテムを取得することができます。こうすることで、ユーザは再び無限のリストを取得できるようになります。
具体的なコードは掲載しませんが、ここで保存するとメモリリークにつながります。コールバックは DataSource と PagedList オブジェクトを保持しているので、コールバックオブジェクトを保存した後、PagedList が更新されるドロップダウン更新があった場合、最後の PagedList を解放することができません。あなたがWeakReferenceを使用する場合は、コールバックがすぐにリリースされますので、WeakReferenceを使用して達成することはできませんので、理想的な実装では、PagedListの更新にする必要がありますnullにコールバックを保存しますので、この時間と新しいリストを初期化し、ユーザーのフィードを切断につながることはありません。