Building a Kotlin project

发表于 5年以前  | 总阅读数:2690 次

Building a Kotlin project

注意 : 翻译完之后请认真的审核一遍有没有错字、语句通不通顺,谢谢~

Part 1

学一门新语言最有效的方法就是写一个实际的例子.

所以这个系列的博客将专注于使用 Kotlin 写一个小例子.

Scenario (使用场景)

为了覆盖各种情景,这个DEMO必须要有以下要求:

  • 网络访问
  • 通过 REST API 请求数据
  • 反序列化数据
  • 在列表中显示图片

为了符合这些要求为什么不做一个显示小猫的app呢?

使用 http://thecatapi.com/ 的 API 我们可以检索到一些可爱的小猫的图片

小猫app

Dependencies (依赖库)

这可是个使用一些很腻害的依赖库的好机会,比如说:

  • Retrofit2 用来请求网络,访问REST API以及数据的反序列化
  • Glide 用来显示图片
  • RxJava 来绑定数据
  • RecyclerView CardView 支持界面显示
  • 整体框架将使用MVP

Set Up the Project (建立工程)

使用 Android Studio 来创建新工程将会非常简单

Start a new Android Project (创建一个新 Android 工程)

Create a new project ( 创建一个项目)

Select Target Android Device (选择需要的android版本)

Add an activity (添加 activity)

Customize the Activity (选择样式)

点击完成,刚刚配置的模板工程将被创建。

我们的 Kitten APP 就建好了!

然而这时候代码还是 java , 接下来我们将它处理成 Kotlin.

Defining Gradle Build Tool

下一步我们将升级 Build Tool 并且 将那些库我们将会用到库引用进来.

开始这步之前,请查看 Android Kotlin 需要的环境支持 post

打开该项目 App中的 build.gradle (图片中指出的地方)

将所有 引用库 和 andorid properties 的版本通过一个另外的 scripts 来管理是一个很好的习惯,可以使用Gradle提供的 ext 属性来使用和访问他们。

最简单的方法是在 build.gradle 文件的开头加上下面的片段

buildscript {
  ext.compileSdkVersion_ver = 23
  ext.buildToolsVersion_ver = '23.0.2'

  ext.minSdkVersion_ver = 21
  ext.targetSdkVersion_ver = 23
  ext.versionCode_ver = 1
  ext.versionName_ver = '1.0'

  ext.support_ver = '23.1.1'

  ext.kotlin_ver = '1.0.0'
  ext.anko_ver = '0.8.2'

  ext.glide_ver = '3.7.0'
  ext.retrofit_ver = '2.0.0-beta4'
  ext.rxjava_ver = '1.1.1'
  ext.rxandroid_ver = '1.1.0'

  ext.junit_ver = '4.12'

  repositories {
      mavenCentral()
  }

  dependencies {
      classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_ver"
  }
}

然后添加 Kotlin 插件 , 如下所示

apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'

在添加我们将使用到的项目引用库之前,将之前添加在头部的ext属性对应的版本设置正确

