天天看點

MVP+Okhttp3+RxJava 簡單實作登入操作

介紹

以登入為案列,MVP架構,okhttp網絡請求實作的登陸操作
1. 使用者輸入使用者名;
2. 點選登入;
3. 彈出progressbar,告知使用者目前正在驗證使用者名密碼;
4. 執行網絡請求;
5. 隐藏progressbar;網絡驗證完成;
6. 根據伺服器傳回結果執行跳轉主界面操作或者顯示錯誤資訊;

注意:可能有小夥伴覺得這樣寫,多了類,多寫了代碼;
    但增強了代碼可讀性,可了解性,手動敲一敲便可了解MVP這樣設計用意;
           

效果圖

MVP+Okhttp3+RxJava 簡單實作登入操作
MVP+Okhttp3+RxJava 簡單實作登入操作

知識點

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代碼機票--------->