鍍金池/ 教程/ iOS/ iOS 開發(fā)者的 Android 第一課
與四軸無(wú)人機(jī)的通訊
在沙盒中編寫腳本
結(jié)構(gòu)體和值類型
深入理解 CocoaPods
UICollectionView + UIKit 力學(xué)
NSString 與 Unicode
代碼簽名探析
測(cè)試
架構(gòu)
第二期-并發(fā)編程
Metal
自定義控件
iOS 中的行為
行為驅(qū)動(dòng)開發(fā)
Collection View 動(dòng)畫
截圖測(cè)試
MVVM 介紹
使 Mac 應(yīng)用數(shù)據(jù)腳本化
一個(gè)完整的 Core Data 應(yīng)用
插件
字符串
為 iOS 建立 Travis CI
先進(jìn)的自動(dòng)布局工具箱
動(dòng)畫
為 iOS 7 重新設(shè)計(jì) App
XPC
從 NSURLConnection 到 NSURLSession
Core Data 網(wǎng)絡(luò)應(yīng)用實(shí)例
GPU 加速下的圖像處理
自定義 Core Data 遷移
子類
與調(diào)試器共舞 - LLDB 的華爾茲
圖片格式
并發(fā)編程:API 及挑戰(zhàn)
IP,TCP 和 HTTP
動(dòng)畫解釋
響應(yīng)式 Android 應(yīng)用
初識(shí) TextKit
客戶端
View-Layer 協(xié)作
回到 Mac
Android
Core Image 介紹
自定義 Formatters
Scene Kit
調(diào)試
項(xiàng)目介紹
Swift 的強(qiáng)大之處
測(cè)試并發(fā)程序
Android 通知中心
調(diào)試:案例學(xué)習(xí)
從 UIKit 到 AppKit
iOS 7 : 隱藏技巧和變通之道
安全
底層并發(fā) API
消息傳遞機(jī)制
更輕量的 View Controllers
用 SQLite 和 FMDB 替代 Core Data
字符串解析
終身學(xué)習(xí)的一代人
視頻
Playground 快速原型制作
Omni 內(nèi)部
同步數(shù)據(jù)
設(shè)計(jì)優(yōu)雅的移動(dòng)游戲
繪制像素到屏幕上
相機(jī)與照片
音頻 API 一覽
交互式動(dòng)畫
常見的后臺(tái)實(shí)踐
糟糕的測(cè)試
避免濫用單例
數(shù)據(jù)模型和模型對(duì)象
Core Data
字符串本地化
View Controller 轉(zhuǎn)場(chǎng)
照片框架
響應(yīng)式視圖
Square Register 中的擴(kuò)張
DTrace
基礎(chǔ)集合類
視頻工具箱和硬件加速
字符串渲染
讓東西變得不那么糟
游戲中的多點(diǎn)互聯(lián)
iCloud 和 Core Data
Views
虛擬音域 - 聲音設(shè)計(jì)的藝術(shù)
導(dǎo)航應(yīng)用
線程安全類的設(shè)計(jì)
置換測(cè)試: Mock, Stub 和其他
Build 工具
KVC 和 KVO
Core Image 和視頻
Android Intents
在 iOS 上捕獲視頻
四軸無(wú)人機(jī)項(xiàng)目
Mach-O 可執(zhí)行文件
UI 測(cè)試
值對(duì)象
活動(dòng)追蹤
依賴注入
Swift
項(xiàng)目管理
整潔的 Table View 代碼
Swift 方法的多面性
為什么今天安全仍然重要
Core Data 概述
Foundation
Swift 的函數(shù)式 API
iOS 7 的多任務(wù)
自定義 Collection View 布局
測(cè)試 View Controllers
訪談
收據(jù)驗(yàn)證
數(shù)據(jù)同步
自定義 ViewController 容器轉(zhuǎn)場(chǎng)
游戲
調(diào)試核對(duì)清單
View Controller 容器
學(xué)無(wú)止境
XCTest 測(cè)試實(shí)戰(zhàn)
iOS 7
Layer 中自定義屬性的動(dòng)畫
第一期-更輕量的 View Controllers
精通 iCloud 文檔存儲(chǔ)
代碼審查的藝術(shù):Dropbox 的故事
GPU 加速下的圖像視覺
Artsy
照片擴(kuò)展
理解 Scroll Views
使用 VIPER 構(gòu)建 iOS 應(yīng)用
Android 中的 SQLite 數(shù)據(jù)庫(kù)支持
Fetch 請(qǐng)求
導(dǎo)入大數(shù)據(jù)集
iOS 開發(fā)者的 Android 第一課
iOS 上的相機(jī)捕捉
語(yǔ)言標(biāo)簽
同步案例學(xué)習(xí)
依賴注入和注解,為什么 Java 比你想象的要好
編譯器
基于 OpenCV 的人臉識(shí)別
玩轉(zhuǎn)字符串
相機(jī)工作原理
Build 過程

