开发一个Android App

从0到1,搭建一个简单的Android App。中间会夹着某些知识点的全量内容,方便下次开发时直接在这里根据自己当时的总结翻找回顾(官方的文档其实也已经很全很方便翻阅)。因为本人是iOS开发出身,所以中间也会夹着些和iOS相似概念的类比,举一反三地思考,加强记忆。

Android应用构成

android_app_structure

主干

  • AndroidManifest:应用的配置,系统需要根据里面的内容运行APP的代码,显示界面;
  • Activity:可以理解为一个视图控制器,在MVC模式下充当C这样的角色;
  • Fragment:表示Activity 中的片段,可组合或重复使用,具有它自己的生命周期和输入事件;
  • Service:在后台执行长时间运行操作而不提供界面的服务;
  • ContentProvider:内容提供者,在进程间进行数据交互和共享,即跨进程通信
  • BoardcastReceiver:广播接收器,是一个全局的监听器,监听接收应用发出的广播消息,并做出响应

四肢

  • View:Android里的图形界面都是由View和ViewGroup以及他们的子类构成的,或可直接将XML布局看作是View;
  • Adapter:也可以看作是另一个控制器,介于Activity和View之间,配合集合性的View使用;
  • Model:是指应用逻辑层(也叫领域层)的对象,如 Account、Order 等等,这些对象是应用程序中的一些核心对象,负责应用的逻辑计算,有许多与业务逻辑有关的方法或操作。仅仅只有getter、setter的用来传递数据的对象,只能叫Value Object,并不是真正的Model层;
  • AsyncTask:轻量级异步类,实现多线程、工作线程与主线程(UI线程)之间的通信;

衣服(Res)

  • layout:页面的布局
  • values:存放属性值,例如颜色、间隔、本地化
  • drawable:存放图片
  • xml:存放自定义的配置
  • anim:动画效果
  • mipmap:存放icon和launch图
  • menu:菜单选项

内脏

  • aar:含资源的库包集合
  • jar:纯代码库包
  • module:源码级的Library,在同一个工程中供主项目Module去引用依赖

应用配置文件 AndroidManifest

具体包含的内容有:

  • 为应用的 Java 软件包命名;
  • 描述应用的各个组件,包括构成应用的 Activity、服务、广播接收器和内容提供程序。它还为实现每个组件的类命名并发布其功能,例如它们可以处理的 Intent 消息。这些声明向 Android 系统告知有关组件以及可以启动这些组件的条件的信息;
  • 确定托管应用组件的进程;
  • 声明应用必须具备哪些权限才能访问 API 中受保护的部分并与其他应用交互。还声明其他应用与该应用组件交互所需具备的权限;
  • 列出 Instrumentation类,这些类可在应用运行时提供分析和其他信息。这些声明只会在应用处于开发阶段时出现在清单中,在应用发布之前将移除;
  • 声明应用所需的最低 Android API 级别;(在build.gradle也能指定)
  • 列出应用必须链接到的库。(3.0的AS之后由build.gradle指定)

它就好比iOS中的Info.plist,包含各种应用运行前或运行时所需的配置项

<manifest>此元素的属性中:

  • package表示应用的包名;
  • android:versionName表示商店上给用户所看的版本号;
  • android:versionCode表示开发者内部的版本号,一般作为标识每次生成的包的号码;

<manifest>中的元素

  • <uses-feature> 将APP所依赖的硬件或者软件条件进行声明,需在单独的<uses-feature>元素中指定每个功能;
    • android:name 指定的功能名。
    • android:required 是否必须功能。
    • android:glEsVersion 需要的Opengl ES版本。
  • <uses-permission> 声明需要使用的权限,一般只设置 android:name= 属性,可选的权限有
    • android.permission.INTERNET
    • android.permission.WRITE_EXTERNAL_STORAGE
  • <application> 描述应用的配置,包含多个子元素来描述应用中的组件;
    • android:allowBackup 是否允许APP加入到备份还原的结构中。
    • android:fullBackupContent 是否支持RTL(Right To Left)布局,targetSdkVersion在17以上起效。
    • android:icon APP的图标,一般就使用mipmap(-hdpi)下对应的ic_launche图片。
    • android:label 用户可读的标签,标签必须设置成一个字符串资源的引用,使其为可配置可定位。
    • android:theme 应用使用的主题,指向style资源的引用,各个activity也可以用自己的theme属性设置自己的主题。
    • android:name Application子类的全名,需包括其整体路径,当应用启动时,这个类的实例被第一个创建,这个属性是可选配置。
  • <activity><application>中的元素,声明一个实现应用可视化界面的Activity,任何未在该处声明的Activity对系统都不可见,并且永远不会被执行;
    • android:name activity的名称,如果第一个字符是点,就需要加上包名。
    • android:label 同application
    • android:configChanges 在运行时发生配置更改时,默认情况下会关闭 Activity 然后将其重新启动,但使用该属性声明配置将阻止 Activity 重新启动,并保持运行状态,系统会调用其 onConfigurationChanged()方法。
      • orientation 设备旋转
      • keyboardHidden 键盘显隐
    • android:launchMode 如何启动Activity的指令,一共有四种指令,默认情况下是standard。
      • standard 可以多次实例化,遵循元素进栈出栈的特性,可位于Activity堆栈中的任何位置。
      • singleTop 可以多次实例化,但如果要开启的activity在任务栈的顶部已经存在,就不会创建新的实例,而是调用 onNewIntent() 方法。避免栈顶的activity被重复的创建。
      • singleTask 单一实例模式,如果要激活的activity,在任务栈里面已经存在,就不会创建新的activity,而是复用这个已经存在的activity,调用 onNewIntent() 方法,并且清空这个activity任务栈上面所有的activity。
      • singleInstance 单一实例模式,整个手机操作系统里面只有一个实例存在,不同的应用去打开这个activity共享公用的同一个activity,运行在自己独立的任务栈里面,并且任务栈里面只有它一个实例存在。
    • android:theme 同application的theme
  • <meta-data>一般是<activity>中的元素,是一个name-value对,提供给其父组件,数据会被组成一个Bundle对象,可以通过PackageItemInfo.metaData字段使用;
    • android:name
    • android:resource
    • android:value
  • <intent-filter>一般也是<activity>中的元素,指明activity可以以什么样的意图(intent)启动,其子元素包括
    • <action>元素,表示activity作为一个什么动作启动,android.intent.action.MAIN表示作为主activity启动
    • <category>元素,表示action元素的额外类别信息,android.intent.category.LAUNCHER表示这个activity为当前应用程序优先级最高的Activity。

示例:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.test.app">

    <uses-permission android:name="android.permission.INTERNET"/>
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>

    <application
        android:name="com.test.app.TestApplication"
        android:networkSecurityConfig="@xml/network_security_config"
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        
        <service
            android:name="com.test.app.service.TestAidlService"
            android:exported="true">
            <intent-filter>
                <action android:name="com.test.app.service.TestAidlService"/>
                <category android:name="android.intent.category.DEFAULT"/>
            </intent-filter>
        </service>
        
        <activity
            android:name="com.test.app.activity.MainActivity"
            android:theme="@style/AppTheme.NoActionBar">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

        <activity android:name="com.test.app.activity.DetailActivity"
            android:parentActivityName="com.efn.testapp.activity.MainActivity"
            android:theme="@style/AppTheme.NoActionBar" >
            <!-- The meta-data tag is required if you support API level 15 and lower -->
            <meta-data
                android:name="android.support.PARENT_ACTIVITY"
                android:value="com.test.app.activity.MainActivity" />
        </activity>

        <provider
            android:name="android.support.v4.content.FileProvider"
            android:authorities="com.efn.testapp.fileprovider"
            android:exported="false"
            android:grantUriPermissions="true">
            <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/file_provider"/>
        </provider>
    </application>
</manifest>

UI

控制器 Activity

官方文档

Android中的Activity,在MVC中就可以直接看作是C,即Controler,与iOS的ViewControler的定位一致。但MVP或MVVM中,可能就需要改视为V,即View更多一些,因为它的业务逻辑会进一步被分化到Presenter或者ViewModel上,只是保留简单地管理View的部分,下面在设计模式一节再详谈。

开发Activity的基本步骤:

  • 创建Activity,最主要的是实现onCreate(),初始化 Activity 的必需组件,其它生命周期的方法可以按需要实现 官方文档
    • onCreate 创建
    • onStart 即将可见
    • onResume 已经可见
    • onPause 聚焦在其他activity上
    • onStop 不再可见
    • onDestroy 准备销毁
  • 在onCreate()中必须调用 setContentView() ,传递布局的资源ID以定义Activity用户界面的布局。
  • 在清单文件中声明 Activity,指定你要的意图过滤器intent-fliter
  • 启动Activity
    • 创建意图 Intent对象,传入将要打开的子Activity的class
    • 不需要回调的调用 startActivity()
    • 需要回调的调用 startActivityForResult(),并实现 onActivityResult(),通过requestCode参数标识来源,resultCode参数标识处理结果
    • 需要额外参数的,对intent调用 putExtra(“key”, “value”)进行设置
  • 结束Activity
    • finish() 结束当前Activity
    • finishActivity() 结束之前启动的另一个Activity

