1. 程式人生 > >【Android學習】案例學開發,天氣記事本專案學習總結。RxJava+Retrofit2+greenDAO

【Android學習】案例學開發,天氣記事本專案學習總結。RxJava+Retrofit2+greenDAO

    之前一直沒怎麼做過涉及資料庫的應用(因為嫌麻煩^_^),只會書上講的的基礎方法進行增刪改查。

最近學了greenDAO,就試著結合以前學的寫個記事本的小應用練手,順便鞏固一下之前所學。

                      專案很簡單,CollapsingToolbarLayout 配合 CoordinatorLayout 使用。

效果圖:


來看設計圖:


我覺得這種控制元件就得自己儲存一個樣例,不然時間一長不去用就會忘掉。

內容用Tablayout+ ViewPager來展示資料。

TabLayout有兩種設定標籤的方式:

第一種

TabLayout tabLayout = ...;
tabLayout.addTab(tabLayout.newTab().setText("Tab 1"));
tabLayout.addTab(tabLayout.newTab().setText("Tab 2"));
tabLayout.addTab(tabLayout.newTab().setText("Tab 3"));

第二種

 <android.support.design.widget.TabLayout
         android:layout_height="wrap_content"
         android:layout_width="match_parent">

     <android.support.design.widget.TabItem
             android:text="@string/tab_text"/>

     <android.support.design.widget.TabItem
             android:icon="@drawable/ic_android"/>

 </android.support.design.widget.TabLayout>

其他佈局略、、

接下來就是程式碼java部分,首先是從網路獲取bing今日的圖片。我之前有文章寫怎麼獲取:

使用Retrofit 獲取圖片地址;

定義介面:

public interface BingApi {
    @GET("bing/day/{what_day}/mkt/{country}")
    Observable<ResponseBody> getBingPicPath(@Path("what_day") String what_day,
@Path("country") String country);
}

建立Retrofit:

public static 
WeatherApi weatherApi; //獲取bing桌布地址 public static BingApi getBingApi() { if (bingApi == null) { Retrofit retrofit = new Retrofit.Builder() .baseUrl("http://test.dou.ms/") .addCallAdapterFactory(RxJavaCallAdapterFactory.create()) .build(); bingApi = retrofit.create(BingApi.class); } return bingApi; }

擷取字串:

// 擷取字串中 圖片的地址
public static String GetBingImageUrl(String str) {
    String[] strArray = str.split("地址:");
    return strArray[1];
}

Bing圖片和天氣資訊我是在啟動介面展示時獲取的。

天氣資訊和之前那一篇文章獲取方式一致,不再貼了。

需要他倆都獲取到資料後在進行跳轉。當然了,我會先判斷有沒有網路,沒有就等2秒後跳轉到主介面,

有網路就獲取後再跳轉。不過要記得自定義超時時間,畢竟網速慢的話不能在啟動介面停留10秒啊(預設是幾秒來著,反正很長啦)。

寫到這裡我想起來我沒有處理進入主介面後如果有網路了怎麼破 ,啊咧。

天氣資訊還好,只要切換城市就能再次發出請求獲取資料,桌布就不行了。

這種事情很簡單啦,可以定義一個服務來監聽網路狀態,對的,正好service生疏了,那麼就下次配合RxBus再寫吧。

所以先看看使用combineLatest操作符的使用吧:

CombineLatest

當兩個Observables中的任何一個發射了資料時,使用一個函式結合每個Observable發射的最近資料項,並且基於這個函式的結果發射資料。

combineLatest

CombineLatest操作符行為類似於zip,但是隻有當原始的Observable中的每一個都發射了一條資料時zip才發射資料。CombineLatest則在原始的Observable中任意一個發射了資料時發射一條資料。當原始Observables的任何一個發射了一條資料時,CombineLatest使用一個函式結合它們最近發射的資料,然後發射這個函式的返回值。

所以用這個來觀測獲取圖片和天氣後進行跳轉。

(異常也直接跳轉)

程式碼:

Observable.combineLatest(NetWork.getBingApi().getBingPicPath("0", "ZH-CN"), NetWork.getWeatherApi()
        .getWeatherInfo(city, API_KEY), new Func2<ResponseBody, Weather, Boolean>() {
    @Override
public Boolean call(ResponseBody responseBody, Weather weather) {
        try {
            AppUtils.back_url = GetBingImageUrl(responseBody.string());
} catch (IOException e) {
            e.printStackTrace();
}

        // 判斷並傳值
if (weather.getError_code() == 0)  //查詢成功 可以儲存
{
            AppUtils.today_weather = weather;
}
        return true;
}
}).compose(this.<Boolean>bindToLifecycle())
        .subscribeOn(Schedulers.io())
        .observeOn(AndroidSchedulers.mainThread())
        .subscribe(new Observer<Boolean>() {
            @Override
public void onCompleted() {

            }

            @Override
public void onError(Throwable e) {
                goHome();
}

            @Override
public void onNext(Boolean aBoolean) {
                goHome();
}
        });