iOS 開發(fā)者的 Android 第一課

隨著移動(dòng)軟件工業(yè)的發(fā)展,一個(gè)移動(dòng)產(chǎn)品只局限于 iOS 系統(tǒng)變得越來(lái)越不切實(shí)際。 Android 目前占有近 80% 的智能手機(jī)份額1,它能給一個(gè)產(chǎn)品帶來(lái)的潛在用戶量實(shí)在不能再被忽略了。

在本文中,我會(huì)在 iOS 的開發(fā)范圍內(nèi)介紹 Android 開發(fā)的核心內(nèi)容。 Android 和 iOS 處理類似的問題集,但在大部分問題上,它們都有不同的解決方式。通過本文,我會(huì)使用一個(gè)配套項(xiàng)目(在 GitHub 上)來(lái)說明如何在兩個(gè)平臺(tái)上開發(fā)以完成相同的任務(wù)。

除了 iOS 開發(fā)的相關(guān)知識(shí),我假設(shè)你在 Java 上也有一定經(jīng)驗(yàn),能夠安裝和使用ADT(Android Development Tools)。此外,如果你最近才開始 Android 開發(fā),讀一遍 Google 編寫的關(guān)于創(chuàng)建你的第一個(gè)應(yīng)用的教程會(huì)很有幫助。

UI設(shè)計(jì)概要

本文不會(huì)深入到介紹 iOS 和 Android 在用戶體驗(yàn)和設(shè)計(jì)模式上的不同。然而,了解一些當(dāng)今 Android 上使用的關(guān)鍵 UI 范式,比如 Action Bar、Overflow Menu、Back Button、Share Action 等,還是會(huì)很有好處的。如果你正在認(rèn)真考慮 Android 開發(fā),我推薦你從 Google Play Store 買個(gè) Nexus 5,將它作為你的主要設(shè)備,用滿一周,強(qiáng)迫自己最大程度的去體驗(yàn)這個(gè)操作系統(tǒng)。一個(gè)開發(fā)者若不清楚要為之開發(fā)的操作系的關(guān)鍵使用模式,就那是對(duì)產(chǎn)品的不負(fù)責(zé)任。

語(yǔ)言應(yīng)用結(jié)構(gòu)

Java

Objective-C 和 Java 之間有很多不同,雖然若能將 Objective-C 的方式帶入 Java 可能會(huì)很有誘惑力,但這樣做很可能導(dǎo)致代碼庫(kù)與驅(qū)動(dòng)它的主要框架產(chǎn)生沖突。總之,有一些需要提防地陷阱:

  • 類前綴就留在 Objective-C 里不要帶過來(lái)了。Java 有實(shí)在的命名空間和包管理,所以不再需要類前綴。
  • 實(shí)例變量的前綴是 m,不是 _。盡可能多的在代碼里使用JavaDoc來(lái)寫方法和類描述,它能讓你和其他人更舒服些。
  • Null 檢查!Objective-C能妥善處理向nil發(fā)送消息,但Java不行。
  • 向?qū)傩哉f再見。如果你想要 setter 和 getter,你只能實(shí)際地創(chuàng)建一個(gè) getVariableName()方法,并顯式的調(diào)用它。使用 this.object 不會(huì)調(diào)用你自定義地getter,你必須使用 this.getObjct
  • 同樣的,給方法名加上 getset 前綴來(lái)更好的識(shí)別 getter 和 setter 。Java 方法通常寫為動(dòng)作和查詢,例如 getCell(),而不是 cellForRowAtIndexPath:。

項(xiàng)目結(jié)構(gòu)

Android 應(yīng)用主要分為兩個(gè)部分,第一部分是 Java 源代碼。源代碼通過 Java 包的方式進(jìn)行組織,所以可按照你的喜好來(lái)決定。然而一個(gè)常見的實(shí)踐是為 Activity、Fragment、View、Adapter 和 Data(模型和管理器)使用頂層的類別(top-level categories)。