在startActivity()或finish()后调用overridePendingTransition(),传入进入动画和退出动画的资源ID,即可自定义启动Activity的动画效果。

Activity一般继承AppCompatActivity就能带有ActionBar(AS中创建Activity是默认继承它的),也就是iOS中的NavigationBar。为了可以兼容旧版的actionBar,support v7提供的ActionBarActivity(其实就是一个AppCompatActivity的空继承)。

在被打开的Activity中,如果是使用自定义toolbar,且不用兼容旧系统的actionBar,则可以这样指定标题和后退按钮

Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setTitle("xx");
toolbar.setTitleTextColor(Color.BLACK);
toolbar.setNavigationIcon(R.drawable.ic_toolbar_back);

若是自定义的toolbar但不需要兼容actionBar,则需要先将布局中自定义的toolbar设定为actionBar,并关闭相关的自定义属性

Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar); //为了兼容旧版本上的actionBar(Toolbar是新的代替工具)
ActionBar actionBar =  getSupportActionBar();
if(actionBar != null) {
    //actionBar.hide(); 
    actionBar.setHomeButtonEnabled(false);
    actionBar.setDisplayShowHomeEnabled(false);
    actionBar.setDisplayShowTitleEnabled(false);
}

TextView titleView = findViewById(R.id.detail_main_title);
titleView.setText(title + "详情");

Button backButton = findViewById(R.id.detail_main_back_btn);
backButton.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        DetailActivity.this.finish(); 
        overridePendingTransition(R.anim.anim_back_in, R.anim.anim_back_out);
    }
});

片段 Fragment

官方文档

可以在一个 Activity 中组合多个片段,从而构建多窗格界面,并在多个 Activity 中重复使用某个片段。

片段必须始终托管在 Activity 中,其生命周期直接受宿主 Activity 生命周期的影响。Fragment的生命周期如下:

  • onAttach/onDettach
  • onCreate/onDestroy
  • onCreateView/onDestroyView
  • onActivityCreate
  • onStart/onStop
  • onResume/onPause

当您将片段作为 Activity 布局的一部分添加时,其位于 Activity 视图层次结构的某个 ViewGroup 中,并且片段会定义其自己的视图布局。您可以通过在 Activity 的布局文件中声明片段,将其作为 元素插入您的 Activity 布局,或者通过将其添加到某个现有的 ViewGroup,利用应用代码将其插入布局。

开发Fragment

如果是普通的Fragment,可以通过onCreateView()返回布局的根视图(View)

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
                         Bundle savedInstanceState) {
    // Inflate the layout for this fragment
    return inflater.inflate(R.layout.example_fragment, container, false);
}
  • inflater参数可利用来加载布局(有点像Activity中的setContentView())
  • container参数是片段布局将插入到的父级ViewGroup(来自Activity)
  • savedInstanceState参数是在恢复片段时,提供上一片段实例相关数据的Bundle
  • inflate()但最后一个布尔型参数,指示是否应在扩展期间将扩展布局附加至 ViewGroup(第二个参数)的布尔值。
  • getArguments()获取传递进来的其它参数
  • getView()获取当前的ViewGroup

如果是DialogFragment,则需要重写onCreateDialog(),并返回一个自定义的Dialog(否则父类默认会返回一个空白的Dialog),示例:

@Override
public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {

    Dialog alertDialog = new Dialog(getContext());
    alertDialog.requestWindowFeature(Window.FEATURE_NO_TITLE);
    View customView = initView(null, null);
    alertDialog.setContentView(customView);
    alertDialog.setCanceledOnTouchOutside(true);
    alertDialog.getWindow().setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));

    WindowManager.LayoutParams windowParams = alertDialog.getWindow().getAttributes();
    windowParams.width = (int) (getResources().getDisplayMetrics().widthPixels -
            getResources().getDisplayMetrics().density * 40);
    windowParams.height = (int) (getResources().getDisplayMetrics().heightPixels -
            getResources().getDisplayMetrics().density * 100);
    alertDialog.getWindow().setAttributes(windowParams);

    return alertDialog;
}

//initView()
···
if (inflater == null) {
    view = LayoutInflater.from(getContext()).inflate(R.layout.detail_advise_main, container);
} else {
    view = inflater.inflate(R.layout.detail_advise_main, container, false);
}
··· //在view上设置控件

效果如下图
DialogFragment效果

ListFragment暂时没用到,但原理基本一致。

在Activity的布局中插入片段,将片段当作视图来为其指定布局属性,创建此Activity布局时,系统会将布局中指定的每个片段实例化,并为每个片段调用onCreateView()方法,以检索每个片段的布局。这个实现比较简单,不细说。

通过编程方式展示,用于运行时,只需指定要将片段放入哪个ViewGroup,示例

FragmentManager fragmentManager = getSupportFragmentManager();
FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
ExampleFragment fragment = new ExampleFragment();
fragmentTransaction.add(R.id.fragment_container, fragment); //还有replace()、remove()、addToBackStack()这些事务操作更改方法
fragmentTransaction.commit();

运行时的编程也有第二种方式展示Fragment

DetailAdviseFragment fragment = new DetailAdviseFragment(); //此处为一个DialogFragment
fragment.setOnPressButtonListener(DetailActivity.this);
Bundle bundle = new Bundle();
bundle.putString("advise", "something");
fragment.setArguments(bundle); //设置Fragment的参数
fragment.show(getSupportFragmentManager(), "detailAdviseFragment");

与Activity通信的话,正向就是通过FragmentManager的findFragmentById找出fragment,反向就是通过Fragment定义接口,Activiyt实现接口(下面的监听者一节再介绍这样的接口实现)

最后,说一个使用频率应该颇高的示例,利用Fragment实现多标签切换的效果。

先定义一个Activity,组织一个ViewPager和两个数据源,数据源包括作为单个标签主体的Fragment的列表和标签标题的列表。

public class MyActivity extends AppCompatActivity {

    ViewPager mViewPager;
    List<Fragment> mFragmentList;
    String[] mTitles = new String[]{"标签1","标签2"};
    
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        ··· //find view
        mFragmentList = new ArrayList<Fragment>();
        mFragmentList.add(MyFirstFragment.getInstance(network));
        mFragmentList.add(MySecondFragment.getInstance(network));
        MyFragmentPagerAdapter adapter = new MyFragmentPagerAdapter(getSupportFragmentManager(), mFragmentList, mTitles);
        mViewPager.setAdapter(adapter);
        mTabLayout.setupWithViewPager(mViewPager);
        ···
    }
}

布局

<LinearLayout
    ···>

    <android.support.design.widget.TabLayout
        ···
        app:tabIndicatorColor="@color/colorBlue"
        app:tabIndicatorHeight="5dp"
        app:tabSelectedTextColor="@color/colorBlue"
        app:tabTextColor="@color/colorDark" />

    <android.support.v4.view.ViewPager
        ···/>

</LinearLayout>

其次ViewPager和数据源之间,需要一个适配器才能进行工作,系统已经准备好了这种功能的适配器FragmentPagerAdapter,继承它自定义自己的数据逻辑

public class MyFragmentPagerAdapter extends FragmentPagerAdapter {
    private List<Fragment> mFragmentList;
    private String[] mTitles;

    public MyFragmentPagerAdapter(FragmentManager fm, List<Fragment> fragmentList, String[] titles) {
        super(fm);
        this.mFragmentList = fragmentList;
        this.mTitles = titles;
    }

    @Override
    public Fragment getItem(int i) {
        return mFragmentList.size()>0 ? mFragmentList.get(i) : null;
    }

    @Override
    public int getCount() {
        return mFragmentList.size();
    }

    @Nullable
    @Override
    public CharSequence getPageTitle(int position) {
        return mTitles[position];
    }
}

最后就是定义Fragment,这里采用单例的方式,提供数据输入口刷新页面内容,假如整个App里面只有一处展示到这个ViewPager,且使用频率又较高,那使用这种方式有一定的好处。

public class MyFirstFragment extends Fragment {

    public DataModel mDataModel;

    private static MyFirstFragment mFragment;
    public static MyFirstFragment getInstance(DataModel model) {
        if (mFragment == null) {
            mFragment = new MyFirstFragment();
        }
        mFragment.mDataModel = model;
        return mFragment;
    }

    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        super.onCreateView(inflater, container, savedInstanceState);
        View view = inflater.inflate(R.layout.my_first_fragment_layout, container, false);
        return view;
    }

    @Override
    public void onResume() {
        super.onResume();
        View view = getView();
        ···//find view ,setup view
    }
}

这就完成了一个标签切换功能,效果如下

效果如下图
FragmentPager效果

视图 View

布局-官方文档

每个ViewGroup都是看不见的用于组织子View的容器,而它的子View可能是输入控件,又或者在UI上绘制某块区域的小部件。

  • View:所有可视化控件的父类,提供组件描绘和事件处理方法,例如ButtonTextView等;
  • ViewGroup:View类的子类,可拥有子控件,可以看作是容器,例如各种Layout类。

