compose简单理解

compose是什么?

https://developer.android.google.cn/jetpack/compose/why-adopt#less-code
Jetpack Compose 是用于构建原生 Android 界面新工具包。它使用更少的代码、强大的工具和直观的 Kotlin API,可以帮助您简化并加快 Android 界面开发,打造生动而精彩的应用。它可让您更快速、更轻松地构建 Android 界面。
更少的代码
编写更少的代码会影响到所有开发阶段:作为代码撰写者,需要测试和调试的代码会更少,出现 bug 的可能性也更小,您就可以专注于解决手头的问题;作为审核人员或维护人员,您需要阅读、理解、审核和维护的代码就更少。
与使用 Android View 系统(按钮、列表或动画)相比,Compose 可让您使用更少的代码实现更多的功能。无论您需要构建什么内容,现在需要编写的代码都更少了。
编写代码只需要采用 Kotlin,而不必拆分成 Kotlin 和 XML 部分:“当所有代码都使用同一种语言编写并且通常位于同一文件中(而不是在 Kotlin 和 XML 语言之间来回切换)时,跟踪变得更容易”
直观
利用 Compose,您可以构建不与特定 activity 或 fragment 相关联的小型无状态组件。这让您可以轻松重用和测试这些组件:“我们给自己设定的目标是,交付一组新的无状态界面组件,确保它们易于使用和维护,且可直观实现/扩展/自定义。就这一点而言,Compose 确实为我们提供了一个可靠的答案。”
功能强大
利用 Compose,您可以凭借对 Android 平台 API 的直接访问和对于 Material Design、深色主题、动画等的内置支持,创建精美的应用:“Compose 不仅解决了声明性界面的问题,还改进了无障碍功能 API、布局等各种内容。将设想变为现实所需的步骤更少了”

先来看看这2个界面


image.png

image.png

下面通过非compose和compose版本对比,来简单理解下。

一、非compose版本

先看看非compose版本,即使用kotlin+xml的方式,是怎么写的。

1.新建一个普通工程
2.准备数据
这里没有api接口,直接从网站https://dogtime.com/
从网站上复制一些数据,如名字,介绍,图片。
然后自定义一个json格式的文件。dog.json。

[
  {
    "name": "Alaskan Klee Kai",
    "avatar_filename": "alaskan_klee_kai",
    "introduction": "Small, smart, and energetic, the Alaskan Klee Kai is a relatively new breed that looks like a smaller version of the Siberian Husky. Even the name “Klee Kai” comes from an Inuit term meaning “small dog.”\n\nWhile Alaskan Klee Kais may resemble larger Husky breeds, they have some key differences, especially when it comes to temperament, that distinguish it from its ancestor working class dogs of the north. This breed is more suited to the life of a companion; although, the Alaskan Klee Kai shares the high energy of the Husky and demands plenty of exercise."
  },
  {
    "name": "Bernedoodle",
    "avatar_filename": "bernedoodle",
    "introduction": "Clever, goofy, gentle, and loyal. Bernedoodle fans boast that this mixed breed has the best of both worlds from its Bernese Mountain Dog and Poodle parents.\n\nDespite their unfortunate status as a designer breed, you may find these dogs in the care of shelters and rescues. Remember to adopt! Don’t shop if this is the mixed breed for you!"
  },
  {
    "name": "Cavachon",
    "avatar_filename": "cavachon",
    "introduction": "The Cavachon is a mixed breed dog–a cross between the Cavalier King Charles Spaniel and Bichon Frise dog breeds. Compact, spunky, and full of fun, these pups inherited some of the best traits from both of their parents.\n\nCavachons don’t go by many other names with the exceptions of Cavalier-Bichon or Bichon-King Charles. Despite their unfortunate status as a designer breed, you may find these mixed breed dogs in shelters and rescues."
  },
  {
    "name": "Dorkie",
    "avatar_filename": "dorkie",
    "introduction": "The Dorkie is a mixed breed dog — a cross between the Dachshund and Yorkshire Terrier. Laid-back and loyal, these pint-sized pups inherited some of the best qualities from both of their parents.\n\nDorkies also go by the names Dorkie Terrier and Doxie Yorkie. Despite their unfortunate status as a designer breed, you can find these mixed breed pups in shelters and breed specific rescues."
  },
  {
    "name": "Eurasier",
    "avatar_filename": "eurasier",
    "introduction": "The Eurasier is a breed of medium-sized dogs of the Spitz type that first came from Germany. These dogs are known to be very smart, loyal, and even-tempered.\n\nEurasiers can go by many other names such as Eurasian, Eurasian Spitz, Eurasian dog, and most notably, Wolf-Chow. You can find Eurasier dogs in shelters and breed specific rescues, so remember to adopt!"
  },
  {
    "name": "French Bullhuahua",
    "avatar_filename": "french_bullhuahua",
    "introduction": "The French Bullhuahua is a mixed breed dog–a cross between the Chihuahua and French Bulldog breeds. Compact, spunky, and loyal, these pups inherited some of the best qualities from both of their parents.\n\nFrench Bullhuahuas go by several names, including Frencheenie, Chibull, and Mexican Frenchie. Despite their unfortunate status as a designer breed, you can find these mixed breed dogs in shelters and breed specific rescues, so remember to adopt! Don’t shop!"
  },
  {
    "name": "Golden Retriever Corgi",
    "avatar_filename": "golden_retriever_corgi",
    "introduction": "The Golden Retriever Corgi is a mixed breed dog — a cross between the Corgi and Golden Retriever dog breeds. Loyal, silly, and active, these pups inherited some of the best qualities from both of their parents.\n\nGolden Retriever Corgis go by several names, including the Golden Corgi, Corgi Retriever, and the Corgi Golden Retriever. Despite their unfortunate designer breed status, you may find these dogs in shelters and breed specific rescues. So remember to adopt! Don’t shop!"
  },
  {
    "name": "Horgi",
    "avatar_filename": "horgi",
    "introduction": "The Horgi is a mixed breed dog — a cross between the Siberian Husky and Corgi dog breeds. Small, playful and full of energy, these pups inherited some of the best qualities from both of their parents.\n\nHorgis are also called Siborgis. Despite their unfortunate status as a designer breed, you may find these mixed breed dogs in shelters and breed specific rescues, so remember to adopt! Don’t shop!"
  },
  {
    "name": "Icelandic Sheepdog",
    "avatar_filename": "icelandic_sheepdog",
    "introduction": "Thought to be companions to the ancient Vikings, the Icelandic Sheepdog breed was used to protect flocks, especially lambs, from birds of prey. They still retain the habit of watching the sky and barking at birds — as well as everything else they see or hear.\n\nThis breed is also known as the Icelandic Spitz or Icelandic Dog. Even though these are purebred dogs, you may find them in the care of shelters or rescue groups. Remember to adopt! Don’t shop if you want to bring a dog home."
  },
  {
    "name": "Jack Chi",
    "avatar_filename": "jack_chi",
    "introduction": "The Jack Chi is a mixed breed dog — a cross between the Jack Russell Terrier and Chihuahua dog breeds. Friendly, playful, and energetic, these pups inherited some of the best qualities from both of their parents.\n\nThe Jack Chi is also sometimes called the Jackahuahua and the Jackhuahua. You may find these mixed breed dogs in shelters and rescues, so remember to always adopt! Don’t shop if you’re looking to add one of these pups to your home!"
  },
  {
    "name": "Kishu Ken",
    "avatar_filename": "kishu_ken",
    "introduction": "Almost exclusive to Japan, the Kishu Ken is an ancient dog breed once used for hunting large game like boar. Some are still used as hunting dogs, but for the most part, the modern day Kishu Ken is a family dog in Japan, and they’ve started to grow in popularity in the United States, as well.\n\nSome fans of the breed affectionately call them Kishu or Kishu Inu. Although these are purebred dogs, you may still find them in shelters and rescues. Remember to adopt! Don’t shop if this is the breed for you."
  },
  {
    "name": "Lagotto Romagnolo",
    "avatar_filename": "lagotto_romagnolo",
    "introduction": "Lagotto Romagnolo means “lake dog from Romagna,” which is a good name for this breed, considering these dogs originally helped hunt waterfowl through the wet marshlands of Romagna in Italy.\n\nThey’re also known as Italian Water Dogs and Romagna Water Dogs. Today, with many of the marshlands of the breed’s homeland drained, these dogs have found a new purpose in truffle hunting. Humans can easily train them to use their super noses for scent work. Lagottos’ thick coats help them stay warm in fall and winter while protecting them from thorns and debris as they run through forests."
  },
  {
    "name": "Maremma Sheepdog",
    "avatar_filename": "maremma_sheepdog",
    "introduction": "The Maremma Sheepdog is considered an “Old World European” breed, sharing ancestry with other Eastern European livestock guardian dogs, especially mountain-dwelling dogs, like the Pyrenean Mountain Dog and Kuvasz. Maremma Sheepdogs can be traced back at least to ancient Roman times. Originally bred in Italy, they are still very popular there, and their original purpose continues on: to guard livestock."
  },
  {
    "name": "Norfolk Terrier",
    "avatar_filename": "norfolk_terrier",
    "introduction": "The Norfolk Terrier is what’s considered a “big dog in a small package.” Alert, gregarious, and nimble, they’re a loyal companion with the heart of a working terrier.\n\nAlthough these are purebred dogs, you may still find them in shelters and rescues. Remember to adopt! Don’t shop if this is the breed for you."
  },
  {
    "name": "Otterhound",
    "avatar_filename": "otterhound",
    "introduction": "The large and rough-coated Otterhound was originally bred for hunting otter in England. Built for work, the dog breed has a keen nose and renowned stamina.\n\nThis is an uncommon breed, with fewer than ten litters born each year in the United States and Canada. Still some may still end up in the care of shelters or rescues. Consider adoption if this is the breed for you."
  },
  {
    "name": "Pyredoodle",
    "avatar_filename": "pyredoodle",
    "introduction": "The Pyredoodle is a mixed breed dog–a cross between the Great Pyrenees and Standard Poodle dog breeds. Calm, fearless, and loyal, these pups inherited some of the best traits from both of their parents.\n\nPyredoodles go by a few other names, including Pyreneespoo, Pyrepoo, and Pyreneesdoodle. Despite their unfortunate status as a designer breed, you can find these mixed breed dogs in shelters and breed specific rescues, so remember to adopt! Don’t shop!"
  },
  {
    "name": "Rat Terrier",
    "avatar_filename": "rat_terrier",
    "introduction": "Members of the Rat Terrier dog breed are adorable, little, digging escape artists who are true terriers: feisty, funny, energetic, lively, vermin-chasing, and incapable of being boring. Stubborn as all get out, they are not big on pleasing people, but the people who love them laugh all the time.\n\nEven though these are purebred dogs, you may find them in the care of shelters or rescue groups. Remember to adopt! Don’t shop if you want to bring a dog home."
  },
  {
    "name": "Sheepadoodle",
    "avatar_filename": "sheepadoodle",
    "introduction": "The Sheepadoodle is a mixed breed dog — a cross between the Old English Sheepdog and Poodle dog breeds. Smart, playful, and loving, these pups inherited some of the best traits from both of their parents.\n\nSheepadoodles go by many names, including Sheep-a-poo, Sheeppoo, Sheepdoodle, and Sheepdogpoo. Despite their status as a designer breed, you may  find these mixed breed dogs in shelters and rescues, so remember to adopt! Don’t shop!"
  },
  {
    "name": "Terripoo",
    "avatar_filename": "terripoo",
    "introduction": "The Terripoo is a cross between the Australian Terrier and the miniature Poodle. Intelligent, intuitive, and playful, these small dogs make excellent family pets.\n\nTerripoos are also known as Terri Poos, Terridoodles, and Terrypoos. They are considered “designer dogs,” bred on purpose to emphasize desirable characteristics from each breed. As always, please adopt if you’re looking to add a Terripoo to your life."
  },
  {
    "name": "Valley Bulldog",
    "avatar_filename": "valley_bulldog",
    "introduction": "The Valley Bulldog is a mixed breed dog–a cross between the Boxer and English Bulldog breeds. Medium in size, active, and loyal, these pups inherited some of the best qualities from both of their parents\n\nValley Bulldogs also go by the name Bull Boxer. Despite their unfortunate status as a designer breed, you can find these mixed pups in shelters and breed-specific rescues, so remember to adopt! Don’t shop!"
  },
  {
    "name": "Westiepoo",
    "avatar_filename": "westiepoo",
    "introduction": "The Westiepoo is a mixed breed dog — a cross between the West Highland White Terrier and Poodle dog breeds. Clever, active, and affectionate, these pups inherited some of the best qualities from both of their parents.\n\nWestiepoos are also sometimes known as Westiedoodles and Wee-Poos. You can find these mixed breed dogs in shelters and breed specific rescues, so remember to always adopt! Don’t shop if you’re looking to add a Westiepoo to your home!"
  }
]

