Android 架构组件 -App 架构指南,你还不收藏嘛

Android-App的设计架构经验谈,2024Android面试真题精选干货整理 组件化就是将一个app分成多个Module,如下图,每个Module都是一个组件(也可以是一个基础库供组件依赖),开发的过程中我们可以单独调试部分组件,组件间不需要互相依赖,但可以相互调用,最终发布的时候所有组件以lib的形式被主app工程依赖并打包成一个apk。是ARouter是阿里巴巴开源的Android平台中对页面、服务提供路由功能的中间件,提倡的是简单且够用。主要用作组件化通信。 阅读详情

本指南适用于那些已经拥有开发Android应用基础知识的开发人员,现在想了解能够开发出更加健壮、优质的应用程序架构。

先给大家来点小福利吧,架构全套学习资料以及高清视频教程,教你如何一步一步走上架构师之路

领取方式,关注小编私信【学】即可获取

 

 

首先需要说明的是:Android Architecture Components 翻译为 Android架构组件 并不是我自己随意翻译的,而是Google官方博客中明确称其为 Android架构组件,因此我遵循了这种叫法。

下面这张图是Android架构组件完整的架构图,其中表示了的架构组件的所有模块以及它们之间如何交互:

Android架构组件-App架构指南,你还不收藏嘛

 

APP开发者面临的常见问题


与传统的桌面应用程序不同,Android应用程序的结构要复杂得多,在大多数情况下,它们只在桌面快捷启动方式中有一个入口,并且作为单个进程运行。一个典型的Android应用程序是由多个 APP组件(Android四大组件) 构成的,包括 activities, fragments, services, content providers and broadcast receivers。

这些 app组件 中的大部分都是在 应用清单(AndroidManifast.xml)中声明的,Android操作系统使用这些组件将应用程序集成到设备的用户界面中。虽然,应用程序通常上是以单个进程运行的,但是一个合理的Android应用需要更加灵活,因为用户可以通过不同的应用程序,在他们的设备上不断切换流程和任务。

想象下在我们最喜爱的社交网络应用中分享照片时会发生什么情况。首先这个应用程序触发一个Camera(拍照或摄像) Intent,由Android操作系统启动一个Camera应用来处理请求。此时,用户虽然离开了这个社交网络应用,但他们的体验是无缝的。相机应用程序又可能触发其他 Intent,例如启动文件管理器,该文件管理器可以启动另一个应用程序,最终用户回到社交网络应用并分享照片。此外,用户在这个过程的任何时候都可能被电话打断,并在打完电话后回来继续分享照片。

在Android中,这种应用程序跳转行为是很常见的,所以我们的应用程序必须正确处理这些流程。请记住,移动设备是资源受限的,所以在任何时候,操作系统都可能需要杀死一些应用程序,以腾出空间给新的应用。

这一切的要点在于,我们的 app组件 可以单独和无序地启动,并且可以在任何时候由用户或系统销毁。由于 app组件 是短暂的,并且它们的生命周期(创建和销毁时)不在我们的控制之下,因此我们不应该在app组件中存储任何 app数据或状态,并且 app组件不应相互依赖。

通用架构原则


如果不使用 app组件存储app数据和状态,那该如何构造应用程序呢?

我们需要关注的最重要的事情是:如何在你的应用中分离关注点。最常见的错误是将所有的代码写入一个 Activity 或 Fragment,任何不处理 UI 或 与操作系统交互的代码都不应该出现在这些类中,我们应该尽可能保持 Activity 或Fragment 精简,这样可以避免许多生命周期相关的问题。

请记住,我们不拥有这些类,它们只是建立操作系统和我们的应用程序之间契约的胶水类。Android操作系统可能会随时根据用户交互或其他因素(如低内存)来销毁它们,最好尽可能地减少依赖他们,以提供可靠的用户体验。

第二个重要原则是: 你应该从一个 Model 驱动你的UI,最好是一个持久化的 Model。之所以说持久化是理想的 Model,原因有两个:如果操作系统销毁你的应用程序以释放资源,那么你的用户就不会丢失数据,即使网络连接不稳定或连接不上,你的应用程序也会继续工作。

Model 是负责处理应用程序数据的组件,它们独立于应用程序的 Views 和 app组件,因此 Model 与这些 app组件的生命周期问题是相隔离的。保持简洁的UI代码,以及不受约束的应用程序逻辑,可以使app的管理更加容易,基于具有明确定义的管理数据责任的模型类的应用程序,会更加具有可测试性,并使我们的应用程序状态保持前后一致。

推荐的App架构


在本节中,我们将演示如何通过使用用例来构造使用了 架构组件(Architecture Components) 的应用程序。

注意:不可能有一种编写应用程序的方法对每个场景都是最好的。对于大多数用例来说,推荐的这个架构可能是一个好的起点。如果你已经有了编写Android应用的好方法,那就不要在更改了。

假如我没正在搭建一个用来显示 用户概况的UI,该用户概况将使用 REST API 从我们自己的服务器端获取。

搭建用户界面

这个UI 将由 UserProfileFragment.java 及 Fragment 相应的 user_profile_layout.xml 布局文件组成。

为了驱动用户界面,我们的数据模型需要保存两个数据元素。

  • 用户ID:用户的标识符。最好使用 fragment 参数(setArguments方法) 将此信息传递到 fragment 中。如果Android系统销毁了你的进程,这些信息将被保留,便于应用在下次重新启动时可用。
  • 用户对象:保存用户数据的 POJO(简单的Java对象)。

我们将创建一个基于ViewModel 的 UserProfileViewModel 类来保存这些信息。

一个 ViewModel 提供了一个特定 UI 组件中的数据,如一个 fragment 或 activity, 并且负责与数据处理业务的通信,例如调用其他 app组件 来加载数据或转发用户信息的修改。ViewModel不知道View,并且不受配置更改的影响,例如由于屏幕旋转而重新创建 Activity。

现在我们有3个文件。

  • user_profile.xml:定义屏幕上的 UI。
  • UserProfileViewModel.java:为 UI 准备数据的类。
  • UserProfileFragment.java:显示 ViewModel 中的数据并对用户交互作出响应的 UI 控制器。

接下来我们将开始实现(为了简单起见,省略了布局文件):

public class UserProfileViewModel extends ViewModel {

    private String userId;
    private User user;

    public void init(String userId) {
        this.userId = userId;
    }

    public User getUser() {
        return user;
    }
}12345678910111213
public class UserProfileFragment extends Fragment {
    private static final String UID_KEY = "uid";
    private UserProfileViewModel viewModel;