好的,資料獲取完就開始展示。

效果如圖:


有一點要說的,就是天氣圖片。聚合資料提供了兩套圖片,都挺好看的,我取了其中一套放到了去年申請的虛擬主機上(免費兩年,快到期了,之前都沒怎麼用過)。

地址在原始碼裡,歡迎給Star。

好的,到這裡要說資料庫的事情了。

greenDAO 是一個將物件對映到 SQLite 資料庫中的輕量且快速的 ORM 解決方案。

我也是在網上看別人的教程。在這就推薦一個:

黃帥(音譯)的文章

但我覺得這都不是重點。因為greenDAO封裝好了Api,我們不需要寫sql語句。輕鬆簡單,所以我遇到的問題

是viewpager 顯示Fragment的時候。

首先 我在建立Fragment時 傳入了一個引數:

ViewPagerAdapter vpAdapter = new ViewPagerAdapter(getSupportFragmentManager());
vpAdapter.addFragment(new DailyFragment().newInstance("學習"), "學習");
vpAdapter.addFragment(new DailyFragment().newInstance("工作"), "工作");
vpAdapter.addFragment(new DailyFragment().newInstance("運動"), "運動");
vpAdapter.addFragment(new DailyFragment().newInstance("日常"), "日常");
main_vp_container.setAdapter(vpAdapter);

然後fragment 獲取到這個引數,通過這個引數去 查詢資料庫:

public DailyFragment newInstance(String type) {
    Bundle args = new Bundle();
args.putString(TYPE, type);
DailyFragment dailyFragment = new DailyFragment();
dailyFragment.setArguments(args);
    return dailyFragment;
}


這都是我想象的流程,實際上並沒有這樣正常進行。

為什麼呢,這就得從Fragment的生命週期和 viewpager快取說起了。


Viewpager不設定預設快取頁面數量的話,預設是兩個。

我們現在有  1 、 2、  3 、  4 ,4個介面。

通過跟蹤宣告周期函式可以知道。 先是(中間的就不追蹤了,大家知道中間還有生命週期函式就行) onCreate 1  、onResume 1、 onCreate  2、 onResume 2

這時候顯示的是 第一個介面。 滑動到第2個後 開始執行onCreate  3、 onResume 3

為什麼呢,因為2已經建立了,只是你沒看到   。有人問滑動到能看到的時候為啥沒onResume ,

這就相當於一個很大的圖片,你在手機上只看到一部分,滑動之後看到其餘部分差不多一個意思。

然後再向右滑動 就是onCreate  4、 onResume 4.

這時候滑動到第四個介面 發現什麼也沒執行,原因就像上面說的,在顯示3的時候已經載入完畢了。 

然後接下來無論怎麼滑動都是執行onResume,且載入的是相鄰的。

也就是說我們無法在獲取create 時傳入的引數。 那麼我們該向資料庫查詢什麼呢?顯然結果會是錯誤的。

在一開始我的解決方法是建立4個一模一樣的fragment,當然名字不一樣。分別執行查詢"學習","運動",什麼的。

這樣當然沒問題,但咱這也太low了,一模一樣的fragment還寫4個。。。

所以我就想在tab改變的時候可以獲取到此時的TabTitle。然後傳給 fragment,讓它執行查詢方法,再次獲取資料。

沒錯,這樣也能解決。但有一個小bug。當時是使用RxBus,發車之後在fragment中監聽獲取事件,但由於之前說,此時有兩個fragment存在,它們都在監聽。

所以如果不加以限制,他們會觸發重新查詢的方法,所以你會在滑動的時候發現相鄰的介面資料和你當前的一樣。

最簡單的辦法就是設定快取頁數:得意

這就是這種辦法的最簡單解決辦法。

main_vp_container.setOffscreenPageLimit(4);  //設定4頁快取

好尷尬0 0,沒關係,加深了對fragment生命週期和tablayout+viewpager的用法。

當然你也可以對fragmentadapter進行改造使其對資料進行快取。

來看資訊展示介面:


然後是新增介面:


使用是DialogFragment :

鴻洋大神的教程,很詳細:


不過我也貼一下簡單的程式碼:

佈局就不貼了。