将json文件放到Assets目录下。


image.png

然后添加图片资源,放到drawable-xxhdpi中。


image.png

3.新增repository和model层
image.png

这里model 是一个Dog类,存储了狗的一些基本信息。
修改gradle。

plugins {
    id 'com.android.application'
   ...
    id 'kotlin-parcelize'
}
dependencies {
    ...
    implementation 'com.google.code.gson:gson:2.9.1'
}
@Parcelize
data class Dog(
    val name: String,
    @SerializedName("avatar_filename")
    val avatarFilename: String,
    val introduction: String,
    var adopted: Boolean
) : Parcelable

新建一个DataHelper类,用于从dogs.json加载数据到Dog.kt这个model类。数据加载是耗时操作,因此使用suspend修饰,使用协程。

class DataHelper {
    /**
     * Read the data from dogs.json and parse it into Dog object list to return.
     */
    suspend fun getDogList() = withContext(Dispatchers.Default) {
            var dogs: List<Dog> = ArrayList()
            try {
                val assetsManager = GlobalApp.context.assets
                val inputReader = InputStreamReader(assetsManager.open("dogs.json"))
                val jsonString = BufferedReader(inputReader).readText()
                val typeOf = object : TypeToken<List<Dog>>() {}.type
                dogs = Gson().fromJson(jsonString, typeOf)
            } catch (e: Exception) {
                e.printStackTrace()
            }
            dogs
        }
}

另外,由于加载assets需要context,所以定义一个伴生对象,提供一个applicationContext。

class GlobalApp : Application() {

    companion object {
        /**
         * Global application context.
         */
        @SuppressLint("StaticFieldLeak")
        lateinit var context: Context
    }

    override fun onCreate() {
        super.onCreate()
        context = applicationContext
    }
}

然后在AndroidManifest中添加application。

增加Repository层,新增类Repository,repository从DataHelper获取数据

class Repository(private val dataHelper: DataHelper) {

    /**
     * Get the dogs by DataHelper and return them.
     */
    suspend fun getDogList() : List<Dog> {
       return dataHelper.getDogList()
    }

}

定义一个ServiceLocator,使用object修饰,获得一个单例。

object ServiceLocator {

    /**
     * Provide the Repository instance that ViewModel should depend on.
     */
    fun provideRepository() = Repository(provideDataHelper())

    /**
     * Provide the DataHelper instance that Repository should depend on.
     */
    private fun provideDataHelper() = DataHelper()
}

4.增加viewmodel层

viewmodel这里需要使用viewmodel扩展库,通过扩展库使用协程。

    // viewmodel
    implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1"

定义一个DogViewModel。

private const val TAG = "DogViewModel"
class DogViewModel: ViewModel(){
    val dogListLiveData: MutableLiveData<List<Dog>> = MutableLiveData()

    private val repository: Repository = ServiceLocator.provideRepository()

    private val dogList  = mutableListOf<Dog>()

    init {
        // 初始化数据
        viewModelScope.launch {
            dogList.addAll(
                //处理耗时操作
                repository.getDogList()
            )
            Log.d(TAG,"inner init DogViewModel " +
                    "size = ${dogList.size}")
            // 更新数据
            dogListLiveData.postValue(dogList)
        }
    }

