第一行代码 Android

本文深入探讨了Android开发中的Fragment使用,包括静态和动态添加,实现返回栈以及与Activity的交互。接着讲解了Retrofit网络库的基本用法,如何处理复杂接口地址,以及在协程中完成网络请求。此外,还详细介绍了MaterialDesign,包括Toolbar菜单栏、滑动菜单、悬浮按钮、Snackbar和协调布局的运用。最后,简述了Jetpack中的ViewModel、Lifecycle和LiveData组件,以及Room数据库和WorkManager的使用,展示了如何进行后台任务管理和数据持久化。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

第5章 Fragment

5.2 fragment的使用方法

5.2.1 Fragment的简单用法(静态添加)

创建一个fragment,继承Fragment

class LeftFragment : Fragment() {
    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        return inflater.inflate(R.layout.left_fragment,container,false)
    }
}
  1. 静态添加
    在宿主Activityxml代码中添加fragment,关键属性name
    <fragment
        android:layout_width="0dp"
        android:layout_height="match_parent"
        android:layout_weight="1"
        android:name="com.example.firstline.FiveChapter.Fragment1.LeftFragment"
        android:id="@+id/left_fragment"/>
5.2.2 动态添加fragment
		//1.创建待添加fragment的实例
        val fragment = LeftFragment()
        //2.获取fragmentManager
        val manager = supportFragmentManager
        //3.开启事务
        val transaction = manager.beginTransaction()
        //实现类似返回栈的效果
        transaction.addToBackStack(null)
        //4.将fragment添加到宿主Activity中id为R.id.right_layout的布局中
        transaction.replace(R.id.right_layout,fragment)
        //5.提交事务
        transaction.commit()
5.2.3 fragment实现返回栈

Activity中无论添加多少fragment只要点击返回键,回直接关闭所有fragment和宿主activity,像要fragment实现activity一层一层返回的效果,需要fragment增加到返回栈中。参考上一个示例

5.2.4 Fragment和Activity之间的交互
  • fragment中获取activityval activity = activity as FragmentMain1Activity

  • activity中获取fragmentval fragment = manager.findFragmentById(R.id.right_layout)
    获取id为R.id.right_layout的布局中存在的fragment

5.3 framgment的生命周期

5.4 动态加载布局的技巧

5.4.1 使用限定符
5.4.2使用最小宽度限定符

第11章 网络

11.6 Retrofit 网络库

11.6.1 Retrofit的基本用法

作用:
数据来源为同一个服务器地址
获取服务器各个接口的数据

  • 添加依赖
    implementation 'com.squareup.retrofit2:retrofit:2.6.1'
    implementation 'com.squareup.retrofit2:converter-gson:2.6.1'
  • 数据的类对象
class RetrofitApp (val id: String, val name: String, val version: String )
  • 创建服务器接口,将同一服务器提供的功能统一归纳在一个接口中
  1. getAppData()方法上添加注解,这里使用了@GET注解,表示当调用getAppData()方法时,会发送一条get请求,并传入请求地址的相对路径
  2. getAppData()方法的返回值必须声明成Retrofit中内置的Call类型,并通过泛型来指定服务器响应的数据转换成什么对象。
interface RetrofitAppService {
    @GET("get_data.gson")
    fun getAppData(): Call<List<RetrofitApp>>
}
  • 网络调用
//获取Retrofit对象
 		val retrofit = Retrofit.Builder()
            .baseUrl("服务器域名")//请求的根路径
            .addConverterFactory(GsonConverterFactory.create())//解析数据时使用的转换库
            .build()
		//传入service接口对应的class类型,创建该接口的动态代理对象
        val appService = retrofit.create(RetrofitAppService::class.java)
        //getAppData方法会返回一个Call<List<RetrofitApp>>对象
        appService.getAppData().enqueue(object: Callback<List<RetrofitApp>>{
            override fun onFailure(call: Call<List<RetrofitApp>>, t: Throwable) {
                t.printStackTrace()
            }
            override fun onResponse(call: Call<List<RetrofitApp>>, response:
             Response<List<RetrofitApp>>) {
                val list = response.body()
                return list
                    }
                }
            }
        })
11.6.2 处理复杂的接口地址类型
  • 对于下面这个地址,代表页数,是一个可变的参数,需要访问这个地址试传入实参
GET http://example.com/<page>/get_data.json
   /*  数据的接口地址中<page>需要传入参数
   * GET中的{page}是一个占位符
   * @Path("page")注解来声明yeshu这个参数是要传入占位符的参数
   * */
    @GET("{page}/get_data.json")
    fun getData(@Path("page") yeshu: String):Call<ContactsContract.Data>
  • 问号后面连接参数,参数直接用&符号分割的请求地址
GET http://example.com/get_data.json?u=<user>&t=<token>

通过@Query注解来声明

    /*
    * 问号后面连接参数,参数直接用&符号分割,这是标准的带参数的GET请求
    * */
    @GET("get_data.json")
    fun getData(@Query("u") user: String,@Query("t") token: String):Call<Data>
协程完成Retrofit网络请求
  • Retrofit已经库中KotlinExtensions.kt文件中定义好回调的代码,只需要三步,我们就可以完成网络请求。
//1.获取Retrofit对象
val retrofit = Retrofit.Builder()
	.baseUrl("服务器域名")//请求的根路径
	.addConverterFactory(GsonConverterFactory.create())//解析数据时使用的转换库
	.build()
//3.在协程的作用域中调用
val coroutineScope = CoroutineScope(Dispatchers.IO)
        coroutineScope.launch {
            getAppData()
        }
//2.获得网络请求返回结果
suspend fun getAppData(){
        try {
            val list = retrofit.create<RetrofitAppService>().getAppData().await()
            //list就是Respose.body
        }catch (e : Exception){
			//捕获异常后操作
        }
    }

库中KotlinExtensions.kt文件源代码:

suspend fun <T : Any> Call<T>.await(): T {
  return suspendCancellableCoroutine { continuation ->
    continuation.invokeOnCancellation {
      cancel()
    }
    enqueue(object : Callback<T> {
      override fun onResponse(call: Call<T>, response: Response<T>) {
        if (response.isSuccessful) {
          val body = response.body()
          if (body == null) {
            val invocation = call.request().tag(Invocation::class.java)!!
            val method = invocation.method()
            val e = KotlinNullPointerException("Response from " +
                method.declaringClass.name +
                '.' +
                method.name +
                " was null but response body type was declared as non-null")
            continuation.resumeWithException(e)
          } else {
            continuation.resume(body)
          }
        } else {
          continuation.resumeWithException(HttpException(response))
        }
      }

      override fun onFailure(call: Call<T>, t: Throwable) {
        continuation.resumeWithException(t)
      }
    })
  }
}
在实际使用中完成Retrofit
  • 需要get的网络地址,根据keywords获取该地区的行政区域,subdistrict是设置显示下级行政区级数1-4,key请求服务权限标识
https://restapi.amap.com/v3/config/district?keywords=<地名>&subdistrict=0&key=<key>
  • 返回的数据格式:PlaceResponse.status==1表示请求成功,PlaceResponse.districts查询成功后返回的行政区域列表
data class PlaceResponse(
    val count: String,
    val districts: List<District>,
    val info: String,
    val infocode: String,
    val status: String,
    val suggestion: Suggestion
)
  • 创建服务器接口,将同一服务器提供的功能统一归纳在一个接口中
interface PlaceService {
    @GET("v3/config/district?subdistrict=0&key=${SunnyWeatherApplication.TOKEN_ADDRESS}")
    fun searchPlaces(@Query("keywords")address: String): Call<PlaceResponse>
}
  • 定义单例类,功能:ServiceCreator .create(服务器的接口)返回动态代理对象
  • 动态代理对象:将某一个类或者接口投射给一个变量,这个变量就可以访问类或者接口的属性和方法
object ServiceCreator {
    private const val BASE_URL = "https://restapi.amap.com/"
    private val retrofit = Retrofit.Builder()
        .baseUrl(BASE_URL)
        .addConverterFactory(GsonConverterFactory.create())
        .build()
    fun <T> create(serviceClass: Class<T>) = retrofit.create(serviceClass)
}

在本例中,动态代理对象的应用:
PlaceService接口通过retrofit.create()方法反射到placeService变量。
placeService变量就是PlaceService接口动态代理对象。
然后通过变量placeService调用PlaceService接口的方法searchPlaces()

object SunnyWeatherNetWork{
    //创建接口的动态代理对象
    private val placeService = ServiceCreator.create(PlaceService::class.java)
    //调用代理对象的方法
    suspend fun searchPlaces(query: String) = placeService.searchPlaces(query).await()

    private suspend fun <T> Call<T>.await(): T{
        return suspendCoroutine {
            enqueue(object : Callback<T>{
                override fun onFailure(call: Call<T>, t: Throwable) {
                    it.resumeWithException(t)
                }

                override fun onResponse(call: Call<T>, response: Response<T>) {
                    val body = response.body()
                    if (body != null)
                        it.resume(body)
                    else
                        it.resumeWithException(RuntimeException("Response body is null"))
                }

            })
        }
    }
}

完成网络请求:
返回的结果就是PlaceResponse类的实例

 val placeResponse = SunnyWeatherNetWork.searchPlaces(query)

第12章 Material Design

12.2 Toolbar菜单栏

  1. 设置App的主题为Actionbar不可见Theme.AppCompat.Light.NoActionBar
  2. 设置Toolbar,在Layout中加入如下代码:
    <androidx.appcompat.widget.Toolbar
        android:id="@+id/toolbar"
        android:layout_width="match_parent"
        android:layout_height="?attr/actionBarSize"
        android:background="@color/colorPrimary"
        android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
        app:popupTheme="@style/ThemeOverlay.AppCompat.Light"
         />
  1. 代码中第一次使用了app属性,因为很多的Material中的属性在老系统中并不存在,为了很好的兼容老系统,增加了 xmlns:app="http://schemas.android.com/apk/res-auto"的命名空间。
  2. 设置Toolbar的主题:android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
  3. 设置菜单项的主题:app:popupTheme="@style/ThemeOverlay.AppCompat.Light"
  4. 在Activity中增加:setSupportActionBar(toolbar)
  • Toolbar中增加菜单
  1. 在res目录中新建menu文件夹,并创建toolbar.xml文件
  2. toolbar.xml文件中代码:
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">
    <item
        android:id="@+id/backup"
        android:icon="@drawable/ic_backup"
        android:title="BackUp"
        app:showAsAction="always"/>
    <item
        android:id="@+id/delete"
        android:icon="@drawable/ic_delete"
        android:title="Delete"
        app:showAsAction="ifRoom"/>
    <item
        android:id="@+id/setting"
        android:icon="@drawable/ic_settings"
        android:title="Setting"
        app:showAsAction="always"/>
</menu>
  1. app:showAsAction属性值有三种情况:
    always表示永远显示在Toolbar中,空间不够则不显示;
    ifRoom表示屏幕空间足够的情况下显示在Toolbar中,不够的话在显示在菜单中;
    never则表示永远显示在菜单中。
  2. 在Activity中添加代码:
    override fun onCreateOptionsMenu(menu: Menu?): Boolean {
        menuInflater.inflate(R.menu.toolbar, menu)
        return true
    }
  1. 全部设置为always
    在这里插入图片描述
  2. 全部设置为never

在这里插入图片描述

12.3 滑动菜单

12.3.1 DrawerLayout抽屉视图
  1. DrawerLyout是一个布局xml的最外层布局更改为androidx.drawerlayout.widget.DrawerLayout
  2. DrawerLyout布局,通过android:layout_gravity属性来识别子布局是否为滑动菜单,在实际使用中建议:在DrawerLyout布局中创建两个子布局,第一个,没有加android:layout_gravity属性的布局就是主屏幕的布局;第二个,加android:layout_gravity属性的布局是滑动菜单。
  3. 滑动菜单的关键就是:android:layout_gravity属性,拥有这个属性的布局或者控件就是滑动菜单,属性的值是start从左侧滑出,为end从右侧滑出。
  4. 记得给DrawerLayout 加id