直接上java程式碼,很簡單。

public class AddDialogFragment extends DialogFragment {

    private EditText et_title;
    private EditText et_info;
    private MaterialSpinner bp;
//建立介面在Acitvity中呼叫
public interface AddDutyInputListener {
        void onAddDutyInputComplete(String title, String type, String info);
}

    @Override
public Dialog onCreateDialog(Bundle savedInstanceState) {

        String[] ITEMS = {"學習", "工作", "運動", "日常"};
ArrayAdapter<String> adapter = new ArrayAdapter<String>(getActivity(), android.R.layout.simple_spinner_item, ITEMS);
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
LayoutInflater inflater = getActivity().getLayoutInflater();
View view = inflater.inflate(R.layout.dialog_add, null);
et_title = (EditText) view.findViewById(R.id.et_title);
et_info = (EditText) view.findViewById(R.id.et_info);
bp = (MaterialSpinner) view.findViewById(R.id.spinner);
bp.setAdapter(adapter);
builder.setView(view)
                .setPositiveButton("確定",
                        new DialogInterface.OnClickListener() {
                            @Override
public void onClick(DialogInterface dialog, int id) {
                                AddDutyInputListener listener = (AddDutyInputListener) getActivity();
listener.onAddDutyInputComplete(et_title.getText().toString(), bp.getSelectedItem().toString(), et_info.getText().toString());
}

                        }).setNegativeButton("取消", null);
        return builder.create();
}


}

然後在activity中呼叫顯示:

AddDialogFragment adddialog = new AddDialogFragment();
adddialog.show(getFragmentManager(), "addDialog");

這樣就會顯示出來。

點選確定後通過實現的介面進行資料的返回。

@Override
public void onAddDutyInputComplete(String title, String type, String info) {

    if (title.trim().isEmpty()) {
        Toast.makeText(MainActivity.this, "標題不能為空!", Toast.LENGTH_SHORT).show();
} else {

        Duty newduty = new Duty(null, title, info, type, false, new Date());
DbServices.getInstance(this).saveNote(newduty);
        if (_rxBus.hasObservers()) {    //是否有觀察者,有,則傳送一個事件
_rxBus.send(new Event.AddEvent(newduty,type));
}
    }
}

在這裡我使用了rxBus 來通知fragment 添加了一個數據, 讓他們看看是不是屬於自己那一組的,

屬於的話就自己往adapter裡增添一條資料。大笑 

程式碼如下:(注意生命週期)

_rxBus.toObserverable()
        .compose(this.bindToLifecycle())
        .subscribe(new Action1<Object>() {
            @Override
public void call(Object event) {
                if (event instanceof Event.AddEvent) {
                    //如果 傳來的 新增事件 和當前 查詢結果型別一致 則直接往裡面填充
if (((Event.AddEvent) event).getMduty().getType() == mytype) {
                        qadapter.add(0, ((Event.AddEvent) event).getMduty());
}
                }
            }
        });

完美實現:


到這裡就算結束了,在無人指引的情況下,多看書,打基礎,然後在程式碼中獲得收穫。

更新:增加了設定桌布功能

首先需要給許可權:

<uses-permission android:name = "android.permission.SET_WALLPAPER"/>
<uses-permission android:name="android.permission.SET_WALLPAPER_HINTS"/>

然後就是使用picasso獲取bitmap 然後設定就好。

程式碼如下:(picasso建立bitmap屬於io操作)

void setBackground() {
    final WallpaperManager instance = WallpaperManager.getInstance(this);
    int desiredMinimumWidth = this.getWindowManager().getDefaultDisplay().getWidth();
    int desiredMinimumHeight = this.getWindowManager().getDefaultDisplay().getHeight();
instance.suggestDesiredDimensions(desiredMinimumWidth, desiredMinimumHeight);
Observable<Void> setBack = Observable.create(new Observable.OnSubscribe<Void>() {
        @Override
public void call(Subscriber<? super Void> subscriber) {
            try {
                Bitmap bmp = Picasso.with(MainActivity.this).load(AppUtils.back_url).get();
instance.setBitmap(bmp);
} catch (Exception e) {
                e.printStackTrace();
}
            subscriber.onNext(null);
subscriber.onCompleted();
}
    }).compose(this.<Void>bindToLifecycle())
            .subscribeOn(Schedulers.io())
            .observeOn(AndroidSchedulers.mainThread());
setBack.subscribe(new Action1<Void>() {
        @Override
public void call(Void aVoid) {
            Toast.makeText(MainActivity.this, "設定成功", Toast.LENGTH_SHORT).show();
}
    });
}

效果: