Jetpack 系列(6)—— ViewBinding 与 Kotlin 委托双剑合璧

简介: Jetpack 系列(6)—— ViewBinding 与 Kotlin 委托双剑合璧


学习路线图


image.png


1. 认识 ViewBinding


1.1 ViewBinding 用于解决什么问题?


ViewBinding 是 Android Gradle Plugin 3.6 中新增的特性,用于更加轻量地实现视图绑定(即视图与变量的绑定),可以理解为轻量版本的 DataBinding。


1.2 ViewBinding 与其他视图绑定方案对比


在 ViewBinding 之前,业界已经有过几种视图绑定方案了,想必你也用过。那么,ViewBinding 作为后起之秀就一定比前者香吗?我从多个维度对比它们的区别:


角度 findViewById ButterKnife Kotlin Synthetics DataBinding ViewBinding
简洁性
编译期检查
编译速度
支持 Kotlin & Java
收敛模板代码


  • 1、简洁性: findViewById 和 ButterKnife 需要在代码中声明很多变量,其他几种方案代码简洁度较好;
  • 2、编译检查: 编译期间主要有两个方面的检查:类型检查 + 只能访问当前布局中的 id。findViewById、ButterKnife 和 Kotlin Synthetics 在这方面表现较差;
  • 3、编译速度: findViewById 的编译速度是最快的,而 ButterKnife 和 DataBinding 中存在注解处理,编译速度略逊色于 Kotlin Synthetics 和 ViewBinding;
  • 4、支持 Kotlin & Java: Kotlin Synthetics 只支持 Kotlin 语言;
  • 5、收敛模板代码: 基本上每种方案都带有一定量的模板代码,只有 Kotlin Synthetics 的模板代码是较少的。


可以看到,并没有一种绝对优势的方法,但越往后整体的效果是有提升的。另外,❓是什么呢?


1.3 ViewBinding 的实现原理


AGP 插件会为每个 XML 布局文件创建一个绑定类文件 xxxBinding ,绑定类中会持有布局文件中所有带 android:id 属性的 View 引用。例如,有布局文件为 fragment_test.xml ,则插件会生成绑定类 FragmentTestBinding.java

那么,所有 XML 布局文件都生成 Java 类,会不会导致包体积瞬间增大?不会的, 未使用的类会在混淆时被压缩。


2. ViewBinding 的基本用法


这一节我们来介绍 ViewBinding 的使用方法,内容不多。

提示: ViewBinding 要求在 Android Gradle Plugin 版本在至少在 3.6 以上。


2.1 添加配置


视图绑定功能按模块级别启用,启用的模块需要在模块级 build.gralde 中添加配置。例如:

build.gradle


android {
    ...
    viewBinding {
        enabled = true
    }
}
复制代码


对于不需要生成绑定类的布局文件,可以在根节点声明 tools:viewBindingIgnore="true" 。例如:


<LinearLayout
    ...
    tools:viewBindingIgnore="true" >
    ...
</LinearLayout>
复制代码


2.2 视图绑定


绑定类中提供了 3 个视图绑定 API:


// 绑定到视图 view 上
fun <T> bind(view : View) : T
// 使用 inflater 解析布局,再绑定到 View 上
fun <T> inflate(inflater : LayoutInflater) : T
// 使用 inflater 解析布局,再绑定到 View 上
fun <T> inflate(inflater : LayoutInflater, parent : ViewGroup?, attachToParent : Boolean) : T
复制代码
  • 1、在 Activity 中使用


MainActivity.kt


class TestActivity: AppCompatActivity(R.layout.activity_test) {
    private lateinit var binding: ActivityTestBinding
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityTestBinding.inflate(layoutInflater)
        setContentView(binding.root)
        binding.tvDisplay.text = "Hello World."
    }
}
复制代码
  • 2、在 Fragment 中使用


TestFragment.kt


class TestFragment : Fragment(R.layout.fragment_test) {
    private var _binding: FragmentTestBinding? = null
    private val binding get() = _binding!!
    override fun onViewCreated(root: View, savedInstanceState: Bundle?) {
        _binding = FragmentTestBinding.bind(root)
        binding.tvDisplay.text = "Hello World."
    }
    override fun onDestroyView() {
        super.onDestroyView()
        // 置空
        _binding = null
    }
}
复制代码


2.3 避免内存泄露


