Most of the code for most modern applications was probably written in the days of Android 4.0. Applications survived the time of ContentProvider, RoboSpice , various libraries and architectural approaches . Therefore, it is very important to have an architecture that will remain flexible not only for functional changes, but also ready for new developments, technologies and tools.
In this article, I would like to talk about the architecture of the IFunny application, the principles that we adhere to, and how the main problems that arise during the development process are solved.
Let's start with the moments that I consider fundamental in the development:
Now let's take a look at what we came to and how we solved each problem.
Initially, when developing an application, there was some kind of MVC, where Activity / Fragment served as the controller. In small applications, this is a fairly convenient pattern that does not require strong abstractions, and this pattern was initially dictated by the platform.
But over time, Activity / Fragment grows to unreadable sizes (our record is 3 thousand lines of code in one of the Fragments). Each new functional is in any way based on the state of the current code, and it is difficult not to continue adding code to these classes.
We came to the conclusion that the entire screen needs to be split into independent components, and we have identified a separate entity for this:
public abstract class ViewController<T extends ViewModel, D> { public abstract void attach(ViewModelContainer<T> container, @Nullable D data); public abstract void detach(); }
public interface ViewModelContainer<T extends ViewModel> extends LifecycleOwner { View getView(); T getViewModel(); }
Now Fragment looks like this:
public class ChatFragment extends TrackedFragmentSubscriber implements ViewModelContainer<ChatViewModel>, IMessengerFragment { @Inject ChatMessagesViewController mChatViewController; @Inject TimeInfoViewController mTimeInfoViewController; @Inject ChatToolbarViewController mChatToolbarViewController; @Inject SendMessageViewController mSendMessageViewController; @Inject MessagesPaginationController mMessagesPaginationController; @Inject ViewModelProvider.Factory mViewModelFactory; @Inject UnreadMessagesViewController mUnreadMessagesViewController; @Inject UploadFileProgressViewController mUploadFileProgressViewController; @Nullable @Override public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { return inflater.inflate(R.layout.face_to_face_chat, container, false); } @Override public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); mChatViewController.attach(this); mSendMessageViewController.attach(this); mChatToolbarViewController.attach(this); mMessagesPaginationController.attach(this); mUnreadMessagesViewController.attach(this); mTimeInfoViewController.attach(this); mUploadFileProgressViewController.attach(this); } @Override public void onDestroyView() { mUploadFileProgressViewController.detach(); mTimeInfoViewController.detach(); mUnreadMessagesViewController.detach(); mMessagesPaginationController.detach(); mChatToolbarViewController.detach(); mSendMessageViewController.detach(); mChatViewController.detach(); super.onDestroyView(); } @Override public ChatViewModel getViewModel() { return ViewModelProviders .of(this, mViewModelFactory) .get(ChatViewModel.class); } }
This approach gives many advantages at once:
To add this behavior, you only need to register in the code:
@Override public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); mSearchFieldViewController.attach(this); }
Or, for example, search results with the possibility of multiple selection, while the types of data themselves, the sources of this data, navigation and strategies, caching are completely different. Matches only the display:
Then you need to organize the data structure. It is necessary to store the status of screens somewhere and experience the re-creation of Activity / Fragment.
Why data storage in the Bundle does not suit us:
Thus, Activity restores the state of its View:
protected void onRestoreInstanceState(Bundle savedInstanceState) { if (mWindow != null) { Bundle windowState = savedInstanceState.getBundle(WINDOW_HIERARCHY_TAG); if (windowState != null) { mWindow.restoreHierarchyState(windowState); } } }
And if within the overridden onRestoreInstanceState to update the adapter RecycleView, the scrolled by default will be reset;
We decided to use the retain fragment , namely, a convenient wrapper for them from Google in the form of a ViewModel. These objects live in the FragmentManager as non-recreated Fragments.
How it works
FragmentManager stores such objects in a separate field in FragmentManagerNonConfig. This object is experiencing a re-creation of the Activity and the FragmentManager in a memory area outside the FragmentManager, in an object called ActivityClientRecord. This object is formed during Activity.onDestroy and restores the state to Activity.attach. But he is able to recover only by turning the screen. Those. if the system “nailed” the Activity, then nothing will be saved .
Each ViewController needs its own ViewModel, in which its state will be located. He also needs a View to display data in it. This data is transmitted via ViewModelContainer, which is implemented by an Activity or Fragment.
Now you need to organize the flow of data and states between components. In fact, in this task, you can use several options. For example, a good solution is to use Rx to interact between the ViewController and the ViewModel.
We decided to try using LiveData for this purpose.
LiveData is a kind of flow in Rx without a lot of operators (there are really not enough operators, so you have to use LiveData and Rx side by side), but with the ability to cache data and process the application life cycle.
In general, all data lies inside the ViewModel. At the same time, data processing takes place outside it. ViewController simply initiates events and waits for data through the observer on the ViewModel.
Inside the ViewModel are the necessary LiveData objects that cache all these states. When you rotate the screen, the ViewController is re-created, subscribes to the data and the last state comes to it.
public class ChatViewModel extends ViewModel { private final MessageRepositoryFacade mMessageRepositoryFacade; private final CurrentChannelProvider mCurrentChannelProvider; private final SendbirdConnectionManager mSendbirdConnectionManager; private final MediatorLiveData<List<MessageModel>> mMessages = new MediatorLiveData<>(); private final MutableLiveData<String> mMessage = new MutableLiveData<>(); @Inject public ChatViewModel(MessageRepositoryFacade messageRepositoryFacade, SendbirdConnectionManager sendbirdConnectionManager, CurrentChannelProvider currentChannelProvider) { mMessageRepositoryFacade = messageRepositoryFacade; mCurrentChannelProvider = currentChannelProvider; mSendbirdConnectionManager = sendbirdConnectionManager; initLiveData(); } public LiveData<List<MessageModel>> getMessages() { return mMessages; } public void writeMessage(String message) { mMessage.postValue(message); } public void sendMessage() { // ... } private void initLiveData() { LiveData<List<MessageModel>> messages = Transformations.switchMap(mCurrentChannelProvider.getCurrentChannel(), input -> { if (!Resource.isDataNotNull(input)) { return AbsentLiveData.create(); } return mMessageRepositoryFacade.getMessagesList(input.data.mUrl); }); mMessages.addSource(messages, mMessages::setValue); mMessages.addSource(mSendbirdConnectionManager.getConnectionStateLiveData(), connectionState -> { if (connectionState == null) { return; } switch (connectionState) { case OPEN: // ... break; case CLOSED: // ... break; } }); } }
To initialize the View, we use the ButterKnife and the ViewHolder approach to get rid of the weakness of the initialized View.
Each ViewController has its own ViewHolder, which is initialized to the call to attach , with the detach ViewHolder set to zero. All fields in the display are written in his successor.
public class ViewHolder { private final Unbinder mUnbinder; private final View mView; public ViewHolder(View view) { mView = view; mUnbinder = ButterKnife.bind(this, view); } public void unbind() { mUnbinder.unbind(); } public View getView() { return mView; } }
Next we describe the controllers for our screen:
@ActivityScope public class SendMessageViewController extends SimpleViewController<ChatViewModel> { @Nullable private ViewHolder mViewHolder; @Nullable private ChatViewModel mChatViewModel; @Inject public SendMessageViewController() {} @Override public void attach(ViewModelContainer<ChatViewModel> container) { mViewHolder = new ViewHolder(container.getView()); mChatViewModel = container.getViewModel(); mViewHolder.mSendMessageButton.setOnClickListener(v -> mChatViewModel.sendMessage()); mViewHolder.mChatTextEdit.addTextChangedListener(new SimpleTextWatcher() { @Override public void afterTextChanged(Editable s) { mChatViewModel.setMessage(s.toString()); } }); } @Override public void detach() { ViewHolderUtil.unbind(mViewHolder); mChatViewModel = null; mViewHolder = null; } public class ChatViewHolder extends ViewHolder { @BindView(R.id.message_edit_text) EmojiconEditText mChatTextEdit; @BindView(R.id.send_message_button) ImageView mSendMessageButton; @BindView(R.id.message_list) RecyclerView mRecyclerView; @BindView(R.id.send_panel) View mSendPanel; public ViewHolder(View view) { super(view); } } }
@ActivityScope public class ChatMessagesViewController extends SimpleViewController<ChatViewModel> { private final ChatAdapter mChatAdapter; @Nullable private ChatViewModel mChatViewModel; @Nullable private ViewHolder mViewHolder; @Inject public ChatMessagesViewController(ChatAdapter chatAdapter) { mChatAdapter = chatAdapter; } @Override public void attach(ViewModelContainer<ChatViewModel> container) { mChatViewModel = container.getViewModel(); mViewHolder = new ViewHolder(container.getView()); mViewHolder.mRecyclerView.setAdapter(mChatAdapter); mChatViewModel.getMessages().observe(container, data -> mChatAdapter.updateMessages(data)); } @Override public void detach() { ViewHolderUtil.unbind(mViewHolder); mViewHolder = null; mChatViewModel = null; } public class SendMessageViewHolder extends ViewHolder { @BindView(R.id.message_list) RecyclerView mRecyclerView; public ViewHolder(View view) { super(view); LinearLayoutManager linearLayoutManager = new LinearLayoutManager(view.getContext()); linearLayoutManager.setReverseLayout(true); linearLayoutManager.setStackFromEnd(true); mRecyclerView.setLayoutManager(linearLayoutManager); } } }
Due to the LiveData logic, our list is not updated between onStop and onStart, since at this time LiveData is inactive, but new messages can still come through the push.
This allows you to encapsulate the implementation of data storage and also makes obvious the order of calls between classes. What do I mean by calling order?
For example, take MVP.
It is implied that Presenter and View have links to each other. View sends user events to the Presenter. He somehow processes them and gives the results back. With this interaction there is no clarity in the data streams. Since both objects have explicit references to each other (and the interfaces do not break this connection, but only abstract it a little), calls go in both directions and a dispute begins about how far the View should be passive; what to forward and what to handle itself, etc. etc. Also in this regard, often start the race for Presenter.
In our case, it is obvious that user data is also cached in the database. But caching occurs asynchronously, and the user response does not depend on it in any way, since immediately after they are received, they fast in LiveData.
All network requests come from the context of classes that do not have references to Activity or Fragment, data from requests are processed in global classes, which are also in the Application. The mapping gets this data through observer or any other listener. If this is done via LiveData, then we will not update our mapping between onPause and onStart.
Heavy operations associated only with the display (pick up data from the database, zadkodit image, write to a file) come from the context of ViewModel and fasting either through Rx or through LiveData. When the display is recreated, the results of these operations remain in memory, and this does not lead to any leaks.
If we talk about the disadvantages of LiveData and ViewModel, we can highlight the following points:
Actually everything that is written in the article seems rather primitive and obvious, but we consider the principle of Keep It Simple, Stupid to be one of the main ones in development, because following the simplest architectural principles you can solve most of the technical problems faced by any developer when writing an application . And no matter what it is called - MVP, MVC or MVVM - the main thing is to understand why you need it and what problems it will help to solve.
https://developer.android.com/topic/libraries/architecture/guide.html
https://en.wikipedia.org/wiki/KISS_principle
https://www.androiddesignpatterns.com/2013/08/fragment-transaction-commit-state-loss.html
https://android.jlelse.eu/android-architecture-components-viewmodel-e74faddf5b94
http://hannesdorfmann.com/android/arch-components-purist
Source: https://habr.com/ru/post/354700/
All Articles