Android UI中的控件都是按照这种层次树的结构堆叠得,而创建UI布局的方式有两种,自己在Java里写代码或者通过XML定义布局,后者显得更加方便和容易理解!

UI布局的层次结构图如下
viewgroup_2x

当App加载上述的布局资源时,Android会将布局中的每个节点进行实例化成一个个(类名对应元素名称的)对象。可以为这些对象定义一些额外的行为,查询对象的状态,或者修改布局。

容器类有

  • FrameLayout 框架布局,所有的控件都会默认出现在视图的左上角
    • layout_gravity(可设值:center、top、bottom、left、right、right|bottom)
  • LinearLayout 线性布局
    • layout_weight
    • orientation:布局方向(可设值:horizontal/vertical)
    • divider:分割线的样式
    • showDividers:加分割线的位置(可设值:middle/end/beginning/none)
  • AbsoluteLayoit 绝对布局,为了布局的适配,基本不使用此种布局。
  • RelativeLayout 相对布局,有子控件相对于父容器的布局,也有子控件直接相对的布局
    • layout_above:(属性值为ID的引用名)
    • layout_alignLeft/Top/Right/Bottom
    • layout_alignParentLeft/etc.
    • layout_toLeftOf/etc.
    • layout_alignBaseLine
    • layout_centerInParent
  • TableLayout 表格布局,将子元素的位置分配到行或者列中,一个TableLayout由许多的TableRow组成
  • ConstraintLayout 约束性布局
  • CoordinatorLayout 协调布局
    • layout_behavior
  • PercentFrameLayout
  • PercentRelativeLayout
  • GridLayout

各种容器概括图
1_wyeYIFdYPsES5b-mlbdaPg

基本的属性有

  • layout_width/height:(可设值:match_parent/wrap_content/XXdp,配合weight使用时注意设为0dip)
  • layout_centerHrizontal/centerVertical/centerInparent:是否水平/垂直/相对父元素居中
  • layout_margin
  • layout_marginLeft/Top/Right/Bottom/Start/End(4.2版本开始应使用Start和End分别代替Left和Right,系统能根据用户所设置的语言,例如需要从右到左排序的阿拉伯语,自行控制这个padding的位置,使用Start就可以交由系统处理,若还使用Left就有失偏颇,除非要兼容旧的系统版本则需要一并设置上)
  • layout_padding
  • layout_paddingLeft/Top/Right/Bottom/Start/End
  • addStatesFromChildren:布尔值,viewgroup的drawable属性是否把它的子类的drawable的state包含进来。
  • tag
  • descendantFocusability:当一个为view获取焦点时,定义viewGroup和其子控件两者之间的关系,可设值:
    • beforeDescendants:viewgroup会优先其子类控件而获取到焦点
    • afterDescendants:viewgroup只有当其子类控件不需要获取焦点时才获取焦点
    • blocksDescendants:viewgroup会覆盖子类控件而直接获得焦点(一般在ListView的自定义Item布局的根布局加上使用此值,能解决因子控件将焦点获取而导致点击item时变化的是子控件但item本身的点击没有响应的问题。)
  • splitMotionEvents:布尔值,定义布局是否传递touch事件到子布局
  • layoutMode:(可设值:clipBounds剪辑子控件的边界/opticalBounds可视的子控件边界)
  • background:本元素的背景
  • foreground:设置布局的前景图,前景图不会被子元素覆盖
  • foregroundGravity:设置布局前景图的位置
  • foregroundInsidePadding
  • scrollX/Y:最初的水平/竖直滚动的偏移,以像素为单位
  • focusable:布尔值,是否能获得焦点(按键)
  • focusableInTouchMode:布尔值,是否可以通过touch获取到焦点
  • fitsSystemWindows:布尔值,布局调整时是否考虑系统窗口(如状态栏)
  • fadeScrollbars:布尔值,滚动条是否自动隐藏
  • fadingEdge:设置拉滚动条时,边框渐变的方向(可设值:none边框颜色不变、horizontal水平方向颜色变淡、vertical垂直方向颜色变淡)。
  • fadingEdgeLength:滚动条渐变长度
  • filterTouchesWhenObscured:布尔值,所在窗口被其它可见窗口遮住时,是否过滤触摸事件
  • visibility:是否可见(可设值:gone/visible/invisible虽不可见,但占据布局位置)
  • scrollbars:设置滚动条(可设值:none/horizontal/vertical)
  • scrollbarStyle:可设值:
    • outsideInset:ScrollBar显示在视图(view)的边缘,增加了view的padding. 如果可能的话,该ScrollBar仅仅覆盖这个view的背景.
    • insideInset:该ScrollBar显示在padding区域里面,增加了控件的padding区域,该ScrollBar不会和视图的内容重叠.
    • outsideOverlay:该ScrollBar显示在视图(view)的边缘,不增加view的padding,该ScrollBar将被半透明覆盖
    • insideOverlay:该ScrollBar显示在内容区域里面,不会增加了控件的padding区域,该ScrollBar以半透明的样式覆盖在视图(view)的内容
  • isScrollContainer:布尔值,设置当前View是否为滚动容器(是否可以为输入法腾出空间而隐藏)
  • scrollbarFadeDuration:褪色时间
  • scrollbarDefaultDelayBeforeFade:设置滚动条N毫秒后开始淡化,以毫秒为单位。
  • scrollbarSize:设置滚动条大小
  • scrollbarThumbHorizontal/Vertical:设置水平/垂直滚动条的drawable
  • scrollbarTrackHorizontal/Vertical:设置水平/垂直滚动条背景(轨迹)的drawable
  • scrollbarAlwaysDrawHorizontalTrack/VerticalTrack:布尔值,设置水平滚动条是否含有轨道
  • scrollbarAlwaysDraw:布尔值,设置垂直滚动条是否含有轨道
  • requiresFadingEdge:定义褪色时滚动边缘(可设值:none/horizontal/vertical)
  • nextFocusLeft/Right/Up/Down/Forward:Up键按下之后,哪一个控件获得焦点(被选中)
  • clickable:布尔值
  • longClickable:布尔值
  • saveEnabled:布尔值,设置是否在窗口冻结时(如旋转屏幕)保存View的数据
  • drawingCacheQuality:设置绘图缓存质量(可设值:auto/low/hight)
  • keepScreenOn:布尔值,View在可见的情况下是否保持唤醒状态
  • duplicateParentState:布尔值,如果设置此属性,将直接从父容器中获取绘图状态(光标,按下等)。 注意根据目前测试情况仅仅是获取绘图状态,而没有获取事件,也就是你点一下LinearLayout时Button有被点击的效果,但是不执行点击事件。
  • minHeight/Width
  • soundEffectsEnabled:布尔值,设置点击或触摸时是否有声音效果
  • hapticFeedbackEnabled:布尔值,实现单击某个视图,系统提供一个触力反馈(震动一下)
  • contentDescription:图片不可见时的文字描述(盲人)
  • onClick:触发方法
  • overScrollMode:滚动到边界时的效果(可设值:ifContentScrolls/always/never)
  • alpha:透明度,小数表示占比
  • translationX/Y:X/Y轴的偏移距离
  • transformPivotX/Y:从某点的X/Y轴偏移距离
  • rotation:旋转(X和Y轴同时)
  • rotationX/Y
  • scaleX/Y:设置X/Y轴缩放比例
  • verticalScrollbarPosition:设置垂直滚动条的位置(可设值:defaultPosition/left/right)
  • layerType:绘图是否开启硬件加速(可设值:none/hardware/software)
  • layoutDirection:定义布局图纸的方向
  • textDirection:文字排列方向
  • textAlignment:文字对齐方式(可设值:inherit/…)
  • importantForAccessibility:设置可达性的重要行(可设值:noHideDescendants/…)
  • accessibilityLiveRegion:
  • labelFor:添加标签
  • measureAllChildren:布尔值,测量时是否考虑所有所有子控件,不考虑则按是否显示来测量
  • animateLayoutChanges:布尔值,添加默认布局动画
  • clipChildren:布尔值,子控件是否要在它应有的边界内进行绘制
  • clipToPadding:布尔值,ViewGroup是否允许在padding中绘制(配合clipChildren适合做放大等的点击特效,不用去更改布局,只需加入这两个属相并引入动画效果即可实现)
  • layoutAnimation:设置layout动画
  • animationCache:定义子布局也有动画效果
  • persistentDrawingCache
  • alwaysDrawnWithCache:定义子布局是否应用绘图的高速缓存

拿最强大的ConstraintLayout举例

