Android MVP架構(Volley+CursorLoader+ContentProvider)
若是,不熟悉MVP架構的,可以先閱讀,Android MVP架構。
本篇,介紹Android MVP架構(Volley+CursorLoader+ContentProvider)來實現需求。
專案結構,分析圖如下:
除開MVP架構外,還具備以下幾種主要知識點:
- 資料庫:ContentProvider+CursorLoader+SQLite實現資料實時重新整理
- 網路通訊:Volley的幾種請求
- 網路圖片:Volley中的ImageLoader
- 資料解析:Gson庫
- MaterialDesign設計庫
採用以上anroid程式設計師必備技術,上手容易,不需要花費太多精力,去學習其他的第三方類庫。
當然,也可以採用RxJava+SQLBrite+Glide+OkHttp+Retrofit
等第三方熱門框架來實現Android MVP架構。具體如何實現,將由下篇部落格介紹。
專案的效果圖和需求:
一個電影列表介面:
一個切換介面的抽屜選單:
一個收藏列表的介面:
根據上面的頁面,歸納出以下功能點:
- 電影列表
- 選擇多部電影進行收藏。
- 檢視被收藏的電影列表。
按模組劃分,可以分為電影列表模組,電影收藏模組。
接著,按上面分析,進行編寫程式碼:
前期準備,專案的gradle中類庫引用如下:
dependencies {
compile fileTree(include: ['*.jar'], dir: 'libs')
testCompile 'junit:junit:4.12'
compile 'com.android.support:appcompat-v7:25.3.1'
compile 'com.android.support:design:25.3.1'
compile 'com.android.volley:volley:1.0.0'
compile 'com.google.code.gson:gson:2.8.0'
compile 'com.android.support:recyclerview-v7:25.3.1'
}
1. 專案通用的BasePrester和BaseView介面:
BasePresenter介面用於一個開始載入資源的方法和解除對View物件引用的方法:
public interface BasePresenter {
/**
* 開啟任務
*/
void start();
/**
* 解除對View的引用
*/
void unbindView();
}
BaseView介面,擁有一個設定Presenter物件的方法:
public interface BaseView<T> {
/**
* 設定Presenter
*/
void setPresenter(T t);
}
2. Modle模組編寫
Model模組分為本地資料和網路遠端資料。
1. 本地資料來源:
根據上面展示的收藏電影列表介面,來建立以下資料庫中表及其欄位。
將表中欄位和表的Uri存放在一個BaseColumns實現類中:
public class MovieConstract implements BaseColumns {
/**
* 資料庫的資訊
*/
public static final String SQLITE_NAME="movie.db";
public static final int SQLITE_VERSON=1;
/**
* 表和欄位資訊
*/
public static final String TABLE_NAME_MOVI="movieData";
public static final String COLUMN_ID ="id";
public static final String COLUMN_YEAR="year";
public static final String COLUMN_TITLE="title";
public static final String COLUMN_IMAGES="image";
/**
* 內容提供者的authority
*/
public static final String AUTHORITY="com.xingen.mvppractice.data.source.local.MovieDataProvider";
public static final String SCHEME="content";
private static final Uri CONTENT_URI=Uri.parse(SCHEME+"://"+AUTHORITY);
public static final Uri MOVIEDATA_URI=Uri.withAppendedPath(CONTENT_URI,TABLE_NAME_MOVI);
}
資料庫建立如下:
public class MovieDataHelper extends SQLiteOpenHelper {
public static final String CREATE_TABLE_MOVIE = "create table " +
MovieConstract.TABLE_NAME_MOVI + "(" +
MovieConstract._ID + " integer primary key autoincrement," +
MovieConstract.COLUMN_ID + " text," +
MovieConstract.COLUMN_TITLE + " text," +
MovieConstract.COLUMN_YEAR + " text," +
MovieConstract.COLUMN_IMAGES + " text"
+ ")";
public MovieDataHelper(Context context) {
super(context, MovieConstract.SQLITE_NAME, null, MovieConstract.SQLITE_VERSON);
}
@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL(CREATE_TABLE_MOVIE);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
}
}
如何自定義的ContentProvider,請閱讀ContetProvider+SQLite+CursorLoader實現資料庫觀察者模式。也可以閱讀本專案中程式碼。
接下來,編寫增,刪,查,改的操作。建立資料庫的表中數對應的操作類的超級介面:
public interface LocalDataSource<T> {
/**
* 獲取全部
* @return
*/
List<T> queryAll();
/**
* 指定條件下的查詢
* @param select
* @param selectArg
* @return
*/
List<T> queryAction(String select,String[] selectArg);
/**
* 新增
* @param t
* @return
*/
long insert(T t);
/**
* 批量插入
* @param list
* @return
*/
int bulkInsert( List<T> list);
/**
* 更新
* @param t
* @param select
* @param selectArg
* @return
*/
int update(T t,String select,String[] selectArg);
/**
* 指定條件的刪除
* @param t
* @param select
* @param selectArg
* @return
*/
int delite(T t,String select,String[] selectArg);
/**
* 刪除全部
*/
void deliteAll();
}
最後,編寫介面的實現類,即每個表的各種對應的操作類,採用ContentResolver物件來完成:
public class MovieLocalSource implements LocalDataSource<MovieData> {
private ContentResolver contentResolver;
public MovieLocalSource(ContentResolver contentResolver){
this.contentResolver=contentResolver;
}
@Override
public List<MovieData> queryAll() {
//查詢工作,由CursorLoader已經完成
return null;
}
@Override
public List<MovieData> queryAction(String select, String[] selectArg) {
//查詢工作,由CursorLoader已經完成
return null;
}
@Override
public long insert(MovieData movieData) {
ContentValues contentValues= TransformUtils.transformMovieData(movieData);
Uri uri=this.contentResolver.insert(MovieConstract.MOVIEDATA_URI,contentValues);
if(uri!=null){
String s= uri.toString();
long rowId=Long.valueOf(s.substring(s.lastIndexOf("/",s.length())));
return rowId;
}
return -1;
}
@Override
public int bulkInsert(List<MovieData> list) {
ContentValues[] contentValuesArray=new ContentValues[list.size()];
for (int i=0;i<list.size();++i){
contentValuesArray[i]= TransformUtils.transformMovieData(list.get(i));
}
return this.contentResolver.bulkInsert(MovieConstract.MOVIEDATA_URI,contentValuesArray);
}
@Override
public int update(MovieData movieData, String select, String[] selectArg) {
return 0;
}
@Override
public int delite(MovieData movieData, String select, String[] selectArg) {
return 0;
}
@Override
public void deliteAll() {
}
}
更多以上SQLite,自定義ContentProvider如何配置,ContetResolver使用。
請閱讀前面教程之[ContetProvider+SQLite+CursorLoader實現資料庫觀察者模式] (http://blog.csdn.net/hexingen/article/details/71597884)。
本地資料來源包結構圖如下:
2. 網路資料來源:
後臺伺服器返回的資料結構有多種(String,xml,json),這裡統一返回String型別的資料,然後各種對應型別再解析。考慮到現在主流的Post傳遞的資料型別為json。因此,重寫StringRequest,使其支援Json資料型別的Body:
public class StringBodyRequest extends StringRequest {
/** Charset for request. */
private static final String PROTOCOL_CHARSET = "utf-8";
/** Content type for request. */
private static final String PROTOCOL_CONTENT_TYPE =
String.format("application/json; charset=%s", PROTOCOL_CHARSET);
/**
* 自定義header:
*/
private Map<String, String> headers;
/**
* post傳遞的引數
*/
private final String mRequestBody;
/**
* 請求結果的監聽器
*/
private RequestResultListener resultListener;
/**
* 請求的id
*/
private int requestId;
public StringBodyRequest( String url, int requestId,StringBodyRequest.RequestResultListener resultListener) {
this(Method.GET,url,null,requestId,resultListener);
}
public StringBodyRequest(int method, String url, JSONObject jsonObject,int requestId,StringBodyRequest.RequestResultListener resultListener) {
super(method, url,null,null);
this.headers = new HashMap<>();
this.mRequestBody=(jsonObject==null?null:jsonObject.toString());
this.resultListener=resultListener;
this.requestId=requestId;
}
/**
* 重寫getHeaders(),新增自定義的header
*
* @return
* @throws AuthFailureError
*/
@Override
public Map<String, String> getHeaders() throws AuthFailureError {
return headers;
}
/**
* 設定請求的header
* "Charset", "UTF-8"://編碼格式:utf-8
* "Cookie", coockie:////設定coockie
* @param
* @return
*/
public Map<String, String> setHeader(String key, String content) {
if(!TextUtils.isEmpty(key)&&!TextUtils.isEmpty(content)){
headers.put(key, content);
}
return headers;
}
/**
* 重寫Content-Type:設定為json
*/
@Override
public String getBodyContentType() {
return PROTOCOL_CONTENT_TYPE;
}
/**
* post引數型別
*/
@Override
public String getPostBodyContentType() {
return getBodyContentType();
}
/**
* post引數
*/
@Override
public byte[] getPostBody() throws AuthFailureError {
return getBody();
}
/**
* 將string編碼成byte
* @return
* @throws AuthFailureError
*/
@Override
public byte[] getBody() throws AuthFailureError {
try {
return mRequestBody == null ? null : mRequestBody.getBytes(PROTOCOL_CHARSET);
} catch (Exception e) {
return null;
}
}
/**
* 重寫傳遞異常的回撥
* @param error
*/
@Override
public void deliverError(VolleyError error) {
this.resultListener.failure(requestId,error);
}
/**
* 重寫傳遞結果的回撥
* @param response
*/
@Override
protected void deliverResponse(String response) {
this.resultListener.success(requestId,response);
}
/**
* 自定義請求結果和異常的回撥介面
*/
public interface RequestResultListener{
void success(int requestId,String response);
void failure(int reqestId,VolleyError error);
}
}
ImageLoader中Lrucache配置和Volley操作類的配置省略不貼出來。在專案中有詳細介紹,請自行閱讀。如何自定義請求,閱讀 Volley原始碼分析之自定義GsonRequest教程。
考慮到請求有幾種情況,有請求方式,header,body差異。因此,遠端操作類介面封裝以下幾種方法:
public interface RemoteDataSource {
void excuteRequest(String url, int requestId, String tag, StringBodyRequest.RequestResultListener resultListener);
void excuteRequest(String url, Map<String, String> headrMap, int requestId, String tag, StringBodyRequest.RequestResultListener resultListener);
void excuteRequest(int method, String url, JSONObject jsonObject, Map<String, String> headrMap, int requestId, String tag, StringBodyRequest.RequestResultListener resultListener);
void excuteRequest(int method, String url, JSONObject jsonObject, int requestId, String tag, StringBodyRequest.RequestResultListener resultListener);
}
遠端網路的操作類的實現具體如下:
public class RemoteDataSourceImp implements RemoteDataSource {
/**
* 靜態方式構建
* @return
*/
public static RemoteDataSource newInstance(){
return new RemoteDataSourceImp();
}
@Override
public void excuteRequest(String url, int requestId, String tag, StringBodyRequest.RequestResultListener resultListener) {
excuteRequest(url,null,requestId,tag,resultListener);
}
@Override
public void excuteRequest(String url, Map<String, String> headrMap, int requestId, String tag, StringBodyRequest.RequestResultListener resultListener) {
excuteRequest(Request.Method.GET,url,null,headrMap,requestId,tag,resultListener);
}
@Override
public void excuteRequest(int method, String url, JSONObject jsonObject, Map<String, String> headrMap, int requestId, String tag, StringBodyRequest.RequestResultListener resultListener) {
excuteRequest(createRequest(method,url,jsonObject,headrMap,requestId,tag,resultListener));
}
@Override
public void excuteRequest(int method, String url, JSONObject jsonObject, int requestId, String tag, StringBodyRequest.RequestResultListener resultListener) {
excuteRequest(Request.Method.GET,url,jsonObject,null,requestId,tag,resultListener);
}
/**
* 建立不同body,Header的請求
* @param method
* @param url
* @param jsonObject
* @param headrMap
* @param requestId
* @param tag
* @param resultListener
* @return
*/
private StringBodyRequest createRequest(int method, String url,JSONObject jsonObject, Map<String,String> headrMap, int requestId,String tag,StringBodyRequest.RequestResultListener resultListener){
StringBodyRequest stringBodyRequest=new StringBodyRequest(method,url,jsonObject,requestId,resultListener);
stringBodyRequest.setTag(tag);
if(headrMap!=null){
Set<Map.Entry<String,String>> headerSet = headrMap.entrySet();
for (Map.Entry<String,String> entry:headerSet){
stringBodyRequest.setHeader(entry.getKey(),entry.getValue());
}
}
return stringBodyRequest;
}
/**
* 執行 Request
* @param stringBodyRequest
*/
private void excuteRequest(StringBodyRequest stringBodyRequest){
VolleySingle.getInstance().addRequest(stringBodyRequest);
}
}
遠端資料來源的包結構圖如下:
3. 實際業務模組:
這裡,列舉:電影列表介面的模組
-
View告訴Presenter要載入資料,Presenter要獲取遠端資料來源,然後回撥的響應資料更新到UI上.
-
View告訴Presenter要收藏的電影,Presenter將收藏資料傳遞給本地資料來源,進行儲存,最後Presenter將儲存結果更新到UI上。
根據上面的View,Presenter間的互動關係,抽出其行為:
public interface MovieListConstract {
interface Presenter extends BasePresenter{
/**
* 收藏的資料
*/
void collectionMovie(List<Movie> list);
}
interface View extends BaseView<MovieListConstract.Presenter>{
/**
* 載入從資料來源中獲取的資料
*/
void loadMovieList(List<Movie> list);
/**
* 顯示最新資訊
*/
void showToast(String s);
}
}
接著編寫View介面的具體實現類Fragment:
public class MovieListFragment extends Fragment implements MovieListConstract.View, View.OnClickListener, SwipeRefreshLayout.OnRefreshListener {
private View rootView;
private RecyclerView recyclerView;
private MovieListAdapter adapter;
private ScrollChildSwipeRefreshLayout swipeRefreshLayout;
public static final String TAG = MovieListFragment.class.getSimpleName();
public static MovieListFragment newInstance() {
return new MovieListFragment();
}
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
this.rootView = inflater.inflate(R.layout.fragment_movielist, container, false);
return this.rootView;
}
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
initView();
//開始載入遠端任務
this.presenter.start();
}
/**
* 初始化控制元件
*/
private void initView() {
this.recyclerView = (RecyclerView) this.rootView.findViewById(R.id.movielist_recyclerView);
this.adapter = new MovieListAdapter();
this.recyclerView.setLayoutManager(new LinearLayoutManager(getActivity()));
this.recyclerView.setAdapter(this.adapter);
this.recyclerView.addItemDecoration(new BaseItemDecoration(getActivity()));
this.rootView.findViewById(R.id.movielist_collection_btn).setOnClickListener(this);
swipeRefreshLayout = (ScrollChildSwipeRefreshLayout) rootView.findViewById(R.id.movielist_refreshLayout);
swipeRefreshLayout.setColorSchemeColors(Color.parseColor("#263238"), Color.parseColor("#ffffff"), Color.parseColor("#455A64"));
swipeRefreshLayout.setScrollUpChild(recyclerView);
swipeRefreshLayout.setOnRefreshListener(this);
//自動載入下拉提示框
setLoadingIndicator(true);
//以上程式碼不響應onRefresh(),需要手動響應onReFresh()。
this.onRefresh();
}
/**
* 控制SwipeRefreshLayout的顯示與隱藏
*
* @param active
*/
public void setLoadingIndicator(final boolean active) {
if (swipeRefreshLayout == null) {
return;
}
/**
* 通過swipeRefreshLayout.post來呼叫swipeRefreshLayout.setRefreshing()來實現,一進入頁面就自動下拉提示窗。
*/
swipeRefreshLayout.post(new Runnable() {
@Override
public void run() {
//確保佈局載入完成後,呼叫
swipeRefreshLayout.setRefreshing(active);
}
});
}
@Override
public void onDestroyView() {
//解除對View的引用
this.presenter.unbindView();
super.onDestroyView();
}
private MovieListConstract.Presenter presenter;
@Override
public void setPresenter(MovieListConstract.Presenter presenter) {
this.presenter = presenter;
}
@Override
public void showToast(String s) {
Toast.makeText(BaseApplication.getAppContext(), s, Toast.LENGTH_SHORT).show();
}
@Override
public void loadMovieList(List<Movie> list) {
this.adapter.upData(list);
this.setLoadingIndicator(false);
}
@Override
public void onClick(View v) {
if (this.adapter.getMoviesCollecion().size() == 0) {
showToast("請勾選中電影");
} else {
this.presenter.collectionMovie(this.adapter.getMoviesCollecion());
}
}
@Override
public void onRefresh() {
swipeRefreshLayout.postDelayed(new Runnable() {
@Override
public void run() {
setLoadingIndicator(false);
}
}, 1000 * 2);
}
}
在接下來編寫Presenter的實現類:
public class MovieListPresenter implements MovieListConstract.Presenter, StringBodyRequest.RequestResultListener {
private MovieListConstract.View view;
private LocalDataSource<MovieData> localDataSource;
private RemoteDataSource remoteDataSource;
public MovieListPresenter(RemoteDataSource remoteDataSource, LocalDataSource<MovieData> localDataSource, MovieListConstract.View view) {
this.remoteDataSource = remoteDataSource;
this.localDataSource = localDataSource;
this.view = view;
this.view.setPresenter(this);
}
@Override
public void start() {
loadRemoteTask();
}
/**
* 豆瓣中電影的Api:
*/
private final String URL = "https://api.douban.com/v2/movie/search?q=張藝謀";
private final int REQUEST_MOVIELIST = 1;
private final String TAG = MovieListPresenter.class.getSimpleName();
/**
* 開始載入遠端的資料
*/
private void loadRemoteTask() {
remoteDataSource.excuteRequest(URL, REQUEST_MOVIELIST, TAG, this);
}
/**
*Presenter將收藏的資料傳遞給本地資料來源,進行儲存。
*/
@Override
public void collectionMovie(List<Movie> list) {
List<MovieData> movieDataList = new ArrayList<>();
for (Movie movie : list) {
movieDataList.add(TransformUtils.transformMovies(movie));
}
//本地資料來源將收藏的電影儲存,將結果反饋給Presenter。
int size= this.localDataSource.bulkInsert(movieDataList);
if(size>0){//批量插入成功
if(isViewBind()){//Presenter傳遞資料到View上,進行UI更新
this.view.showToast("收藏成功,可在收藏頁面檢視");
}
}
}
@Override
public void unbindView() {
this.view = null;
}
@Override
public void success(int requestId, String response) {
Log.i(TAG," 響應的資料 "+response);
switch (requestId) {
case REQUEST_MOVIELIST://響應成功,解析資料
List<Movie> list = GsonUtils.paserJson(response, MovieList.class).getSubjects();
// Presenter將資料傳遞到View上, 進行UI更新
this.view.loadMovieList(list);
this.view.showToast("獲取列表成功");
break;
default:
break;
}
}
@Override
public void failure(int requestId, VolleyError error) {
switch (requestId) {
case REQUEST_MOVIELIST:
if (isViewBind()) {
this.view.showToast("載入失敗");
}
break;
default:
break;
}
}
/**
* 檢查View是否被繫結
*
* @return
*/
private boolean isViewBind() {
return this.view == null ? false : true;
}
}
最後,在Activity中建立View 和 Presenter物件:
public class MovieListActivity extends AppCompatActivity implements NavigationView.OnNavigationItemSelectedListener , View.OnClickListener{
private MovieListConstract.Presenter presenter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_movielist);
initView();
MovieListFragment fragment=null;
if(savedInstanceState!=null){
fragment=(MovieListFragment) getSupportFragmentManager().findFragmentByTag(MovieListFragment.TAG);
}else{
fragment=MovieListFragment.newInstance();
getSupportFragmentManager().beginTransaction().add(R.id.movielist_content_layout,fragment,MovieListFragment.TAG).commit();
}
this.presenter=new MovieListPresenter(RemoteDataSourceImp.newInstance(),new MovieLocalSource(ContentResolverUtils.createResolver(BaseApplication.getAppContext())),fragment);
}
/**
* 初始化控制元件
*/
private void initView() {
NavigationView navigationView=(NavigationView) this.findViewById(R.id.movielist_navigationview);
FloatingActionButton floationActionButton=(FloatingActionButton) this.findViewById(R.id.movielist_floationActionBtn);
floationActionButton.setOnClickListener(this);
navigationView.setNavigationItemSelectedListener(this);
}
@Override
public boolean onNavigationItemSelected(@NonNull MenuItem item) {
switch (item.getItemId()){
case R.id.activity_movielist_drawer_collect://轉調收藏電影的介面.
Intent intent=new Intent(this, CollectionMovieActivity.class);
startActivity(intent);
break;
case R.id.activity_movielist_drawer_movielist:
break;
}
//關閉抽屜選單
DrawerLayout drawerLayout=(DrawerLayout) this.findViewById(R.id.movielist_drawer);
drawerLayout.closeDrawer(GravityCompat.START);
return true;
}
@Override
public void onClick(View v) {
Snackbar.make(v,"MVP案例",Snackbar.LENGTH_SHORT).setAction("Action",null).show();
}
@Override
public void onBackPressed() {
DrawerLayout drawerLayout=(DrawerLayout) this.findViewById(R.id.movielist_drawer);
if(drawerLayout.isDrawerOpen(GravityCompat.START)){//按Back鍵,關閉抽屜選單。
drawerLayout.closeDrawer(GravityCompat.START);
}else{
super.onBackPressed();
}
}
}
電影收藏的業務也是類似,只要要抽出View與Presenter的互動行為,剩下的便是呼叫資料來源。
最好,可以結合Android MVP架構來加深理解。
4. 其他配置:
一個具備新增資料的Adapter抽象類:
public abstract class BaseRecyclerViewAdapter<T ,VH extends RecyclerView.ViewHolder> extends RecyclerView.Adapter<VH> {
public abstract void upData(T t);
}
一個支援非直接子類滾動檢視的SwipeRefreshLayout:
public class ScrollChildSwipeRefreshLayout extends SwipeRefreshLayout{
private View scrollUpChild;
public ScrollChildSwipeRefreshLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
/**
* 設定在哪個view中觸發重新整理。
* @param view
*/
public void setScrollUpChild(View view){
this.scrollUpChild=view;
}
/**
*ViewCompat..canScrollVertically():用於檢查view是否可以在某個方向上垂直滑動
* @return
*/
@Override
public boolean canChildScrollUp() {
if(scrollUpChild!=null){
return ViewCompat.canScrollVertically(scrollUpChild,-1);
}
return super.canChildScrollUp();
}
}
一些其他的工具類:
public class TransformUtils {
/**
* 將Cursor 生成MovieData物件
* @param cursor
* @return
*/
public static MovieData transformMovieData(Cursor cursor) {
MovieData movieData = new MovieData();
movieData.setId(cursor.getString(cursor.getColumnIndex(MovieConstract.COLUMN_ID)));
movieData.setTitle(cursor.getString(cursor.getColumnIndex(MovieConstract.COLUMN_TITLE)));
movieData.setYear(cursor.getString(cursor.getColumnIndex(MovieConstract.COLUMN_YEAR)));
movieData.setImages(cursor.getString(cursor.getColumnIndex(MovieConstract.COLUMN_IMAGES)));
return movieData;
}
public static MovieData transformMovies(Movie movie){
MovieData movieData=new MovieData();
movieData.setId(movie.getId());
movieData.setYear(movie.getYear());
movieData.setTitle(movie.getTitle());
movieData.setImages(movie.getImages().getLarge());
return movieData;
}
/**
* 將Movie生成Cursor.
* @param movie
* @return
*/
public static ContentValues transformMovieData(MovieData movie){
ContentValues contentValues=new ContentValues();
contentValues.put(MovieConstract.COLUMN_ID,movie.getId());
contentValues.put(MovieConstract.COLUMN_TITLE,movie.getTitle());
contentValues.put(MovieConstract.COLUMN_YEAR,movie.getYear());
contentValues.put(MovieConstract.COLUMN_IMAGES,movie.getImages());
return contentValues;
}
/**
* 將Movie生成Cursor.
* @param movie
* @return
*/
public static ContentValues transformMovie(Movie movie){
ContentValues contentValues=new ContentValues();
contentValues.put(MovieConstract.COLUMN_ID,movie.getId());
contentValues.put(MovieConstract.COLUMN_TITLE,movie.getTitle());
contentValues.put(MovieConstract.COLUMN_YEAR,movie.getYear());
contentValues.put(MovieConstract.COLUMN_IMAGES,movie.getImages().getLarge());
return contentValues;
}
}
工具包,UI包結構圖如下:
5. 專案執行效果如下:
本專案程式碼:https://github.com/13767004362/MVPDemo
todo-mvp-contentproviders官方案例
1. 專案結構圖:
2. 專案連結:https://github.com/googlesamples/android-architecture/tree/todo-mvp-contentproviders/
s/
相關文章
- Android MVP 架構AndroidMVP架構
- Android-MVP架構AndroidMVP架構
- MVP:有呼吸的Android架構MVPAndroid架構
- Android架構系列-MVP架構的實際應用Android架構MVP
- Android 架構選型 (MVP+DataBinding)Android架構MVP
- Android應用架構之MVP實現Android應用架構MVP
- Android架構系列-基於MVP建立適合自己的架構Android架構MVP
- Android MVP架構改造~如何重用頂層業務AndroidMVP架構
- 談談 Android MVP 架構 | 掘金技術徵文AndroidMVP架構
- Android開發中的MVP架構詳解AndroidMVP架構
- Android 架構設計:MVC、MVP、MVVM和元件化Android架構MVCMVPMVVM元件化
- Android MVP架構(RxJava+SQLBrite+Retrofit+OkHttp+Glide)AndroidMVP架構RxJavaSQLHTTPIDE
- MVP應用架構模式MVP應用架構模式
- MVP架構設計 初探MVP架構
- android-MVP架構中Presenter的單元測試AndroidMVP架構
- iOS開發-MVP架構模式iOSMVP架構模式
- iOS 架構模式–解密 MVC,MVP,MVVM以及VIPER架構iOS架構模式解密MVCMVPMVVM
- Android MVP架構從入門到精通-真槍實彈AndroidMVP架構
- Android從零開始(第三篇)MVP架構搭建AndroidMVP架構
- Kotlin如何實現MVP架構KotlinMVP架構
- MVC、MVP、MVVM,談談我對Android應用架構的理解MVCMVPMVVMAndroid應用架構
- 【Android架構】基於MVP模式的Retrofit2+RXjava封裝(一)Android架構MVP模式RxJava封裝
- MVP那些事兒 (2) 初探MVC架構MVPMVC架構
- 【Android】Dagger2實現更為規範化的MVP架構AndroidMVP架構
- iOS MVC、MVVM、MVP架構模式淺淺析iOSMVCMVVMMVP架構模式
- iOS架構淺談從 MVC、MVP 到 MVVMiOS架構MVCMVPMVVM
- 看完不會寫MVP架構我跪搓板MVP架構
- 架構設計的歷史·MVC·MVP·MVVM架構MVCMVPMVVM
- 開源專案Philm的MVP架構分析MVP架構
- MVP+Dagger2設計,MVP架構模式實現新思路 (Demo)MVP架構模式
- 【Android架構】基於MVP模式的Retrofit2+RXjava封裝之多Url(七)Android架構MVP模式RxJava封裝
- 設計Android應用程式架構的基本指南:MVP:第2部分Android架構MVP
- 帶你動手實現 MVP+Clean架構!MVP架構
- [譯]iOS架構模式——解密MVC、MVP、MVVM和VIPERiOS架構模式解密MVCMVPMVVM
- 轉享:表現層架構模式比較:MVP(SC),MVP(PV),PM,MVVM 和 MVC架構模式MVPMVVMMVC
- iOS架構設計:揭祕MVC, MVP, MVVM以及VIPERiOS架構MVCMVPMVVM
- MVP架構由淺入深篇一(基礎版)MVP架構
- 死磕安卓前序:MVP架構探究之旅—基礎篇安卓MVP架構