这里有一个隐藏的内存泄露问题,你需要理解清楚(严格来说这并不是 ViewBinding 的问题,即使你采用其它视图绑定方案也要考虑这个问题)。


问题:为什么 Fragment#onDestroyView() 里需要置空绑定类对象,而 Activity 里不需要?答:Activity 实例和 Activity 视图的生命周期是同步的,而 Fragment 实例和 Fragment 视图的生命周期并不是完全同步的,因此需要在 Fragment 视图销毁时,手动回收绑定类对象,否则造成内存泄露。例如:detach Fragment,或者 remove Fragment 并且事务进入返回栈,此时 Fragment  视图销毁但 Fragment 实例存在。关于 Fragment 生命周期和事务在我之前的一篇文章里讨论过:Android | Fragment 核心原理 & 面试题 (AndroidX 版本)


总之,在视图销毁但是控制类对象实例还存活的时机,你就需要手动回收绑定类对象,否则造成内存泄露。


2.4 ViewBinding 绑定类源码


反编译如下:

ActivityTestBinding.java


public final class ActivityTestBinding implements ViewBinding {
    private final ConstraintLayout rootView;
    public final TextView tvDisplay;
    private ActivityTestBinding (ConstraintLayout paramConstraintLayout1, TextView paramTextView)
        this.rootView = paramConstraintLayout1;
        this.tvDisplay = paramTextView;
    }
    public static ActivityTestBinding bind(View paramView) {
        TextView localTextView = (TextView)paramView.findViewById(2131165363);
        if (localTextView != null) {
            return new ActivityMainBinding((ConstraintLayout)paramView, localTextView);
        }else {
          paramView = "tvDisplay";
        }
        throw new NullPointerException("Missing required view with ID: ".concat(paramView));
    }
    public static ActivityMainBinding inflate(LayoutInflater paramLayoutInflater) {
        return inflate(paramLayoutInflater, null, false);
    }
    public static ActivityMainBinding inflate(LayoutInflater paramLayoutInflater, ViewGroup paramViewGroup, boolean paramBoolean) {
        paramLayoutInflater = paramLayoutInflater.inflate(2131361821, paramViewGroup, false);
        if (paramBoolean) {
            paramViewGroup.addView(paramLayoutInflater);
        }
        return bind(paramLayoutInflater);
    }
    public ConstraintLayout getRoot() {
        return this.rootView;
    }
}
复制代码



3. ViewBinding 与 Kotlin 委托双剑合璧


到这里,ViewBinding 的使用教程已经说完了。但是回过头看,有没有发现一些局限性呢?


  • 1、创建和回收 ViewBinding 对象需要重复编写样板代码,特别是在 Fragment 中使用的案例;
  • 2、binding 属性是可空的,也是可变的,使用起来不方便。


那么,有没有可优化的方案呢?我们想起了 Kotlin 属性委托,关于 Kotlin 委托机制在我之前的一篇文章里讨论过:Kotlin | 委托机制 & 原理。如果你还不太了解 Kotlin 委托,下面的内容对你会有些难度。下面,我将带你一步步封装 ViewBinding 属性委托工具。首先,我们梳理一下我们要委托的内容与需求,以及相应的解决办法:


需求 解决办法
需要委托 ViewBinding#bind() 的调用 反射
需要委托 binding = null 的调用 监听 Fragment 视图生命周期
期望 binding 属性声明为非空不可变变量 ReadOnlyProperty<F, V>

3.1 ViewBinding + Kotlin 委托 1.0


我们现在较复杂的 Fragment 中尝试使用 Kotlin 委托优化:

FragmentViewBindingPropertyV1.kt


private const val TAG = "ViewBindingProperty"
public inline fun <reified V : ViewBinding> viewBindingV1() = viewBindingV1(V::class.java)
public inline fun <reified T : ViewBinding> viewBindingV1(clazz: Class<T>): FragmentViewBindingPropertyV1<Fragment, T> {
    val bindMethod = clazz.getMethod("bind", View::class.java)
    return FragmentViewBindingPropertyV1 {
        bindMethod(null, it.requireView()) as T
    }
}
/**
 * @param viewBinder 创建绑定类对象
 */
