Adnroid Jetpack(一)

根据Google官网Android Jetpack翻译

1. 关注分离

Android Jetpack最重要的原则就是关注点分离原则。要竟可能地精简Activity和Fragment,让他们处理UI和与操作系统的交互。这样可以避免很多生命周期引起的问题。
因为操作系统随时又能销毁Activity跟Fragment,所以已经尽量地减少依赖。

2. 从model驱动UI

另一个重要的原则就是从模型(最好是持久型model)驱动UI。model是负责处理数据的组件,独立于视图对象和应用程序组件,因此不受程序的生命周期影响。原因如下:
1. 如果系统销毁App来释放资源,用户不会丢失数据。
2. 程序可以在网络不可用或者出现问题的时候继续工作。

3. 推荐的app架构

可以使用架构组件来构建AppArchitecture Components

3.1 导读

下图展示了模块间是如何相互交互的
在这里插入图片描述

这里除了Repository以外,所有的模块都只是依赖下面的一个模块。为了有一个好的用户体验,应该先将本地的数据展示给用户,如果本地数据已经过时了,就从远程获取数据。

3.2 构建用户界面

UI包含了fragment UserProfileFragment,布局文件user_profile_layout.xml。同时为了驱动UI,我们的数据层需要包含下面的这些元素:
1) User ID:用户的标识,最好使用fragment的argument将userid传递到fragment中。如果系统销毁了我们的进程,那么这个信息会被保存下来,下次App重启是就可以使用ID。
2)User Object:保存用户信息的class类。

当我们使用UserProfileViewModel时,我们需要使用ViewModel的架构组件来保存这些信息

ViewModel为activity或者fragment提供数据,并且用于模型通信的业务数据处理逻辑。
例如ViewModel可以调用其他组件来加载数据,并且可以转发用户请求来修改数据。
因为ViewModel并不会持有activity或者fragment,因此他不回受到生命周期的影响

现在我们创建了三个文件:
user_profile.xml: 展示UI
UserProfileFragment: 控制UI展示数据
UserProfileViewModel: 为UserProfileFragment准备数据,并且反馈用户交互.

UserProfileViewModel

public class UserProfileViewModel extends ViewModel {
    private String userId;
    private User user;

    public void init(String userId) {
        this.userId = userId;
    }
    public User getUser() {
        return user;
    }
}

UserProfileFragment

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);
    }
}

现在我们已经有了代码了,但是我们要怎么将这两者联系起来呢?当我们设置在UserProfileViewModel中设置user的时候,我们需要去通知UI的方法,这时候就需要使用到LiveData架构组件了。

LiveDat是一个可观测的数据保持器。应用程序中的其他组件可以通过使用它来监视对象的更改,而不需要在对象之间创建依赖。
Livedata组件还遵循应用程序组件的生命周期状态,例如activity、fragment和service,并且包括清理逻辑,以防止对象泄漏和过度内存消耗。
tips:当程序中已经使用了RxJava或者Agera。你可以不使用LiveData,但是请确保在使用过程中与生命周期同步。

public class UserProfileViewModel extends ViewModel {
    ...
    private User user;
    private LiveData<User> user;
    public LiveData<User> getUser() {
        return user;
    }
}

现在UserProfileFragment可以更新UI了

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

每次数据有改动时,都会调用OnChange()方法来刷新UI
如果你对其他第三方的观察回调库熟悉的话,你会发现LiveData没有在onStop()方法中去停止观察数据。
对于LiveData来说这是没有必要的,因为LIveData已经遵循了生命周期,只有在活跃状态才会调用onChange()方法,也就是说在Fragment中会在onStart()接收但不会在onStop()接收。LiveData会在Fragment destroy的时候自动移除。

3.3 读取数据

现在我们已经通过LiveData将UserProfileFragment与UserProfileViewModel联系到一起了,那么我们如何获取用户数据呢?
在这里我们假设已经有后台的REST API服务

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);
}

一开始的想法应该是在继承了ViewModel的地方来直接使用WebService获取数据,并将数据注入到LiveData中,但是这样做的话,程序会变得越来越难维护。这样做的话,就给到了UserProfileViewModel太多的职责,违背了关注分离原则。此外ViewModel与Activity和Fragment的生命周期相关联,在UI对象的生命周期结束时,WebService获取到的数据将会丢失。这样会产生非常差的用户体验。

我们的ViewModel将数据获取的过程全部委托给了一个新的model,Repository

Repository Model处理数据操作。它提供程序其他部分检索数据简便的API。我们可以将Repository当做是数据源的中介,例如持久模型、webService和缓存。

我们的UserRepository类(如下面的代码片段所示)使用WebService实例来获取用户数据:
UserRepository

public class UserRepository {
    private Webservice webservice;
    // ...
    public LiveData<User> getUser(int userId) {
        // This isn't an optimal implementation. We'll fix it later.
        final MutableLiveData<User> data = new MutableLiveData<>();
        webservice.getUser(userId).enqueue(new Callback<User>() {
            @Override
            public void onResponse(Call<User> call, Response<User> response) {
                data.setValue(response.body());
            }

            // Error case is left out for brevity.
        });
        return data;
    }
}

管理组件之间的依赖,可以使用依赖注入

3.4 连接ViewModel和Repository

现在我们修改我们的UserProfileViewModel来使用UserRepository
UserProfileViewModel

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

    // Instructs Dagger 2 to provide the UserRepository parameter.
    @Inject
    public UserProfileViewModel(UserRepository userRepo) {
        this.userRepo = userRepo;
    }

    public void init(int userId) {
        if (this.user != null) {
            // ViewModel is created on a per-Fragment basis, so the userId
            // doesn't change.
            return;
        }
        user = userRepo.getUser(userId);
    }

    public LiveData<User> getUser() {
        return this.user;
    }
}
3.5 缓存

UserRepository实现了WebService的调用,但是由于数据源的来源单一,所以不是很灵活。
UserRepository的主要问题是由于没有保存数据,所以当用户退出UserProfileFragment之后,用户需要重新获取数据,即使数据没有发生改变。
这种设计不是最好的原因如下:
1、他浪费了宝贵的带宽。
2、他迫使用户等待新数据的查询。
为了解决这些问题,我们想UserRepository添加了一个将User保存在内存的新数据源

UserRepository

// Informs Dagger that this class should be constructed only once.
@Singleton
public class UserRepository {
    private Webservice webservice;

    // Simple in-memory cache. Details omitted for brevity.
    private UserCache userCache;

    public LiveData<User> getUser(int userId) {
        LiveData<User> cached = userCache.get(userId);
        if (cached != null) {
            return cached;
        }

        final MutableLiveData<User> data = new MutableLiveData<>();
        userCache.put(userId, data);

        // This implementation is still suboptimal but better than before.
        // A complete implementation also handles 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;
    }
}
3.6 持久化数据

根据我们现在的实现,当设备旋转或者离开之后立即返回程序,现有的UI会立刻显示,且不会发生改变,因为Repository可以从内存中直接读取数据。
但是当用户在系统杀死这个进程之后再返回程序会发生什么呢?在我们之前的实现中,我们需要去再次获取网络数据。这不仅仅是一种不好的用户体验,而且还是浪费了移动流量。
或许你可以通过缓存web请求来解决这个问题,但同时这也会导致一个新的问题:如果同样的用户数据通过不一样的请求,比如获取好友列表,会发生什么呢?程序将会展示不一样的数据,这是最混乱的。例如,同一个用户在不同的时间请求用户列表和单用户,我们的应用程序将会展示用户的两个版本的数据。我们的程序需要知道如何合并这些数据。
处理这种情况的方法是使用持久化数据模型,这时候我们就需要使用Room了

Room是一个对象映射库,它使用最少的样板代码提供本地数据持久性。在编译时,他将会根据数据模式验证每个查询,因此终端的SQL查询会导致编译时错误而不是运行时错误。Room抽象了处理原始SQL表和查询的一些基础实现细节。它还允许您观察对数据库数据的更改,包括集合和连接查询,并使用LiveData对象公开这些更改。它甚至显式地定义了解决常见线程问题的执行约束,例如访问主线程上的存储

为了使用Room,我们需要定义我们的局域模式。首先在User模型类中使用注解@Entity,在id上使用注解@PrimaryKey。这些注解在数据库中,为我们创建了一张User表以及主键。

User

@Entity
class User {
  @PrimaryKey
  private int id;
  private String name;
  private String lastName;

  // Getters and setters for fields.
}

然后我们创建一个数据库类通过继承RoomDatabase

UserDatabase

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

注意这个UserDatabase类是一个抽象类,Room会自动帮我们实现他,详情查看Room
我们需要将数据插入到数据库中的方法,为此我们创建了一个数据访问对象
UserDao

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

注意,Load方法返回一个LiveData<User>的对象。Room知道何时修改数据库,并在数据更改时自动通知所有活动观察者。因为Room使用LiveData,所以此操作是有效的;它只在至少有一个活动观察者时才更新数据。
现在我们已经已经创建了UserDao了,我们需要去修改UserDatabase

UserDatabase

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

