
安卓大作业(半成品...时间实在是不够了,主要单纯技术的重复无意义)
这学期写了个基于MVVM与jetpack新技术的安卓项目,但由于时间与精力因素,我是在是没空去写了,今天来总结一下项目代码与开发时的经验。
以页面的布局路线来讲好了
1.登陆页面
界面如图(注册实在没时间写了,主要技术难度不高,还有个打开本地相册取图片当头像的代码也做过了,只不过不在这个项目...)
介绍一下界面,普通的输入框按钮就不介绍了,这边验证码是通过后端接口Kaptch拿来的
@RequestMapping("/checkcode")
@RestController
public class Checkcode {
@Resource
DefaultKaptcha defaultKaptcha;
@Autowired
private StringRedisTemplate stringRedisTemplate;
//生成验证码
@GetMapping("/Code")
public void defaultKaptcha(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse)
throws Exception {
byte[] captchaChallengeAsJpeg = null;
ByteArrayOutputStream jpegOutputStream = new ByteArrayOutputStream();
try {
// 生产验证码字符串并保存到session中
String createText = defaultKaptcha.createText();
System.out.println("生成的是"+createText);
createText = createText.toLowerCase();
ValueOperations<String, String> operations = stringRedisTemplate.opsForValue();
operations.set(createText, "0", 5, TimeUnit.MINUTES);//未验证
httpServletRequest.getSession().setAttribute("picCode", createText);
// 使用生成的验证码字符串返回一个BufferedImage对象并转为byte写入到byte数组中
BufferedImage challenge = defaultKaptcha.createImage(createText);
ImageIO.write(challenge, "jpg", jpegOutputStream);
} catch (IllegalArgumentException e) {
httpServletResponse.sendError(HttpServletResponse.SC_NOT_FOUND);
return;
}
// 定义response输出类型为image/jpeg类型,使用response输出流输出图片的byte数组
captchaChallengeAsJpeg = jpegOutputStream.toByteArray();
httpServletResponse.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
httpServletResponse.setHeader("Pragma", "no-cache");
httpServletResponse.setDateHeader("Expires", 0);
httpServletResponse.setContentType("image/jpeg");
ServletOutputStream responseOutputStream = httpServletResponse.getOutputStream();
responseOutputStream.write(captchaChallengeAsJpeg);
responseOutputStream.flush();
responseOutputStream.close();
}
// @ApiOperation(value = "校对验证码")
@PostMapping("/check")
public Result checkVerificationCode(@RequestParam String picCode, HttpServletRequest httpServletRequest) {
String verificationCodeIn = (String)httpServletRequest.getSession().getAttribute("picCode");
httpServletRequest.getSession().removeAttribute("picCode");
if ((StringUtils.isEmpty(verificationCodeIn)) || (!verificationCodeIn.equals(picCode))) {
return Result.error("验证码错误,或已失效");
} else {
//移除验证码
stringRedisTemplate.delete(verificationCodeIn);
return Result.success();
}
}
}
还有记住密码实现--就是从本地sharedPreferences读取而已
以下是全部代码
public class MainActivity extends AppCompatActivity {
private MainViewModel viewModel;
private ImageView captchaImageView;
private Button loginEntryBtn;
private TextInputEditText loginAccount;
private TextInputEditText loginPassword;
private TextInputEditText loginVerify;
private CheckBox rememberPasswordCheckbox;
private SharedPreferences sharedPreferences;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
EdgeToEdge.enable(this);
setContentView(R.layout.activity_main);
// 初始化 TokenManager
try {
TokenManager.init(this);
} catch (Exception e) {
e.printStackTrace();
Tools.Toast(MainActivity.this, "初始化 TokenManager 失败");
return;
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
ActivityCompat.requestPermissions(this,
new String[]{Manifest.permission.POST_NOTIFICATIONS}, 1);
}
// 使用工具类设置状态栏
StatusBarUtil.setImmersiveStatusBarR(this);
// 初始化控件
captchaImageView = findViewById(R.id.captchaImageView);
loginEntryBtn = findViewById(R.id.login_entry_btn);
loginAccount = findViewById(R.id.login_account);
loginPassword = findViewById(R.id.login_password);
loginVerify = findViewById(R.id.login_verify);
rememberPasswordCheckbox = findViewById(R.id.remember_password_checkbox);
sharedPreferences = getSharedPreferences("LoginPrefs", MODE_PRIVATE);
// 读取保存的账号和密码
String savedAccount = sharedPreferences.getString("account", "");
String savedPassword = sharedPreferences.getString("password", "");
boolean isRemembered = sharedPreferences.getBoolean("remember", false);
if (isRemembered) {
loginAccount.setText(savedAccount);
loginPassword.setText(savedPassword);
rememberPasswordCheckbox.setChecked(true);
}
// 初始化 ViewModel
viewModel = new ViewModelProvider(this).get(MainViewModel.class);
// 观察验证码图片的变化
viewModel.getCaptchaImage().observe(this, bitmap -> captchaImageView.setImageBitmap(bitmap));
// 观察 toast 消息的变化
viewModel.getToastMessage().observe(this, message -> Tools.Toast(MainActivity.this, message));
// 观察认证 token 的变化
viewModel.getAuthToken().observe(this, token -> {
if (token != null) {
try {
// 将 token 存储到本地
TokenManager.encryptAndStoreToken(MainActivity.this, token);
//获取User
viewModel.getUserInfo();
// 启动 UserManageLayout 并销毁 MainActivity
Intent intent = new Intent(MainActivity.this, UserManageLayout.class);
//Service启动
Intent serviceIntent = new Intent(this, GoodSyncService.class);
startService(serviceIntent);
startActivity(intent);
finish();//销毁界面
} catch (Exception e) {
e.printStackTrace();
Tools.Toast(MainActivity.this, "存储 token 失败");
}
}
});
// 获取验证码图片
viewModel.getCheckcodeImage();
captchaImageView.setOnClickListener(v -> viewModel.getCheckcodeImage());
// 登录按钮逻辑
loginEntryBtn.setOnClickListener(v -> {
String account = Objects.requireNonNull(loginAccount.getText()).toString();
String password = Objects.requireNonNull(loginPassword.getText()).toString();
String verify = Objects.requireNonNull(loginVerify.getText()).toString();
if (account.isEmpty()) {
Tools.Toast(MainActivity.this, "请输入账号!");
return;
}
if (password.isEmpty()) {
Tools.Toast(MainActivity.this, "请输入密码!");
return;
}
if (verify.isEmpty()) {
Tools.Toast(MainActivity.this, "请输入验证码!");
return;
}
// 保存账号和密码
SharedPreferences.Editor editor = sharedPreferences.edit();
if (rememberPasswordCheckbox.isChecked()) {
editor.putString("account", account);
editor.putString("password", password);
editor.putBoolean("remember", true);
} else {
editor.remove("account");
editor.remove("password");
editor.putBoolean("remember", false);
}
editor.apply();
viewModel.verifyLogin(account, password, verify);
});
// 添加 TextWatcher 到 loginVerify
loginVerify.addTextChangedListener(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) {
if (s.length() == 4) {
hideKeyboard();
}
}
});
}
// 关闭软键盘的方法
private void hideKeyboard() {
InputMethodManager imm = (InputMethodManager) getSystemService(INPUT_METHOD_SERVICE);
if (imm != null && getCurrentFocus() != null) {
imm.hideSoftInputFromWindow(getCurrentFocus().getWindowToken(), 0);
}
}
}
登陆后将token存入本地,后期请求其他接口要用
2.主界面
界面展示
主界面(商品界面)主要由viewpager2与自定义tabLayout组成
商品个体item xml界面代码如下
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="100dp"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="horizontal"
android:background="@drawable/good_item_bg"
android:layout_margin="5dp"
android:padding="10dp">
<androidx.cardview.widget.CardView
android:layout_width="120dp"
android:layout_height="match_parent"
app:cardCornerRadius="8dp"
>
<ImageView
android:id="@+id/goodImage"
android:layout_width="120dp"
android:layout_height="match_parent"
android:src="@drawable/bg"
android:scaleType="centerCrop"
/>
</androidx.cardview.widget.CardView>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:paddingHorizontal="10dp"
>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginBottom="5dp"
>
<TextView
android:id="@+id/goodTitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:textColor="@android:color/black"
android:textSize="16sp"
android:text="啦啦啦"
android:layout_weight="0.55"
android:textStyle="bold"
android:ellipsize="end"
android:maxLines="1"/>
<TextView
android:id="@+id/goodPrice"
android:layout_width="0dp"
android:layout_weight="0.45"
android:layout_height="wrap_content"
android:textColor="@android:color/black"
android:textSize="14sp"
android:text="原价:$2500000"
android:gravity="center"
android:textStyle="bold"
android:ellipsize="end"
android:maxLines="1"/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginBottom="5dp"
>
<TextView
android:id="@+id/goodInventory"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:textColor="@android:color/black"
android:textSize="14sp"
android:text="库存:1300件"
android:layout_weight="0.55"
android:textStyle="bold"
android:ellipsize="end"
android:maxLines="1"/>
<TextView
android:id="@+id/goodSellingPrice"
android:layout_width="0dp"
android:layout_weight="0.45"
android:layout_height="wrap_content"
android:textColor="@android:color/black"
android:textSize="14sp"
android:text="现价:$2500000"
android:gravity="center"
android:textStyle="bold"
android:ellipsize="end"
android:maxLines="1"/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
>
<TextView
android:id="@+id/goodNowVisits"
android:layout_width="0dp"
android:layout_weight="0.55"
android:layout_height="wrap_content"
android:textColor="@android:color/black"
android:textSize="12sp"
android:text="目前关注人数:25000+人"
android:gravity="start"
android:textStyle="bold"
android:ellipsize="end"
android:maxLines="1"/>
<TextView
android:id="@+id/goodSoldNum"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:textColor="@android:color/black"
android:textSize="12sp"
android:text="已售:1300+件"
android:gravity="center"
android:layout_weight="0.45"
android:textStyle="bold"
android:ellipsize="end"
android:maxLines="1"/>
</LinearLayout>
</LinearLayout>
</LinearLayout>
外框架tablayout和pageView2
public class GoodListFragment extends Fragment {
private RecyclerView recyclerView;
private GoodAdapter goodAdapter;
private GoodViewModel goodViewModel;
private List<Good> goodList;
private List<GoodDetailInfo> goodDetailInfoList;
private List<HeatData> heatDataList;
private static List<Category> categoryListS;
private Integer categoryId;
// 使用 newInstance 方法来接收 categoryId
public static GoodListFragment newInstance(Integer categoryId) {
GoodListFragment fragment = new GoodListFragment();
Bundle args = new Bundle();
if(categoryId != null){
args.putInt("categoryId", categoryId);
}
else {
args.putInt("categoryId", -1);
}
fragment.setArguments(args);
return fragment;
}
@Override
public void onCreate(@androidx.annotation.Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
goodViewModel = new ViewModelProvider(this).get(GoodViewModel.class);
goodViewModel.getGoodCategoryList();
//监听数据变化
goodViewModel.getCategoryListLiveData().observe(this, categoryList -> {
categoryListS = categoryList;
});
// 初始化为一个空列表
goodList = new ArrayList<>();
goodDetailInfoList = new ArrayList<>();
heatDataList = new ArrayList<>();
// 获取 categoryId
if (getArguments() != null) {
categoryId = getArguments().getInt("categoryId");
}
if(categoryId == -1){
categoryId = null;
}
// 调用接口获取数据
goodViewModel.getGoodList(1, 20, categoryId, null, null, null, null);
// 监听数据变化
goodViewModel.getGoodListLiveData().observe(this, newGoodList -> {
goodList = newGoodList;
updateAdapter();
});
goodViewModel.getGoodDetailInfoListLiveData().observe(this, newGoodDetailInfoList -> {
goodDetailInfoList = newGoodDetailInfoList;
updateAdapter();
});
goodViewModel.getHeatDataListLiveData().observe(this, newHeatDataList -> {
heatDataList = newHeatDataList;
updateAdapter();
});
}
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.page_layout, container, false);
recyclerView = view.findViewById(R.id.recyclerView);
recyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
goodAdapter = new GoodAdapter(goodList, goodDetailInfoList, heatDataList);
goodAdapter.setOnItemClickListener((item, position) -> {
// 处理点击事件
// Toast.makeText(getContext(), String.valueOf(item.getId()), Toast.LENGTH_SHORT).show();
// 这里可以跳转到详情页或执行其他操作
if (getContext() != null) {
Intent intent = new Intent(getContext(), GoodClickDetail.class);
intent.putExtra("goodId", item.getId());
AtomicReference<String> categoryName = new AtomicReference<>("");
categoryListS.stream()
.skip(1) // 跳过第一个元素
.filter(category -> category.getId().equals(item.getCategoryId()))
.findFirst()
.ifPresent(category -> {
categoryName.set(category.getCategoryName());
});
intent.putExtra("categoryName", categoryName.get());
startActivity(intent);
}
});
recyclerView.setAdapter(goodAdapter);
return view;
}
private void updateAdapter() {
if (goodAdapter != null) {
goodAdapter.updateData(goodList, goodDetailInfoList, heatDataList);
goodAdapter.notifyDataSetChanged();
}
}
}
这个就是简单的绑定了
3.搜索界面
这里主要对搜索历史进行操作,通过ROOM对sqlite进行操作,增删改查
排序条件为
1.根据搜索次数排序
2.次数相同按时间倒序
可惜没完成后续界面没时间了
4.商品详情界面
这个界面我花费的时间挺长的,从构思到数据绑定,databinding也是在这个界面不断试错学会了
这个界面上面是Toolbar,视频播放使用了StyledPlayerView,好像之前我用的的videoView貌似只能播放本地视频,还是现成组件好用,也做了按钮的点击变色事件(增删改查没做,道理相同的,重复无意义),下面是购买按钮、价格、简介和其他相关信息,我还做了评论显示,最多支持二级评论,不同级别评论以缩进不同距离显示,比较简陋,说实在,没达到我预期效果,要下方弹出抽屉,我不知道为什么还有间距...
具体的用户名,点赞数,数据里有但我还没绑定...哭了工程量好大...,大概就这些了,这学期主要中间我跑去学react了,后面又强化uniCloud去了...要学的好多...
这次主要用到了MVVM架构,底层的ViewModel类负责处理与管理基本数据类和数据请求等,而Activity负责observice这些ViewModel的属性,当属性发生变化时,Activity会随之变化,这样即使翻转数据也可以有效保存,,databinding除了有时候奇怪的报错(对了,包名一定要小写不然databinding认不出...),其他的感觉没什么和java一样,请求是用retrofit(okhttp的封装加强版),以后试试kotlin好了(如果有时间)
gitee仓库代码链接:android_test_first_work: 第一次安卓项目尝试