Android开发补充内容
- fragment
- Okhttp
- Retrofit
- RxJava
- Hilt
- 组件库
-
- [Material Components](#Material Components)
- [Jetpack Compose](#Jetpack Compose)
fragment
通信
fragment
于activity
通信的一种原生方法是使用Bundle
:
java
Bundle bundle = new Bundle();
bundle.putString('msg', "数据");
BlankFragment bf = new BlankFragment();
bf.setArguments(bundle);
生命周期
- 第一次显示:onAttach -> onCreate -> onCreateView -> onActivityCreated -> onStart -> onResume
- 按home:onPause -> onStop(界面显示的fragment不变)
- 重新显示:onStart -> onResume
- 按返回:onPause -> onStop -> onDestroyView -> onDestroy -> onDetach
使用了FragmentTransaction
的addToBackStack
方法
- 切换fragment:onPause -> onStop -> onDestroyView(但没销毁时,同栈切换)
- 返回原fragment:onCreateView -> onActivityCreated -> onStart -> onResume(同栈返回)
Okhttp
基本使用
模块级的build.gradle
导入依赖:
kotlin
implementation 'com.squareup.okhttp3:okhttp:4.9.1'
创建客户端:
kotlin
client = OkHttpClient.Builder()
.addInterceptor {
val request = it.request()
val response = it.proceed(request)
Log.d("biluo", request.url.toString())
response
}
.connectTimeout(20, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.build()
创建和使用请求:
kotlin
val request = Request.Builder().url("http://localhost/user/list").build()
// 阻塞方式
val response = client.newCall(request).execute()
// 异步方式
val response = client.newCall(request).enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
TODO("Not yet implemented")
}
override fun onResponse(call: Call, response: Response) {
TODO("Not yet implemented")
}
})
websocket
kotlin
val observable = Observable.create<String> { emitter ->
val request = Request.Builder().url("${AppConstant.WS_URL}/${user.username}").build()
val ws = client.newWebSocket(request, object : WebSocketListener() {
override fun onOpen(webSocket: WebSocket, response: Response) {
wsObservable.ws = webSocket
Log.d("biluo", "socket连接成功")
}
override fun onMessage(webSocket: WebSocket, text: String) {
Log.d("biluo", "接收到消息:$text")
emitter.onNext(text)
}
override fun onClosing(webSocket: WebSocket, code: Int, reason: String) {
Log.d("biluo", "socket连接准备断开")
}
override fun onClosed(webSocket: WebSocket, code: Int, reason: String) {
wsObservable.ws = null
Log.d("biluo", "socket连接已经断开")
//emitter.onComplete()
}
override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
Log.e("biluo", "异常:${t.message}", t)
emitter.onError(t)
}
})
emitter.setDisposable(object : Disposable {
override fun dispose() {
Log.d("biluo", "dispose...")
ws.close(1000, "Closing by dispose")
}
override fun isDisposed(): Boolean {
return ws.close(1000, null)
}
})
}
wsObservable.observable = observable
return wsObservable
Retrofit
基于OkHttp,同时支持了RESTful API风格设计
基本使用
模块级的build.gradle
导入依赖:
kotlin
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
implementation 'com.squareup.retrofit2:adapter-rxjava3:2.9.0'
创建Retrofit实例:
添加了GsonConvert后,会自动把响应回来的Json字符串转换为对象。
kotlin
val retrofit = Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJava3CallAdapterFactory.create())
.client(client)
.build()
定义Api类:
kotlin
interface UserApi {
@GET("$USER_URL/{username}")
fun getByUsername(@Path("username") username: String): Call<MsgResult<User>>
@POST("$USER_URL/register")
fun register(@Body user: User): Call<MsgResult<Nothing>>
}
创建Api类:
kotlin
UserApi = retrofit.create(UserApi::class.java)
RxJava
可以结合Retrofit一起使用。
基本使用
前提:依赖在Retrofit中已经导入,并创建了RxJavaConvert到Retrofit中。
之前定义Api的返回值可以改为RxJava提供的类型。
它提供了:
-
Observable:流式数据类型,用于访问多个项的异步序列。
-
Flowable:与Observable类似,支持背压(backpressure)机制,即当数据生产速度超过消费速度时,能够控制数据的流动。
-
Single:单发数据类型,只能且必须发射一个数据。
-
Maybe:单发数据类型,可以发射零个或一个数据。
-
Completable:不发射任何数据,只通知流的结束。
之前的UserApi类变为:
kotlin
interface UserApi {
@GET("$USER_URL/{username}")
fun getByUsername(@Path("username") username: String): Single<MsgResult<User>>
@POST("$USER_URL/register")
fun register(@Body user: User): Single<MsgResult<Nothing>>
}
使用:
kotlin
val disposable: Disposable = single.subscribeOn(Schedulers.io()).observeOn(AndroidScheduler.mainScheduler)
.subscribe({ res ->
//成功获取结果res,做什么
}, { e ->
//出现异常后做些什么
})
Retrofix下的RxJava没有AndroidScheduler类,可以自己定义一个:
kotlin
object AndroidScheduler : Executor {
val mainScheduler: Scheduler = Schedulers.from(this)
private val handler: Handler = Handler(Looper.getMainLooper())
override fun execute(command: Runnable) {
handler.post(command)
}
}
定时任务
kotlin
val disposable = Observable.interval(15L, TimeUnit.MINUTES)
.subscribe({ _ ->
val json = Gson().toJson(Message("ping", null, appUser.username))
wsObservable.ws?.send(json)
Log.d("biluo", "发送了一个心跳包")
}) { t ->
Log.e("biluo", "异常:", t)
wsObservable.ws?.close(1000, "心跳续约失败")
}
Hilt
可以进行实例的管理,并进行依赖注入。类似spring IOC
项目级build.gradle
导入插件:
kotlin
id 'com.google.dagger.hilt.android' version '2.51.1' apply false
模块级build.gradle
导入插件:
kotlin
id 'kotlin-kapt'
id 'dagger.hilt.android.plugin'
模块级build.gradle
导入依赖:
kotlin
implementation 'com.google.dagger:hilt-android:2.51.1'
kapt 'com.google.dagger:hilt-compiler:2.51.1'
基本使用
参考教程:https://blog.csdn.net/Mr_Tony/article/details/124516871
使用Hilt
必须要先用@HiltAndroidApp
绑定Application
。
哪个类需要进行依赖注入,就得加上@AndroidEntryPoint
进行绑定,然后注入的地方需要使用@Inject
。
@HiltAndroidApp
:用于标注Application
类,触发 Hilt 的代码生成操作,生成应用级别的组件。
@AndroidEntryPoint
:用于标注 Android 组件(如Activity
、Fragment
等),告知 Hilt 这些组件可以接收依赖注入。
@Inject
- 用于标注需要注入的依赖项,可以是字段、构造函数或方法。
- 对于字段,Hilt 会自动注入相应的依赖;对于构造函数,Hilt 会使用它来创建类的实例
注:
@AndroidEntryPoint
不仅仅可以绑定到Activity,还可以绑定到其他地方,请参考:https://developer.android.google.cn/training/dependency-injection/hilt-android
kotlin
@HiltAndroidApp
class App: Application()
// 类似spring的 @Component
class DateFormatter @Inject constructor() {
fun testDateFormatter(){
Log.e("YM--->","---->获取DateFormatter")
}
}
// 类似spring在类中进行依赖注入也需要当前类被spring管理
@AndroidEntryPoint
class HiltActivity : AppCompatActivity() {
@Inject // 类似spring的 @Autowire
lateinit var dateFormatter: DateFormatter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_hilt)
dateFormatter.testDateFormatter()
}
}
进阶使用
@Module
:用于标注提供依赖项的类。@Provides
:用于标注Module
类中的提供依赖项的方法,Hilt 会在需要时调用这些方法。@Binds
:用于在Module
类中绑定接口和实现类,告知 Hilt 在需要提供接口的实例时要使用哪种实现。@InstallIn
:用于标注Module
或EntryPoint
,指定其作用范围。@Singleton
:用于标注提供的依赖项为单例,即在整个应用程序的生命周期内只存在一个实例。@HiltViewModel
- 用于标注
ViewModel
类,使其可以使用 Hilt 进行依赖注入。 - 注意:
ViewModel
不能直接使用@Inject
注解,需要使用@HiltViewModel
。
- 用于标注
@ActivityScoped
:用于标注提供的依赖项为 Activity 级别的作用域,即依赖项的生命周期与 Activity 相同。
补充:
@HiltViewModel
用于标注
ViewModel
类,使其可以使用 Hilt 进行依赖注入。注意:
ViewModel
不能直接使用@Inject
注解,需要使用@HiltViewModel
。
@ViewModelScoped
:用于标注提供的依赖项为 ViewModel 级别的作用域,即依赖项的生命周期与 ViewModel 相同。
@EntryPoint
:用于获取 Hilt 提供的实例,特别是在不能直接使用@AndroidEntryPoint
的类中(如ContentProvider
、BroadcastReceiver
)。
@Provides
举例:
kotlin
class HelloClass {
fun hello() = "Hello"
}
@Module
// 表示整个应用程序的生命周期内都是单例
@InstallIn(SingletonComponent::class)
object AppModule {
@Provides
@Singleton // 单例
fun provideSomeOtherClass(): HelloClass {
return HelloClass()
}
}
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@Inject
lateinit var helloClass: HelloClass
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
helloClass.hello()
}
}
@Binds
举例:在这个示例中,@Binds
注解用于将ApiServiceImpl
绑定到ApiService
接口上,这样当需要ApiService
的实例时,Hilt 会提供ApiServiceImpl
的实例。
kotlin
interface ApiService {
fun doSomething()
}
class ApiServiceImpl @Inject constructor() : ApiService {
override fun doSomething() {
println("Doing something in ApiServiceImpl")
}
}
@Module
@InstallIn(SingletonComponent::class)
abstract class ApiServiceModule {
@Binds
abstract fun bindApiService(apiServiceImpl: ApiServiceImpl): ApiService
}
@ActivityScope
例子:
kotlin
@Module
@InstallIn(ActivityComponent::class)
object ActivityModule {
@Provides
@ActivityScoped
fun provideActivityDependency(): ActivityDependency {
return ActivityDependency()
}
}
例子
Application类:
kotlin
@HiltAndroidApp
class MyApplication : Application() {}
模块类:
kotlin
@InstallIn(SingletonComponent::class)
@Module
class AppModule {
@Provides
@Singleton
fun okHttpClient(): OkHttpClient {
return OkHttpClient.Builder()
.addInterceptor {
val request = it.request()
val response = it.proceed(request)
Log.d("biluo", request.url.toString())
response
}
.connectTimeout(20, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.retryOnConnectionFailure(false)
.build()
}
@Provides
@Singleton
fun retrofit(client: OkHttpClient): Retrofit {
return Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJava3CallAdapterFactory.create())
.client(client)
.build()
}
@Provides
@Singleton
fun userApi(retrofit: Retrofit): UserApi = retrofit.create(UserApi::class.java)
}
使用:
kotlin
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@Inject
lateinit var userApi: UserApi
}
组件库
Material Components
Material Components for Android:Google 官方提供的 Material Design 组件库。
- 特点
- 完全遵循 Material Design 规范。
- 提供丰富的组件(如
Button
、CardView
、BottomNavigationView
等)。 - 与 AndroidX 库无缝集成。
- 适用场景:需要遵循 Material Design 规范的项目。
- GitHub:https://github.com/material-components/material-components-android
举例:BottomNavigationView------底部导航栏
-
编写菜单xml文件
bottom_nav_menu.xml
。xml<?xml version="1.0" encoding="utf-8" ?> <menu xmlns:android="http://schemas.android.com/apk/res/android"> <item android:id="@+id/nav_home" android:icon="@drawable/home" android:title="@string/tab1" /> <item android:id="@+id/nav_tab2" android:icon="@drawable/tab2" android:title="@string/tab2" /> <item android:id="@+id/nav_tb3" android:icon="@drawable/tb3" android:title="@string/tab3" /> </menu>
-
在相应的布局文件中引入BottomNavigationView,例如MainActivity。
xml<LinearLayout 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:orientation="vertical"> <androidx.fragment.app.FragmentContainerView android:id="@+id/fragment_container" android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1" /> <com.google.android.material.bottomnavigation.BottomNavigationView android:id="@+id/bottomNavigationView" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_gravity="bottom" android:background="@color/purple_200" app:menu="@menu/bottom_nav_menu"/> </LinearLayout>
-
准备好相应的
Fragment
,为BottomNavigationView
设置点击事件,根据点击的项切换Fragment
。kotlinclass MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) val bottomNavigationView = findViewById<BottomNavigationView>(R.id.bottom_navigation) val fragmentContainer = supportFragmentManager.beginTransaction() // 默认加载 HomeFragment fragmentContainer.replace(R.id.fragment_container, HomeFragment()) fragmentContainer.commit() bottomNavigationView.setOnItemSelectedListener { item -> when (item.itemId) { R.id.nav_home -> { supportFragmentManager.beginTransaction() .replace(R.id.fragment_container, HomeFragment()) .commit() true } R.id.nav_tb2 -> { supportFragmentManager.beginTransaction() .replace(R.id.fragment_container, NavTb2Fragment()) .commit() true } R.id.nav_tb3 -> { supportFragmentManager.beginTransaction() .replace(R.id.fragment_container, NavTb3Fragment()) .commit() true } else -> false } } } }
Jetpack Compose
Jetpack Compose Material:Jetpack Compose 的 Material Design 组件库。
- 特点
- 声明式 UI 开发。
- 提供 Material Design 风格的组件(如
Scaffold
、Button
、TextField
等)。 - 与 Jetpack Compose 生态完全兼容。
- 适用场景:使用 Jetpack Compose 开发的项目。
- 文档:Jetpack Compose 使用入门 | Android Developers (google.cn)
举例:
kotlin
@AndroidEntryPoint
class LoginActivity : AppCompatActivity() {
private lateinit var shared: SharedPreferences
@Inject lateinit var userApi: VehicleOwnerApi
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
shared = getSharedPreferences("user", MODE_PRIVATE)
val id = shared.getString("id", "")!!
val password = shared.getString("password", "")!!
setContent {
MaterialTheme { loginScreen(id, password) }
}
}
@Composable
fun loginScreen(originId: String, originPwd: String) {
val ID_REGEX = Regex("^[1-9]\\d{5}(18|19|20)\\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\\d|3[01])\\d{3}[\\dXx]$")
val PWD_REGEX = Regex("^[a-zA-Z\\d]{4,16}$")
var id by remember { mutableStateOf(originId) }
var password by remember { mutableStateOf(originPwd) }
var idErrorMsg by remember { mutableStateOf("") }
var pwdErrorMsg by remember { mutableStateOf("") }
var checked by remember { mutableStateOf(false) }
fun checkIdFormat(): Boolean {
idErrorMsg = if (id.isEmpty()) "身份证号不能为空" else {
if (!ID_REGEX.matches(id)) "身份证号格式错误" else ""
}
return idErrorMsg.isEmpty()
}
fun checkPwdFormat(): Boolean {
pwdErrorMsg = if (password.isEmpty()) "密码不能为空" else {
if (!PWD_REGEX.matches(password)) "密码必须由4-16位的字母或数字组成" else ""
}
return pwdErrorMsg.isEmpty()
}
fun login() {
if (!checkIdFormat() || !checkPwdFormat()) return
Utils.handleSingle(this, "token", userApi.login(VehicleOwner(id, password))) {
GlobalData.token = it.toString()
// 如果选择了记住用户,将身份证号和密码写入sharePreferences
if (checked) {
shared.edit().putString("id", id).apply()
shared.edit().putString("password", password).apply()
} else {
shared.edit().remove("id").apply()
shared.edit().remove("password").apply()
}
// 登录成功,跳转到主页
Utils.showToast(this, "登录成功")
val intent = Intent(this, MainActivity::class.java)
intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK
startActivity(intent)
}
}
Column(modifier = Modifier.padding(16.dp)) {
TextField( // 身份证输入框
value = id,
onValueChange = { id = it },
label = { Text("身份证号") },
placeholder = { Text("请输入身份证号") },
isError = idErrorMsg.isNotEmpty(),
trailingIcon = {
if (idErrorMsg.isNotEmpty()) { // 如果有错误提示,则显示错误图标
Icon(Icons.Filled.Error, contentDescription = "Error")
}
},
supportingText = { Text(idErrorMsg) }, // 显示在下方的错误提示
modifier = Modifier.fillMaxWidth()
/*.onFocusChanged { focusState -> // 进入页面时会直接显示错误提示,不友好;改为点击登录时再检查格式
if (!focusState.isFocused) {
idErrorMsg = if (id.isEmpty()) "身份证号不能为空" else {
if (!ID_REGEX.matches(id)) "身份证号格式错误" else ""
}
}
}*/
)
TextField( // 密码输入框
value = password,
onValueChange = { password = it },
label = { Text("密码") },
placeholder = { Text("请输入密码") },
isError = pwdErrorMsg.isNotEmpty(),
trailingIcon = {
if (pwdErrorMsg.isNotEmpty()) { // 如果有错误提示,则显示错误图标
Icon(Icons.Filled.Error, contentDescription = "Error")
}
},
supportingText = { Text(pwdErrorMsg) }, // 显示在下方的错误提示
modifier = Modifier.fillMaxWidth().padding(top = 4.dp)
)
Row(
verticalAlignment = Alignment.CenterVertically, // 行内元素垂直居中
horizontalArrangement = Arrangement.spacedBy((-6).dp), // 行内元素间距
modifier = Modifier.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = null // 移除点击效果
) { // 点击整个 Row 切换选中状态
checked = !checked
}
) {
Checkbox(checked = checked, onCheckedChange = { checked = it }) // 复选框
Text("记住用户") // 文本
}
Button( // 按钮
onClick = { login() },
modifier = Modifier.fillMaxWidth()
) {
Text("登 录")
}
}
}
}