    @Override
    public void onActivityCreated(@Nullable Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        String userId = getArguments().getString(UID_KEY);
        viewModel = ViewModelProviders.of(this).get(UserProfileViewModel.class);
        viewModel.init(userId);
    }

    @Override
    public View onCreateView(LayoutInflater inflater,
                @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        return inflater.inflate(R.layout.user_profile, container, false);
    }
}123456789101112131415161718

现在,我们如何将它们联系在一起呢?当给 ViewModel 的 User 字段设值后,我们需要一种方法来通知用户界面,这就是 LiveData类的作用。

LiveData 是一个可观察的数据持有者。它允许应用程序中的组件观察 LiveData 对象的更改,但不会在它们之间创建明确的和严格的依赖关系路径。 LiveData 还会关联 app组件(activities, fragments, services) 的生命周期状态,并做出合适的事情来防止内存泄漏。


注意:如果你已经在使用类似 RxJava 或 Agera 的库 ,则可以继续使用它们而不是LiveData。但是,当你使用它们或其他方式时,请确保正确处理生命周期,以便在相关的LifecycleOwner 停止时暂停数据流,并在销毁 LifecycleOwner 时销毁数据流。你还可以添加android.arch.lifecycle:reactivestreams 以将 LiveData 与其他的响应流库(例如RxJava2)一起使用。

现在我们用 LiveData<User> 替换 UserProfileViewModel 中的 User 字段,以便在数据更新时通知 Fragment。最主要的是:LiveData是生命周期感知的,并且在不在需要时,它将自动清理引用。

public class UserProfileViewModel extends ViewModel {
    ...
    private User user;
    private LiveData<User> user;

    public LiveData<User> getUser() {
        return user;
    }
}123456789

现在我们修改 UserProfileFragment 以便观察数据并更新 UI。

@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
    super.onActivityCreated(savedInstanceState);
    viewModel.getUser().observe(this, user -> {
      // update UI
    });
}1234567

每次更新用户数据时, 都会调用 onChanged 回调,并刷新 UI。

如果你熟悉其他 可观察回调的库,你可能已经意识到,我们没有重写 fragment 的 onStop() 方法来停止观察数据。这对于 LiveData来说是没有必要的,因为它是生命周期感知的,这意味着它不会调用回调,除非 Fragment 处于 活动状态(已收到 onStart() 但未收到 onStop())。当 fragment 收到 onDestroy() 时,LiveData也将自动移除观察者 。

对于配置变化(例如,用户旋转屏幕)我们也没有做任何特殊的处理。当配置改变时,ViewModel 会自动恢复,所以一旦新的 Fragment 生效,它将接收到相同的 ViewModel实例,并且 ViewModel 的回调将立即被当前数据调用,这就是 ViewModels 为什么不应该直接引用 Views 的原因,他们可以比 View的生命周期更持久。想了解更多信息的请查看 The lifecycle of a ViewModel 。

获取数据


现在我们已经将 ViewModel 关联到了 Fragment,但是 ViewModel 如何获取用户数据呢?在这个例子中,我们假设服务器端提供了一个 REST API。我们将使用 Retrofit 库来访问我们的服务器端,虽然你可以自由使用不同的库来达到同样的目的。

下面是retrofit 的 Webservice ,负责与服务器端进行通信:

public interface Webservice {
    /**
     * @GET declares an HTTP GET request
     * @Path("user") annotation on the userId parameter marks it as a
     * replacement for the {user} placeholder in the @GET path
     */
    @GET("/users/{user}")
    Call<User> getUser(@Path("user") String userId);
}123456789

ViewModel 的一个简单实现是直接调用 Webservice 来获取数据并将其 赋值给 user 对象,虽然这样是可行的,但是我们的应用程序以后将很难维护。它赋予了 ViewModel 类太多的职责,违背了我们前面提到的关注点分离原则。此外,ViewModel 的作用域与一个 Activity 或一个 Fragment 生命周期相关联,当他们的生命周期完成时将丢失所有的数据,这是非常糟糕的用户体验。因此,我们将 ViewModel 的这个工作委托给了一个新的模块 Repository 。

Repository 模块负责数据处理操作。他们为应用的其余部分提供了一个干净的API,他们知道从何处获取数据以及在更新数据时调用哪些API。你可以将它们视为不同数据源 (持久化模型, web服务, 缓存, etc.)之间的中介。

UserRepository 类使用 WebService 来获取用户数据项,如下:

public class UserRepository {

    private Webservice webservice;
    // ...
    public LiveData<User> getUser(int userId) {
        // This is not an optimal implementation, we'll fix it below
        final MutableLiveData<User> data = new MutableLiveData<>();
        webservice.getUser(userId).enqueue(new Callback<User>() {
            @Override
            public void onResponse(Call<User> call, Response<User> response) {
                // error case is left out for brevity
                data.setValue(response.body());
            }
        });
        return data;
    }
}1234567891011121314151617

虽然 repository 模块看起来没有必要,但是它有一个重要的目的,它从应用程序的其余部分提取数据源。现在我们的 ViewModel 不知道数据是从 Webservice 获取到的,这意味着我们可以根据需要,将它(Webservice)替换为其他的实现。

注意:为了简单起见,我们忽略了网络错误的情况。对于暴露错误和加载状态的另一个实现,请查看 附录:暴露网络状态。

管理组件之间的依赖关系:

上面的 UserRepository 类需要一个 Webservice 的实例来工作,UserRepository 可以简单地创建Webservice ,但要做到这一点,它必须需要知道 Webservice 类的依赖关系来构造它,这会使代码显著和成倍的复杂化(例如,每个需要 Webservice实例的类 都需要知道如何用它的依赖来构造它)。另外,UserRepository 可能不是唯一需要 Webservice 的类。如果每个类创建一个新的 WebService,这将是非常沉重的资源。

现在我们有两种模式可以用来解决这个问题:

  • 依赖注入:依赖注入允许类在不构造它们的情况下定义它们的依赖关系。在运行时,另一个类负责提供这些依赖关系。我们推荐 Google 的 Dagger 2 库,在Android应用中实现依赖注入。Dagger 2 通过遍历依赖关系树来自动构造对象,并为依赖关系提供编译时间保证。
  • 服务定位器:服务定位器提供了一个注册表,这个类可以获得它们的依赖 而不是 构建它们。实现起来比依赖注入(DI)更容易,所以如果你不熟悉DI,可以使用 Service Locator。

这些模式允许您扩展代码,因为它们提供了用于管理依赖关系的清晰模式,无需复制代码或增加复杂性。这两个模式也允许交换实现测试, 这是使用它们的主要好处之一。

