Android 实践:做一款新闻 APP 您所在的位置:网站首页 安卓软件开发实践实训 Android 实践:做一款新闻 APP

Android 实践:做一款新闻 APP

2024-07-11 14:14| 来源: 网络整理| 查看: 265

跟代码相关的工作,大多唯手熟尔,所以这里花了点时间做了款简易版的新闻 APP,虽然都是些基础的内容,不过还是可以加深自己对部分代码的理解。至少,可以加深自己的记忆

步骤

依赖库网络请求网络解析界面布局最后运行界面运行GIF完整代码下载地址(github)

依赖库

过程中需要用到一些开源依赖库文件,先在 build.grade 中声明:

compile 'com.google.code.gson:gson:2.8.0' //网络解析 compile 'com.squareup.okhttp3:okhttp:3.7.0' //网络请求 compile 'com.github.bumptech.glide:glide:3.8.0' //图片加载 compile 'com.android.support:design:24.2.1' //Material Design中用到的依赖库 compile 'de.hdodenhof:circleimageview:2.1.0' //显示圆形图片

网络请求

在包下创建一个文件夹 util 用来存放工具类,创建文件 HttpUtil.class 用来请求数据:

public class HttpUtil { public static void sendOkHttpRequest(String address, okhttp3.Callback callback){ OkHttpClient client = new OkHttpClient(); Request request = new Request.Builder() .url(address).build(); client.newCall(request).enqueue(callback); } }

这里用到的是 okhttp3.Callback 的回调接口,结果会返回到 callback 的回调函数中,后面会进行处理

网络解析

我们先从数据解析开始,毕竟这才是这个小项目的重点。这次项目使用的数据来源是天行数据(http://www.tianapi.com/ )的新闻资讯 API ,先看 API 的说明:

新闻资讯 API

可以看到返回数据为 JSON, 默认返回 10 条参数。请求地址为:

这里写图片描述

其中, APIKEY 需要用个人的 API KEY 代替,可以在个人中心中看到,其他的请求地址也是大同小异

这里写图片描述

JSON 返回示例:

这里写图片描述

还有错误返回码,用来判断返回数据的异常情况:

这里写图片描述

根据 gson 的返回示例,我们可以写出对应的实体类文件,通过 gson 将返回数据转化为对应的类型。先创建一个 gson 文件夹存放实体类文件。

在 gson 文件夹下创建 New.class 文件:

public class News { @SerializedName("ctime") public String time; public String title; public String description; public String picUrl; public String url; }

创建 NewsList.class 文件:

public class NewsList { public int code; public String msg; @SerializedName("newslist") public List newsList ; }

至此,我们就已经创建好了与返回数据对应的实体类。

在 util 文件夹下创建文件 Utility.class 文件:

public class Utility { public static NewsList parseJsonWithGson(final String requestText){ Gson gson = new Gson(); return gson.fromJson(requestText, NewsList.class); } }

将请求得到的数据解析为 NewList 实体类对象。现在网络请求和解析都准备好了,就开始界面文件了

界面布局

修改 values 目录下的 styles.xml 文件:

-- Customize your theme here. --> @color/colorPrimary @color/colorPrimary @color/colorAccent

修改通知栏颜色和标题栏颜色一样,是处于视觉统一的原因,也可以不修改(非必须)

主要采用的是 Material Design 的设计,整体布局采用的是滑动菜单,主界面内容为 ToolBar 和 ListView(这里为了方便,就直接使用),侧边栏内容为 NavigationView

主界面: 因为要用 ToolBar 替代 ActionBar, 我们先修改 values 下面的 styles 文件,修改主题为:

在layout 下创建 nav_header 文件

这里在头部文件中放置了一个CircleImageView,两个 TextView,没有什么理解难度

在 res 目录下创建 menu 文件夹,新建 nav_menu.xml 文件:

这里创建了若干个 ITEM 子项,只有 title,没有 icon,大家可以自行放置

主界面 activity_main.xml:

因为是一步到位,所以……大家最好之前用过使用过相同的布局设计(比如:第一行代码)

DrawerLayout 中有两个直接子布局文件: 1. CoordinatorLayout:一种 FrameLayout, 作为显示主界面内容的最外层布局 2. NavigationView:作为显示侧边栏的最外层布局,不过已经封装好了,可以直接通过 app:headerLayout 和 app:menu 属性引用之前我们已经写好的 头部和菜单布局文件

CoordinatorLayout 中有两个直接子布局文件: 1. AppBarLayout :通过 AppBarLayout 属性,可以将 ToolBar 和 ListView 分隔开,可以对滚动事件进行响应,实现 Material 效果 2. SwipeRefreshLayout:用来刷新 ListView 中的内容

创建 list_view_item.xml 文件,设计 ListView 的子项布局:

子项布局内包含 3 个控件,ImageView 显示返回的图片,TextView 显示返回的标题和出处

创建一个 Title.class类:

public class Title { private String title; private String descr; private String imageUrl; private String uri; public Title(String title,String descr, String imageUrl, String uri){ this.title = title; this.imageUrl = imageUrl; this.descr = descr; this.uri = uri; } public String getTitle() { return title; } public String getImageUrl() { return imageUrl; } public String getDescr() { return descr; } public String getUri() { return uri; } }

这里之所以除了 标题,出处,图片显示在 ListViw 中,uri 传入另一个布局,显示内容文件

接下来就是 TitleAdapter.class

public class TitleAdapter extends ArrayAdapter { private int resourceId; public TitleAdapter(@NonNull Context context, @LayoutRes int resource, @NonNull List objects) { super(context, resource, objects); resourceId = resource; } @NonNull @Override public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) { Title title = getItem(position); View view; ViewHolder viewHolder; /** * 缓存布局和实例,优化 listView */ if (convertView == null){ view = LayoutInflater.from(getContext()).inflate(resourceId,parent,false); viewHolder = new ViewHolder(); viewHolder.titleText = (TextView)view.findViewById(R.id.title_text); viewHolder.titlePic = (ImageView) view.findViewById(R.id.title_pic); viewHolder.titleDescr = (TextView)view.findViewById(R.id.descr_text); view.setTag(viewHolder); }else{ view = convertView; viewHolder = (ViewHolder) view.getTag(); } Glide.with(getContext()).load(title.getImageUrl()).into(viewHolder.titlePic); viewHolder.titleText.setText(title.getTitle()); viewHolder.titleDescr.setText(title.getDescr()); return view; } public class ViewHolder{ TextView titleText; TextView titleDescr; ImageView titlePic; } }