    val dogLiveData: MutableLiveData<Dog> = MutableLiveData()

    fun initSelectedDog(position: Int){
        Log.d(TAG,"inner initSelectedDog")
        val selectedDog = dogList[position]
        dogLiveData.postValue(selectedDog)
    }

    fun updateDog(dog: Dog,position: Int){
        Log.d(TAG,"inner updateDog")
        dogList[position] = dog
        // 更新数据
        dogListLiveData.postValue(dogList)
    }

}

5.添加recyclerview ,定义Adapter和ViewHolder
添加recyclerview和cardview 依赖。

    // 添加 recyclerview
    implementation 'androidx.recyclerview:recyclerview:1.2.1'
    implementation "androidx.cardview:cardview:1.0.0"
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="220dp"
    android:layout_margin="10dp">

    <androidx.cardview.widget.CardView
        android:id="@+id/card_view"
        android:layout_width="match_parent"
        android:layout_height="220dp"
        android:background="@color/white"
        android:elevation="3dp"
        app:cardCornerRadius="10dp"
        app:cardUseCompatPadding="true">

        <ImageView
            android:id="@+id/dogImageView"
            android:layout_width="match_parent"
            android:layout_height="220dp"
            android:adjustViewBounds="true"
            android:scaleType="fitXY"
            android:src="@drawable/alaskan_klee_kai"
         />

        <TextView
            android:id="@+id/dogNameTextView"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:background="@drawable/edge"
            android:padding="10dp"
            android:text="TextView"
            android:textColor="@color/white"
            android:textSize="16sp"
            android:layout_gravity="bottom"/>

    </androidx.cardview.widget.CardView>
</LinearLayout>
image.png

定义DogListAdapter,这里使用了DiffUtil。

//显示DogList数据
private const val TAG = "DogListAdapter"
class DogListAdapter : ListAdapter<Dog,DogListHolder>(DogDiffCallback()) {

/*    private val dogList  = mutableListOf<Dog>()
    fun setList(dogList: List<Dog>) {
        this.dogList.addAll(dogList)
    }*/

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DogListHolder {
        val layoutInflater = LayoutInflater.from(parent.context)
        val view = layoutInflater.inflate(R.layout.dog_list_item, parent, false)
        return DogListHolder(view)
    }

    override fun onBindViewHolder(holder: DogListHolder, position: Int) {
        Log.d(TAG, "inner onBindViewHolder")
       // val dog = dogList.get(position)
        val dog = getItem(position)
        Log.d(TAG, "dog = $dog")
        holder.bind(dog)
        holder.bindItemClick(onClick)
    }

    private lateinit var onClick: (position: Int) -> Unit

    fun setOnItemClickListener(onClick : (position: Int) -> Unit){
        this.onClick = onClick
    }

/*    override fun getItemCount(): Int {
        return dogList.size
    }*/
}

class DogDiffCallback : DiffUtil.ItemCallback<Dog>() {

    override fun areItemsTheSame(oldItem: Dog, newItem: Dog): Boolean {
        return oldItem.hashCode() == newItem.hashCode()
    }

    override fun areContentsTheSame(oldItem: Dog, newItem: Dog): Boolean {
        return oldItem == newItem
    }

}

定义DogListHolder。

private const val TAG = "DogListHolder"
class DogListHolder(view: View) : RecyclerView.ViewHolder(view){
    lateinit var dog:Dog

    private val image: ImageView = itemView.findViewById(R.id.dogImageView)

    private val dogNameTextView: TextView = itemView.findViewById(R.id.dogNameTextView)

    // 把数据和视图的绑定工作都放在Holder里处理
    fun bind(dog: Dog) {
        this.dog = dog
        val imageIdentity =
            GlobalApp.context.resources.getIdentifier(
                this.dog.avatarFilename, "drawable",
                GlobalApp.context.packageName
            )
        image.setImageResource(imageIdentity)
        dogNameTextView.text = this.dog.name + "  " +
                if (this.dog.adopted){
                    "Adopted"
                }else{
                    "Not Adopted"
                }
    }


    fun bindItemClick(onClick : (position: Int) -> Unit) {
        Log.d(TAG,"absoluteAdapterPosition = $absoluteAdapterPosition")
        //将 absoluteAdapterPosition 数据回传
        //设置 itemView 监听
        itemView.setOnClickListener{
            onClick(absoluteAdapterPosition)
        }
    }
}

6.使用navigation
开启viewBinding ,添加navigation依赖。

android {
    ...
    viewBinding {
        enabled = true
    }
}
dependencies {
    ...
    //Navigation
    implementation "androidx.navigation:navigation-fragment-ktx:2.5.3"
    implementation "androidx.navigation:navigation-ui-ktx:2.5.3"
}

MainActivity 如下所示。

class MainActivity : AppCompatActivity() {
    private val binDing by lazy {
        ActivityMainBinding.inflate(layoutInflater)
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(binDing.root)
    }

}

修改activity_main.xml,使用FragmentContainerView。

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/container"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">
    <androidx.fragment.app.FragmentContainerView
        android:id="@+id/my_nav_host_fragment"
        android:name="androidx.navigation.fragment.NavHostFragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:defaultNavHost="true"
        app:navGraph="@navigation/nav" />
</FrameLayout>
image.png

res目录下新增navigation目录,再新建nav.xml。
新增DogListFragment和DogDetailFragment。
设置默认fragment为DogListFragment,并添加action。
nav.xml如下所示。

