kako.dev

開発、自作アプリのこと

MVVMでYoutubePlayerアプリを作る

こんなサンプルアプリを作りました。 YoutubeDataAPIでYoutube動画を検索・再生できる簡単なアプリです。

YoutubePlayerスクリーンショット
スクリーンショット

ソースコードはこちら。

github.com

雑なところ色々あるのですが、、、そこは追々直していきます。💦

使ったライブラリ

API,非同期通信系

  • Retrofit2
  • rxJava2
  • Moshi
  • OKHttp3

この辺はおなじみのやつって感じですね。 いいかげんCoroutine覚えなければなーとは思ってます。

  • Timber(ログ)
  • Glide(画像)
  • Koin(DI)
  • Epoxy(RecyclerView)
  • ExoPlayer(動画プレイヤー)
  • android-youtubeExtractor
    • Youtube動画をExoPlayerで再生できる形式に変換するために用います

アーキテクチャ

そんなにしっかり分けてもないんですが、MVVMにしています。(しているつもり)

1画面のみのあっさりしたサンプルアプリなので、Fragmentは使ってません。

View(Activity)

UI周りの処理を担当。

Model

データ周り。

ViewModel

ActivityとModelをつなぐ。 RepositoryやLiveDataはここで。

実装

サービスの生成

interface YoutubeApiService {

    @GET("youtube/v3/search")
    fun getYoutubeData(
        @Query("key") key: String,
        @Query("type") type: String,
        @Query("part") part: String,
        @Query("q") q: String,
        @Query("maxResults") maxResults: Int
    ): Single<YoutubeDataResponse>

    companion object : KoinComponent {

        fun create(): YoutubeApiService {
            val baseUrl = "https://www.googleapis.com/"

            val okhttpClient = OkHttpClient.Builder()
                .build()

            val retrofit = Retrofit.Builder()
                .addConverterFactory(
                    MoshiConverterFactory.create(
                        Moshi.Builder()
                            .add(KotlinJsonAdapterFactory())
                            .build()
                    ))
                .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
                .baseUrl(baseUrl)
                .client(okhttpClient)
                .build()

            return retrofit.create(YoutubeApiService::class.java)
        }
    }
}

interfaceとして定義します。このYoutubeApiServiceをRepository内で生成してViewModelにはRepositoryを渡します。

ViewModel

repositoryはKoinで注入しています。

class MainViewModel : ViewModel(), KoinComponent {
    val repository: YoutubeApiRepository by inject()
    val disposable: CompositeDisposable = CompositeDisposable()
    val jsonKeys: String = BuildConfig.API_KEY
    val youtubeDataResponse: MutableLiveData<YoutubeDataResponse> = MutableLiveData()

    fun getYoutubeData(q: String) {
        disposable.addAll(
            repository.getYoutubeData(jsonKeys, q).subscribeOn(Schedulers.io()).subscribe({
                youtubeDataResponse.postValue(it)
            }, {
                Timber.e(it)
            })
        )
    }

    override fun onCleared() {
        super.onCleared()
        disposable.clear()
    }
}

Activity

もうちょっとスッキリさせたい感はするんですが、、UI周りをメインに実装します。

class MainActivity : AppCompatActivity(), MainController.ClickLister {
    val youtubeLink: String = "http://youtube.com/watch?v="
    var youTubeVideoID: String = ""
    var parseUrl: String = ""
    private val appName: String = "youtubeplayer"
    private var playerManager: PlayerManager? = null

    val viewModel: MainViewModel by viewModel()
    lateinit var controller: MainController
    lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
        binding.lifecycleOwner = this

        observeItem()

        controller = MainController(this)
        recycle_view.adapter = controller.adapter
        recycle_view.layoutManager = LinearLayoutManager(this, RecyclerView.VERTICAL, false)

        searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
            override fun onQueryTextSubmit(query: String?): Boolean {
                viewModel.getYoutubeData(query!!)
                return true
            }

            override fun onQueryTextChange(newText: String?): Boolean {
                return false
            }
        })
    }

    private fun observeItem() {
        viewModel.youtubeDataResponse.observeForever {
            controller.setData(it.items)
        }
    }

    fun dlYoutube() = object : YouTubeExtractor(this) {
        override fun onExtractionComplete(ytFiles: SparseArray<YtFile>, vMeta: VideoMeta) {
            if (ytFiles != null) {
                parseUrl = ytFiles.get(ytFiles.keyAt(0)).url
                setPlayer()
            }
        }
    }.extract(youtubeLink + youTubeVideoID, true, true)

    fun setPlayer() {
        if (playerManager != null) playerManager?.release()
        playerManager = PlayerManager(this, appName)
        playerManager?.playserNewInstatnce()
        playerManager?.buildMediaSource(Uri.parse(parseUrl), null)
        playerManager?.playerView = playerView
        playerManager?.execute()
    }

    override fun onItemClickListener(item: Item) {
        if (youTubeVideoID == item.id.videoId) return

        youTubeVideoID = item.id.videoId
        dlYoutube()
    }

    override fun onStop() {
        super.onStop()
        playerManager?.stop()
    }

    override fun onResume() {
        super.onResume()
        playerManager?.replay()
    }
}

他には、ExoPlayerのためのPlayerManagerなども実装しているのですが、ExoPlayerについては別記事にしてまとめようと思います。

機会があれば、今度はこれにpagingを実装したり、マルチモジュール化したりして遊んでみたいなと思ってます。

その時はまたブログにすると思います〜〜 ( ´ ▽ ` )ノシ