现在我们可以在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);
        // Returns a LiveData object directly from the database.
        return userDao.load(userId);
    }

    private void refreshUser(final String userId) {
        // Runs in a background thread.
        executor.execute(() -> {
            // Check if user data was fetched recently.
            boolean userExists = userDao.hasUser(FRESH_TIMEOUT);
            if (!userExists) {
                // Refreshes the data.
                Response<User> response = webservice.getUser(userId).execute();

                // Check for errors here.

                // Updates the database. The LiveData object automatically
                // refreshes, so we don't need to do anything else here.
                userDao.save(response.body());
            }
        });
    }
}
3.7 正在进行中的操作

在某些用例中,如下拉刷新,显示正在进行的网络操作是很重要的。由于数据会因为各种原因更新,将UI操作与实际数据分离是一种很好的实践。例如,如果我们获取好友列表,同一个用户可能会以编程方式再次获取,从而触发LiveData<User>更新。从UI的角度来看,请求正在传输的事实只是另一个数据点,类似于User对象本身中的任何其他数据段。
我们可以使用以下策略之一在UI中显示一致的数据更新状态,而不管更新数据的请求来自何处:
1.更改getUser()返回LiveData类型的对象。此对象将包括网络操作的状态。
例如GitHub上的android-architecture-components项目中的NetworkBoundResource实现
2.在UserRepository类中提供另一个公共函数,该函数可以返回User的刷新状态。如果仅当数据获取过程源自显式用户操作(如下拉刷新)时,才希望在UI中显示网络状态,则此选项更好。

3.8 测试每个组件

4. 最佳经验

尽管以下建议不是强制性的,但我们的经验是,遵循这些建议可以使您的代码库从长远来看更加健壮、可测试和可维护:
1.避免将应用程序的入口(如activity、services和broadcastReceiver)指定为数据源。

相反,它们应该只与其他组件协调以检索与该入口相关的数据子集。每个应用程序组件都相当短暂,这取决于用户与设备的交互以及系统的总体当前健康状况。
2.在应用程序的各个模块之间创建明确定义的职责边界。
例如,不要将从网络加载数据的代码分散到代码库中的多个类或包中。同样,不要定义多个无关的职责,例如数据缓存和数据绑定到同一个类中。
3.尽可能地少暴露每个模块
不要试图创建“仅此一个”快捷方式,以公开来自一个模块的内部实现细节。您可能在短期内获得一些时间,但是随着代码库的发展,您会多次招致技术债务。
4.考虑如何使每个模块在隔离状态下可测试。
例如,使用定义良好的API从网络获取数据,可以更容易地测试将数据持久化在本地数据库中的模块。相反,如果在一个地方混合来自这两个模块的逻辑,或者将网络代码分布在整个代码库中,那么测试就变得非常困难(如果不是不可能的话)。
5.专注于你的应用程序的独特核心,让它从其他应用程序中脱颖而出。
不要反复编写相同的样板代码来重新发明轮子。相反,把时间和精力集中在是什么让你的应用程序独特,让Android架构组件和其他推荐的库处理重复的样板
6.尽可能多地保持相关和新的数据。
这样,即使用户的设备处于脱机模式,用户也可以享受应用程序的功能。记住,不是所有的用户都喜欢持续、高速的连接。
7.指定一个数据源作为真实的唯一来源。
每当你的应用程序需要访问这段数据时,它应该总是源自于这个单一的真实来源。

附录:网络状态

在这之前,我们忽视了网络错误和加载状态来保持代码的简洁。
在这里我们通过Resource类来处理分装数据和网络状态。

Resource

// A generic class that contains data and status about loading this data.
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<>(Status.SUCCESS, data, null);
    }

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

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

    public enum Status { SUCCESS, ERROR, LOADING }
}

由于加载网络数据和本地数据是一个公共方法,所以我们最好创建一个可以再多处重用帮助类。这里我们创建一个NetworkBoundResource类。
下图展示了NetworkBoundResource的决策树

在这里插入图片描述
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 to fetch
    // potentially updated data 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 object that represents the resource that's implemented
    // in the base class.
    public final LiveData<Resource<ResultType>> getAsLiveData();
}

一些细节重点:
1.它定义了两个类型参数,ResultType和RequestType,因为从API返回的数据类型可能与本地使用的数据类型不匹配。
2.它使用一个称为ApiResponse的类来处理网络请求。ApiResponse是Retrofit2.Call类的一个简单包装器,用于将响应转换为LiveData实例。
具体的实现可以参照android-architecture-components GitHub project
然后就可以完善UserRepository类了

UserRepository

class UserRepository {
    Webservice webservice;
    UserDao userDao;

    public LiveData<Resource<User>> loadUser(final int 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();
    }
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值