问题

一个人如何开发一款 App?

回答
开发一款App,听起来高大上,但拆解开来,其实就是一步一步把脑子里的想法变成手机上能用的东西。这过程说复杂也复杂,说简单也简单,关键是找对方向,持之以恒。下面我就一点点跟你掰扯掰扯,怎么把一个App从无到有做出来。

第一步:你的“点子”在哪里?—— 构思与需求分析

别急着上手写代码,你得先知道自己要做什么。

为什么要做这个App? 是为了解决某个痛点?满足某种需求?还是纯粹觉得好玩、有创意?这个“为什么”很重要,它会贯穿你整个开发过程,让你不至于迷失方向。
目标用户是谁? 是学生党?上班族?特定爱好者?了解你的用户,你才知道他们需要什么,喜欢什么。比如,给老年人设计的App,界面就要简单明了,字体要大。
这个App的核心功能是什么? 不要一开始就想把所有牛鬼蛇神都塞进去。想想最最核心、最能解决问题的那个功能。就像淘宝,最开始可能就是个卖东西的地方,而不是现在这么庞大。
市面上有类似的产品吗? 别怕竞争,看看别人是怎么做的,他们做得好的地方在哪里,做得不好的地方又是什么。你能否做出差异化?能否做得更好?

这一步,你可以多跟朋友聊聊,多做做市场调研,甚至可以画画草图,把你的想法勾勒出来。这时候,“原型设计工具”就派上用场了,像Figma、Sketch、Axure之类的,它们能帮你把零散的想法变成可视化的流程和界面草图。这就像是建筑师在动工前画蓝图,至关重要。

第二步:画出骨架和血肉—— UI/UX设计

用户体验(UX)和用户界面(UI)设计是App的“颜值”和“大脑”。

用户体验(UX): 这是关于用户使用你的App的感受。它流畅吗?直观吗?容易上手吗?有没有让人抓狂的地方?一个好的UX设计,用户会觉得用起来“顺手”,就像用惯了的老朋友。这涉及到信息架构(内容怎么组织)、交互流程(用户一步步操作的路径)等等。
用户界面(UI): 这是App的“长相”。颜色、字体、按钮、图标,每一个元素都要精心设计。它得符合你的App定位,也得吸引你的目标用户。比如,游戏App可能色彩鲜艳活泼,而办公App则倾向于简洁专业。

现在有很多UI/UX设计软件,你可以学习使用它们。很多时候,你需要自己或者找一个设计师来完成这一步。别小看这一步,用户第一次打开你的App,给他们的第一印象就来自这里。

第三步:选择你的“武器”—— 技术选型

准备好蓝图和样子了,现在要决定用什么工具把它们造出来。

原生开发(Native Development):
iOS平台: 主要使用Swift或ObjectiveC语言,配合Xcode开发工具。优点是性能最好,能完美调用设备所有功能,用户体验最顺畅。缺点是只能在苹果设备上运行,开发成本高(需要Mac电脑,并且需要学习苹果的开发体系)。
Android平台: 主要使用Java或Kotlin语言,配合Android Studio开发工具。优点同样是性能优越,功能调用全面。缺点同样是只能在安卓设备上运行。
跨平台开发(CrossPlatform Development):
React Native: 使用JavaScript和React框架。一次编写,多平台运行(iOS和Android)。开发效率高,适合逻辑不那么复杂的App。
Flutter: 由Google推出,使用Dart语言。性能接近原生,UI表现力强,开发速度也很快。
Xamarin: 由微软推出,使用C语言。同样可以实现跨平台。

怎么选?

如果预算充足,对性能要求极高,且想做到最极致的用户体验: 考虑原生开发。
如果想快速推向市场,降低开发成本,并且App功能相对简单: 跨平台开发是个不错的选择。

第四步:开始“砌砖盖瓦”—— 代码开发

这一步就是把设计稿和需求变成实际的代码。

前端开发(Clientside): 就是你在手机上看到的、能触摸操作的部分。这部分需要根据你选择的技术栈来编写。
原生开发: Swift/ObjectiveC (iOS), Java/Kotlin (Android)。
跨平台开发: JavaScript/React (React Native), Dart (Flutter), C (Xamarin)。
后端开发(Serverside): 如果你的App需要存储数据、处理用户登录、发送通知等,那么你就需要一个后端。
后端语言: Python (Django/Flask), Java (Spring), Node.js (Express), Ruby (Rails) 等等。
数据库: MySQL, PostgreSQL, MongoDB, Redis 等等。
服务器: AWS, Azure, Google Cloud, 阿里云等。
API接口: 后端和前端之间沟通的桥梁,非常重要。

第五步:让它“活起来”—— 测试

别以为写完代码就没事了,测试是必不可少的环节。

单元测试: 测试代码的最小单元,确保每个功能模块都能正常工作。
集成测试: 测试不同模块组合在一起时是否正常。
UI测试: 检查界面是否符合设计,操作是否流畅。
兼容性测试: 在不同设备、不同系统版本上测试,确保App的稳定性。
压力测试: 在大量用户同时使用的情况下,看App的性能如何。

你可以自己进行测试,也可以请专业的测试人员来完成。在早期,也可以邀请一些种子用户来试用,收集他们的反馈。