第二個(gè)主要部分是 res 文件夾,也就是資源文件夾。res 文件夾包含有圖像、 XML 布局文件,以及 XML 值文件,它們構(gòu)成了大部分非代碼資源。在 iOS 上,圖像可有 @2x 版本,但在 Android 上有好幾種不同的屏幕密度文件夾要考慮2。Android 使用文件夾來(lái)組織文件、字符串以及其他與屏幕密度相關(guān)的值。res 文件夾還包含有 XML 布局文件,就像 xib 文件一樣。最后,還有其他 XML 文件存儲(chǔ)了字符串、整數(shù)和樣式資源。

最后一個(gè)與項(xiàng)目結(jié)構(gòu)相關(guān)的是 AndroidManifest.xml 文件。這個(gè)文件等同于 iOS 上的 Project-Info.plist 文件,它存儲(chǔ)著 Activity 信息、應(yīng)用名字,并設(shè)置應(yīng)用能處理的 Intent 3(系統(tǒng)級(jí)事件)。關(guān)于 Intent 的更多信息,繼續(xù)閱讀本文,或者閱讀 Intents 這篇文章。

Activity

Activity 是 Android 應(yīng)用的基本顯示單元,就像 UIViewController 是iOS的基本顯示組件一樣。作為 UINavigationController 的替代,Android 由系統(tǒng)來(lái)維護(hù)一個(gè) Activity 棧。當(dāng)應(yīng)用完成加載,系統(tǒng)將應(yīng)用的主 Activity(main activity)壓到棧上。注意你也可以加載其他應(yīng)用的 Activity 并將它們放在棧里。默認(rèn),Android 上的返回(back)按鈕將從系統(tǒng)的 Activity 棧中彈出 Activity,所以當(dāng)用戶不停地按下返回時(shí),他可以見到多個(gè)曾經(jīng)加載過的應(yīng)用。

通過使用包含有額外的數(shù)據(jù) Intent,Activity 同樣可以初始化其他 Activity 。通過 Intent 啟動(dòng) Activity 類似于通過自定義的 init 方法創(chuàng)建一個(gè) UIViewController。因?yàn)樽畛R姷募虞d新 Activity 的方法是創(chuàng)建一個(gè)有數(shù)據(jù)的 Intent,在 Android 上暴露自定義初始化方法的一個(gè)非常棒的方式是創(chuàng)建一個(gè)靜態(tài) Intent getter 方法。Activity 同樣能在完成時(shí)返回結(jié)果(再見,modal 代理),當(dāng)其完成時(shí)在 Intent 上放置額外數(shù)據(jù)即可。

Android 和 iOS 的一大差別是任何 Activity 都可以作為你應(yīng)用的入口,只要它們?cè)?AndroidManifest文件里正確注冊(cè)即可。在 AndroidManifest.xml 文件中,為一個(gè) Activity 設(shè)置一個(gè) media intent 的 Intent 過濾器的話,就能讓系統(tǒng)知道這個(gè) Activity 可以作為包含有媒體數(shù)據(jù)的 Intent 的入口。一個(gè)不錯(cuò)的例子是相片編輯 Activity ,它打開一個(gè)相片,修改它,并在 Activity 完成時(shí)返回修改后的圖片。

作為一個(gè)旁注,如果你想在 Activity 和 Fragment 之間發(fā)送模型對(duì)象的話,它們必須實(shí)現(xiàn) Parcelable 接口。實(shí)現(xiàn) Parcelable 接口很類似于 iOS 上實(shí)現(xiàn) <NSCopying> 協(xié)議。同樣值得一提的是,Parcelable 對(duì)象可以存儲(chǔ)在 activity 或者 fragment 的 savedInstanceState 里,這是為了能更容易地在它們被銷毀后恢復(fù)它們的狀態(tài)。

接下來(lái)就看看一個(gè) Activity 啟動(dòng)另一個(gè) Activity,同時(shí)能在第二個(gè) Activity 完成時(shí)做出響應(yīng)。

加載另一個(gè)Activity并得到結(jié)果

// request code 是為返回 activities 所設(shè)置的特定值
private static final int REQUEST_CODE_NEXT_ACTIVITY = 1234;

