仕事を終え、ゆったりと座ってTV番組を視聴しているときに、読み込み中のアイコンをただ見つめながらアプリが起動するのをじっと待つというのは、開発者にとってもユーザーにとっても不本意な時間です。ユーザーにとってアプリのパフォーマンスはとても重要です。読み込み待ちや遅延が発生することなく、シームレスに動作する必要があります。スムーズで反応の良いストリーミング体験を提供するために、Fire TV対応アプリのパフォーマンスでよくある3つの問題と、その解決方法について考えていきましょう。
アプリの起動時間はユーザーとアプリとの最初のインタラクションであり、全体的な印象が決まります。起動が遅かったり、反応がなかったりすると、コンテンツを利用してもらう以前にユーザーが不満を感じ、アプリを離れる原因となってしまいます。アプリ起動時のパフォーマンスを把握して最適化することは、第一印象を良くし、ユーザーのリテンションを高める上で大事な要素です。
TTIDとTTFDについて
起動時のパフォーマンスを効果的に測定・改善するには、以下の2つの重要な指標を理解することが大切です。
TTIDの取得
アプリのTTIDを確認するには、Logcatで「Displayed」の値を探します。
TTFDの測定
TTFDを測定するには、reportFullyDrawn()
メソッドを使用します。
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
HelloWorldTVTheme {
// Your app content
}
}
// Call this when your app is fully loaded and interactive
reportFullyDrawn()
}
}
Jetpack Composeを使用したTTIDとTTFDの測定
Fire TV対応アプリでJetpack Composeを使用している場合は、固有のAPIを使用して、前の指標を正確に測定できます。
@Composable
fun HomeScreen() {
ReportDrawn()
// Your UI content
}
@Composable
fun ContentList(items: List<Item>) {
ReportDrawnWhen(items.size > 0)
LazyColumn {
items(items) { item ->
ItemRow(item)
}
}
}
@Composable
fun DataDependentScreen(viewModel: MyViewModel) {
ReportDrawnAfter {
viewModel.loadInitialData()
}
// UI that depends on the loaded data
}
上記やその他の測定値をさらに詳しく調べるには、オープンソースのFireOSパフォーマンステストツール(英文のみ)を活用して、コンピューターに接続された実際のデバイス上でアプリを分析し、最適化プロセスの包括的なログを生成することもできます。
TTIDとTTFDを短縮する方法
TTIDやTTFDの数値が大きすぎる場合(こちらに記載されている値が大きい場合など)は、アプリの起動パフォーマンスが、以下のよくある問題のいずれかの影響を受けている可能性があります。
ビデオ再生は、あらゆるストリーミングアプリの核となる機能です。パフォーマンスの問題やユーザーエクスペリエンスの低下は、多くの場合、プレーヤーインスタンスの不適切な管理が原因で発生します。
こちらのコード例には、ビデオ再生管理に関するよくある間違いがいくつか含まれています。どこに問題があるか、探してみてください。
class VideoPlayerActivity : ComponentActivity() {
private lateinit var player: ExoPlayer
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
player = ExoPlayer.Builder(this).build()
val mediaItem = MediaItem.fromUri("https://example.com/video.mp4")
player.setMediaItem(mediaItem)
player.prepare()
setContent {
VideoPlayerComposable(player)
}
}
override fun onDestroy() {
super.onDestroy()
player.release()
}
}
@Composable
fun VideoPlayerComposable(player: ExoPlayer) {
AndroidView({ context ->
PlayerView(context).apply {
this.player = player
}
})
}
問題は見つかりましたか? 問題となる点は以下のとおりです。
onCreate
で実行されているため、ほかのUIの初期化が遅延する可能性がある。
問題の解決方法
class VideoPlayerViewModel : ViewModel() {
private val _playerState = MutableStateFlow<PlayerState>(PlayerState.Initializing)
val playerState: StateFlow<PlayerState> = _playerState
private var player: ExoPlayer? = null
fun initializePlayer(context: Context) {
// Ensure this is called on the main thread
player = ExoPlayer.Builder(context).build().apply {
val mediaItem = MediaItem.fromUri("https://example.com/video.mp4")
setMediaItem(mediaItem)
// Prepare asynchronously
prepare()
addListener(object : Player.Listener {
override fun onPlaybackStateChanged(state: Int) {
when (state) {
Player.STATE_READY -> _playerState.value = PlayerState.Ready(this@apply)
Player.STATE_BUFFERING -> _playerState.value = PlayerState.Buffering
Player.STATE_ENDED -> _playerState.value = PlayerState.Ended
Player.STATE_IDLE -> _playerState.value = PlayerState.Idle
}
}
override fun onPlayerError(error: PlaybackException) {
_playerState.value = PlayerState.Error(error.message ?: "Unknown error")
}
})
}
}
fun releasePlayer() {
player?.release()
player = null
_playerState.value = PlayerState.Initializing
}
override fun onCleared() {
releasePlayer()
super.onCleared()
}
}
sealed class PlayerState {
object Initializing : PlayerState()
object Buffering : PlayerState()
class Ready(val player: ExoPlayer) : PlayerState()
object Ended : PlayerState()
object Idle : PlayerState()
class Error(val message: String) : PlayerState()
}
@Composable
fun VideoPlayerScreen(viewModel: VideoPlayerViewModel = viewModel()) {
val playerState by viewModel.playerState.collectAsState()
val context = LocalContext.current
LaunchedEffect(Unit) {
viewModel.initializePlayer(context)
}
DisposableEffect(Unit) {
onDispose {
viewModel.releasePlayer()
}
}
when (val state = playerState) {
is PlayerState.Initializing, PlayerState.Buffering -> LoadingIndicator()
is PlayerState.Ready -> VideoPlayer(state.player)
is PlayerState.Error -> ErrorMessage(state.message)
PlayerState.Ended -> PlaybackEndedUI()
PlayerState.Idle -> IdleStateUI()
}
}
@Composable
fun VideoPlayer(player: ExoPlayer) {
AndroidView(
factory = { context ->
PlayerView(context).apply {
this.player = player
}
},
modifier = Modifier.fillMaxSize()
)
}
ViewModelおよびComposeのLaunchedEffect
とDisposableEffect
を使用して、ビデオプレーヤーの初期化とクリーンアップが適切に処理されるようにしました。
LaunchedEffect(Unit) {
viewModel.initializePlayer(context)
}
DisposableEffect(Unit) {
onDispose {
viewModel.releasePlayer()
}
}
さらに、読み込み中を表すインジケーターとエラーメッセージを追加してユーザーへのフィードバックを改善し、再生エラーをカバーしてクラッシュを回避できるようにしました。
when (val state = playerState) {
is PlayerState.Initializing, PlayerState.Buffering -> LoadingIndicator()
is PlayerState.Ready -> VideoPlayer(state.player)
is PlayerState.Error -> ErrorMessage(state.message)
PlayerState.Ended -> PlaybackEndedUI()
PlayerState.Idle -> IdleStateUI()
}
Fire TV対応アプリでは、映画のポスター、番組のサムネイル、俳優の顔写真など、同じ画面上に多くの画像が表示されることがあります。画像の読み込み方法によっては、アプリのパフォーマンスに多大な影響を及ぼす可能性もあります。画面上に構成可能な画像を描画する場合は、以下のような対策を講じることをお勧めします。
@Composable
fun UnoptimizedThumbnail(url: String) {
AsyncImage(
model = url,
contentDescription = null,
modifier = Modifier.size(200.dp)
)
}
こういったコンポーネントは、主に以下の3つの問題を引き起こす可能性があります。
これは、コード内に以下のようなパフォーマンス上の落とし穴がいくつか隠されているためです。
解決策:
これらの問題は、画像読み込みライブラリ(Coilなど)を使用して対処できます。
@Composable
fun OptimizedThumbnail(url: String) {
AsyncImage(
model = ImageRequest.Builder(LocalContext.current)
.data(url)
.size(width = 200, height = 200)
.crossfade(true)
.build(),
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier.size(200.dp)
)
}
改善内容について詳しく見ていきましょう。
1. サイズの指定:
.size(width = 200, height = 200)
で、(理想的にはサーバー上またはダウンロード中に)画像のサイズを200x200ピクセルに変更するようCoilに指示しています。これにより、転送されるデータ量と画像の保存に使用されるメモリが大幅に削減されます。2. クロスフェード効果:
.crossfade(true)
で、画像の読み込み時にスムーズな遷移効果を追加しています。これにより、体感的なパフォーマンスが向上し、画像の読み込みがよりシームレスに感じられるようになります。3. コンテンツのスケーリング:
contentScale = ContentScale.Crop
で、画像がゆがみなく領域全体に表示されるようにしています。これにより、画像の縦横比が異なっていても一貫した外観を提供できるようになります。まとめ
パフォーマンスが高いFire TV対応アプリの基盤は、こうした最適化によって形成されます。上記のような手法を実装することで、競争の激しいTV向けアプリの世界で差別化されたストリーミング体験を提供し、ユーザーの満足度を高められるようになります。
ただし、最適なパフォーマンスの追求に終わりはありません。さらなる最適化を目指して、今後の記事でも引き続き情報をお届けしてまいります。
どうぞご期待ください。