class FragmentViewBindingPropertyV1<in F : Fragment, out V : ViewBinding>(
    private val viewBinder: (F) -> V
) : ReadOnlyProperty<F, V> {
    private var viewBinding: V? = null
    @MainThread
    override fun getValue(thisRef: F, property: KProperty<*>): V {
        // 已经绑定,直接返回
        viewBinding?.let { return it }
        // Use viewLifecycleOwner.lifecycle other than lifecycle
        val lifecycle = thisRef.viewLifecycleOwner.lifecycle
        val viewBinding = viewBinder(thisRef)
        if (lifecycle.currentState == Lifecycle.State.DESTROYED) {
            Log.w(
                TAG, "Access to viewBinding after Lifecycle is destroyed or hasn't created yet. " +
                        "The instance of viewBinding will be not cached."
            )
            // We can access to ViewBinding after Fragment.onDestroyView(), but don't save it to prevent memory leak
        } else {
            lifecycle.addObserver(ClearOnDestroyLifecycleObserver())
            this.viewBinding = viewBinding
        }
        return viewBinding
    }
    @MainThread
    fun clear() {
        viewBinding = null
    }
    private inner class ClearOnDestroyLifecycleObserver : LifecycleObserver {
        private val mainHandler = Handler(Looper.getMainLooper())
        @MainThread
        @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
        fun onDestroy(owner: LifecycleOwner) {
            owner.lifecycle.removeObserver(this)
            mainHandler.post { clear() }
        }
    }
}
复制代码


使用示例:


class TestFragment : Fragment(R.layout.fragment_test) {
    private val binding : FragmentTestBinding by viewBindingV1()
    override fun onViewCreated(root: View, savedInstanceState: Bundle?) {
        binding.tvDisplay.text = "Hello World."
    }
}
复制代码


干净清爽!前面提出的三个需求也都实现了,现在我为你解答细节:


  • 问题 1、为什么可以使用 V::class.java,不是泛型擦除了吗? 利用了 Kotlin 内敛函数 + 实化类型参数,编译后函数体整体被复制到调用处,V::class.java 其实是 FragmentTestBinding::class.java。具体分析见:Java | 关于泛型能问的都在这里了(含Kotlin)
  • 问题 2、ReadOnlyProperty 是什么? ReadOnlyProperty 是不可变属性代理,通过 getValue(...) 方法实现委托行为。第一个类型参数 F 是属性所有者,第二个参数 V 是属性类型,因为我们在 Fragment 中定义属性,属性类型为 ViewBinding,所谓定义类型参数为 ;
  • 问题 3、解释下 getValue(...) 方法? 直接看注释:


FragmentViewBindingPropertyV1.kt


@MainThread
override fun getValue(thisRef: F, property: KProperty<*>): V {
    // 1、viewBinding 不为空说明已经绑定,直接返回
    viewBinding?.let { return it }
    // 2、Fragment 视图的生命周期
    val lifecycle = thisRef.viewLifecycleOwner.lifecycle
    // 3、实例化绑定类对象
    val viewBinding = viewBinder(thisRef)
    if (lifecycle.currentState == Lifecycle.State.DESTROYED) {
        // 4.1 如果视图生命周期为 DESTROYED,说明视图被销毁,此时不缓存绑定类对象(避免内存泄漏)
    } else {
        // 4.2 定义视图生命周期监听者
        lifecycle.addObserver(ClearOnDestroyLifecycleObserver())
        // 4.3 缓存绑定类对象
        this.viewBinding = viewBinding
    }
    return viewBinding
}
复制代码
  • 问题 4、为什么 onDestroy() 要采用 Handler#post(Message) 完成? 因为 Fragment#viewLifecycleOwner 通知生命周期事件 ON_DESTROY 的时机在 Fragment#onDestroyView 之前。如果不使用 post 的方式,那么业务方要是在 onDestroyView 中访问了 binding,则会二次执行 getValue() 这是不必要的。


3.2 ViewBinding + Kotlin 委托 2.0


V1.0 版本使用了反射,真的一定要反射吗?反射调用 bind 函数的目的就是获得一个 ViewBinding 绑定类对象,或许我们可以试试把创建对象的行为交给外部去定义,类似这样用一个 lambda 表达式实现工厂函数:


FragmentViewBindingPropertyV2.kt