protected void startNextActivity() {
    // Intents 需要一個(gè) context, 所以將當(dāng)前的 activity 作為 context 給入
    Intent nextActivityIntent = new Intent(this, NextActivity.class);
       startActivityForResult(nextActivityResult, REQUEST_CODE_NEXT_ACTIVITY);
}

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    switch (requestCode) {
    case REQUEST_CODE_NEXT_ACTIVITY:
        if (resultCode == RESULT_OK) {
            // 這表示我們的 activity 成功返回了?,F(xiàn)在顯示一段提示文字
            // 這里在屏幕上創(chuàng)建了一個(gè)簡(jiǎn)單的 pop-up 消息框
                Toast.makeText(this, "Result OK!", Toast.LENGTH_SHORT).show();
            }
            return;
        }    
        super.onActivityResult(requestCode, resultCode, data);
}

在 Activity finish() 上返回結(jié)果

public static final String activityResultString = "activityResultString";

/*
 * 結(jié)束時(shí)調(diào)用, 在 intent 上設(shè)置 object ID 并調(diào)用成功結(jié)束
 * @param returnObject 是要處理的對(duì)象
 */
private void onActivityResult(Object returnObject) {
        Intent data = new Intent();
        if (returnObject != null) {
            data.putExtra(activityResultString, returnObject.uniqueId);
        }

        setResult(RESULT_OK, data);
        finish();        
}

Fragments

Fragment 的概念是 Android 獨(dú)有的,它最近才隨著 Android 3.0 的問世而出現(xiàn)。Fragment 是一種迷你控制器,能夠被實(shí)例化來(lái)填充 Activity。它們可以存儲(chǔ)狀態(tài)信息,還有可能包含視圖邏輯,但區(qū)別 Activity 和 Fragment 的最大不同在于。同一時(shí)間里屏幕上可能有多個(gè) Fragment。同時(shí)注意 Fragment 自身沒有上下文,它們嚴(yán)重依賴 Activity 來(lái)將它們和應(yīng)用的狀態(tài)聯(lián)系起來(lái)。

平板是使用 Fragment 的絕好場(chǎng)景:你可以在左邊放一個(gè)列表 Fragment,右邊放一個(gè)詳細(xì)信息 Fragment。4Fragment 能讓你將 UI 和控制器邏輯分割到更小、可重用的層面上。但要當(dāng)心,F(xiàn)ragment 的生命周期有不少細(xì)微差別,我們會(huì)在后面詳細(xì)談到。

http://wiki.jikexueyuan.com/project/objc/images/11-1.png" alt="" />

Fragment 是實(shí)現(xiàn) App 的新方式,就像在 iOS 上 UICollectionView 是可取代 UITableView 的構(gòu)造列表數(shù)據(jù)的新方式。5 雖然在一開始避開 Fragment 而使用 Activity 會(huì)比較容易,但你之后可能會(huì)為之后悔。然而,我們也要抗拒那種想完全放棄 Activity,轉(zhuǎn)而只在單個(gè) Activity 上使用 Fragment 的沖動(dòng),因?yàn)槿绻敲醋隽?,那么?dāng)你想獲得 Intent 的好處且想在同一個(gè) Activity 上使用多個(gè) Fragment 時(shí),你將陷入困境。

現(xiàn)在來(lái)看一個(gè)例子,UITableViewControllerListFragment 是如何分別顯示一個(gè)地鐵行程預(yù)測(cè)時(shí)刻表,數(shù)據(jù)由 MBTA 所提供。

Table View Controller 實(shí)現(xiàn)

http://wiki.jikexueyuan.com/project/objc/images/11-2.png" alt="" />

@interface MBTASubwayTripTableTableViewController ()

@property (assign, nonatomic) MBTATrip *trip;

@end

@implementation MBTASubwayTripTableTableViewController

- (instancetype)initWithTrip:(MBTATrip *)trip
{
    self = [super initWithStyle:UITableViewStylePlain];
    if (self) {
        _trip = trip;
        [self setTitle:trip.destination];
    }
    return self;
}

- (void)viewDidLoad
{
    [super viewDidLoad];

    [self.tableView registerClass:[MBTAPredictionCell class] forCellReuseIdentifier:[MBTAPredictionCell reuseId]];
    [self.tableView registerNib:[UINib nibWithNibName:NSStringFromClass([MBTATripHeaderView class]) bundle:nil] forHeaderFooterViewReuseIdentifier:[MBTATripHeaderView reuseId]];
}