<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/nav"
    app:startDestination="@id/dogListFragment">

    <fragment
        android:id="@+id/dogListFragment"
        android:name="com.example.dogdisplay.DogListFragment"
        android:label="DogListFragment" >
        <action
            android:id="@+id/action_dogListFragment_to_dogDetailFragment"
            app:destination="@id/dogDetailFragment" />
    </fragment>
    <fragment
        android:id="@+id/dogDetailFragment"
        android:name="com.example.dogdisplay.DogDetailFragment"
        android:label="DogDetailFragment" />
</navigation>

DogListFragment和DogDetailFragment具体如下。

class DogListFragment : Fragment() {

    private  val dogViewModel: DogViewModel by activityViewModels()

    private lateinit var dogRecyclerView: RecyclerView

    private lateinit var adapter: DogListAdapter


    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // TODO: Use the ViewModel

    }

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        val view = inflater.inflate(R.layout.fragment_dog_list, container, false)
        dogRecyclerView =  view.findViewById(R.id.recyclerView) as RecyclerView
        dogRecyclerView.layoutManager = LinearLayoutManager(context)
        // 为RecyclerView配置adapter
        adapter = DogListAdapter()

        adapter.setOnItemClickListener {
            Log.d("adapter","position = $it")
            val host: NavHostFragment = requireActivity().supportFragmentManager.findFragmentById(R.id.my_nav_host_fragment) as NavHostFragment
            val navController = host.navController
            // 设置显示 dogDetailFragment
            val args = Bundle()
            args.putInt("position", it)
            //跳转到带参数的 fragment
            //调用navigate跳转之前先判断
            if (navController.currentDestination?.id == R.id.dogListFragment) {
                navController.navigate(R.id.action_dogListFragment_to_dogDetailFragment,args);
            }
        }
        dogRecyclerView.adapter = adapter
        return view
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        startObserver()
    }

    private fun startObserver() {
        dogViewModel.dogListLiveData.observe(viewLifecycleOwner){
            // adapter.setList(it)
            //数据改变刷新视图
          //  adapter.notifyDataSetChanged()
            // 使用 submitList() 及时更新列表
            it?.let {
                Log.d("DogListFragment","submitList")
                adapter.submitList(it)
            }
        }
    }
}

fragment_dog_list.xml如下。

<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recyclerView"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:scrollbars="vertical"
        app:layoutManager="LinearLayoutManager"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

DogDetailFragment 定义如下。

private const val ARG_POSITION = "position"
private const val TAG = "DogDetailFragment"
class DogDetailFragment : Fragment() {

    // 在 fragment 中通过 by activityViewModels(),可以实现在fragment之间
    // 共享同一个 dogList 数据,DogViewModel的init 只执行一次
    // 这里通过DogViewModel 初始化 dog ,修改 dogList 数据
    private  val dogViewModel: DogViewModel by activityViewModels()
    private lateinit var selectedDog: Dog
    private var selectedPosition = 0

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
       //fragment中 订阅的时机,一般会选择放到 onViewCreated 中进行
        Log.d(TAG,"inner onViewCreated")
        startObserver()
    }


    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        Log.d(TAG,"inner onCreate")
        // 写在onCreate中会报错
        // java.lang.IllegalStateException: Can't access the Fragment View's LifecycleOwner when getView() is null i.e., before onCreateView() or after onDestroyView()
        // startObserver()
        arguments?.let {
            selectedPosition = it.getInt(ARG_POSITION)
        }

        // 初始化数据
        dogViewModel.initSelectedDog(selectedPosition)
    }

    private fun startObserver() {
        Log.d(TAG,"inner startObserver")
        dogViewModel.dogLiveData.observe(viewLifecycleOwner){
            Log.d(TAG,"dog = $it")
            // 初始化dog
            selectedDog = it
            Log.d(TAG,"selectedDog = ${selectedDog.name} , adopted = " +
                    " ${selectedDog.adopted}")
            updateUI()
        }
    }

    private fun updateUI(){
        Log.d(TAG,"inner updateUI")
        // toolbar 使用
        _binding?.toolbar?.title = selectedDog.name
        _binding?.toolbar?.setNavigationOnClickListener {
            Log.d(TAG,"inner NavigationOnClickListener")
            // fragmentManager 已废弃
            parentFragmentManager.popBackStackImmediate()
/*            fragmentManager?.popBackStack("DogDetailFragment",
                FragmentManager.POP_BACK_STACK_INCLUSIVE)*/
        }

        val imageIdentity = resources.getIdentifier(
            selectedDog.avatarFilename, "drawable",
            GlobalApp.context.packageName
        )
        _binding?.avatarImageView?.setImageResource(imageIdentity)

        Log.d(TAG," selectedDog.adopted = ${selectedDog.adopted}")
        if (selectedDog.adopted){
            _binding?.adoptButton?.isEnabled = false
            Log.d(TAG," isEnabled = false")
        }else{
            _binding?.adoptButton?.isEnabled = true
            Log.d(TAG," isEnabled = true")
        }
        _binding?.adoptButton?.text = if (selectedDog.adopted) "Adopted" else "Adopt"
        _binding?.adoptButton?.setOnClickListener {
            showDialog()
        }

        _binding?.introductionTextView?.text = selectedDog.introduction
    }


    private var _binding: FragmentDogDetailBinding? = null
    private val binding get() = _binding!!

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        // Inflate the layout for this fragment
        Log.d(TAG,"inner onCreateView")
        _binding = FragmentDogDetailBinding.inflate(inflater, container, false)
        return binding.root
    }


    private fun showDialog() {
        val builder : AlertDialog.Builder = createDialog()
        // 创建dialog
        val dialog : AlertDialog = builder.create()
        // 设置点击弹框以外的区域会不会消失,true消失,false不消失
        dialog.setCanceledOnTouchOutside(false)
        dialog.show()
    }

    // 创建build对象
    private fun createDialog() : AlertDialog.Builder{
        val builder : AlertDialog.Builder =
            AlertDialog.Builder(requireActivity())
        builder.setMessage("Do you want to adopt this lovely dog?")
        builder.setNegativeButton("No") { _, _ ->
            Toast.makeText(context, "No", Toast.LENGTH_SHORT).show()
        }
        builder.setPositiveButton("Yes") { _, _ ->
            Toast.makeText(context, "Yes", Toast.LENGTH_SHORT).show()
            // 修改adopted状态
            selectedDog.adopted = !selectedDog.adopted
            Log.d(TAG," selectedPosition = $selectedPosition")
            dogViewModel.updateDog(selectedDog,selectedPosition)
            if (selectedDog.adopted){
                _binding?.adoptButton?.text = "Adopted"
                _binding?.adoptButton?.isEnabled = false
            }
        }
        return builder
    }
}