第六步:上线让大家用—— 发布与部署

一切就绪,就可以把它打包好,送往应用商店了。

应用商店:
iOS: Apple App Store。需要注册Apple开发者账号(付费),遵循苹果的审核规则。
Android: Google Play Store。注册Google Play开发者账号(一次性付费),审核相对宽松一些。国内还有其他应用商店,如华为应用市场、小米应用商店等,需要分开提交。
服务器部署: 如果你的App有后端,需要将后端代码部署到服务器上,确保可以稳定运行。

第七步:让它不断成长—— 维护与迭代

App上线只是一个开始,真正的工作还在后面。

用户反馈收集: 关注用户评论、论坛反馈,了解用户在使用过程中遇到的问题和建议。
Bug修复: 及时修复用户报告的Bug,保证App的稳定性。
功能更新: 根据用户需求和市场变化,不断优化现有功能,增加新功能。
性能优化: 随着用户量增加,可能需要优化服务器性能,提升App的响应速度。
市场推广: 让更多人知道你的App,可以考虑广告、社交媒体推广等方式。

开发过程中需要注意的一些“小细节”:

版本控制: 使用Git这样的版本控制系统,可以记录代码的每一次修改,方便回溯和协作。
学习能力: 技术更新很快,你需要保持持续学习的热情。
沟通协作: 如果是团队开发,良好的沟通是成功的关键。
心态调整: 开发过程中会遇到各种困难和挫折,保持积极的心态很重要。

开发一款App,就像是经历一次“创业”。从一个想法到一个可以被成千上万用户使用的产品,这个过程充满了挑战,但也充满了成就感。一步一个脚印,总能把你的想法变成现实。祝你成功!

网友意见

user avatar

来了。手把手教你做一个吧。

前言

从零开始,手把手带你实现一个「专注睡前的 APP」。睡觉之前如果能有一个 APP,能让我们写一写这一天的见闻或者心得,同时又能看一会段子、瞄一会好看的妹子,放松一下疲惫的身心那该多好,这也是我完成这个 APP 的原因。APP 的全部代码我已经分享到 Github 上了,需要的直接 点击这里,如果喜欢的话,麻烦给个赞,谢谢啦。

在开始写正文之前,先来一波效果的展示,看看五天过后我们能实现怎样的效果


本次的教程分为 5 天,内容分别为:

  • Day one,准备
  • 功能需求
  • 可行性分析
  • Day two,UI 及公共类的封装
    • 界面的设计及实现
    • 公共类的实现
  • Day three,日记模块
    • 日记的展示
    • 悬浮菜单的实现
    • 日记增删改的实现
  • Day four,妹子模块
    • 图片的获取
    • 图片的展示
    • 详情页面的展示
  • Day five,段子模块
    • 段子数据的获取
    • 段子的显示

Day one

俗话说,万事开头难,在开始敲代码之前,先让我们来做一些必要的准备,这样才能事半功倍嘛!

一、功能需求

既然要做一个 APP,那我们首先还是得把 APP 的功能都列出来,有了方向才能更好的努力,因为我想做的是一个专门给睡觉前用的 APP,所以我觉得应该有以下的这些功能

  • 1、日记的增删改
  • 2、显示一些有趣好玩的段子
  • 3、瀑布流展示漂亮的妹子
  • 4、保存日记的内容以及缓存妹子图片

虽然说需求不多,但是却要运用到网络、数据存储、图片缓存、UI 设计等内容,相信整个 APP 完成下来,必定能巩固我们的 Android 基础。

二、可行性分析

我们这个 APP 主要有三个模块,日记模块主要是运用到了数据库的知识,难度不大。但是,段子模块和妹子模块的数据要从哪来,这便是要好好考虑的了。幸好现在是个开源的时代,很多的数据,网上已经开源出来了。

我们先来看一下数据的内容

       group: {         text: "教授在河边,常常看到两只龟,缩着一动不动。有天忍不住好奇,问一农               民:这两只乌龟在干吗?农民说:他们在pk。教授不解地问:动都没动过p什么             k。老农说:他们在比谁寿命长。教授说:可是壳上有甲骨文的那只,早就死了埃         这时,另一只猛然探出头来骂到:md,死了也不吭一声!有甲骨文的那只也伸         出头来:“专家说啥你信啥1",          user: {               user_id: 4669064575,                name: "馒头啊",                avatar_url: "http://p3.pstatp.com/medium/6237/7969345239", },            content: "教授在河边,常常看到两只龟,缩着一动不动。有天忍不住好奇,问                    一农民:这两只乌龟在干吗?农民说:他们在pk。教授不解地问:动都没动过            p什么k。老农说:他们在比谁寿命长。教授说:可是壳上有甲骨文的那只,早            就死了埃这时,另一只猛然探出头来骂到:md,死了也不吭一声!有甲骨文            的那只也伸出头来:“专家说啥你信啥1", ...   }     


       {           id: "56cc6d1d421aa95caa7076df",            type: "福利",            url: "http://ww1.sinaimg.cn/large/7a8aed7bgw1esxxi1vbq0j20qo0hstcu.jpg",            used: true,            who: "张涵宇"  }     