#pragma mark - UITableViewDataSource

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
    return 1;
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    return [self.trip.predictions count];
}

#pragma mark - UITableViewDelegate

- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section
{
    return [MBTATripHeaderView heightWithTrip:self.trip];
}

- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section
{
    MBTATripHeaderView *headerView = [self.tableView dequeueReusableHeaderFooterViewWithIdentifier:[MBTATripHeaderView reuseId]];
    [headerView setFromTrip:self.trip];
    return headerView;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:[MBTAPredictionCell reuseId] forIndexPath:indexPath];

    MBTAPrediction *prediction = [self.trip.predictions objectAtIndex:indexPath.row];
    [(MBTAPredictionCell *)cell setFromPrediction:prediction];

    return cell;
}

- (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath
{
    return NO;
}

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
    [tableView deselectRowAtIndexPath:indexPath animated:YES];
}

@end

List Fragment 實(shí)現(xiàn)

http://wiki.jikexueyuan.com/project/objc/images/11-3.png" alt="" />

public class TripDetailFragment extends ListFragment {

    /**
     * Trip Detail Fragment的配置標(biāo)識(shí).
     */
    public static final class TripDetailFragmentState {
        public static final String KEY_FRAGMENT_TRIP_DETAIL = "KEY_FRAGMENT_TRIP_DETAIL";
    }

    protected Trip mTrip;

    /**
     * 根據(jù)提供的參數(shù)使用這個(gè)工廠方法來(lái)創(chuàng)建 fragment 的新的實(shí)例
     *
     * @param trip trip的詳細(xì)信息
     * @return fragment TripDetailFragment 的新實(shí)例.
     */
    public static TripDetailFragment newInstance(Trip trip) {
        TripDetailFragment fragment = new TripDetailFragment();
        Bundle args = new Bundle();
        args.putParcelable(TripDetailFragmentState.KEY_FRAGMENT_TRIP_DETAIL, trip);
        fragment.setArguments(args);
        return fragment;
    }

    public TripDetailFragment() { }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        Prediction[] predictions= mTrip.predictions.toArray(new Prediction[mTrip.predictions.size()]);
        PredictionArrayAdapter predictionArrayAdapter = new PredictionArrayAdapter(getActivity(), predictions);
        setListAdapter(predictionArrayAdapter);
        return super.onCreateView(inflater,container, savedInstanceState);
    }

    @Override
    public void onViewCreated(View view, Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        TripDetailsView headerView = new TripDetailsView(getActivity());
        headerView.updateFromTripObject(mTrip);
        getListView().addHeaderView(headerView);
    }
}

下一節(jié),我們將研究一些 Android 獨(dú)有的組件。

通用Android組件

列表視圖與適配器

ListView 是 Android 上 UITableView 的近似物,也是最常使用的一種組件。就像 UITableView 有一個(gè)助手 View Controller UITableViewController 那樣,ListView 也有一個(gè)助手 Activity 叫做 ListActivity,它還有一個(gè)助手 Fragment 叫做 ListFragment。同UITableViewController類似,這些助手為你處理布局(類似 xib)并提供管理適配器(下面將討論)能夠使用的簡(jiǎn)便方法。上面的例子使用一個(gè) ListFragment 來(lái)顯示來(lái)自一個(gè) Prediction 模型對(duì)象列表的數(shù)據(jù),類比一下,其實(shí)就相當(dāng)于 UITableView 的 datasource 提供了一個(gè) Prediction 模型對(duì)象數(shù)組,并用它來(lái)填充 ListView。

說到 datasource,在 Android 上,我們沒有用于 ListView 的 datasource 和 delegate。作為代替,我們有適配器 (adapters)。適配器有多種形式,但它們的主要目標(biāo)類似于將 datasource 和 delegate 合二為一。適配器拿到數(shù)據(jù)并通過實(shí)例化視圖適配它去填充 ListView,這樣 ListView 就會(huì)顯示出來(lái)了。讓我們來(lái)看看上面使用的數(shù)組適配器:

public class PredictionArrayAdapter extends ArrayAdapter<Prediction> {

    int LAYOUT_RESOURCE_ID = R.layout.view_three_item_list_view;

    public PredictionArrayAdapter(Context context) {
        super(context, R.layout.view_three_item_list_view);
    }