<android.support.constraint.ConstraintLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <Button
        android:id="@+id/login_btn"
        ...
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintBottom_toBottomOf="parent" />
    <EditText
        android:id="@+id/password_textview"
        ...
        app:layout_constraintVertical_bias="0.5"
        app:layout_constraintBottom_toTopOf="@+id/login_btn"
        app:layout_constraintTop_toTopOf="@id/accoutn_textview"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"/>
    <EditText
        android:id="@+id/accoutn_textview"
        ...
        app:layout_constraintVertical_bias="0.8"
        app:layout_constraintBottom_toTopOf="@+id/password_textview"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent" />
    <TextView
        android:id="@+id/separate_line"
        ...
        app:layout_constraintVertical_bias="0.2"
        app:layout_constraintTop_toBottomOf="@+id/login_btn"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent" />
    <Button
        android:id="@+id/guest_btn"
        ...
        app:layout_constraintHorizontal_bias="0.3"
        app:layout_constraintVertical_bias="0.2"
        app:layout_constraintTop_toBottomOf="@+id/separate_line"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"/>
</android.support.constraint.ConstraintLayout>

在Activity中使用以下方法应用布局,当然也可以在运行时用代码

setContentView(R.layout.main_layout);

找到布局后接着使用以下方法根据资源ID获取布局中的视图

Button myButton = (Button) findViewById(R.id.my_button);

ToolBar

自定义示例(不能用android:style,只能用android:theme)

<android.support.design.widget.AppBarLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:theme="@style/AppTheme.AppBarOverlay" >

    <android.support.v7.widget.Toolbar
        android:id="@+id/toolbar"
        android:layout_width="match_parent"
        android:layout_height="?attr/actionBarSize"
        android:background="?attr/colorPrimary"
        app:contentInsetEnd="0dp"
        app:contentInsetLeft="0dp"
        app:contentInsetRight="0dp"
        app:contentInsetStart="0dp"
        app:popupTheme="@style/AppTheme.PopupOverlay">

        <LinearLayout
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:orientation="vertical"
            android:layout_gravity="center">

            <TextView
                android:id="@+id/toolbar_title"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:layout_weight="2"
                android:textAlignment="center"
                android:textColor="@color/colorBlack"
                android:gravity="center" />
            <TextView
                android:id="@+id/toolbar_subtitle"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:layout_weight="1"
                android:textAlignment="center"
                android:textColor="@color/colorDark"
                android:layout_gravity="center"/>

        </LinearLayout>
    </android.support.v7.widget.Toolbar>
</android.support.design.widget.AppBarLayout>

ListView

其实ListView本身没多少内容需要讲,其主要得依靠一个扩展(继承)于BaseAdapter的适配器,来获取数据源的Item数、Item标识、Item视图

mMyAdapter = new MyAdapter(this, R.layout.list_item, datas);
mListView = (ListView) findViewById(R.id.list);
mListView.setAdapter(mMyAdapter);//不能赋null
mListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
    @Override
    public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
        updateData(mMyAdapter.getItem(position));
    }
});

public class MyAdapter extends ArrayAdapter<GameInterface> {
    private ArrayList<ModelInterface> mDatas;
    private int resourceId;
    private Context context;

    public MyAdapter(Context context, int resourceId, ArrayList<ModelInterface> objects) {
        super(context, resourceId, objects);
        this.mDatas = objects;
        this.resourceId = resourceId;
        this.context = context;
    }

    @Override
    public int getCount() {
        return mDatas == null ? 0 : mDatas.size();
    }

    @Override
    public GameInterface getItem(int position) {
        if (mDatas == null || mDatas.size() <= position){
            return null;
        }
        return mDatas.get(position);
    }

    @Override
    public long getItemId(int position) {
        return position;
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        ModelInterface game = (ModelInterface) getItem(position);

        View view;
        ViewHolder viewHolder;

        if (convertView == null) {
            view = LayoutInflater.from(getContext()).inflate(resourceId, null);
            viewHolder = new ViewHolder();
            viewHolder.titleView = (TextView) view.findViewById(R.id.title_view);
            viewHolder.contentView = (TextView) view.findViewById(R.id.content_view);
            view.setTag(viewHolder); //缓存
        } else {
            view = convertView;
            viewHolder = (ViewHolder) view.getTag();
        }

        viewHolder.titleView.setText("title");
        viewHolder.contentView.setText("content");
        return view;
    }

    class ViewHolder {
        TextView titleView;
        TextView contentView;
    }
}

RecyclerView

对比ListView,RecyclerView可以理解为一个加强版的ListView,它更灵活、自定义更容易,回收机制也更完善、性能更好、滑动更流畅。灵活体现在RecyclerView所细分的各司其职的对象

  • LayoutManager //布局管理器
    • ListView
    • GridView
    • 瀑布流
  • Adapter //适配器
  • ItemAnimator //Item的动画
  • ItemDecoration //Item的分割

其次RecycleView还提供了:

  • 提供了局部刷新
  • 支持嵌套滑动(NestedScrollView或者CoordinatorLayout)
  • 把点击事件的控制权完全的交给开发者,避免了Item与其内的控件点击事件的冲突

创建一个最普通的RecyclerView示例

mMyRecyclerView = findViewById(R.id.my_recyclerview);
mMyAdapter = new MyAdapter(this);
mMyAdapter.setOnItemClickListener(this);

// 创建RecyclerView的布局管理器
LinearLayoutManager layoutManager = new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false);
mMyRecyclerView.setLayoutManager(layoutManager);

mMyRecyclerView.setAdapter(mMyAdapter);

// 创建Item分割样式
DividerItemDecoration dividerItemDecoration = new DividerItemDecoration(this, DividerItemDecoration.VERTICAL);
dividerItemDecoration.setDrawable(getResources().getDrawable(R.drawable.inset_recyclerview_divider));
mMyRecyclerView.addItemDecoration(dividerItemDecoration);

// 创建(默认)Item动画效果
mMyRecyclerView.setItemAnimator(new DefaultItemAnimator());

/// RecyclerView Adapter
public class MyAdapter extends RecyclerView.Adapter <MyAdapter.ViewHolder> {

    private LayoutInflater mLayoutInflater;
    private Context mContext;
    private ArrayList<ReportModel> datas;
    private OnClickItemListener mOnItemClickListener;
    
    public interface OnClickItemListener {
        void onClickItem(Model model);
    }

    public MyAdapter(Context context) {
        this.mLayoutInflater = LayoutInflater.from(context);
        this.mContext = context;
        this.datas = new ArrayList<ReportModel>();
    }

    @NonNull
    @Override
    public MyViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int i) {
    //当Item的ViewHolder复用池中不够显示数量时需创建ViewHolder的方法进行回调式的封装(对应着需显示的View,而View由RecyclerView内部创建)
        return new MyViewHolder(mLayoutInflater.inflate(R.layout.list_item , viewGroup, false));
    }

    @Override
    public void onBindViewHolder(@NonNull final MyViewHolder viewHolder, int i) {
    //对复用的ViewHolder进行更新,也采用回调式的封装
        final Model model = (Model)datas.get(i);
        viewHolder.titleView.setText("title");
        viewHolder.contentView.setText("content");
        viewHolder.accountSubTitle.setText(report.getCreatedUser());
        viewHolder.imageView.setImageResource(R.drawable.item_image);
        viewHolder.itemLayout.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    mOnItemClickListener.onClickItem(model);
                }
            });
        }
    }

    @Override
    public int getItemCount() {
        return datas == null ? 0 : datas.size();
    }

    public static class MyViewHolder extends RecyclerView.ViewHolder {

        public TextView titleView;
        public TextView contentView;
        public ImageView imageView;
        RelativeLayout itemLayout;

        MyViewHolder(View view) {
            super(view);
            titleView = view.findViewById(R.id.list_item_title);
            contentView = view.findViewById(R.id.list_item_content);
            imageView = view.findViewById(R.id.list_item_iamge);
            itemLayout = view.findViewById(R.id.list_item_relative_layout);
        }
    }

    public void setOnItemClickListener(OnClickItemListener listener) {
        this.mOnItemClickListener = listener;
    }
}

其它集合View

  • ViewPager(代替以往的Gallery)
  • HorizontalScrollView(代替以往的Gallery)
  • GridView
  • NestedScrollView

控件

  • TextView
    类似iOS中的UILable
<TextView
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/colorBlue"
    android:gravity="bottom"
    android:textAlignment="center"
    android:padding="4dp"
    android:text="@string/title"
    android:textColor="@color/colorBlack"
    android:textStyle="bold" 
    ···/>
  • EditView
    类似iOS中UITextField、UITextView
<EditText
    android:layout_width="250dp"
    android:layout_height="wrap_content"
    android:gravity="center_vertical"
    android:background="@android:drawable/edit_text"
    android:imeOptions="actionDone"
    android:hint="请输入密码"
    android:inputType="textPassword"
    android:textColorHint="@color/colorDark"
    ···/>
//TextView.OnEditorActionListener
editView.setOnEditorActionListener(this);
@Override
public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
    Log.d("", "text: "  + v.getText().toString());
    return false;
}
  • Button
<Button
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="按钮"
    ···/>
  • ImageView
<ImageView
    android:layout_width="0dp"
    android:layout_height="30dp"
    android:src="@drawable/ic_image"
    android:gravity="center"
    ···/>
    
imageView.setImageResource("R.id.image");
  • ProgressBar