这里还是一样的老套路,通过convertView 来缓存布局,通过类 ViewHolder 缓存控件实例,这样做,可以节省 50% 的效率,所以还是按照老套路走吧。

接下来就是 Activity 文件 MainActivity.class:

public class MainActivity extends AppCompatActivity { private static final int ITEM_SOCIETY= 1; private static final int ITEM_COUNTY= 2; private static final int ITEM_INTERNATION= 3; private static final int ITEM_FUN= 4; private static final int ITEM_SPORT= 5; private static final int ITEM_NBA= 6; private static final int ITEM_FOOTBALL= 7; private static final int ITEM_TECHNOLOGY= 8; private static final int ITEM_WORK= 9; private static final int ITEM_APPLE= 10; private static final int ITEM_WAR= 11; private static final int ITEM_INTERNET= 12; private static final int ITEM_TREVAL= 13; private static final int ITEM_HEALTH= 14; private static final int ITEM_STRANGE= 15; private static final int ITEM_LOOKER= 16; private static final int ITEM_VR= 17; private static final int ITEM_IT= 18; private List titleList = new ArrayList(); private ListView listView; private TitleAdapter adapter; private NavigationView navigationView; private DrawerLayout drawerLayout; private SwipeRefreshLayout refreshLayout; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); Toolbar toolbar = (Toolbar)findViewById(R.id.tool_bar); setSupportActionBar(toolbar); final ActionBar actionBar = getSupportActionBar(); if (actionBar != null){ actionBar.setDisplayHomeAsUpEnabled(true); actionBar.setHomeAsUpIndicator(R.drawable.ic_menu); } actionBar.setDisplayShowTitleEnabled(true); actionBar.setTitle("社会新闻"); refreshLayout = (SwipeRefreshLayout)findViewById(R.id.swipe_layout); refreshLayout.setColorSchemeColors(getResources().getColor(R.color.colorPrimary)); listView = (ListView)findViewById(R.id.list_view); adapter = new TitleAdapter(this,R.layout.list_view_item, titleList); listView.setAdapter(adapter); listView.setOnItemClickListener(new AdapterView.OnItemClickListener() { Intent intent = new Intent(MainActivity.this, ContentActivity.class); @Override public void onItemClick(AdapterView parent, View view, int position, long id) { Title title = titleList.get(position); intent.putExtra("title",actionBar.getTitle()); intent.putExtra("uri",title.getUri()); startActivity(intent); } }); drawerLayout = (DrawerLayout)findViewById(R.id.drawer_layout); navigationView = (NavigationView)findViewById(R.id.nav_view); navigationView.setCheckedItem(R.id.nav_society); navigationView.setNavigationItemSelectedListener(new NavigationView.OnNavigationItemSelectedListener() { @Override public boolean onNavigationItemSelected(@NonNull MenuItem item) { switch (item.getItemId()){ case R.id.nav_society: handleCurrentPage("社会新闻",ITEM_SOCIETY); break; case R.id.nav_county: handleCurrentPage("国内新闻",ITEM_COUNTY); break; case R.id.nav_internation: handleCurrentPage("国际新闻",ITEM_INTERNATION); break; case R.id.nav_fun: handleCurrentPage("娱乐新闻",ITEM_FUN); break; case R.id.nav_sport: handleCurrentPage("体育新闻",ITEM_SPORT); break; case R.id.nav_nba: handleCurrentPage("NBA新闻",ITEM_NBA); break; case R.id.nav_football: handleCurrentPage("足球新闻",ITEM_FOOTBALL); break; case R.id.nav_technology: handleCurrentPage("科技新闻",ITEM_TECHNOLOGY); break; case R.id.nav_work: handleCurrentPage("创业新闻",ITEM_WORK); break; case R.id.nav_apple: handleCurrentPage("苹果新闻",ITEM_APPLE); break; case R.id.nav_war: handleCurrentPage("军事新闻",ITEM_WAR); break; case R.id.nav_internet: handleCurrentPage("移动互联",ITEM_INTERNET); break; case R.id.nav_travel: handleCurrentPage("旅游资讯",ITEM_TREVAL); break; case R.id.nav_health: handleCurrentPage("健康知识",ITEM_HEALTH); break; case R.id.nav_strange: handleCurrentPage("奇闻异事",ITEM_STRANGE); break; case R.id.nav_looker: handleCurrentPage("美女图片",ITEM_LOOKER); break; case R.id.nav_vr: handleCurrentPage("VR科技",ITEM_VR); break; case R.id.nav_it: handleCurrentPage("IT资讯",ITEM_IT); break; default: break; } drawerLayout.closeDrawers(); return true; } }); refreshLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() { @Override public void onRefresh() { refreshLayout.setRefreshing(true); int itemName = parseString((String)actionBar.getTitle()); requestNew(itemName); } }); requestNew(ITEM_SOCIETY); } /** * 判断是否是当前页面,如果不是则 请求处理数据 */ private void handleCurrentPage(String text, int item){ ActionBar actionBar = getSupportActionBar(); if (!text.equals(actionBar.getTitle().toString())){ actionBar.setTitle(text); requestNew(item); refreshLayout.setRefreshing(true); } } /** * 请求处理数据 */ public void requestNew(int itemName){ // 根据返回到的 URL 链接进行申请和返回数据 String address = response(itemName); // key HttpUtil.sendOkHttpRequest(address, new Callback() { @Override public void onFailure(Call call, IOException e) { runOnUiThread(new Runnable() { @Override public void run() { Toast.makeText(MainActivity.this, "新闻加载失败", Toast.LENGTH_SHORT).show(); } }); } @Override public void onResponse(Call call, Response response) throws IOException { final String responseText = response.body().string(); final NewsList newlist = Utility.parseJsonWithGson(responseText); final int code = newlist.code; final String msg = newlist.msg; if (code == 200){ titleList.clear(); for (News news:newlist.newsList){ Title title = new Title(news.title,news.description,news.picUrl, news.url); titleList.add(title); } runOnUiThread(new Runnable() { @Override public void run() { adapter.notifyDataSetChanged(); listView.setSelection(0); refreshLayout.setRefreshing(false); }; }); }else{ runOnUiThread(new Runnable() { @Override public void run() { Toast.makeText(MainActivity.this, "数据错误返回",Toast.LENGTH_SHORT).show(); refreshLayout.setRefreshing(false); } }); } } }); } /** * 输入不同的类型选项,返回对应的 URL 链接 */ private String response(int itemName){ String address = "https://api.tianapi.com/social/?key=339a8b166f397f008236e596616a5f54&num=50&rand=1"; switch(itemName){ case ITEM_SOCIETY: break; case ITEM_COUNTY: address = address.replaceAll("social","guonei"); break; case ITEM_INTERNATION: address = address.replaceAll("social","world"); break; case ITEM_FUN: address = address.replaceAll("social","huabian"); break; case ITEM_SPORT: address = address.replaceAll("social","tiyu"); break; case ITEM_NBA: address = address.replaceAll("social","nba"); break; case ITEM_FOOTBALL: address = address.replaceAll("social","football"); break; case ITEM_TECHNOLOGY: address = address.replaceAll("social","keji"); break; case ITEM_WORK: address = address.replaceAll("social","startup"); break; case ITEM_APPLE: address = address.replaceAll("social","apple"); break; case ITEM_WAR: address = address.replaceAll("social","military"); break; case ITEM_INTERNET: address = address.replaceAll("social","mobile"); break; case ITEM_TREVAL: address = address.replaceAll("social","travel"); break; case ITEM_HEALTH: address = address.replaceAll("social","health"); break; case ITEM_STRANGE: address = address.replaceAll("social","qiwen"); break; case ITEM_LOOKER: address = address.replaceAll("social","meinv"); break; case ITEM_VR: address = address.replaceAll("social","vr"); break; case ITEM_IT: address = address.replaceAll("social","it"); break; default: } return address; } /** * 通过 actionbar.getTitle() 的参数,返回对应的 ItemName */ private int parseString(String text){ if (text.equals("社会新闻")){ return ITEM_SOCIETY; } if (text.equals("国内新闻")){ return ITEM_COUNTY; } if (text.equals("国际新闻")){ return ITEM_INTERNATION; } if (text.equals("娱乐新闻")){ return ITEM_FUN; } if (text.equals("体育新闻")){ return ITEM_SPORT; } if (text.equals("NBA新闻")){ return ITEM_NBA; } if (text.equals("足球新闻")){ return ITEM_FOOTBALL; } if (text.equals("科技新闻")){ return ITEM_TECHNOLOGY; } if (text.equals("创业新闻")){ return ITEM_WORK; } if (text.equals("苹果新闻")){ return ITEM_APPLE; } if (text.equals("军事新闻")){ return ITEM_WAR; } if (text.equals("移动互联")){ return ITEM_INTERNET; } if (text.equals("旅游资讯")){ return ITEM_TREVAL; } if (text.equals("健康知识")){ return ITEM_HEALTH; } if (text.equals("奇闻异事")){ return ITEM_STRANGE; } if (text.equals("美女图片")){ return ITEM_LOOKER; } if (text.equals("VR科技")){ return ITEM_VR; } if (text.equals("IT资讯")){ return ITEM_IT; } return ITEM_SOCIETY; } /** * 对侧边栏按钮进行处理,打开侧边栏 */ @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()){ case android.R.id.home: drawerLayout.openDrawer(GravityCompat.START); break; default: } return true; } /** * 对返回键进行处理,如果侧边栏打开则关闭侧边栏,否则关闭 activity */ @Override public void onBackPressed() { if(drawerLayout.isDrawerOpen(GravityCompat.START)){ drawerLayout.closeDrawers(); }else{ finish(); } } }

本文的代码量虽然很大,只是比较繁琐,因为需要根据点击的 ITEM 来对不同的 接口地址提出申请,大部分的函数功能都有进行注释,所以略过了

public void requestNew(int itemName){ // 根据返回到的 URL 链接进行申请和返回数据 String address = response(itemName); // key HttpUtil.sendOkHttpRequest(address, new Callback() { @Override public void onFailure(Call call, IOException e) { runOnUiThread(new Runnable() { @Override public void run() { Toast.makeText(MainActivity.this, "新闻加载失败", Toast.LENGTH_SHORT).show(); } }); } @Override public void onResponse(Call call, Response response) throws IOException { final String responseText = response.body().string(); final NewsList newlist = Utility.parseJsonWithGson(responseText); final int code = newlist.code; final String msg = newlist.msg; if (code == 200){ titleList.clear(); for (News news:newlist.newsList){ Title title = new Title(news.title,news.description,news.picUrl, news.url); titleList.add(title); } runOnUiThread(new Runnable() { @Override public void run() { adapter.notifyDataSetChanged(); listView.setSelection(0); refreshLayout.setRefreshing(false); }; }); }else{ runOnUiThread(new Runnable() { @Override public void run() { Toast.makeText(MainActivity.this, "数据错误返回",Toast.LENGTH_SHORT).show(); refreshLayout.setRefreshing(false); } }); } } }); } 外部调用时传入 itemName 参数,通过 response() 函数得到所需要请求数据的地址通过sendOkHttpRequest() 回调方法,在返回数据成功的 onResponse() 方法中使用 parseJsonWithGson() 方法获取对应的实体类将实体类中的数据添加到 Title对应中,将 Title 对象添加到 titleList 中,最后通过 runOnUiThread() 方法,切换到主线程提醒适配器进行数据更新。

至此,主界面的代码逻辑都已经处理好了,还有 ListView 子项布局的点击事件处理:

listView.setOnItemClickListener(new AdapterView.OnItemClickListener() { Intent intent = new Intent(MainActivity.this, ContentActivity.class); @Override public void onItemClick(AdapterView parent, View view, int position, long id) { Title title = titleList.get(position); intent.putExtra("title",actionBar.getTitle()); intent.putExtra("uri",title.getUri()); startActivity(intent); } });

在点击 ListView 子项布局时,会传入 标题栏文本 和 内容 URL

文件 activity_content.xml:

如果了解了 activity_main.xml 的布局,这个布局也就没什么难度了,主要是新增了 WebView 控件,用来显示传入的 URL

文件 ContentActivity.class:

public class ContentActivity extends AppCompatActivity { private WebView webView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_content); Toolbar toolbar = (Toolbar)findViewById(R.id.tool_bar); setSupportActionBar(toolbar); ActionBar actionBar = getSupportActionBar(); if (actionBar != null){ actionBar.setDisplayHomeAsUpEnabled(true); actionBar.setHomeAsUpIndicator(R.drawable.ic_back); } webView = (WebView)findViewById(R.id.web_view); webView.getSettings().setJavaScriptEnabled(true); webView.setWebViewClient(new WebViewClient()); String uri = getIntent().getStringExtra("uri"); String title = getIntent().getStringExtra("title"); actionBar.setDisplayShowTitleEnabled(true); actionBar.setTitle(title); webView.loadUrl(uri); } /** * 点击返回键做了处理 */ @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()){ case android.R.id.home: finish(); break; default: } return true; } }

显示传入的 URL网址

最后

到这里就结束了 ? 如果你认为都结束了,那你可以就需要面对打开应用之后马上闪退的情况了…….权限

我们还没有对权限进行申请,在 AndroidManifest 文件中添加声明:

不过个人还是建议把 权限的考虑放在最先的优先级,毕竟养成这个习惯,就可以专注于代码的 bug…………………..

运行界面

这里写图片描述 (主界面)

这里写图片描述 (侧边栏)

这里写图片描述 (内容界面)

运行 GIF

这里写图片描述 (由于大小限制,所以就只能传这么大了)

完整代码下载地址

https://github.com/lentitude/NewsMD



【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有