[toc]
网址导航
Compose与Kotlin的兼容对应关系
Kotlin 预发布版本兼容的 Compose Compiler 版本
快速入门
Navigation
图片加载
Gradle国内镜像
动画
手势
Blur模糊效果
Scrcpy 手机投屏
网络请求
老方法
interface ApiServices{
@Headers(value = ["Content-type:application/json;charset=UTF-8"])
@POST
fun post(@Url url: String, @Body params: RequestBody): ApiCall<String>
@GET
fun get(@Url url: String): ApiCall<String>
}
object ApiNetwork{
private val service = ServiceCreator.create(ApiServices::class.java)
fun get(url: String){
service.get(url).enqueue(object : ApiCallback<String> {
override fun success(response: Response<String>?) {
super.success(response)
// String 结果进行处理
}
override fun error(response: Response<*>?, t: Throwable?) {
super.error(response, t)
}
override fun onComplete() {
}
})
}
}
object ServiceCreator {
fun <T> create(service: Class<T>): T = create().create(service)
private fun create(): Retrofit {
val okHttpClient = OkHttpClient().newBuilder()
if (BuildConfig.NetTest) {
okHttpClient.addInterceptor(HttpLoggingInterceptor().apply {
this.level = HttpLoggingInterceptor.Level.BODY
})
}
val build = okHttpClient
.retryOnConnectionFailure(true)
.writeTimeout(60, TimeUnit.SECONDS)
.connectTimeout(60, TimeUnit.SECONDS)
.readTimeout(60, TimeUnit.SECONDS)
.proxy(Proxy.NO_PROXY) // 有效避免抓包,请求不走任何协议, 未测试,谨慎使用
.build()
return Retrofit.Builder()
.client(build)
.baseUrl("https://www.google.com/")
.addCallAdapterFactory(ErrorHandlingCallAdapterFactory())
.addConverterFactory(ScalarsConverterFactory.create())
// .addConverterFactory(MoshiConverterFactory.create())
// .addConverterFactory(GsonConverterFactory.create())
.build()
}
}
interface ApiCallback<T> {
/** Called for [200, 300) responses. */
fun success(response: Response<T>?) {
}
/** Called for 401 responses. */
fun unauthenticated(response: Response<*>?) {
error(response)
}
/** Called for [400, 500) responses, except 401. */
fun clientError(response: Response<*>?) {
error(response)
}
/** Called for [500, 600) response. */
fun serverError(response: Response<*>?) {
error(response)
}
/** Called for network errors while making the call. */
fun networkError(e: Exception?) {
error(t = e)
}
/** Called for unexpected errors while making the call. */
fun unexpectedError(t: Throwable?) {
error(t = t)
}
fun error(response: Response<*>= null, t: Throwable= null) {
}
fun onComplete()
}
interface ApiCall<T> {
fun cancel()
fun enqueue(callback: ApiCallback<T>?)
fun clone(): ApiCall<T> // Left as an exercise for the reader...
fun isRunning(): Boolean
}
class ErrorHandlingCallAdapterFactory : CallAdapter.Factory() {
@Nullable
override fun get(
returnType: Type,
annotations: Array<Annotation>,
retrofit: Retrofit
): CallAdapter<*, *>{
if (getRawType(returnType) != ApiCall::class.java) {
return null
}
check(returnType is ParameterizedType) { "ApiCall must have generic type (e.g., ApiCall<ResponseBody>)" }
val responseType =
getParameterUpperBound(
0,
returnType
)
val callbackExecutor = retrofit.callbackExecutor()
return ErrorHandlingCallAdapter<Any>(
responseType,
callbackExecutor
)
}
private class ErrorHandlingCallAdapter<R> internal constructor(
private val responseType: Type,
private val callbackExecutor: Executor?
) :
CallAdapter<R, ApiCall<R>> {
override fun responseType(): Type {
return responseType
}
override fun adapt(call: Call<R>): ApiCall<R> {
return ApiCallAdapter(
call,
callbackExecutor
)
}
}
}
/** Adapts a [Call] to [ApiCall]. */
internal class ApiCallAdapter<T>(
private val call: Call<T>,
private val callbackExecutor: Executor?
) :
ApiCall<T> {
override fun cancel() {
call.cancel()
}
var isAsyncRunning = false
override fun enqueue(callback: ApiCallback<T>?) {
isAsyncRunning = true
call.enqueue(
object : Callback<T> {
override fun onResponse(
call: Call<T>,
response: Response<T>
) {
try {
when (response.code()) {
in 200..299 -> {
callback?.success(response)
}
401 -> {
callback?.unauthenticated(response)
}
in 400..499 -> {
callback?.clientError(response)
}
in 500..599 -> {
callback?.serverError(response)
}
else -> {
callback?.unexpectedError(RuntimeException("Unexpected response $response"))
}
}
} catch (e: Exception) {
}
try {
callback?.onComplete()
} catch (e: Exception) {
}
isAsyncRunning = false
}
override fun onFailure(
call: Call<T>,
t: Throwable
) {
try {
if (t is Exception) {
callback?.networkError(t)
} else {
callback?.unexpectedError(t)
}
} catch (e: Exception) {
}
try {
callback?.onComplete()
} catch (e: Exception) {
}
isAsyncRunning = false
}
})
}
override fun clone(): ApiCall<T> {
return ApiCallAdapter(
call.clone(),
callbackExecutor
)
}
override fun isRunning(): Boolean {
return isAsyncRunning
}
}
新方法
val viewModel: HomePageViewModel = viewModel()
val bannerData by viewModel.bannerState.observeAsState(PlayLoading)
if (bannerData !is PlaySuccess<*>) {
viewModel.getBanner()
}
data class BaseModel<T>(
val `data`: T,
val errorCode: Int,
val errorMsg: String
)
sealed class PlayState<out R> {
fun isLoading() = this is PlayLoading
fun isSuccessful() = this is PlaySuccess
override fun toString(): String {
return when (this) {
is PlaySuccess<*> -> "Success[data=$data]"
is PlayError -> "Error[exception=${error}]"
PlayLoading -> "Loading"
}
}
}
data class PlaySuccess<out T>(val data: T) : PlayState<T>()
data class PlayError(val error: Throwable) : PlayState<Nothing>()
object PlayLoading : PlayState<Nothing>()
/**
* [PlayState.data] if [Result] is of query [PlayState]
*/
fun <T> PlayState<T>?.successOr(fallback: T): T {
if (this == null) return fallback
return (this asPlaySuccess<T>)?.data ?: fallback
}
val <T> PlayState<T>.data: T?
get() = (this asPlaySuccess)?.data
abstract class BaseArticleViewModel(application: Application) : AndroidViewModel(application) {
abstract val repositoryArticle: BaseArticlePagingRepository
}
class HomePageViewModel(application: Application) : BaseArticleViewModel(application) {
override val repositoryArticle: BaseArticlePagingRepository
get() = HomeArticlePagingRepository()
private var bannerJob: Job= null
private val _bannerState = MutableLiveData<PlayState<List<BannerBean>>>()
val bannerState: LiveData<PlayState<List<BannerBean>>>
get() = _bannerState
fun getBanner() {
bannerJob?.cancel()
bannerJob = viewModelScope.launch(Dispatchers.IO) {
(repositoryArticle as HomeArticlePagingRepository).getBanner(_bannerState)
}
}
}
abstract class BaseArticlePagingRepository {}
class HomeArticlePagingRepository : BaseArticlePagingRepository() {
suspend fun getBanner(state: MutableLiveData<PlayState<List<BannerBean>>>) {
state.postValue(PlayLoading)
try {
val bannerResponse = PlayAndroidNetwork.getBanner()
if (bannerResponse.errorCode == 0) {
val bannerList = bannerResponse.data
bannerList.forEach {
it.data = it.imagePath
}
state.postValue(PlaySuccess(bannerList))
} else {
state.postValue(PlayError(RuntimeException("response status is ${bannerResponse.errorCode} msg is ${bannerResponse.errorMsg}")))
}
} catch (e: Exception) {
if (e is HttpException) {
state.postValue(PlayError(RuntimeException("response status is ${e.code()} msg is ${e.message()}")))
} else {
state.postValue(PlayError(RuntimeException("response status is unKnow")))
}
}
}
}
object PlayAndroidNetwork{
private val homePageService = ServiceCreator.create(HomePageService::class.java)
suspend fun getBanner() = homePageService.getBanner()
}
interface HomePageService {
@GET
suspend fun get(@Url url: String): String
@GET("banner/json")
suspend fun getBanner(): BaseModel<List<BannerBean>>
}
object ServiceCreator {
private fun create(): Retrofit {
// okHttpClientBuilder
val okHttpClientBuilder = OkHttpClient().newBuilder().apply {
connectTimeout(30L, TimeUnit.SECONDS)
readTimeout(10L, TimeUnit.SECONDS)
addInterceptor(HttpLoggingInterceptor().apply {
this.level = HttpLoggingInterceptor.Level.BODY
})
}
return RetrofitBuild(
url = "https://www.google.com/",
client = okHttpClientBuilder.build(),
gsonFactory = GsonConverterFactory.create()
).retrofit
}
/**
* get ServiceApi
*/
fun <T> create(service: Class<T>): T = create().create(service)
}
class RetrofitBuild(
url: String, client: OkHttpClient,
gsonFactory: GsonConverterFactory
) {
val retrofit: Retrofit = Retrofit.Builder().apply {
baseUrl(url)
client(client)
addConverterFactory(ScalarsConverterFactory.create())
addConverterFactory(gsonFactory)
}.build()
}
使用Page+Flow请求分页数据
class HomePagingSource : PagingSource<Int, ItemBean>() {
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, ItemBean> {
return try {
val page = params.key ?: 1 // set page 1 as default
val articleList = getPageData(page)
val prevKey = if (page > 1) page - 1 else null
val nextKey = if (articleList.isNotEmpty()) page + 1 else null
LoadResult.Page(articleList, prevKey, nextKey)
} catch (e: Exception) {
LoadResult.Error(e)
}
}
override fun getRefreshKey(state: PagingState<Int, ItemBean>): Int=null
suspend fun getPageData(page: Int): List<ItemBean> {
val apiResponse = PlayAndroidNetwork.getPageData(page)
return apiResponse.data.datas
}
}
class HomeArticlePagingRepository : BaseArticlePagingRepository() {
@ExperimentalPagingApi
override fun getPagingData(query: Query) = Pager(
PagingConfig(
pageSize = 15,
enablePlaceholders = false
)
) {
HomePagingSource()
}.flow
}
class HomePageViewModel(application: Application) : AndroidViewModel(application) {
val repositoryArticle = HomeArticlePagingRepository()
private val searchResults = MutableSharedFlow<Query>(replay = 1)
@OptIn(ExperimentalCoroutinesApi::class)
val dataResult: Flow<PagingData<ItemBean>> = repositoryArticle.getPagingData(Query()).cachedIn(viewModelScope)
}
val viewModel: HomePageViewModel = viewModel()
val lazyPagingItems = viewModel.dataResult.collectAsLazyPagingItems()
val listState = rememberLazyListState()
val context = LocalContext.current
LazyColumn(
modifier = modifier,
state = listState
) {
items(lazyPagingItems) { data ->
// do what you do
}
val loadStates = lazyPagingItems.loadState
when {
loadStates.refresh is LoadState.Loading -> {
}
loadStates.append is LoadState.Loading -> {
}
loadStates.refresh is LoadState.Error -> {
}
loadStates.append is LoadState.Error -> {
val e = lazyPagingItems.loadState.append as LoadState.Error
showToast(context, e.error.localizedMessage ?: "")
item {
Row(
modifier = Modifier.fillMaxWidth().padding(8.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center,
) {
Button(
onClick = { lazyPagingItems.retry() }) {
Text("Retry")
}
}
}
}
}
}
沉浸式状态栏
/**
* 设置透明状态栏
*/
fun Activity.transparentStatusBar() {
transparentStatusBar(window)
}
private fun transparentStatusBar(window: Window) {
window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS)
window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS)
val option = View.SYSTEM_UI_FLAG_LAYOUT_STABLE or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
val vis = window.decorView.systemUiVisibility
window.decorView.systemUiVisibility = option or vis
window.statusBarColor = Color.TRANSPARENT
}
/**
* 状态栏反色
*/
fun Activity.setAndroidNativeLightStatusBar() {
val decor = window.decorView
val isDark = resources.configuration.uiMode == 0x21
if (!isDark) {
decor.systemUiVisibility =
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
} else {
decor.systemUiVisibility =
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or View.SYSTEM_UI_FLAG_LAYOUT_STABLE
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
transparentStatusBar()
setAndroidNativeLightStatusBar()
setContent {
PlayAndroidTheme {
ProvideWindowInsets { // 沉浸式状态栏
Column(modifier = Modifier.background(color = MaterialTheme.colors.primary)) {
Spacer(Modifier.statusBarsHeight()). // 状态栏高度
}
}
}
}
}
ViewModel 三板斧
如果ViewMode 需要使用Context 则继承 AndroidViewModel,否则继承 ViewModel。
class BaseViewModel(application: Application) : AndroidViewModel(application) {
private val _position = MutableLiveData(0)
val position: LiveData<Int> = _position
fun onPositionChanged(position: Int) {
_position.value = position
}
}
val viewModel by viewModels< BaseViewModel >()
val treePosition by viewModel.position.observeAsState(0)
使用全局统一的 ViewModel, 把ViewModel写到 Application里:
val mainViewModel by lazy {
ViewModelProvider.AndroidViewModelFactory(this).create(MainViewModel::class.java)
}
添加 Android View
@Composable
fun rememberWebViewWithLifecycle(): WebView {
val context = LocalContext.current
val webView = remember {
WebView(context)
}
val lifecycleObserver = rememberWebViewLifecycleObserver(webView)
val lifecycle = LocalLifecycleOwner.current.lifecycle
DisposableEffect(lifecycle) {
lifecycle.addObserver(lifecycleObserver)
onDispose {
lifecycle.removeObserver(lifecycleObserver)
}
}
return webView
}
@Composable
private fun rememberWebViewLifecycleObserver(webView: WebView): LifecycleEventObserver =
remember(webView) {
LifecycleEventObserver { _, event ->
when (event) {
Lifecycle.Event.ON_RESUME -> webView.onResume()
Lifecycle.Event.ON_PAUSE -> webView.onPause()
Lifecycle.Event.ON_DESTROY -> webView.destroy()
else -> Log.e("WebView", event.name)
}
}
}
val webView = rememberWebViewWithLifecycle()
AndroidView(
factory = { webView },
modifier = Modifier
.fillMaxSize()
) { view ->
view.webViewClient = object : WebViewClient() {
override fun shouldOverrideUrlLoading(view: WebView?, url: String): Boolean {
return try {
if (url.startsWith("http:") || url.startsWith("https:")) {
view!!.loadUrl(url)
} else {
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
webView.context.startActivity(intent)
}
true
} catch (e: Exception) {
false
}
}
}
val settings: WebSettings = view.settings
settings.mixedContentMode =
WebSettings.MIXED_CONTENT_ALWAYS_ALLOW
settings.javaScriptEnabled = true //启用js
settings.blockNetworkImage = false //解决图片不显示
view.loadUrl("https://www.baidu.com")
}
XML使用 ComposeView
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical">
<EditText
android:id="@+id/mainEditName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="50dp"
android:layout_marginBottom="20dp"
android:hint="name" />
<androidx.compose.ui.platform.ComposeView
android:id="@+id/composeView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="50dp" />
</LinearLayout>
composeView.setContent {
Button(onClick = {
Toast.makeText(this@MainActivity,mainEditName.text.toString(),Toast.LENGTH_LONG).show()
}){
Text("ComposeView")
}
}
判断横竖屏
// 当前是否为横屏
val isLand = LocalConfiguration.current.orientation == Configuration.ORIENTATION_LANDSCAPE
设置点击效果颜色
.clickable(interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(color = AppColor))
PX DP SP 互转
@Composable
fun dpToPx(dp: Dp) = with(LocalDensity.current) { dp.toPx() }
@Composable
fun pxToDp(px: Int) = with(LocalDensity.current) { px.toDp() }
@Composable
fun spToDp(sp: TextUnit) = with(LocalDensity.current) { sp.toDp() }
@Composable
fun spToPx(sp: TextUnit) = with(LocalDensity.current) { sp.toPx() }
END