Compose 项目笔记概要

[toc]

网址导航

Compose与Kotlin的兼容对应关系

Kotlin 预发布版本兼容的 Compose Compiler 版本

快速入门

Navigation

图片加载

Gradle国内镜像

动画

手势

Blur模糊效果

Scrcpy 手机投屏


版本对应示例

 id 'org.jetbrains.kotlin.android' version '1.8.10' apply false

 buildFeatures {
        compose true
    }
    composeOptions {
        kotlinCompilerExtensionVersion '1.4.3'
    }


    def composeBom = platform('androidx.compose:compose-bom:2023.06.00')
    implementation(composeBom)
    androidTestImplementation(composeBom)

    implementation "androidx.compose.runtime:runtime"
    implementation "androidx.compose.ui:ui"
    implementation "androidx.compose.foundation:foundation"
    implementation "androidx.compose.foundation:foundation-layout"
    implementation "androidx.compose.material:material"
    implementation "androidx.compose.runtime:runtime-livedata"
    implementation "androidx.compose.ui:ui-tooling"

    // Android Studio Preview support
    implementation 'androidx.compose.ui:ui-tooling-preview'
    debugImplementation 'androidx.compose.ui:ui-tooling'

    // Optional - Integration with activities
    implementation 'androidx.activity:activity-compose:1.7.2'
    // Optional - Integration with ViewModels
    implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.6.1'
    // Optional - Integration with LiveData
    implementation 'androidx.compose.runtime:runtime-livedata'

网络请求

老方法

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 as? PlaySuccess<T>)?.data ?: fallback
}

val <T> PlayState<T>.data: T?
    get() = (this as? PlaySuccess)?.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)
    }

Application 级别:

 lateinit var application: AppApplication

        val roomViewModel by lazy {
            ViewModelProvider.AndroidViewModelFactory(application).create(RoomViewModel::class.java)
        }

        val adsViewModel by lazy {
            ViewModelProvider.AndroidViewModelFactory(application).create(AdsViewModel::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() }

走马灯

modifier = Modifier.basicMarquee()

END

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,884评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,755评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,369评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,799评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,910评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,096评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,159评论 3 411
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,917评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,360评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,673评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,814评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,509评论 4 334
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,156评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,882评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,123评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,641评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,728评论 2 351

推荐阅读更多精彩内容