<androidx.drawerlayout.widget.DrawerLayout 		  
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:id="@+id/drawerLayout">
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_gravity="end"
        android:orientation="vertical">

        <TextView
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:text="右边的滑动菜单"
            android:background="#FFF"
            android:textSize="30sp" />

    </LinearLayout>
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        <androidx.appcompat.widget.Toolbar
            android:id="@+id/toolbar"
            android:layout_width="match_parent"
            android:layout_height="?attr/actionBarSize"
            android:background="@color/colorPrimary"
            android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
            app:popupTheme="@style/ThemeOverlay.AppCompat.Light" />
    </LinearLayout>
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_gravity="start">
        <TextView
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:text="左边的滑动菜单"
            android:background="#FFF"
            android:textSize="30sp" />
    </LinearLayout>

</androidx.drawerlayout.widget.DrawerLayout>
  1. 通过按键点击滑出滑动菜单。滑动菜单设置好以后,在屏幕中并没有办法看到滑动菜单这个功能,所以建议的做法是在Toolbar的左侧加一个导航按钮,在Activity中添加代码:
    在这里插入图片描述
  2. drawerLayout.openDrawer(GravityCompat.START)START菜单打开
        //在Actionbar上面的android.R.id.home按钮设置可见和图标
        supportActionBar?.let {
            //设置home键可见
            it.setDisplayHomeAsUpEnabled(true)
            //设置home键的图标
            it.setHomeAsUpIndicator(R.drawable.ic_menu)
        }
    override fun onOptionsItemSelected(item: MenuItem): Boolean {
        when (item.itemId){
		//在Actionbar上android.R.id.home按键的点击事件
        //drawerLayout是滑动菜单布局的id
        //GravityCompat.START属性值,表示展示android:layout_gravity="start"属性的布局
            android.R.id.home -> drawerLayout.openDrawer(GravityCompat.START)
        }
        return true
    }
12.3.2 NavigationView导航视图
  1. 添加依赖
    implementation 'com.google.android.material:material:1.1.0'
    implementation 'de.hdodenhof:circleimageview:3.0.1'

circleimageview实现图片圆形化的功能
2. App主题:parent="Theme.MaterialComponents.Light.NoActionBar"
3. NavigationView分为两部分:menuheaderLayout
4. 在menu文件夹中新建nav_menu.xml文件
5. group 表示一个组,android:checkableBehavior="single"组内的菜单项都是单选

<menu xmlns:android="http://schemas.android.com/apk/res/android">
    <group android:checkableBehavior="single">
        <item
            android:id="@+id/navCall"
            android:icon="@drawable/nav_call"
            android:title="Call"/>
        <item
            android:id="@+id/navFriends"
            android:icon="@drawable/nav_friends"
            android:title="Friends"/>
        <item
            android:id="@+id/navLocation"
            android:icon="@drawable/nav_location"
            android:title="Location"/>
        <item
            android:id="@+id/navMail"
            android:icon="@drawable/nav_mail"
            android:title="Mail"/>
        <item
            android:id="@+id/navTask"
            android:icon="@drawable/nav_task"
            android:title="Task"/>
    </group>
</menu>
  1. 在layout文件夹中新建nav_header.xml文件
  2. de.hdodenhof.circleimageview.CircleImageView控件,是一个将图片圆形化的控件
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical" android:layout_width="match_parent"
    android:layout_height="180dp"
    android:padding="10dp"
    android:background="@color/colorPrimary">
    <de.hdodenhof.circleimageview.CircleImageView
        android:id="@+id/iconImage"
        android:layout_width="70dp"
        android:layout_height="70dp"
        android:src="@drawable/nav_icon"
        android:layout_centerInParent="true"/>
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:id="@+id/mailText"
        android:textColor="#FFF"
        android:textSize="14sp"
        android:text="tony@mail.com"/>
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_above="@+id/mailText"
        android:textColor="#FFF"
        android:textSize="14sp"
        android:text="Tony Green"
        android:id="@+id/userText"/>
</RelativeLayout>
  1. activity_main.xml代码修改
  2. 加入导航视图控件com.google.android.material.navigation.NavigationView
  3. android:layout_gravity="start"属性使得该控件为滑动菜单
    app:menu="@menu/nav_menu"属性,加载导航的目录
    app:headerLayout="@layout/nav_header"属性,加载头部
<!--    左侧滑动菜单-->
    <com.google.android.material.navigation.NavigationView
        android:id="@+id/navView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_gravity="start"
        app:menu="@menu/nav_menu"
        app:headerLayout="@layout/nav_header"/>

在这里插入图片描述
11. 处理导航视图的菜单点击事件,在Activity中加入代码:

        //导航菜单设置默认选择项
        navView.setCheckedItem(R.id.navCall)
        //导航菜单监听点击,点击后关闭滑动菜单,返回true表示事件已经处理
        navView.setNavigationItemSelectedListener {
            when (it.itemId){
                R.id.navCall -> Toast.makeText(this,"call",Toast.LENGTH_SHORT).show()
            }
            drawerLayout.closeDrawers()
            true
        }
  1. 记得在点击后加上:drawerLayout.closeDrawers()方法关闭滑动菜单

12.4悬浮按钮和可交互提示

12.4.1 FloatingActionButton悬浮操作按钮
  1. 在页面中添加控件,
    app:elevation属性:按钮的高度值;高度越高,投影的范围越大,投影效果越淡。
  2. 由于悬浮按钮需要指定位置,所以外层布局需要相对布局或者帧布局一类可以定位的布局
        <com.google.android.material.floatingactionbutton.FloatingActionButton
            android:id="@+id/fab"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="bottom|end"
            android:layout_margin="16dp"
            android:src="@drawable/ic_done"
            app:elevation="8dp"/>
  1. 在Activity中添加点击事件:
		fab.setOnClickListener {
            Toast.makeText(this,"悬浮按钮",Toast.LENGTH_SHORT).show()
        }