inline fun <F : Fragment, V : ViewBinding> viewBindingV2(
    crossinline viewBinder: (View) -> V,
    // 类似于创建工厂
    crossinline viewProvider: (F) -> View = Fragment::requireView
) = FragmentViewBindingPropertyV2 { fragment: F ->
    viewBinder(viewProvider(fragment))
}
class FragmentViewBindingPropertyV2<in F : Fragment, out V : ViewBinding>(
    private val viewBinder: (F) -> V
) : ReadOnlyProperty<F, V> {
    // 以下源码相同 ...
}
复制代码


使用示例:


class TestFragment : Fragment(R.layout.fragment_test) {
    private val binding by viewBindingV2(FragmentTestBinding::bind)
    override fun onViewCreated(root: View, savedInstanceState: Bundle?) {
        binding.tvDisplay.text = "Hello World."
    }
}
复制代码


干净清爽!不使用反射也可以实现,现在我为你解答细节:


  • 问题 5、(View) -> V 是什么? Kotlin 高阶函数,可以把 lambda 表达式直接作为参数传递,其中 View 是函数参数,而 T 是函数返回值。lambda 表达式本质上是 「可以作为值传递的代码块」。在老版本 Java 中,传递代码块需要使用匿名内部类实现,而使用 lambda 表达式甚至连函数声明都不需要,可以直接传递代码块作为函数值;
  • 问题 6、Fragment::requireView 是什么? 把函数 requireView() 作为参数传递。Fragment#requireView() 会返回 Fragment 的根节点,但要注意在 onCreateView() 之前调用 requireView() 会抛出异常;
  • 问题 7、FragmentTestBinding::bind 是什么? 把函数 bind() 作为参数传递,bind 函数的参数为 View,返回值为 ViewBinding,与函数声明 (View) -> V 匹配。


3.3 ViewBinding + Kotlin 委托最终版


V2.0 版本已经完成了针对 Fragment 的属性代理,但是实际场景中只会在 Fragment 中使用 ViewBinding 吗?显然并不是,我们还有其他一些场景:


  • Activity
  • Fragment
  • DialogFragment
  • ViewGroup
  • RecyclerView.ViewHolder


所以,我们有必要将委托工具适当封装得更通用些,完整代码和演示工程你可以直接下载查看: AndroidFamilyDemo · KotlinDelegate

ViewBindingProperty.kt