android {
  compileSdkVersion "$compileSdkVersion_ver".toInteger()
  buildToolsVersion "$buildToolsVersion_ver"

  defaultConfig {
    applicationId "com.github.cirorizzo.kshows"
    minSdkVersion "$minSdkVersion_ver".toInteger()
    targetSdkVersion "$targetSdkVersion_ver".toInteger()
    versionCode "$versionCode_ver".toInteger()
    versionName "$versionName_ver"
}
...

再改变一个 builTypes 选项

buildTypes {
    debug {
        buildConfigField("int", "MAX_IMAGES_PER_REQUEST", "10")
        debuggable true
        minifyEnabled false
        proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
    }

    release {
        buildConfigField("int", "MAX_IMAGES_PER_REQUEST", "500")
        debuggable false
        minifyEnabled true
        shrinkResources true
        proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
    }
}
sourceSets {
    main.java.srcDirs += 'src/main/kotlin'
}

下一步将是申明引用库在项目中的使用

dependencies {
  compile fileTree(dir: 'libs', include: ['*.jar'])
  testCompile "junit:junit:$junit_ver"

  compile "com.android.support:appcompat-v7:$support_ver"
  compile "com.android.support:cardview-v7:$support_ver"
  compile "com.android.support:recyclerview-v7:$support_ver"
  compile "com.github.bumptech.glide:glide:$glide_ver"

  compile "com.squareup.retrofit2:retrofit:$retrofit_ver"
  compile ("com.squareup.retrofit2:converter-simplexml:$retrofit_ver") {
    exclude module: 'xpp3'
    exclude group: 'stax'
}

  compile "io.reactivex:rxjava:$rxjava_ver"
  compile "io.reactivex:rxandroid:$rxandroid_ver"
  compile "com.squareup.retrofit2:adapter-rxjava:$retrofit_ver"

  compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_ver"
  compile "org.jetbrains.anko:anko-common:$anko_ver"
}

终于项目的 build.gradle 文件配置好了

还有一件事,添加访问网络的权限,将以下代码添加到AndroidManifest.xml中

<uses-permission android:name="android.permission.INTERNET" />

可以进入下一步了

Designing Project Structure (设计项目的结构)

另一个好习惯是 根据在项目中类的不同用途来设计包和文件夹,将相同类型的类放在一个包中,我们可以这样设计项目的结构:

右键点击 com.github.cirorizzo.kshows 包,然后选择 New ->Package

Coding(写代码!)

下一篇将介绍如何编写 Kitten app

Part 2

上一篇我们介绍了如何创建一个项目,并且对 Kitten APP 需要的 build.gradle 文件进行设置

下一步我们将开始对app进行编写

Data Model (数据模型)

项目中的一个重要功能就是通过网络请求网站 http://thecatapi.com 中的数据

完整的域名将是 http://thecatapi.com/api/images/get?format=xml&results_per_page=10

API 返回一个 xml 文件

必须对数据进行解析才能拿到我们需要的Kitten image的url

Kotlin 有一个非常适合的 class 叫做 data class 完美适合这样的需求

让我们再包名.cats 中创建一个新的class,右键包名然后选择 New->Kotlin File/Class ,命名为cats然后选择为 class

为了构建解析xml的class,Cats.kt 是这样的

data class Cats(var data: Data? = null)

data class Data(var images: ArrayList<Image>? = null)

data class Image(var url: String? = "", var id: String? = "", var source_url: String? = "")

看到这是不是觉得特别简洁?

如果用java代码将会长很多

Kotlin的data class 有很多特点,比如说 对 getter(), setter() 和 toString() 方法的自动生成,对于 equals() hashCode() 和 copy()也是一样的,所以对于解析数据这真是完美啊

API Call

访问网络有许多种方法,也有很多支持库,其中有一个来自Square的Retrofit2

这是一个非常强大的 HTTPClient 而且非常容易使用

我们从接口开始,在 network package中创建它

命名为CatAPI

interface CatAPI {
    @GET("/api/images/get?format=xml&results_per_page=" + BuildConfig.MAX_IMAGES_PER_REQUEST)
    fun getCatImageURLs(): Observable<Cats>
}

这个接口将会处理对接口 /api/images/get?format=xml&results_per_page=. 的请求

在这里 results_per_page 参数是从build.gradle中读取的,其中一个参数叫做 MAX_IMAGES_PER_REQUEST ,根据在buildTypes中设置不同值来定义它

buildTypes {
    debug {
        buildConfigField("int", "MAX_IMAGES_PER_REQUEST", "10")
        ...

用这个方法来定义值是非常方便的,在我们编译 debug版本和release版本时候非常方便,特别是在你需要区分这两者的值的时候

CatAPI 这个接口非常有趣,这个方法调用请求,并返回回调 ,从 fun getCatImageURLs(): Observable

所以下一步是将它实现 让我们在同一个包(network)中创建一个新的class,命名为CatAPINetwork

class CatAPINetwork {
    fun getExec(): Observable<Cats> {
        val retrofit = Retrofit.Builder()
            .baseUrl("http://thecatapi.com")
            .addConverterFactory(SimpleXmlConverterFactory.create())
            .addCallAdapterFactory(RxJavaCallAdapterFactory.create())
            .build()

        val catAPI: CatAPI = retrofit.create(CatAPI::class.java)

        return catAPI.getCatImageURLs().
            subscribeOn(Schedulers.io()).
            observeOn(AndroidSchedulers.mainThread())
    }
}

fun getExec(): Observable 这个方法被设置成 public 的意味着它可以被外者调用

.addConverterFactory(SimpleXmlConverterFactory.create())这一行说明了使用XML转换器来解析从API获得的数据

然后 .addCallAdapterFactory(RxJavaCallAdapterFactory.create()) 在AIP回调中调用了方法使 adapter 被使用

return 的这一行请参照 RxJava Observable

return catAPI.getCatImageURLs().
            subscribeOn(Schedulers.io()).
            observeOn(AndroidSchedulers.mainThread())

Presenter(提供者)

这个 Presenter 负责的是APP中的逻辑 还有将数据从model层绑定到试图层的业务逻辑

在我们的使用中它将实现一些 被试图层调用返回数据的方法,并且将这些数据提供给adapter以供呈现

为了和试图层的通信,我们将在presenter包中新建一个叫做MasterPresenter的接口

interface MasterPresenter {
    fun connect(imagesAdapter: ImagesAdapter)
    fun getMasterRequest()
}

第一个方法 fun connect(imagesAdapter: ImagesAdapter) 将被用于连接adapter的接口来显示数据,然后 fun getMasterRequest() 将被用于开始API请求

我们在同一个包中新建一个实现类,并命名为 MasterPresenterImpl

class MasterPresenterImpl : MasterPresenter {
    lateinit private var imagesAdapter: ImagesAdapter

    override fun connect(imagesAdapter: ImagesAdapter) {
        this.imagesAdapter = imagesAdapter
    }

    override fun getMasterRequest() {
        imagesAdapter.setObservable(getObservableMasterRequest(CatAPINetwork()))
    }

    private fun getObservableMasterRequest(catAPINetwork: CatAPINetwork): Observable<Cats> {
        return catAPINetwork.getExec()
    }
}

lateinit private var imagesAdapter: ImagesAdapter , 这一行代码十分有趣,Kotlin给我们提供了声明一个非空变量而不需要设定初始值的功能,使用 lateinit 即可,变量将在他被使用的时候设定初始值,在我们的例子中它调用了 fun connect(imagesAdapter: ImagesAdapter).

fun getMasterRequest() 这个方法发起了网络请求,在启动了 catAPINetwork.getExec() 请求网络数据后 , 设置Observable绑定到adapter中

View section

在view包中的class主要负责对UI的管理

Layouts

在开始实现之前,让我们看看设计图先

实现这个视图我们基本上需要两个视图容器和一个子布局容器

最底层的视图应该是包含整个list的视图,我们将视图描述在 activity_main.xml 中并房子啊 res->layout文件夹中,这个文件在创建工程时是自动生成的

在我们app中我们需要使用的时候 RecyclerView这个组件(一个十分强大,完美的组件)

activity_main.xml 将会长成这样

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    tools:context=".view.MainActivity"
    android:gravity="center">

    <android.support.v7.widget.RecyclerView
        android:id="@+id/containerRecyclerView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:scrollbars="vertical"
        android:layout_centerInParent="true" />
</RelativeLayout>

RecylerView 的父视图组件就是这个list和item的主要视图

row_card_view.xml 则是item的布局,它大概长这样:

<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.CardView
    xmlns:card_view="http://schemas.android.com/apk/res-auto"
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/card_view"
    android:layout_gravity="center"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    card_view:cardCornerRadius="4dp"
    android:layout_margin="16dp"
    android:background="@android:color/transparent"
    android:layout_centerInParent="true"
    android:elevation="4dp">

    <RelativeLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        android:gravity="center"
        android:foregroundGravity="center">

        <ImageView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:id="@+id/imgVw_cat"
            android:padding="4dp"
            android:layout_centerInParent="true"
            android:scaleType="fitCenter"
            android:contentDescription="@string/cat_image" />
    </RelativeLayout>
</android.support.v7.widget.CardView>

如你所见item的父布局是一个card_view , 里面是一个 RelativeLayout 包含了一个 ImageView

Adapter

现在我们完成了基本的layout,接下来将实现 MainActivity和adapter

开始处理adapter的第一件事就是创建被MasterPresenterImpl调用的接口,在view 包中创建一个命名为ImagesAdapter的文件

interface ImagesAdapter {
    fun setObservable(observableCats: Observable<Cats>)
    fun unsubscribe()
}

setObservable(observableCats: Observable) 这个方法被MasterPresenterImpl调用来设置 Observalbe 并且让 adapter 来写入数据

unsubscribe() 这个方法被 MainActivity 调用来解除 adapter 和 Observable 的绑定,在activity被销毁的时候

现在让我们实现他们,在ImagesAdapterImpl 包中的一个新 class

class ImagesAdapterImpl : RecyclerView.Adapter<ImagesAdapterImpl.ImagesURLsDataHolder>(), ImagesAdapter {
    private val TAG = ImagesAdapterImpl::class.java.simpleName

    private var cats: Cats? = null
    private val subscriber: Subscriber<Cats> by lazy { getSubscribe() }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ImagesURLsDataHolder {
        return ImagesURLsDataHolder(
                LayoutInflater.from(parent.context).inflate(R.layout.row_card_view, parent, false))
    }

    override fun getItemCount(): Int {
        return cats?.data?.images?.size ?: 0
    }

    override fun onBindViewHolder(holder: ImagesURLsDataHolder, position: Int) {
        holder.bindImages(cats?.data?.images?.get(position)?.url ?: "")
    }

    private fun setData(cats: Cats?) {
        this.cats = cats
    }

    override fun setObservable(observableCats: Observable<Cats>) {
        observableCats.subscribe(subscriber)
    }

    override fun unsubscribe() {
        if (!subscriber.isUnsubscribed) {
            subscriber.unsubscribe()
        }
    }

    private fun getSubscribe(): Subscriber<Cats> {
        return object : Subscriber<Cats>() {
            override fun onCompleted() {
                Log.d(TAG, "onCompleted")
                notifyDataSetChanged()
            }

            override fun onNext(cats: Cats) {
                Log.d(TAG, "onNextNew")
                setData(cats)
            }

            override fun onError(e: Throwable) {
                //TODO : Handle error here
                Log.d(TAG, "" + e.message)
            }
        }
    }

    class ImagesURLsDataHolder(view: View) : RecyclerView.ViewHolder(view) {

        fun bindImages(imgURL: String) {
            Glide.with(itemView.context).
                    load(imgURL).
                    placeholder(R.mipmap.document_image_cancel).
                    diskCacheStrategy(DiskCacheStrategy.ALL).
                    centerCrop().
                    into(itemView.imgVw_cat)
        }
    }
}

这个class为 row_card_view.xml 提供数据,你能看见在 onCreateViewHolder 方法中都是对 item 的容器的操作

getSubscribe() 这个方法提供了 Observable 写入adapter的数据, 在 private val subscriber: Subscriber by lazy { getSubscribe() } 这一行被调用,注意一下 lazy 初始化(懒加载),,这声明了一个固定的object,它会通过括在大括号的函数来创建(即getSubscribe())在第一次运行时调用。

Subscriber 和 Observable 概念来自 RxJava,在后面的博客将深入研究

最后,有一段十分有趣的代码,在ImagesURLsDataHolder这个类中,通过Glide library用填充 imgVw_cat , 通过 API请求传回来的URL将绑定到imageView中被显示出来, bindImages(imgURL: String) 方法中包装了这部分内容, 在同一个类中的方法 onBindViewHolder 中被调用

Activity

最后但同样重要的Activity

class MainActivity : AppCompatActivity() {
    private val imagesAdapterImpl: ImagesAdapterImpl by lazy { ImagesAdapterImpl() }

    private val masterPresenterImpl: MasterPresenterImpl
            by lazy {
                MasterPresenterImpl()
            }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        initRecyclerView()
        connectingToMasterPresenter()
        getURLs()
    }

    override fun onDestroy() {
        imagesAdapterImpl.unsubscribe()
        super.onDestroy()
    }

    private fun initRecyclerView() {
        containerRecyclerView.layoutManager = GridLayoutManager(this, 1)
        containerRecyclerView.adapter = imagesAdapterImpl
    }

    private fun connectingToMasterPresenter() {
        masterPresenterImpl.connect(imagesAdapterImpl)
    }

    private fun getURLs() {
        masterPresenterImpl.getMasterRequest()
    }
}

注意这些方法

  • initRecyclerView()
  • connectingToMasterPresenter()
  • getURLs()

各自用作于

  • 初始化主要布局
  • 建立MainActivity和MasterPresenterImpl的连接,并将它传给ImagesAdapterImpl
  • getURLs() 开始请求返回的xml数据,并运行接下来的步骤(解析数据,显示adapter中的图片)

Kitten app现在已经可以运行了

整个项目在github上,请搜索 KShow

其中也有java版本,方便进行对比

 相关推荐

刘强东夫妇:“移民美国”传言被驳斥

京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。

发布于:7月以前  |  808次阅读  |  详细内容 »

博主曝三大运营商,将集体采购百万台华为Mate60系列

日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。

发布于:7月以前  |  770次阅读  |  详细内容 »

ASML CEO警告:出口管制不是可行做法,不要“逼迫中国大陆创新”

据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。

发布于:7月以前  |  756次阅读  |  详细内容 »

抖音中长视频App青桃更名抖音精选,字节再发力对抗B站

今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。

发布于:7月以前  |  648次阅读  |  详细内容 »

威马CDO:中国每百户家庭仅17户有车

日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。

发布于:7月以前  |  589次阅读  |  详细内容 »

研究发现维生素 C 等抗氧化剂会刺激癌症生长和转移

近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。

发布于:7月以前  |  449次阅读  |  详细内容 »

苹果据称正引入3D打印技术,用以生产智能手表的钢质底盘

据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。

发布于:7月以前  |  446次阅读  |  详细内容 »

千万级抖音网红秀才账号被封禁

9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...

发布于:7月以前  |  445次阅读  |  详细内容 »

亚马逊股东起诉公司和贝索斯,称其在购买卫星发射服务时忽视了 SpaceX

9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。

发布于:7月以前  |  444次阅读  |  详细内容 »

苹果上线AppsbyApple网站,以推广自家应用程序

据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。

发布于:7月以前  |  442次阅读  |  详细内容 »

特斯拉美国降价引发投资者不满:“这是短期麻醉剂”

特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。

发布于:7月以前  |  441次阅读  |  详细内容 »

光刻机巨头阿斯麦:拿到许可,继续对华出口

据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。

发布于:7月以前  |  437次阅读  |  详细内容 »

马斯克与库克首次隔空合作:为苹果提供卫星服务

近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。

发布于:7月以前  |  430次阅读  |  详细内容 »

𝕏(推特)调整隐私政策,可拿用户发布的信息训练 AI 模型

据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。

发布于:7月以前  |  428次阅读  |  详细内容 »

荣耀CEO谈华为手机回归:替老同事们高兴,对行业也是好事

9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。

发布于:7月以前  |  423次阅读  |  详细内容 »

AI操控无人机能力超越人类冠军

《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。

发布于:7月以前  |  423次阅读  |  详细内容 »

AI生成的蘑菇科普书存在可致命错误

近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。

发布于:7月以前  |  420次阅读  |  详细内容 »

社交媒体平台𝕏计划收集用户生物识别数据与工作教育经历

社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”

发布于:7月以前  |  411次阅读  |  详细内容 »

国产扫地机器人热销欧洲,国产割草机器人抢占欧洲草坪

2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。

发布于:7月以前  |  406次阅读  |  详细内容 »

罗永浩吐槽iPhone15和14不会有区别,除了序列号变了

罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。

发布于:7月以前  |  398次阅读  |  详细内容 »
 相关文章
简化Android的UI开发 4年以前  |  520716次阅读
Android 深色模式适配原理分析 3年以前  |  28654次阅读
Android阴影实现的几种方案 1年以前  |  10807次阅读
Android 样式系统 | 主题背景覆盖 3年以前  |  9601次阅读
 目录