<ProgressBar
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_gravity="center"
    android:visibility="gone"/>

progressBar.setVisibility(View.VISIBLE);
progressBar.setVisibility(View.GONE);
  • Spinner
<Spinner
    android:entries="@array/distribute_platform"
    android:spinnerMode="dialog"
    android:layout_width="match_parent"
    android:layout_height="0dp"
    android:layout_weight="1"
    android:layout_margin="5dp"
    android:textColor="@color/colorBlack" />
    
<resources>
    <string-array name="distribute_platform">
        <item>1</item>
        <item>2</item>
        <item>3</item>
    </string-array>
</resources>
  • SearchView
<SearchView
    ···
    android:iconifiedByDefault="false"
    android:queryHint="请输入gameCode搜索">
</SearchView>

SearchView mSearchView = findViewById(R.id.search_bar);
mSearchView.setEnabled(true);
mSearchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
    // 当点击搜索按钮时触发该方法
    @Override
    public boolean onQueryTextSubmit(String query) {
        // 查询、过滤
        return true;
    }

    // 当搜索内容改变时触发该方法
    @Override
    public boolean onQueryTextChange(String newText) {
        if (newText.isEmpty()) {
            // 刷新界面
        }
        return false;
    }
});
  • Switch
<Switch
    android:layout_width="wrap_content"
    android:layout_height="@dimen/test_config_list_item_height"
    android:scaleX="2"
    android:scaleY="2"
    android:textOff=""
    android:textOn=""
    android:gravity="center"
    ···
     />
     
//CompoundButton.OnCheckedChangeListener
switch.setChecked(true);
switch.setOnCheckedChangeListener(this);
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
    Integer buttonPosition = (Integer) buttonView.getTag(buttonNumber);
    ···
}

监听者 Listener

可以用到监听者的场景包括:

  • 按钮点击事件的响应
  • 下级页面的回调方法提供
  • 列表Item或其子控件的点击事件的响应
  • 导航栏的Item点击事件的响应
  • TextView编辑动作的监听

其实它就和iOS中的代理模式如出一辙,linstener是去实现Interface的,delegate对象是认领遵循一个Protocol的,它们在概念上的区别是:

  • 协议,是当委托对象执行某方法中,需要一个遵循某协议的对象帮忙完成该协议中的事情时,就会委托它所持有的delegate属性(代理对象)去执行协议方法
  • 监听,是当执行某方法的中的主体,认为此时需要告知它所持有的需要知道此刻此情况的监听者的话(因此获取通知的方法是主体所定义),就会调用listener属性(监听者)去执行接口定义中获取通知的方法

示例

// demo1
public class MySecondActivity extends AppCompatActivity {
    
    public CallbackListener mCallbackListener;

    public interface CallbackListener {
        void onCallback(Model model);
    }
    
    private void doSomething() {
        ···
        this.mCallbackListener = this.context;
        mCallbackListenr.onCallback(data);
        ···
    }
}

public class MyFirstActivity extends AppCompatActivity implements MySecondActivity.CallbackListener {
    private void onCallback(Model model) {
        // update
        ···
    }
    
    private void doSomething() {
        Intent intent = new Intent(this, MySecondActivity.class);
        startActivity(intent);
    }
}

// demo2
Button button = findViewById(R.id.button_id);
button.setOnClickListener(new View.OnClickListener() { //创建一个实现OnClickListener接口的内部匿名类并同时进行实例化new,监听按钮的点击
    @Override
    public void onClick(View v) {
        ···
    }
});

当然,Interface并不仅仅局限于监听者这个概念上,作为解耦、抽象的利器,作用范围非常广,就看怎么去结合你的设计模式去利用,例如对多种同类的模型对象进行抽象,使展示页面与这种种类逻辑解耦,就可以用到接口设计,等等。

资源

资源-官方文档

Android应用的资源,是指代码使用的附加文件和静态内容,例如位图、布局定义、界面字符串、动画说明等,都集中存放于res(src/main/res)和assets文件夹下,这样才能起到资源脱离代码实现外部化,以便单独对其进行维护。

所有资源ID都在项目的R类中进行定义,该类由aapt工具自动生成,在R类中通过资源类型+资源名称这样的组合来访问资源。例如:

//代码中
R.string.hello

//XML中
@string/hello

//格式模板
[<package_name>.]R.<resource_type>.<resource_name>
@[<package_name>:]<resource_type>/<resource_name>

同一种资源类型resource_type,还可区分不同的语言和屏幕密度。

res

res中包含以下常见的资源类型

  • animator:定义属性动画
  • drawable:存放位图文件、可绘制对象
  • mipmap:启动器的可绘制对象
  • layout:定义界面布局
  • menu:定义应用菜单
  • raw:以原始形式保存的任意文件,运行时通过 Resources.openRawResource() , 资源ID为R.raw.filename
  • values:包含字符串、整型数和颜色等简单值。像控件、字符等的颜色(colors),间距(dimens),字符串(strings),控件样式(style)
  • xml:自定义的配置,运行时通过 Resources.getXML() 读取
  • font:字体
<resources xmlns:android="http://schemas.android.com/apk/res/android">//导入可引用R类中的资源ID
    <item name="ic_menu_camera" type="drawable">@android:drawable/ic_menu_camera</item>
</resources>

如果还要引用其它辅助功能的话,布局上则需要带上 xmlns:app=”http://schemas.android.com/apk/res-auto" 和 xmlns:tools=”http://schemas.android.com/tools" 的声明,不过一般只在Layout上使用较多。

assets

assets/中的文件没有资源ID,因此只能使用AssetManager读取这些文件。如需访问原始文件名和文件层次结构,就应考虑将某些资源保存在assets/目录。

模型 Model

数据的获取、存储、数据状态变化都将是Model层的任务。而单单描述一个事物对象属性的类,只能称之为Value Object,是模型层的一部分而已。

先从Model层的架构说起,按Google和Boilerplate的说法,Model层的结构可以分别按如下两图设计

Xnip2019-10-04_14-06-23

Repository层的作用时屏蔽底层细节,使上层不需要知道数据的细节,包括像网络、数据库,内存等等的这些数据的操作细节。

Xnip2019-10-04_14-05-44

  • View层:处理用户的交互和输入事件,并且触发Presenter中的相应操作。
  • Presenter层:Presenters 订阅(subscibe) RxJava的Observables,负责处理订阅周期,处理由DataManager提供的数据,并调用View层中的相应方法展示数据。
  • Model层:负责获取、保存、缓存以及修改数据。负责与本地数据库、其他数据存储、restful APIs、以及第三方SDKs的交互。
  • DataManager:结合并且转化不同的Helpers类为Rx操作符,向Presenter层提供Observables类型的数据(provide meaningful data to the Presenter),并且同时处理数据的并发操作(group actions that will always happen together.)。这一层也包含实际的Model类,用于定义当前数据架构,也就是不同人所说的Entry,Bean,Pojo等。DataManager就好比上面的Repository

其次,再说Model层中的Model实体类的实现,包括

  • 构成方法:自定义构造方法,如果实体比较复杂,可能会用到工厂模式或者是建造者模式
  • 序列化:比如实现Serializable接口,Parcelable接口。
  • Json解析:有时候直接使用的是json数据,比如@SerializedName注解。
  • 自定义方法:对Model的字段有setter,getter方法,toString的实现,在处理hash的时候,需要实现equals和hashcode方法。

关于持久化的实现,最近使用过一个出名的持久化第三方库Realm。Realm是由美国YCombinator孵化的创业团队历时几年打造,它是第一个专门针对移动平台设计的数据库,是一个跨平台的移动数据库引擎,是由核心数据引擎C++打造,拥有独立的数据库存储引擎,可以方便、高效的完成数据库的各种操作。Realm具有开源、简单易用、跨平台、线程安全这些优点,在Mac上有可视化工具Realm Browser辅助。Java的仓库戳这里👇

Realm的使用步骤:

  • 主工程build.gradle中的dependencies添加 classpath “io.realm:realm-gradle-plugin:x.y.z”,应用Module的build.gradle中apply plugin: ‘realm-android’
  • 在MainActivity或者自定义的Application的OnCreate()中初始化 Realm.init(this);
  • Model类继承RealmObject
  • Model的所有属性设置为private并实现对应的getter和setter
Realm realm = Realm.getDefaultInstance();
Model model = mRealm.where(Model.class).findFirst();  
RealmResults<Model> results = realm.where(Model.class).findAll();
RealmResults<Model> results = realm.where(Model.class).equalTo("key", "value").findAll();
results = results.sort("key", Sort.DESCENDING);
List<Model> models = realm.copyFromRealm(results);
//除了findAll(),还有sum()、average()、min()、max()、findAllSorted()
Realm realm = Realm.getDefaultInstance();
realm.beginTransaction();
Model model = realm.createObject(Model.class);
model.setName("Bob");
model.setSex("male")
//realm.copyToRealm(model); //可在beginTransaction外先生成好model和设置好属性,再直接使用此方法在beginTransaction中同步到Realm数据库中,最后也需要commitTransaction
realm.commitTransaction();