上面那两段代码分别是段子和妹子模块的 json 类型的数据,我已经将一些没用的字段去掉了。剩下的都是我们想要的数据。可以看到段子数据中,有着段子的内容,以及发布者的头像和名字。而妹子数据中有着图片的 url、id、以及图片的类型。相信有了这么丰富的数据,我们想要完成这个 APP 也是有底气了。

Day two

一、界面的设计及实现

既然我们想要完成一个好看的 APP,那么好看的界面便是必不可少的,这里我强烈推荐 APP 界面的设计必须尽量遵从 Google 提出的 Material Design,在这个推荐一个能够让我们实现 Material Design 变得更加简单的网站 material design palette,我这个 APP 的配色就是用这个网站完成的,贴几张图片,让你感受一下它的强大。

借助这个网站便能让我们完成 APP 的配色以及图标的收集,为下一步功能的实现,先打好了基础,至于界面的设计就仁者见仁智者见智了,篇幅有限,我就不多讲了。

APP 的最终设计效果如下:

二、公共类的实现

因为这个项目有三个模块,有一些东西其实是可以通用的,如果我们先把这些能够通用的东西,封装起来,供给所有的模块调用的话,相信会大大提高我们的开发效率。

1、网络工具类的封装

这个 APP 中,很多地方都要用到网络请求,因此也就很有必要将网络请求封装起来,因为这个 APP 的规模比较小,因此我选择了 Volley 这个网络框架作为我们网络请求库,把网络请求封装起来,哪个地方需要,调用一下就行了。对于网络请求,我觉得每个程序员都该懂点 HTTP,这里附上一篇有关 HTTP 的文章程序员都该懂点 HTTP。

先让我们来写个将网络请求进行回调的接口

       public interface VolleyResponseCallback {     void onSuccess(String response);     void onError(VolleyError error); }     

然后将网络请求封装起来

       public class VolleyHelper {      /**      * 用于发送 Get 请求的封装方法      *      * @param context Activity 的实例      * @param url 请求的地址      * @param callback 用于网络回调的接口      */     public static void sendHttpGet(Context context, String url, final VolleyResponseCallback callback){         RequestQueue requestQueue = Volley.newRequestQueue(context);         StringRequest stringRequest = new StringRequest(url                 , new Response.Listener<String>() {             @Override             public void onResponse(String s) {                 callback.onSuccess(s);             }         }, new Response.ErrorListener() {             @Override             public void onErrorResponse(VolleyError error) {                 callback.onError(error);             }         });         requestQueue.add(stringRequest);     }  }     

2、Json 解析的帮助类

因为我们这个 APP 中,获取到的数据都是 Json 格式的,因此也就有必要将有关的 Json 解析封装成一个工具类,传入一个 String 类型的数据,直接得到数据实体类的 List。

       public class CommonParser {      /**      * 用来解析列表性的JSON数据      * 如:      * {"success":true,"fileList":[{"filename":"文件名1","fileSize":"文件大小1"},      * {"filename":"文件名2","fileSize":"文件大小2"}]}      *      * @param result     网络返回来的JSON数据   比如:上面的整串数据      * @param successKey 判断网络是否成功的字段  比如:上面的success字段      * @param arrKey     列表的字段            比如:上面的fileList字段      * @param clazz      需要解析成的Bean类型      * @param <T>        需要解析成的Bean类型      * @return      */     public static <T> List<T> parseForList(String result, String successKey, String arrKey, Class<T> clazz) {         List<T> list = new ArrayList<>();         JSONObject rootJsonObject = null;         try {             rootJsonObject = new JSONObject(result);             if (rootJsonObject.getBoolean(successKey)) {                 JSONArray rootJsonArray = rootJsonObject.getJSONArray(arrKey);                 Gson g = new Gson();                 for (int i = 0; i < rootJsonArray.length(); i++) {                     T t = g.fromJson(rootJsonArray.getJSONObject(i).toString(), clazz);                     list.add(t);                 }             }         } catch (JSONException e) {             e.printStackTrace();         }         return list;     } }     

3、HomeActivity(主页面)的封装

主页面我用的是 TabLayout + ViewPager + Fragment,也是现在主流 APP 主页面的显示方式。主界面底部是我们三个模块的图标和名称,通过左右滑动能实现界面的跳转。

底部图标的实体类 CommonTabBean

       public class CommonTabBean implements CustomTabEntity{      private int selectedIcon;     private int unselectedIcon;     private String title;      public CommonTabBean(String title){         this.title = title;     }      public CommonTabBean(String title, int selectedIcon, int unselectedIcon) {         this.title = title;         this.selectedIcon = selectedIcon;         this.unselectedIcon = unselectedIcon;     }      @Override     public String getTabTitle() {         return title;     }      @Override     public int getTabSelectedIcon() {         return selectedIcon;     }      @Override     public int getTabUnselectedIcon() {         return unselectedIcon;     } }     

ViewPager + Fragment 通用的 Adapter

       public class CommonPagerAdapter extends FragmentPagerAdapter {      private List<Fragment> mFragments;      public CommonPagerAdapter(FragmentManager fragmentManager, List<Fragment> mFragments){         super(fragmentManager);         this.mFragments = mFragments;     }      @Override     public Fragment getItem(int position) {         return mFragments.get(position);     }      @Override     public int getCount() {         return mFragments.size();     } }     

Day three

关于日记模块的实现,其实我是复用了以前写过的一个日记 APP,具体的思路和做法,可以参考我的这篇文章 Android 一款十分简洁、优雅的日记 APP

Day four

一、图片的获取

1、根据返回的数据来编写图片的实体类

       public class MeiziBean {      @SerializedName("_id")     private String id;     @SerializedName("url")     private String imageUrl;     @SerializedName("who")     private String who;      public String getId() {         return id;     }      public void setId(String id) {         this.id = id;     }      public String getImageUrl() {         return imageUrl;     }      public MeiziBean(String imageUrl){         this.imageUrl = imageUrl;     } }     

2、图片的展示

可以看到我是用瀑布流的方式来实现图片的展示,效果还不错,但其实实现起来也是很简单的

先写个图片的布局作为 RecyclerView 的 Item

       <android.support.v7.widget.CardView     xmlns:android="http://schemas.android.com/apk/res/android"     android:layout_width="match_parent"     android:layout_height="wrap_content">                  <ImageView                     android:id="@+id/item_iv_meizi"                     android:layout_width="match_parent"                     android:layout_height="wrap_content"                     android:layout_centerHorizontal="true"                     android:layout_centerVertical="true"                     /> </android.support.v7.widget.CardView>     

可以看到我在 ImageView 的外面加了一个 CardView,这个一种卡片式布局,能让图片看起来就像一张卡片一样,相当的优雅、美观。

接着编写 Adapter,将数据和界面进行绑定

       public class MeiziAdapter extends RecyclerView.Adapter<MeiziAdapter.MeiziViewHolder> {      private List<MeiziBean> mMeiziBeanList;     private Fragment mFragment;      public MeiziAdapter(List<MeiziBean> mMeiziBeanList, Fragment mFragment){         this.mMeiziBeanList = mMeiziBeanList;         this.mFragment = mFragment;     }      @Override     public MeiziViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {         View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_meizi, null);         return new MeiziViewHolder(view);     }      @Override     public void onBindViewHolder(MeiziViewHolder holder, final int position) {          Glide.with(mFragment)                 .load(mMeiziBeanList.get(position).getImageUrl())                 .fitCenter()                 .dontAnimate()                 .diskCacheStrategy(DiskCacheStrategy.ALL)                 .into(holder.mIvMeizi);          holder.mIvMeizi.setOnClickListener(new View.OnClickListener() {             @Override             public void onClick(View v) {                 ArrayList<String> resultList = new ArrayList<String>();                 for (MeiziBean meiziBean : mMeiziBeanList) {                     resultList.add(meiziBean.getImageUrl());                 }                 DetailActivity.startActivity(mFragment.getActivity(), resultList, position);              }         });      }      @Override     public int getItemCount() {         if(mMeiziBeanList.size() > 0){             return mMeiziBeanList.size();         }         return 0;     }      public static class MeiziViewHolder extends RecyclerView.ViewHolder{          ImageView mIvMeizi;          public MeiziViewHolder(View itemView) {             super(itemView);             mIvMeizi = (ImageView) itemView.findViewById(R.id.item_iv_meizi);         }     } }     

最后在 Fragment 进行数据的获取,以及布局的初始化就行了

       public class MeiziFragment extends Fragment {      ......      @Nullable     @Override     public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {         View view = inflater.inflate(R.layout.fragment_meizi, container, false);         ButterKnife.bind(this, view);         initView();         refreshMeizi();         return view;     }      /**      * 刷新当前界面      */     private void refreshMeizi() {         mRefresh.setColorSchemeResources(R.color.colorPrimary);         mRefresh.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {             @Override             public void onRefresh() {                 initView();                 mRefresh.setRefreshing(false);             }         });     }      private void initView() {         VolleyHelper.sendHttpGet(getActivity(), MeiziApi.getMeiziApi(), new VolleyResponseCallback() {             @Override             public void onSuccess(String s) {                 response = s;                 meiziBeanList = GsonHelper.getMeiziBean(response);                 mRvShowMeizi.setLayoutManager(new StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL));                 Collections.shuffle(meiziBeanList);                 mRvShowMeizi.setAdapter(new MeiziAdapter(meiziBeanList, MeiziFragment.this));             }              @Override             public void onError(VolleyError error) {                 Logger.d(error);             }         });     }     

3、详情页面的展示

干巴巴的,整个模块只能显示妹子的图片怎么行呢!!!怎么着也得能查看大图,根据手势放大缩小,以及浏览下一张图片才行嘛,说干就干。

因为图片需要有根据手势来放大缩小的功能,因此我便想到了 PhotoView,这是网上一个大神写的,继承自 ImageView 的一个自定义控件。图片加载我用的是

Glide,如果没了解过这个库的,强烈推荐,一行代码就能搞定图片加载,你确定不研究一下。

       public class DetailFragment extends Fragment {      public static DetailFragment newInstance(String imageUrl) {         DetailFragment fragment = new DetailFragment();         Bundle bundle = new Bundle();         bundle.putString(IMAGE_URL, imageUrl);         fragment.setArguments(bundle);         return fragment;     }      @Nullable     @Override     public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {         View view = inflater.inflate(R.layout.fragment_detail, container, false);         ButterKnife.bind(this, view);         Bundle bundle = getArguments();         String imageUrl = bundle.getString(IMAGE_URL);         Glide.with(this).load(imageUrl).into(mPvShowPhoto);         mPvShowPhoto.setOnPhotoTapListener(new PhotoViewAttacher.OnPhotoTapListener() {             @Override             public void onPhotoTap(View view, float v, float v1) {                 getActivity().finish();             }              @Override             public void onOutsidePhotoTap() {              }         });         return view;     } }     

Day five

一、段子数据的获取

段子数据的获取其实跟妹子模块的方法基本一样

先编写实体类

       public class DuanziBean {      @SerializedName("group")     private GroupBean groupBean;     private String type;      public GroupBean getGroupBean() {         return groupBean;     }      public void setGroupBean(GroupBean groupBean) {         this.groupBean = groupBean;     }      public String getType() {         return type;     }      public void setType(String type) {         this.type = type;     }  } public class GroupBean {      private String text;     private long id;     private UserBean user;      public String getText() {         return text;     }      public long getId() {         return id;     }      public UserBean getUser() {         return user;     }      public static class UserBean {          private long user_id;         private String name;         private String avatar_url;          public String getName() {             return name;         }          public String getAvatar_url() {             return avatar_url;         }      } }     

写好实体类之后,使用我们之前已经封装好的网络请求工具以及解析工具,便能将返回的数据,解析成一个包含段子实体类的 List。

二、段子的显示

老规矩,先写个 RecyclerView 的 Item

       <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"               android:layout_width="match_parent"               android:layout_height="match_parent"               android:orientation="vertical"     >      <LinearLayout         android:layout_width="match_parent"         android:layout_height="40dp"         android:paddingLeft="8dp"         >          <de.hdodenhof.circleimageview.CircleImageView             android:id="@+id/duanzi_civ_avatar"             android:layout_width="24dp"             android:layout_height="24dp"             android:src="@drawable/avatar"             android:layout_gravity="center"             />          <TextView             android:id="@+id/duanzi_tv_author"             android:paddingLeft="8dp"             android:paddingStart="8dp"             android:layout_width="match_parent"             android:layout_height="16dp"             android:text="DeveloperHaoz"             android:layout_gravity="center_vertical"             />      </LinearLayout>      <TextView         android:id="@+id/duanzi_tv_content"         android:layout_width="match_parent"         android:layout_height="wrap_content"         android:paddingBottom="10dp"         android:paddingLeft="40dp"         android:paddingRight="10dp"         android:text=""         />     <include layout="@layout/layout_app_divide"/>  </LinearLayout>     

然后编写将数据和界面进行绑定的 Adapter

       public class DuanziAdapter extends RecyclerView.Adapter<DuanziAdapter.DuanziViewHolder>{      private Fragment mFragment;     private List<DuanziBean> mDuanziBeanList;      public DuanziAdapter(Fragment fragment, List<DuanziBean> duanziBeanList){         this.mFragment = fragment;         this.mDuanziBeanList = duanziBeanList;     }      @Override     public DuanziViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {         View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_duanzi, null);         return new DuanziViewHolder(view);     }      @Override     public void onBindViewHolder(DuanziViewHolder holder, int position) {         try {             DuanziBean duanziBean = mDuanziBeanList.get(position);             Glide.with(mFragment).load(duanziBean.getGroupBean().getUser().getAvatar_url()).into(holder.mCivAvatar);             holder.mTvContent.setText(duanziBean.getGroupBean().getText());             holder.mTvAuthor.setText(duanziBean.getGroupBean().getUser().getName());         } catch (Exception e) {             e.printStackTrace();         }     }      @Override     public int getItemCount() {         return mDuanziBeanList.size();     }      public static class DuanziViewHolder extends RecyclerView.ViewHolder{          private CircleImageView mCivAvatar;         private TextView mTvAuthor;         private TextView mTvContent;          public DuanziViewHolder(View itemView) {             super(itemView);             mCivAvatar = (CircleImageView) itemView.findViewById(R.id.duanzi_civ_avatar);             mTvAuthor = (TextView) itemView.findViewById(R.id.duanzi_tv_author);             mTvContent = (TextView) itemView.findViewById(R.id.duanzi_tv_content);         }     }  }     

最后段子页面中进行数据和获取以及界面的初始化

       public class DuanziFragment extends Fragment {      @Nullable     @Override     public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {         View view = inflater.inflate(R.layout.fragment_duanzi, container, false);         ButterKnife.bind(this, view);         initView();         initRefresh();         return view;     }      private void initRefresh() {         mRefresh.setColorSchemeResources(R.color.colorPrimary);         mRefresh.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {             @Override             public void onRefresh() {                 initView();                 mRefresh.setRefreshing(false);             }         });     }      private void initView() {         VolleyHelper.sendHttpGet(getActivity(), DuanziApi.GET_DUANZI, new VolleyResponseCallback() {             @Override             public void onSuccess(String response) {                 List<DuanziBean> mDuanziBeanList = GsonHelper.getDuanziBeanList(response);                 mDuanziBeanList.remove(3);                 mRvShowDuanzi.setLayoutManager(new LinearLayoutManager(getActivity()));                 mRvShowDuanzi.setAdapter(new DuanziAdapter(DuanziFragment.this, mDuanziBeanList));             }              @Override             public void onError(VolleyError error) {                 Logger.d(error);             }         });     }  }     