12.4.2 Snackbar
  1. Snackbar在Toast的基础上增加了动作,可以用来确认用户操作
        fab.setOnClickListener {
            Snackbar.make(it,"确认删除吗?",Snackbar.LENGTH_SHORT)
                .setAction("Undo"){
                    Toast.makeText(this,"已经删除",Toast.LENGTH_SHORT).show()
                }
                .show()
        }
12.4.3 CoodinatorLayout 协调布局
  1. 当Snackbar弹出时,挡住了悬浮按钮
    在这里插入图片描述
  2. 更改悬浮按钮所在布局为androidx.coordinatorlayout.widget.CoordinatorLayout
  3. CoodinatorLayout 就是一个加强版的FrameLayout,没有什么方便的定位方式
  4. 修改布局后,注意布局内各控件的位置属性是否需要修改
<androidx.coordinatorlayout.widget.CoordinatorLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        <androidx.appcompat.widget.Toolbar
            android:id="@+id/toolbar"
            android:layout_width="match_parent"
            android:layout_height="?attr/actionBarSize"
            android:background="@color/colorPrimary"
            android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
            app:popupTheme="@style/ThemeOverlay.AppCompat.Light" />
        <com.google.android.material.floatingactionbutton.FloatingActionButton
            android:id="@+id/fab"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="bottom|end"
            android:layout_margin="16dp"
            android:src="@drawable/ic_done"
            app:elevation="8dp"/>
    </androidx.coordinatorlayout.widget.CoordinatorLayout>

在这里插入图片描述

12.5 卡片式布局

12.5.1 MaterialCardView 卡片式布局
  1. MaterialCardView 布局适合与Adapter配合应用于图片展示,主要作用就是对每个item进行了美化
    app:cardCornerRadius属性:每个卡片的圆角的弧度
    app:elevation属性:按钮的高度值;高度越高,投影的范围越大,投影效果越淡。
  2. MaterialCardView 布局就是一个FrameLayout布局,没有什么方便的定位方式,所以内部需要再嵌套一个布局,方便放置控件
<com.google.android.material.card.MaterialCardView 
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_margin="5dp"
    app:cardCornerRadius="4dp"
    app:elevation="8dp">
    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">
		.
		.
		.
    </RelativeLayout>
</com.google.android.material.card.MaterialCardView>
  1. 在示例中还引入Glide库:implementation 'com.github.bumptech.glide:glide:4.9.0'
    Glide库是一个图片加载库,可以加载本地图片、网络图片、GIF图片甚至是本地视频
  2. 使用示例:Glide.with(context).load("R.drawable.id").into(ImageView)
    首先调用Glide.with()方法并传入一个ContextActivityFragment参数,然后调用load()方法加载图片,可以是url地址,也可以是本地路径或者是资源id,最后调用into()方法将图片设置到具体某一个ImageView控件中就可以了
    在这里插入图片描述
12.5.2 AppBarLayout
  1. 作用:效果如上图,可以看到图片遮蔽了导航栏,使用AppBarLayout解决这个问题
  2. com.google.android.material.appbar.AppBarLayout布局包裹Toolbar
        <com.google.android.material.appbar.AppBarLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content">
            <androidx.appcompat.widget.Toolbar
                android:id="@+id/toolbar"
                .
                .
             	/>
        </com.google.android.material.appbar.AppBarLayout>

在这里插入图片描述

  1. 这下反过来了,导航栏遮蔽了图片,需要给RecyclerView增加属性app:layout_behavior="@string/appbar_scrolling_view_behavior"
        <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/recyclerView"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            app:layout_behavior="@string/appbar_scrolling_view_behavior"/>

在这里插入图片描述
这次看起来完美了

  1. AppbarLayout还给带来了其他的滚动效果,在Toolbar增加属性 app:layout_scrollFlags="scroll|enterAlways|snap",就能实现上拉隐藏导航栏,下拉显示导航栏的效果
    scroll:当RecyclerView向上滚动的时候,Toolbar会跟着一起向上滚动实现隐藏
    enterAlways:当RecyclerView向下滚动的时候,Toolbar会跟着一起向下滚动并重新显示
    snap:当Toolbar还没有完全隐藏的时候,根据当前滚动的距离,自动选择是隐藏还是显示
            <androidx.appcompat.widget.Toolbar
                android:id="@+id/toolbar"
                .
                .
                app:layout_scrollFlags="scroll|enterAlways|snap"/>

12.6下拉刷新

  1. 添加依赖implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.0.0'
  2. SwipeRefreshLayout布局使得RecyclerView有了下拉刷新的功能
        <androidx.swiperefreshlayout.widget.SwipeRefreshLayout
            android:id="@+id/swipeRefresh"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            app:layout_behavior="@string/appbar_scrolling_view_behavior">
            <androidx.recyclerview.widget.RecyclerView
                android:id="@+id/recyclerView"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                app:layout_behavior="@string/appbar_scrolling_view_behavior"/>
        </androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
  1. 在Activity中,增加下拉刷新的功能代码
        //设置刷新进度条颜色
        swipeRefresh.setColorSchemeResources(R.color.colorPrimary)
        //设置下拉监听
        swipeRefresh.setOnRefreshListener {
        	//开始刷新显示进度条
        	swipeRefresh.isRefreshing = true
			//刷新Adapter
			.
			.
            //刷新完成关闭进度条
            swipeRefresh.isRefreshing = false
        }

12.7 可折叠式标题栏

12.7.1 CollapsingToolbarLayout 折叠标题栏
  1. 使用CollapsingToolbarLayout布局是不能独立存在的,必须是AppBarLayout的子布局,而AppBarLayout必须是CoordinatorLayout的子布局
  2. app:contentScrim="@color/colorPrimary"属性:折叠之后的背景色
  3. app:layout_scrollFlags="scroll | exitUntilCollapsed"属性:
    scorllCollapsingToolbarLayout布局会随着滚动而一起滚动
    exitUntilCollapsedCollapsingToolbarLayout布局随着滚动完成折叠之后留在界面上
