Jetpack ComposeとRecyclerView(やGroupie)のパフォーマンス比較
概要
FlutterでもSwiftUIでもそうですが、宣言的UIなアプリを作る際は、ちゃんとパフォーマンスを意識して作らないと効率の悪い状態になります。
では、公式で書いてある通りに実装したとして、旧来の方式(RecyclerView)と比べてもJetpack Composeはそもそもパフォーマンス的に問題ないのかを検証したいと思います。
この検証は、単純なデザインでの検証なので複雑なデザインの場合は結果が変わるかもしれないのでご了承ください。
以下の実装をGPUレンダリングのプロファイル作成(リリースビルド)でそれぞれ確認したいと思います。
- RecyclerViewだけを使った場合
- Jetpack Composeの
LazyColumn
を使った場合 - RecyclerViewのHolderで
ComposeView
を使った場合 - Groupieを使った場合
- Groupieの
BindableItem
でComposeView
を使った場合
段階的にJetpack Composeに移行したい場合に使われるComposeView
がどうなるかが、個人的に一番気になるところです。
検証ソースは以下にあります
結果
結論、RecyclerViewやGroupieをそのまま使った場合は(当然ですが)効率がとてもよかったです。
一方、それに比べて Jetpack Composeとのハイブリッドにした場合は、RecyclerView、Groupieともに単体で利用したときに比べるとパフォーマンスが悪い です。
Jetpack Composeだけを使った場合は、RecyclerView単体での利用時とほとんど変わりませんでした。ただし、Jetpack Composeだけの場合は、少しの力だけでどんどんスクロール していきました。
RecyclerView単体の場合は、おもいっきり下スクロールさせて120行目ぐらいまで移動しましたが、Jetpack Composeだけの場合は軽くスクロールさせて120行目ぐらいまで移動しました。
同じ力でやるとJetpack Composeの場合は、どんどんスクロールするので結果、たくさんのGPUを使ってました。パフォーマンスがいいと言えるのかもしれません。
1度だけスクロールさせた場合
1度のスクロールの惰性の結果です。
見方は公式を見て欲しいのですが、棒グラフが高い方がGPUを多く利用していてパフォーマンスが良くないと捉えるといいでしょう。
明らかにハイブリッドの場合がパフォーマンスが悪いです。
RecyclerView | Jetpack Compose | RecyclerViewとComposeView |
---|---|---|
Groupie | GroupieとComposeView |
5回スクロールさせた場合
通常利用時に近いと思います。下に少しづつスクロールさせる感じの結果です。
RecyclerView | Jetpack Compose | RecyclerViewとComposeView |
---|---|---|
Groupie | GroupieとComposeView |
ソース
実装方法に問題がある可能性もあるので、おかしい部分があれば指摘していただければ幸いです。
まずは、一覧のアイテムのコンポーザブル関数を用意しておきます。このクラスをComposeView
に設定します。
@Composable
fun ComposeListItemView(item: Item) {
Column(Modifier.padding(8.dp)) {
Text(
text = item.title,
style = TextStyle(fontSize = 24.sp, color = Purple200),
textAlign = TextAlign.Center
)
Text(
text = item.description,
style = TextStyle(fontSize = 16.sp, color = Color.Black),
textAlign = TextAlign.Center
)
}
}
ComposeView
を使った場合
RecyclerViewのHolderで Fragmentは特質すべきことはないです。
class RecyclerViewWithComposeFragment : Fragment(R.layout.fragment_recycler_view) {
private lateinit var viewModel: MyViewModel
private var _binding: FragmentRecyclerViewBinding? = null
private val binding get() = _binding!!
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel = ViewModelProvider(this)[MyViewModel::class.java]
_binding = DataBindingUtil.bind(view)
binding.viewModel = viewModel
binding.lifecycleOwner = viewLifecycleOwner
binding.recyclerView.apply {
layoutManager = LinearLayoutManager(this@RecyclerViewWithComposeFragment.context)
adapter = RecyclerViewWithComposeAdapter(viewModel.list)
}
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
ViewHolderにComposeView
を渡しています。
アイテムが画面から消えるたびにCompositionを破棄したいためにonViewRecycled()
で ComposeView
の disposeComposition()
を呼び出しています。これで、適切にViewHolderのインスタンスを再利用しているはずです。
また、デフォルトのComposeは、ビューがウィンドウからデタッチされると、Composition を破棄しますが、Fragmentのlifecyleに沿って削除したいためにViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed
に変更しています。
onBindViewHolder()
で ComposeView
に 上記で作成した一覧のアイテムのコンポーザブル関数(ComposeListItemView
)をバインドさせています。
class RecyclerViewWithComposeAdapter(private val listItems: List<Item>) :
RecyclerView.Adapter<ComposeItemViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ComposeItemViewHolder {
return ComposeItemViewHolder(ComposeView(parent.context))
}
override fun onBindViewHolder(holder: ComposeItemViewHolder, position: Int) {
holder.bindView(listItems[position])
}
override fun onViewRecycled(holder: ComposeItemViewHolder) {
super.onViewRecycled(holder)
holder.composeView.disposeComposition()
}
override fun getItemCount(): Int {
return listItems.size
}
}
class ComposeItemViewHolder(
val composeView: ComposeView
) : RecyclerView.ViewHolder(composeView) {
init {
composeView.setViewCompositionStrategy(
ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed
)
}
fun bindView(content: Item) {
composeView.setContent {
ComposeListItemView(content)
}
}
}
BindableItem
でComposeView
を使った場合
GroupieのGroupieの実装では、GroupieAdapter
に BindableItem
の実装クラスを設定しますが、その実装クラスのインスタンスに一覧アイテムのコンポーザブル関数(ComposeListItemView
)を渡しています。
class GroupieWithComposeFragment : Fragment(R.layout.fragment_recycler_view) {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
・・・
binding.recyclerView.apply {
layoutManager = LinearLayoutManager(this@GroupieWithComposeFragment.context)
val adapter = GroupieAdapter()
// 本当はobserveで設定すべき
val items = viewModel.list.map {
GroupieItemWithCompose { ComposeListItemView(it) }
}
adapter.addAll(items)
this.adapter = adapter
}
}
・・・
}
一覧アイテムのコンポーザブル関数を Groupieのbind()
とunbind()
メソッドで、適切なタイミングで呼ばれるようにしています。RecylerViewのときと同じようにViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed
にし、disposeComposition()
を呼び出しています。
class GroupieItemWithCompose(
private val composable: @Composable () -> Unit
) : BindableItem<ListItemViewComposeBinding>() {
override fun bind(viewBinding: ListItemViewComposeBinding, position: Int) {
viewBinding.composeView.setContent(composable)
}
override fun getLayout(): Int = R.layout.list_item_view_compose
override fun initializeViewBinding(view: View): ListItemViewComposeBinding {
return ListItemViewComposeBinding.bind(view).also {
it.composeView.setViewCompositionStrategy(
ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed
)
}
}
override fun unbind(viewHolder: com.xwray.groupie.viewbinding.GroupieViewHolder<ListItemViewComposeBinding>) {
viewHolder.binding.composeView.disposeComposition()
super.unbind(viewHolder)
}
}
LazyColumn
を使った場合
Jetpack Composeの 特質することはなく、LazyColumn
で一覧表示しています。
class ComposeActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
val viewModel: MyViewModel = viewModel()
MyLazyColumn(
list = viewModel.list
)
}
}
}
@Composable
fun MyLazyColumn(
modifier: Modifier = Modifier,
list: List<Item>,
) {
LazyColumn(
modifier = modifier,
) {
items(list) {
ComposeListItemView(item = it)
Spacer(
modifier = Modifier
.fillMaxWidth()
)
}
}
}
ソーシャル経済メディア NewsPicks メンバーの発信を集約しています。公式テックブログはこちら→ tech.uzabase.com/archive/category/NewsPicks
Discussion