作者:developerHaoz

来源:慕课网

本文原创发布于慕课网 ,转载请注明出处,谢谢合作

user avatar

说一下我个人开发「小决定」app 的经历吧。

起因

一开始是因为我想找一个帮助做决定的 app。我的想法是可以帮我系统列出待选择事物的优缺点,并且对各个优缺点设置权重最终算出一个分数来作比较。机缘巧合之下,被我搜到一个用转盘随机做决定的 app。具体是哪一个 app 我已然不记得了,只记得当时觉得这个 app 做得实在太烂了,让老夫做一个完爆它。让我决定做这个 app 的另一个很重要的原因是:功能足够简单,不用耗费太多时间。

说做就做。

因为我买了苹果 99 刀的开发者账号,所以我的目标很简单,把这 99 刀赚回来。

设计

首先要做的是,考察市面上同类的 app。

  • 找出目前业界做得最好的 app,搜一下它的排名,如果排名过低,说明这个类型的 app 实在太小众,不值得去做。
  • 可以参(chao)考(xi)现有同类 app 的设计。通过亲自体验,找出各个 app 的亮点和缺点取长补短。

从 App Store 就可以获取很多信息:

  • 从详情页里可以看到 app 的功能介绍。
  • 从发布历史可以看到一路踩了多少坑。
  • 从用户评价里可以听到用户真实的声音。

在此基础上再来规划自己的功能会更加合理。

拿随机转盘这个类型来说,市面上做得最好的是 ‎Decide Now,排名在各个国家都很高,尤其是美国。首页的交互和视觉做得非常好,但它存在三个问题:

  • 轮盘选项设定起来非常麻烦,除了首页之外交互非常混乱。
  • 定价策略有问题。它同时提供一个免费的 lite 版本,但是却不支持自定义选项,导致评分很低。而收费版本售价 2 刀,有点偏高。
  • 在排除已选项后,被排除选项两侧的选项被转到的概率更大(功能设计问题)。

所以还存在抢占市场的机会。获取 app 排名信息的渠道有很多,国外有著名的 App Annie,国内蝉大师七麦等。

然后就要为 app 起名了。之所以叫「小决定」是因为它只能用来做不重要的小决定。它的定位很简单:拯救选择困难症患者。除此之外还可以做很多:决定谁来买单、谁来做家务、上课点名、真心话大冒险等需要随机选择的地方

接下来就开始设计功能。一开始只实现最核心的功能,转盘本身和自定义转盘,后面再慢慢增加新功能。事实上有一些功能的使用频率非常低,属于我意淫出来的需求,所以还是要让用户反馈和埋点数据来说话。

平时多体验不同的 app 并且细致观察,设计出来的交互就不会太差。

视觉由于没有专门的设计师,只能自己瞎画了,Sketch 的基本操作还是得会一些的。这里有几个资源可以参考:

最终的 UI 是这样的:


开发

由于都是最基本的功能加上一点点动画,所以技术上没什么难度。有几个可以利用的免费资源:

  • Google 的 Firebase:其中的 Analytics 功能非常强大,最重要的是国内可用。
  • Apple 的 CloudKit:不用担心数据存储的问题。

国际化

相比于美国等发达国家,国人的购买力还是有很大的差距的。因此想赚回票价,就必须重视海外市场。

英文部分可以自己搞定,对于其他语言,我对谷歌翻译的质量很不放心,于是找了有道的付费人工翻译。但事实证明,有道的人工翻译对于这种细分的领域也还是不够专业,我的一个略懂日语的同事就看出了翻译结果的很多问题,不过总是比机器翻译强吧。国际化是一件非常麻烦的事情,哪怕是对于这么简单的一个 app。你需要制作 App Store 截图、翻译文案、产品介绍、关键词。还得避免前面 Decide Now! 被翻译成「迪賽腦!」的悲剧。:]

后来我在 app 中增加了 Be a Translator 来发动用户帮我翻译,目前已经有 5 位用户帮我翻译了。

定价

接下来是一个很关键的环节:定价。目前主流的定价策略有几种:

  • 完全免费:我还要实现我的小目标,pass。
  • 免费 + 内购:内购去广告或解锁功能。好处是用户下载的门槛更低,坏处几乎没有。
  • 收费 + 免费 lite 版:好处是在 App Store 上可以占两个坑位,坏处是维护起来麻烦,并且容易让用户困惑。

在我看来,对于小工具类型的 app,第二种是更合理的收费方法。对于应用内的广告,要想得到高的收益必须要极大损害用户体验,并且会让 app 变得丑陋无比,因此我选择了免费 + 内购解锁功能。虽然这种收费方法被美国佬吐槽很多(美国市场上绝大部分的低分都是在吐槽需要付费解锁功能,相比之下中国市场却没有一个人吐槽这件事,也许是文化差异吧),但我仍然相信这是最好的策略。

在 app 上架之后,还可以动态调节价格来实现利益最大化。

上架

上架的时候还出现了一个小插曲。实际这个 app 在年前就已经提交审核了,但是一直过了二十多天,一直都都在审核中,因为在过春节所以我也没太在意。年后我重新提交审核,又等了十几天,我忍无可忍在 iTunes Connect 上多次反馈,终于通过审核。

