{"id":1075,"date":"2019-08-22T17:05:00","date_gmt":"2019-08-22T09:05:00","guid":{"rendered":"https:\/\/www.ray650128.com\/wordpress\/?p=1075"},"modified":"2022-02-18T11:24:51","modified_gmt":"2022-02-18T03:24:51","slug":"%e6%9e%b6%e6%a7%8bmvvm%e5%ad%b8%e7%bf%92%e7%b4%80%e9%8c%84","status":"publish","type":"post","link":"https:\/\/blog.ray650128.com\/?p=1075","title":{"rendered":"Android APP\u5e38\u898b\u7684\u67b6\u69cb"},"content":{"rendered":"\n<h2 class=\"wp-block-heading\" id=\"\u524d\u8a00\">\u524d\u8a00<\/h2>\n\n\n\n<p>\u5f9e\u4e8bAndroid\u8edf\u9ad4\u958b\u767c\u4e5f\u6709\u4e00\u9663\u5b50\u4e86\u3002<\/p>\n\n\n\n<p>\u5c0d\u65bc\u67b6\u69cb\u9019\u500b\u8a2d\u8a08\u6a21\u5f0f\u7684\u6982\u5ff5\u4e00\u76f4\u90fd\u662f\u6a21\u7cca\u72c0\u614b\uff0c\u56e0\u6b64\u624d\u6709\u9019\u7bc7\u7684\u7522\u751f\u3002<\/p>\n\n\n\n<p><\/p>\n\n\n\n<p>\u7c21\u55ae\u4f86\u8aaa\u67b6\u69cb\u5c31\u662f\u5c07\u7a0b\u5f0f\u78bc\u4f9d\u7167\u529f\u80fd\u5207\u5272\u6210\u5404\u500b\u6a21\u7d44\uff0c\u5f7c\u6b64\u4e4b\u9593\u53ef\u4ee5\u4e92\u76f8\u5f15\u7528\uff0c\u4f46\u4e0d\u5fc5\u5b8c\u5168\u4f9d\u8cf4\u5c0d\u65b9\u3002<\/p>\n\n\n\n<p>\u56e0\u6b64\u6211\u5011\u53ef\u4ee5\u4efb\u610f\u62bd\u63db\u5176\u4e2d\u4e00\u500b\u6a21\u7d44\uff0c\u5be6\u73fe\u4e0d\u540c\u7684\u529f\u80fd\u3002<\/p>\n\n\n\n<p>\u5c31\u76ee\u524d\u6211\u6240\u4e86\u89e3\u7684\uff0c\u67b6\u69cb\u5927\u81f4\u4e0a\u5206\u70ba\u4e09\u5c64\uff1a<\/p>\n\n\n\n<ul class=\"wp-block-list\"><li>Presentation<\/li><li>Business<\/li><li>Data<\/li><\/ul>\n\n\n\n<p>\u9996\u5148\u662fPresentation-\u8868\u9054\u5c64\u3002\u4ee5\u5716\u5f62\u5316\u4ecb\u9762\u7684\u61c9\u7528\u7a0b\u5f0f\u4f86\u8aaa\uff0c\u9019\u88e1\u6307\u7684\u5c31\u662f\u61c9\u7528\u7a0b\u5f0f\u7684\u756b\u9762\uff0c\u8ca0\u8cac\u986f\u793a\u7d50\u679c\u3001\u8655\u7406\u4f7f\u7528\u8005\u4e92\u52d5\u7b49\u5de5\u4f5c\u3002<\/p>\n\n\n\n<p>\u518d\u4f86\u662fBusiness-\u696d\u52d9\u908f\u8f2f\u5c64\u3002\u63a5\u6536\u8868\u9054\u5c64\u7684\u8acb\u6c42\uff0c\u5c08\u6ce8\u8655\u7406\u61c9\u7528\u7a0b\u5f0f\u7684\u696d\u52d9\u908f\u8f2f\u3002<\/p>\n\n\n\n<p>\u6700\u5f8c\u662fData-\u8cc7\u6599\u5c64\uff0c\u9019\u4e00\u5c64\u4e3b\u8981\u662f\u8ca0\u8cac\u5b58\u53d6\u8cc7\u6599\u3002\u7576\u63a5\u6536\u5230\u696d\u52d9\u908f\u8f2f\u5c64\u547c\u53eb\u6642\uff0c\u8cc7\u6599\u5c64\u5c31\u6703\u5f9e\u7db2\u8def\u3001\u6a94\u6848\u3001\u6216\u662f\u8cc7\u6599\u5eab\u7b49\u5730\u65b9\u53d6\u5f97\u8cc7\u6599\uff0c\u57f7\u884c\u8655\u7406\u4e4b\u5f8c\u518d\u56de\u50b3\u3002<\/p>\n\n\n\n<p><\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"mvc\">MVC<\/h2>\n\n\n\n<p>MVC\u5168\u540d\u662fModel-View-Controller\u3002<\/p>\n\n\n\n<p>\u5728MVC\u7684\u8a2d\u8a08\u7406\u5ff5\u4e2d\uff1a<\/p>\n\n\n\n<ul class=\"wp-block-list\"><li>Model\u5c64-\u5c31\u8ca0\u8cac\u8655\u7406\u8cc7\u6599<\/li><li>View\u5c64-\u986f\u793a\u7d50\u679c\u53ca\u8655\u7406\u4f7f\u7528\u8005\u4e92\u52d5<\/li><li>Controller\u5c64-\u7a76\u8cac\u8655\u7406\u696d\u52d9\u908f\u8f2f\u3002<\/li><\/ul>\n\n\n\n<p>\u4ee5\u4e0b\u662fMVC\u67b6\u69cb\u7684\u793a\u610f\u5716\uff1a<\/p>\n\n\n\n<div class=\"wp-block-image\"><figure class=\"aligncenter size-full is-resized\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/www.ray650128.com\/wordpress\/wp-content\/uploads\/2021\/10\/MVC.png\" alt=\"\" class=\"wp-image-1200\" width=\"376\" height=\"263\" srcset=\"https:\/\/blog.ray650128.com\/wp-content\/uploads\/2021\/10\/MVC.png 634w, https:\/\/blog.ray650128.com\/wp-content\/uploads\/2021\/10\/MVC-300x210.png 300w\" sizes=\"auto, (max-width: 376px) 100vw, 376px\" \/><\/figure><\/div>\n\n\n\n<p><\/p>\n\n\n\n<p>\u82e5\u5c0d\u7167Android APP\u7684\u7d44\u4ef6\uff0c\u5206\u5225\u5982\u4e0b\uff1a<\/p>\n\n\n\n<ul class=\"wp-block-list\"><li>Model-\u8cc7\u6599\u53ca\u5e8f\u5217\u5316\u5f8c\u7684\u8cc7\u6599\u6a21\u578b\uff0c\u4f86\u6e90\u53ef\u80fd\u662f\u5f9e\u6a94\u6848\u3001\u7db2\u8def\u3001\u672c\u5730\u8cc7\u6599\u5eab\u4e0d\u7b49<\/li><li>View-APP\u7684\u756b\u9762\uff0c\u901a\u5e38\u6307\u7684\u662flayout.xml\u6587\u4ef6<\/li><li>Controller-APP\u7684\u696d\u52d9\u908f\u8f2f\uff0c\u901a\u5e38\u6307\u7684\u662fActivity\u3001Fragment\u7b49\u5143\u4ef6<\/li><\/ul>\n\n\n\n<p><\/p>\n\n\n\n<p>\u4f46\u662f\u6211\u5011\u5be6\u969b\u5728\u958b\u767c\u7684\u6642\u5019\uff0c\u5f80\u5f80\u6703\u53d7\u5236\u65bclayout.xml\u7684\u529f\u80fd\u592a\u5f31\uff0c\u5c0e\u81f4\u6211\u5011\u5fc5\u9808\u5c07View\u7684\u8655\u7406\u908f\u8f2f\u5beb\u5728 Activity\u3001Fragment\u4e0a\u3002\u4e45\u800c\u4e45\u4e4b\uff0c Activity\u3001Fragment\u7684\u7a0b\u5f0f\u78bc\u6703\u8d8a\u4f86\u8d8a\u591a\uff0c\u8d8a\u4f86\u8d8a\u80a5\uff0c\u8b8a\u5f97\u96e3\u4ee5\u7dad\u8b77\u3002<\/p>\n\n\n\n<p>\u56e0\u6b64\uff0c\u624d\u6703\u6709MVVM\u4e4b\u985e\u7684\u65b0\u67b6\u69cb\u51fa\u73fe\u3002\u76ee\u7684\u662f\u70ba\u4e86\u89e3\u6c7aView\u548cController\u9ad8\u5ea6\u8026\u5408\u7684\u554f\u984c\u3002<\/p>\n\n\n\n<p><\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"mvvm\">MVVM<\/h2>\n\n\n\n<p>MVVM\u5168\u540d\u662fModel-View-ViewModel\u7684\u7c21\u7a31\uff0c\u4ee5\u4e0b\u662f\u4ed6\u5404\u5c64\u6240\u4ee3\u8868\u7684\u8077\u8cac\uff1a<\/p>\n\n\n\n<ul class=\"wp-block-list\"><li>Model\uff1a\u8cc7\u6599\u5c64\uff0c\u5373Data Model\u3002\u7528\u4f86\u53d6\u5f97\u6216\u662f\u5132\u5b58\u6578\u64da\u3002<\/li><li>View\uff1a\u7a0b\u5f0f\u7684\u756b\u9762\uff0c\u9019\u88e1\u6307\u7684\u662fActivity\u3001Fragment\u3001\u4f48\u5c40\u6587\u4ef6\u7b49\u3002\u7528\u4f86\u986f\u793a\u61c9\u7528\u7a0b\u5f0f\u7684\u4ecb\u9762\u3001\u8ca0\u8cac\u4f7f\u7528\u8005\u4e92\u52d5\u7684\u90e8\u5206\u3002<\/li><li>ViewModel\uff1aView\u548cModel\u7684\u4e2d\u4ecb\u5c64\u3002\u8ca0\u8cac\u8655\u7406\u696d\u52d9\u908f\u8f2f\u3002<\/li><\/ul>\n\n\n\n<p><\/p>\n\n\n\n<p>\u4ee5\u4e0b\u662fMVVM\u67b6\u69cb\u7684\u793a\u610f\u5716\uff1a<\/p>\n\n\n\n<div class=\"wp-block-image\"><figure class=\"aligncenter size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"516\" height=\"97\" src=\"https:\/\/www.ray650128.com\/wordpress\/wp-content\/uploads\/2021\/07\/mvvm.png\" alt=\"\" class=\"wp-image-1077\" srcset=\"https:\/\/blog.ray650128.com\/wp-content\/uploads\/2021\/07\/mvvm.png 516w, https:\/\/blog.ray650128.com\/wp-content\/uploads\/2021\/07\/mvvm-300x56.png 300w\" sizes=\"auto, (max-width: 516px) 100vw, 516px\" \/><\/figure><\/div>\n\n\n\n<p><\/p>\n\n\n\n<p>\u548cMVC\u4e0d\u540c\u7684\u662f\uff0c\u5728MVVM\u67b6\u69cb\u4e2d\uff0cActivity\u3001Fragment\u7b49\u5143\u4ef6\u88ab\u6b78\u985e\u5230\u4e86View\u5c64\uff0c\u56e0\u6b64Activity\u3001Fragment\u53ea\u9700\u8655\u7406\u756b\u9762\u8207\u4f7f\u7528\u8005\u4ea4\u4e92\u5c31\u597d\u3002<\/p>\n\n\n\n<p>\u81f3\u65bc\u696d\u52d9\u908f\u8f2f\uff0c\u5247\u7d93\u7531ViewModel\u8655\u7406\u3002\u800cModel\u5247\u4f9d\u820a\u8ca0\u8cac\u8655\u7406\u8cc7\u6599\u5b58\u53d6\u3002<\/p>\n\n\n\n<hr class=\"wp-block-separator\"\/>\n\n\n\n<p><\/p>\n\n\n\n<p>\u4ee5\u4e0b\u5be6\u969b\u7528\u8b80\u53d6\u624b\u6a5f\u4e2d\u97f3\u6a02\u6a94\uff0c\u4e26\u900f\u904eRecyclerView\u5448\u73fe\u505a\u70ba\u793a\u7bc4\uff1a<\/p>\n\n\n\n<p><\/p>\n\n\n\n<p>\u9996\u5148\u662fMVC\u67b6\u69cb\u7684\u5be6\u4f5c\u65b9\u5f0f\uff1a<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"activity-main-xml\">activity_main.xml<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"xml\" class=\"language-xml line-numbers\">&lt;?xml version=\"1.0\" encoding=\"utf-8\"?&gt;\n&lt;androidx.constraintlayout.widget.ConstraintLayout xmlns:android=\"http:\/\/schemas.android.com\/apk\/res\/android\"\n    xmlns:app=\"http:\/\/schemas.android.com\/apk\/res-auto\"\n    xmlns:tools=\"http:\/\/schemas.android.com\/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    tools:context=\".MainActivity\"&gt;\n\n    &lt;Button\n        android:id=\"@+id\/btnLoad\"\n        android:layout_width=\"0dp\"\n        android:layout_height=\"wrap_content\"\n        android:text=\"\u8b80\u53d6\u6e05\u55ae\"\n        app:layout_constraintEnd_toEndOf=\"parent\"\n        app:layout_constraintStart_toStartOf=\"parent\"\n        app:layout_constraintTop_toTopOf=\"parent\" \/&gt;\n\n    &lt;androidx.recyclerview.widget.RecyclerView\n        android:id=\"@+id\/list\"\n        android:layout_width=\"0dp\"\n        android:layout_height=\"0dp\"\n        app:layout_constraintBottom_toBottomOf=\"parent\"\n        app:layout_constraintEnd_toEndOf=\"parent\"\n        app:layout_constraintStart_toStartOf=\"parent\"\n        app:layout_constraintTop_toBottomOf=\"@+id\/btnLoad\" \/&gt;\n&lt;\/androidx.constraintlayout.widget.ConstraintLayout&gt;<\/code><\/pre>\n\n\n\n<p><\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"mainactivity-kt\">MainActivity.kt<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"kotlin\" class=\"language-kotlin\">class MainActivity : AppCompatActivity() {\n\n    private var songList = ArrayList&lt;Song&gt;()\n\n    private val songAdapter = SongAdapter(songList)\n\n    private val mContext: Context by lazy { this }\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        setContentView(R.layout.activity_main)\n\n        btnLoad.setOnClickListener {\n            \/\/ READ_EXTERNAL_STORAGE \u5c6c\u5371\u96aa\u6b0a\u9650\uff0c\u56e0\u6b64\u9700\u52d5\u614b\u7533\u8acb\n            checkAndRequirePermission()\n        }\n\n        list.apply {\n            adapter = songAdapter\n            setHasFixedSize(true)\n            layoutManager = LinearLayoutManager(mContext, RecyclerView.VERTICAL, false)\n        }\n    }\n\n    override fun onRequestPermissionsResult(\n        requestCode: Int,\n        permissions: Array&lt;out String&gt;,\n        grantResults: IntArray\n    ) {\n        super.onRequestPermissionsResult(requestCode, permissions, grantResults)\n        var isDenied = false\n\n        \/\/ \u8655\u7406 mRequestCode \u56de\u50b3\u7684\u6b0a\u9650\u72c0\u614b\n        if (requestCode == mRequestCode) {\n            for (position in grantResults.indices) {\n                if (grantResults[position] == -1) {\n                    isDenied = true\n                }\n            }\n        }\n\n        \/\/ \u5728\u6b0a\u9650\u90fd\u662f\u88ab\u5141\u8a31\u7684\u72c0\u614b\u4e0b\uff0c\u8b80\u53d6\u6b4c\u66f2\u6e05\u55ae\n        if (!isDenied) {\n            getSongList()\n        }\n    }\n\n    \/**\n     * \u6aa2\u67e5\u4e26\u8981\u6c42\u6b0a\u9650\n     *\/\n    private fun checkAndRequirePermission() {\n        val permission = checkSelfPermission(Manifest.permission.READ_EXTERNAL_STORAGE)\n        if (permission != PackageManager.PERMISSION_GRANTED) {\n            requestPermissions(\n                arrayOf(\n                    Manifest.permission.READ_EXTERNAL_STORAGE\n                ),\n                mRequestCode\n            )\n        } else {\n            getSongList()\n        }\n    }\n\n    \/**\n     * \u53d6\u5f97\u6b4c\u66f2\u6e05\u55ae\n     *\/\n    private fun getSongList() {\n        songList.clear()\n\n        \/\/retrieve song info\n        val musicResolver = mContext.contentResolver\n        val musicUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI\n        val selection = MediaStore.Audio.Media.IS_MUSIC\n        val sortOrder = MediaStore.Audio.Media.DISPLAY_NAME\n\n        \/\/ \u7d93\u7531ContentProvider\u4f86\u53d6\u5f97\u5916\u90e8\u5132\u5b58\u5a92\u9ad4\u4e0a\u7684\u97f3\u6a02\u6a94\u6848\u7684\u60c5\u5831\n        val musicCursor = musicResolver.query(musicUri, null, selection, null, sortOrder)\n\n        if (musicCursor != null &amp;&amp; musicCursor.moveToFirst()) {\n            \/\/get columns\n            val idColumn = musicCursor.getColumnIndex(MediaStore.Audio.Media._ID)\n            val titleColumn = musicCursor.getColumnIndex(MediaStore.Audio.Media.TITLE)\n            val artistColumn = musicCursor.getColumnIndex(MediaStore.Audio.Media.ARTIST)\n            val totalTimeColumn = musicCursor.getColumnIndex(MediaStore.Audio.Media.DURATION)\n            \/\/add songs to list\n            do {\n                val thisId = musicCursor.getLong(idColumn)\n                val thisTitle = musicCursor.getString(titleColumn)\n                val thisArtist = musicCursor.getString(artistColumn)\n                val thisTime = musicCursor.getInt(totalTimeColumn)\n                val song = Song(thisId, thisTitle, thisArtist, thisTime)\n\n                songList.add(song)\n            } while (musicCursor.moveToNext())\n        }\n\n        musicCursor?.close()\n\n        songAdapter.notifyDataSetChanged()\n\n        Log.i(TAG, \"$songList\")\n    }\n\n    companion object {\n        private const val mRequestCode = 0x100\n        private val TAG = MainActivity::class.java.simpleName\n    }\n}<\/code><\/pre>\n\n\n\n<p><\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"item-song-xml\">item_song.xml<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"xml\" class=\"language-xml line-numbers\">&lt;?xml version=\"1.0\" encoding=\"utf-8\"?&gt;\n&lt;androidx.constraintlayout.widget.ConstraintLayout xmlns:android=\"http:\/\/schemas.android.com\/apk\/res\/android\"\n    xmlns:app=\"http:\/\/schemas.android.com\/apk\/res-auto\"\n    xmlns:tools=\"http:\/\/schemas.android.com\/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"&gt;\n\n    &lt;TextView\n        android:id=\"@+id\/textTitle\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginStart=\"16dp\"\n        android:layout_marginTop=\"16dp\"\n        android:text=\"TextView\"\n        app:layout_constraintStart_toStartOf=\"parent\"\n        app:layout_constraintTop_toTopOf=\"parent\" \/&gt;\n\n    &lt;TextView\n        android:id=\"@+id\/textArtist\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginStart=\"16dp\"\n        android:layout_marginTop=\"8dp\"\n        android:layout_marginBottom=\"16dp\"\n        android:text=\"TextView\"\n        app:layout_constraintBottom_toBottomOf=\"parent\"\n        app:layout_constraintStart_toStartOf=\"parent\"\n        app:layout_constraintTop_toBottomOf=\"@+id\/textTitle\" \/&gt;\n\n    &lt;TextView\n        android:id=\"@+id\/textTime\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginEnd=\"16dp\"\n        android:layout_marginBottom=\"16dp\"\n        android:text=\"TextView\"\n        app:layout_constraintBottom_toBottomOf=\"parent\"\n        app:layout_constraintEnd_toEndOf=\"parent\" \/&gt;\n&lt;\/androidx.constraintlayout.widget.ConstraintLayout&gt;<\/code><\/pre>\n\n\n\n<p><\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"songadapter-kt\">SongAdapter.kt<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"kotlin\" class=\"language-kotlin\">class SongAdapter(data: ArrayList&lt;Song&gt;) : RecyclerView.Adapter&lt;SongAdapter.ViewHolder&gt;() {\n\n    private var mData: ArrayList&lt;Song&gt; = data\n\n    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {\n        val context = parent.context\n        val view = LayoutInflater.from(context).inflate(R.layout.item_song, parent, false)\n        return ViewHolder(view)\n    }\n\n    override fun onBindViewHolder(holder: ViewHolder, position: Int) {\n        val song = mData[position]\n        holder.bindView(song)\n    }\n\n    override fun getItemCount(): Int = mData.size\n\n    inner class ViewHolder(view: View): RecyclerView.ViewHolder(view) {\n        private val textTitle = view.findViewById&lt;TextView&gt;(R.id.textTitle)\n        private val textArtist = view.findViewById&lt;TextView&gt;(R.id.textArtist)\n        private val textTime = view.findViewById&lt;TextView&gt;(R.id.textTime)\n\n        fun bindView(song: Song) {\n            textTitle.text = song.title\n            textArtist.text = song.artist\n            textTime.text = song.timeFormattedString\n        }\n    }\n}<\/code><\/pre>\n\n\n\n<p><\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"song-kt\">Song.kt<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"kotlin\" class=\"language-kotlin line-numbers\">data class Song(\n    var id: Long,\n    var title: String,\n    var artist: String,\n    var time: Int\n) {\n    val timeFormattedString: String\n        get() {\n            val minute = time \/ 1000 \/ 60\n            val sec = time \/ 1000 % 60\n            return \"%02d:%02d\".format(minute, sec)  \/\/ \u56de\u50b3\u7d93\u904e\u683c\u5f0f\u5316\u7684\u6642\u9593\u5b57\u4e32\uff0cex: 00:00\n        }\n}<\/code><\/pre>\n\n\n\n<hr class=\"wp-block-separator\"\/>\n\n\n\n<p>\u518d\u4f86\u662fMVVM\u7684\u7248\u672c\uff08\u7531\u65bclayout.xml\u6587\u4ef6\u3001data class\u5e7e\u4e4e\u4e00\u6a23\uff0c\u56e0\u6b64\u4e0d\u91cd\u8907\u8cbc\u4e0a\u539f\u59cb\u78bc\uff09\uff1a<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"mainactivity-kt\">MainActivity.kt<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"kotlin\" class=\"language-kotlin line-numbers\">class MainActivity : AppCompatActivity() {\n\n    private val viewModel: MainViewModel by viewModels()\n\n    private var songAdapter = SongAdapter(arrayListOf())\n\n    private val mContext: Context by lazy { this }\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        setContentView(R.layout.activity_main)\n\n        viewModel.songList.observe(this, {\n            songAdapter.updateData(it)\n            Log.i(TAG, \"Songs: $it\")\n        })\n\n        btnLoad.setOnClickListener {\n            \/\/ READ_EXTERNAL_STORAGE \u5c6c\u5371\u96aa\u6b0a\u9650\uff0c\u56e0\u6b64\u9700\u52d5\u614b\u7533\u8acb\n            checkAndRequirePermission()\n        }\n\n        list.apply {\n            adapter = songAdapter\n            setHasFixedSize(true)\n            layoutManager = LinearLayoutManager(mContext, RecyclerView.VERTICAL, false)\n        }\n    }\n\n    override fun onRequestPermissionsResult(\n        requestCode: Int,\n        permissions: Array&lt;out String&gt;,\n        grantResults: IntArray\n    ) {\n        super.onRequestPermissionsResult(requestCode, permissions, grantResults)\n        var isDenied = false\n\n        \/\/ \u8655\u7406 mRequestCode \u56de\u50b3\u7684\u6b0a\u9650\u72c0\u614b\n        if (requestCode == mRequestCode) {\n            for (position in grantResults.indices) {\n                if (grantResults[position] == -1) {\n                    isDenied = true\n                }\n            }\n        }\n\n        \/\/ \u5728\u6b0a\u9650\u90fd\u662f\u88ab\u5141\u8a31\u7684\u72c0\u614b\u4e0b\uff0c\u8b80\u53d6\u6b4c\u66f2\u6e05\u55ae\n        if (!isDenied) {\n            viewModel.getSongList()\n        }\n    }\n\n    \/**\n     * \u6aa2\u67e5\u4e26\u8981\u6c42\u6b0a\u9650\n     *\/\n    private fun checkAndRequirePermission() {\n        val permission = checkSelfPermission(Manifest.permission.READ_EXTERNAL_STORAGE)\n        if (permission != PackageManager.PERMISSION_GRANTED) {\n            requestPermissions(\n                arrayOf(\n                    Manifest.permission.READ_EXTERNAL_STORAGE\n                ),\n                mRequestCode\n            )\n        } else {\n            viewModel.getSongList()\n        }\n    }\n\n    companion object {\n        private const val mRequestCode = 0x100\n        private val TAG = MainActivity::class.java.simpleName\n    }\n}<\/code><\/pre>\n\n\n\n<p><\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"songadapter-kt\">SongAdapter.kt<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"kotlin\" class=\"language-kotlin line-numbers\">class SongAdapter(data: ArrayList&lt;Song&gt;) : RecyclerView.Adapter&lt;SongAdapter.ViewHolder&gt;() {\n\n    private var mData: ArrayList&lt;Song&gt; = data\n\n    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {\n        val context = parent.context\n        val view = LayoutInflater.from(context).inflate(R.layout.item_song, parent, false)\n        return ViewHolder(view)\n    }\n\n    override fun onBindViewHolder(holder: ViewHolder, position: Int) {\n        val song = mData[position]\n        holder.bindView(song)\n    }\n\n    override fun getItemCount(): Int = mData.size\n\n    fun updateData(newData: ArrayList&lt;Song&gt;) {\n        val diffCallback = SongDiffCallback(this.mData, newData)\n        val diffResult = DiffUtil.calculateDiff(diffCallback)\n\n        this.mData.clear()\n        this.mData.addAll(newData)\n        diffResult.dispatchUpdatesTo(this)\n    }\n\n    inner class ViewHolder(view: View): RecyclerView.ViewHolder(view) {\n        private val textTitle = view.findViewById&lt;TextView&gt;(R.id.textTitle)\n        private val textArtist = view.findViewById&lt;TextView&gt;(R.id.textArtist)\n        private val textTime = view.findViewById&lt;TextView&gt;(R.id.textTime)\n\n        fun bindView(song: Song) {\n            textTitle.text = song.title\n            textArtist.text = song.artist\n            textTime.text = song.timeFormattedString\n        }\n    }\n\n    inner class SongDiffCallback(oldSongList: List&lt;Song&gt;, newSongList: List&lt;Song&gt;) : DiffUtil.Callback() {\n\n        private val mOldSongList: List&lt;Song&gt; = oldSongList\n        private val mNewSongList: List&lt;Song&gt; = newSongList\n\n        override fun getOldListSize(): Int {\n            return mOldSongList.size\n        }\n\n        override fun getNewListSize(): Int {\n            return mNewSongList.size\n        }\n\n        override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {\n            return mOldSongList[oldItemPosition].id == mNewSongList[newItemPosition].id\n        }\n\n        override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {\n            val oldSong: Song = mOldSongList[oldItemPosition]\n            val newSong: Song = mNewSongList[newItemPosition]\n            return oldSong.title == newSong.title\n        }\n\n    }\n}<\/code><\/pre>\n\n\n\n<p><\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"songrepository-kt\">SongRepository.kt<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"kotlin\" class=\"language-kotlin line-numbers\">object SongRepository {\n\n    private val songList = ArrayList&lt;Song&gt;()\n\n    fun getSongs(): ArrayList&lt;Song&gt; {\n        songList.clear()\n\n        \/\/retrieve song info\n        val musicResolver = MyApp.getAppContext().contentResolver\n        val musicUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI\n        val selection = MediaStore.Audio.Media.IS_MUSIC\n        val sortOrder = MediaStore.Audio.Media.DISPLAY_NAME\n\n        \/\/ \u7d93\u7531ContentProvider\u4f86\u53d6\u5f97\u5916\u90e8\u5132\u5b58\u5a92\u9ad4\u4e0a\u7684\u97f3\u6a02\u6a94\u6848\u7684\u60c5\u5831\n        val musicCursor = musicResolver.query(musicUri, null, selection, null, sortOrder)\n\n        if (musicCursor != null &amp;&amp; musicCursor.moveToFirst()) {\n            \/\/get columns\n            val idColumn = musicCursor.getColumnIndex(MediaStore.Audio.Media._ID)\n            val titleColumn = musicCursor.getColumnIndex(MediaStore.Audio.Media.TITLE)\n            val artistColumn = musicCursor.getColumnIndex(MediaStore.Audio.Media.ARTIST)\n            val totalTimeColumn = musicCursor.getColumnIndex(MediaStore.Audio.Media.DURATION)\n            \/\/add songs to list\n            do {\n                val thisId = musicCursor.getLong(idColumn)\n                val thisTitle = musicCursor.getString(titleColumn)\n                val thisArtist = musicCursor.getString(artistColumn)\n                val thisTime = musicCursor.getInt(totalTimeColumn)\n                val song = Song(thisId, thisTitle, thisArtist, thisTime)\n\n                songList.add(song)\n            } while (musicCursor.moveToNext())\n        }\n\n        musicCursor?.close()\n\n        return songList\n    }\n}<\/code><\/pre>\n\n\n\n<p><\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"mainviewmodel-kt\">MainViewModel.kt<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"kotlin\" class=\"language-kotlin line-numbers\">class MainViewModel: ViewModel() {\n\n    val songList: MutableLiveData&lt;ArrayList&lt;Song&gt;&gt; by lazy {\n        MutableLiveData&lt;ArrayList&lt;Song&gt;&gt;()\n    }\n\n    fun getSongList() {\n        val data = SongRepository.getSongs()\n        songList.postValue(data)\n    }\n}<\/code><\/pre>\n\n\n\n<hr class=\"wp-block-separator\"\/>\n\n\n\n<p>\u900f\u904eMVC\u3001MVVM\u9019\u5169\u500b\u67b6\u69cb\u7684\u5be6\u4f5c\u7d50\u679c\uff0c\u6211\u5011\u53ef\u4ee5\u770b\u5230\uff0cActivity\u7684\u529f\u80fd\u8b8a\u55ae\u7d14\u4e86\u3002<\/p>\n\n\n\n<p>\u539f\u672cActivity\u8981\u8655\u7406\u756b\u9762\u53ca\u4f7f\u7528\u8005\u4ea4\u4e92\u3001\u9084\u8981\u8ca0\u8cac\u52d5\u614b\u53d6\u5f97READ_EXTERNAL_STORAGE\u6b0a\u9650\u3001\u6700\u5f8c\u9084\u8981\u53d6\u5f97\u624b\u6a5f\u4e2d\u7684\u6b4c\u66f2\u8cc7\u6599\u3001\u6700\u5f8c\u986f\u793a\u51fa\u4f86\u3002<\/p>\n\n\n\n<p>\u73fe\u5728\u53ea\u9700\u8655\u7406\u756b\u9762\u548c\u4f7f\u7528\u8005\u4ea4\u4e92\u3001\u52d5\u614b\u53d6\u5f97READ_EXTERNAL_STORAGE\u6b0a\u9650\u5c31\u597d\uff0c\u5269\u4e0b\u7684\u52d5\u4f5c\u5247\u4ea4\u7d66ViewModel\u5b8c\u6210\u3002Activity\u7684\u5de5\u4f5c\u9813\u6642\u8b8a\u5f97\u55ae\u7d14\u8a31\u591a\u3002<\/p>\n\n\n\n<p><\/p>\n\n\n\n<p>\u6700\u5f8c\u9644\u4e0a\u53c3\u8003\u4f86\u6e90\uff1a<\/p>\n\n\n\n<ul class=\"wp-block-list\"><li><a rel=\"noreferrer noopener\" href=\"https:\/\/medium.com\/evan-android-note\/android-tdd-%E7%B3%BB%E5%88%97-19-android-mvvm-%E6%9E%B6%E6%A7%8B-databinding-1de161c3e1df\" data-type=\"URL\" data-id=\"https:\/\/medium.com\/evan-android-note\/android-tdd-%E7%B3%BB%E5%88%97-19-android-mvvm-%E6%9E%B6%E6%A7%8B-databinding-1de161c3e1df\" target=\"_blank\">Android TDD \u7cfb\u5217 \u2014 19 Android MVVM \u67b6\u69cb:DataBinding<\/a><\/li><li><a rel=\"noreferrer noopener\" href=\"https:\/\/zhuanlan.zhihu.com\/p\/346711847\" data-type=\"URL\" data-id=\"https:\/\/zhuanlan.zhihu.com\/p\/346711847\" target=\"_blank\">\u201c\u7ec8\u4e8e\u61c2\u4e86\u201c\u7cfb\u5217\uff1aJetpack AAC\u5b8c\u6574\u89e3\u6790\uff08\u56db\uff09MVVM &#8211; Android\u67b6\u6784\u63a2\u7d22\uff01<\/a><\/li><li><a rel=\"noreferrer noopener\" href=\"https:\/\/zh.wikipedia.org\/wiki\/MVVM\" data-type=\"URL\" data-id=\"https:\/\/zh.wikipedia.org\/wiki\/MVVM\" target=\"_blank\">\u7dad\u57fa\u767e\u79d1MVVM<\/a><\/li><\/ul>\n\n\n\n<p><\/p>\n","protected":false},"excerpt":{"rendered":"<p>\u524d\u8a00 \u5f9e\u4e8bAndroid\u8edf\u9ad4\u958b\u767c\u4e5f\u6709\u4e00\u9663\u5b50\u4e86\u3002 \u5c0d\u65bc\u67b6\u69cb\u9019\u500b\u8a2d\u8a08\u6a21\u5f0f\u7684\u6982\u5ff5\u4e00\u76f4\u90fd\u662f\u6a21\u7cca\u72c0\u614b\uff0c\u56e0\u6b64\u624d\u6709\u9019\u7bc7\u7684\u7522\u751f &hellip; <\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[2],"tags":[32],"class_list":["post-1075","post","type-post","status-publish","format-standard","hentry","category-2","tag-32"],"_links":{"self":[{"href":"https:\/\/blog.ray650128.com\/index.php?rest_route=\/wp\/v2\/posts\/1075","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/blog.ray650128.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/blog.ray650128.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/blog.ray650128.com\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/blog.ray650128.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=1075"}],"version-history":[{"count":4,"href":"https:\/\/blog.ray650128.com\/index.php?rest_route=\/wp\/v2\/posts\/1075\/revisions"}],"predecessor-version":[{"id":1206,"href":"https:\/\/blog.ray650128.com\/index.php?rest_route=\/wp\/v2\/posts\/1075\/revisions\/1206"}],"wp:attachment":[{"href":"https:\/\/blog.ray650128.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=1075"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/blog.ray650128.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=1075"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/blog.ray650128.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=1075"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}