<!---->
<!--删除了不重要的代码-->
<!--CoordinatorLayout协调布局:加强版的FrameLayout,没有什么定位方式-->
<!--整个页面分为两个部分,可折叠的标题栏和可滚动的内容-->
<androidx.coordinatorlayout.widget.CoordinatorLayout>
	<!--AppBarLayout,内部包裹的是整个页面的头部-->
    <com.google.android.material.appbar.AppBarLayout
        android:id="@+id/appBar">
        <!--1.CollapsingToolbarLayout必须嵌套在上面两个布局内-->
		<!--2.CollapsingToolbarLayout包含两个控件,imageView和Toolbar-->
		<!--3.当CollapsingToolbarLayout没有折叠的时候,页面是内部两个控件共同作用显示的效果,
		在折叠之后就只有Toolbar的效果了-->
		<!--4.折叠之后只有Toolbar,而app:contentScrim属性就是折叠之后Toolbar背景色-->
        <com.google.android.material.appbar.CollapsingToolbarLayout
            android:id="@+id/collapsingToolbar"
            android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
            app:contentScrim="@color/colorPrimary"
            app:layout_scrollFlags="scroll|exitUntilCollapsed">
            <!--ImageView控件设置app:layout_collapseMode="parallax"属性
            表示在CollapsingToolbarLayout折叠的过程中产生一定的视觉效果,纯粹为了好看-->
            <ImageView
                android:id="@+id/fruitImageView"
                android:scaleType="centerCrop"
                app:layout_collapseMode="parallax" />
            <!--app:layout_collapseMode="pin"属性,保证在折叠完成以后,toolbar不变,
            如果属性值设为parallax,则标题栏中的返回按钮会随着折叠而消失-->
            <androidx.appcompat.widget.Toolbar
                android:id="@+id/toolbar"
                app:layout_collapseMode="pin" />
    </com.google.android.material.appbar.CollapsingToolbarLayout>
    </com.google.android.material.appbar.AppBarLayout>
    <!--页面的下半部分:可滚动的内容-->
    <!--1.NestedScrollView是滚动ScrollView的升级版,增加了嵌套响应滚动事件的功能。-->
    <!--如使用ScrollView,则不会触发滚动事件,折叠栏标题不会折叠-->
    <!--2.因为整个页面的布局为FrameLayout没有什么可定位的方式,
    app:layout_behavior属性保证了该布局在appbar布局下面-->
    <!--3.NestedScrollView布局下只能由一个子布局,增加了LinearLayout的子布局-->
    <androidx.core.widget.NestedScrollView
        app:layout_behavior="@string/appbar_scrolling_view_behavior">
        <LinearLayout>
            <!--MaterialCardView卡片式布局,单纯为了美观,让布局由一个卡片的造型,四个角为圆弧-->
            <com.google.android.material.card.MaterialCardView
                app:cardCornerRadius="4dp">
                <TextView
                    android:id="@+id/fruitContentText"/>
            </com.google.android.material.card.MaterialCardView>
        </LinearLayout>
    </androidx.core.widget.NestedScrollView>
    <!--FloatingActionButton悬浮按钮-->
    <!--1.app:layout_anchor="@id/appBar"属性,称为锚,决定这个悬浮按钮显示在哪个布局中-->
    <com.google.android.material.floatingactionbutton.FloatingActionButton
        app:layout_anchor="@id/appBar"
        app:layout_anchorGravity="bottom|end"/>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
12.7.2 充分利用系统状态栏空间
  1. 在style文件中增加状态栏透明主题:
    <style name="transparentTheme" parent="AppTheme">
        <!-- Customize your theme here. -->
        <!-- 属性:状态栏,颜色:透明 -->
        <item name="android:statusBarColor">@android:color/transparent</item>
    </style>
  1. 设置AndroidManifest应用该透明主题
        <activity
            android:name=".****Activity"
            android:theme="@style/transparentTheme">
        </activity>
  1. 在布局中添加android:fitsSystemWindows="true"属性,可以使得有此属性的布局可以显示在系统状态栏中。
    在这里插入图片描述

第13章 Jetpack

13.2 ViewModel

个人理解:实现原理很简单,就是创建了一个类来保存Activity中的数据,假如只创建一个普通的类保存数据,那么这个类会随着Activity的生命周期而创建和销毁,而使用ViewModelProvider来获取ViewModel的实例,实例就不会随着Activity的生命周期销毁或者新建。其实这个功能使用单例类也可以实现。

  • 作用: Activity的任务太重了,既要负责逻辑处理,控制UI,还要处理网络请求。
    ViewModel可以帮助Activiyt分担一部分工作,专门用户存放与用户界面相关的数据
  • 实际应用:Activity在横竖屏切换时需要重新onCteate,数据就会丢失,将界面相关的变量存入ViewModel就可以完全避免这些问题
  1. 创建继承ViewModel的类
  2. 将类通过ViewModelProvider创建ViewModel的实例,将Activity中的数据保存在实例中
//添加依赖
implementation "androidx.lifecycle:lifecycle-extensions:2.2.0"
//继承ViewModel
class ViewModelClass(): ViewModel() {
    var counter = 0
}
//Activity
		lateinit var viewModelClass: ViewModelClass
		
        viewModelClass = ViewModelProvider(this).get(ViewModelClass::class.java)
        plus_bt.setOnClickListener {
            viewModelClass.counter ++
        }
    }
  • 使用工厂类来创建ViewModel

作用:可以传递参数


class ViewModelClass(countReserved: Int): ViewModel() {
    var counter = countReserved
}
class ViewModelFactory(private val countReserved: Int): ViewModelProvider.Factory{
    override fun <T : ViewModel?> create(modelClass: Class<T>): T {
        return ViewModelClass(countReserved) as T
    }
}
//Activity
lateinit var viewModelClass: ViewModelClass

viewModelClass = ViewModelProvider(this, 
ViewModelFactory(countReserved)).get(ViewModelClass::class.java)