    public PredictionArrayAdapter(Context context, Prediction[] objects) {
        super(context, R.layout.view_three_item_list_view, objects);
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent)
    {
        Prediction prediction = this.getItem(position);
        View inflatedView = convertView;
        if(convertView==null)
        {
            LayoutInflater inflater = (LayoutInflater)getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
            inflatedView = inflater.inflate(LAYOUT_RESOURCE_ID, parent, false);
        }

        TextView stopNameTextView = (TextView)inflatedView.findViewById(R.id.view_three_item_list_view_left_text_view);
        TextView middleTextView = (TextView)inflatedView.findViewById(R.id.view_three_item_list_view_middle_text_view);
        TextView stopSecondsTextView = (TextView)inflatedView.findViewById(R.id.view_three_item_list_view_right_text_view);

        stopNameTextView.setText(prediction.stopName);
        middleTextView.setText("");
        stopSecondsTextView.setText(prediction.stopSeconds.toString());

        return inflatedView;
    }
}

你會(huì)注意到此適配器有一個(gè)叫做 getView 的重要方法,它非常類似于 cellForRowAtIndexPath: 。另一個(gè)易被發(fā)現(xiàn)的相似點(diǎn)是一個(gè)重用視圖的模式,類似于 iOS 6。視圖重用同在 iOS 上的情況一樣重要,因?yàn)樗芊浅o@著地提高性能!這個(gè)適配器有點(diǎn)兒簡(jiǎn)單,因?yàn)樗褂昧藘?nèi)建的父類 ArrayAdapter<T> ,用于數(shù)組數(shù)據(jù),但它依然說明了如何用一個(gè)數(shù)據(jù)集來(lái)填充一個(gè) ListView。

AsyncTasks

作為 iOS 上 GCD(Grand Central Dispatch)的替代,Android 上可以使用 AsyncTasksAsyncTasks 是一個(gè)以更加友好的方式處理異步的工具。AsyncTasks有點(diǎn)超出本文的范圍,但我強(qiáng)烈建議你閱讀相關(guān)文檔。

Activity 生命周期

從 iOS 開發(fā)轉(zhuǎn)過來(lái)時(shí)候,我們需要注意的首要事情之一是安卓的生命周期。讓我們從查看 Activity 生命周期文檔 開始:

http://wiki.jikexueyuan.com/project/objc/images/11-4.png" alt="" />

從本質(zhì)上看來(lái),Activity 生命周期近似于 UIViewController 生命周期。主要的不同是 Android 系統(tǒng)在銷毀 Activity 上比較無(wú)情,因此保證數(shù)據(jù)和 Activity 的狀態(tài)的保存是非常重要的,因此只有這樣它們才能在 onCreate() 中從被保存的狀態(tài)里恢復(fù)。做到這個(gè)的最好方式是使用綁定數(shù)據(jù)(bundled data)并從 savedInstanceState 和/或 Intents 中恢復(fù)。例如,下面是來(lái)自我們示例項(xiàng)目中 TripListActivity 的部分代碼,它能跟蹤當(dāng)前顯示的地鐵線路:

public static Intent getTripListActivityIntent(Context context, TripList.LineType lineType) {
    Intent intent = new Intent(context, TripListActivity.class);
    intent.putExtra(TripListActivityState.KEY_ACTIVITY_TRIP_LIST_LINE_TYPE, lineType.getLineName());
    return intent;
}

public static final class TripListActivityState {
    public static final String KEY_ACTIVITY_TRIP_LIST_LINE_TYPE = "KEY_ACTIVITY_TRIP_LIST_LINE_TYPE";
}

TripList.LineType mLineType;    

@Override
protected void onCreate(Bundle savedInstanceState) {
   super.onCreate(savedInstanceState);
   mLineType = TripList.LineType.getLineType(getIntent().getStringExtra(TripListActivityState.KEY_ACTIVITY_TRIP_LIST_LINE_TYPE));
}    

注意旋轉(zhuǎn):在旋轉(zhuǎn)時(shí),生命周期會(huì)被完全重設(shè)。就是說,在旋轉(zhuǎn)發(fā)生時(shí),你的 Activity 將被摧毀并重建。如果數(shù)據(jù)被正確保存在 savedInstanceState 里,而且 Activity 能在重新創(chuàng)建后正確恢復(fù)的話,那么旋轉(zhuǎn)看起來(lái)就會(huì)無(wú)縫平滑。許多應(yīng)用開發(fā)者開發(fā)的應(yīng)用因?yàn)? Activity 沒有正確處理狀態(tài)的改變,導(dǎo)致在旋轉(zhuǎn)時(shí)出現(xiàn)穩(wěn)定性問題。小心!不要通過鎖定屏幕旋轉(zhuǎn)來(lái)避免這種問題,這樣做只會(huì)掩蓋生命周期 bug,它們依然會(huì)在 Activity 被系統(tǒng)摧毀的時(shí)候冒出來(lái)。