/* 也可使用内部匿名类的方式创建Transaction
mRealm.executeTransaction(new Realm.Transaction() {  
  @Override  
  public void execute(Realm realm) {  
      //···
  }  
});  
*/
Realm realm = Realm.getDefaultInstance();
RealmResults<Model> results = realm.where(Model.class).equalTo("name", "Bob").findAll(); //查唯一值的条件
if (result.size() > 0) {
    realm.beginTransaction();
    Model model = result.get(0);
    model.setSex("Female");
    realm.commitTransaction();
}
Realm realm = Realm.getDefaultInstance();
RealmResults<Model> results = realm.where(Model.class).equalTo("key", "value").findAll(); //查唯一值的条件
if (result.size() > 0) {
    realm.beginTransaction();
    Model model = result.get(0);
    model.deleteFromRealm();
    //results.deleteFirstFromRealm();
    //results.deleteLastFromRealm();
    //results.deleteAllFromRealm()
    realm.commitTransaction();
}
  • 数据库表结构更新

先继承 RealmMigration,用于描述此新版App的数据库表结构有哪些更新,oldVersion 在没对RealmConfiguration设置过schemaVersion下默认为0

public class AppRealmMigration implements RealmMigration {
    @Override
    public void migrate(DynamicRealm realm, long oldVersion, long newVersion) {
        RealmSchema schema = realm.getSchema();
        if(oldVersion == 0) {
        // 也可以用schema进行create创建类
            schema.get("TestConfigModel")
                    .addField("isOpenIpListen", boolean.class); // 加基础属性
//                    .addRealmObjectField("favoriteDog", schema.get("Dog")) // 加对象属性
//                    .addRealmListField("dogs", schema.get("Dog")); // 加对象列表属性
            oldVersion++;
        }
    }
}

应用启动时进行 migrate 迁移,实质就是将旧数据上的属性进行更新

Realm.init(this);
RealmConfiguration config = new RealmConfiguration.Builder()
                .schemaVersion(1) // Must be bumped when the schema changes
                .migration(new AppRealmMigration())// Migration to run instead of throwing an exception
                .build();
Realm.setDefaultConfiguration(config);

网络请求方面,一般会选择使用okhttp

适配器 Adapter

Adapter是用来帮助填充数据的,将各种数据以合适的形式显示到view上,提供给用户看!

Adapter和iOS中UITableViewController非常相似,继承BaseAdapter就好比遵循了UITableDelegate和UITableViewDataSource两个协议,为自定义的需要而实现数据源和视图的输出方法。

Adapter的继承结构图如下
77919389

实际开发中用得最多的其实是BaseAdapter,它是一个抽象类,实现了ListAdapterSpinnerAdapter两个接口,而这两个接口都是Adapter这个接口的扩展接口。

  • ListAdapter定义了
    • areAllItemsEnabled() 是否所有Item可选择点击
    • isEnabled() 某一位置上的Item是否可选择点击两个方法
    • BaseAdapter的实现中默认都为true
  • SpinnerAdapter定义了
    • getDropDownView() 返回下拉视图的方法
    • BaseAdapter的实现中默认是通过调用它自己的getView()返回
  • Adapter定义的方法中,关键且常被重写的方法为
    • getCount() 返回数据的总数
    • getItem() 返回某行数据的模型
    • getItemId() 返回某行数据的ID,一般就是位置索引
    • getView() 返回某行的自定义视图

继承BaseAdapter的开发步骤:

  • 获取并持有数据源
  • 实现数据相关的输出方法(getCount、getItem、getItemId)
  • 定义内部的ViewHolder类,包含每行需显示的控件属性,不同类型行的ViewHolder需分开定义
  • 实现视图相关的输出方法(getView),第一次创建view时,将对应的ViewHolder对象塞入要返回的view的tag中,同时对ViewHolder对象中的控件设置上数据值;下次获取到view时只需从tag中取出ViewHolder对象更新控件的数据值即可,达到了控件复用的效果。

另一个使用率较高的就是RecyclerView.Adapter,与BaseAdapter有相似之处,也有不同之处,其具体开发步骤:

  • getItemCount() 返回Item的总数,注意判空
  • getItemViewType() 返回Item视图的类型
  • onCreateViewHolder() 根据上一点的方法返回类型,创建ViewHolder,给Adapter去关联view
  • onBindViewHolder() 触发绑定ViewHolder,在这个方法中,我们只需负责更新ViewHolder对象中控件的数据值即可,所以,我们可以为RecyclerView.Adapter指定它的泛型。

说到RecyclerView.Adapter,有一个比较出名的第三方BaseRecyclerViewAdapter,封装了很多便利的方法供直接调用和省去很多接口的实现,也基本能满足大部分效果的需求,有兴趣可以去额外了解下-随意门

  • ArrayAdapter:
    • 例化时可以使用泛型构造,可在构造函数的第二个参数绑定一个layout,第三个参数绑定数据源(其内部使用此数据源去实现getCount、getItem等方法,无需亲自实现一遍)
    • 增加了很多和数组相对应的操作,如add()、addAll()、insert()、clear()、remove()、sort()、setNotifyOnChange()(控制以上操作方法执行后是否自动执行notifyDataSetChanged(),当参数为false时则需手动执行。应用场景为当处理的数据量大时,可在处理前设置为false,处理后手动执行notifyDataSetChanged())
    • ArrayAdapter默认期望布局文件里只有一个TextView,实现稍微复杂的TextView可在构造方法中传入指定的Field ID,实现复杂布局时需重写getView()方法。
    • ArrayAdapter默认下会调用List中对象的toString()方法去设置TextView
    • 一个小技巧,当使用固定的数据源时,可在res中直接创建数据源的xml,然后给ListView布局中引用(系统内部创建ArrayAdapter去接收),
<?xml version="1.0" encoding="utf-8"?>  
<resources>  
    <string-array name="myarray">  
    <item>语文</item>  
    <item>数学</item>  
    <item>英语</item>  
    </string-array>      
</resources>
<ListView  
    android:id="@id/list_test"  
    android:layout_height="match_parent"  
    android:layout_width="match_parent"   
    android:entries="@array/myarray"/>
  • SimpleAdapter:更方便地套用Item的显示样式
SimpleAdapter myAdapter = new SimpleAdapter(getApplicationContext(), listitem, R.layout.list_item, new String[]{"header", "name", "says"}, new int[]{R.id.header, R.id.name, R.id.says});
  • SimpleCursorAdapter:过时的一种类,其实就是存入一个Cursor对象,由其内部利用cursor读取数据,用于在数据库获取简单文本显示的listView。不太推荐使用,直接扩展BaseAdapter,自行读取数据库会更好。
  • FragmentPagerAdapter:顾名思义
    • getCount() 返回title或者fragment的总数
    • getItem() 返回指定位置的fragment
    • getPageTitle() 返回指定位置的title

创建了Adapter后,最后记得设置到ListView或者RecyclerView的adapter属性中去。

跨应用通信 AIDL

服务-官方文档
AIDL-官方文档

在开发跨应用通信前,首先必须去了解4大组件之一的服务是什么东西,看完服务的文档,才能理解跨应用的工作原理。

这里简单概括一下,服务可以分为前台后台绑定这三种类型,均需自行创建线程执行操作,否则是在主线程上执行。前台和后台的服务生命周期结束是由调用者或自己所控制,绑定服务则全由发起绑定的应用组件所管理。可以使用ServiceIntentService创建服务,前者需自行创建线程执行操作,否则会影响应用性能,后者则由其内部使用工作线程逐一处理所有启动请求。而进程间通信 (IPC)就是利用绑定实现的,一般描述是发起绑定的为客户端,提供服务的为服务端

下面再讲述如何使用AIDL(安卓接口定义语言)。

AIDL是什么,它是可以被利用去所定义的客户端与服务使用进程间通信 (IPC) 相互通信时都认可的编程接口。

实现的主要步骤如下:

  1. 使用Java语言在.aidl文件中定义出AIDL接口,然后分别保存在服务和客户端的源码中(src/ 目录),一般是指定一样的具体路径,例如服务的.aidl文件存放于服务项目下的src/main/aidl/com/xx/yy/IXxAidlInterface.aidl,那么客户端的.aidl文件则存放于客户端项目下的同名路径下(作为副本存在,因为客户端也需具有对interface类的访问权限),其中com.xx.yy就是服务所在项目的包名,注意确保两端的aidl文件内容也是一致的。
  2. 编译后,IBinder接口文件–IXxAidlInterface.java文件会被自动生成(与aidl文件名同名),。
  3. IXxAidlInterface.java里具有一个名为Stub的内部抽象类(继承自Binder并实现IXxAidlInterface),用它来给服务扩展Binder类并重写实现AIDL接口中的方法。
  4. 最后,服务向客户端公开接口的方式,就是实现Service并重写 onBind() 以返回Stub类,去定义服务的RPC接口。
  5. RPC是同步调用的,需自行处理多线程工作和保障线程安全(或客户端从单独的线程中调用服务),否则可能会导致客户端出现“Application is Not Responding”的提示,同时异常是不会进行传递。
  6. 客户端实现ServiceConnection,在onServiceConnected()的回调中会接收服务的 onBind() 方法返回的 mBinder 实例,此时需调用IXxAidlInterface.Stub.asInterface(service)以将返回的参数转换成自己的ServiceInterface类型(service即是IBinder),后面就可以使用它来调用服务的接口方法进行通信了。
  7. 客户端触发IPC,调用Context.bindService(),传入我们的ServiceConnection实现(作为连接的回调对象)。
  8. 如果需要在接口中传递对象,则类必须实现Parcelable接口,同时服务和客户端两侧都要有此类可供引用。

