根據Realm Report (2017-Q4,https://realm.io/realm-report/2017-q4 ) ,過去的一年在Android 端的開發:Java 從 95% 下降到 Java 85%, 而 Kotlin 從 5% 漲到 15% ,以下圖所示html
從這個趨勢來看,加上最新 Android Studio 3.0的發佈(內置 Kotlin 開發 Android 項目的支持),Kotlin 將會很快顛覆 Java 在 Android 領域的地位。java
本章將帶領你們快速入門使用 Kotlin 進行 Android 應用程序的開發。android
咱們從一個簡單的 Kotlin 版本的Hello World Android 應用程序開始。git
首先準備好開發工具。Android 開發仍是用 Google 官方支持的 IDE Android Studio 好。程序員
Google 在 2017-10-26 發佈了 Android 8.1 Oreo 開發者預覽版的同時還正式發佈了 Android Studio 3.0 ,爲其 IDE 引入了一系列新功能。github
Android Studio 3.0 專一於加速 Android 應用開發,包含大量更新內容,其中最主要的功能之一就包括對 Kotlin 的支持。正如谷歌在 Google I / O 2017.5 所宣佈的那樣,Kotlin 已被官方支持用於 Android 開發。 Android Studio 3.0是第一個支持 Kotlin 語言的里程碑式版本(在此以前,可使用Android Studio 的 Kotlin 插件的方式)。編程
在該版本中提供了許多方便實用的功能如代碼自動補全和語法高亮顯示,另外,Android Studio 內置轉換工具能夠很是方便地把 Java 代碼轉換成 Kotlin 代碼,以下圖所示json
Android Studio 是 Android 的官方 IDE。Android Studio 3.0的一個亮點就是內置了 Kotlin 的支持(https://developer.android.google.cn/kotlin/index.html)。正如 Google I/O 2017 所說的那樣, Kotlin 已成爲 Android 官方開發語言。後端
使用 Android Studio 3.0, 咱們能夠方便地把Java 源代碼自動轉換成 Kotlin 代碼,也能夠直接建立 Kotlin 語言開發的 Android 項目, 只須要在新建項目的時候勾選 Include Kotlin support 便可。api
首先去官網下載安裝:https://developer.android.google.cn/studio/install.html 。筆者當前下載的安裝包版本是 android-studio-ide-171.4408382-mac.dmg ,下載完畢點擊 dmg 文件
拷貝至應用程序便可。
首先新建項目。若是您未打開項目,請在 Welcome to Android Studio 窗口中點擊 Start a new Android Studio project
若是您已打開項目,請依次點擊 File > New > New Project ,以下圖所示
進入 Create Android Project 對話框。在建立 Android 項目對話框中配置應用基本信息,注意勾選 Kotlin 支持選項,點擊 Next。以下圖所示
進入 Target Android Devices 配置應用運行 SDK 以及環境信息
咱們勾選 Phone and Tablet 選項,API 15:Android 4.0.3 ,點擊 Next 進入添加 Activity 界面
咱們選擇 Empty Activity,點擊 Next,進入配置 Activity 界面
配置好 Activity Name與 Layout Name 以後,點擊 Finish。咱們將獲得一個 Kotlin 版本的Hello World Android 應用程序。工程目錄以下
其中,在頂層的 Gradle 配置文件 build.gradle 中添加了 kotlin-gradle-plugin 插件的依賴
buildscript { ext.kotlin_version = '1.1.51' ... dependencies { classpath 'com.android.tools.build:gradle:3.0.0' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } ...
app 目錄下的build.gradle 配置文件內容以下
apply plugin: 'com.android.application' apply plugin: 'kotlin-android' apply plugin: 'kotlin-android-extensions' dependencies { ... implementation"org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version" ... }
其中的apply plugin: 'kotlin-android-extensions' 表示使用 Kotlin Android Extensions插件。這個插件是 Kotlin 專門針對 Android 擴展的插件,實現了與 Data-Binding、 Dagger等框架的功能。
佈局文件activity_main.xml內容以下
<?xml version="1.0" encoding="utf-8"?> <android.support.constraint.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" tools:context="com.easy.kotlin.myapplication.MainActivity"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Hello World!" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent" /> </android.support.constraint.ConstraintLayout>
MainActivity.kt 代碼以下
package com.easy.kotlin.myapplication import android.support.v7.app.AppCompatActivity import android.os.Bundle class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) } }
AndroidManifest.xml 文件內容以下
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.easy.kotlin.myapplication"> <application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/AppTheme"> <activity android:name=".MainActivity"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application> </manifest>
點擊功能菜單欄中的運行按鈕
會提示咱們選擇應用程序部署運行的目標設備
須要注意的是,手機要打開鏈接 USB 調試模式。點擊 OK,Android Studio 會爲咱們完成打包、安裝等事項。最終的運行效果以下
本節咱們將開發一個Android 應用程序, 列出流行/最高評級的電影, 顯示預告片和評論。
運行效果
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.easy.kotlin"> <application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/AppTheme"> <activity android:name=".ItemListActivity" android:label="@string/app_name" android:theme="@style/AppTheme.NoActionBar"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> <activity android:name=".ItemDetailActivity" android:label="@string/title_item_detail" android:parentActivityName=".ItemListActivity" android:theme="@style/AppTheme.NoActionBar"> <meta-data android:name="android.support.PARENT_ACTIVITY" android:value="com.easy.kotlin.ItemListActivity" /> </activity> </application> </manifest>
其中 android.intent.action.MAIN 處的配置指定了應用程序的啓動Activity 爲 .ItemListActivity , 其中的點號 「.」 表示該類位於 package="com.easy.kotlin" 路徑下。
<activity android:name=".ItemListActivity" android:label="@string/app_name" android:theme="@style/AppTheme.NoActionBar"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity>
下面咱們來介紹應用程序的啓動主類 ItemListActivity 。
Kotlin 代碼以下
package com.easy.kotlin import android.content.Intent import android.os.Bundle import android.support.v7.app.AppCompatActivity import android.support.v7.widget.RecyclerView import android.support.design.widget.Snackbar import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.TextView import com.easy.kotlin.dummy.DummyContent import kotlinx.android.synthetic.main.activity_item_list.* import kotlinx.android.synthetic.main.item_list_content.view.* import kotlinx.android.synthetic.main.item_list.* /** * An activity representing a list of Pings. This activity * has different presentations for handset and tablet-size devices. On * handsets, the activity presents a list of items, which when touched, * lead to a [ItemDetailActivity] representing * item details. On tablets, the activity presents the list of items and * item details side-by-side using two vertical panes. */ class ItemListActivity : AppCompatActivity() { /** * Whether or not the activity is in two-pane mode, i.e. running on a tablet * device. */ private var mTwoPane: Boolean = false override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_item_list) setSupportActionBar(toolbar) toolbar.title = title fab.setOnClickListener { view -> Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG) .setAction("Action", null).show() } if (item_detail_container != null) { // The detail container view will be present only in the // large-screen layouts (res/values-w900dp). // If this view is present, then the // activity should be in two-pane mode. mTwoPane = true } setupRecyclerView(item_list) } private fun setupRecyclerView(recyclerView: RecyclerView) { recyclerView.adapter = SimpleItemRecyclerViewAdapter(this, DummyContent.ITEMS, mTwoPane) } class SimpleItemRecyclerViewAdapter(private val mParentActivity: ItemListActivity, private val mValues: List<DummyContent.DummyItem>, private val mTwoPane: Boolean) : RecyclerView.Adapter<SimpleItemRecyclerViewAdapter.ViewHolder>() { private val mOnClickListener: View.OnClickListener init { mOnClickListener = View.OnClickListener { v -> val item = v.tag as DummyContent.DummyItem if (mTwoPane) { val fragment = ItemDetailFragment().apply { arguments = Bundle() arguments.putString(ItemDetailFragment.ARG_ITEM_ID, item.id) } mParentActivity.supportFragmentManager .beginTransaction() .replace(R.id.item_detail_container, fragment) .commit() } else { val intent = Intent(v.context, ItemDetailActivity::class.java).apply { putExtra(ItemDetailFragment.ARG_ITEM_ID, item.id) } v.context.startActivity(intent) } } } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { val view = LayoutInflater.from(parent.context) .inflate(R.layout.item_list_content, parent, false) return ViewHolder(view) } override fun onBindViewHolder(holder: ViewHolder, position: Int) { val item = mValues[position] holder.mIdView.text = item.id holder.mContentView.text = item.content with(holder.itemView) { tag = item setOnClickListener(mOnClickListener) } } override fun getItemCount(): Int { return mValues.size } inner class ViewHolder(mView: View) : RecyclerView.ViewHolder(mView) { val mIdView: TextView = mView.id_text val mContentView: TextView = mView.content } } }
佈局文件 XML 代碼 activity_item_list.xml
<?xml version="1.0" encoding="utf-8"?> <android.support.design.widget.CoordinatorLayout 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" android:fitsSystemWindows="true" tools:context="com.easy.kotlin.ItemListActivity"> <android.support.design.widget.AppBarLayout android:id="@+id/app_bar" android:layout_width="match_parent" android:layout_height="wrap_content" android:theme="@style/AppTheme.AppBarOverlay"> <android.support.v7.widget.Toolbar android:id="@+id/toolbar" android:layout_width="match_parent" android:layout_height="?attr/actionBarSize" app:popupTheme="@style/AppTheme.PopupOverlay" /> </android.support.design.widget.AppBarLayout> <FrameLayout android:id="@+id/frameLayout" android:layout_width="match_parent" android:layout_height="match_parent" app:layout_behavior="@string/appbar_scrolling_view_behavior"> <include layout="@layout/item_list" /> </FrameLayout> <android.support.design.widget.FloatingActionButton android:id="@+id/fab" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="bottom|end" android:layout_margin="@dimen/fab_margin" app:srcCompat="@android:drawable/ic_dialog_email" /> </android.support.design.widget.CoordinatorLayout>
對應的 UI 設計效果圖以下
在使用Android Studio開發Android應用的時候,建立項目時,自動繼承的是AppCompatActivity。這樣咱們能夠在自定義的 Activity 類中添加 android.support.v7.app.ActionBar( API level 7 +)。例如activity_item_list.xml 佈局中的
<android.support.design.widget.AppBarLayout android:id="@+id/app_bar" android:layout_width="match_parent" android:layout_height="wrap_content" android:theme="@style/AppTheme.AppBarOverlay"> <android.support.v7.widget.Toolbar android:id="@+id/toolbar" android:layout_width="match_parent" android:layout_height="?attr/actionBarSize" app:popupTheme="@style/AppTheme.PopupOverlay" /> </android.support.design.widget.AppBarLayout>
Activity 中添加 Toolbar 的代碼是
class ItemListActivity : AppCompatActivity() { /** * Whether or not the activity is in two-pane mode, i.e. running on a tablet * device. */ private var mTwoPane: Boolean = false override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_item_list) setSupportActionBar(toolbar) toolbar.title = title fab.setOnClickListener { view -> Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG) .setAction("Action", null).show() } if (item_detail_container != null) { // The detail container view will be present only in the // large-screen layouts (res/values-w900dp). // If this view is present, then the // activity should be in two-pane mode. mTwoPane = true } setupRecyclerView(item_list) } }
AppCompatActivity 背後也是繼承的 Activity 。推出Android 5.0以後,提供了不少新功能,因而 support v7 也更新了,出現了AppCompatActivity。AppCompatActivity 是用來替代ActionBarActivity的 。 AppCompatActivity 的類圖繼承層次以下
Activity 生命週期以下圖所示(圖來自官網)
相信很多朋友也已經看過這個流程圖了,關於Activity生命週期的幾個過程,咱們簡單說明以下
1.開始啓動Activity,系統會先調用onCreate方法,而後調用onStart方法,最後調用onResume,Activity進入運行狀態。
2.當前Activity被其餘Activity覆蓋其上或被鎖屏:系統會調用onPause方法,暫停當前Activity的執行。
3.當前Activity由被覆蓋狀態回到前臺或解鎖屏:系統會調用onResume方法,再次進入運行狀態。
4.當前Activity轉到新的Activity界面或按Home鍵回到主屏:系統會先調用onPause方法,而後調用onStop方法,進入中止狀態。
5.用戶後退回到此Activity:系統會先調用onRestart方法,而後調用onStart方法,最後調用onResume方法,再次進入運行狀態。
6.當前Activity處於被覆蓋狀態或者後臺不可見狀態,即第2步和第4步,系統內存不足,殺死當前Activity,然後用戶退回當前Activity:再次調用onCreate方法、onStart方法、onResume方法,進入運行狀態。
7.用戶退出當前Activity:系統先調用onPause方法,而後調用onStop方法,最後調用onDestory方法,結束當前Activity。
這個過程能夠用下面的狀態圖來簡單說明
在上面的ItemListActivity.onCreate 函數中,其中的這行代碼
setSupportActionBar(toolbar)
是設置支持的 ActionBar方法。可是咱們發現,這裏並無使用 findViewById()方法來獲取這個 android:id="@+id/toolbar" Toolbar 的 View 對象,以前咱們可能都是這樣寫的
Toolbar toolbar = (Toolbar)findViewById(R.id.toolbar); setSupportActionBar(toolbar);
而這裏直接就使用了 toolbar 這個 Toolbar 的對象變量。這是怎麼作到的呢?其實這是經過 Kotlin Android Extensions 插件作到的。咱們在app 目錄下的 Gradle 配置文件 build.gradle 中添加了這個配置
apply plugin: 'com.android.application' apply plugin: 'kotlin-android' apply plugin: 'kotlin-android-extensions'
有了這個插件咱們就能夠永遠跟 findViewById 說再見了。Kotlin Android Extensions 插件是 Kotlin 針對 Android 開發專門定製的通用插件, 經過它咱們可以以極簡的無縫方式實現從 Activity, Fragment 和 View 佈局組件中建立和獲取視圖 View 。使用 Kotlin 開發 Android 大大減小了咱們的樣板代碼。
就像上面的示例代碼同樣,咱們只要在代碼中直接使用這個佈局組件的 id 名稱做爲變量名便可,剩下的Kotlin 插件會幫咱們所有搞定。Kotlin Android Extensions 插件將會爲咱們生成一些額外的代碼,使得咱們能夠在佈局XML中直接經過 id 獲取到其 View 對象。另外,它還生成一個本地視圖緩存,當第一次使用屬性時,它將執行一個常規的findViewById。但在下一次使用屬性的時候,視圖將從緩存中恢復,所以訪問速度將更快。
只要佈局添加一個 View,在 Activity、View、Fragment 中均可以直接用 id 來引用這個 View,Kotlin 把 Android 編程極簡風格發揮得淋漓盡致。
咱們能夠經過Kotlin 對應的字節碼來更加本質深刻地理解 Kotlin 所作的事情。Android Studio 中跟 IDEA 同樣提供了 Kotlin 的工具箱。在菜單欄中依次選擇 Code > Kotlin > Show Kotlin Bytecode , 以下圖所示
點擊 Show Kotlin Bytecode 以後,咱們將會看到以下圖所示的 Kotlin Bytecode 界面
其中這兩行代碼
setSupportActionBar(toolbar) toolbar.title = title
對應的字節碼是
LINENUMBER 39 L2 ALOAD 0 ALOAD 0 GETSTATIC com/easy/kotlin/R$id.toolbar : I INVOKEVIRTUAL com/easy/kotlin/ItemListActivity._$_findCachedViewById (I)Landroid/view/View; CHECKCAST android/support/v7/widget/Toolbar INVOKEVIRTUAL com/easy/kotlin/ItemListActivity.setSupportActionBar (Landroid/support/v7/widget/Toolbar;)V L3 LINENUMBER 40 L3 ALOAD 0 GETSTATIC com/easy/kotlin/R$id.toolbar : I INVOKEVIRTUAL com/easy/kotlin/ItemListActivity._$_findCachedViewById (I)Landroid/view/View; CHECKCAST android/support/v7/widget/Toolbar ALOAD 0 INVOKEVIRTUAL com/easy/kotlin/ItemListActivity.getTitle ()Ljava/lang/CharSequence; INVOKEVIRTUAL android/support/v7/widget/Toolbar.setTitle (Ljava/lang/CharSequence;)V L4
其實從字節碼中
GETSTATIC com/easy/kotlin/R$id.toolbar : I INVOKEVIRTUAL com/easy/kotlin/ItemListActivity._$_findCachedViewById
咱們已經看到了 Kotlin 爲咱們所作的事情了。反編譯成 Java 代碼可能會看的更加清楚
public final class ItemListActivity extends AppCompatActivity { private boolean mTwoPane; private HashMap _$_findViewCache; protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); this.setContentView(2131361820); this.setSupportActionBar((Toolbar)this._$_findCachedViewById(id.toolbar)); ((Toolbar)this._$_findCachedViewById(id.toolbar)).setTitle(this.getTitle()); ... } public View _$_findCachedViewById(int var1) { if(this._$_findViewCache == null) { this._$_findViewCache = new HashMap(); } View var2 = (View)this._$_findViewCache.get(Integer.valueOf(var1)); if(var2 == null) { var2 = this.findViewById(var1); this._$_findViewCache.put(Integer.valueOf(var1), var2); } return var2; } ... }
其中的ItemListActivity 類中的 HashMap 類型的私有成員變量 _$_findViewCache 就是本地緩存。這裏其實反映出 Kotlin 語言設計的核心思想:經過更高一層的對 Java 的封裝,不只大大簡化了程序員的樣板化的代碼量,同時還根據一些特定的能夠優化的問題場景,順帶提供了更好的性能。
一樣的,上面的代碼中的 fab 變量
fab.setOnClickListener { view -> Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG) .setAction("Action", null).show() }
也是直接使用的佈局 XML 中的 android:id="@+id/fab"
<android.support.design.widget.FloatingActionButton android:id="@+id/fab" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="bottom|end" android:layout_margin="@dimen/fab_margin" app:srcCompat="@android:drawable/ic_dialog_email" />
item_detail_container 、setupRecyclerView(item_list)中的 item_list 都是使用上面的方式。這樣代碼確實是大大精簡了許多。
上面的 activity_item_list.xml 佈局中嵌套的 FrameLayout 佈局配置以下
<FrameLayout android:id="@+id/frameLayout" android:layout_width="match_parent" android:layout_height="match_parent" app:layout_behavior="@string/appbar_scrolling_view_behavior"> <include layout="@layout/item_list" /> </FrameLayout>
裏面的 <include layout="@layout/item_list" /> 表示引用 layout 文件夾下面的 item_list.xml
<?xml version="1.0" encoding="utf-8"?> <android.support.v7.widget.RecyclerView 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:id="@+id/item_list" android:name="com.easy.kotlin.ItemListFragment" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_marginLeft="16dp" android:layout_marginRight="16dp" app:layoutManager="LinearLayoutManager" tools:context="com.easy.kotlin.ItemListActivity" tools:listitem="@layout/item_list_content" />
而佈局 item_list.xml 中的 tools:listitem="@layout/item_list_content" 表示又引用了layout 文件夾下面的 item_list_content.xml 佈局文件。
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="wrap_content" android:layout_height="wrap_content" android:orientation="horizontal"> <TextView android:id="@+id/id_text" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_margin="@dimen/text_margin" android:textAppearance="?attr/textAppearanceListItem" /> <TextView android:id="@+id/content" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_margin="@dimen/text_margin" android:textAppearance="?attr/textAppearanceListItem" /> </LinearLayout>
這個是 Item 詳情頁的Activity 。Kotlin 代碼以下
package com.easy.kotlin import android.content.Intent import android.os.Bundle import android.support.design.widget.Snackbar import android.support.v7.app.AppCompatActivity import android.view.MenuItem import kotlinx.android.synthetic.main.activity_item_detail.* /** * An activity representing a single Item detail screen. This * activity is only used on narrow width devices. On tablet-size devices, * item details are presented side-by-side with a list of items * in a [ItemListActivity]. */ class ItemDetailActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_item_detail) setSupportActionBar(detail_toolbar) fab.setOnClickListener { view -> Snackbar.make(view, "Replace with your own detail action", Snackbar.LENGTH_LONG) .setAction("Action", null).show() } // Show the Up button in the action bar. supportActionBar?.setDisplayHomeAsUpEnabled(true) // savedInstanceState is non-null when there is fragment state // saved from previous configurations of this activity // (e.g. when rotating the screen from portrait to landscape). // In this case, the fragment will automatically be re-added // to its container so we don't need to manually add it. // For more information, see the Fragments API guide at: // // http://developer.android.com/guide/components/fragments.html // if (savedInstanceState == null) { // Create the detail fragment and add it to the activity // using a fragment transaction. val arguments = Bundle() arguments.putString(ItemDetailFragment.ARG_ITEM_ID, intent.getStringExtra(ItemDetailFragment.ARG_ITEM_ID)) val fragment = ItemDetailFragment() fragment.arguments = arguments supportFragmentManager.beginTransaction() .add(R.id.item_detail_container, fragment) .commit() } } override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) { android.R.id.home -> { // This ID represents the Home or Up button. In the case of this // activity, the Up button is shown. For // more details, see the Navigation pattern on Android Design: // // http://developer.android.com/design/patterns/navigation.html#up-vs-back navigateUpTo(Intent(this, ItemListActivity::class.java)) true } else -> super.onOptionsItemSelected(item) } }
UI 佈局 XML 文件 item_detail.xml 以下
<android.support.design.widget.CoordinatorLayout 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" android:fitsSystemWindows="true" tools:context="com.easy.kotlin.ItemDetailActivity" tools:ignore="MergeRootFrame"> <android.support.design.widget.AppBarLayout android:id="@+id/app_bar" android:layout_width="match_parent" android:layout_height="@dimen/app_bar_height" android:fitsSystemWindows="true" android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"> <android.support.design.widget.CollapsingToolbarLayout android:id="@+id/toolbar_layout" android:layout_width="match_parent" android:layout_height="match_parent" android:fitsSystemWindows="true" app:contentScrim="?attr/colorPrimary" app:layout_scrollFlags="scroll|exitUntilCollapsed" app:toolbarId="@+id/toolbar"> <android.support.v7.widget.Toolbar android:id="@+id/detail_toolbar" android:layout_width="match_parent" android:layout_height="?attr/actionBarSize" app:layout_collapseMode="pin" app:popupTheme="@style/ThemeOverlay.AppCompat.Light" /> </android.support.design.widget.CollapsingToolbarLayout> </android.support.design.widget.AppBarLayout> <android.support.v4.widget.NestedScrollView android:id="@+id/item_detail_container" android:layout_width="match_parent" android:layout_height="match_parent" app:layout_behavior="@string/appbar_scrolling_view_behavior" /> <android.support.design.widget.FloatingActionButton android:id="@+id/fab" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_vertical|start" android:layout_margin="@dimen/fab_margin" app:layout_anchor="@+id/item_detail_container" app:layout_anchorGravity="top|end" app:srcCompat="@android:drawable/stat_notify_chat" /> </android.support.design.widget.CoordinatorLayout>
打開item_detail.xml ,咱們能夠看到設計圖 UI 的效果
咱們能夠看到詳情頁的佈局主要有3大塊:AppBarLayout 、NestedScrollView和 FloatingActionButton 。
在 ItemDetailActivity 的onCreate 函數裏的
setContentView(R.layout.activity_item_detail)
設置詳情頁 ItemDetailActivity 的顯示界面使用 activity_item_detail.xml 佈局文件進行佈局。
setSupportActionBar(detail_toolbar)
設置詳情頁的 android.support.v7.widget.Toolbar 控件佈局。
下面咱們來看在 ItemDetailActivity 中建立 ItemDetailFragment 的過程。代碼以下
override fun onCreate(savedInstanceState: Bundle?) { ... if (savedInstanceState == null) { // Create the detail fragment and add it to the activity // using a fragment transaction. val arguments = Bundle() arguments.putString(ItemDetailFragment.ARG_ITEM_ID, intent.getStringExtra(ItemDetailFragment.ARG_ITEM_ID)) val fragment = ItemDetailFragment() fragment.arguments = arguments supportFragmentManager.beginTransaction() .add(R.id.item_detail_container, fragment) .commit() } }
val arguments = Bundle() arguments.putString(ItemDetailFragment.ARG_ITEM_ID, intent.getStringExtra(ItemDetailFragment.ARG_ITEM_ID)) val fragment = ItemDetailFragment() fragment.arguments = arguments
supportFragmentManager.beginTransaction() .add(R.id.item_detail_container, fragment) .commit()
其中,supportFragmentManager 用來獲取能管理和當前 Activity 有關聯的Fragment的 FragmentManager,使用supportFragmentManager 咱們能夠向Activity 狀態中添加一個Fragment 。
上面代碼中的 R.id.item_detail_container 對應的佈局是一個 NestedScrollView ,代碼以下
<android.support.v4.widget.NestedScrollView android:id="@+id/item_detail_container" android:layout_width="match_parent" android:layout_height="match_parent" app:layout_behavior="@string/appbar_scrolling_view_behavior" />
UI 界面設計效果以下圖所示
最後須要注意的是,若是當前 Activity 在前面已經保存了 Fragment 狀態數據,那麼 savedInstanceState 的值就是非空的, 這個時候咱們就不須要手動再去手工建立 Fragment 對象保存到當前的 Activity 中了。由於當咱們的 Activty 被異常銷燬時,Activity會對自身狀態進行保存(這裏麪包含了咱們添加的Fragment)。而在Activity被從新建立時,又會對咱們以前保存的 Fragment 進行恢復。
因此,添加 Fragment 前千萬要記得去檢查是否有保存的Activity狀態。若是沒有狀態保存,說明Acitvity是第1次被建立,咱們添加Fragment。若是有狀態保存,說明 Activity 剛剛出現過異常被銷燬過,以前的 Fragment 會被恢復,咱們再也不添加 Fragment。
上面的代碼中,咱們使用了 FragmentTransaction 的 add 方法,該方法簽名以下
public abstract FragmentTransaction add(@IdRes int containerViewId, Fragment fragment);
其中參數 containerViewId 傳入 Activity 中某個視圖容器的 id。若是containerViewId 傳 0,則這個Fragment不會被放置在一個容器中。請注意,不要認爲 Fragment 沒添加進來,其實咱們只是添加了一個沒有視圖的Fragment 而已,這個Fragment能夠用來作一些相似於Service的後臺工做。
FragmentTransaction 經常使用的 API 以下表
API 方法 | 說明 |
---|---|
add(int containerViewId, Fragment fragment, String tag) | 向Activity state中添加一個Fragment。參數containerViewId通常會傳Activity中某個視圖容器的id。若是containerViewId傳0,則這個Fragment不會被放置在一個容器中。添加Fragment前檢查是否有保存的Activity狀態。 |
remove(Fragment fragment) | 移除一個已經存在的Fragment。Fragment被remove後,Fragment的生命週期會一直執行完onDetach,以後Fragment的實例也會從FragmentManager中移除。 |
replace(int containerViewId, Fragment fragment) | 替換一個已被添加進視圖容器的Fragment。以前添加的Fragment 會在 replace 時被視圖容器移除。 |
addToBackStack(String name) | 記錄已提交的事務(transation),可用於回退操做。參數 name是此次回退操做的一個名稱(或標識),不須要能夠傳null。 |
show(Fragment fragment) | 隱藏一個存在的Fragment。 |
hide(Fragment fragment) | 顯示一個之前被隱藏過的Fragment。Fragment被hide/show,僅僅是隱藏/顯示Fragment的視圖,不會有任何生命週期方法的調用。在Fragment中重寫onHiddenChanged方法能夠對Fragment的hide和show狀態進行監聽。 |
attach(Fragment fragment) | 從新關聯一個Fragment(當這個Fragment的detach執行以後)。當Fragment被detach後,執行attach操做,會讓Fragment從onCreateView開始執行,一直執行到onResume。attach沒法像add同樣單獨使用,單獨使用會拋異常。方法存在的意義是對detach後的Fragment進行界面恢復。 |
detach(Fragment fragment) | 分離指定Fragment的UI視圖。當Fragment被detach後,Fragment的生命週期執行完onDestroyView就終止了,這意味着Fragment的實例並無被銷燬,只是UI界面被移除了(注意和remove是有區別的)。 |
setCustomAnimations(int enter, int exit) | 爲Fragment的進入/退出設置指定的動畫資源。 |
commit() | 提交事務。安排一個針對該事務的提交。提交併無馬上發生,會安排到在主線程下次準備好的時候來執行。 |
commitNow() | 同步的提交這個事務。任何被添加的Fragment都將會被初始化,並將他們徹底帶入他們的生命週期狀態。 使用commitNow()時不能進行添加回退棧的操做,若是使用 addToBackStack(String)將會拋出一個 IllegalStateException的異常。 |
下面咱們來介紹 ItemDetailFragment 。
這個 ItemDetailFragment 表示單個 Item 詳細信息。此片斷在雙窗格模式 (在平板電腦上) 包含在 ItemListActivity 中,在手機上則是包含在ItemDetailActivity中。其 Kotlin 代碼以下
package com.easy.kotlin import android.os.Bundle import android.support.v4.app.Fragment import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import com.easy.kotlin.dummy.DummyContent import kotlinx.android.synthetic.main.activity_item_detail.* import kotlinx.android.synthetic.main.item_detail.view.* class ItemDetailFragment : Fragment() { /** * 測試數據 dummy content this fragment is presenting. */ private var mItem: DummyContent.DummyItem? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) if (arguments.containsKey(ARG_ITEM_ID)) { // 加載數據 mItem = DummyContent.ITEM_MAP[arguments.getString(ARG_ITEM_ID)] mItem?.let { // 給 toolbar_layout 佈局設置標題 activity.toolbar_layout?.title = it.content } } } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { val rootView = inflater.inflate(R.layout.item_detail, container, false) // 在 TextView 中顯示測試數據文本 mItem?.let { rootView.item_detail.text = it.details } return rootView } companion object { /** * The fragment argument representing the item ID that this fragment * represents. */ const val ARG_ITEM_ID = "item_id" } }
在 onCreate 中,activity.toolbar_layout?.title = it.content 這行代碼是給詳情頁ToolBar 的大標題賦值
<android.support.design.widget.AppBarLayout android:id="@+id/app_bar" android:layout_width="match_parent" android:layout_height="@dimen/app_bar_height" android:fitsSystemWindows="true" android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"> <android.support.design.widget.CollapsingToolbarLayout android:id="@+id/toolbar_layout" android:layout_width="match_parent" android:layout_height="match_parent" android:fitsSystemWindows="true" app:contentScrim="?attr/colorPrimary" app:layout_scrollFlags="scroll|exitUntilCollapsed" app:toolbarId="@+id/toolbar"> <android.support.v7.widget.Toolbar android:id="@+id/detail_toolbar" android:layout_width="match_parent" android:layout_height="?attr/actionBarSize" app:layout_collapseMode="pin" app:popupTheme="@style/ThemeOverlay.AppCompat.Light" /> </android.support.design.widget.CollapsingToolbarLayout> </android.support.design.widget.AppBarLayout>
對應的 UI 以下圖
在 onCreateView中, rootView.item_detail.text = it.details 該行代碼對應的佈局是單個 Item 的詳情展現 TextView 視圖,其佈局 XML 代碼 item_detail.xml 以下
<TextView xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/item_detail" style="?android:attr/textAppearanceLarge" android:layout_width="match_parent" android:layout_height="match_parent" android:padding="16dp" android:textIsSelectable="true" tools:context="com.easy.kotlin.ItemDetailFragment" />
UI 效果圖是
Fragment 必須嵌入在 Activity 中才能生存,其生命週期也直接受宿主 Activity 的生命週期的影響。好比,若宿主 Activity 處於 pause 狀態,它所管轄的 Fragment 也將進入 pause 狀態。而當 Activity 處於 resume 狀態的時候,您能夠獨立地控制每個 Fragment,如添加或刪除等。爲了建立Fragment,須要繼承一個 Fragment 類,並實現 Fragment 的生命週期回調方法,如 onCreate(), onStart(), onPause(), onStop() 等。事實上,若須要在一個應用中加入 Fragment,只須要將原來的 Activity 替換爲 Fragment,並將 Activity 的生命週期回調方法簡單地改成 Fragment 的生命週期回調方法便可。Fragment 的生命週期以下所示:
另外, Fragment 與 Activity 的生命週期的對比圖以下
1.當一個fragment被建立的時候,它會經歷如下狀態.
onAttach() onCreate() onCreateView() onActivityCreated()
2.當這個fragment對用戶可見的時候,它會經歷如下狀態
onStart() onResume()
3.當這個fragment進入「後臺模式」的時候,它會經歷如下狀態
onPause() onStop()
4.當這個Fragment被銷燬了(或者持有它的activity被銷燬了),它會經歷如下狀態
onPause() onStop() onDestroyView() onDetach()
5.就像 Activity 同樣,在如下的狀態中,可使用Bundle對象保存一個Fragment的對象
onCreate() onCreateView() onActivityCreated()
6.Fragments的大部分狀態都和 Activity 很類似,但 Fragment 有一些新的狀態
onAttached() —— 當fragment和activity關聯以後,調用這個方法。 onCreateView() —— 建立fragment中的視圖的時候,調用這個方法。 onActivityCreated() —— 當activity的onCreate()方法被返回以後,調用這個方法。 onDestroyView() —— 當fragment中的視圖被移除的時候,調用這個方法。 onDetach() —— 當fragment和activity分離的時候,調用這個方法。
通常來講,在 Fragment 中應至少重寫下面3個生命週期方法:
onCreate() 當建立 Fragment 實例時,系統回調的方法。在該方法中,須要對一些必要的組件進行初始化,以保證這個組件的實例在 Fragment 處於 pause或stop 狀態時仍然存在。
onCreateView() 當第一次在 Fragment 上繪製UI時,系統回調的方法。該方法返回一個 View 對象,該對象表示 Fragment 的根視圖;若 Fragment 不須要展現視圖,則該方法能夠返回 null。
onPause() 當用戶離開 Fragment 時回調的方法(並不意味着該 Fragment 被銷燬)。在該方法中,能夠對 Fragment 的數據信息作一些持久化的保存工做,由於用戶可能再也不返回這個 Fragment。
大多數狀況下,須要重寫上述三個方法,有時還須要重寫其餘生命週期方法。
當執行一個 Fragment 事務時,也能夠將該 Fragment 加入到一個由宿主 Activity 管轄的後退棧中,並由 Activity 記錄加入到後退棧的 Fragment 信息,按下後退鍵能夠將 Fragment 從後退棧中一次彈出。
將 Fragment 添加至 Activity 的視圖佈局中有兩種方式:一種是使用fragment標籤加入,Fragment的父視圖應是一個ViewGroup;另外一種使用代碼動態加入,並將一個ViewGroup做爲Fragment的容器。在某些狀況下,Fragment並不做爲Activity視圖展現的一部分,它可能只是用來做爲非顯示性的功能。
Fragment 是 Android 3.0 (API level 11) 新加入的API,主要的設置目的是爲了使UI在不一樣的屏幕上表現得更加靈活。因爲平板比手機屏幕大的多,所以平板上能夠呈現更多的內容,而 Fragment 能夠實現同一視圖佈局在不一樣大小的屏幕上顯示不一樣的效果,將 Fragment 加入到 Activity 的 Layout 中,能夠在運行時動態替換 Fragment 並將 Fragment 保存至 Activity 管轄的「後退棧」中。另外,一樣的界面Activity佔用內存比Fragment要多,在中低端手機上Fragment 比 Activity 都響應速度要快不少。
爲了方便起見,繼承下面這些特殊的Fragment能夠簡化其初始化過程:
DialogFragment:可展現一個懸浮的對話框。使用該類建立的對話框能夠很好地替換由 Activity 類中的方法建立的對話框,由於您能夠像管理其餘 Fragment 同樣管理 DialogFragment——它們都被壓入由宿主 Activity 管理的 Fragment 棧中,這能夠很方便的找回已被壓入棧中的 Fragment。
ListFragment:能夠展現一個內置的 AdapterView,該 AdapterView 由一個 Adapter 管理着,如 SimpleCursorAdapter。ListFragment 相似於 ListActivity,它提供了大量的用於管理 ListView 的方法,好比回調方法 onListItemClick(),它用於處理點擊項事件。
PreferenceFragment:能夠展現層級嵌套的 Preference 對象列表。PreferenceFragment 相似於 PreferenceActivity,該類通常用於爲應用程序編寫設置頁面。
Fragment 綁定 UI 佈局必須重寫 onCreateView() 方法,爲 Fragment 綁定佈局,該方法返回的 View 就是 Fragment 的根視圖
class ItemDetailFragment : Fragment() { private var mItem: DummyContent.DummyItem? = null override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { val rootView = inflater.inflate(R.layout.item_detail, container, false) // Show the dummy content as text in a TextView. mItem?.let { rootView.item_detail.text = it.details } return rootView } }
其中,val rootView = inflater.inflate(R.layout.item_detail, container, false) 這一行代碼中的 inflater.inflate 是用於填充佈局的, 這是佈局填充器 LayoutInflater 類的方法。一般咱們加載佈局的任務都是在 Activity 中調用 setContentView() 方法來完成的。其實 setContentView() 方法的內部也是使用LayoutInflater 來加載佈局的,相關的代碼在 android.support.v7.app.AppCompatDelegateImplV9 中
@Override public void setContentView(int resId) { ensureSubDecor(); ViewGroup contentParent = (ViewGroup) mSubDecor.findViewById(android.R.id.content); contentParent.removeAllViews(); LayoutInflater.from(mContext).inflate(resId, contentParent); mOriginalWindowCallback.onContentChanged(); }
在實際開發中 LayoutInflater 這個類仍是很是有用的,它的做用相似於findViewById()。不一樣點是LayoutInflater 是用來找 res/layout/ 下的 xml 佈局文件並實例化(填充佈局);而 findViewById() 是找 xml 佈局文件下的具體widget控件(如Button、TextView等) 並實例化。
具體做用說明以下:
一、對於一個沒有被載入或者想要動態載入的界面,都須要使用LayoutInflater.inflate()來載入;
二、對於一個已經載入的界面,就可使用Activiyt.findViewById()方法來得到其中的界面元素。
注意:若繼承的 Fragment 是 ListFragment,onCreateView() 方法已默認返回了 ListView 對象,故無需再重寫該方法。
有關 Fragment 的更多信息,請參見「Fragment API指南」 (http://developer.android.com/guide/components/fragments.html)。
這個類中構造了咱們在 ListActivity 中展現的測試數據。代碼以下
package com.easy.kotlin.dummy import java.util.ArrayList import java.util.HashMap object DummyContent { val ITEMS: MutableList<DummyItem> = ArrayList() val ITEM_MAP: MutableMap<String, DummyItem> = HashMap() private val COUNT = 25 init { // Add some sample items. for (i in 1..COUNT) { addItem(createDummyItem(i)) } } private fun addItem(item: DummyItem) { ITEMS.add(item) ITEM_MAP.put(item.id, item) } private fun createDummyItem(position: Int): DummyItem { return DummyItem(position.toString(), "Item " + position, makeDetails(position)) } private fun makeDetails(position: Int): String { val builder = StringBuilder() builder.append("Details about Item: ").append(position) for (i in 0..position - 1) { builder.append("\nMore details information here.") } return builder.toString() } data class DummyItem(val id: String, val content: String, val details: String) { override fun toString(): String = content } }
至此,咱們已經瞭解了怎樣使用 Android Studio 3.0 建立一個帶 ListActivity 和Fragment 的列表及其詳情頁展現,同時學習了 Activity 和 Fragment 的基本用法。下面咱們來實現後端 API 的接入與數據的展示。
在本節中咱們將實現後端 API 的接入及其數據展現的邏輯。
data class Movie(val id: String, val title: String, val overview: String, val posterPath: String) { override fun toString(): String { return "Movie(id='$id', title='$title', overview='$overview', posterPath='$posterPath')" } }
咱們調用的 API 是
val VOTE_AVERAGE_API = "http://api.themoviedb.org//3/discover/movie?certification_country=US&certification=R&sort_by=vote_average.desc&api_key=7e55a88ece9f03408b895a96c1487979"
它的數據返回是
{ "page": 1, "total_results": 10350, "total_pages": 518, "results": [ { "vote_count": 28, "id": 138878, "video": false, "vote_average": 10, "title": "Fatal Mission", "popularity": 3.721883, "poster_path": "/u351Rsqu5nd36ZpbWxIpd3CpbJW.jpg", "original_language": "en", "original_title": "Fatal Mission", "genre_ids": [ 10752, 28, 12 ], "backdrop_path": "/wNq5uqVDT7a5G1b97ffYf4hxzYz.jpg", "adult": false, "overview": "A CIA Agent must rely on reluctant help from a female spy in the North Vietnam jungle in order to pass through enemy lines.", "release_date": "1990-07-25" }, ... ] }
咱們使用 fastjson 來解析這個數據。在 app 下面的 build.gradle中添加依賴
dependencies { ... // https://mvnrepository.com/artifact/com.alibaba/fastjson compile group: 'com.alibaba', name: 'fastjson', version: '1.2.39' }
解析代碼以下
val jsonstr = URL(VOTE_AVERAGE_API).readText(Charset.defaultCharset()) try { val obj = JSON.parse(jsonstr) as Map<*, *> val dataArray = obj.get("results") as JSONArray } } catch (ex: Exception) { }
而後咱們把這個 dataArray 放到咱們的 MovieContent 對象中
dataArray.forEachIndexed { index, it -> val title = (it as Map<*, *>).get("title") as String val overview = it.get("overview") as String val poster_path = it.get("poster_path") as String addMovie(Movie(index.toString(), title, overview, getPosterUrl(poster_path))) }
其中,addMovie 的代碼是
object MovieContent { val MOVIES: MutableList<Movie> = ArrayList() val MOVIE_MAP: MutableMap<String, Movie> = HashMap() ... private fun addMovie(movie: Movie) { MOVIES.add(movie) MOVIE_MAP.put(movie.id, movie) } }
而後,咱們再新建 MovieDetailActivity、MovieDetailFragment、MovieListActivity 以及 activity_movie_list.xml、activity_movie_detail.xml 、 movie_detail.xml、movie_list.xml、movie_list_content.xml ,它們的代碼分別介紹以下。
MovieListActivity 是電影列表頁面的 Activity,代碼以下
package com.easy.kotlin import android.content.Intent import android.os.Bundle import android.support.v7.app.AppCompatActivity import android.support.v7.widget.RecyclerView import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.ImageView import android.widget.TextView import com.easy.kotlin.bean.MovieContent import com.easy.kotlin.util.HttpUtil import kotlinx.android.synthetic.main.activity_movie_detail.* import kotlinx.android.synthetic.main.activity_movie_list.* import kotlinx.android.synthetic.main.movie_list.* import kotlinx.android.synthetic.main.movie_list_content.view.* class MovieListActivity : AppCompatActivity() { private var mTwoPane: Boolean = false override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_movie_list) setSupportActionBar(toolbar) toolbar.title = title if (movie_detail_container != null) { mTwoPane = true } setupRecyclerView(movie_list) } private fun setupRecyclerView(recyclerView: RecyclerView) { recyclerView.adapter = SimpleItemRecyclerViewAdapter(this, MovieContent.MOVIES, mTwoPane) } class SimpleItemRecyclerViewAdapter(private val mParentActivity: MovieListActivity, private val mValues: List<MovieContent.Movie>, private val mTwoPane: Boolean) : RecyclerView.Adapter<SimpleItemRecyclerViewAdapter.ViewHolder>() { private val mOnClickListener: View.OnClickListener init { mOnClickListener = View.OnClickListener { v -> val item = v.tag as MovieContent.Movie if (mTwoPane) { val fragment = MovieDetailFragment().apply { arguments = Bundle() arguments.putString(MovieDetailFragment.ARG_MOVIE_ID, item.id) } mParentActivity.supportFragmentManager .beginTransaction() .replace(R.id.movie_detail_container, fragment) .commit() } else { val intent = Intent(v.context, MovieDetailActivity::class.java).apply { putExtra(MovieDetailFragment.ARG_MOVIE_ID, item.id) } v.context.startActivity(intent) } } } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { val view = LayoutInflater .from(parent.context) .inflate(R.layout.movie_list_content, parent, false) return ViewHolder(view) } override fun onBindViewHolder(holder: ViewHolder, position: Int) { val item = mValues[position] holder.mIdView.text = item.id holder.mTitle.text = item.title holder.mMoviePosterImageView.setImageBitmap(HttpUtil.getBitmapFromURL(item.posterPath)) with(holder.itemView) { tag = item setOnClickListener(mOnClickListener) } } override fun getItemCount(): Int { return mValues.size } inner class ViewHolder(mView: View) : RecyclerView.ViewHolder(mView) { val mIdView: TextView = mView.id_text val mTitle: TextView = mView.title val mMoviePosterImageView: ImageView = mView.movie_poster_image } } }
對應的佈局文件以下
activity_movie_list.xml
<?xml version="1.0" encoding="utf-8"?> <android.support.design.widget.CoordinatorLayout 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" android:fitsSystemWindows="true" tools:context="com.easy.kotlin.MovieListActivity"> <android.support.design.widget.AppBarLayout android:id="@+id/app_bar" android:layout_width="match_parent" android:layout_height="wrap_content" android:theme="@style/AppTheme.AppBarOverlay"> <android.support.v7.widget.Toolbar android:id="@+id/toolbar" android:layout_width="match_parent" android:layout_height="?attr/actionBarSize" app:popupTheme="@style/AppTheme.PopupOverlay" /> </android.support.design.widget.AppBarLayout> <FrameLayout android:id="@+id/frameLayout" android:layout_width="match_parent" android:layout_height="match_parent" app:layout_behavior="@string/appbar_scrolling_view_behavior"> <include layout="@layout/movie_list" /> </FrameLayout> </android.support.design.widget.CoordinatorLayout>
movie_list.xml
<?xml version="1.0" encoding="utf-8"?> <android.support.v7.widget.RecyclerView 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:id="@+id/movie_list" android:name="com.easy.kotlin.MovieListFragment" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_marginLeft="16dp" android:layout_marginRight="16dp" app:layoutManager="LinearLayoutManager" tools:context="com.easy.kotlin.MovieListActivity" tools:listitem="@layout/movie_list_content" />
movie_list_content.xml
<?xml version="1.0" encoding="utf-8"?> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="320dp" android:layout_gravity="center" android:layout_margin="0dp" android:clickable="true" android:foreground="?attr/selectableItemBackground" android:orientation="horizontal"> <TextView android:id="@+id/id_text" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_margin="@dimen/text_margin" android:textAppearance="?attr/textAppearanceListItem" /> <ImageView android:id="@+id/movie_poster_image" android:layout_width="match_parent" android:layout_height="match_parent" android:scaleType="centerCrop" /> <View android:id="@+id/title_background" android:layout_width="match_parent" android:layout_height="48dp" android:layout_gravity="bottom" android:alpha="0.8" android:background="@color/colorPrimaryDark" android:gravity="center" /> <TextView android:id="@+id/title" android:layout_width="match_parent" android:layout_height="48dp" android:layout_gravity="bottom" android:gravity="center" android:paddingLeft="@dimen/activity_horizontal_margin" android:paddingRight="@dimen/activity_horizontal_margin" android:textColor="@android:color/white" android:textSize="12sp" /> </FrameLayout>
電影列表的總體佈局的 UI 以下圖所示
咱們在建立 MovieListActivity 過程當中須要展現響應的數據,這些數據由 ViewAdapter 來承載,對應的代碼以下
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_movie_list) setSupportActionBar(toolbar) toolbar.title = title if (movie_detail_container != null) { mTwoPane = true } setupRecyclerView(movie_list) } private fun setupRecyclerView(recyclerView: RecyclerView) { recyclerView.adapter = SimpleItemRecyclerViewAdapter(this, MovieContent.MOVIES, mTwoPane) }
在上面代碼中,咱們定義了一個繼承 RecyclerView.Adapter 的 SimpleItemRecyclerViewAdapter 類來裝載 View 中要顯示的數據,實現數據與視圖的解耦。View 要顯示的數據從Adapter裏面獲取並展示出來。Adapter負責把真實的數據是配成一個個View,也就是說View要顯示什麼數據取決於Adapter裏面的數據。
其中,在函數 SimpleItemRecyclerViewAdapter.onBindViewHolder 中,咱們設置 View 組件與Model 數據的綁定。其中的電影海報是圖片,因此咱們的佈局文件中使用了 ImageView,對應的佈局文件是 movie_list_content.xml ,代碼以下
<?xml version="1.0" encoding="utf-8"?> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="320dp" android:layout_gravity="center" android:layout_margin="0dp" android:clickable="true" android:foreground="?attr/selectableItemBackground" android:orientation="horizontal"> <TextView android:id="@+id/id_text" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_margin="@dimen/text_margin" android:textAppearance="?attr/textAppearanceListItem" /> <ImageView android:id="@+id/movie_poster_image" android:layout_width="match_parent" android:layout_height="match_parent" android:scaleType="centerCrop" /> <View android:id="@+id/title_background" android:layout_width="match_parent" android:layout_height="48dp" android:layout_gravity="bottom" android:alpha="0.8" android:background="@color/colorPrimaryDark" android:gravity="center" /> <TextView android:id="@+id/title" android:layout_width="match_parent" android:layout_height="48dp" android:layout_gravity="bottom" android:gravity="center" android:paddingLeft="@dimen/activity_horizontal_margin" android:paddingRight="@dimen/activity_horizontal_margin" android:textColor="@android:color/white" android:textSize="12sp" /> </FrameLayout>
UI 設計效果圖
關於圖片的視圖組件是 ImageView
<ImageView android:id="@+id/movie_poster_image" android:layout_width="match_parent" android:layout_height="match_parent" android:scaleType="centerCrop" />
咱們這裏是根據圖片的 URL 來展現圖片,ImageView 類有個setImageBitmap 方法,能夠直接設置 Bitmap 圖片數據
holder.mMoviePosterImageView.setImageBitmap(HttpUtil.getBitmapFromURL(item.posterPath))
而經過 url 獲取Bitmap 圖片數據的代碼是
object HttpUtil { fun getBitmapFromURL(src: String): Bitmap? { try { val url = URL(src) val input = url.openStream() val myBitmap = BitmapFactory.decodeStream(input) return myBitmap } catch (e: Exception) { e.printStackTrace() return null } } }
MovieDetailActivity 是電影詳情頁面,代碼以下
package com.easy.kotlin import android.content.Intent import android.os.Bundle import android.support.design.widget.Snackbar import android.support.v7.app.AppCompatActivity import android.view.MenuItem import kotlinx.android.synthetic.main.activity_movie_detail.* class MovieDetailActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_movie_detail) setSupportActionBar(detail_toolbar) fab.setOnClickListener { view -> Snackbar.make(view, "Replace with your own detail action", Snackbar.LENGTH_LONG) .setAction("Action", null).show() } supportActionBar?.setDisplayHomeAsUpEnabled(true) if (savedInstanceState == null) { val arguments = Bundle() arguments.putString(MovieDetailFragment.ARG_MOVIE_ID, intent.getStringExtra(MovieDetailFragment.ARG_MOVIE_ID)) val fragment = MovieDetailFragment() fragment.arguments = arguments supportFragmentManager.beginTransaction() .add(R.id.movie_detail_container, fragment) .commit() } } override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) { android.R.id.home -> { navigateUpTo(Intent(this, MovieListActivity::class.java)) true } else -> super.onOptionsItemSelected(item) } }
其中的詳情頁的佈局 XML 文件是activity_item_detail.xml, 代碼以下
<android.support.design.widget.CoordinatorLayout 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" android:fitsSystemWindows="true" tools:context="com.easy.kotlin.ItemDetailActivity" tools:ignore="MergeRootFrame"> <android.support.design.widget.AppBarLayout android:id="@+id/app_bar" android:layout_width="match_parent" android:layout_height="@dimen/app_bar_height" android:fitsSystemWindows="true" android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"> <android.support.design.widget.CollapsingToolbarLayout android:id="@+id/toolbar_layout" android:layout_width="match_parent" android:layout_height="match_parent" android:fitsSystemWindows="true" app:contentScrim="?attr/colorPrimary" app:layout_scrollFlags="scroll|exitUntilCollapsed" app:toolbarId="@+id/toolbar"> <android.support.v7.widget.Toolbar android:id="@+id/detail_toolbar" android:layout_width="match_parent" android:layout_height="?attr/actionBarSize" app:layout_collapseMode="pin" app:popupTheme="@style/ThemeOverlay.AppCompat.Light" /> </android.support.design.widget.CollapsingToolbarLayout> </android.support.design.widget.AppBarLayout> <android.support.v4.widget.NestedScrollView android:id="@+id/item_detail_container" android:layout_width="match_parent" android:layout_height="match_parent" app:layout_behavior="@string/appbar_scrolling_view_behavior" /> <android.support.design.widget.FloatingActionButton android:id="@+id/fab" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_vertical|start" android:layout_margin="@dimen/fab_margin" app:layout_anchor="@+id/item_detail_container" app:layout_anchorGravity="top|end" app:srcCompat="@android:drawable/stat_notify_chat" /> </android.support.design.widget.CoordinatorLayout>
咱們把電影詳情的 Fragment 的展現放到 NestedScrollView 中
<android.support.v4.widget.NestedScrollView android:id="@+id/movie_detail_container" android:layout_width="match_parent" android:layout_height="match_parent" android:fitsSystemWindows="true" app:layout_behavior="@string/appbar_scrolling_view_behavior" />
電影詳情的 Fragment 代碼是 MovieDetailFragment
package com.easy.kotlin import android.os.Bundle import android.support.v4.app.Fragment import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import com.easy.kotlin.bean.MovieContent import com.easy.kotlin.util.HttpUtil import kotlinx.android.synthetic.main.activity_movie_detail.* import kotlinx.android.synthetic.main.movie_detail.view.* class MovieDetailFragment : Fragment() { private var mItem: MovieContent.Movie? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) if (arguments.containsKey(ARG_MOVIE_ID)) { mItem = MovieContent.MOVIE_MAP[arguments.getString(ARG_MOVIE_ID)] mItem?.let { activity.toolbar_layout?.title = it.title } } } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { // 綁定 movieDetailView val movieDetailView = inflater.inflate(R.layout.movie_detail, container, false) mItem?.let { movieDetailView.movie_poster_image.setImageBitmap(HttpUtil.getBitmapFromURL(it.posterPath)) movieDetailView.movie_overview.text = "影片簡介: ${it.overview}" movieDetailView.movie_vote_count.text = "打分次數:${it.vote_count}" movieDetailView.movie_vote_average.text = "評分:${it.vote_average}" movieDetailView.movie_release_date.text = "發行日期:${it.release_date}" } return movieDetailView } companion object { const val ARG_MOVIE_ID = "movie_id" } }
其中的 R.layout.movie_detail 佈局文件 movie_detail.xml 以下
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_gravity="center" android:layout_margin="0dp" android:clickable="true" android:foreground="?attr/selectableItemBackground" android:orientation="vertical"> <TextView android:id="@+id/movie_release_date" android:layout_width="match_parent" android:layout_height="match_parent" android:padding="16dp" android:textIsSelectable="true" android:textSize="18sp" tools:context="com.easy.kotlin.MovieDetailFragment" /> <ImageView android:id="@+id/movie_poster_image" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_centerVertical="true" android:fitsSystemWindows="true" android:scaleType="fitCenter" /> <TextView android:id="@+id/movie_overview" android:layout_width="match_parent" android:layout_height="match_parent" android:padding="16dp" android:textIsSelectable="true" android:textSize="18sp" tools:context="com.easy.kotlin.MovieDetailFragment" /> <TextView android:id="@+id/movie_vote_average" android:layout_width="match_parent" android:layout_height="match_parent" android:padding="16dp" android:textIsSelectable="true" android:textSize="18sp" tools:context="com.easy.kotlin.MovieDetailFragment" /> <TextView android:id="@+id/movie_vote_count" android:layout_width="match_parent" android:layout_height="match_parent" android:padding="16dp" android:textIsSelectable="true" android:textSize="18sp" tools:context="com.easy.kotlin.MovieDetailFragment" /> </LinearLayout>
咱們定義了一個 MovieContent 對象類來存儲從 API 獲取到的數據,代碼以下
package com.easy.kotlin.bean import android.os.StrictMode import com.alibaba.fastjson.JSON import com.alibaba.fastjson.JSONArray import java.net.URL import java.nio.charset.Charset import java.util.* object MovieContent { val MOVIES: MutableList<Movie> = ArrayList() val MOVIE_MAP: MutableMap<String, Movie> = HashMap() val VOTE_AVERAGE_API = "http://api.themoviedb.org//3/discover/movie?sort_by=popularity.desc&api_key=7e55a88ece9f03408b895a96c1487979&page=1" init { val policy = StrictMode.ThreadPolicy.Builder().permitAll().build() StrictMode.setThreadPolicy(policy) initMovieListData() } private fun initMovieListData() { val jsonstr = URL(VOTE_AVERAGE_API).readText(Charset.defaultCharset()) try { val obj = JSON.parse(jsonstr) as Map<*, *> val dataArray = obj.get("results") as JSONArray dataArray.forEachIndexed { index, it -> val title = (it as Map<*, *>).get("title") as String val overview = it.get("overview") as String val poster_path = it.get("poster_path") as String val vote_count = it.get("vote_count").toString() val vote_average = it.get("vote_average").toString() val release_date = it.get("release_date").toString() addMovie(Movie(id = index.toString(), title = title, overview = overview, vote_count = vote_count, vote_average = vote_average, release_date = release_date, posterPath = getPosterUrl(poster_path))) } } catch (ex: Exception) { ex.printStackTrace() } } private fun addMovie(movie: Movie) { MOVIES.add(movie) MOVIE_MAP.put(movie.id, movie) } fun getPosterUrl(posterPath: String): String { return "https://image.tmdb.org/t/p/w185_and_h278_bestv2$posterPath" } data class Movie(val id: String, val title: String, val overview: String, val vote_count: String, val vote_average: String, val release_date: String, val posterPath: String) }
在 Android 4.0 以後默認的線程模式是不容許在主線程中訪問網絡。爲了演示效果,咱們在訪問網絡的代碼前,把 ThreadPolicy 設置爲容許運行訪問網絡
val policy = StrictMode.ThreadPolicy.Builder().permitAll().build() StrictMode.setThreadPolicy(policy)
咱們使用了一個 data class Movie 來存儲電影對象數據
data class Movie(val id: String, val title: String, val overview: String, val vote_count: String, val vote_average: String, val release_date: String, val posterPath: String)
最後,咱們配置 AndroidManifest.xml文件內容以下
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.easy.kotlin"> <application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/AppTheme"> ... <activity android:name=".MovieListActivity" android:label="@string/app_name" android:theme="@style/AppTheme.NoActionBar"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> <activity android:name=".MovieDetailActivity" android:label="@string/title_movie_detail" android:parentActivityName=".MovieListActivity" android:theme="@style/AppTheme.NoActionBar"> <meta-data android:name="android.support.PARENT_ACTIVITY" android:value="com.easy.kotlin.MovieListActivity" /> </activity> </application> <uses-permission android:name="android.permission.INTERNET" /> </manifest>
由於咱們要訪問網絡,因此須要添加該行配置
<uses-permission android:name="android.permission.INTERNET" />
再次打包安裝運行,效果圖以下
電影列表頁面
點擊進入電影詳情頁
Android 中常常出現的空引用、API的冗餘樣板式代碼等都是是驅動咱們轉向 Kotlin 語言的動力。另外,Kotlin 的 Android 視圖 DSL Anko 能夠咱們從繁雜的 XML 視圖配置文件中解放出來。咱們能夠像在 Java 中同樣方便的使用 Android 開發的流行的庫諸如 Butter Knife、Realm、RecyclerView等。固然,咱們使用 Kotlin 集成這些庫來進行 Andorid 開發,既可以直接使用咱們以前的開發庫,又可以從 Java 語言、Android API 的限制中出來。這不得不說是一件好事。