在这个例子中,我们将使用 依赖注入 来管理依赖关系。

关联ViewModel和repository


现在我们修改 UserProfileViewModel 使用的 repository。

public class UserProfileViewModel extends ViewModel {

    private LiveData<User> user;
    private UserRepository userRepo;

    @Inject // UserRepository parameter is provided by Dagger 2
    public UserProfileViewModel(UserRepository userRepo) {
        this.userRepo = userRepo;
    }

    public void init(String userId) {
        if (this.user != null) {
            // ViewModel is created per Fragment so
            // we know the userId won't change
            return;
        }
        user = userRepo.getUser(userId);
    }

    public LiveData<User> getUser() {
        return this.user;
    }
}1234567891011121314151617181920212223

缓存数据


上面的 repository 实现 对抽象调用 Web服务是有好处的,但是因为它只依赖于一个数据源,所以它不是很有用。

UserRepository 实现的问题是,在获取数据之后,它不保存在任何地方。如果用户离开 UserProfileFragment 并返回,应用程序将重新获取数据。这是不好的,原因有两个:浪费宝贵的网络带宽并强制用户等待新的查询完成。为了解决这个问题,我们将添加一个新的数据源到 UserRepository ,这个数据源可以将 User 对象 缓存 到内存中。

@Singleton  // informs Dagger that this class should be constructed once
public class UserRepository {
    private Webservice webservice;
    // simple in memory cache, details omitted for brevity
    private UserCache userCache;
    public LiveData<User> getUser(String userId) {
        LiveData<User> cached = userCache.get(userId);
        if (cached != null) {
            return cached;
        }

        final MutableLiveData<User> data = new MutableLiveData<>();
        userCache.put(userId, data);
        // this is still suboptimal but better than before.
        // a complete implementation must also handle the error cases.
        webservice.getUser(userId).enqueue(new Callback<User>() {
            @Override
            public void onResponse(Call<User> call, Response<User> response) {
                data.setValue(response.body());
            }
        });
        return data;
    }
}123456789101112131415161718192021222324

持久化数据


在我们当前的实现中,如果用户旋转屏幕或离开并返回到应用,现有UI将立即可见,因为 repository 从内存中检索缓存的数据。但是,如果用户离开应用程序并且数小时后回来,或Android 系统杀死该进程后,会发生什么?

在目前的实现中,我们将需要从网络上重新获取数据。这不仅是一个糟糕的用户体验,而且会浪费资源,因为它会使用移动数据重新获取相同的数据。你可以简单地通过缓存Web请求来解决这个问题,但是这会产生新的问题。如果相同的用户数据从另一种类型的请求中显示出来(例如,获取朋友列表),会发生什么情况?那么你的应用程序可能会显示不一致的数据,这是一个混乱的用户体验。例如,由于好友列表请求和用户请求可以在不同的时间执行,所以相同用户的数据可能会以不同的方式显示。您的应用需要合并它们以避免显示不一致的数据。

处理这个问题的正确方法是使用 持久化模型。这就是 Room 持久化库可以拯救的地方。

Room 是一个对象映射库,使用最小的模板代码来提供本地数据持久化。在编译时,它会根据 Schema 验证每个查询,因此,有问题的SQL查询会导致编译时出错,而不是运行时失败。Room 抽象了处理原始SQL表和查询的一些底层实现细节。它还允许观察对数据库数据(包括集合和 join 查询)的更改,通过 LiveData对象 公开这些更改 。另外,它明确定义了解决常见问题的线程约束,例如在主线程上的访问存储。


注意:如果你的应用程序已经使用另一个持久化解决方案(如SQLite对象关系映射(ORM)),则不需要使用 Room 替换现有的解决方案。但是,如果你正在编写新的应用程序或重构现有的应用程序,我们建议使用 Room 来保存应用程序的数据。这样,你可以利用库的抽象和查询 验证功能。

要使用 Room,我们需要定义我们的本地 Schema。首先,使用 @Entity 注解 User 类 以将其标记为数据库中的表。

@Entity
class User {
  @PrimaryKey
  private int id;
  private String name;
  private String lastName;
  // getters and setters for fields
}12345678

然后,为我们的 app 创建一个数据库类继承于 RoomDatabase:

@Database(entities = {User.class}, version = 1)
public abstract class MyDatabase extends RoomDatabase {
}123

注意 MyDatabase 是抽象的。Room 自动提供一个它的实现。有关详细信息,请查看 Android架构组件- Room数据库的使用

现在我们需要一种将用户数据插入数据库的方法。为此,我们将创建一个数据访问对象(DAO: data access object)。

@Dao
public interface UserDao {
    @Insert(onConflict = REPLACE)
    void save(User user);
    @Query("SELECT * FROM user WHERE id = :userId")
    LiveData<User> load(String userId);
}1234567

然后,从我们的数据库类中引用 DAO (Data Access Object)。

@Database(entities = {User.class}, version = 1)
public abstract class MyDatabase extends RoomDatabase {
    public abstract UserDao userDao();
}1234

请注意,该 load 方法返回一个 LiveData。Room 知道数据库何时被修改,当数据改变时它会自动通知所有活跃的的察者。因为它使用的是 LiveData,所以这将是有效的,因为只有至少有一个活动的观察者才会更新数据。

注意:Room 根据 table 的修改来检查失效,这意味着它可能发送误报的通知。

现在我们可以修改 UserRepository 来包含 Room 数据源。

@Singleton
public class UserRepository {
    private final Webservice webservice;
    private final UserDao userDao;
    private final Executor executor;

    @Inject
    public UserRepository(Webservice webservice, UserDao userDao, Executor executor) {
        this.webservice = webservice;
        this.userDao = userDao;
        this.executor = executor;
    }

    public LiveData<User> getUser(String userId) {
        refreshUser(userId);
        // return a LiveData directly from the database.
        return userDao.load(userId);
    }

    private void refreshUser(final String userId) {
        executor.execute(() -> {
            // running in a background thread
            // check if user was fetched recently
            boolean userExists = userDao.hasUser(FRESH_TIMEOUT);
            if (!userExists) {
                // refresh the data
                Response response = webservice.getUser(userId).execute();
                // TODO check for error etc.
                // Update the database.The LiveData will automatically refresh so
                // we don't need to do anything else here besides updating the database
                userDao.save(response.body());
            }
        });
    }
}1234567891011121314151617181920212223242526272829303132333435