13.3 Lifecycle生命周期

作用:感知Activity的生命周期,当Activity的生命周期由变化时执行相应的方法

  1. 创建实现了LifecycleObserver的类(生命周期观察者)
  2. Activity和是Fragment是继承了LifecycleOwner(生命周期拥有者),直接使用lifecycle.addObserver(myObserver)在拥有者中增加观察者就完成了
  3. 观察者类MyObserev中的lifecycle属性,会主动提供拥有者的生命周期。

观察者类中的Lifecycle属性:
这个属性不是必须的,可有可无。
没有此属性,只有Activity中的生命周期变化了,观察者才会执行相应方法,是被动接收。
有此属性,观察者时刻都可以通过该属性的lifecycle.currentState来获得被观察者的状态,是主动获取。

class MyObserver(val lifecycle: Lifecycle): LifecycleObserver {
    fun getState(): Lifecycle.State {
        return lifecycle.currentState
    }
    @OnLifecycleEvent(Lifecycle.Event.ON_START)
    fun activityStart(){
        Log.d("yuelei", "MyObserver_activityStart: ON_START")
    }
    @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
    fun activityPause(){
        Log.d("yuelei", "MyObserver_activityPause: ON_PAUSE")
    }
    @OnLifecycleEvent(Lifecycle.Event.ON_ANY)
    fun activityAny(){
        Log.d("yuelei", "MyObserver_activityAny: ON_ANY")
    }
}
//Activity
val myObserver = MyObserver(lifecycle)
lifecycle.addObserver(myObserver)

13.4 LiveData实时数据

  • 概念: LiveData是一种响应式编程组件,它可以包含任何数据的类型,并在数据发生变化的时候通知给观察者,通常与ViewModel结合在一起使用
  1. 创建ViewModel类及其工厂类(工厂类单纯为了传递参数)
  2. ViewModel类中的某一属性定义为MutableLiveData<T>()可变的实时数据类型,并且指定其泛型。
  3. 对实时数据的读写方法,get、set、put(非主线程赋值使用)
class LiveDataViewModel(num: Int): ViewModel() {
    val counter = MutableLiveData<Int>()
    init {
        counter.value = num
    }
    fun plusOne(){
        counter.value = (counter.value?:0) + 1
    }
    fun clear(){
        counter.value = 0
    }
}

class LDViewModelFactroy(val num: Int): ViewModelProvider.Factory {
    override fun <T : ViewModel?> create(modelClass: Class<T>): T {
        return LiveDataViewModel(num) as T
    }
}
  1. 在Activity中设置该属性的观察者observe,当数据又变动时,会触发观察者执行代码

observe方法
第一参数是生命周期的拥有者(lifecycleOwner),在Activity中就是Activity本身
第二个参数是Observer接口,当LiveData数据发生变化时,会触发接口中的操作。

		lateinit var viewModel: LiveDataViewModel
		
        viewModel = ViewModelProvider(this, 
        		LDViewModelFactroy(0)).get(LiveDataViewModel::class.java)
        plus_bt.setOnClickListener {
            viewModel.plusOne()
        }
        clear_bt.setOnClickListener {
            viewModel.clear()
        }
        viewModel.counter.observe(this,
            Observer<Int> { textView.text = it.toString() })
  • 官方建议写法:
  1. 被观察的数据设置为私有的
  2. 定义一个新的变量为LiveData类型,get()方法返回被观察的实例
class LiveDataViewModel(num: Int) : ViewModel() {
    val counter: LiveData<Int>
        get() = _counter
    private val _counter = MutableLiveData<Int>()

    init {
        _counter.value = num
    }

    fun plusOne() {
        _counter.value = (_counter.value ?: 0) + 1
    }

    fun clear() {
        _counter.value = 0
    }
}
13.4.2 map和swithMap

总结:用哪一个映射方法,取决于第二个参数的来源,如果是ViewModel中定义的就用map,如果是外部来源就用swithmap

  • map映射
    作用:通过对MutableLiveData数据的修改触发map转换函数,转换后的数据就是提供给观察者的数据

Transformations.map方法有两个参数:

  1. MutableLiveData数据
  2. 转换后的数据,给观察者提供观察者想要的数据。
  3. map方法中第二个参数的it,就是第一个参数的value

