Guide to App Architecture
Common problem faced by app Developers
Android have a much more complex structure.
Android Components:
- Activities
- Fragments
- Services
- Content Providers
- Broadcast Receivers
In Android, this app-hopping behavior is common
Ex) Social network app -> photo app -> share the photo -> phone call
You should not store any app data or state in your app components
Common architecture principles
If you can't use app components to store app data and state, how should apps be structured?
- Ex) It is a common mistake to write all your code in an Activity or a Fragment
2. Drive your UI from a model (persistent model)
- your users won't lose data if the OS destroys your app to free up resources
- your app will continue to work even when a network connection is flaky or not connected
Models only handle the data and independent from the Views and other components
Recommended app architecture
(If you already have a good way of writing Android apps, you don't need to change)
- Building the user interface
- Fetching data
- Connecting ViewModel and the repository
- Caching data
- Persisting data
- Testing
- The final architecture
Recommended app architecture
1. Building the user interface
View:
- UserProfileFragment.java ( The UI controller that displays the data in the ViewModel and reacts to user interactions.)
- user_profile_layout.xml
Data:
- User Id : The identifier for the user, User object (POJO)
ViewModel
- provides the data for a specific UI component
- handles the communication with the business part of data handling, such as calling other components to load the data or forwarding user modifications
- not know about the View, not affected by configuration changes such as recreating an activity due to rotation.
1. Building the user interface
public class UserProfileViewModel extends ViewModel {
private String userId;
private User user;
public void init(String userId) {
this.userId = userId;
}
public User getUser() {
return user;
}
}
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);
}
}
1. Building the user interface
When the ViewModel's user field is set, we need a way to inform the UI. This is where the LiveData class comes in.
LiveData : observable data holder.
- It lets the components in your app observe LiveData objects for changes without creating explicit and rigid dependency paths between them.
- LiveData also respects the lifecycle state of your app components (activities, fragments, services)
- prevent object leaking
You can use the RxJava, Agera instead of LiveData, but you need to handle the lifecycle.
1. Building the user interface
public class UserProfileViewModel extends ViewModel {
...
private User user;
private LiveData<User> user;
public LiveData<User> getUser() {
return user;
}
}
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
viewModel.getUser().observe(this, user -> {
// update UI
});
}
1. Building the user interface
Situation) When the configuration changes!
- The fragment is destroyed.
- The fragment is created.
- The fragment receive the same instance of ViewModel
- Interact with the same ViewModel.
This is the reason why ViewModels should not reference Views directly, they can outlive the View's lifecycle.
2. Fetching data
How does the ViewModel fetch the user data?
Retrofit
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);
}
Navie implementation : ViewModel could directly call the Webservice to fetch the data
=> too much responsibility
Repository : handling data operations. mediators between different data sources (persistent model, web service, cache, etc.).
2. Fetching data
Repository abstracts the data sources from the rest of the app.
ViewModel does not know that the data is fetched by the 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;
}
}
Manage dependencies between components
Problem) UserRepository class above needs an instance of the Webservice. need to know the dependencies of the Webservice class to construct it => complicate and duplicate
- Dependency Injection: Dependency Injection allows classes to define their dependencies without constructing them. At runtime, another class is responsible for providing these dependencies.
- Service Locator: Service Locator provides a registry where classes can obtain their dependencies instead of constructing them. It is relatively easier to implement than Dependency Injection (DI)
3. Connecting ViewModel and the 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;
}
}
4. Caching data
If the user leaves the UserProfileFragment and comes back to it, the app re-fetches the data => Cache!
@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;
}
}
4. Caching data
If the user leaves the UserProfileFragment and comes back to it, the app re-fetches the data => Cache!
@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;
}
}
5. Persisting data
If the user leaves the app and comes back hours later, after the Android OS has killed the process => Persisting data!
Room: object mapping library that provides local data persistence with minimal boilerplate code. At compile time, it validates each query against the schema, so that broken SQL queries result in compile time errors instead of runtime failures.
=> library's abstraction and query validation capabilities.
5. Persisting data
Table
@Entity
class User {
@PrimaryKey
private int id;
private String name;
private String lastName;
// getters and setters for fields
}
Database - MyDatabase is abstract. Room automatically provides an implementation of it
@Database(entities = {User.class}, version = 1)
public abstract class MyDatabase extends RoomDatabase {
}
5. Persisting data
Way to insert the user data into the database
@Dao
public interface UserDao {
@Insert(onConflict = REPLACE)
void save(User user);
@Query("SELECT * FROM user WHERE id = :userId")
LiveData<User> load(String userId);
}
@Database(entities = {User.class}, version = 1)
public abstract class MyDatabase extends RoomDatabase {
public abstract UserDao userDao();
}
@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());
}
});
}
}
Guide to App Architecture
By JongHo Kim
Guide to App Architecture
Summary of the "Guide to App Architecture" https://developer.android.com/topic/libraries/architecture/guide.html#common_problems_faced_by_app_developers
- 656