// -------------------------------------------------------
// ViewBindingProperty for Activity
// -------------------------------------------------------
@JvmName("viewBindingActivity")
inline fun <V : ViewBinding> ComponentActivity.viewBinding(
    crossinline viewBinder: (View) -> V,
    crossinline viewProvider: (ComponentActivity) -> View = ::findRootView
): ViewBindingProperty<ComponentActivity, V> = ActivityViewBindingProperty { activity: ComponentActivity ->
    viewBinder(viewProvider(activity))
}
@JvmName("viewBindingActivity")
inline fun <V : ViewBinding> ComponentActivity.viewBinding(
    crossinline viewBinder: (View) -> V,
    @IdRes viewBindingRootId: Int
): ViewBindingProperty<ComponentActivity, V> = ActivityViewBindingProperty { activity: ComponentActivity ->
    viewBinder(activity.requireViewByIdCompat(viewBindingRootId))
}
// -------------------------------------------------------
// ViewBindingProperty for Fragment / DialogFragment
// -------------------------------------------------------
@Suppress("UNCHECKED_CAST")
@JvmName("viewBindingFragment")
inline fun <F : Fragment, V : ViewBinding> Fragment.viewBinding(
    crossinline viewBinder: (View) -> V,
    crossinline viewProvider: (F) -> View = Fragment::requireView
): ViewBindingProperty<F, V> = when (this) {
    is DialogFragment -> DialogFragmentViewBindingProperty { fragment: F ->
        viewBinder(viewProvider(fragment))
    } as ViewBindingProperty<F, V>
    else -> FragmentViewBindingProperty { fragment: F ->
        viewBinder(viewProvider(fragment))
    }
}
@Suppress("UNCHECKED_CAST")
@JvmName("viewBindingFragment")
inline fun <F : Fragment, V : ViewBinding> Fragment.viewBinding(
    crossinline viewBinder: (View) -> V,
    @IdRes viewBindingRootId: Int
): ViewBindingProperty<F, V> = when (this) {
    is DialogFragment -> viewBinding(viewBinder) { fragment: DialogFragment ->
        fragment.getRootView(viewBindingRootId)
    } as ViewBindingProperty<F, V>
    else -> viewBinding(viewBinder) { fragment: F ->
        fragment.requireView().requireViewByIdCompat(viewBindingRootId)
    }
}
// -------------------------------------------------------
// ViewBindingProperty for ViewGroup
// -------------------------------------------------------
@JvmName("viewBindingViewGroup")
inline fun <V : ViewBinding> ViewGroup.viewBinding(
    crossinline viewBinder: (View) -> V,
    crossinline viewProvider: (ViewGroup) -> View = { this }
): ViewBindingProperty<ViewGroup, V> = LazyViewBindingProperty { viewGroup: ViewGroup ->
    viewBinder(viewProvider(viewGroup))
}
@JvmName("viewBindingViewGroup")
inline fun <V : ViewBinding> ViewGroup.viewBinding(
    crossinline viewBinder: (View) -> V,
    @IdRes viewBindingRootId: Int
): ViewBindingProperty<ViewGroup, V> = LazyViewBindingProperty { viewGroup: ViewGroup ->
    viewBinder(viewGroup.requireViewByIdCompat(viewBindingRootId))
}
// -------------------------------------------------------
// ViewBindingProperty for RecyclerView#ViewHolder
// -------------------------------------------------------
@JvmName("viewBindingViewHolder")
inline fun <V : ViewBinding> RecyclerView.ViewHolder.viewBinding(
    crossinline viewBinder: (View) -> V,
    crossinline viewProvider: (RecyclerView.ViewHolder) -> View = RecyclerView.ViewHolder::itemView
): ViewBindingProperty<RecyclerView.ViewHolder, V> = LazyViewBindingProperty { holder: RecyclerView.ViewHolder ->
    viewBinder(viewProvider(holder))
}
@JvmName("viewBindingViewHolder")
inline fun <V : ViewBinding> RecyclerView.ViewHolder.viewBinding(
    crossinline viewBinder: (View) -> V,
    @IdRes viewBindingRootId: Int
): ViewBindingProperty<RecyclerView.ViewHolder, V> = LazyViewBindingProperty { holder: RecyclerView.ViewHolder ->
    viewBinder(holder.itemView.requireViewByIdCompat(viewBindingRootId))
}
// -------------------------------------------------------
// ViewBindingProperty
// -------------------------------------------------------
private const val TAG = "ViewBindingProperty"
interface ViewBindingProperty<in R : Any, out V : ViewBinding> : ReadOnlyProperty<R, V> {
    @MainThread
    fun clear()
}
class LazyViewBindingProperty<in R : Any, out V : ViewBinding>(
    private val viewBinder: (R) -> V
) : ViewBindingProperty<R, V> {
    private var viewBinding: V? = null
    @Suppress("UNCHECKED_CAST")
    @MainThread
    override fun getValue(thisRef: R, property: KProperty<*>): V {
        // Already bound
        viewBinding?.let { return it }
        return viewBinder(thisRef).also {
            this.viewBinding = it
        }
    }
    @MainThread
    override fun clear() {
        viewBinding = null
    }
}
abstract class LifecycleViewBindingProperty<in R : Any, out V : ViewBinding>(
    private val viewBinder: (R) -> V
) : ViewBindingProperty<R, V> {
    private var viewBinding: V? = null
    protected abstract fun getLifecycleOwner(thisRef: R): LifecycleOwner
    @MainThread
    override fun getValue(thisRef: R, property: KProperty<*>): V {
        // Already bound
        viewBinding?.let { return it }
        val lifecycle = getLifecycleOwner(thisRef).lifecycle
        val viewBinding = viewBinder(thisRef)
        if (lifecycle.currentState == Lifecycle.State.DESTROYED) {
            Log.w(
                TAG, "Access to viewBinding after Lifecycle is destroyed or hasn't created yet. " +
                        "The instance of viewBinding will be not cached."
            )
            // We can access to ViewBinding after Fragment.onDestroyView(), but don't save it to prevent memory leak
        } else {
            lifecycle.addObserver(ClearOnDestroyLifecycleObserver(this))
            this.viewBinding = viewBinding
        }
        return viewBinding
    }
    @MainThread
    override fun clear() {
        viewBinding = null
    }
    private class ClearOnDestroyLifecycleObserver(
        private val property: LifecycleViewBindingProperty<*, *>
    ) : LifecycleObserver {
        private companion object {
            private val mainHandler = Handler(Looper.getMainLooper())
        }
        @MainThread
        @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
        fun onDestroy(owner: LifecycleOwner) {
            mainHandler.post { property.clear() }
        }
    }
}
class FragmentViewBindingProperty<in F : Fragment, out V : ViewBinding>(
    viewBinder: (F) -> V
) : LifecycleViewBindingProperty<F, V>(viewBinder) {
    override fun getLifecycleOwner(thisRef: F): LifecycleOwner {
        try {
            return thisRef.viewLifecycleOwner
        } catch (ignored: IllegalStateException) {
            error("Fragment doesn't have view associated with it or the view has been destroyed")
        }
    }
}
class DialogFragmentViewBindingProperty<in F : DialogFragment, out V : ViewBinding>(
    viewBinder: (F) -> V
) : LifecycleViewBindingProperty<F, V>(viewBinder) {
    override fun getLifecycleOwner(thisRef: F): LifecycleOwner {
        return if (thisRef.showsDialog) {
            thisRef
        } else {
            try {
                thisRef.viewLifecycleOwner
            } catch (ignored: IllegalStateException) {
                error("Fragment doesn't have view associated with it or the view has been destroyed")
            }
        }
    }
}
// -------------------------------------------------------
// Utils
// -------------------------------------------------------
@RestrictTo(RestrictTo.Scope.LIBRARY)
class ActivityViewBindingProperty<in A : ComponentActivity, out V : ViewBinding>(
    viewBinder: (A) -> V
) : LifecycleViewBindingProperty<A, V>(viewBinder) {
    override fun getLifecycleOwner(thisRef: A): LifecycleOwner {
        return thisRef
    }
}
fun <V : View> View.requireViewByIdCompat(@IdRes id: Int): V {
    return ViewCompat.requireViewById(this, id)
}
fun <V : View> Activity.requireViewByIdCompat(@IdRes id: Int): V {
    return ActivityCompat.requireViewById(this, id)
}
/**
 * Utility to find root view for ViewBinding in Activity
 */
