Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Compose Multiplatform for iOSで音声再生しようぜ!!

ねも
September 26, 2023
240

Compose Multiplatform for iOSで音声再生しようぜ!!

スライドに出てくるデモアプリの動画はこちらですhttps://drive.google.com/file/d/1eJpXPjDh2SMFBrA_COJMRD5LoShBxOOk/view?usp=drive_link

potatotips #84 iOS/Android開発Tips共有会での登壇資料

ねも

September 26, 2023
Tweet

Transcript

  1. 🙆話すこと • アーキテクチャについて • 音声再生周りのAPI紹介 • 音声再生機能をどう作るか • サンプルを作る時に使ったライブラリ •

    UI部分のコード紹介 🙅 話さないこと • Kotlin Multiplatform, Compose Multiplatformとは何か? • Compose for iOSとは何か?
  2. Compose Multiplatform Kotlin Multiplatform とは何か → 公式ドキュメント Compose Multiplatform for

    iOSとは何か → Compose で Android/iOS アプリを作る - Speaker Deck → Compose for iOS for ZOZOTOWN - Speaker Deck
  3. 🎵 音声APIはどこでどう扱う? UI層と音声APIを疎結合に保ちたい - 今回は動画ではなく、音声再生が目的 - Compose Multiplatform for iOSはまだalphaであり変更が多いので、音声APIと密結合になるのは

    まずい → Viewで音声APIのインスタンスを作り再生、といったことはしたくない UiStateに音声APIの情報は載せない - ComposeのRecompositionの仕組みを考えると、UiStateのプロパティにはequalsが実装されてい る物のみを持たせたい 音声APIの都合はDomainには持ち込みたくない - 音声APIは具体的で変更頻度がかなり高いので、Domain層にも持ち込みたくない
  4. Compose Multiplatform Reducer Repository Processor emit action notify state dispatch

    intent notify event AudioService (音声API) ここで扱う
  5. actual class AudioServiceImpl expect class AudioServiceImpl : AudioService actual class

    AudioServiceImpl domain ui androidMain commonMain iosMain モジュール構成図(一部分) audio Compose Multiplatform Audio AudioService
  6. 音声についてのドメインモデル data class Audio( val id: Int, val title: String,

    val authorName: String, val authorImageUrl: String?, val url: String, )
  7. 右のように音声を扱うドメインサービス • 再生する • 一時停止する • etc… 右のようなinterface用意し • UI層はinterfaceに依存する

    • iOSとAndroid用にそれぞれの AudioServiceの実装を用意する という方針とする interface AudioService { val audio: Audio? suspend fun setAudio(audio: Audio) suspend fun play() suspend fun seek() suspend fun pause() suspend fun resume() suspend fun complete() }
  8. actual class AudioServiceImpl( private val context: Context, override val audio:

    Audio, ) : AudioService { override val id: String = UUID.randomUUID().toString() private var _exoPlayer: ExoPlayer? = null override fun play() { _exoPlayer = createExoPlayer(context, audio) _exoPlayer?.playWhenReady = true } override fun seek() {/* 省略 */} override fun pause() {/* 省略 */} override fun resume() {/* 省略 */} override fun complete() {/* 省略 */} private fun createExoPlayer(context: Context, audio: Audio): ExoPlayer {/* 省略 */} } ⭐ Multiplatform Library Module内の androidMainパッケージ ⭐ androidMainなので、Javaや Androidの機能が使える (java.util.UUID, ExoPlayer 等) ⭐ ExoPlayerのインスタンス作成の ためにContextを使用する
  9. @OptIn(ExperimentalForeignApi::class) actual class AudioServiceImpl(override val audio: Audio) : AudioService {

    private var player: AVPlayer? = null override val id: String = NSUUID().UUIDString override fun play() { val url: NSURL = NSURL.URLWithString(URLString = audio.url)!! val playerItem = AVPlayerItem(url) player = AVPlayer(playerItem = playerItem) player?.play() } override fun pause() {/* 省略 */} override fun resume() {/* 省略 */} override fun seek() {/* 省略 */} override fun complete() {/* 省略 */} } ⭐ Multiplatform Library Module内の iosMainパッケージ ⭐ iosMainなので、iOSの機能が使 える(AVPlayer 等)
  10. actual val moduleAudio = module { single<AudioServiceFactory> { object :

    AudioServiceFactory { override fun create(audio: Audio): AudioService { return AudioServiceImpl( context = androidContext(), audio = audio, ) } } } } actual val moduleAudio = module { single<AudioServiceFactory> { object : AudioServiceFactory { override fun create(audio: Audio): AudioService { return AudioServiceImpl( audio = audio, ) } } } } androidMain iosMain 実際の処理をactualでそれぞれの プラットフォームで書く。
  11. actual class DiHelper(private val context: Context) { actual fun initKoin()

    { startKoin { androidContext(context) modules( moduleAudio, moduleOthers, ) } } } actual class DiHelper { actual fun initKoin() { startKoin { modules( moduleAudio, moduleOthers, ) } } } androidMain iosMain あとはKoinの初期化処理を定義し て、アプリの初期化部分で実行
  12. • 画面遷移 → Voyager • リソース管理 → MOKO Resources •

    image loader → Kamel • DIライブラリ → Koin → Compose Multiplatform Wizardで選択可能なものを使用した
  13. class HomeStateMachine( private val postRepository: PostRepository, private val audioServiceFactory: AudioServiceFactory,

    ) : StateMachine<HomeIntent, HomeAction, HomeState>( builder = { // Intentを受け取ってActionに変換 // Actionを受け取ってStateを更新 } ) DSLを使用してStateMachineを作り、、
  14. class HomeScreenModel( homeStateMachine: HomeStateMachine, defaultStoreFactory: DefaultStoreFactory, ) : ScreenModel {

    val store = defaultStoreFactory.create( initialState = HomeState.Initial, processor = homeStateMachine.processor, reducer = homeStateMachine.reducer, ) } VoyagerのScreenModelを定義し、、
  15. class HomeScreen : Screen { @Composable override fun Content() {

    val screenModel = getScreenModel<HomeScreenModel>() val contract = contract(screenModel.store) HomePage(contract = contract) } // 省略 HomePage Composable } VoyagerのScreenを継承したHomeScreenを作 成
  16. @Composable private fun HomePage( contract: Contract<HomeIntent, HomeAction, HomeState>, ) {

    LaunchedEffect(Unit) { contract.dispatch(HomeIntent.OnInit) } HomeContent( modifier = Modifier.fillMaxSize(), state = contract.state, onPostPlayButtonClicked = { postId -> contract.dispatch(HomeIntent.ClickPostPlayButton(postId)) }, onAudioBarButtonClicked = { contract.dispatch(HomeIntent.ClickAudioPlayButton) }, onReload = { contract.dispatch(HomeIntent.ClickErrorRetry) }, ) } HomePageはこんな感じ
  17. @Composable private fun HomePage( contract: Contract<HomeIntent, HomeAction, HomeState>, ) {

    LaunchedEffect(Unit) { contract.dispatch(HomeIntent.OnInit) } HomeContent( modifier = Modifier.fillMaxSize(), state = contract.state, onPostPlayButtonClicked = { postId -> contract.dispatch(HomeIntent.ClickPostPlayButton(postId)) }, onAudioBarButtonClicked = { contract.dispatch(HomeIntent.ClickAudioPlayButton) }, onReload = { contract.dispatch(HomeIntent.ClickErrorRetry) }, ) } HomePageはこんな感じ
  18. 参考 • Compose Multiplatform https://www.jetbrains.com/ja-jp/lp/compose-multiplatform/ • MVIに基づくStateMachineアーキテクチャ:KMMとJetpack ComposeとSwiftUIを組み合わせる https://speakerdeck.com/gmvalentino8/mviniji-dukustatemachineakitekutiya-kmmtojetpack-composetoswiftuiwozu- mihe-waseru

    • Jetpack Compose で Android/iOSアプリを作る https://speakerdeck.com/m_coder/ios-ahuriwozuo-ru • Compose for iOS for ZOZOTOWN Compose for iOS for ZOZOTOWN - Speaker Deck • Compose Multiplatform Wizard Compose Multiplatform Wizard (terrakok.github.io) • 裸の王様 by 騒音のない世界 https://noiselessworld.net/