fragment_dog_detail.xml如下。

<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent">


    <androidx.appcompat.widget.Toolbar
        android:id="@+id/toolbar"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        app:title="name"
        android:background="@color/white"
        android:minHeight="?attr/actionBarSize"
        app:navigationIcon="@drawable/ic_arrow_back"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintVertical_bias="0.0" />

    <com.google.android.material.imageview.ShapeableImageView
        android:id="@+id/avatarImageView"
        android:layout_width="150dp"
        android:layout_height="150dp"
        android:scaleType="fitXY"
        android:src="@drawable/alaskan_klee_kai"
        app:shapeAppearance="@style/CircleStyle"
        app:layout_constraintBottom_toTopOf="@+id/adoptButton"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/toolbar"/>
    <Button
        android:id="@+id/adoptButton"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="20dp"
        android:layout_marginBottom="20dp"
        android:text="Adopt"
        app:layout_constraintBottom_toTopOf="@+id/introductionTextView"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/avatarImageView" />

    <TextView
        android:id="@+id/introductionTextView"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:layout_margin="10dp"
        android:text="introduction"
        android:textSize="18sp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.0"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/adoptButton"
        app:layout_constraintVertical_bias="0.0" />
</androidx.constraintlayout.widget.ConstraintLayout>
image.png

ic_arrow_back是Vector Asset中的图标。


image.png

另外,AndroidManifest中修改下activity的主题。

//AndroidManifest.xml
 <activity
            android:name=".MainActivity"
            android:exported="true"
            android:theme="@style/AppTheme_NoActionBar">


//res/values/themes.xml
    <style name="AppTheme_NoActionBar" parent="Theme.DogDisplay">
        <item name="windowActionBar">false</item>
        <item name="windowNoTitle">true</item>
        <item name="textAllCaps">false</item>
    </style>
    <!--ShapeableImageView 圆 -->
    <style name="CircleStyle">
        <item name="cornerFamily">rounded</item>
        <item name="cornerSize">50%</item>
    </style>

至此,使用navigation+viewmodel+livedata显示数据的demo完成。接下来,来看看compose版本的写法。

二、compose版本

新建一个module,使用compose activity。


image.png

这里navigation的使用与上面有差别,viewmodel的定义没有差别,使用上有一点差别。
添加依赖,注意,版本别用太高的,有坑。例如我使用lifecycle-viewmodel-compose:2.6.0-alpha03就有坑,数据更新后,view不更新,回退一个版本就好。


    implementation 'com.google.code.gson:gson:2.9.1'

    //livedata
    implementation "androidx.compose.runtime:runtime-livedata:1.3.0-rc01"
    //viewmodel
  //  implementation "androidx.lifecycle:lifecycle-viewmodel-compose:2.6.0-alpha03"
    implementation "androidx.lifecycle:lifecycle-viewmodel-compose:2.6.0-alpha02"
    // viewmodel扩展依赖库
    implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1"
    //ktx
   // implementation "androidx.fragment:fragment-ktx:1.5.4"
    //navigation
    implementation "androidx.navigation:navigation-compose:2.6.0-alpha02"

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            DogDisplayTheme {
                // A surface container using the 'background' color from the theme
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colors.background
                ) {
                  //  Greeting("Android")
                    FragmentNavigation()
                }
            }
        }
    }
}

定义相关compose。
FragmentCompose.kt,RecyclerviewCompose.kt和DisplayDogDetailCompose.kt。

//FragmentCompose.kt
private const val TAG = "Navigation"