请注意,尽管我们改变了 来自于 UserRepository 的数据,我们并不需要改变我们 UserProfileViewModel 或 UserProfileFragment。这是抽象提供的灵活性。这对于测试来说有好处的,因为你可以在测试你的UserProfileViewModel 的时候提供一个假的 UserRepository。

现在我们的代码是完整了。如果用户以后回到相同的用户界面,他们会立即看到用户信息,因为我们持久化了。同时,如果数据过期了,我们的仓库将在后台更新数据。当然,根据您的使用情况,如果数据太旧,您可能不希望显示持久化数据。

在一些使用情况下,如 下拉刷新,UI 显示用户是否正在进行网络操作是非常重要的。将UI 操作与实际数据分开是一个很好的做法,因为它可能因各种原因而导致更新(例如,如果我们获取朋友列表,同一用户可能会再次触发 LiveData<User> 更新)。站在UI 的角度,事实上,当有一个请求执行的时候,另一个数据点,类似于任何其他的数据 (比如 User 对象)。

这个用例有两种常见的解决方案:

  • 更改 getUser 为返回包含网络操作状态的 LiveData 。附录中提供了一个示例实现:公开网络状态部分。
  • 在 repository 类中提供另一个可以返回用户刷新状态的公共函数。如果只想响应显式的用户操作(如下拉刷新)来显示网络状态,则此选项更好。

单一的真相来源:

不同的 REST API 端点通常返回相同的数据。例如,如果我们的服务器端拥有另一个返回 朋友列表的端点,则同一个用户对象可能来自两个不同的API 端点,也许粒度不同。如果 UserRepository 从 Webservice请求返回原本的响应,我们的UI可能会显示不一致的数据,因为在这些请求过程中数据可能已经在服务器端发生了改变。这就是为什么在 UserRepository 实现中,Web服务回调只是将数据保存到数据库中。然后,对数据库的更改将触发回调给 活跃状态的 LiveData 对象。

在这个模型中,数据库充当了 单一的真相来源,应用程序的其他部分通过 Repository 访问它。无论你是否使用磁盘缓存,我们都建议将你的 Repository 指定为应用程序其余部分唯一的真相来源。

测试


我们已经提到分离的好处之一就是可测试性,让我们看看如何测试每个代码模块。

  • 用户界面和交互:你唯一需要花费时间的是 Android UI Instrumentation 。测试UI 代码的最好方法是创建一个 Espresso测试。您可以创建 Fragment 并为其提供一个模拟的ViewModel。由于该 Fragment 只与 ViewModel 联系,所以伪造它足以完全测试这个UI。
  • ViewModel:ViewModel 可以使用 JUnit 来测试 。你只需要模拟 UserRepository 来测试它。
  • UserRepository:你同样也可以使用 JUnit 来测试 UserRepository。你需要模拟 Webservice 和 DAO。你可以测试它是否做出了正确的Web服务调用,并将结果保存到数据库中,如果数据已缓存且最新,则不会发出任何不必要的请求。因为 Webservice 和 UserDao 都是接口,你可以模拟它们,或者为更复杂的测试用例创建伪造的实现…
  • UserDao:测试 DAO 类的推荐方法是使用 instrumentation 测试。由于这些 instrumentation 测试不需要任何用户界面,他们将会运行得很快。对于每个测试,您可以创建一个处于内存中的数据库,以确保测试没有任何副作用(如更改磁盘上的数据库文件)。 Room 也允许指定数据库的实现,所以你可以通过提供 JUnit 来测试 SupportSQLiteOpenHelper 的实现。通常不建议使用这种方法,因为设备上运行的SQLite版本可能与主机上的SQLite版本不同。
  • Webservice:使测试独立于外界是很重要的,所以你的 Webservice 测试也应该避免对后端进行网络调用。有很多库可以帮助你,例如, MockWebServer 是一个强大的库,可以帮助你为测试创建一个伪造的本地服务器。
  • Testing Artifacts 架构组件提供了一个Maven artifact 来控制其后台线程。在android.arch.core:core-testing artifact 内部 ,有2个 JUnit 规则:InstantTaskExecutorRule:此规则可用于强制架构组件立即在调用线程上执行任何后台操作。CountingTaskExecutorRule:此规则可用于检测测试,以等待架构组件的后台操作或将其作为闲置资源连接到 Espresso。

最终的架构


下图显示了我们推荐的架构中的所有模块以及它们如何相互交互:

Android架构组件-App架构指南,你还不收藏嘛

 

指导原则


编程是一个创造性的领域,开发Android应用也不例外。解决问题的方法有很多种,可以在多个Activity 或 Fragment 之间传递数据,检索远程数据并将其保存在本地以进行离线模式,也可以使用其他常见应用程序遇到的情况。

虽然以下建议不是强制性的,但是根据我们的经验,从长远来看遵循这些建议将使您的代码更加健壮,变得可测试和可维护。

  • 你在 AndroidManifest 中定义的入口点(activities, services, broadcast receivers, 等等)不是数据的来源。相反,他们只应该协调与该入口点相关的数据子集。由于每个 app组件的 存活相当短,这取决于用户与设备的交互以及当前运行时的状况,因此你不希望这些入口点中的任何一个成为数据源。
  • 在应用程序的各个模块之间建立明确的职责界限时要毫不留情。例如,不要将从网络中加载数据的代码分散到多个类或包中。同样,不要把不相关的职责 - 比如数据缓存和数据绑定 - 放到同一个类中。
  • 尽可能少地暴露每个模块。不要试图创建“只有那一个”的快捷方式,从一个模块公开其内部实现细节。你可能会在短期内获得一些时间,但随着代码库的不断发展,你将会花费更多时间付出技术代价。
  • 在定义模块之间的交互时,请考虑如何使每个模块独立地进行测试。例如,如果有一个定义良好的 API 从网络中获取数据,测试将数据保存在本地数据库中的模块会变得更容易。相反,如果将这两个模块的逻辑混合在一起,或者在整个代码库中分散网络请求代码,那么要测试就会更加困难。
  • 你的APP的核心是让它从其他APP中脱颖而出。不要花费时间重新造轮子,或者一次又一次地写出相同的样板代码。相反,将精力集中在可以让你的应用独特的东西上,让Android 架构 和其他推荐的库处理重复的样板代码。
  • 持久化尽可能多的相关和最新的数据,以便当设备处于离线模式时你的APP依然可用。虽然你可能喜欢恒定的高速连接,但你的用户可能并不会。
  • 你的 repository 应该指定一个数据源作为单一的事实来源。无论你的应用程序何时需要访问这些数据,都应始终从单一的事实源头发起。有关更多信息,请查看 单一的真相来源

附录:暴露网络状态


在上面推荐的App架构部分,我们故意省略了网络错误和加载状态,以保持示例的简单。在本节中,我们演示了如何使用 Resource 类来暴露网络状态以及封装数据及其状态。

以下是一个实现的例子:

//a generic class that describes a data with a status
public class Resource<T> {
    @NonNull public final Status status;
    @Nullable public final T data;
    @Nullable public final String message;
    private Resource(@NonNull Status status, @Nullable T data, @Nullable String message) {
        this.status = status;
        this.data = data;
        this.message = message;
    }

    public static <T> Resource<T> success(@NonNull T data) {
        return new Resource<>(SUCCESS, data, null);
    }

    public static <T> Resource<T> error(String msg, @Nullable T data) {
        return new Resource<>(ERROR, data, msg);
    }

    public static <T> Resource<T> loading(@Nullable T data) {
        return new Resource<>(LOADING, data, null);
    }
}1234567891011121314151617181920212223

因为从网络加载数据,而从磁盘显示数据是一个常见的用例,我们将创建一个辅助类 NetworkBoundResource ,它可以在多个地方重复使用。以下是 NetworkBoundResource 的决策树:

Android架构组件-App架构指南,你还不收藏嘛

 

它首先通过对资源的数据库进行观察。当第一次从数据库加载条目时,**NetworkBoundResource**0 会检查结果是否足够好以便被分发,或者它应该从网络中获取。请注意,这两种情况可能同时发生,因为你可能希望在从网络获取数据时显示缓存的数据。

如果网络调用成功完成,则将响应保存到数据库中并重新初始化流。如果网络请求失败,我们直接发送失败。

注意:在将新数据保存到磁盘之后,我们会重新初始化数据库中的数据流,但通常我们不需要这样做,因为数据库将会发送更改。另一方面,依靠数据库来发送更改,将产生依赖副作用,因为如果数据没有变化,数据库可以避免发送变化,那么它可能会中断。我们也不希望发送从网络返回的结果,因为这将违背单一的真相来源(也许数据库中会有触发机制可以改变保存的值)。我们也不想在没有新数据的情况下发送 SUCCESS,因为它会向客户发送错误的信息。

下面是 公开API 是为 NetworkBoundResource 的子类提供的 :

// ResultType: Type for the Resource data
// RequestType: Type for the API response
public abstract class NetworkBoundResource<ResultType, RequestType> {
    // Called to save the result of the API response into the database
    @WorkerThread
    protected abstract void saveCallResult(@NonNull RequestType item);

    // Called with the data in the database to decide whether it should be
    // fetched from the network.
    @MainThread
    protected abstract boolean shouldFetch(@Nullable ResultType data);

    // Called to get the cached data from the database
    @NonNull @MainThread
    protected abstract LiveData<ResultType> loadFromDb();

    // Called to create the API call.
    @NonNull @MainThread
    protected abstract LiveData<ApiResponse<RequestType>> createCall();

    // Called when the fetch fails. The child class may want to reset components
    // like rate limiter.
    @MainThread
    protected void onFetchFailed() {
    }

    // returns a LiveData that represents the resource, implemented
    // in the base class.
    public final LiveData<Resource<ResultType>> getAsLiveData();
}123456789101112131415161718192021222324252627282930

请注意,上面的类定义了两个类型参数(ResultType, RequestType),因为从 API 返回的数据类型可能与本地使用的数据类型不匹配。

另请注意,上面的 ApiResponse 代码用于网络请求。 ApiResponse是一个简单的Retrofit2.Call类包装,将其响应转换为 LiveData。

以下是该 NetworkBoundResource 类的其余部分:

public abstract class NetworkBoundResource<ResultType, RequestType> {
    private final MediatorLiveData<Resource<ResultType>> result = new MediatorLiveData<>();

    @MainThread
    NetworkBoundResource() {
        result.setValue(Resource.loading(null));
        LiveData<ResultType> dbSource = loadFromDb();
        result.addSource(dbSource, data -> {
            result.removeSource(dbSource);
            if (shouldFetch(data)) {
                fetchFromNetwork(dbSource);
            } else {
                result.addSource(dbSource,
                        newData -> result.setValue(Resource.success(newData)));
            }
        });
    }

    private void fetchFromNetwork(final LiveData<ResultType> dbSource) {
        LiveData<ApiResponse<RequestType>> apiResponse = createCall();
        // we re-attach dbSource as a new source,
        // it will dispatch its latest value quickly
        result.addSource(dbSource,
                newData -> result.setValue(Resource.loading(newData)));
        result.addSource(apiResponse, response -> {
            result.removeSource(apiResponse);
            result.removeSource(dbSource);
            //noinspection ConstantConditions
            if (response.isSuccessful()) {
                saveResultAndReInit(response);
            } else {
                onFetchFailed();
                result.addSource(dbSource,
                        newData -> result.setValue(
                                Resource.error(response.errorMessage, newData)));
            }
        });
    }

    @MainThread
    private void saveResultAndReInit(ApiResponse<RequestType> response) {
        new AsyncTask<Void, Void, Void>() {

            @Override
            protected Void doInBackground(Void... voids) {
                saveCallResult(response.body);
                return null;
            }

            @Override
            protected void onPostExecute(Void aVoid) {
                // we specially request a new live data,
                // otherwise we will get immediately last cached value,
                // which may not be updated with latest results received from network.
                result.addSource(loadFromDb(),
                        newData -> result.setValue(Resource.success(newData)));
            }
        }.execute();
    }

    public final LiveData<Resource<ResultType>> getAsLiveData() {
        return result;
    }
}12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364

现在,我们可以使用 NetworkBoundResource 将在 repository 中绑定 User 实现 写入我们的磁盘和网络。

class UserRepository {
    Webservice webservice;
    UserDao userDao;

    public LiveData<Resource<User>> loadUser(final String userId) {
        return new NetworkBoundResource<User,User>() {
            @Override
            protected void saveCallResult(@NonNull User item) {
                userDao.insert(item);
            }

            @Override
            protected boolean shouldFetch(@Nullable User data) {
                return rateLimiter.canFetch(userId) && (data == null || !isFresh(data));
            }

            @NonNull @Override
            protected LiveData<User> loadFromDb() {
                return userDao.load(userId);
            }

            @NonNull @Override
            protected LiveData<ApiResponse<User>> createCall() {
                return webservice.getUser(userId);
            }
        }.getAsLiveData();
    }
}

喜欢的给小编点个小关注啦,感谢!

Android架构组件-App架构指南(1) 本指南适用于那些已经拥有开发Android应用基础知识的开发人员,现在想了解能够开发出更加健壮、优质的应用程序架构。注意:本指南假定读者熟悉Android框架。如果你不熟悉Android应用程序开发,请查看Android入门培训教程,其中包含本指南的必备内容。在这里我先贴上Google原文地址,以及Android架构组件官方开源示例代码地址。 阅读详情

相关推荐

Android架构组件-App架构指南

在Android中,这种应用程序跳转行为是很常见的,所以您的应用程序必须正确处理这些流程。请记住,移动设备是资源受限的,所以在任何时候,操作系统都可能需要杀死一些应用程序,以腾出空间给新的应用。这一切的要点在于,您的可以单独和无序地启动,并且可以在任何时候由用户或系统销毁。由于是短暂的,并且它们的生命周期(创建和销毁时)不在您的控制之下,,并且 你的 app组件不应相互依赖。

2501_90404745的博客 1058

App架构经验总结

架构因人而异,不同的架构师大多会有不同的看法;架构也因项目而异,不同的项目需求不同,相应的架构也会不同。然而,有些东西还是通用的,是所有架构师都需要考虑的,也是所有项目都会有的需求,比如API如何设计?架构如何分层?开发环境和生产环境如何分离?这几年,我负责研发过的App,有餐饮类的、社交类的、智能家居类的、电商类的、新闻媒体类的等等。当有了一定的经验之后,你总会有一些自己的心得体会。而以下内容就

u014651643的博客 2210

Android架构组件-App架构指南,Android面试基础技能罗列

UI这块知识是现今使用者最多的。当年火爆一时的Android入门培训,学会这小块知识就能随便找到不错的工作了。不过很显然现在远远不够了,拒绝无休止的CV,亲自去项目实战,读源码,研究原理吧!

2401_84149570的博客 637

Android 官方现代 App 架构指南

今年的 Google I/O 发布了一个最新的官方示例 Now in Android,这个示例的完整度比之前的 JetNews、Sunflower 要高,后面也将基于这个仓库做进一步的说明解析,从一个完整项目的角度来看 Android 新推出的架构指南。

m0_72538886的博客 2781

【学习笔记】人群归因分数 PAF 以及combined PAF

人群归因风险比例(Population Attributable Fraction,PAF)是一个流行病学指标,它估计了在总体中,如果能消除一个特定的危险因素,能预防多少百分比的疾病或健康结果。简单来说,PAF回答的是:“如果没有这个风险因素,总体中会有多少病例是可以避免的?

weixin_53499537的博客 4905

sap外币重估流程图_SAP 外币评估方法(Valuation Method,评估方法)

在FI-GL模块评估外币时(T-code:F.05),需要输入ValuationMethod。下面分解一下评估方法是如何定义的。IMG -> FI -> General LedgerAccounting ->Business Transactions ->Closing ->Valuating -> ForeignCurrency Valuation ->...

weixin_39640543的博客 2357

Android 应用框架指南

使用单项数据流方式(Unidirectional Data Flow)将职责进行分离,使得状态(UI State)的来源位置(Data,也就是数据层)、转换位置(State Holder,也就是ViewModel)、使用位置(UI Element,也就是Activity/Fragment)分散到不同类中。事件分为 UI Event(UI自己能够处理的事件,如页面跳转、列表展开、权限请求等与Contex相关的)和 User Event(与用户交互产生的事件需要执行的逻辑,如点击事件、数据更新)。

HugMua的博客 1380

uniapp自定义tabbar闪屏?3步搞定原生tabbar隐藏+自定义占位(附完整代码)

本文针对UniApp开发中自定义TabBar切换页面闪烁的痛点,提供了一套根治方案。通过隐藏原生TabBar并利用自定义组件进行视觉占位,巧妙结合原生切换的无闪烁优势与自定义设计的灵活性。文章详细解析了闪屏根源,并附上完整的page.json配置与代码实现,帮助开发者彻底解决此问题,提升小程序应用质感。

postgres8guard的博客 1531

APP架构设计_1.官方应用架构指南

比如我做的应用是运动健康app,用户的睡眠相关的数据有一个 SleepResposity,用户体重相关的数据有一个 WeightReposity,由于应用中很多界面都可能需要展示用户的睡眠数据和体重数据,所以 SleepResposity 和 WeightReposity 可以供不同界面层使用。因为当未向用户显示视图时,界面不应观察界面状态。数据层不应该是页面级别的(一个页面对应一个数据层),而应该是应用级别的(数据层有多个存储仓库,每种数据类型有一个对应的存储仓库,不同的界面层可以复用存储仓库)。

huaqianzkh的专栏 2453

【AIGC】COT思维链:让AI学会拆解问题,像人一样思考

COT思维链的出现,为AI的发展开辟了一条新的道路。它让AI学会了像人类一样思考,将复杂问题分解成简单的步骤,并逐步推理出最终答案。希望我的引导会对你产生启发。

智兔唯新的博客 9003

【保姆级教程】YOLOv8_Pose多目标+关键点检测:训练自己的数据集

【保姆级教程】YOLOv8_Pose多目标+关键点检测:训练自己的数据集

m0_51579041的博客 1万+

Android(安卓)APP渗透测试-总结

文章目录基础知识安卓系统目录结构组件基础APK目录结构ADB使用Drozer客户端安全测试反编译测试(脱壳与混淆)脱壳回编译测试本地敏感信息存储四大组件安全本地注入动/静态扫描安全的通信方式HOOK开源移动端扫描工具APP安全在线检测系统服务端渗透测试SSL证书抓包问题安装证书问题证书校验问题 前言:这是一份资料整理,不是一份教程,教程在“心中”。 很多时候对产品进行测试,但是总会遇到一些重复的问题,后来发现大部分时间都花在问题检索和筛选上了(检索五分钟,测试一分钟),因此有了这份总结。学会把时间花费在有意

H5oSir 2万+

Xilinx高级调试方法--远程调试

本文主要介绍Xilinx的一些高级调试方法,以及如何使用Xilinx的相关IP。

斌赋的博客 3311

【安卓App】安卓App开发基础指南

Android 应用开发是一个系统性的过程,涉及多个关键概念和技术。

Flask Web 942

【kubernetes】kubernetes kubeadm init 错误了 怎么回退?

