[课设]烟台大学课程表app

一、题目名称

烟大课程表

二、系统分析

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等等,自己的编码水平和分层架构思想也有了较大提高。最后,感谢老师一年来的辛勤教学,祝老师身体健康,工作顺利。

附录:运行截图

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

完整源代码:https://github.com/nicahead/ytuClass

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值