fun findRootView(activity: Activity): View {
    val contentView = activity.findViewById<ViewGroup>(android.R.id.content)
    checkNotNull(contentView) { "Activity has no content view" }
    return when (contentView.childCount) {
        1 -> contentView.getChildAt(0)
        0 -> error("Content view has no children. Provide root view explicitly")
        else -> error("More than one child view found in Activity content view")
    }
}
fun DialogFragment.getRootView(viewBindingRootId: Int): View {
    val dialog = checkNotNull(dialog) {
        "DialogFragment doesn't have dialog. Use viewBinding delegate after onCreateDialog"
    }
    val window = checkNotNull(dialog.window) { "Fragment's Dialog has no window" }
    return with(window.decorView) {
        if (viewBindingRootId != 0) requireViewByIdCompat(
            viewBindingRootId
        ) else this
    }
}
复制代码



4. 总结


ViewBinding 是一个轻量级的视图绑定方案,Android Gradle 插件会为每个 XML 布局文件创建一个绑定类。在 Fragment 中使用 ViewBinding 需要注意在 Fragment#onDestroyView() 里置空绑定类对象避免内存泄漏。但这会带来很多重复编写样板代码,使用属性委托可以收敛模板代码,保证调用方代码干净清爽。


角度 findViewById ButterKnife Kotlin Synthetics DataBinding ViewBinding ViewBindingProperty
简洁性
编译期检查
编译速度
支持 Kotlin & Java
收敛模板代码