个人理解:
触发数据----------------中间人map----------------观察者
中间人:通过触发数据被触发时,给观察者提供观察者需要的数据

  1. 触发数据:中间人的开关。(定义实时数据为私有的并且是可变的:private val 实时数据= MutableLiveData<实时数据的类型>()
  2. 中间人:触发数据发生变化时,map会监听到变化,并执行转换函数中的逻辑,再将转换后的数据通知给观察者val 中间人: LiveData<(给观察者的数据)的数据类型> = Transformations.map( 触发数据,{ 给观察者的数据 } )
  3. 观察者:userViewModel.中间人.observe(this, Observer{ 中间人提供给观察者的数据 -> 观察者根据中间人提供的数据而做出的动作})
data class User(var firstName: String,var lastName:String,var age: Int)
class UserViewModel:ViewModel() {
    private val userLiveData = MutableLiveData<User>()
    val userName: LiveData<String> = Transformations.map(userLiveData){
        "${it.firstName} ${it.lastName}"
    }
    fun changeName(){
        userLiveData.value =  User("xiao" ,"mei")
    }
}
//Activity
lateinit var userViewModel: UserViewModel
userViewModel = ViewModelProvider(this).get(UserViewModel::class.java)

userViewModel.userName.observe(this, Observer{
            Log.d("ViewModel", "LiveDataActivity_onCreate1: $it")
        })
  • swithMap 转换映射
    基本与Map相同
    区别:switchMap的第二个参数是ViewModel之外定义的LiveData
    当中间人转换后的数据的LiveData是外部提供的,就需要用swithMap
    不懂的地方,返回给观察着的是一个LiveData类型,但是观察者是怎么把LiveData变成User类型的
object Repostory{
    fun getUser(userId: String): LiveData<User>{
        val liveData = MutableLiveData<User>()
        liveData.value = User(userId,userId,0)
        return liveData
    }
}
class UserSwithMap:ViewModel(){
    private val userIdLiveData = MutableLiveData<String>()
    val user = Transformations.switchMap(userIdLiveData) {
            userId -> Repostory.getUser(userId)}
    fun getUserId(userId: String){
        userIdLiveData.value = userId
    }
}
		lateinit var userSwithMap: UserSwithMap

 		userSwithMap = ViewModelProvider(this).get(UserSwithMap::class.java)
 		
        userSwithMap.user.observe(this, Observer {
            livedata_tv.text = it.firstName
        })
        
        switchMap_bt.setOnClickListener {
            var userId = (0..100).random().toString()
            userSwithMap.getUserId(userId)
        }
  • LiveData与lifecycle的关系:
  1. 当Activity处于不可见状态的时候,如果LiveData中的数据发生了变化,是不会通知观察者的。只有当Activity重新恢复可见状态时,才会将数据通知观察者。
  2. 如果在不可见状态发生了很多次数据变化,LiveData会将最新的那份数据通知观察者,之前的数据都会被丢掉。
在实际使用中的LiveData

liveData()函数:

  1. 可指定线程
  2. 返回一个LiveData对象,
  3. 在它的代码块中可以执行挂起函数

在此例中的Result.successResult.failure应用的非常巧妙,首先是返回值,在示例中除了Result.failure(RuntimeException)是返回一个异常外,另外两个Result的返回值是相同的类型,Result.failure<List>(e)是指定了返回值的类型,Result.success(places)则places本身就是List类型。

liveData(Dispatchers.IO){//即可以返回一个LiveData对象,有在此代码中可以执行挂起函数
	val result =  
	//这三个Result使用的非常巧妙
		try{
			if (success)Result.success(T)
			else Result.failure(RuntimeException("异常原因"))
		}catch(e :Exception){
			Result.failure<T>(e)
		}
		emit(result)//与liveData配合使用
}


object Repository {
    fun searchPlaces(query: String) = liveData(Dispatchers.IO) {
        val result = try {
        //执行Retrofit的网络请求
            val placeResponse = SunnyWeatherNetWork.searchPlaces(query)
            //请求返回的数据中status为1则表示请求成功
            if (placeResponse.status == "1"){
                val places = placeResponse.districts
                //使用Result.success这种方式返回
                Result.success(places)
            }else{
                Result.failure(RuntimeException("response status is ${placeResponse.status}"))
            }
        }catch (e :Exception){
            Result.failure<List<District>>(e)
        }
        //liveData无法返回数据,使用emit与其配合,强制返回数据
        emit(result)
    }
}
class PlaceViewModel:ViewModel() {
	//通过seachPlace方法,导致searchLiveData的value值发生变化
    private val searchLiveData = MutableLiveData<String>()
    //当searchLiveData的value值发生变化后,执行代码块中的代码
    //代码块中的it就是searchLiveData.value
    val placeLiveData = Transformations.switchMap(searchLiveData){
        Repository.searchPlaces(it)
    }
    fun seachPlace(query: String){
        searchLiveData.value = query
    }
   //用于保存Activity界面的数据,当屏幕反转的时候不会导致数据丢失
    val placeList = ArrayList<District>()
}

13.5 Room数据库存储

  • 添加依赖:
apply plugin: 'kotlin-kapt'
dependencies {
	implementation "androidx.room:room-runtime:2.1.0"
	kapt "androidx.room:room-compiler:2.1.0"
}
  • Room的3个组成部分:
  1. Entity。用于定义封装的实体类,每个实体类对应一张表,表中的列与实体类的字段对应。
  2. Dao。数据访问对象,对数据库的各种操作都是由Dao操作的。
  3. Database。定义数据库,包括数据库的版本号、包含的实体类、Dao。

Room操作数据库相对比较简单,从Dao的操作可以看出,Room就是面向对象的数据库,它将每一条数据都看做是Entity类的实例对象。

//Room的3大组成部分,各种注释定义数据库
@Entity
data class UserData(val name: String,val age: Int,val gender:String) {
    @PrimaryKey(autoGenerate = true)
    var id :Long = 0
}

@Dao
interface UserDao{
    @Insert
    fun insertUser(user: UserData):Long
    @Update
    fun updataUser(newUser: UserData)
    @Query("select * from UserData")
    fun loadAllUser(): List<UserData>
    @Query("select * from UserData where age> :age")
    fun loadUserOldrThan(age: Int): List<UserData>
    @Delete
    fun deleteUserData(user: UserData)
    @Query("delete from UserData where gender=:gender")
    fun deleteUserByGender(gender: Boolean)
}

@Database(version = 1,entities = [UserData::class])
abstract class AppDatabase : RoomDatabase(){
    abstract fun UserDao(): UserDao
    companion object{
        private var instance :AppDatabase? = null
        @Synchronized
        fun getDatabase(comtext: Context):AppDatabase{
            instance?.let {
                return it
            }
            return Room.databaseBuilder(comtext.applicationContext,
                AppDatabase::class.java,"app_database.db")
                .build().apply{
                    instance = this
                }
        }
    }
}
  • 所以数据库操作不能在主线程执行
//Activity
//创建数据库并拿到Dao就可以对数据库各种操作了
val userDao = AppDatabase.getDatabase(this).UserDao()

13.6 WorkManager定时任务

  • 作用:后台权限逐步收紧,很多任务无法执行,产生了定时任务工具WorkManager
  • 注意:WorkManager定时任务不一定会准时执行。系统为了减少cpu的唤醒次数,有效延长电池的的使用时间,会把触发时间临近的几个任务放在一起执行。
  • 添加依赖
    implementation("androidx.work:work-runtime:2.2.0")
  • WorkManager用法:
  • 执行任务
  1. 定义一个后台任务,在dowork中实现具体的任务逻辑
  2. 配置该后台任务的,约束信息和运行条件,例如:执行一次、循环执行、间隔时间等
  3. 将后台任务传入WorkManagerequeue()方法中,系统会在合适的时间运行
1. 定义一个后台任务,在dowork中实现具体的任务逻辑
class SimpleWorker(context: Context,params: WorkerParameters): Worker(context,params) {
    //三种返回结果
    override fun doWork(): Result {
        Log.d("yuelei", "SimpleWorker_doWork")
        return Result.success() //成功
//        return Result.failure() //失败
//        return Result.retry() //失败,结合setBackoffCriteria使用,重新执行
    }
}
 2. 配置该后台任务的,约束信息和运行条件,例如:执行一次、循环执行、间隔时间等
//只执行一次OneTimeWorkRequest
val request = OneTimeWorkRequest.Builder(SimpleWorker::class.java)
				.build()
//周期性运行PeriodicWorkRequest,间隔不少于15分钟
val request = PeriodicWorkRequest.Builder(SimpleWorker::class.java,
				15, TimeUnit.MINUTES)
				.build()
//延迟运行
val request = OneTimeWorkRequest.Builder(SimpleWorker::class.java)
                .setInitialDelay(5,TimeUnit.MINUTES)
                .build()
 3. 将后台任务传入`WorkManager`的`equeue()`方法中,系统会在合适的时间运行         
WorkManager.getInstance(this).enqueue(request)
  • 取消任务

通过标签或者id取消任务

//通过addTag方法给任务设置标签
val request = OneTimeWorkRequest.Builder(SimpleWorker::class.java)
	.addTag("simple")
	.build()
//通过运行对象id取消任务
WorkManager.getInstance(this).cancelWorkById(request.id)
//取消所有持有此标签的任务
WorkManager.getInstance(this).cancelAllWorkByTag("simple")
  • 任务返回Result.retry()的结果时

返回Result.retry(),主要是setBackoffCriteria的参数设置
setBackoffCriteria

  1. 第二个和第三个参数用于指定在多长时间后第一次重试,时间不得少于10秒
  2. 第一个参数用于指定任务重试失败后,到下次执行的间隔时间,可选值有两种:
    BackoffPolicy.LINEAR,间隔时间线性增长
    BackoffPolicy.EXPONENTIAL,间隔时间指数级增长
 /*
* 后台任务doWork()返回Result.retry()时,结合setBackoffCriteria使用
* */
val request = OneTimeWorkRequest.Builder(SimpleWorker::class.java)
				.setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 10 ,TimeUnit.SECONDS)
				.build()
