Jetpack ComposeとRecyclerView(やGroupie)のパフォーマンス比較

2022/06/09に公開

概要

FlutterでもSwiftUIでもそうですが、宣言的UIなアプリを作る際は、ちゃんとパフォーマンスを意識して作らないと効率の悪い状態になります。

では、公式で書いてある通りに実装したとして、旧来の方式(RecyclerView)と比べてもJetpack Composeはそもそもパフォーマンス的に問題ないのかを検証したいと思います。
この検証は、単純なデザインでの検証なので複雑なデザインの場合は結果が変わるかもしれないのでご了承ください。

以下の実装をGPUレンダリングのプロファイル作成(リリースビルド)でそれぞれ確認したいと思います。

  • RecyclerViewだけを使った場合
  • Jetpack Composeの LazyColumn を使った場合
  • RecyclerViewのHolderで ComposeView を使った場合
  • Groupieを使った場合
  • GroupieのBindableItemComposeView を使った場合

段階的にJetpack Composeに移行したい場合に使われるComposeViewがどうなるかが、個人的に一番気になるところです。

検証ソースは以下にあります
https://github.com/ko2ic/spike-compose-performance

結果

結論、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
RecyclerView Jetpack Compose RecyclerViewとComposeView
GroupieGroupie GroupieとComposeViewGroupieとComposeView

5回スクロールさせた場合

通常利用時に近いと思います。下に少しづつスクロールさせる感じの結果です。

RecyclerView Jetpack Compose RecyclerViewとComposeView
RecyclerView Jetpack Compose RecyclerViewとComposeView
GroupieGroupie GroupieとComposeViewGroupieと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
    )
  }
}

RecyclerViewのHolderで ComposeView を使った場合

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()ComposeViewdisposeComposition() を呼び出しています。これで、適切に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)
    }
  }
}

GroupieのBindableItemComposeView を使った場合

Groupieの実装では、GroupieAdapterBindableItemの実装クラスを設定しますが、その実装クラスのインスタンスに一覧アイテムのコンポーザブル関数(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)
  }
}

Jetpack Composeの LazyColumn を使った場合

特質することはなく、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 の Zenn

Discussion