📁 项目目录结构
app/src/main/java/com/example/app/
├── model/
│ ├── LoginRequest.java
│ └── LoginResponse.java
├── api/
│ └── LoginApiService.java
├── network/
│ └── ApiClient.java
└── activity/
└── LoginActivity.java
🔵 标准做法
- MVVM架构 - 继承BaseMvvmActivity,使用ViewBinding
- Retrofit网络框架 - Android主流网络请求库
- 单例模式 - ApiClient管理Retrofit实例
- 异步回调 - 使用Callback处理网络响应
- TextWatcher - 监听输入框变化进行实时验证
- Intent传递数据 - Activity间标准数据传递方式
- Toast提示 - 标准的用户反馈方式
- 输入验证 - 使用Android内置Patterns验证邮箱
- 生命周期管理 - 在适当的生命周期方法中初始化
🟡 项目特定优化
- @Data注解 - 使用Lombok减少getter/setter样板代码
- TP-Link API参数 - 所有带注释"项目特定"的API参数配置
- 简化登录流程 - 去掉缓存、自动登录等复杂功能
- 自定义验证规则 - 用户名≥3位,密码≥6位的业务规则
- 设备信息硬编码 - terminalName、platform等设备参数
dependencies {
compileOnly 'org.projectlombok:lombok:1.18.30'
annotationProcessor 'org.projectlombok:lombok:1.18.30'
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
implementation 'com.squareup.okhttp3:logging-interceptor:4.10.0'
implementation 'com.google.code.gson:gson:2.10.1'
}
*/
import lombok.Data;
@Data
public class LoginRequest {
private String appType;
private String appVersion;
private String cloudPassword;
private String cloudUserName;
private String platform;
private boolean refreshTokenNeeded;
private String terminalMeta;
private String terminalName;
private String terminalUUID;
public LoginRequest(String username, String password) {
this.appType = "TP-Link_aria_Android";
this.appVersion = "3.8.33";
this.cloudUserName = username;
this.cloudPassword = password;
this.platform = "Android 12";
this.refreshTokenNeeded = false;
this.terminalMeta = "1";
this.terminalName = "Redmi Note 9 Pro";
this.terminalUUID = generateTerminalUUID();
}
private String generateTerminalUUID() {
return java.util.UUID.randomUUID().toString().replace("-", "").toUpperCase();
}
}
import lombok.Data;
@Data
public class LoginResponse {
private int error_code;
private LoginResult result;
@Data
public static class LoginResult {
private String accountId;
private String regionCode;
private String regTime;
private String avatarUrl;
private String appServerUrl;
private int riskDetected;
private String nickname;
private String avatarHttpsUrl;
private String errorCode;
private String email;
private String token;
}
}
import retrofit2.Call;
import retrofit2.http.Body;
import retrofit2.http.POST;
import retrofit2.http.Query;
public interface LoginApiService {
@POST("/api/v2/account/captchaLogin")
Call<LoginResponse> login(
@Query("appName") String appName,
@Query("appVer") String appVer,
@Query("netType") String netType,
@Query("termID") String termID,
@Query("ospf") String ospf,
@Query("brand") String brand,
@Query("locale") String locale,
@Query("model") String model,
@Query("termName") String termName,
@Query("termMeta") String termMeta,
@Body LoginRequest loginRequest
);
}
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;
import okhttp3.OkHttpClient;
import okhttp3.logging.HttpLoggingInterceptor;
import java.util.concurrent.TimeUnit;
public class ApiClient {
private static final String BASE_URL = "https:
private static Retrofit retrofit;
public static Retrofit getRetrofitInstance() {
if (retrofit == null) {
HttpLoggingInterceptor logging = new HttpLoggingInterceptor();
logging.setLevel(HttpLoggingInterceptor.Level.BODY);
OkHttpClient client = new OkHttpClient.Builder()
.addInterceptor(logging)
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
.build();
retrofit = new Retrofit.Builder()
.baseUrl(BASE_URL)
.client(client)
.addConverterFactory(GsonConverterFactory.create())
.build();
}
return retrofit;
}
public static LoginApiService getLoginService() {
return getRetrofitInstance().create(LoginApiService.class);
}
}
import android.content.Intent;
import android.os.Bundle;
import android.text.Editable;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.widget.Toast;
import androidx.annotation.Nullable;
import com.example.app.databinding.ActivityLoginBinding;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
public class LoginActivity extends BaseMvvmActivity {
private LoginApiService loginApiService;
@Nullable
@Override
protected ActivityLoginBinding bindContentView(@Nullable Bundle bundle) {
return ActivityLoginBinding.inflate(getLayoutInflater());
}
@Override
protected void subscribeViewModel(@Nullable Bundle bundle) {
loginApiService = ApiClient.getLoginService();
setupViews();
}
private void setupViews() {
getViewBinding().loginButton.setEnabled(false);
TextWatcher textWatcher = new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {}
@Override
public void afterTextChanged(Editable s) {
validateInputs();
}
};
getViewBinding().usernameEditText.addTextChangedListener(textWatcher);
getViewBinding().passwordEditText.addTextChangedListener(textWatcher);
getViewBinding().forgotPasswordButton.setOnClickListener(v -> {
startActivity(new Intent(LoginActivity.this, ForgotPasswordActivity.class));
});
getViewBinding().registerButton.setOnClickListener(v -> {
startActivity(new Intent(LoginActivity.this, RegisterActivity.class));
});
getViewBinding().loginButton.setOnClickListener(v -> performLogin());
}
private void validateInputs() {
String username = getViewBinding().usernameEditText.getText().toString().trim();
String password = getViewBinding().passwordEditText.getText().toString().trim();
boolean isValid = !TextUtils.isEmpty(username) && !TextUtils.isEmpty(password) &&
username.length() >= 3 && password.length() >= 6;
getViewBinding().loginButton.setEnabled(isValid);
}
private void performLogin() {
String username = getViewBinding().usernameEditText.getText().toString().trim();
String password = getViewBinding().passwordEditText.getText().toString().trim();
if (TextUtils.isEmpty(username)) {
showError("请输入用户名");
return;
}
if (TextUtils.isEmpty(password)) {
showError("请输入密码");
return;
}
if (!android.util.Patterns.EMAIL_ADDRESS.matcher(username).matches()) {
showError("请输入有效的邮箱地址");
return;
}
setLoadingState(true);
LoginRequest loginRequest = new LoginRequest(username, password);
Call<LoginResponse> call = loginApiService.login(
"TP-Link_aria_Android",
"3.8.33",
"wifi",
loginRequest.getTerminalUUID(),
"Android 12",
"TPLINK",
"en_US",
"Redmi Note 9 Pro",
"Redmi Note 9 Pro",
"1",
loginRequest
);
call.enqueue(new Callback<LoginResponse>() {
@Override
public void onResponse(Call<LoginResponse> call, Response<LoginResponse> response) {
setLoadingState(false);
if (response.isSuccessful() && response.body() != null) {
handleLoginSuccess(response.body());
} else {
handleLoginError("登录失败,请检查网络连接");
}
}
@Override
public void onFailure(Call<LoginResponse> call, Throwable t) {
setLoadingState(false);
handleLoginError("网络错误: " + t.getMessage());
}
});
}
private void handleLoginSuccess(LoginResponse response) {
if (response.getError_code() == 0 && response.getResult() != null) {
LoginResponse.LoginResult result = response.getResult();
Toast.makeText(this, "登录成功,欢迎 " + result.getNickname(), Toast.LENGTH_SHORT).show();
Intent intent = new Intent(this, MainActivity.class);
intent.putExtra("user_token", result.getToken());
intent.putExtra("user_id", result.getAccountId());
intent.putExtra("user_email", result.getEmail());
startActivity(intent);
finish();
} else {
handleLoginError("登录失败,请检查用户名和密码");
}
}
private void handleLoginError(String message) {
showError(message);
getViewBinding().passwordEditText.setText("");
getViewBinding().passwordEditText.requestFocus();
}
private void setLoadingState(boolean isLoading) {
getViewBinding().loginButton.setEnabled(!isLoading);
getViewBinding().usernameEditText.setEnabled(!isLoading);
getViewBinding().passwordEditText.setEnabled(!isLoading);
if (isLoading) {
getViewBinding().loginButton.setText("登录中...");
} else {
getViewBinding().loginButton.setText("登录");
}
}
private void showError(String message) {
Toast.makeText(this, message, Toast.LENGTH_LONG).show();
}
}
public class RegionSelectionHandler {
private Context context;
private TextView regionTextView;
private static final String PREFS_NAME = "app_settings";
private static final String KEY_SELECTED_REGION = "selected_region";
private static final String DEFAULT_REGION = "中国大陆";
public RegionSelectionHandler(Context context, TextView regionTextView) {
this.context = context;
this.regionTextView = regionTextView;
}
public void initRegionDisplay() {
updateRegionDisplay();
}
public void updateRegionDisplay() {
String selectedRegion = getSelectedRegion();
regionTextView.setText(selectedRegion);
}
public String getSelectedRegion() {
SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
return prefs.getString(KEY_SELECTED_REGION, DEFAULT_REGION);
}
public void saveSelectedRegion(String region) {
SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
prefs.edit().putString(KEY_SELECTED_REGION, region).apply();
updateRegionDisplay();
}
public void setupModalContent(View contentView, Runnable onRegionSelected) {
TPListViewRadioGroup checkGroup = contentView.findViewById(R.id.check_group);
TPSearchBar tpSearch = contentView.findViewById(R.id.tp_search);
setInitialSelection(checkGroup);
setupSearchFunction(tpSearch, checkGroup);
checkGroup.setOnCheckedChangeListener((group, checkedId) -> {
});
contentView.setTag(R.id.region_callback, onRegionSelected);
contentView.setTag(R.id.region_handler, this);
}
public static void handleDoneClick(View contentView) {
RegionSelectionHandler handler = (RegionSelectionHandler) contentView.getTag(R.id.region_handler);
Runnable callback = (Runnable) contentView.getTag(R.id.region_callback);
if (handler != null) {
TPListViewRadioGroup checkGroup = contentView.findViewById(R.id.check_group);
int checkedId = checkGroup.getCheckedItemId();
if (checkedId != -1) {
String selectedRegion = handler.getRegionNameById(checkedId);
if (selectedRegion != null) {
handler.saveSelectedRegion(selectedRegion);
Toast.makeText(handler.context, "已选择地区: " + selectedRegion, Toast.LENGTH_SHORT).show();
if (callback != null) {
callback.run();
}
}
}
}
}
private void setInitialSelection(TPListViewRadioGroup checkGroup) {
String currentRegion = getSelectedRegion();
Map<String, String> regionMap = getRegionMap();
for (Map.Entry<String, String> entry : regionMap.entrySet()) {
if (entry.getValue().equals(currentRegion)) {
int viewId = context.getResources().getIdentifier(entry.getKey(), "id", context.getPackageName());
if (viewId != 0) {
checkGroup.check(viewId);
}
break;
}
}
}
private void setupSearchFunction(TPSearchBar tpSearch, TPListViewRadioGroup checkGroup) {
List<RegionItem> allRegions = getAllRegions();
List<RegionItem> filteredRegions = new ArrayList<>(allRegions);
SearchViewBaseAdapter<RegionItem> searchAdapter = new SearchViewBaseAdapter<RegionItem>(filteredRegions,
new SearchViewBaseAdapter.OnBindDataListener<RegionItem>() {
@Override
public void onBindViewHolder(RegionItem model, SearchViewBaseViewHolder viewHolder, int type, int position) {
if (viewHolder != null && model != null) {
viewHolder.setText(R.id.suggestion_text, model.displayName);
}
}
@Override
public int getLayoutId(int type) {
return R.layout.search_suggestion_item;
}
});
searchAdapter.setItemClickListener((adapter, position) -> {
if (position < filteredRegions.size()) {
RegionItem selectedItem = filteredRegions.get(position);
selectRegion(checkGroup, selectedItem);
tpSearch.closeSearch();
}
});
tpSearch.setIsTextChangeListener(s -> {
filterRegions(allRegions, filteredRegions, s);
searchAdapter.updateData(filteredRegions);
});
tpSearch.setOnQueryTextListener(new MaterialSearchView.OnQueryTextListener() {
@Override
public boolean onQueryTextSubmit(String query) {
filterRegions(allRegions, filteredRegions, query);
if (!filteredRegions.isEmpty()) {
selectRegion(checkGroup, filteredRegions.get(0));
}
return true;
}
@Override
public boolean onQueryTextChange(String newText) {
return false;
}
});
tpSearch.setAdapter(searchAdapter);
}
private void selectRegion(TPListViewRadioGroup checkGroup, RegionItem region) {
int viewId = context.getResources().getIdentifier(region.id, "id", context.getPackageName());
if (viewId != 0) {
checkGroup.check(viewId);
}
}
private void filterRegions(List<RegionItem> allRegions, List<RegionItem> filteredRegions, String query) {
if (TextUtils.isEmpty(query)) {
filteredRegions.clear();
filteredRegions.addAll(allRegions);
} else {
filteredRegions.clear();
String lowerQuery = query.toLowerCase();
for (RegionItem region : allRegions) {
if (region.displayName.toLowerCase().contains(lowerQuery) ||
region.name.toLowerCase().contains(lowerQuery)) {
filteredRegions.add(region);
}
}
}
}
private String getRegionNameById(int checkedId) {
Map<String, String> regionMap = getRegionMap();
String resourceName = context.getResources().getResourceEntryName(checkedId);
return regionMap.get(resourceName);
}
private Map<String, String> getRegionMap() {
Map<String, String> regionMap = new HashMap<>();
regionMap.put("check_item1", "中国大陆");
regionMap.put("check_item2", "中国香港");
regionMap.put("check_item3", "中国澳门");
regionMap.put("check_item4", "中国台湾");
regionMap.put("check_item5", "新加坡");
regionMap.put("check_item6", "美国");
regionMap.put("check_item7", "日本");
regionMap.put("check_item8", "越南");
regionMap.put("check_item9", "巴西");
regionMap.put("check_item10", "法国");
regionMap.put("check_item11", "英国");
regionMap.put("check_item12", "德国");
return regionMap;
}
private List<RegionItem> getAllRegions() {
return Arrays.asList(
new RegionItem("check_item1", "mainland_china", "中国大陆"),
new RegionItem("check_item2", "hong_kong", "中国香港"),
new RegionItem("check_item3", "macau", "中国澳门"),
new RegionItem("check_item4", "taiwan", "中国台湾"),
new RegionItem("check_item5", "singapore", "新加坡"),
new RegionItem("check_item6", "usa", "美国"),
new RegionItem("check_item7", "japan", "日本"),
new RegionItem("check_item8", "vietnam", "越南"),
new RegionItem("check_item9", "brazil", "巴西"),
new RegionItem("check_item10", "france", "法国"),
new RegionItem("check_item11", "uk", "英国"),
new RegionItem("check_item12", "germany", "德国")
);
}
public static class RegionItem {
public String id;
public String name;
public String displayName;
public RegionItem(String id, String name, String displayName) {
this.id = id;
this.name = name;
this.displayName = displayName;
}
}
}
public class RegisterActivity extends BaseMvvmActivity<ActivityRegisterBinding> {
private ArrayList<String> countryList;
private SearchViewBaseAdapter<String> searchAdapter;
private TPModalBottomSheet currentModal;
private ArrayList<TPSingleLineItemView> dynamicItemViews;
@Nullable
@Override
protected ActivityRegisterBinding bindContentView(@Nullable Bundle bundle) {
return ActivityRegisterBinding.inflate(getLayoutInflater());
}
@Override
protected void subscribeViewModel(@Nullable Bundle bundle) {
initCountryData();
setupViews();
}
private void initCountryData() {
countryList = new ArrayList<String>(Arrays.asList(
"中国大陆", "中国香港", "中国澳门", "中国台湾",
"新加坡", "美国", "日本", "越南", "巴西",
"法国", "英国", "德国", "加拿大", "澳大利亚",
"韩国", "印度", "泰国", "马来西亚", "印度尼西亚"
));
dynamicItemViews = new ArrayList<TPSingleLineItemView>();
}
private void setupViews() {
viewBinding.btnNext.setEnabled(false);
TextWatcher textWatcher = new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
@Override
public void afterTextChanged(Editable s) {
String email = viewBinding.account.getText().toString().trim();
boolean isValidEmail = EmailValidator.isValidEmail(email);
viewBinding.btnNext.setEnabled(!email.isEmpty() && isValidEmail);
}
};
viewBinding.account.addTextChangedListener(textWatcher);
viewBinding.btnNext.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
String account = viewBinding.account.getText().toString();
SharedPreferences sharedPreferences = getSharedPreferences("register_email", MODE_PRIVATE);
SharedPreferences.Editor editor = sharedPreferences.edit();
editor.putString("account", account);
editor.apply();
Intent intent = new Intent(RegisterActivity.this, SetPasswordActivity.class);
startActivity(intent);
}
});
viewBinding.btnLogin.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
startActivity(new Intent(RegisterActivity.this, LoginActivity.class));
}
});
viewBinding.tvRegion.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
showCountrySelectionModal();
}
});
}
private void showCountrySelectionModal() {
currentModal = new TPModalBottomSheet.Builder()
.setScreenType(TPModalBottomSheet.ScreenType.FULL_SCREEN)
.setDividerEnable(false)
.setTitle("选择国家/地区")
.setEndOptionText("完成")
.setContentLayoutId(R.layout.fragment_region)
.setContentViewListener(new TPModalBottomSheet.OnContentViewListener() {
@Override
public void onContentViewCreated(TPModalBottomSheet tpModalBottomSheet, View view) {
setupSearchView(view);
setupDynamicRadioGroup(view);
}
})
.show(getSupportFragmentManager(), "country_selection");
}
private void setupSearchView(View contentView) {
TPSearchBar tpSearchBar = contentView.findViewById(R.id.tp_search);
if (tpSearchBar == null) {
Toast.makeText(this, "搜索框未找到,请检查布局文件", Toast.LENGTH_SHORT).show();
return;
}
searchAdapter = new SearchViewBaseAdapter<String>(
new ArrayList<String>(countryList),
new SearchViewBaseAdapter.OnBindDataListener<String>() {
@Override
public void onBindViewHolder(String model, SearchViewBaseViewHolder viewHolder, int type, int position) {
viewHolder.setText(R.id.suggestion_text, model);
}
@Override
public int getLayoutId(int type) {
return R.layout.layout_search_suggest_item;
}
}
);
searchAdapter.setItemClickListener(new SearchViewBaseAdapter.OnItemClickListener() {
@Override
public void onItemClick(View view, int position) {
String selectedCountry = searchAdapter.getData().get(position);
selectCountry(selectedCountry);
}
});
tpSearchBar.setManager(getSupportFragmentManager());
tpSearchBar.setSearchViewAdapter(searchAdapter);
tpSearchBar.setTextChangeListener(new TPSearchBar.OnTextChangeListener() {
@Override
public void onTextChange(String searchText) {
filterCountries(contentView, searchText);
}
});
tpSearchBar.setOnQueryTextListener(new TPMaterialSearchView.OnQueryTextListener() {
@Override
public boolean onQueryTextSubmit(String query) {
Toast.makeText(RegisterActivity.this, "搜索: " + query, Toast.LENGTH_SHORT).show();
return false;
}
@Override
public boolean onQueryTextChange(String newText) {
return false;
}
});
}
private void setupDynamicRadioGroup(View contentView) {
TPListViewRadioGroup checkGroup = contentView.findViewById(R.id.check_group);
checkGroup.removeAllViews();
dynamicItemViews.clear();
for (int i = 0; i < countryList.size(); i++) {
final String country = countryList.get(i);
TPSingleLineItemView itemView = new TPSingleLineItemView(this);
itemView.setItemTitle(country);
itemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
selectCountry(country);
}
});
TPListViewRadioGroup.LayoutParams layoutParams =
new TPListViewRadioGroup.LayoutParams(
TPListViewRadioGroup.LayoutParams.MATCH_PARENT,
TPListViewRadioGroup.LayoutParams.WRAP_CONTENT
);
itemView.setLayoutParams(layoutParams);
checkGroup.addView(itemView);
dynamicItemViews.add(itemView);
}
}
private void filterCountries(View contentView, String searchText) {
if (searchText == null || searchText.trim().isEmpty()) {
showAllCountries();
searchAdapter.updateData(new ArrayList<String>(countryList));
} else {
ArrayList<String> filteredList = getFilteredCountries(searchText);
hideUnmatchedCountries(filteredList);
searchAdapter.updateData(filteredList);
}
}
private ArrayList<String> getFilteredCountries(String searchText) {
ArrayList<String> filteredList = new ArrayList<String>();
for (String country : countryList) {
if (country.toLowerCase().contains(searchText.toLowerCase())) {
filteredList.add(country);
}
}
return filteredList;
}
private void showAllCountries() {
for (TPSingleLineItemView itemView : dynamicItemViews) {
itemView.setVisibility(View.VISIBLE);
}
}
private void hideUnmatchedCountries(ArrayList<String> visibleCountries) {
for (TPSingleLineItemView itemView : dynamicItemViews) {
String itemTitle = itemView.getItemTitle();
if (visibleCountries.contains(itemTitle)) {
itemView.setVisibility(View.VISIBLE);
} else {
itemView.setVisibility(View.GONE);
}
}
}
private void selectCountry(String countryName) {
viewBinding.tvRegion.setText(countryName);
SharedPreferences sharedPreferences = getSharedPreferences("register_info", MODE_PRIVATE);
SharedPreferences.Editor editor = sharedPreferences.edit();
editor.putString("selected_country", countryName);
editor.apply();
Toast.makeText(this, "已选择: " + countryName, Toast.LENGTH_SHORT).show();
dismissModal();
}
private void dismissModal() {
if (currentModal != null) {
Fragment fragment = getSupportFragmentManager().findFragmentByTag("country_selection");
if (fragment != null) {
getSupportFragmentManager().beginTransaction().remove(fragment).commit();
}
}
}
}