WorkManager.getInstance(this).enqueue(request)
  • 任务返回Result.success()和Result.failure()的结果时

可以通过id观察任务的执行结果:getWorkInfoByIdLiveData
也可以通过Tag观察任务的执行结果:getWorkInfoByTagLiveData

//doWork返回Result.failure()和Result.success()时有什么作用
//通过LiveData的observe观察返回的结果
val request = OneTimeWorkRequest.Builder(SimpleWorker::class.java)
			.addTage("simple")
			.build()

WorkManager.getInstance(this).getWorkInfoByIdLiveData(request.id).observe(this, Observer {
       when(it.state){
           WorkInfo.State.SUCCEEDED -> Log.d(
                        "",
                        "ThirteenWorkManagerActivity_onCreate:SUCCEEDED "
                    )
           WorkInfo.State.FAILED -> Log.d(
                        "yuelei",
                        "ThirteenWorkManagerActivity_onCreate: FAILED"
                    )
      }
})
  • 链式反应

需要多个任务配合来完成工作

//链式反应
val first = OneTimeWorkRequest.Builder(SimpleWorker::class.java).build()
val second = OneTimeWorkRequest.Builder(SimpleWorker::class.java).build()
val third = OneTimeWorkRequest.Builder(SimpleWorker::class.java).build()
WorkManager.getInstance(this)
        .beginWith(first)
        .then(second)
        .then(third)
        .enqueue()

第14章进阶,高级技巧

14.1 全局获取Context的技巧

  1. 创建一个MyApplication类,继承Application
class MyApplication: Application() {
    companion object{
        lateinit var context: Context
    }
    override fun onCreate() {
        super.onCreate()
        context = applicationContext
    }
}
  1. 修改AndroidManifest.xml代码
<application
		.
		.
        android:name=".FourteenChapter.MyApplication">
  1. 在项目的任何地方都可以通过MyApplication.context得到context

14.2 使用Intent传递对象

作用:传递自定义的类对象等

14.2.1 Serializable序列化方式
  1. 类继承Serializable
  2. 将类对象通过Intent发送
  3. 接收对象,反序列化,向下转型
class Person(val name:String,val age: Int): Serializable
//发送
val intent = Intent(this, SecondActivity::class.java)
intent.putExtra("person",Person("tom",10))
startActivity(intent)
//接收
 val serializableExtra = intent.getSerializableExtra("person") as Person
14.2.2 Parcelable可包装方式
  1. 类中的数据必须封装在主构造函数中
  2. 接收对象
@Parcelize
class Person(val name:String,val age: Int): Parcelable
//发送
val intent = Intent(this, SecondActivity::class.java)
intent.putExtra("person",Person("tom",10))
startActivity(intent)
//接收
val person = intent.getParcelableExtra<Person>("person") as Person

14.3 定制自己的日志工具

object LogUtli {
    private const val VERBOSE = 1
    private const val DEBUG = 2
    private const val INFO = 3
    private const val WARN = 4
    private const val ERROR = 5
    //自定义打印级别
    private const val level = VERBOSE
    fun v(tag: String, msg: String){
        if (level <= VERBOSE){
            Log.v(tag,msg)
        }
    }
    fun d(tag: String, msg: String){
        if (level <= DEBUG){
            Log.d(tag,msg)
        }
    }
    fun i(tag: String, msg: String){
        if (level <= INFO){
            Log.i(tag,msg)
        }
    }
    fun w(tag: String, msg: String){
        if (level <= VERBOSE){
            Log.v(tag,msg)
        }
    }
    fun e(tag: String, msg: String){
        if (level <= VERBOSE){
            Log.e(tag,msg)
        }
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值