提交审核的时候需要输入很多信息,其中的关键字和产品介绍非常重要。要知道一个小众 app 绝大部分的用户都来自于 App Store 搜索,因此 ASO 很关键。我综合了其他同类 app 的关键词和介绍,这些信息都可以在各大 ASO 网站上找到。

推广

个人开发的 app 还是有一些免费的渠道可以用来推广的:

  • 可以在各大论坛(如威锋)发帖,赠送几个兑换码就会有很多人关注。
  • 国内有很多 app 推荐网站,如少数派、小众软件、最美应用等等,个人开发者可以提交自己的作品。只要 app 还不错,他们会免费写评测来帮你宣传。如果被选中,推广效果还是很不错的。国外类似的网站大多需要收费评测。

如果有主播愿意在视频里使用你的作品,宣传效果就会非常棒。曾经 YouTube 上有一个我并不认识的主播使用小决定来玩 slime challenge(需科学上网),那段时间下载量暴增。

成果

在没有任何国外推广资源的情况下,小决定在美国市场的「娱乐(免费)」榜单中最高排到了 20 名。

日活也一度达到 37k 多,已经大大超出了我的预期。

当然,赚回 99 刀的目标也很快完成了。因为开发抛硬币的功能,还现学了一点用 Blender 建 3D 模型。

我还因此认识了几个新朋友,但最最重要的还是那种无拘无束去创造的成就感。

