【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 staticWeatherApi 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
操作符行為類似於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(); } }); }
效果: