Android 应用中的整洁代码

工程 | Roy Clarkson | 2011 年 8 月 26 日 | ...

假设有一天早上你醒来,心想:“嘿,我今天要开发一个 Android 应用。”首先,这是个不错的选择!截至六月底,每天有 500,000 台 Android 设备被激活,甚至超过了 iPhone。这意味着你的应用有一个庞大的潜在受众。此外,Android 是用 Java 构建的。这看起来可能没什么大不了,但我曾在 Objective-CiOS 平台上工作了几年,虽然我现在已经很熟悉它,但 iOS SDK 的学习曲线比我在 Android 上遇到的要陡峭。当我刚开始使用 Android SDK 时,Android 感觉更容易上手。话虽如此,它与你过去构建过的任何其他 Java 应用都有一些明显的区别,我将在第一部分介绍其中的一些区别。

时光荏苒,你完成了你的第一个应用,并将其提交到了 Android Market。值得祝贺的是,你的朋友们都在下载你的应用并在 Twitter 上讨论它。现在是时候开始你的第二个应用了。你花几天时间,突然意识到你开始重用第一个应用中的代码,这本身并不是一件坏事。代码重用可能很有价值。但你注意到有很多样板代码经常重复出现,这可能会分散你对业务逻辑的关注。幸运的是,有一些方法可以对此进行改进。

在这篇博客文章中,我将概述 Android 及其应用生命周期,并讨论框架施加的一些限制。我还将回顾一些技术和第三方项目,它们可以帮助你整理 Android 代码,让你专注于希望通过应用实现的目标。

Android 概览

让我们先简要概述一下 Android 的工作原理。Android 应用(app)使用 Java 构建,并编译成 class 文件。然后 class 文件被编译成 Dalvik Executable (DEX) 格式,以便它们可以在 Android 使用的 Dalvik 虚拟机上运行。转换为 DEX 格式后,class 文件被压缩成 Android Package (APK) 以分发到设备上。由于使用了 DEX 格式,Dalvik VM 并不是一个真正的 Java 虚拟机,因为它不操作 Java 字节码。此外,Dalvik VM 的核心类库基于 Apache Harmony 项目的一个子集。这意味着你在 Java SE 中习惯的许多类和方法是可用的,但肯定不是全部。我发现 Android 开发者网站上的 API 参考文档是审查这些差异的宝贵资源。

默认情况下,Android 操作系统会为每个 Android 应用分配一个唯一的 Linux 用户 ID。当系统启动时,应用会在自己的 Linux 进程中运行,并在自己的虚拟机 (VM) 内运行。系统在需要时管理该进程的启动和关闭。正如你所猜测的,这意味着每个应用都与其他正在运行的应用隔离运行。安装时,应用可以请求访问硬件功能或与其他应用交互的权限。用户可以选择授予应用这些权限或选择不安装它。应用所需或请求的权限在每个应用的 Android Manifest 文件中定义。这是一个 XML 文件,列出了应用的所有组件以及这些组件的任何设置。应用组件有四种类型:activities (活动)services (服务)content providers (内容提供者)broadcast receivers (广播接收者)。出于本文的目的,我将重点关注活动。

Activity (活动) 基本代表 Android 应用的一个屏幕。例如,一个 Twitter 应用可能有一个登录屏幕、一个显示推文列表的屏幕以及一个撰写新推文的屏幕。这些屏幕中的每一个都代表了应用中不同的 activity。作为开发者,你永远不会自己实例化一个 activity 对象。Activities 通过发送一个称为 Intent (意图) 的异步消息来激活,如下面的示例所示。


startActivity(new Intent(context, HomeActivity.class));

当调用 startActivity(Intent intent) 时,系统会创建一个新实例或重用一个现有实例,以便向用户显示 activity。重要的是,系统控制着应用和每个 activity 的启动、停止、创建和销毁。如果你想与这个过程交互,那么应用和 activity 类提供了针对不同生命周期事件的方法,你可以在子类中重写这些方法。

依赖注入

Spring Android 项目最近发布了其第四个里程碑版本。通过该版本,我们继续改进了对 Android 的 RestTemplateSpring Social 支持,这简化了进行 RESTful HTTP 请求和访问由 OAuth 保护的 REST API 的过程。虽然我们认为这些是 Android 开发的宝贵补充,但一些开发者询问了关于 Spring Android 中是否可能支持依赖注入的问题,因为正如你可能知道的,Spring Framework 已经提供了一个流行的控制反转 (IOC) 容器,用于在企业级 Java 应用中实现依赖注入。在 Spring Android 的早期规划阶段,依赖注入支持被确定为可能纳入项目的功能。当时,还不清楚该支持会包含什么,以及如何实现。因此,我开始研究和调查在 Android 中执行依赖注入的可行方法和限制。

那么,什么是依赖注入?如果你问两个不同的开发者,你可能会得到两个不同的答案。你可能会听到关于 IOC、XML 文件、注解或其他实现细节的说法。实际上,依赖注入只是一种通过将对象所需的资源直接提供给它来减少耦合的技术,而不是让对象自己去环境中寻找。这听起来足够简单,你可能会想,通过类构造函数和 setter 方法已经可以做到这一点了,这是完全正确的。然而,回想上面的概述部分,Android 系统驱动着应用的生命周期,所以我们执行这种操作的方式是受限的。

Android 方式

不使用任何第三方库,将依赖项传递给 Activity 相当容易。如前所述,系统创建应用实例。因此,通过扩展 application,你可以有效地创建一个单例依赖项实例,然后应用的任何 activity 都可以访问它。


public class MainApplication extends Application  {

    private MyService service;

    @Override
    public void onCreate() {
        super.onCreate();
        service = new MyServiceImpl();
    }

    public MyService getMyService() {
        return this.service;
    }
}

activity 类有一个名为 getApplication() 的方法,该方法返回拥有该 activity 的 application 对象的引用。我们只需将其强制转换为 MainApplication,就可以访问 MyService 的 getter 方法。当然,activity 现在必须“知道”关于 application 的信息,这可能看起来像一个缺点。但请记住,activity 已经知道它的 application。该方法是内置的。


public class MainActivity extends Activity  {

    private MyService service;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        MainApplication app = (MainApplication) getApplication();
        service = app.getMyService();
    }
}

RoboGuice

RoboGuice 项目利用 Google 的 Guice 库为 Android 添加依赖注入支持。Guice 本身有两种版本,带 AOP (Aspect Oriented Programming,面向切面编程) 支持和不带 AOP 支持。在内部,标准 Guice 依赖于字节码生成来执行方法拦截。然而,Android 不支持运行时字节码生成,因此 RoboGuice 依赖于不带 AOP 的 Guice 版本。让我们看看如何使用 RoboGuice 实现前面的示例。要添加自定义绑定,你必须实现一个继承自 RoboApplication 的 Application 对象。然后重写 addApplicationModules(…) 方法,并添加一个绑定你对象的模块实例。


public class MainApplication extends RoboApplication {

    @Override
    protected void addApplicationModules(List<Module> modules) {
        // add your module with custom bindings
        modules.add(new MainModule());
    }

    @Override
    public void onCreate() {
        super.onCreate();
    }
}

AbstractAndroidModule 继承自标准的 Guice AbstractModule。重写 configure() 方法来指定绑定


public class MainModule extends AbstractAndroidModule {

    @Override
    protected void configure() {
        bind(MyService.class).to(MyServiceImpl.class);
    }
}

每个 activity 都必须继承自 RoboActivity,以便进行注入


public class MainActivity extends RoboActivity {

	@Inject 
	MyService service;

	@Override
	public void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
	}
}

这个例子并不是特别令人印象深刻,因为它需要更多代码才能完成类似的任务。RoboGuice 的布线在包含大量业务逻辑的多个领域模块组成的大型应用中会变得更有用。此外,它提供的注入视图、资源和系统服务的支持通常也很有用。你可以从下面的示例中看到这一点。第一个例子展示了标准的 Android 方法,而第二个则使用了 RoboGuice。如你所见,RoboGuice 允许你消除大部分样板查询代码。


public class MyActivity extends Activity {
	
    private TextView label;
    
    private Drawable image;

    private SearchManager searchManager;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.myactivity);
        this.label = (TextView) findViewById(R.id.mylabel);
        this.image = getResources().getDrawable(R.drawable.myimage);
        this.searchManager = (SearchManager) getSystemService(Activity.SEARCH_SERVICE);
    }
}

public class MyActivity extends RoboActivity {
	
    @InjectView(R.id.mylabel)
    TextView label;

    @InjectResource(R.drawable.myimage)
    Drawable image;

    @Inject
    SearchManager searchManager;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.myactivity);
    }
}

这里的编程模型还有一些额外的事情让我印象深刻。以前我们继承自标准的 Android 类,现在我们继承自 RoboGuice 特定的变体。这又回到了前面关于 Android 架构的部分。需要继承框架特定的类型,是因为第三方框架接入 Android 应用生命周期的方式是有限的。此外,注入是在运行时执行的,这带来了性能和占用空间的成本。最后,值得注意的是,最新的发布版本基于 Guice 2.0 构建。我的理解是正在进行 Guice 3.0 的更新。

我目前还不确定 RoboGuice 的好处是否足够大,值得将你的应用与第三方框架紧密耦合。我通常发现自己手动实现依赖注入技术就足够好了,正如前一节所概述的,并在 Greenhouse 参考应用中展示的那样。此外,Guice 本身并不小,会使你的应用大小增加约 400 KB。RoboGuice 项目页面甚至在配置 ProGuard 以减小应用大小时讨论了这一点。虽然配置 ProGuard 不一定微不足道,但无论你是否使用 RoboGuice,在任何应用上使用它都是一个好习惯。

超越依赖注入

我们已经介绍了依赖注入和 RoboGuice,并讨论了该项目方法的优缺点。这篇文章的标题是关于 Android 中的整洁代码以及如何减少冗余,所以现在让我们超越依赖注入,讨论一些其他技术。

Android Annotations

Android Annotations 是 Pierre-Yves Ricau 启动并维护的一个项目。该项目的目标是利用注解来专门帮助减少 Android 项目中的样板代码。它并不试图成为一个通用的依赖注入框架,事实上,它可以与 RoboGuice 并行工作。它们之间存在一些重叠,因为 Android Annotations 提供了一些类似的注解用于注入视图和资源,但它也提供了许多其他有用的功能。主要区别在于,Android Annotations 在编译时生成样板代码,因此使用它没有运行时性能开销。它通过为每个 activity 生成一个子类,并将注解替换为标准的样板代码来实现这一点。这种方法的一个小缺点是,你必须在清单文件中的每个 activity 名称后附加一个下划线。例如,如果我创建一个 MyActivity 类,Android Annotations 将生成一个相应的 MyActivity_ 类。此外,startActivity(…) 中的任何引用也必须更新以使用新类。

在这里你可以看到与 RoboGuice 的一些相似之处。这是前面 RoboGuice 示例的 Android Annotations 版本。请注意,在类上使用 @EActivity 注解可以设置 activity 的布局。另一个不错的细节是,如果你省略注解中的资源 ID,Android Annotations 将查找与变量名匹配的资源 ID。


@EActivity(R.layout.myactivity) // Sets content view to R.layout.myactivity
public class MyActivity extends Activity {

    @InjectView  // Injects R.id.mylabel
    TextView mylabel;

    @DrawableRes(R.drawable.myimage)
    Drawable image;

    @SystemService
    SearchManager searchManager;
}

这个注解库相当广泛。目前,它支持 SDK 的几个领域

  • 视图
  • 事件处理
  • 访问资源
  • 附加数据
  • 系统服务
  • 线程处理

这里还有一些其他可能实现的示例。我想重点介绍点击事件处理和线程支持,因为它们确实展示了这个库的强大之处。


public class AnotherActivity extends Activity {

    @Click // When R.id.runProcess button is clicked 
    void runProcess() {
        runInBackground();
    }
 
   @Background // Executed in a background thread
   void runInBackground() {
        // perform some long running task
        notifyUser();
    }
   
    @UiThread // Executed in the ui thread
    void notifyUser() {
         // display a notification that task is complete
    }
}

一些开发者可能对代码生成望而却步,但考虑到 Android 架构的限制,Android Annotations 提供了一种优雅的方法来最小化样板代码的使用,帮助你专注于业务逻辑,同时不增加运行时占用空间。

Android Binding

我想提及的最后一个项目是 Android Binding。Android Binding 是一个 MVVM (Model-View-ViewModel) 框架,旨在将 activity 与用户界面的直接操作分离。为此,几乎所有的代码都被移出了 Android activity 类,并放在一个 ViewModel 对象中,这有助于提高可测试性。ViewModel 对象处理视图(布局)的所有事件和数据,但它与视图没有紧密耦合。视图中发生的事件作为命令发送到 ViewModel。这些命令然后执行一些业务逻辑,可能与模型交互,并更新 ViewModel 的属性,以便视图可以绑定到它们。如果你曾使用过 .Net,那么数据绑定的概念对你来说会非常熟悉,Android Binding 正努力将这一概念应用于 Android。Android Binding 由 Andy Tsui 启动并维护。它仍然很年轻,缺乏文档,但有一些不错的潜力。

下面的示例展示了一个继承自 BindingActivity 的 EmailActivity。当你继承自 BindingActivity 时,你使用 setAndBindRootView(…) 方法为视图设置 Model


public class EmailActivity extends BindingActivity {

    private EmailViewModel model;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        model = new EmailViewModel();
        setAndBindRootView(R.layout.main, model);
    }
}

下一个示例片段展示了视图声明。对我来说,这是真正有趣的部分。查看这个布局文件,你可以看到添加了“binding”属性,以及自定义的“binding” xmlns 声明。此示例展示了绑定两个文本字段和一个按钮,但该项目支持绑定到许多其他控件


<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:binding="http://www.gueei.com/android-binding"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent">
    		
    <EditText
        android:id="@+id/subject"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        binding:text="subject"
    />
		
    <EditText
        android:id="@+id/message"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        binding:text="message"
    />
	
    <Button
        android:id="@+id/submit"
        android:text="Submit"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        binding:onClick="submit"
    />
	
</LinearLayout>

最后,EmailViewModel 包含视图的数据和控制逻辑。下面的摘录显示了文本字段值的 Observables,以及单击提交按钮时执行的 Command


public class EmailViewModel {

    @Required
    public StringObservable subject = new StringObservable();

    @Required
    public StringObservable message = new StringObservable();

    public Command submit = new Command() {
        public void Invoke(View arg0, Object... arg1) {
            ValidationResult result = ModelValidator.ValidateModel(EmailViewModel.this);
            if (result.isValid()){
                // send email
            } else {
                // display validation error message
            }
        }		
    };
}

这个库的功能远不止我在此处包含的这些。它还通过使用一组注解来支持模型验证。其中包含一些内置规则,你也可以使用正则表达式匹配,以及实现自己的自定义规则。最新版本还添加了对绑定到选项菜单的支持。

与 RoboGuice 一样,Android Binding 是基于运行时的,并且要求你继承特定的 Activity 基类。这种对具体继承的依赖可能会导致在同一应用中同时使用 RoboGuice 和 Binding 变得困难。我认为 Binding 真正闪光的地方在于需要数据输入和验证要求的大型业务应用。声明式的 xml 绑定语法非常不错。我不熟悉其他项目在做这个,它展示了 Android 开发的一个新视角。

结语

所有这些技术和项目都在努力在 Android 框架内实现已知的设计模式。这是一个值得称赞的目标,因为如果正确应用,这些模式可以帮助我们更好地构建应用。一般来说,当我接触一个新的平台或语言时,我首先尝试使用 SDK 提供的功能。然后我可以决定是使用第三方库还是应用特定的设计模式。我的担忧之一是,有时在一种情况或技术中运行良好的设计模式或想法,在另一种情况或技术中却无法很好地转换。尽管 Android 架构存在限制,本文重点介绍的技术和项目仍然成功地帮助你编写更整洁的代码。

在本文讨论的所有方法中,我对 Android Annotations 最感兴趣。我喜欢它,因为它旨在保持你的代码整洁和可读,同时不牺牲性能或使用额外的资源。因为它在编译时生成标准的 Android 代码,这意味着你无需通过第三方库即可调试你的应用。Spring Roo 采用了非常相似的方法,只是其代码生成模型基于 AspectJ 而不是 Java 注解处理。如果 Android Annotations 项目可以集成到 Spring Roo 插件中,那将令人兴奋。这将允许社区创建新的 Roo 命令来搭建 Android 项目和类型等。此外,由于 Roo 使用 AspectJ 编译时织入,因此可以消除对使用下划线命名生成的单独类的需求。

总而言之,我展示了一些技术和第三方库的使用,以帮助消除样板代码并简化 Android 应用的开发过程。是的,准备环境需要一些设置。但是,其好处可以超过最初的时间投入。我建议花一些时间研究这些项目和技术。它们都为 Android 开发提供了独特且有价值的视角。

感谢阅读!我非常乐意听到你的反馈。你如何看待本文讨论的库 - 你正在使用它们吗?还有哪些你认为必不可少的库?你是否认为一个基于 Android Annotations 项目构建的 Android Roo 插件会有价值?

获取 Spring 通讯

订阅 Spring 通讯,保持联系

订阅

抢占先机

VMware 提供培训和认证,助力你的职业发展。

了解更多

获取支持

Tanzu Spring 在一个简单的订阅中提供了对 OpenJDK™、Spring 和 Apache Tomcat® 的支持和二进制文件。

了解更多

即将举行的活动

查看 Spring 社区所有即将举行的活动。

查看全部