示例:

  • 定义AIDL的接口
package com.xx.yy
interface IXxAidlInterface {
    void doSomething(String data);
}
  • 定义服务,实现绑定相关方法
public class MyAidlService extends Service {

    @Override
    public IBinder onBind(Intent intent) {
        // ···
        return new MyBinder(); //也可以在此直接返回IXxAidlInterface.Stub的匿名内部类的实例,省去定义Binder的类
    }

    @Override
    public boolean onUnbind(Intent intent) {
        // ···
        return super.onUnbind(intent);
    }
  • 服务端定义Binder,实现AIDL中的接口方法
public class MyBinder extends IXxAidlInterface.Stub {
    @Override
    public void doSomething(String data) throws RemoteException {
        ···
    }
}
  • 服务端AndroidManifest中声明Service
<service
    android:name="com.xx.yy.service.MyAidlService"
    android:exported="true">
    <intent-filter>
        <action android:name="com.xx.yy.service.MyAidlService"/>
        <category android:name="android.intent.category.DEFAULT"/>
    </intent-filter>
</service>
  • 客户端实现ServiceConnection
public class MyServiceConnection implements ServiceConnection {

    private IXxAidlInterface mService;

    @Override
    public void onServiceConnected(ComponentName name, IBinder service) {
        mService = IXxAidlInterface.Stub.asInterface(service);
    }

    @Override
    public void onServiceDisconnected(ComponentName name) {
        mService = null;
    }

    //可以通过返回mService,外面新建类使用其与业务逻辑进行封装
    public void doSomething(String data) {
        mService.doSomething(data);
    }
}
  • 客户端触发IPC
Intent service = new Intent("com.xx.yy.service.IXxAidlInterface");//sevice的路径
service.setPackage("com.xx.yy");
service.putExtra("key","value");
List<ResolveInfo> list = context.getPackageManager().queryIntentServices(service, PackageManager.MATCH_DEFAULT_ONLY);
if (list == null || list.isEmpty() || list.size() == 0){
    return;//服务(App)不存在
}
MyServiceConnection connection = new MyServiceConnection();
if(connection != null) {
    try {
        context.bindService(service, connection, Context.BIND_AUTO_CREATE);
    } catch (Exception e) {
        e.printStackTrace();
    }
}

多线程

AsyncTask

AsyncTask是用来实现工作线程与主线程(UI线程)之间通信

使用步骤

  • 创建AsyncTask子类并根据需求实现核心方法
  • 创建AsyncTask子类的实例对象
  • 手动调用实例对象的execute()从而执行异步线程任务
private class MyTask extends AsyncTask<Params, Progress, Result> {
    ....

    //执行异步线程前的回调,处理UI的初始化,是在主线程中回调的
    @Override
    protected void onPreExecute() { 
        ...
    }

    //执行异步线程任务
    @Override
    protected String doInBackground(String... params) {
        ...// 自定义的线程任务
        
        // 可调用publishProgress()显示进度,之后将回调onProgressUpdate()
        publishProgress(count);   
    }

    //进度回调,在主线程中回调的
    @Override
    protected void onProgressUpdate(Integer... progresses) {
        ...
    }
    
    //返回异步线程任务的执行结果,在主线程中回调
    @Override
    protected void onPostExecute(String result) {
        ...// UI操作
    }

    //将异步线程任务状态置为取消后,需自行在doInBackground判断任务的状态进行实质性的阻止中断,即回调此函数
    @Override
    protected void onCancelled() {
        ...
    }
}

//实例必须在UI线程中创建
MyTask mTask = new MyTask();

//a. 必须在UI线程中调用
//b. 同一个AsyncTask实例对象只能执行1次,若执行第2次将会抛出异常
//c. 执行任务中,系统会自动调用AsyncTask的一系列方法:onPreExecute() 、doInBackground()、onProgressUpdate() 、onPostExecute() 
//d. 不能手动调用上述方法
mTask.execute(); //执行异步线程的任务

Thread + Handler

创建线程有两种方法

  • 继承Thread类,重写Thread的run()方法(Thread也实现了Runnable接口)
  • 实现Runnable接口,重写Runnable的run()方法,并将其作为参数实例化Thread,这样能避免Thread单继承的局限和多Thread之间可以共享Runnable中的资源。
public class MyThread extends Thread {
	@Overide
	public void run() {
		...//异步线程任务
	}
}

MyThread t = new MyThread();//创建已内含任务的线程
t.start();
public class MyRunnable implements Runnable {
	public void run() {
		...//任务
	}
}

MyRunnable runnable = new MyRunnable();
Thread r = new Thread(runnable, "threadName");//创建线程并指定任务
r.start();

若要进行线程之间的切换通信,而多线程又选用了Thread实现,那么就需要搭配Handler使用了,因为Handler的最大作用就是线程的切换。

子线程与主线程通信的示例:

//在主线程实例化Handler
Handler handler = new Handler(){
    @Override
    public void handleMessage(Message msg) {
        //msg是从子线程发来的消息
        switch (msg.what) {
            case 1:
                Log.i("",  msg.obj);
                break;
        }
    }
};

//开启子线程
new Thread(new Runnable() {
    @Override
    public void run() {
        //在子线程发送一个消息。
        Message msg = new Message(); //正常应使用Message.obtain();,这样可利用到系统的Message缓存池,优化内存
        msg.obj = "自定义传递的数据对象";
        msg.what = 0; //what相当于msg的ID
        handler.sendMessage(msg);//同一个消息不要发送两次,否则报错,因为消息队列中不允许存在重复的消息
        //可使用post代替sendMessage,post内部会将Runnable封装成Message再发出去,且此Runnable的run方法比Handler中的handleMessage更高优先级
        handler.post(new Runnable() { 
            @Override
            public void run() {
                //这里是消息处理的方法,运行在主线程。
            }
        }
    }
}).start();

在主线程实例化Handler的话,其handleMessage是在主线程执行,因为Handler默认采用了主线程的Looper进行绑定,即是Handler在默认情况下,哪个线程创建它,就获取那个线程的Looper进行绑定,消息的处理也就在Looper所在的线程进行,除非在实例方法中额外指定Looper给Handler。(Thread拥有Looper,Looper拥有Handler和MessageQueue)

若要在子线程创建Handler,则需要自行创建Looper并开启其循环,才能使Handler正常工作,因为其它线程利用绑定该子线程Looper的Handler向往此子线程发消息时,会将Message塞入子线程的MessageQueue去,没有Looper的话就子线程就不会去它的MessageQueue上取消息出来分发给Handler处理(可能连消息的发送阶段就已不能成功?)。

Xnip2019-10-04_22-01-57

//在主线程创建并开启子线程异步任务
Handler handler;
new Thread(new Runnable() {
    @Override
    public void run() {
        //创建当前线程的Looper,因一个线程只能有一个Looper所以不能调用两次
        Looper.prepare();
        //在子线程创建Handler对象
        handler = new Handler() {
            @Override
            public void handleMessage(Message msg) {
                //这里是消息处理,运行在子线程
            }
       };
       //开启Looper的消息轮询
       Looper.loop();
    }
    
    //如果Handler不再需要发送和处理消息,那么就需要退出子线程的消息轮询
    //Looper.myLooper().quit();
    //Looper.myLooper().quitSafely();
}).start();

//在主线程发送一个消息到子线程
Message msg = new Message();
handler.sendMessage(msg);

还有其它一些扩展的方法

 //延时发送消息
public final boolean sendMessageDelayed(Message msg, long delayMillis)
public final boolean postDelayed(Runnable r, long delayMillis);

//定时发送消息(类比iOS的NSTimer)
public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
public final boolean postAtTime(Runnable r, long uptimeMillis);
public final boolean postAtTime(Runnable r, Object token, long uptimeMillis);

Activity类的runOnUiThread()、View类的post()、HandlerThread类,这些其实都是内部实现了或方便实现Handler的机制,封装起了一系列的方法,方便我们使用,例如runOnUiThread内部就是调用Activity在主线程创建的Handler去执行post(runnable),再例如HandlerThread本质就是个Thread,与普通Thread的差别在于其内部实现了Looper的创建,方便地让Handler可以直接指定使用这个线程的Looper。

话说回iOS,实现这种跨线程的通信,GCD相对方便一点,从发起异步任务,到与其它线程通信,这两部分逻辑的代码可以整合在一起实现,非常清晰,不过这是系统封装后的得益而已。NSThread也一样,系统通过封装好各种performSelector的方法,由我们自由选择执行的指定方法所在的线程(比Handler只能在Runnable Callback或者handleMessage这两个地方回调更有灵活性),实现线程间的通信,只要持有目标通信的NSThread,就好比持有着Handler,需要通信时就用其来发消息(performSelector),performSelector内部其实也是通过消息队列机制来触发任务线程去处理任务(借助NSTimer),也是需要一个循环体NSRunLoop才能起作用。

应用内安装

涉及到两个点,包括下载文件和安装包。

下载文件

String url = "http://www.xxx.com/xxx.apk"
File saveFile = new File(getContext().getCacheDir(), "MyApp.apk");
if (saveFile != null && saveFile.exists()) {
    saveFile.delete();
}
final Request request = new Request.Builder().url(url).build();
OkHttpClient okHttpClient = new OkHttpClient();
okHttpClient.newCall(request).enqueue(new Callback() {
    @Override
    public void onFailure(Call call, IOException e) {
        // 下载失败
    }
    @Override
    public void onResponse(Call call, Response response) throws IOException {
        InputStream is = null;
        byte[] buf = new byte[2048];
        int len = 0;
        FileOutputStream fos = null;

        try {
            is = response.body().byteStream();
            long total = response.body().contentLength();
            fos = new FileOutputStream(saveFile);
            long sum = 0;
            while ((len = is.read(buf)) != -1) {
                fos.write(buf, 0, len);
                sum += len;
                int progress = (int) (sum * 1.0f / total * 100);
                // 下载中
            }
            fos.flush();
            // 下载完成
        } catch (Exception e) {
            // 下载失败
        } finally {
            try {
                if (is != null) is.close();
                if (fos != null) fos.close();
            } catch (IOException e) {
            }
        }
    }
});

其中,调用 getContext().getCacheDir() 此类方法去获取存储路径,需先指定一个file_provider.xml文件来设定共享的目录(因7.0引入了类似iOS的沙盒机制,需设定一个共享目录来以uri形式给外界使用,通过provider),例如

<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
    <external-path name="share" path="/"/>
    <cache-path name="cache" path="/"/>
</paths>

其中设定的路径元素与获取方法对应关系如下

  • cache-path : Context.getExternalCacheDir()
  • files-path : Context.getFileDir()
  • external-path : Environment.getExternalStorageDirectory()
  • external-files-path : Context.getExternalStorageDirectory()
  • external-cache-path : Context.getExternalCacheDir()

若xml中路径元素中的paht有设值,比如test,则在以上方法获取的路径后再拼接此子路径,如此处的’/test/‘

最后在 Manifest 中也需要声明这个Provider及对应的xml文件

<provider
    android:name="android.support.v4.content.FileProvider"
    android:authorities="com.test.pr.fileprovider"
    android:exported="false"
    android:grantUriPermissions="true">
    <meta-data
        android:name="android.support.FILE_PROVIDER_PATHS"
        android:resource="@xml/file_provider"/>
</provider>

应用内安装Apk

Android 6.0 或以下可使用

public static void install(Context context) {
    Intent intent = new Intent(Intent.ACTION_VIEW);
    intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
    intent.setDataAndType(Uri.fromFile(
        new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), "myApp.apk")), "application/vnd.android.package-archive");
    context.startActivity(intent);
}

