美文网首页jetpack
Android JetPack Compose实现网络请求分页加

Android JetPack Compose实现网络请求分页加

作者: 被违规后的昵称 | 来源:发表于2021-11-24 18:53 被阅读0次
    最近几天一直在研究谷歌的JetPack Compose,给我最大的感受就是便捷,往往使用RecyclerView和Adapter需要实现的功能,包括自定义View,或者简单到一个View的自定义,代码比起Compose要多了很多。
    

    自己尝试实现了一款视频列表播放Demo,代码还有很多需要优化的地方,目前只是实现了简单的效果。

    效果图

    一、分析

    1. 网络请求与API:

    网络请求依然用retrofit, 视频列表API随便找一个即可,分页跟上次一样选用Paging3,个人感觉非常搭配Jetpack Compose

    1. 视频播放器的选择:

    播放器可以选用大名鼎鼎的 ijkplayer,我就用ExoPlayer,自己贴了个controller_view上去。

    1. 横竖屏切换:

    同一个PlayerView,全屏的时候 ,先从列表item中remove(),然后addView()给R.id.content ,竖屏反过来操作。

    二、分页与网络请求:

    1. 实例化Retrofit:
      
      object RetrofitClient {
      
          private val instance: Retrofit by lazy {
      
              val logInterceptor = HttpLoggingInterceptor()
              if (BuildConfig.DEBUG) {
              //显示日志
              logInterceptor.level = HttpLoggingInterceptor.Level.BODY
              } else {
                  logInterceptor.level = HttpLoggingInterceptor.Level.NONE
              }
      
              val okhttpClient = OkHttpClient.Builder().addInterceptor(logInterceptor)
                  .connectTimeout(5, TimeUnit.SECONDS)//设置超时时间
                  .retryOnConnectionFailure(true).build()
      
              Retrofit.Builder()
                  .client(okhttpClient)
                  .baseUrl(BASE_URL)
                  .addConverterFactory(GsonConverterFactory.create())
                  .build()
          }
      
          fun <T> createApi(clazz: Class<T>): T {
              return instance.create(clazz) as T
          }
      }
      
    1. 定义列表接口:

      interface VideoListService {
      
          @GET("api/v4/discovery/hot")
          suspend fun getVideoList(
              @Query("start") itemStart: Int = 1,
              @Query("num") pageSize: Int = 6
          ): VideoStore
      }
      
    2. Paging分页逻辑在VideoListDataSource.kt完成:

      class VideoListDataSource(private val repository: Repository) : PagingSource<Int, VideoItem>() {
      
          private val TAG = "--ExamSource"
      
          override fun getRefreshKey(state: PagingState<Int, VideoItem>): Int? {
              return null
          }
      
          override suspend fun load(params: LoadParams<Int>): LoadResult<Int, VideoItem> {
      
              return try {
                  val currentPage = params.key ?: 1
                  val pageSize = params.loadSize
      
                  // 每一页请求几条数据
                  val everyPageSize = 4
                  // 第一次初始请求,多加载一点
                  val initPageSize = 8
                  // 当前请求的起始位置,指起始下标
                  val curStartItem =
                      if (currentPage == 1) 1 else (currentPage - 2) * everyPageSize + 1 + initPageSize
      
                  val responseList = repository.getVideoList(curStartItem, pageSize = pageSize)
                      .videoList ?: emptyList<VideoItem>()
                  // 上一页页码
                  val preKey = if (currentPage == 1) null else currentPage.minus(1)
                  // 下一页页码
                  var nextKey: Int? = currentPage.plus(1)
                  Log.d(TAG, "currentPage: $currentPage")
                  Log.d(TAG, "preKey: $preKey")
                  Log.d(TAG, "nextKey: $nextKey")
                  if (responseList.isEmpty()) {
                      nextKey = null
                  }
      
                  LoadResult.Page(
                      data = responseList,
                      prevKey = preKey,
                      nextKey = nextKey
                  )
              } catch (e: Exception) {
                  e.printStackTrace()
                  LoadResult.Error(e)
              }
          }
      }
      
    3. 数据请求:Repository,

      谷歌之前推荐的架构库 官方Android应用架构库(Architecture Components)推荐将ViewModel中的网络请求数据库交互部分交给Repository来处理,而ViewModel专注于业务和UI交互,并等待Repository去拿网络数据,大部分应用不需要每次都请求新的页面数据,最好是缓存到本地。于是该架构推荐Room数据库作为本地缓存,这样是比较完美的,也就是请求完列表页面数据给Room, 页面绘制优先拿Room的数据。但是我这里没有考虑实现。

    object Repository {
    
        suspend fun getVideoList(itemStart: Int, pageSize: Int) =
            RetrofitClient.createApi(VideoListService::class.java)
                .getVideoList(itemStart, pageSize)
    }
    
    1. ViewModel拿到数据:

      这里拿到是PagingData<T> 的流,被viewModel收集,需要传入协程作用域,Paging内部会安排发送流:

       /**
           * The actual job that collects the upstream.
           */
          private val job = scope.launch(start = CoroutineStart.LAZY) {
              src.withIndex()
                  .collect {
                      mutableSharedSrc.emit(it)
                      pageController.record(it)
                  }
          }.also {
              it.invokeOnCompletion {
                  // Emit a final `null` message to the mutable shared flow.
                  // Even though, this tryEmit might technically fail, it shouldn't because we have
                  // unlimited buffer in the shared flow.
                  mutableSharedSrc.tryEmit(null)
              }
          }
      
    val videoItemList = Pager(
            config = PagingConfig(
                pageSize = 4,
                initialLoadSize = 8, // 第一次加载数量
                prefetchDistance = 2,
            )
        ) {
            VideoListDataSource(Repository)
        }.flow.cachedIn(viewModelScope)
    

    三、加载列表

    上面viewModel 我们得到Flow<PagingData<T>数据流,Compose提供了一种便捷加载LazyColumn(其实类似RecyclerView 只是用不着RecyclerAdapter)的方式:

    /**
    *从[PagingData]的[流]收集数据,将他们表现为一个[LazyPagingItems]实例。
    * [LazyPagingItems]实例可以被[items]和[itemsIndexed]方法使用
    *[LazyListScope]应该是个上下文作用域,使用它就是为了从[PagingData]的[Flow]流获取的数据能够被LazyColumn使用。大概是这个意思,总之就是方便开发者。
     *
     * @sample androidx.paging.compose.samples.PagingBackendSample
     */
    @Composable
    public fun <T : Any> Flow<PagingData<T>>.collectAsLazyPagingItems(): LazyPagingItems<T> {
        val lazyPagingItems = remember(this) { LazyPagingItems(this) }
    
        LaunchedEffect(lazyPagingItems) {
            lazyPagingItems.collectPagingData()
        }
        LaunchedEffect(lazyPagingItems) {
            lazyPagingItems.collectLoadState()
        }
    
        return lazyPagingItems
    }
    

    列表实现:

    没有什么特别的地方,但是有一点需要注意:列表随着滑动,始终对顶部可见的Item做播放,所以需要判断列表中顶部可见的项。

    LazyListState源码中有这样一个方法:

     /**
         * The index of the first item that is visible
         */
        val firstVisibleItemIndex: Int get() = scrollPosition.observableIndex
    

    ”可见的第一项的索引“ 就是第一项眼睛看到的Item索引

    /**
     * 首页列表加载 ---普通加载,没有下拉刷新,可加载下一页
     * */
    
    @Composable
    fun NormalVideoListScreen(
        viewModel: MainViewModel,
        context: Context,
    ) {
    
        val collectAsLazyPagingIDataList = viewModel.videoItemList.collectAsLazyPagingItems()
    
        // 首次加载业务逻辑
        when (collectAsLazyPagingIDataList.loadState.refresh) {
            is LoadState.NotLoading -> {
                ContentInfoList(
                    collectAsLazyPagingIDataList = collectAsLazyPagingIDataList,
                    context = context,
                    viewModel = viewModel
                )
            }
            is LoadState.Error -> ErrorPage() { collectAsLazyPagingIDataList.refresh() }
            is LoadState.Loading -> LoadingPageUI()
        }
    }
    
    @ExperimentalCoilApi
    @Composable
    fun ContentInfoList(
        context: Context,
        collectAsLazyPagingIDataList: LazyPagingItems<VideoItem>,
        viewModel: MainViewModel
    ) {
        val lazyListState = rememberLazyListState()
        val focusIndex by derivedStateOf { lazyListState.firstVisibleItemIndex }
    
        LazyColumn(
            state = lazyListState
        ) {
            itemsIndexed(collectAsLazyPagingIDataList) { index, videoItem ->
                // 传入列表卡片Item
                VideoCardItem(
                    videoItem = videoItem!!,
                    isFocused = index == focusIndex,
                    onClick = { Toast.makeText(context, "ccc", Toast.LENGTH_SHORT).show() },
                    index = index,
                    viewModel = viewModel
                )
            }
    
            // 加载下一页业务逻辑
            when (collectAsLazyPagingIDataList.loadState.append) {
                is LoadState.NotLoading -> {
                    itemsIndexed(collectAsLazyPagingIDataList) { index, videoItem ->
                        VideoCardItem(
                            videoItem = videoItem!!,
                            isFocused = index == focusIndex,
                            onClick = { Toast.makeText(context, "ccc", Toast.LENGTH_SHORT).show() },
                            index = index,
                            viewModel = viewModel
                        )
                    }
                }
                is LoadState.Error -> item {
                    NextPageLoadError {
                        collectAsLazyPagingIDataList.retry()
                    }
                }
                LoadState.Loading -> item {
                    LoadingPageUI()
                }
            }
        }
    }
    
    
    /**
     * 页面加载失败重试
     * */
    @Composable
    fun ErrorPage(onclick: () -> Unit = {}) {
        Column(
            modifier = Modifier.fillMaxSize(),
            verticalArrangement = Arrangement.Center,
            horizontalAlignment = Alignment.CenterHorizontally
        ) {
            Image(
                modifier = Modifier.size(219.dp, 119.dp),
                painter = painterResource(id = R.drawable.ic_default_empty),
                contentDescription = "网络问题",
                contentScale = ContentScale.Crop
            )
            Button(
                modifier = Modifier.padding(8.dp),
                onClick = onclick,
            ) {
                Text(text = "网络不佳,请点击重试")
            }
        }
    }
    
    /**
     * 加载中动效
     * */
    @Composable
    fun LoadingPageUI() {
        Box(
            modifier = Modifier
                .fillMaxSize()
                .padding(170.dp), contentAlignment = Alignment.Center
        ) {
            val animator by rememberInfiniteTransition().animateFloat(
                initialValue = 0f,
                targetValue = 360f,
                animationSpec = infiniteRepeatable(
                    tween(800, easing = LinearEasing),
                    repeatMode = RepeatMode.Restart
                )
            )
            Canvas(modifier = Modifier.fillMaxSize()) {
                translate(80f, 80f) {
                    drawArc(
                        color = RedPink,
                        startAngle = 0f,
                        sweepAngle = animator,
                        useCenter = false,
                        size = Size(80 * 2f, 80 * 2f),
                        style = Stroke(12f),
                        alpha = 0.6f,
                    )
                }
            }
        }
    }
    
    /**
     * 加载下一页失败
     * */
    @Composable
    fun NextPageLoadError(onClick: () -> Unit) {
        Row(
            modifier = Modifier
                .fillMaxWidth()
                .padding(8.dp),
            verticalAlignment = Alignment.CenterVertically,
            horizontalArrangement = Arrangement.Center,
        ) {
            Button(onClick = onClick) {
                Text(text = "重试")
            }
        }
    }
    

    四、列表的Item

    Item中需要嵌入播放器,由于播放器和布局是java代码写的,所以涉及两者相互调用。
    LaunchedEffect:利用它,我们可以在@Compose中使用协程,官方文档是这么描述的:
    <u>要在可组合函数中安全地调用挂起函数,请使用launchedeeffect可组合函数。当launchedeeffect进入Composition时,它会启动一个协程,并将代码块作为参数传递。如果LaunchedEffect离开组合,协程将被取消。</u>

    先看布局代码:

    分为上面的文案描述和播放器部分,这里我通过判断:

     if(当前item的下标 == 第一个可见Item的下标){
          布局播放器并preper
    }else{
    
          贴一张视频封面占位
    }
    
    1. 卡片上面的文字和封面部分:
    @ExperimentalCoilApi
    @Composable
    fun VideoCardItem(
        videoItem: VideoItem,
        isFocused: Boolean,
        onClick: () -> Unit,
        index: Int,
        viewModel: MainViewModel?
    ) {
        val videoInfo = videoItem.videoInfo
        Card(
            modifier = Modifier
                .fillMaxWidth()
                .padding(start = 5.dp, top = 5.dp, end = 5.dp, bottom = 5.dp),
            shape = RoundedCornerShape(10.dp),
            elevation = 8.dp,
            backgroundColor = if (isFocused) gray300 else MaterialTheme.colors.surface
        ) {
            Column(
                modifier = Modifier.padding(16.dp)
            ) {
    
                Text(
                    text = "$index: ${videoInfo?.description}",
                    style = MaterialTheme.typography.h6
                )
                Text(
                    modifier = Modifier.padding(top = 8.dp),
                    text = videoInfo?.title ?: "",
                    style = MaterialTheme.typography.body1,
                    color = gray600
                )
                var width = 1280
                var height = 720
                videoInfo?.playInfo?.let {
                    if (it.isNotEmpty()) {
                        width = it[0].width
                        height = it[0].height
                    }
                }
    
                // 如果该Item是顶部可见,给它一个播放器自动播放,否则给一张海报占位
                if (isFocused) {
                    ExoPlayerView(isFocused, videoInfo, viewModel)
                } else {
                    // 截断以下图片Url
                    val coverUrl = videoInfo?.cover?.feed?.substringBefore('?')
                    CoilImage(
                        url = coverUrl,
                        modifier = Modifier
                            .aspectRatio(width.toFloat() / height)
                            .fillMaxWidth()
                    )
                }
            }
        }
    }
    
    1. 播放器部分,需要在Compose调用Android SDK的UI逻辑,俗称Compose调用Android:
    @ExperimentalCoilApi
    @Composable
    fun ExoPlayerView(isFocused: Boolean, videoInfo: VideoInfo?, viewModel: MainViewModel?) {
    
        val context = LocalContext.current
        // 获取播放器实例
        val exoPlayer = remember { ExoPlayerHolder.get(context = context) }
        var playerView: MyPlayerView? = null
    
        var width = 1280
        var height = 720
        videoInfo?.playInfo?.let {
            if (it.isNotEmpty()) {
                width = it[0].width
                height = it[0].height
            }
        }
    
        if (isFocused) {
            videoInfo?.let {
                LaunchedEffect(key1 = videoInfo.playUrl, key2 = it) {
                    val playUri = Uri.parse(it.playUrl)
                    val dataSourceFactory = VideoDataSourceHolder.getCacheFactory(context)
                    val mediaSource = when (Util.inferContentType(playUri)) {
                        C.TYPE_DASH -> DashMediaSource.Factory(dataSourceFactory)
                            .createMediaSource(MediaItem.fromUri(playUri))
                        C.TYPE_HLS -> HlsMediaSource.Factory(dataSourceFactory)
                            .createMediaSource(MediaItem.fromUri(playUri))
                        else -> ProgressiveMediaSource.Factory(dataSourceFactory)
                            .createMediaSource(MediaItem.fromUri(playUri))
                    }
    
                    exoPlayer.setMediaSource(mediaSource)
                    exoPlayer.prepare()
                }
            }
            // Compose中使用传统Android View ,谷歌文档有这样的描述:
            /**
             * 你可以在Compose UI中包含一个Android View层次结构。如果你想使用在Compose中还不能使用的UI元素,比如AdView或             
            *  MapView,这种方法特别有用。这种方法还允许重用您设计的自定义视图。要包含视图元素或层次结构,请使用AndroidView可组          
            *  合。AndroidView被传递一个lambda,返回一个View。AndroidView还提供了一个更新回调函数,当视图膨胀时调用它。每当在          
            *  回调中读取State时,AndroidView就会重新组合。
            */
    
            AndroidView(
                modifier = Modifier.aspectRatio(width.toFloat() / height),
                factory = { context ->
                     // 创建你需要的ViewGroup 或者 View
                    val frameLayout = FrameLayout(context)
                    frameLayout.setBackgroundColor(context.getColor(android.R.color.holo_purple))
                    frameLayout
                },
                update = { frameLayout ->
                    // 假如你定义了状态,则状态发生改变或者它的父节点状态改变,这里都会重建
                    logD("update removeAllViews, playerViewMode: ${PlayerViewManager.playerViewMode}, isFocused:$isFocused")
                    if (PlayerViewManager.playerViewMode == PlayViewMode.HALF_SCREEN) {
                        frameLayout.removeAllViews()
                        if (isFocused) {
                            playerView = PlayerViewManager.get(frameLayout.context)
    
                            // 切换播放器
                            MyPlayerView.switchTargetView(
                                exoPlayer,
                                PlayerViewManager.currentPlayerView,
                                playerView
                            )
                            PlayerViewManager.currentPlayerView = playerView
    
                            playerView?.apply {
                                player?.playWhenReady = true
                                (parent as? ViewGroup)?.removeView(this)
                            }
    
                            frameLayout.addView(
                                playerView,
                                FrameLayout.LayoutParams.MATCH_PARENT,
                                FrameLayout.LayoutParams.MATCH_PARENT
                            )
                            viewModel?.saveFrameLayout(frameLayout)
                            logD("update, frameLayout:$frameLayout")
                        } else if (playerView != null) {
                            playerView?.apply {
                                (parent as? ViewGroup)?.removeView(this)
                                PlayerViewManager.release(this)
                            }
                            playerView = null
                        }
                    }
                }
            )
    
            DisposableEffect(key1 = videoInfo?.playUrl) {
                onDispose {
                    logD("--onDispose, isFocused: $isFocused")
                    if (isFocused) {
                        playerView?.apply {
                            (parent as? ViewGroup)?.removeView(this)
                        }
                        exoPlayer.stop()
                        playerView?.let {
                            PlayerViewManager.release(it)
                        }
                        playerView = null
                    }
                }
            }
        }
    }
    
    1. 那么传统Android如何调用Compose呢?

      代码或者xml中,Fragment中都可以使用Compose,如果是在代码中,假设前面的视频封面把他写在上面的方法中,就可以这么写:

      if (isFocused) {
         // ....
      }else{
         // 这里是Compose中插入Android View
         AndroidView(
                 modifier = Modifier.aspectRatio(width.toFloat() / height),
                 factory = { context ->
                     val coverLayout = FrameLayout(context)
                     coverLayout.setBackgroundColor(context.getColor(android.R.color.darker_gray))
                     coverLayout
                 },
                 update = { coverLayout ->
                     val coverUrl = videoInfo?.cover?.feed?.substringBefore('?')
                     // 这里在Android View中插入Compose,使用ComposeView
                     coverLayout.addView(ComposeView(context).apply {
                         // 这个id需要注册在res/values/ids.xml文件中
                         id = R.id.compose_view_cover
                         setContent {
                             MaterialTheme {
                                 CoilImage(
                                     url = coverUrl,
                                     modifier = Modifier.fillMaxWidth()
                                 )
                             }
                         }
                     })
                 }
              )
      }
      
    1. Android View与Compose调用其实还有很多,这里不多介绍,用到了就去了解。下面再说说播放器逻辑:

      播放器布局就用了exo自带的PlayerView,添加了一个自己的player_controller_layout.xml
      PlayView.java 和 PlayControllerView可以抽出来,自己按需要修改。

      <?xml version="1.0" encoding="utf-8"?>
      <com.google.android.exoplayer2.ui.MyPlayerView xmlns:android="http://schemas.android.com/apk/res/android"
          xmlns:app="http://schemas.android.com/apk/res-auto"
          android:id="@+id/playerView"
          android:layout_width="match_parent"
          android:layout_height="match_parent"
          android:layout_gravity="center"
          android:minHeight="200dp"
          app:keep_content_on_player_reset="false"
          app:resize_mode="fixed_width"
          app:show_buffering="when_playing"
          app:shutter_background_color="@android:color/black"
          app:surface_type="surface_view"
          app:use_controller="true"
          app:controller_layout_id="@layout/player_controller_layout" />
      

    五、exoPlayer播放器:

    1. 播放器创建:
    /**
     * 播放器实例创建
     * */
    object ExoPlayerHolder {
        private var exoplayer: SimpleExoPlayer? = null
    
        fun get(context: Context): SimpleExoPlayer {
            if (exoplayer == null) {
                exoplayer = createExoPlayer(context)
            }
            exoplayer!!.addListener(object : Player.Listener {
                override fun onPlayerError(error: PlaybackException) {
                    super.onPlayerError(error)
                    Toast.makeText(context, error.message, Toast.LENGTH_SHORT).show()
                    logD("onPlayerError:${error.errorCode} ,${error.message}")
                }
    
                override fun onVideoSizeChanged(videoSize: VideoSize) {
                    super.onVideoSizeChanged(videoSize)
                    logD("onVideoSizeChanged:${videoSize.width} x ${videoSize.height} | ratio: ${videoSize.pixelWidthHeightRatio}")
                }
    
                override fun onSurfaceSizeChanged(width: Int, height: Int) {
                    super.onSurfaceSizeChanged(width, height)
                    logD("onSurfaceSizeChanged:$width x $height")
                }
            })
            return exoplayer!!
        }
    
        // 创建ExoPlayer实例
        private fun createExoPlayer(context: Context): SimpleExoPlayer {
            return SimpleExoPlayer.Builder(context)
                .setLoadControl(
                    DefaultLoadControl.Builder().setBufferDurationsMs(
                        // 设置预加载上限下限
                        DefaultLoadControl.DEFAULT_MIN_BUFFER_MS,
                        DefaultLoadControl.DEFAULT_MAX_BUFFER_MS,
                        DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_MS / 10,
                        DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS / 10
                    ).build()
                )
                .build()
                .apply {
                    // 播放模式,设置为不重复播放
                    repeatMode = Player.REPEAT_MODE_ONE
                }
        }
    }
    
    1. 实例化PlayerView:

      PlayerViewManager.kt

    这里用到了androidx.core.util.Pools工具,他是个对象池,复用对象池中的对象,可以避免频繁创建和销毁堆中的对, 进而减少垃圾收集器的负担。设置为2就够用了,acquire()先获取对象,如果没有获取到就创建,使用完后release()即归还给对象池。

    对象池介绍: https://www.jianshu.com/p/eb04e4e1869d

    /**
     * 用来管理 PlayerView
     * */
    object PlayerViewManager : ExoEventListener {
    
        var currentPlayerView: MyPlayerView? = null
    
        var playerViewMode = PlayViewMode.HALF_SCREEN
        var activity: MainActivity? = null
        var viewModel: MainViewModel? = null
    
        private val playerViewPool = Pools.SimplePool<MyPlayerView>(2)
    
        fun get(context: Context): MyPlayerView {
            return playerViewPool.acquire() ?: createPlayerView(context)
        }
    
        fun release(player: MyPlayerView) {
            playerViewPool.release(player)
        }
    
        /**
         * 创建PlayerView
         * */
        private fun createPlayerView(context: Context): MyPlayerView {
            val playView = (LayoutInflater.from(context)
                .inflate(R.layout.exoplayer_texture_view, null, false) as MyPlayerView)
            playView.setShowMultiWindowTimeBar(true)
            playView.setShowBuffering(MyPlayerView.SHOW_BUFFERING_ALWAYS)
            playView.controllerAutoShow = true
            playView.playerController.setExoEventListener(this)
    
            initOther(playView)
            return playView
        }
    }
    
    1. 缓存设置与缓存策略:

      114031.jpg
    /**
     * 缓存基本设置,exo内部会提供一个命名 exoplayer_internal.db 的数据库作为缓存
     * */
    object CacheHolder {
        private var cache: SimpleCache? = null
        private val lock = Object()
    
        fun get(context: Context): SimpleCache {
            synchronized(lock) {
                if (cache == null) {
                    val cacheSize = 20L * 1024 * 1024
                    val exoDatabaseProvider = ExoDatabaseProvider(context)
    
                    cache = SimpleCache(
                        // 缓存文件地址
                        context.cacheDir,
                        // 释放上次的缓存数据
                        LeastRecentlyUsedCacheEvictor(cacheSize),
                        // 提供数据库
                        exoDatabaseProvider
                    )
                }
            }
            return cache!!
        }
    }
    
    /**
     * 设置缓存策略
     * */
    object VideoDataSourceHolder {
        private var cacheDataSourceFactory: CacheDataSource.Factory? = null
        private var defaultDataSourceFactory: DataSource.Factory? = null
    
        fun getCacheFactory(context: Context): CacheDataSource.Factory {
            if (cacheDataSourceFactory == null) {
                val simpleCache = CacheHolder.get(context)
                val defaultFactory = getDefaultFactory(context)
                cacheDataSourceFactory = CacheDataSource.Factory()
                    .setCache(simpleCache)
                    // 设置Uri协议相关参数,用来从缓存做读取操作
                    .setUpstreamDataSourceFactory(defaultFactory)
                    // 设置CacheDataSource工厂类型,用来读取缓存
                    .setCacheReadDataSourceFactory(FileDataSource.Factory())
                    // 缓存写入设置
                    .setCacheWriteDataSinkFactory(
                        CacheDataSink.Factory()
                            .setCache(simpleCache)
                            .setFragmentSize(CacheDataSink.DEFAULT_FRAGMENT_SIZE)
                    )
            }
    
            return cacheDataSourceFactory!!
        }
    
        private fun getDefaultFactory(context: Context): DataSource.Factory {
            if (defaultDataSourceFactory == null) {
                defaultDataSourceFactory = DefaultDataSourceFactory(
                    context,
                    Util.getUserAgent(context, context.packageName)
                )
            }
            return defaultDataSourceFactory!!
        }
    
    }
    

    六、代码:
    ExoPlayer视频播放

    相关文章

      网友评论

        本文标题:Android JetPack Compose实现网络请求分页加

        本文链接:https://www.haomeiwen.com/subject/gfkztrtx.html