因为虚拟机设置问题首先报错:然后我根据文章修改了核数。重启后,查看VIP运行所在机器,发现机器没有变化然后重新执行初始化后面报错了根据提供的信息,问题主要集中在 kubelet 未能成功启动控制平面组件,导致初始化过程超时。以下是可能的原因及相应的排查步骤:原因 1:kubelet 未运行或运行状态异常正如错误提示所指出,kubelet 可能未运行或处于不健康状态。请按照提示执行以下命令检查 kubelet 的状态和服务日志:根据输出结果,判断 kubelet 是否正常运行。后续一大堆错误。

九师兄 121

深度学习工具包 DeepLearningToolbox

深度学习工具包Deprecation notice.-----This toolbox is outdated and no longer maintained.There are much better tools available for deep learning than this toolbox, e.g. [Theano](http://deeplearning.net/software/theano/), [torch](http://torch.ch/) or [tensorflow](http://www.tensorflow.org/)I would suggest you use one of the tools mentioned above rather than use this toolbox.Best, Rasmus.DeepLearnToolbox================A Matlab toolbox for Deep Learning.Deep Learning is a new subfield of machine learning that focuses on learning deep hierarchical models of data.It is inspired by the human brain's apparent deep (layered, hierarchical) architecture.A good overview of the theory of Deep Learning theory is[Learning Deep Architectures for AI](http://www.iro.umontreal.ca/~bengioy/papers/ftml_book.pdf)For a more informal introduction, see the following videos by Geoffrey Hinton and Andrew Ng.* [The Next Generation of Neural Networks](http://www.youtube.com/watch?v=AyzOUbkUf3M) (Hinton, 2007)* [Recent Developments in Deep Learning](http://www.youtube.com/watch?v=VdIURAu1-aU) (Hinton, 2010)* [Unsupervised Feature Learning and Deep Learning](http://www.youtube.com/watch?v=ZmNOAtZIgIk) (Ng, 2011)If you use this toolbox in your research please cite [Prediction as a candidate for learning deep hierarchical models of data](http://www2.imm.dtu.dk/pubdb/views/publication_details.php?id=6284)```@MASTERSTHESIS\{IMM2012-06284, author = "R. B. Palm", title = "Prediction as a candidate for learning deep hierarchical models of data", year = "2012",}```Contact: rasmusbergpalm at gmail dot comDirectories included in the toolbox-----------------------------------`NN/` - A library for Feedforward Backpropagation Neural Networks`CNN/` - A library for Convolutional Neural Networks`DBN/` - A library for Deep Belief Networks`SAE/` - A library for Stacked Auto-Encoders`CAE/` - A library for Convolutional Auto-Encoders`util/` - Utility functions used by the libraries`data/` - Data used by the examples`tests/` - unit tests to verify toolbox is workingFor references on each library check REFS.mdSetup-----1. Download.2. addpath(genpath('DeepLearnToolbox'));Example: Deep Belief Network---------------------```matlabfunction test_example_DBNload mnist_uint8;train_x = double(train_x) / 255;test_x = double(test_x) / 255;train_y = double(train_y);test_y = double(test_y);%% ex1 train a 100 hidden unit RBM and visualize its weightsrand('state',0)dbn.sizes = [100];opts.numepochs = 1;opts.batchsize = 100;opts.momentum = 0;opts.alpha = 1;dbn = dbnsetup(dbn, train_x, opts);dbn = dbntrain(dbn, train_x, opts);figure; visualize(dbn.rbm{1}.W'); % Visualize the RBM weights%% ex2 train a 100-100 hidden unit DBN and use its weights to initialize a NNrand('state',0)%train dbndbn.sizes = [100 100];opts.numepochs = 1;opts.batchsize = 100;opts.momentum = 0;opts.alpha = 1;dbn = dbnsetup(dbn, train_x, opts);dbn = dbntrain(dbn, train_x, opts);%unfold dbn to nnnn = dbnunfoldtonn(dbn, 10);nn.activation_function = 'sigm';%train nnopts.numepochs = 1;opts.batchsize = 100;nn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.10, 'Too big error');```Example: Stacked Auto-Encoders---------------------```matlabfunction test_example_SAEload mnist_uint8;train_x = double(train_x)/255;test_x = double(test_x)/255;train_y = double(train_y);test_y = double(test_y);%% ex1 train a 100 hidden unit SDAE and use it to initialize a FFNN% Setup and train a stacked denoising autoencoder (SDAE)rand('state',0)sae = saesetup([784 100]);sae.ae{1}.activation_function = 'sigm';sae.ae{1}.learningRate = 1;sae.ae{1}.inputZeroMaskedFraction = 0.5;opts.numepochs = 1;opts.batchsize = 100;sae = saetrain(sae, train_x, opts);visualize(sae.ae{1}.W{1}(:,2:end)')% Use the SDAE to initialize a FFNNnn = nnsetup([784 100 10]);nn.activation_function = 'sigm';nn.learningRate = 1;nn.W{1} = sae.ae{1}.W{1};% Train the FFNNopts.numepochs = 1;opts.batchsize = 100;nn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.16, 'Too big error');```Example: Convolutional Neural Nets---------------------```matlabfunction test_example_CNNload mnist_uint8;train_x = double(reshape(train_x',28,28,60000))/255;test_x = double(reshape(test_x',28,28,10000))/255;train_y = double(train_y');test_y = double(test_y');%% ex1 Train a 6c-2s-12c-2s Convolutional neural network %will run 1 epoch in about 200 second and get around 11% error. %With 100 epochs you'll get around 1.2% errorrand('state',0)cnn.layers = { struct('type', 'i') %input layer struct('type', 'c', 'outputmaps', 6, 'kernelsize', 5) %convolution layer struct('type', 's', 'scale', 2) %sub sampling layer struct('type', 'c', 'outputmaps', 12, 'kernelsize', 5) %convolution layer struct('type', 's', 'scale', 2) %subsampling layer};cnn = cnnsetup(cnn, train_x, train_y);opts.alpha = 1;opts.batchsize = 50;opts.numepochs = 1;cnn = cnntrain(cnn, train_x, train_y, opts);[er, bad] = cnntest(cnn, test_x, test_y);%plot mean squared errorfigure; plot(cnn.rL);assert(er<0.12, 'Too big error');```Example: Neural Networks---------------------```matlabfunction test_example_NNload mnist_uint8;train_x = double(train_x) / 255;test_x = double(test_x) / 255;train_y = double(train_y);test_y = double(test_y);% normalize[train_x, mu, sigma] = zscore(train_x);test_x = normalize(test_x, mu, sigma);%% ex1 vanilla neural netrand('state',0)nn = nnsetup([784 100 10]);opts.numepochs = 1; % Number of full sweeps through dataopts.batchsize = 100; % Take a mean gradient step over this many samples[nn, L] = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.08, 'Too big error');%% ex2 neural net with L2 weight decayrand('state',0)nn = nnsetup([784 100 10]);nn.weightPenaltyL2 = 1e-4; % L2 weight decayopts.numepochs = 1; % Number of full sweeps through dataopts.batchsize = 100; % Take a mean gradient step over this many samplesnn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.1, 'Too big error');%% ex3 neural net with dropoutrand('state',0)nn = nnsetup([784 100 10]);nn.dropoutFraction = 0.5; % Dropout fraction opts.numepochs = 1; % Number of full sweeps through dataopts.batchsize = 100; % Take a mean gradient step over this many samplesnn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.1, 'Too big error');%% ex4 neural net with sigmoid activation functionrand('state',0)nn = nnsetup([784 100 10]);nn.activation_function = 'sigm'; % Sigmoid activation functionnn.learningRate = 1; % Sigm require a lower learning rateopts.numepochs = 1; % Number of full sweeps through dataopts.batchsize = 100; % Take a mean gradient step over this many samplesnn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.1, 'Too big error');%% ex5 plotting functionalityrand('state',0)nn = nnsetup([784 20 10]);opts.numepochs = 5; % Number of full sweeps through datann.output = 'softmax'; % use softmax outputopts.batchsize = 1000; % Take a mean gradient step over this many samplesopts.plot = 1; % enable plottingnn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.1, 'Too big error');%% ex6 neural net with sigmoid activation and plotting of validation and training error% split training data into training and validation datavx = train_x(1:10000,:);tx = train_x(10001:end,:);vy = train_y(1:10000,:);ty = train_y(10001:end,:);rand('state',0)nn = nnsetup([784 20 10]); nn.output = 'softmax'; % use softmax outputopts.numepochs = 5; % Number of full sweeps through dataopts.batchsize = 1000; % Take a mean gradient step over this many samplesopts.plot = 1; % enable plottingnn = nntrain(nn, tx, ty, opts, vx, vy); % nntrain takes validation set as last two arguments (optionally)[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.1, 'Too big error');```[![Bitdeli Badge](https://d2weczhvl823v0.cloudfront.net/rasmusbergpalm/deeplearntoolbox/trend.png)](https://bitdeli.com/free "Bitdeli Badge")

安卓app开发教程pdf!Android组件化架构实践,值得收藏!

前言 回顾一下自己这段时间的经历,三月份的时候,疫情原因公司通知了裁员,我匆匆忙忙地出去面了几家,但最终都没有拿到offer,我感觉今年的寒冬有点冷。到五月份,公司开始第二波裁员,我决定主动拿赔偿走人。后续的面试过程我做了一些准备,基本都能走到hr面,后面我也顺利地拿到了offer,我给大家分享下我的求职体会,希望能给大家一些参考。 一般Android面试分为两部分:Java部分和Android部分,下面说一下自己面试过程遇到的一些具体题目和一些相关知识点。 技能储备 (一)架构师必备Java基础 (二)设

fanzhang_vip0723的博客 1248

华为的C语言编程规范.pdf

华为技术有限公司内部技术规范。本规范制定了编写C语言程序的基本原则、规则和建议。从代码的清晰、简洁、可测试、安全、程序效率、可移植各个方面对C语言编程作出了具体指导。

安卓进阶之android系统架构

安卓进阶知识之android系统架构学习的笔记,结合了《进阶之光》、一些博客、官方文档和个人的理解

Suzy_MM的博客 3351

假人有限元模型

假人模型,车辆碰撞假人有限元模型,适合做乘员安全的研究者利用

C# WinForm 基于DirectShow.dll编写的程序,可边预览边保存视频,音频和图片

基于微软的DirectShow.dll的二次开发1.自动识别连接的摄像头跟麦克风且可以对其进行选择(默认选中第一个)2.可录制wmv和avi两种格式的视频并且都带有音频,avi可以录制的分辨率和帧数进行选择,wmv默认进行了淸晰度优化如不喜欢可去除代码中有相关提示3.可在预览点开之后进行拍照,也可边录制视频边拍照4.添加了视频和音频的压缩功能(酌情使用)这次二次开发发现C#对DirectShow的相关信息太少,这次做完也不敢藏私共享出来,还望大家指正

Android 架构 - 组件化 Componentization(Gradle版)

继承自 lib_common 中提供的 BaseApplication,组件自定义的 Application 写在 debug 目录中,在集成模式下剔除。(别忘了在下方 debug 目录中的 AndroidManifest 中注册)

HugMua的博客 4636

安卓app开发系列之-组件与服务

Activity:用户界面组件,表示应用的一屏幕。Service:后台运行组件,不提供用户界面,常用于执行长时间运行的操作。:用于接收和处理广播消息的组件。:负责管理应用之间的数据共享。Activity是Android应用的一个重要组件。每个Activity代表一个用户界面,并负责与用户进行交互。用户可以通过点击按钮、输入文本等方式与Activity进行交互。Service是一种在后台运行的Android组件,用于进行长时间运行的任务,无需与用户交互。

Jamson 1279

安卓手机APP开发__平台的架构

安卓是一个开源的,基于Linux的软件栈,它创建一个设备和形式因素的很宽的矩阵

红孩儿编程大师 970

如何用Python编写一个安卓App,不需要太复杂?

Kivy是一个开源的Python库,专门用于创建多点触控应用程序。它不仅可以用于开发桌面程序,还能够生成适用于iOS和Android设备的应用包。Kivy的核心特点是其高度可定制化的图形界面设计和优秀的硬件加速性能,这使得开发者可以轻松构建流畅、美观的应用程序。BeeWare是一个由澳大利亚的一群志愿者发起并维护的开源项目。它的设计理念是“Write once, run anywhere”,即一次编写代码,到处运行。

cda2024的博客 4338

Android App 架构设计

简介本文是对谷歌原生文档的翻译,仅供学习参照。原文链接此文档写给希望学习最优编程实践和架构以开发健壮、高质量APP的开发者。开发者常遇到的问题传统的桌面程序大多数使用场景是有一个启动入口,作为一个独立进程运行。Android app结构要复杂很多,一个典型的Android app由很多组件构成,包括activities,fragment,services,content providers 和bro

读书未遍、未敢妄下雌黄 1万+
上一篇: Java入门请不要放弃,学习路线以及侧重点分析
下一篇: 性能优越的轻量级日志收集工具,微软、亚马逊都在用
Java架构技术官
博客等级 码龄6年 9粉丝 58原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值