Fragment 生命周期

Fragment 生命周期相似于 Activity 生命周期,但有些附加的東西:

http://wiki.jikexueyuan.com/project/objc/images/11-5.png" alt="" />

能讓開發(fā)者措手不及的問題之一是 Fragment 和 Activity 之間的通信問題。注意 onAttach() 先于 onActivityCreated() 調(diào)用。這就意味著 Activity 不能保證在 Fragment 被創(chuàng)建前存在。onActivityCreated() 方法應(yīng)該在有必要的時(shí)候用于將 interface(delegate)設(shè)置到父親 Activity 上。

Fragment 同樣被操作系統(tǒng)積極地創(chuàng)建和銷毀,為了保存它們的狀態(tài),需要同 Activity 一樣多的勞動(dòng)量。下面是來(lái)自示例項(xiàng)目的一個(gè)例子,此處的旅程列表 Fragment 一直追蹤TripList數(shù)據(jù),以及地鐵線路類型:

/**
 * Trip List Fragment 的配置標(biāo)識(shí).
 */
public static final class TripListFragmentState {
    public static final String KEY_FRAGMENT_TRIP_LIST_LINE_TYPE = "KEY_FRAGMENT_TRIP_LIST_LINE_TYPE";
    public static final String KEY_FRAGMENT_TRIP_LIST_DATA = "KEY_FRAGMENT_TRIP_LIST_DATA";
}

/**
 * Use this factory method to create a new instance of
 * this fragment using the provided parameters.
 *
 * @param lineType the subway line to show trips for.
 * @return A new instance of fragment TripListFragment.
 */
public static TripListFragment newInstance(TripList.LineType lineType) {
    TripListFragment fragment = new TripListFragment();
    Bundle args = new Bundle();
    args.putString(TripListFragmentState.KEY_FRAGMENT_TRIP_LIST_LINE_TYPE, lineType.getLineName());
    fragment.setArguments(args);
    return fragment;
}

protected TripList mTripList;
protected void setTripList(TripList tripList) {
    Bundle arguments = this.getArguments();
    arguments.putParcelable(TripListFragmentState.KEY_FRAGMENT_TRIP_LIST_DATA, tripList);
    mTripList = tripList;
    if (mTripArrayAdapter != null) {
        mTripArrayAdapter.clear();
        mTripArrayAdapter.addAll(mTripList.trips);
    }
}

@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    if (getArguments() != null) {
        mLineType = TripList.LineType.getLineType(getArguments().getString(TripListFragmentState.KEY_FRAGMENT_TRIP_LIST_LINE_TYPE));
        mTripList = getArguments().getParcelable(TripListFragmentState.KEY_FRAGMENT_TRIP_LIST_DATA);
    }
}    

注意到 Fragment 總是用 onCreate 里的綁定參數(shù)(bundled arguments)來(lái)恢復(fù)它的狀態(tài),用于 TripList 模型對(duì)象的自定義的 setter 會(huì)將對(duì)象添加到綁定參數(shù)中去。這就保證了如果 Fragment 在例如設(shè)備被旋轉(zhuǎn)時(shí)被銷毀并重建的話,F(xiàn)ragment 總是有最新的數(shù)據(jù)并從中恢復(fù)。

布局

類似于 Android 開發(fā)的其他部分,Android 對(duì)比 iOS,在指定布局這里同樣有優(yōu)點(diǎn)和缺點(diǎn)。布局被存儲(chǔ)為人類可讀的 XML 文件,放在 res/layouts 文件夾中。

地鐵列表視圖布局

http://wiki.jikexueyuan.com/project/objc/images/11-6.png" alt="" />

<RelativeLayout 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"
    tools:context="com.example.androidforios.app.activities.MainActivity$PlaceholderFragment">

    <ListView
        android:id="@+id/fragment_subway_list_listview"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:paddingBottom="@dimen/Button.Default.Height"/>

    <Button
        android:id="@+id/fragment_subway_list_Button"
        android:layout_width="match_parent"
        android:layout_height="@dimen/Button.Default.Height"
        android:minHeight="@dimen/Button.Default.Height"
        android:background="@drawable/button_red_selector"
        android:text="@string/hello_world"
        android:textColor="@color/Button.Text"
        android:layout_alignParentBottom="true"
        android:gravity="center"/>

