一、题目名称
烟大课程表
二、系统分析
1.功能描述:
本app使用教务系统api接口,可通过选择学院名称和班级一键导入班级课表,若课表信息有误还可以对课表进行修改,包括添加课程,修改课程,删除课程。详细运行结果见附录截图。
2.结构分析:
本项目使用视图层、实体层和数据操作层三层架构,降低了层与层之间的依赖,实现了高内聚,低耦合”,利于各层逻辑的复用。Util包中包括自定义的一些工具函数,提高了代码复用率。结构较为清晰。
其中,view包中包括MainActivity、ChooseClassActivity、ChangeCourseActivity、AddCourseActivity、AboutActivity、ContactActivity七个类,分别对应七个页面
Dao包中包括CourseDao类,是对数据库的增删改查操作
Model包中包括实体类Course
Util包中包括数据库操作类DatabaseHelper、网络请求类HttpHelper、数据解析类ParseHelper
三、系统设计
1.页面迁移状态图:
项目共包含六个页面,启动软件后,会判断是否为第一次打开,如果是则进入班级选择界面,否则进入课程主页面,可根据导航选择进入“修改班级”,“添加课程”,“修改课程”,“关于课表”,“联系作者”页面,子页面中返回则回到主页面。
2.数据库设计:
数据库中只有一张表,用来存储当前班级从教务系统获取并经用户添加、删除或修改的课表信息,包括课程名称、教师名、上课地点、周次、开始节次、结束节次、备注。主键id唯一标识一条课程信息记录,对课程信息进行修改或删除是通过id找到相应的数据记录。
3.功能模块的详细设计
1)导入课表:
用户在修改课表页面选择学院、班级,软件接收参数、整合url向接口提交请求,接口返回Unicode编码的数据,软件对数据进行解码得到汉字的json数据。解析json数据,把每一条不为空的课程信息存入数据库。返回到主页面时,先将之前的课程信息清空,重新从数据库加载数据
2)添加课程:
用户在添加课程页面填写新课程的信息,提交之后验证信息是否合法,若合法则封装成course对象,通过数据操作层存入数据库。同时,添加课程页面返回course对象到主界面,主界面通过onActivityResult()函数接收返回的course对象,从而在在主界面添加对应的课程
3)修改课程:
用户在修改课程界面选择周次和节次,数据操作层根据查询条件从数据库获取到相应的课程信息,显示在修改课程界面。用户可以对内容进行修改。确认修改后,软件检验课程信息是否合法,若合法则将该对象信息根据唯一标识符id在数据库中进行修改。修改完成后返回主界面,清空已存在的课程视图,重新加载数据。
四、核心代码
软件完整源代码:https://github.com/nicahead/ytuClass
1.判断是否为第一次使用本软件,是则跳转选择班级界面
通过SharedPreferences存储使用记录,第一次使用时获取不到 isfirst字段则取true,跳转到选择班级界面,并修改该字段为false,保证以后每次打开都会跳转到主界面
private void judge() {
SharedPreferences shared = getSharedPreferences("is" ,MODE_PRIVATE);
boolean isfirst = shared.getBoolean("isfirst", true);
//获取不到则为true
SharedPreferences.Editor editor = shared.edit();
if (isfirst) {
//第一次进入则跳转到班级选择界面
Intent intent = new Intent(MainActivity.this,ChooseClassActivity.class);
startActivity(intent);
editor.putBoolean("isfirst ", false); //更新isfirst的值
editor.commit();
}
}
2.动态创建课程节数:
通过LayoutInflater实例化与Layout XML文件对应的View对象,通过LayoutInflater.LayoutParams设置相关的属性实现动态添加左侧的课程节数
private void createLeftView(Course course) {
int len = course.getClassEnd();
if (len > maxCoursesNumber) {
for (int i = 0; i < len - maxCoursesNumber; i++) {
View view = LayoutInflater.from(this).Inflate(R.layout.order_view, null); //实例化view对象,加载布局
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(80, 180);
view.setLayoutParams(params);
TextView text = view.findViewById(R.id.class_number_text);
text.setText(String.valueOf(++currentCoursesNumber));
LinearLayout leftViewLayout = findViewById(R.id.left_view_layout);
leftViewLayout.addView(view);
}
}
maxCoursesNumber = len;
}
3.动态创建课程视图
<android.support.v7.widget.CardView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="45dp"
android:layout_height="70dp"
app:cardBackgroundColor="#7feacdd1"
app:cardCornerRadius="7dp">
<TextView
android:id="@+id/text_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center"/>
</android.support.v7.widget.CardView>
课程使用卡片布局。根据周次找到对应的线性布局,在该布局里设置课程的起始位置和高度,并填写课程相关信息,实现动态添加课程
private void createCourseView(final Course course) {
int height = 180;
int integer = course.getDay();
if ((integer < 1 || integer > 7))
Toast.makeText(this, "请填写正确的周次", Toast.LENGTH_LONG).show();
else if (course.getClassStart() > course.getClassEnd())
Toast.makeText(this, "课程结束时间比开始时间还早,请核对", Toast.LENGTH_LONG).show();
else {
switch (integer) {
case 1:
day = findViewById(R.id.monday);
break;
case 2:
day = findViewById(R.id.tuesday);
break;
case 3:
day = findViewById(R.id.wednesday);
break;
case 4:
day = findViewById(R.id.thursday);
break;
case 5:
day = findViewById(R.id.friday);
break;
case 6:
day = findViewById(R.id.saturday);
break;
case 7:
day = findViewById(R.id.weekday);
break;
}
final View view = LayoutInflater.from(this).inflate(R.layout.course_card, null); //加载单个课程布局
view.setY(height * (course.getClassStart() - 1)); //设置开始高度,即第几节课开始
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams
(ViewGroup.LayoutParams.MATCH_PARENT, (course.getClassStart() - course.getClassStart() + 2) * height); //设置布局高度,即跨多少节课
view.setLayoutParams(params);
TextView text = view.findViewById(R.id.text_view);
text.setText(course.getCourseName() + "\n" + course.getTeacher() + "\n" + course.getClassRoom()); //显示课程名
day.addView(view);
}
}
4.请求数据:
首先需要在AndroidMainifest.xml中声明使用网络权限
<uses-permission android:name="android.permission.INTERNET" />
根据传入的学院,班级参数组装url,使用第三方类库OkHttp向接口发送请求并返回获取到的数据
public static String getResponseStr(String faculty,String _class) throws Exception {
StringBuilder url=new StringBuilder("https://api.mayuko.cn/v2/ytukb/?sk=99a6dde0c45e5313c014efee381f4eb3");
url.append("&xy="+java.net.URLEncoder.encode(faculty,"utf-8"));
url.append("&bj="+_class);
URL u = new URL(url.toString());
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder().url(u).build();
Response response = client.newCall(request).execute();
String responseData = response.body().string();
return unicodeToString(responseData);
}
5.获取到的数据为Unicode编码,编写函数进行解码(本函数来自网络):
public static String unicodeToString(String str) {
Pattern pattern = Pattern.compile("(\\\\u(\\p{XDigit}{4}))");
Matcher matcher = pattern.matcher(str);
char ch;
while (matcher.find()) {
String group = matcher.group(2);
ch = (char) Integer.parseInt(group, 16);
String group1 = matcher.group(1);
str = str.replace(group1, ch + "");
}
return str;
}
6.解析json数据,存入数据库
获取到的json数据为这种形式,需要先获取到序号对应的的外层对象,再获取到课程信息对象,且该数据排列方式为横向排列,由此算出课程周次=课程序号%7+1,课程开始节次= ((int)(课程序号/7))*2+1
public static void parse(String responseData){
Course course = new Course();
try {
JSONObject jsonObject = new JSONObject(responseData);//每一层都是一个Object对象
for (int i = 0;i < 42;i++){
JSONObject courseObj = jsonObject.getJSONObject(String.valueOf(i)).getJSONObject("0");
if (!courseObj.getString("kbName").equals("")){
course.setCourseName(courseObj.getString("kbName"));
course.setTeacher(courseObj.getString("kbTeacher"));
course.setClassRoom(courseObj.getString("kbAddr"));
course.setRemark(courseObj.getString("kbWeek"));
course.setDay(i%7+1);
course.setClassStart(((int)(i/7))*2+1);
course.setClassEnd(((int)(i/7))*2+2);
// Log.v("debug", course.toString());
courseDao.addCourse(course);
}
}
} catch (JSONException e) {
e.printStackTrace();
}
}
7.操作数据库
定义DatabaseHelper类继承SQLiteOpenHelper类,重写onCreate方法,第一次创建数据库的时候回调该方法,当使用getReadableDatabase()方法获取数据库实例的时候, 如果数据库不存在, 也会调用这个方法
public void onCreate(SQLiteDatabase db) {
db.execSQL("create table courses(" +
"id integer primary key autoincrement," +
"courseName text," +
"teacher text," +
"classRoom text," +
"day integer," +
"classStart integer," +
"classEnd integer,"+
"remark text)");
}
对课表数据的操作封装在CourseDao类中,在构造方法中获取DatabaseHelper类的实例
public CourseDao(Context context) {
//得到数据库对象
dbHelper = new DatabaseHelper(context);
}
例如,对课程信息进行条件查询根据周次和节次进行条件查询。先通过getReadableDatabase()方法获取数据库实例,再执行查询操作,返回数据集的游标,移动遍历数据集的每一行,取出数据
public Course getCourse(String day,String classStart) {
SQLiteDatabase db = this.dbHelper.getWritableDatabase();
Cursor cursor=db.query("courses",null,"day=? and classStart=?",new String[]{day,classStart},null,null,null);
Course course = new Course();
if(cursor.moveToFirst()){
do{
course.setId(Integer.parseInt(cursor.getString(cursor.getColumnIndex("id"))));
course.setCourseName(cursor.getString(cursor.getColumnIndex("courseName"))); course.setTeacher(cursor.getString(cursor.getColumnIndex("teacher"))); course.setClassRoom(cursor.getString(cursor.getColumnIndex("classRoom"))); course.setDay(Integer.parseInt(cursor.getString(cursor.getColumnIndex("day")))); course.setClassStart(Integer.parseInt(cursor.getString(cursor.getColumnIndex("classStart")))); course.setClassEnd(Integer.parseInt(cursor.getString(cursor.getColumnIndex("classEnd")))); course.setRemark(cursor.getString(cursor.getColumnIndex("remark")));
}while (cursor.moveToNext());
}
cursor.close();
return course;
}
根据id删除课程:
public void deleteCourse(int id) {
SQLiteDatabase db = this.dbHelper.getWritableDatabase();
db.delete("courses","id = ?",new String[]{id+""});
}
修改课程
public void updateCourse(Course course) {
SQLiteDatabase db = this.dbHelper.getWritableDatabase();
ContentValues values = new ContentValues();
values.put("courseName",course.getCourseName());
values.put("teacher",course.getTeacher());
values.put("classRoom",course.getClassRoom());
values.put("day",course.getDay());
values.put("classStart",course.getClassStart());
values.put("classEnd",course.getClassEnd());
values.put("remark",course.getRemark());
db.update("courses",values,"id = ?",new String[]{course.getId()+""});
}
8.修改课表时,选择周次节次,动态修改文本框内容
首先,定义全局变量,存储当前Spinner选中的选项,初始都为1
String search_day = “1”, search_begin = “1”;
对周次的Spinner添加监听事件,将本次选择的周次和全局变量里存储的节次作为查询参数,从数据库中获取course对象,调用changeView()函数,填充文本框
final String[] days = new String[]{"星期一", "星期二", "星期三", "星期四", "星期五", "星期六", "星期日"};
ArrayAdapter daysAdapter = new ArrayAdapter(this, android.R.layout.simple_list_item_1, days);
day.setAdapter(daysAdapter);
day.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
search_day = position + 1 + "";
Course course = courseDao.getCourse(search_day, search_begin);
changeView(course);
}
@Override
public void onNothingSelected(AdapterView<?> parent) {
}
});
同理,在改变节次时,将本次选择的节次和全局变量里存储的周次作为查询参数,从数据库中获取course对象,调用changeView()函数,填充文本框
final String[] class_begins = new String[]{"1", "3", "5", "7", "9", "11"};
ArrayAdapter beginsAdapter = new ArrayAdapter(this, android.R.layout.simple_list_item_1, class_begins);
class_begin.setAdapter(beginsAdapter);
class_begin.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
search_begin = 2 * position + 1 + "";
Course course = courseDao.getCourse(search_day, search_begin);
changeView(course);
}
@Override
public void onNothingSelected(AdapterView<?> parent) {
}
});
8.全局获取context
编码过程中很多地方都要传入context参数,当我们的操作在活动中进行的时候,活动本身就是一个context对象。但是,一些逻辑代码(比如我在json数据解析类中想创建CourseDao的实例,需要传入context参数)是脱离Activity类的,这时候可以使用全局获取context的技巧
创建一个MyApplication类继承自Application:
public class MyApplication extends Application {
private static Context context;
@SuppressLint("MissingSuperCall")
@Override
public void onCreate() {
context = getApplicationContext();
}
public static Context getContext(){
return context;
}
}
在AndroidMainifest.xml里指定,告知系统在程序初始化的时候初始化MyApplication类:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.nic.ytuclass">
<application
android:name="com.example.nic.ytuclass.util.MyApplication"
...
</application>
...
</manifest>
这个时候就可以在需要的地方获取context了,如:
CourseDao courseDao = new CourseDao(MyApplication.getContext());
八、经验与总结
本次安卓大作业我做的是一个课表app,做这个是因为之前发现145一个学长的网站上有获取学校课程信息的api,想着使用它做个app也是不错的选择。但是真正上手去做才发现有很多问题。
首先,从接口获取到的数据显示为乱码,平时utf8、iso-8859-1、Gbk 这些编码接触的比较多,Unicode编码倒是第一次遇到。发现不能通过指定编码方式的方法进行解码。几经周折,在网上找到Unicode转汉字的一个小函数,问题终于解决。关于编码还遇到的问题是,url中存在汉字参数,而通过代码request的方式会造成服务器无法正确解码,最后通过java.net.URLEncoder.encode先以utf8进行编码,再组装url字符串的方式解决了此问题。字符编码真是平时编码中很令人头疼的问题。
在页面布局中也遇到了一些问题。比如课程要以什么样的布局显示。这是整个编码过程中至关重要的一步。通过网上查阅资料,发现可以使用LayoutInflater布局服务实例化与Layout XML文件对应的View对象,然后通过LayoutInflater.LayoutParams来设置相关的属性,最终实现动态添加布局。在这个过程中也学习了卡片布局和抽屉布局,从而使页面更加美观。
在这个过程中我还学习到了一些小技巧,比如全局获取context等等,自己的编码水平和分层架构思想也有了较大提高。最后,感谢老师一年来的辛勤教学,祝老师身体健康,工作顺利。
附录:运行截图