@Composable
fun FragmentNavigation(
    startDestination: String = "dogList",
    dogViewModel: DogViewModel = viewModel()
) {
    // https://developer.android.com/topic/libraries/architecture/viewmodel?hl=zh-cn#sharing
    // https://developer.android.com/jetpack/compose/libraries?hl=zh-cn
    // 如果您使用 Architecture Components ViewModel 库,
    // 可以通过调用 viewModel() 函数,从任何可组合项访问 ViewModel。
    // https://developer.android.com/guide/fragments/communicate?hl=zh-cn
    // 在目的地之间传递数据
    /*
    * 通常情况下,强烈建议您仅在目的地之间传递最少量的数据。
    * 例如,您应该传递键来检索对象而不是传递对象本身,
    * 因为在 Android 上用于保存所有状态的总空间是有限的。
    * 如果您需要传递大量数据,不妨考虑使用 ViewModel(如在 Fragment 之间共享数据中所述)
    * */
    // https://developer.android.com/guide/navigation/navigation-pass-data?hl=zh-cn
    val navController = rememberNavController()
    NavHost(
        navController = navController,
        startDestination = startDestination,
    ) {
        // 定义 route 为 dogList
        // 切勿将 ViewModel 实例传递给其他可组合项,请仅传递其所需要的数据以及以参数形式执行所需逻辑的函数。
        composable("dogList") {
            val dogList by dogViewModel.dogListLiveData.observeAsState(listOf())
            // 显示DogList
            DogListFragment(navController,dogList)
        }
        // 在目的地之间传递数据
        // https://developer.android.com/guide/navigation/navigation-pass-data?hl=zh-cn#supported_argument_types
        // 向路线中添加参数占位符
        composable("dogDetail/{position}", arguments = listOf(
            navArgument("position") {
                type = NavType.IntType  //类型
            })
        ) {
            val dog by dogViewModel.dogLiveData.observeAsState()
            Log.d(TAG,"after observeAsState")
            val position = it.arguments?.getInt("position", 0)?: 0
            Log.d(TAG,"position = $position ")
            // 在viewmodel中初始化 selected dog 数据
            dogViewModel.initSelectedDog(position)
            Log.d(TAG,"dog = $dog")

            //传递 dog 数据 到 fragment,并传入回调函数 ,更新 dogList 数据
            dog?.let { it1 -> DogDetailFragment(navController,it1){ changedDog ->
                dogViewModel.updateDog(changedDog,position)
            }}
        }

    }


}


@Composable
fun DogListFragment(navController: NavController,dogList: List<Dog>) {
    DisplayDogListWithOnClick(dogList){ position, dog ->
        // 监听
        Log.d(
            TAG,"inner DisplayDogListWithOnClick, position = $position," +
                "dog = $dog")
        // 传递数据
        navController.navigate("dogDetail/$position")
    }
}


@Composable
fun DogDetailFragment(navController: NavController,selectedDog: Dog,
                      onChanged: (changedDog: Dog) -> Unit) {
    Log.d(TAG,"inner DogDetailFragment ,dog = $selectedDog")
    // 显示的Dog详情页
    Scaffold(
        // 顶部导航栏
        topBar = {
            TopAppBar(
                title = {
                    Text(
                        text = selectedDog.name
                    )
                },
                backgroundColor = Color.Transparent, elevation = 0.dp,
                navigationIcon = {
                    IconButton(onClick = {
                        // 回退栈
                        navController.popBackStack()
                    }) {
                        val backIcon: Painter = painterResource(R.drawable.ic_arrow_back)
                        Icon(painter = backIcon, contentDescription = "ic_back")
                    }
                }
            )
        }
    ) {
        DisplayDogDetail(dog = selectedDog){changedDog ->
            onChanged(changedDog)
        }
    }
}

//RecyclerviewCompose.kt
@Composable
fun DisplayDogListWithOnClick(dogList: List<Dog>,onClick: (position: Int,dog: Dog)-> Unit){
    LazyColumn {
        itemsIndexed(dogList) { position, dog ->
            //显示
            DisplayDogItem(position,dog,onClick)
        }
    }
}

@Composable
fun DisplayDogItem(position: Int, dog: Dog, onClick: (position: Int, dog: Dog) -> Unit) {
    Card(
        elevation = 4.dp,
        shape = RoundedCornerShape(8.dp),
        modifier = Modifier
            .padding(start = 16.dp, end = 16.dp, top = 8.dp, bottom = 8.dp)
            .fillMaxWidth()
            .requiredHeight(220.dp)
            .clickable { onClick(position, dog) }
    ) {
        CardItem(name = dog.name, avatar = dog.avatarFilename, isAdopted = dog.adopted)
    }
}

@Composable
fun CardItem(name: String, avatar: String, isAdopted :Boolean) {
    val imageIdentity = GlobalApp.context.resources.getIdentifier(
        avatar, "drawable",
        GlobalApp.context.packageName
    )
    val image: Painter = painterResource(imageIdentity)
    Image(
        painter = image,
        contentDescription = name,
        modifier = Modifier
            .fillMaxWidth()
            .clip(shape = RoundedCornerShape(8.dp)),
        contentScale = ContentScale.Crop
    )
    Column(
        modifier = Modifier.fillMaxSize(),
        verticalArrangement = Arrangement.Bottom
    ) {
        Surface(
            color = Color(0x99000000),
            modifier = Modifier.fillMaxWidth()
        ) {
            Text(
                text = if (isAdopted){
                    "$name,Adopted"
                }else{
                    "$name,Not Adopted"
                },
                color = Color.White,
                fontSize = 20.sp,
                modifier = Modifier.padding(8.dp)
            )
        }
    }
}
//DisplayDogDetailCompose.kt
// mutableStateOf
//会给变量赋予监听数值变化的能力,从而会触发使用该值的View进行重绘
var showConfirmDialog by mutableStateOf(false)
@Composable
fun DisplayDogDetail(dog: Dog,onChanged: (changedDog: Dog) -> Unit) {
   // val stateDog by remember { mutableStateOf(dog) }
    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(16.dp)
            .verticalScroll(rememberScrollState())
    ) {
        //显示Dog图片
        DogAvatar(
            avatar = dog.avatarFilename,
            name = dog.name
        )
        Spacer(
            modifier = Modifier.requiredHeight(26.dp)
        )
        // Adopt 按钮
        AdoptButton(
            adopted = dog.adopted
        )

        Spacer(
            modifier = Modifier.requiredHeight(26.dp)
        )
        DogIntroduction(
            introduction = dog.introduction
        )
    }
    if (showConfirmDialog) {
        AdoptConfirmDialog(dog = dog){
            onChanged(it)
        }
    }
}

//显示Dog图片
@Composable
fun DogAvatar(avatar: String, name: String) {
    val imageIdentity = GlobalApp.context.resources.getIdentifier(
        avatar, "drawable",
        GlobalApp.context.packageName
    )
    val image: Painter = painterResource(imageIdentity)
    Box(
        modifier = Modifier.fillMaxWidth(),
        contentAlignment = Alignment.Center
    ) {
        Image(
            painter = image,
            contentDescription = name,
            modifier = Modifier
                .requiredSize(150.dp)
                .clip(shape = CircleShape),
            contentScale = ContentScale.Crop
        )
    }
}

//Dog 介绍
@Composable
fun DogIntroduction(introduction: String) {
    Text(
        text = introduction,
        fontSize = 18.sp,
        style = MaterialTheme.typography.body1
    )
}

@Composable
fun AdoptButton(adopted: Boolean) {
    Box(
        modifier = Modifier.fillMaxWidth(),
        contentAlignment = Alignment.Center
    ) {
        Button(
            onClick = { showConfirmDialog = true },
            enabled = !adopted
        ) {
            Text(text = if (adopted) "Adopted" else "Adopt")
        }
    }
}

@Composable
fun AdoptConfirmDialog(dog: Dog,onChanged: (changedDog: Dog) -> Unit) {
    AlertDialog(
        onDismissRequest = {
            showConfirmDialog = false
        },
        text = {
            Text(
                text = "Do you want to adopt this lovely dog?",
                style = MaterialTheme.typography.body2
            )
        },
        confirmButton = {
            Button(
                onClick = {
                    showConfirmDialog = false
                    dog.adopted = true
                    // update
                    onChanged(dog)
                }
            ) {
                Text(
                    text = "Yes"
                )
            }
        },
        dismissButton = {
            Button(
                onClick = {
                    showConfirmDialog = false
                }
            ) {
                Text(
                    text = "No"
                )
            }
        }
    )
}

image.png

其实compose最直观的感受,就是xml没有了,也不用定义nav.xml了。并且生命周期更简单了,而且compose中使用recyclerview,变得更简单了,不用定义adapter和viewholder了。
简洁,是用kotlin+compose 给我最直观的感受,Compose 原理可参考这几篇:
沉思录 | 揭秘 Compose 原理:图解 Composable 的本质
【辨析】Compose 完全脱离 View 系统了吗?
Jetpack Compose Runtime : 声明式 UI 的基础

Compose 将通过读取 State<T> 对象自动重组界面。
如果您在 Compose 中使用 LiveData 等其他可观察类型,应该先将其转换为 State<T>,然后再使用诸如 LiveData<T>.observeAsState() 之类的可组合扩展函数在可组合项中读取它。

https://developer.android.google.cn/jetpack/compose/mental-model

声明性编程范式

长期以来,Android 视图层次结构一直可以表示为界面 widget 树。由于应用的状态会因用户交互等因素而发生变化,因此界面层次结构需要进行更新以显示当前数据。最常见的界面更新方式是使用 findViewById() 等函数遍历树,并通过调用 button.setText(String)container.addChild(View)img.setImageBitmap(Bitmap) 等方法更改节点。这些方法会改变 widget 的内部状态。
手动操纵视图会提高出错的可能性。如果一条数据在多个位置呈现,很容易忘记更新显示它的某个视图。此外,当两项更新以出人意料的方式发生冲突时,也很容易造成异常状态。例如,某项更新可能会尝试设置刚刚从界面中移除的节点的值。一般来说,软件维护的复杂性会随着需要更新的视图数量而增长。
在过去的几年中,整个行业已开始转向声明性界面模型,该模型大大简化了与构建和更新界面关联的工程任务。该技术的工作原理是在概念上从头开始重新生成整个屏幕,然后仅执行必要的更改。此方法可避免手动更新有状态视图层次结构的复杂性。Compose 是一个声明性界面框架。

生命周期:

可组合项的生命周期通过以下事件定义:进入组合,执行 0 次或多次重组,然后退出组合。


https://developer.android.google.cn/jetpack/compose/lifecycle

如果某一可组合项多次被调用,在组合中将放置多个实例。每次调用在组合中都有自己的生命周期。

@Composable
fun MyComposable() {
    Column {
        Text("Hello")
        Text("World")
    }
}

Jetpack Compose 架构分层

Jetpack Compose 不是一个单体式项目;它由一些模块构建而成,这些模块组合在一起,构成了一个完整的堆栈。

Jetpack Compose 的主要层包括:


https://developer.android.google.cn/jetpack/compose/layering#anchor
https://developer.android.google.cn/jetpack/compose/layering#anchor

本文代码地址:
https://github.com/VIVILL/SimpleDemo/tree/main/DogDisplay

参考链接:
Compose官方文档
Jetpack Compose 架构分层
我参加了Jetpack Compose开发挑战赛

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容