</RelativeLayout>

下面是 iOS 上在 Interface Builder 中用UITableView和一個(gè)通過 Auto Layout 釘在底部的 UIButton 實(shí)現(xiàn)的同一個(gè)視圖:

http://wiki.jikexueyuan.com/project/objc/images/11-7.png" alt="" />

http://wiki.jikexueyuan.com/project/objc/images/11-8.png" alt="" />

你會(huì)注意到 Android 布局文件更易閱讀和理解。Android 中的布局視圖有許多不同的部分,但這里我們只會(huì)覆蓋到少數(shù)幾個(gè)重要的部分。

你需要處理的主要結(jié)構(gòu)就是 ViewGroup 的子類,比如 RelativeLayout、LinearLayout,以及 FrameLayout,這些就是最常見的。這些 ViewGroup 包含其他視圖并暴露屬性來(lái)在屏幕上安排它們。

一個(gè)不錯(cuò)的例子是使用上面提到的 RelativeLayout,一個(gè)相對(duì)布局允許我們?cè)诓季种惺褂?android:layout_alignParentBottom="true" 這樣的語(yǔ)句來(lái)將按鈕釘在底部。

最后,要將布局連接到 Fragment 或 Activity,只需要簡(jiǎn)單地在 onCreateView 上使用布局的資源 ID 即可:

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
    return inflater.inflate(R.layout.fragment_subway_listview, container, false);
}

布局小貼士

  • 總是去處理 dp(密度無(wú)關(guān)的像素)的情況,而不是直接使用像素。
  • 不要在可視化編輯器里去調(diào)整的部件 - 通常來(lái)說可視化編輯器會(huì)在部件之間加間隔,而不是像你所期望的那樣去調(diào)整高寬。最佳選擇應(yīng)該是直接在 XML 文件中進(jìn)行編輯。
  • 如果你曾看到 fill_parent 值用于布局的高或?qū)挘@個(gè)值在好幾年前的 API 8 中就被廢棄并被 match_parent 取代了。

查看看這篇響應(yīng)式 Android 應(yīng)用的文章能得到更多布局小貼士。

數(shù)據(jù)

Android 上可用的數(shù)據(jù)存儲(chǔ)選項(xiàng)同樣類似于 iOS 上可用的:

  • Shared Preferences <-> NSUserDefaults
  • 內(nèi)存對(duì)象
  • 通過內(nèi)部外部文件存儲(chǔ)將數(shù)據(jù)保存到文件結(jié)構(gòu)或是從文件結(jié)構(gòu)獲取數(shù)據(jù) <-> 保存數(shù)據(jù)到 documents 目錄
  • SQLite <-> Core Data

主要的不同是缺少 Core Data,作為替代,Android 提供了直接訪問 SQLite 數(shù)據(jù)庫(kù)的方式,并返回一個(gè)游標(biāo) (cursor) 對(duì)象作為結(jié)果。請(qǐng)看這篇在 Android 上使用 SQLite 的文章獲取更多此問題的細(xì)節(jié)。

Android 家庭作業(yè)

我們目前為止討論的只是一些皮毛而已。要真正從一些 Android 特有的事物里獲取好處,我建議關(guān)注以下這些特性:

編者注 關(guān)于模擬器,個(gè)人推薦GenyMotion的相關(guān)解決方案

最后的話

本文中我們討論的大部分都實(shí)現(xiàn)在 MBTA 地鐵交通示例項(xiàng)目中,并放在了Github上。創(chuàng)建這個(gè)項(xiàng)目的目的是在兩個(gè)平臺(tái)上展示應(yīng)用的結(jié)構(gòu)、綁定數(shù)據(jù)、構(gòu)建 UI 等相似內(nèi)容的方式。

雖然在 Android 上一些純粹的實(shí)現(xiàn)細(xì)節(jié)非常不同,但將從 iOS 學(xué)來(lái)的問題解決技能和模式用于實(shí)踐依然非常容易。也許懂得一些 Android 的工作方式可能可以讓你準(zhǔn)備好面對(duì)下一版的 iOS,誰(shuí)又知道會(huì)怎樣呢?