目录
相关文章
|
22天前
|
存储 缓存 Android开发
安卓Jetpack Compose+Kotlin, 使用ExoPlayer播放多个【远程url】音频,搭配Okhttp库进行下载和缓存,播放完随机播放下一首
这是一个Kotlin项目,使用Jetpack Compose和ExoPlayer框架开发Android应用,功能是播放远程URL音频列表。应用会检查本地缓存,如果文件存在且大小与远程文件一致则使用缓存,否则下载文件并播放。播放完成后或遇到异常,会随机播放下一首音频,并在播放前随机设置播放速度(0.9到1.2倍速)。代码包括ViewModel,负责音频管理和播放逻辑,以及UI层,包含播放和停止按钮。
103 0
|
13天前
|
安全 Java Android开发
安卓开发中的新趋势:Kotlin与Jetpack的完美结合
【6月更文挑战第20天】在不断进化的移动应用开发领域,Android平台以其开放性和灵活性赢得了全球开发者的青睐。然而,随着技术的迭代,传统Java语言在Android开发中逐渐显露出局限性。Kotlin,一种现代的静态类型编程语言,以其简洁、安全和高效的特性成为了Android开发中的新宠。同时,Jetpack作为一套支持库、工具和指南,旨在帮助开发者更快地打造优秀的Android应用。本文将探讨Kotlin与Jetpack如何共同推动Android开发进入一个新的时代,以及这对开发者意味着什么。
|
22天前
|
存储 数据库 Android开发
安卓Jetpack Compose+Kotlin,支持从本地添加音频文件到播放列表,支持删除,使用ExoPlayer播放音乐
为了在UI界面添加用于添加和删除本地音乐文件的按钮,以及相关的播放功能,你需要实现以下几个步骤: 1. **集成用户选择本地音乐**:允许用户从设备中选择音乐文件。 2. **创建UI按钮**:在界面中创建添加和删除按钮。 3. **数据库功能**:使用Room数据库来存储音频文件信息。 4. **更新ViewModel**:处理添加、删除和播放音频文件的逻辑。 5. **UI实现**:在UI层支持添加、删除音乐以及播放功能。
|
7天前
|
设计模式 Java Kotlin
Kotlin中的委托、属性委托和延迟加载
Kotlin中的委托、属性委托和延迟加载
8 1
|
17天前
|
安全 JavaScript 前端开发
kotlin开发安卓app,JetPack Compose框架,给webview新增一个按钮,点击刷新网页
在Kotlin中开发Android应用,使用Jetpack Compose框架时,可以通过添加一个按钮到TopAppBar来实现WebView页面的刷新功能。按钮位于右上角,点击后调用`webViewState?.reload()`来刷新网页内容。以下是代码摘要:
|
18天前
|
JavaScript Java Android开发
kotlin安卓在Jetpack Compose 框架下跨组件通讯EventBus
**EventBus** 是一个Android事件总线库,简化组件间通信。要使用它,首先在Gradle中添加依赖`implementation &#39;org.greenrobot:eventbus:3.3.1&#39;`。然后,可选地定义事件类如`MessageEvent`。在活动或Fragment的`onCreate`中注册订阅者,在`onDestroy`中反注册。通过`@Subscribe`注解方法处理事件,如`onMessageEvent`。发送事件使用`EventBus.getDefault().post()`。
|
18天前
|
JavaScript 前端开发 Android开发
kotlin安卓在Jetpack Compose 框架下使用webview , 网页中的JavaScript代码如何与native交互
在Jetpack Compose中使用Kotlin创建Webview组件,设置JavaScript交互:`@Composable`函数`ComposableWebView`加载网页并启用JavaScript。通过`addJavascriptInterface`添加`WebAppInterface`类,允许JavaScript调用Android方法如播放音频。当页面加载完成时,执行`onWebViewReady`回调。
|
21天前
|
监控 Android开发 数据安全/隐私保护
安卓kotlin JetPack Compose 实现摄像头监控画面变化并录制视频
在这个示例中,开发者正在使用Kotlin和Jetpack Compose构建一个Android应用程序,该程序 能够通过手机后置主摄像头录制视频、检测画面差异、实时预览并将视频上传至FTP服务器的Android应用
|
20天前
|
安全 网络安全 API
kotlin安卓开发JetPack Compose 如何使用webview 打开网页时给webview注入cookie
在Jetpack Compose中使用WebView需借助AndroidView。要注入Cookie,首先在`build.gradle`添加WebView依赖,如`androidx.webkit:webkit:1.4.0`。接着创建自定义`ComposableWebView`,通过`CookieManager`设置接受第三方Cookie并注入Cookie字符串。最后在Compose界面使用这个自定义组件加载URL。注意Android 9及以上版本可能需要在网络安全配置中允许第三方Cookie。
130 0
|
22天前
|
Android开发 Kotlin
kotlin安卓开发【Jetpack Compose】:封装SnackBarUtil工具类方便使用
GPT-4o 是一个非常智能的模型,比当前的通义千问最新版本在能力上有显著提升。作者让GPT开发一段代码,功能为在 Kotlin 中使用 Jetpack Compose 框架封装一个 Snackbar 工具类,方便调用