类似的话题

  • 回答
    开发一款App,听起来高大上,但拆解开来,其实就是一步一步把脑子里的想法变成手机上能用的东西。这过程说复杂也复杂,说简单也简单,关键是找对方向,持之以恒。下面我就一点点跟你掰扯掰扯,怎么把一个App从无到有做出来。第一步:你的“点子”在哪里?—— 构思与需求分析别急着上手写代码,你得先知道自己要做什.............
  • 回答
    想找程序员免费帮你开发APP?这可真是个挑战,不过也不是完全没可能,但你需要转换一下思路,并且做好充分的准备。毕竟,程序员也是要吃饭,要投入时间和精力的。与其说是“免费开发”,不如说是“以其他方式为程序员提供价值,换取他们的开发服务”。直接找程序员说“给我免费做一个APP”,成功率非常低,而且也不太.............
  • 回答
    嘿,哥们儿姐们儿,咱们今天不聊代码,不聊设计稿,也不聊 KPI 了!咱们齐心协力,把这老北京炸酱面馆搞得风生水起!想想看,香喷喷的炸酱面,配上爽口的黄瓜丝、豆芽菜,再来点蒜泥,一口下去,那滋味,绝了!咱们的使命,就是把这份绝了的滋味,带给城里所有爱面的人!我,作为咱们这个小团队的“总指挥”,我的核心.............
  • 回答
    高考结束了。准确地说,是那个属于我的、那个沉重了十八年的“高考”结束了。当最后一门科目的铃声响起,我的笔尖还在试卷上留下最后的墨迹,胸腔里却像是被什么东西压住了,闷得透不过气。我甚至没来得及看一眼卷子,只是茫然地将它合上,然后就那样,呆坐在椅子上。周围的嘈杂声,监考老师例行的“请考生将试卷和答题卡交.............
  • 回答
    城外下了一场雪,死了一个人。这场雪来得有些突然,前一日还是萧瑟秋风,秋叶零落,次日晨起,便是银装素裹,天地茫茫。漫天的雪花细密而洁白,无声无息地覆盖了这座古老的小镇,也淹没了所有昨日的痕迹。死去的那个是王寡妇的儿子,李二壮。没人记得他究竟是谁,只知道他是个沉默寡言的年轻人,瘦削的身板,常年穿着一件洗.............
  • 回答
    这世界上只剩下最后一个人,这时门口传来了敲门声。伊恩在黑暗中猛地惊醒,胸腔里剧烈地跳动着,仿佛要挣脱胸骨的束缚。他的心跳声在死寂的屋子里回荡,比任何雷鸣都更响亮。又过了几秒,他才敢细细地分辨,那是一种低沉而有规律的“咚…咚…咚…”。敲门声。这不可能。伊恩紧紧抓着身上粗糙的毛毯,指甲几乎陷进掌心。这世.............
  • 回答
    夜幕像一张被浸透了墨水的幕布,缓缓拉开,城市的霓虹灯在湿漉漉的窗玻璃上晕染开模糊的光斑。我蜷在沙发里,手里捧着一杯早已凉透的茶,无聊地看着对面单元楼的灯光。说是无聊,其实是有点心烦意乱。白天工作上的不顺,像一团缠绕不清的毛线,此刻在我脑子里乱蓬蓬的。对面三层的一户窗户,灯光异常昏暗,只有一盏台灯勉强.............
  • 回答
    “我去洗澡了。”我把这句话说完,看着对面那个男人一脸诧异的表情。他叫亚当,一个我刚认识不到半小时的陌生人。我们在街角的咖啡馆里偶然搭讪,聊得倒是挺投缘,从古老的哲学谈到最新的电影。就在我以为今晚可能会有点什么实质性的进展时,我这么说了。亚当的眉毛微微抬起,眼神里闪过一丝困惑,又很快被另一种情绪取代,.............
  • 回答
    我的猫,名叫“影子”,并非一只普通的猫。牠是一只通体乌黑的暹罗猫,皮毛光滑得如同黑曜石,一双碧绿的眼睛在黑暗中闪烁着一种异样的光芒。我总是觉得牠的眼神里藏着许多我无法理解的东西,仿佛洞悉世事,又带着一丝我从未在其他猫身上见过的孤傲。那天,我坐在书房里,窗外是漆黑的夜,雨点噼里啪啦地敲打着玻璃,如同无.............
  • 回答
    你好,我非常理解你现在的心情。这种极度的怕黑怕鬼的恐惧,确实会给生活带来很大的困扰,甚至影响到睡眠质量,让你无法独自入睡。这不是你的错,很多人在成长过程中都会遇到类似的恐惧,关键在于我们如何去面对和克服它。要解决这个问题,我们需要一个系统性的方法,从心理层面到行为层面,一步步来。别急,我们慢慢来,把.............
  • 回答
    范可新公开表示9岁就开始训练,并将王濛视为偶像,这是一个非常典型的榜样力量体现。偶像对个人成长的作用是多方面且深刻的,尤其是在青少年时期,这种影响更是显著。1. 树立榜样,明确目标与方向: 具体化的奋斗蓝图: 偶像不仅仅是一个名字或一张脸,对于像范可新这样的年轻运动员来说,王濛的成功经历,比如她.............
  • 回答
    夜幕如墨,吞噬了最后一抹残阳。地球,这个曾经生机勃勃的蓝色星球,此刻却弥漫着一种难以言喻的静默。不是宁静,而是压抑,是沉重,是一种即将到来的终结所带来的肃杀之气。在位于北极圈深处,一座戒备森严的地下基地中央,聚集了来自世界各地的最高决策者。空气中弥漫着消毒水的味道,混合着金属和某种难以描述的、带着一.............
  • 回答
    那一声低吼,如同来自地狱的呢喃,瞬间扼住了我的呼吸。窗外,夜色如墨,浓稠得仿佛能滴下水来。我僵在原地,心跳擂鼓般撞击着胸腔,每一个节拍都在提醒我,有什么东西,正悄悄地,却又不可阻挡地,逼近。灯光摇曳,将房间投下斑驳的阴影,每一个阴影都仿佛变成了伺机而动的猎食者。我该逃,还是该躲?又或者,我早已陷入了.............
  • 回答
    村子里的人都得了一种怪病。这话说起来轻描淡写,但落在我们这些身处其中的人耳朵里,却如同惊雷炸响,震得人心神不宁。我叫李老栓,今年六十有二,土生土长在这叫做“桃花源”的村子,以前,谁家添了丁,谁家盖了新房,谁家的小娘子要出嫁,我们都会聚在一起,吃酒喝肉,唱上几天的戏。可现在,这样的热闹场景,已经成了遥.............
  • 回答
    我的驸马,是个极温柔的人。我至今仍清晰地记得初见他时的情景,那日我刚及笄,宫中张灯结彩,锣鼓喧天,我被簇拥着坐在盖着红绸的软轿里,心中既有对未知的忐忑,也有对未来生活的一丝憧憬。当红盖头被掀开的那一瞬,我的目光落在面前那个年轻的男子身上。他穿着一袭织金的蟒袍,面如冠玉,眉目如画,最让我动容的,是他那.............
  • 回答
    我被全村的人送去祭天(海),但他们不知道的是,我根本不是人。他们脸上带着虔诚又恐惧的表情,将我推向那被晚霞染成血色的海边。海风裹挟着咸湿的气息,吹拂在我身上,带来一丝凉意,但这种凉意与我内心深处的冰冷截然不同。村子里的人们,无论是白发苍苍的老者,还是初识愁滋味的少年,都目不转睛地盯着我,眼神中混合着.............
  • 回答
    倘若一个人一天最初说出的三句话真的能左右现实,这世界可就热闹得紧了。想象一下,咱们每天醒来,就像拆盲盒一样,不知道今天会冒出什么奇妙或者糟糕的景象。就说早晨吧。你迷迷糊糊地揉着眼睛,嘴里嘟囔一句:“哎呀,今天天气真好啊!”话音刚落,窗外的乌云顿时散去,阳光灿烂得像是有人专门开了聚光灯。你再接着说:“.............
  • 回答
    我理解您想了解在极端情况下的选择和考量。这是一个非常沉重的话题,涉及到道德、法律、心理等多个层面。情景设定: 设想一下,在一个漆黑的夜晚,道路狭窄,四周没有路灯,手机信号也时有时无。您开着车,心情可能有些疲惫,突然间,一个人影晃到了车灯里,您出于本能踩下刹车,但终究还是发生了碰撞。等您下车,发现情况.............
  • 回答
    .......
  • 回答
    .......

本站所有内容均为互联网搜索引擎提供的公开搜索信息,本站不存储任何数据与内容,任何内容与数据均与本站无关,如有需要请联系相关搜索引擎包括但不限于百度google,bing,sogou

© 2025 tinynews.org All Rights Reserved. 百科问答小站 版权所有