Android 7.0 或以上需使用

if (Build.VERSION.SDK_INT >= AppConstants.ANDROID_VERSION_7) {
    File file = new File(getContext().getCacheDir(), "myApp.apk");
    //获取存储文件的uri,此处需传入Manifest上声明的provider的authorities
    Uri uri = FileProvider.getUriForFile(mContext, "com.test.pr.fileprovider", file);
    install.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
    //赋予临时权限
    install.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
    //设置dataAndType
    install.setDataAndType(uri, "application/vnd.android.package-archive");
    context.startActivity(intent);
}

Android 8.0 或以上

还需要在 Manifest 上声明获取以下权限

<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>

未有此权限时可主动引导用户去设置

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
    //获取是否有安装未知来源应用的权限
    if (!getContext().getPackageManager().canRequestPackageInstalls()) {
        Toast.makeText(getContext(), "请打开安装未知来源应用的权限", Toast.LENGTH_SHORT).show();
        Uri packageURI = Uri.parse("package:" + EfnTestApplication.getContext().getPackageName());
        Intent intent = new Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES, packageURI);
        getContext().startActivity(intent);
        return;
    }
}

注意,更新的Apk版本号必须比现应用版本号高,且签名一致,才能安装成功。

打包

Apk

操作路径:Build->Build Bundle(s) / APK(s)->Build APK(s)
产出路径:[project]/[module]/build/outputs/apk/debug

选择Build Bundle(s)的话,产出的是aab(Android Application Bundle),aab是一种新型的上传包格式,它能包裹应用所编译的代码和资源,但只能用于Google Play。

若要使APK带签名的,则选择Generate Signed Bundle / APK,这项是需要指定keystore文件和其密码及别名的。

jar

使用Android Studio对工程进行编译后,就会自动生成jar,所在路径为

/build/intermediates/bundles/debug(release)/classes.jar
/build/intermediates/packaged-classes/release/classes.jar

但若想删除jar中一些无用的内容,可以在要打jar的Module中的build.gradle里自定义一个打jar的Task,如下为制作一个指定输出路径、输出文件名、删除BuildConfig.class文件的jar

task makeJar(type: Jar, dependsOn: ['assembleRelease']) {
    destinationDir = file('build/outputs/libs')
    baseName = "jar_name" // SDK名称
    version = "1.0.0" // 版本号
    from(zipTree('build/intermediates/packaged-classes/release/classes.jar')) // jar的实际来源
    exclude('com/xx/yy/BuildConfig.class')
}

由上面build.gradle中可以看出,我们单独去调用Gradle的assembleRelease这个Task,也是可以进行编译生成jar的。

arr

对比jar,aar是更适合作为SDK的格式,因为它可以同时把class和res文件一起打包。

打包方式也很简单,只要在一个Project下,新建一个用于打包的Module(Android libraries),SDK的源码和资源都在这个Module下添加,那么只要此Module一被编译,aar就能被生成,生成路径为

/build/outputs/aar/libraryname.aar

开发时在Project下新建另一个Module,作为Demo App,其依赖上述的aar Module,就可以实现模拟接入和调试。

设计模式

MVP

在MVC的基础上增加了Presenter层后,Activity应该要改为视作View层,其原本控制器的职能已转移到了Presenter层上,进一步减少View和Model的耦合。

做法:

  • 抽象View层的更新界面接口,由View(Activity)实现此接口,接口提供传入Value Object的方法去刷新View上的数据值
  • 抽象出Presenter绑定(泛型的)抽象View的接口类,具体业务Presenter实现此接口并持有(绑定)View和(创建)Model(即每一个Activity都有一个相应的Presenter来处理数据)
  • 具体业务Presenter需同时作为Model获取数据后的回调对象,即Model也持有Presenter,Presenter给Model提供回传出Value Object的获取数据接口
  • 具体业务Presenter控制Model去获取数据和刷新View,以及响应View修改Model的数据

MVVM

将Presenter改名为ViewModel,基本上与MVP模式完全一致。唯一的区别是,它采用双向绑定(data-binding):View的变动,自动反映在ViewModel,反之亦然,即是Presenter不但要作为Modle的回调对象,还要作为View的回调对象,拿MVP上的做法为例,若View在实现时也持有了(创建)Presenter和会调用Presenter的更新数据接口,那么这个Presenter就可以理解为ViewModel了,这时候Model层也应该得抽象出一套业务逻辑接口

总结

在Android Studio上选择一个模板创建起一个应用(Google提供了挺多流行的界面框架选择),从MainActivity上开始一步步运用上面所写的各项知识点,例如使用MVP设计App的框架、读取资源显示到UI上、布局自己的界面、根据需求安插各式各样的控件、用列表展示出设计好的模型数据等等,再结合自己的业务Idea,搭建出属于你的Android App吧。

但还有以下一些我未用到、上面也没详细介绍的功能,日后有机会再补充补充

参考

AndroidManifest.xml详解

View与ViewGroup的概念

Adapter基础讲解

Android Adapter:ArrayAdapter篇

浅析 MVP 中 model 层设计

Android MVP 实例

谈谈我理解的Android应用架构

Android 多线程:手把手教你使用AsyncTask

Handler的使用方式和注意事项

Handler机制的实现与工作原理

Android 6.0 7.0 8.0三个版本Install Apk 采坑记录


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 mingfungliu@gmail.com

文章标题:开发一个Android App

文章字数:14.9k

本文作者:Mingfung

发布时间:2019-10-05, 16:11:04

最后更新:2022-01-13, 17:30:49

原始链接:http://blog.ifungfay.com/Android/开发一个Android-App/

版权声明: "署名-非商用-相同方式共享 4.0" 转载请保留原文链接及作者。

目录
×

喜欢就点赞,疼爱就打赏

宝贝回家