介紹
以登入為案列,MVP架構,okhttp網絡請求實作的登陸操作
1. 使用者輸入使用者名;
2. 點選登入;
3. 彈出progressbar,告知使用者目前正在驗證使用者名密碼;
4. 執行網絡請求;
5. 隐藏progressbar;網絡驗證完成;
6. 根據伺服器傳回結果執行跳轉主界面操作或者顯示錯誤資訊;
注意:可能有小夥伴覺得這樣寫,多了類,多寫了代碼;
但增強了代碼可讀性,可了解性,手動敲一敲便可了解MVP這樣設計用意;
效果圖
![](https://img.laitimes.com/img/__Qf2AjLwojIjJCLyojI0JCLiAzNfRHLGZkRGZkRfJ3bs92YsYTMfVmepNHL9UlaNlXUU1keFRlWzY0MMBjVtJWd0ckW65UbM5WOHJWa5kHT20ESjBjUIF2X0hXZ0xCMx81dvRWYoNHLrdEZwZ1Rh5WNXp1bwNjW1ZUba9VZwlHdssmch1mclRXY39CXldWYtlWPzNXZj9mcw1ycz9WL49zZuBnL0UzMzUTNwUTMzIjMxgTMwIzLc52YucWbp5GZzNmLn9Gbi1yZtl2Lc9CX6MHc0RHaiojIsJye.png)
知識點
MVP+RxJava+Okhttp
1. MVP 開發架構;
2. RxJava 線程控制;
3. Okhttp 網絡請求;
4. Gson 解析json字元串;
5. 安卓基本控件:
ConstraintLayout
Textview
EditText
Button
ProgressBar
代碼實作
建立項目
這個沒什麼說的了,new project。
引入三方庫
在Module的 build.gradle中加入下方代碼;
然後右上角會提示讓你同步的 “sync now”,點一下同步完成。
// 用于網絡請求
implementation("com.squareup.okhttp3:okhttp:3.12.0")
// 用于切換線程,RxJava RxAndroid 都需要引入
implementation 'io.reactivex.rxjava2:rxandroid:2.1.0'
implementation 'io.reactivex.rxjava2:rxjava:2.2.4'
//解析json字元串用
implementation 'com.google.code.gson:gson:2.8.5'
搭建MVP架構
1. 建立基本的BaseView、BasePresenter
2. 建立包login:所有登入相關的界面、網絡操作、資料儲存的類都放這裡面;
3. 建立view與presenter的連接配接接口類:ILoginContract;
4. 建立LoginActivity與LoginFragment,activity是fragment的容器,放一個FrameLayout就行;
5. 建立LoginFresenter,用于執行登入相關操作;
6. 建立LoginRepository,用于登入的網絡請求;
以下是關鍵類的代碼:
public interface BaseView<T> {
void setPresenter(T presenter);// 設定 presenter
}
public interface BasePresenter {
void subscribe();// 訂閱 用于綁定view
void unsubscribe();// 解除與view的綁定
}
/**
LoginFragment 與 LoginPresenter 之間的聯系紐帶
*/
public interface ILoginContract {
interface View extends BaseView<Presenter> {
void showProgressBar(Boolean isShow);//是否顯示 progressbar
void showMessage(String message);// 顯示一些展示消息
void goToMainActivity(User user);// 跳轉到主界面,展示使用者資訊
}
interface Presenter extends BasePresenter {
void doLogin(String name,String pwd);// 執行登入操作
}
}
/**
用于 LoginFragment 的容器,并将view與presenter 關聯起來
*/
public class LoginActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_login);
LoginFragment loginFragment = LoginFragment.getInstance();
getSupportFragmentManager().beginTransaction().add(R.id.fl_container,loginFragment,LoginFragment.class.toString()).commit();
// 将 presenter 與 view 關聯起來
new LoginPresenter(loginFragment);
}
}
/**
對應的xml布局 隻提供一個 FrameLayout 當容器用,放fragment
*/
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".login.LoginActivity">
<FrameLayout
android:id="@+id/fl_container"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</android.support.constraint.ConstraintLayout>
MVP中的 V -> LoginFragment
實作了ILoginContract 中的View接口類
重寫view,界面相關的方法,例如控件的顯示與隐藏,不關心網絡操作是如何實作
public class LoginFragment extends Fragment implements ILoginContract.View {
private ILoginContract.Presenter presenter;
private View view;
private ProgressBar pb;
private EditText etName;
private EditText etPwd;
private Button btnLogin;
public static LoginFragment getInstance(){
return new LoginFragment();
}
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
view = inflater.inflate(R.layout.fragment_login, container, false);
initView();
presenter.subscribe();
return view;
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
// 登入按鈕的點選事件
btnLogin.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//執行登入操作
presenter.doLogin(etName.getText().toString(),etPwd.getText().toString());
}
});
}
private void initView() {
pb = view.findViewById(R.id.pb_login);
etName = view.findViewById(R.id.et_name);
etPwd = view.findViewById(R.id.et_pwd);
btnLogin = view.findViewById(R.id.btn_login);
}
@Override
public void showProgressBar(Boolean isShow) {
if(isShow){
// 顯示 轉圈圈
pb.setVisibility(View.VISIBLE);
}else {
// 隐藏
pb.setVisibility(View.GONE);
}
}
@Override
public void showMessage(String message) {
Toast.makeText(getContext(),message,Toast.LENGTH_SHORT).show();
}
@Override
public void goToMainActivity(User user) {
Intent intent = new Intent(getContext(), MainActivity.class);
intent.putExtra("user",user);
getContext().startActivity(intent);
}
@Override
public void setPresenter(ILoginContract.Presenter presenter) {
if(presenter!=null) this.presenter = presenter;
}
@Override
public void onDestroy() {
super.onDestroy();
presenter.unsubscribe();//解除訂閱,
}
}
登入界面LoginFragment的xml
使用限制性布局,控制控制之間的相對位置;
根據百分比預留邊界空白部分。
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".LoginActivity">
<!--留出整個螢幕上部分13%-->
<android.support.constraint.Guideline
app:layout_constraintGuide_percent="0.13"
android:orientation="horizontal"
android:id="@+id/gl_top"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<!--留出整個螢幕左邊10%-->
<android.support.constraint.Guideline
app:layout_constraintGuide_percent="0.1"
android:orientation="vertical"
android:id="@+id/gl_left"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<!--留出整個螢幕由部分10%-->
<android.support.constraint.Guideline
app:layout_constraintGuide_percent="0.9"
android:orientation="vertical"
android:id="@+id/gl_right"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<TextView
android:textSize="18sp"
android:text="使用者名"
android:id="@+id/tv_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintLeft_toRightOf="@+id/gl_left"
app:layout_constraintTop_toBottomOf="@+id/gl_top"
/>
<TextView
android:textSize="18sp"
android:text="密 碼"
android:id="@+id/tv_pwd"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintLeft_toRightOf="@+id/gl_left"
app:layout_constraintTop_toBottomOf="@+id/tv_name"
app:layout_constraintRight_toRightOf="@+id/tv_name"
android:layout_marginTop="10dp"
/>
<EditText
android:id="@+id/et_name"
android:hint="請輸入使用者名"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginLeft="4dp"
app:layout_constraintLeft_toRightOf="@+id/tv_name"
app:layout_constraintTop_toTopOf="@+id/tv_name"
app:layout_constraintBottom_toBottomOf="@+id/tv_name"
app:layout_constraintRight_toRightOf="@+id/gl_right"
/>
<EditText
android:id="@+id/et_pwd"
android:hint="請輸入密碼"
android:inputType="textPassword"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginLeft="4dp"
app:layout_constraintLeft_toLeftOf="@+id/et_name"
app:layout_constraintTop_toTopOf="@+id/tv_pwd"
app:layout_constraintBottom_toBottomOf="@+id/tv_pwd"
app:layout_constraintRight_toRightOf="@+id/gl_right"
/>
<Button
android:id="@+id/btn_login"
android:text="登入"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@+id/et_pwd"
android:layout_marginTop="20dp"
app:layout_constraintLeft_toLeftOf="@id/gl_left"
app:layout_constraintRight_toRightOf="@id/gl_right"
/>
<!--用于展示目前正在執行網絡請求操作-->
<ProgressBar
android:visibility="gone"
android:id="@+id/pb_login"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
/>
</android.support.constraint.ConstraintLayout>
MVP中的 P->LoginPresenter
ILoginContract 中的Presenter接口類
重寫doLogin方法,不關心界面view是如何展示,
不關心網絡操作具體實作細節,
隻負責資料解析與界面顯示的調用;
public class LoginPresenter implements ILoginContract.Presenter {
private ILoginContract.View view;
private CompositeDisposable compositeDisposable;// 用于管理網絡請求的線程
private LoginRepository repository;
private String url = "http://soyoyo.esy.es/testmvp.php";// 測試登入用的url
private Gson gson;
public LoginPresenter(ILoginContract.View view) {
this.view = view;
this.view.setPresenter(this);
}
@Override
public void doLogin(String name,String pwd) {
//顯示 progressbar
view.showProgressBar(true);
Disposable disposable = repository.getUserInfo(url, name, pwd)
.subscribe(next -> {
view.showProgressBar(false);
if (next.contains("錯誤")) {
// 登入失敗
view.showMessage(next);
} else {
//登入成功
view.showMessage("登入成功!");
// 解析使用者資訊
User user = gson.fromJson(next, User.class);
view.goToMainActivity(user);
}
}, error -> {
error.printStackTrace();
view.showMessage("登入失敗");
view.showProgressBar(false);
});
compositeDisposable.add(disposable);
}
@Override
public void subscribe() {
// 初始化變量
compositeDisposable = new CompositeDisposable();
repository = new LoginRepository();
gson = new Gson();
}
@Override
public void unsubscribe() {
repository = null;//手動置空
if(compositeDisposable!=null){
// 關閉所有網絡請求,避免記憶體洩漏
compositeDisposable.clear();
}
}
}
MVP中的 M->LoginRepository
負責網絡操作,資料操作相關等
public class LoginRepository {
private OkHttpClient okHttpClient;
public LoginRepository(){
if(okHttpClient ==null){
okHttpClient = new OkHttpClient()
.newBuilder()
.callTimeout(10,TimeUnit.SECONDS)// 設定連接配接逾時時間
.build();
}
}
public Observable<String> getUserInfo(String url,String name,String pwd){
return Observable.create(new ObservableOnSubscribe<String>() {
@Override
public void subscribe(ObservableEmitter<String> emitter) throws Exception {
// 表單方式送出參數
RequestBody requestBody = new FormBody.Builder()
.add("username",name)// username 與 伺服器對應,伺服器也是通過這個 拿到 name的值
.add("userpassword",pwd)
.build();
Request request = new Request.Builder()
.url(url)
.post(requestBody)
.build();
// 開始請求伺服器
okHttpClient.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
e.printStackTrace();
//請求失敗
emitter.onError(e);
}
@Override
public void onResponse(Call call, Response response) throws IOException {
//請求成功
emitter.onNext(response.body().string());
}
});
}
})
.observeOn(AndroidSchedulers.mainThread())// 指定 最後拿到資料操,解析,顯示發生在主線程
.subscribeOn(Schedulers.io());// 指定 網絡請求耗時操作發生在子線程
}
}
從伺服器拿到消息的實體類,其中包涵的字段看代碼注釋。
public class User implements Serializable {
private String name;// 姓名
private String age;// 年齡
private String sex;// 性别
private String describe;//描述
private String mobile;//手機号
public User() {
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getAge() {
return age;
}
public void setAge(String age) {
this.age = age;
}
public String getSex() {
return sex;
}
public void setSex(String sex) {
this.sex = sex;
}
public String getDescribe() {
return describe;
}
public void setDescribe(String describe) {
this.describe = describe;
}
public String getMobile() {
return mobile;
}
public void setMobile(String mobile) {
this.mobile = mobile;
}
}
MainActivity:當登入成功後,跳轉到的目标界面,顯示登入成功從伺服器拿到的使用者資訊;
其中字段命名很明顯了,就不一一解釋。
public class MainActivity extends AppCompatActivity {
private TextView tv_name;
private TextView tv_sex;
private TextView tv_age;
private TextView tv_mobile;
private TextView tv_describe;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initView();
User user = (User) getIntent().getSerializableExtra("user");
if(user!=null){
tv_name.setText(tv_name.getText().toString()+""+user.getName());
tv_sex.setText(tv_sex.getText().toString()+""+user.getSex());
tv_age.setText(tv_age.getText().toString()+""+user.getAge());
tv_mobile.setText(tv_mobile.getText().toString()+""+user.getMobile());
tv_describe.setText(tv_name.getText().toString()+"\n"+user.getDescribe());
}
}
private void initView() {
tv_name = findViewById(R.id.tv_name);
tv_sex = findViewById(R.id.tv_sex);
tv_age = findViewById(R.id.tv_age);
tv_mobile = findViewById(R.id.tv_mobile);
tv_describe = findViewById(R.id.tv_describe);
}
}
最後别忘了在AndroidManifests.xml中添加網絡請求的權限
<uses-permission android:name="android.permission.INTERNET"/>
伺服器登入檔案的編寫
簡單的用php寫的 使用者名,密碼驗證檔案
url:http://soyoyo.esy.es/testmvp.php
請求方式:post
使用者名:wuming
密碼:123
傳回資訊:
{
"name":"無名",
"age":"18",
"sex":"男",
"describe":"這個人很懶,沒有寫自我介紹。但是留了一句話:我還會再回來的!",
"mobile":"13874389438"
}
<?php
$name = $_POST['username'];
$pwd = $_POST['userpassword'];
if($name!="wuming"){
die ("使用者名錯誤!");
}
if($pwd !="123"){
die ("密碼錯誤!");
}
$info = array('name'=>'無名',
'age'=>'18',
'sex'=>'男',
'describe'=>'這個人很懶,沒有寫自我介紹。但是留了一句話:我還會再回來的!',
'mobile'=>'13874389438');
echo json_encode($info);
?>
總結
熟悉MVP架構
Rxjava的皮毛運用,還有很多的方法,妙用等着去開發實踐:map,flitMap,interval等
github代碼機票--------->