万书网 > 文学作品 > Kotlin从入门到进阶实战 > 14.2 综合项目实战:开发一个电影指南应用程序

14.2 综合项目实战:开发一个电影指南应用程序

    下面我们来开发一个电影指南Android应用程序,列出流行/最高评级的电影,显示预告片和评论。

    14.2.1 创建Kotlin Android项目

    首先创建一个Kotlin Android项目,然后单击Next按钮,如图14-17所示。

    在Target Android Devices界面中选择目标设备后单击Next按钮,如图14-18所示。

    图14-17 创建Kotlin Android项目

    图14-18 选择目标设备

    然后在Add an Activity to Mobile界面中添加一个Master/Detail Flow,单击Next按钮,如图14-19所示。

    在Configure Activity界面中配置Activity,单击Finish完成配置,如图14-20所示。

    图14-19 添加Master/Detail Flow

    图14-20 配置Activity

    最终生成的Android项目工程目录结构如图14-21所示。

    图14-21 Android项目工程目录结构

    运行之后的列表页如图14-22所示。

    选择Item1进入详情页,如图14-23所示。

    图14-22 运行之后的列表页

    图14-23 进入Item1详情页

    其中AndroidManifest.xml代码如下:

    其中,android.intent.action.MAIN处的配置指定了应用程序的启动Activity为.ItemListActivity,其中的点号“.”表示该类位于package="com.easy.kotlin"路径下。

    14.2.2 启动主类ItemListActivity

    下面来介绍应用程序的启动主类ItemListActivity。ItemListActivity的Kotlin代码如下:

    布局文件XML代码中的activity_item_list.xml代码如下:

    对应的UI设计效果图如图14-24所示。

    图14-24 列表页的UI设计图

    14.2.3 AppCompatActivity类介绍

    在使用Android Studio开发Android应用的时候,创建项目时,自动继承的是AppCompatActivity。这样我们可以在自定义的Activity类中添加android.support.v7.app.ActionBar (APIlevel7+)。例如activity_item_list.xml布局中的

    Activity中添加Toolbar的代码如下:

    AppCompatActivity背后继承的也是Activity。Android 5.0推出之后,提供了很多新功能,于是support v7也更新了,出现了AppCompatActivity。AppCompatActivity是用来替代ActionBarActivity的。AppCompatActivity的类图继承层次如图14-25所示。

    图14-25 AppCompatActivity的类图继承层次

    14.2.4 Activity生命周期

    Activity的生命周期示意图如图14-26所示(图来自官网)。

    相信不少朋友已经看过这个流程图了,这里面简单说明一下。

    (1)开始启动Activity,系统会先调用onCreate()方法,然后调用onStart()方法,最后调用onResume()方法,Activity进入运行状态。

    (2)当前Activity被其他Activity覆盖或被锁屏:系统会调用onPause()方法,暂停当前Activity的执行。

    (3)当前Activity由被覆盖状态回到前台或解锁屏:系统会调用onResume()方法,再次进入运行状态。

    (4)当前Activity转到新的Activity界面或按Home键回到主屏:系统会先调用onPause()方法,然后调用onStop()方法,进入停止状态。

    图14-26 Activity的生命周期

    (5)用户后退到此Activity:系统会先调用onRestart()方法,然后调用onStart()方法,最后调用onResume()方法,再次进入运行状态。

    (6)当前Activity处于被覆盖状态或者后台不可见状态,即第(2)步和第(4)步,系统内存不足,“杀死”当前Activity。而后用户退回当前Activity:再次调用onCreate()方法、onStart()方法和onResume()方法进入运行状态。

    (7)用户退出当前Activity:系统先调用onPause()方法,然后调用onStop()方法,最后调用onDestory方法,结束当前Activity。

    这个过程可以用下面的状态图来简单说明,如图14-27所示。

    图14-27 Activity状态图

    14.2.5 Kotlin Android Extensions插件

    在上面的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的工具箱。在菜单栏中依次选择Tools | Kotlin | Show Kotlin Bytecode命令,如图14-28所示。

    图14-28 依次选择Tools | Kotlin | Show Kotlin Bytecode命令

    之后将会看到如图14-29所示的Kotlin Bytecode界面。

    图14-29 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代码可能会看得更加清楚:

    其中,ItemListActivity类中HashMap类型的私有成员变量$findViewCache就是本地缓存。这里其实反映出了Kotlin语言设计的核心思想:通过对Java更高一层的封装,不仅大大简化了样板化的代码量,同时根据一些特定的可以优化的问题场景,提供了更好的性能。

    同样的,上面代码中的fab变量:

    也是直接使用的布局XML中的android:id="@+id/fab":

    item_detail_container、setupRecyclerView(item_list)中的item_list都是使用了上面的方式,这样代码确实精简了许多。

    上面的activity_item_list.xml布局中嵌套的FrameLayout布局配置如下:

    其中,表示引用layout文件夹下面的item_list.xml,item_list.xml文件内容如下:

    而布局item_list.xml中的tools:listitem="@layout/item_list_content"表示引用了layout文件夹下面的item_list_content.xml布局文件。

    14.2.6 详情页ItemDetailActivity

    ItemDetailActivity是Item详情页的Activity,对应的Kotlin代码如下:

    UI布局XML文件的item_detail.xml内容如下:

    打开item_detail.xml,可以看到设计图的UI效果如图14-30所示。

    图14-30 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的过程。代码如下:

    (1)首先判断当前savedInstanceState是否为空。如果为空,执行步骤(2)。

    (2)创建ItemDetailFragment()对象,并设置其Bundle信息(Fragment中的成员变量mArguments)如下:

    val arguments = Bundle() arguments.putString(ItemDetailFragment.ARG_ITEM_ID, intent.getStringExtra(ItemDetailFragment.ARG_ITEM_ID)) val fragment = ItemDetailFragment() fragment.arguments = arguments

    (3)通过supportFragmentManager添加Fragment与布局空间的映射关系。

    supportFragmentManager.beginTransaction() .add(R.id.item_detail_container, fragment) mit()

    其中,supportFragmentManager用来获取能管理和当前Activity有关联的Fragment的FragmentManager,使用supportFragmentManager可以向Activity状态中添加一个Fragment。

    上面代码中的R.id.item_detail_container对应的布局是一个NestedScrollView,代码如下:

    ppbar_scrolling_view_behavior" />

    UI界面的设计效果图如图14-31所示。

    最后需要注意的是,如果当前Activity在前面已经保存了Fragment状态的数据,那么savedInstanceState的值就是非空的,这个时候我们就不需要再去手工创建Fragment对象保存到当前的Activity中了。因为当我们的Activty被异常销毁时,Activity会对自身状态进行保存(这里包含了我们添加的Fragment)。而在Activity被重新创建时,又会对我们之前保存的Fragment进行恢复。

    图14-31 NestedScrollView代码布局效果图

    所以,添加Fragment前千万要记得检查是否有保存的Activity状态。如果没有状态保存,说明Activity是第1次被创建,我们需要添加Fragment;如果有状态保存,说明Activity刚刚出现过异常被销毁过,之前的Fragment会被恢复,我们不用再添加Fragment。

    14.2.7 碎片事务类FragmentTransaction

    前面的代码中使用了FragmentTransaction的add()方法,该方法签名如下:

    public abstract FragmentTransaction add(@IdRes int containerViewId, Fragment fragment);

    其中,参数containerViewId为传入Activity中某个视图容器的ID。如果containerViewId传入0,则这个Fragment不会被放置在一个容器中。请注意,不要认为Fragment没添加进来,其实我们只是添加了一个没有视图的Fragment而已,这个Fragment可以用来做一些类似于Service的后台工作。

    FragmentTransaction常用的API如表14-1所示。

    表14-1 FragmentTransaction常用的API方法

    下面介绍ItemDetailFragment。

    ItemDetailFragment表示单个Item的详细信息。此片段在双窗格模式(在平板电脑上)包含在ItemListActivity中,在手机上则是包含在ItemDetailActivity中。其Kotlin代码如下:

    在onCreate()中,activity.toolbar_layout?.title=it.content这行代码是给详情页ToolBar的大标题赋值。

    对应的UI效果图如图14-32所示。

    图14-32 AppBarLayout的UI界面效果图

    在onCreateView()中,rootView.item_detail.text = it.details这行代码对应的布局是单个Item的详情展示TextView视图,其布局XML代码中的item_detail.xml代码如下:

    

    UI效果图如图14-33所示。

    图14-33 item_detail.xml的布局UI效果图

    14.2.8 Fragment生命周期

    Fragment必须嵌入在Activity中才能生存,其生命周期也直接受宿主Activity生命周期的影响。例如,若宿主Activity处于pause状态,则它所管辖的Fragment也将进入pause状态。而当Activity处于resume状态时,则可以独立地控制每一个Fragment,如添加或删除等。为了创建Fragment,需要继承一个Fragment类,并实现Fragment的生命周期回调方法,如onCreate()、onStart()、onPause()和onStop()等。事实上,若需要在一个应用中加入Fragment,只需要将原来的Activity替换为Fragment,并将Activity的生命周期回调方法简单地改为Fragment的生命周期回调方法即可。Fragment的生命周期示意图如图14-34所示。

    图14-34 Fragment的生命周期示意图

    另外,Fragment与Activity的生命周期对比如图14-35所示。

    图14-35 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。

    大多数情况下,需要重写上述3个方法,有时还需要重写其他生命周期方法。

    当执行一个Fragment事务时,也可以将该Fragment加入到一个由宿主Activity管辖的后退栈中,并由Activity记录加入到后退栈的Fragment信息,按下后退键可以将Fragment从后退栈中一次弹出。

    将Fragment添加至Activity的视图布局中有两种方式:一种是使用Fragment标签加入。Fragment的父视图应是一个ViewGroup;另一种使用代码动态加入,并将一个ViewGroup作为Fragment的容器。

    为了方便,继承下面这些特殊的Fragment可以简化其初始化过程。

    DialogFragment:可展示一个悬浮对话框。使用该类创建的对话框可以很好地替换由Activity类中的方法创建的对话框,因为可以像管理其他Fragment一样管理DialogFragment——它们都被压入由宿主Activity管理的Fragment栈中,这可以很方便地找回已被压入栈中的Fragment。

    ListFragment:可以展示一个内置的AdapterView,该AdapterView由一个Adapter管理,如SimpleCursorAdapter。ListFragment类似于ListActivity,它提供了大量的用于管理ListView的方法,如回调方法onListItemClick(),其用于处理单击项事件。

    PreferenceFragment:可以展示层级嵌套的Preference对象列表。PreferenceFragment类似于PreferenceActivity,该类一般用于为应用程序编写设置页面。

    Fragment绑定UI布局需要重写onCreateView()方法,该方法返回一个View视图对象,代码如下:

    其中,val rootView=inflater.inflate(R.layout.item_detail,container, false)这一行代码中的inflater.inflate是用于填充布局的,这是布局填充器LayoutInflater类的方法。通常我们加载布局的任务都是在Activity中调用setContentView()方法来完成的。其实setContentView()方法的内部也是使用LayoutInflater类来加载布局的,相关的代码在android.support.v7.app.AppCompatDelegateImplV9中。

    在实际开发中,LayoutInflater类还是非常有用的,它的作用类似于findViewById()。不同点是LayoutInflater是用来找res\layout\下的XML布局文件并实例化(填充布局);而findViewById()是找XML布局文件下的具体widget控件(如Button、TextView等)并实例化。

    LayoutInflater具体作用说明如下:

    对于一个没有被载入或者想要动态载入的界面,都需要使用LayoutInflater.inflate()来载入;

    对于一个已经载入的界面,可以使用Activiyt.findViewById()方法来获得其中的界面元素。

    注意:若继承的Fragment是ListFragment,onCreateView()方法已默认返回了ListView对象,因此无须再重写该方法。

    有关Fragment的更多信息,请参见“Fragment API指南”:http://developer.android/guide/components/fragments.html。

    14.2.9 测试数据类DummyContent

    在DummyContent类中构造了我们在ListActivity中展示的测试数据。代码如下:

    至此,我们已经了解了怎样使用Android Studio 3.0创建一个带ListActivity和Fragment列表及其详情页的方法,同时学习了Activity和Fragment的基本用法。

    下面我们来实现后端API的接入与数据的展现。

    14.2.10 创建领域对象类Movie

    创建领域对象类Movie。我们使用Kotlin中的数据类来实现,代码如下:

    其中的id、title、overview、posterPath分别与JSON中的key对应。接下来是JSON数据的解析。

    14.2.11 JSON数据解析

    我们调用的API是:

    val VOTE_AVERAGE_API = "http://api.themoviedb.org//3/discover/movie? certification_country=US&cer- tification=R&sort_by=vote_average.desc&api_key=7e55a88ece9f03408b895a96 c1487979"

    其数据返回如下:

    我们使用fastjson来解析这个JSON数据。在app文件夹下的build.gradle中添加以下依赖:

    解析代码如下:

    然后把这个dataArray放到我们的MovieContent对象中。

    其中,addMovie代码如下:

    然后再分别新建MovieDetailActivity、MovieDetailFragment、MovieListActivity及activity_movie_list.xml、activity_movie_detail.xml、movie_detail.xml、movie_list.xml、movie_list_content.xml文件,下面分别对它们进行介绍。

    14.2.12 电影列表页面

    MovieListActivity是电影列表页面的Activity,代码如下:

    对应的布局文件分别如下。

    activity_movie_list.xml文件如下:

    movie_list.xml文件如下:

    movie_list_content.xml文件如下:

    电影列表整体布局的UI效果图如图14-36所示。

    图14-36 电影列表的整体布局

    14.2.13 视图数据适配器ViewAdapter

    在创建MovieListActivity过程中需要展示响应的数据,这些数据由ViewAdapter来承载,对应的代码如下:

    在上面的代码中定义了一个继承RecyclerView.Adapter的SimpleItemRecycler ViewAdapter类,来装载View中要显示的数据,实现数据与视图的解耦。View要显示的数据从Adapter里获取并展现出来。Adapter负责把真实的数据适配成一个个View,也就是说View要显示什么数据,取决于Adapter里的数据。

    14.2.14 视图中图像的展示

    在函数SimpleItemRecyclerViewAdapter.onBindViewHolder()中,设置View组件与Model数据的绑定。其中的电影海报是图片,所以我们的布局文件中使用了ImageView,对应的布局文件是movie_list_content.xml,代码如下:

    UI设计效果图如图14-37所示。

    图14-37 movie_list_content.xml布局UI效果图

    列表中图片的视图组件标签是ImageView,布局的XML代码如下:

    

    这里是根据图片的URL来展示图片,ImageView类有个setImageBitmap方法,可以直接设置Bitmap图片数据。见下面的代码:

    holder.mMoviePosterImageView.setImageBitmap(HttpUtil.getBitmapFromURL (item.posterPath))

    而通过URL获取Bitmap图片数据的代码是:

    14.2.15 电影详情页面

    MovieDetailActivity文件(电影详情页面)代码如下:

    其中,详情页的布局XML文件是activity_item_detail.xml,其代码如下:

    我们把电影详情的Fragment的展示放到NestedScrollView中:

    ppbar_scrolling_view_behavior" />

    电影详情的Fragment代码是MovieDetailFragment:

    其中的R.layout.movie_detail布局文件movie_detail.xml如下:

    14.2.16 电影源数据的获取

    首先定义一个MovieContent对象类来存储从API获取的数据,代码如下:

    在Android 4.0之后默认的线程模式是不允许在主线程中访问网络的。为了演示效果,我们在访问网络代码前,把ThreadPolicy设置为允许运行访问网络。

    val policy = StrictMode.ThreadPolicy.Builder().permitAll().build() StrictMode.setThreadPolicy(policy)

    然后使用一个data class Movie来存储电影对象数据。

    14.2.17 配置AndroidManifest.xml

    最后配置AndroidManifest.xml文件内容如下:

    因为我们要访问网络,所以需要添加以下配置:

    

    14.2.18 打包安装测试

    再次打包安装并运行程序,电影列表页面如图14-38所示。

    点击进入电影详情页,如图14-39所示。

    图14-38 电影列表页面

    图14-39 电影详情页

    14.3 本章小结

    Android中经常出现的空引用、API的冗余样板式代码等都是驱动我们转向Kotlin语言的动力。另外,Kotlin的Android视图DSL Anko可以让我们从繁杂的XML视图配置文件中解放出来。

    我们可以像在Java中一样方便地使用Android开发流行的库,如Butter Knife、Realm、RecyclerView等。当然,使用Kotlin集成这些库进行Android开发,既能够直接使用我们之前的开发库,又能够从Java语言、Android API的限制中解脱出来,这不得不说是一件好事。未来,Android领域将是Kotlin的天下。

    本章工程源码地址是https://github/Android-Kotlin/MovieGuideDB。