Tips for creating apps for graduating from Yandex Mobile School

  • Tutorial
Already very soon recruitment to the School of Mobile Development, which will traditionally be held in Moscow, will be completed. The emphasis in it will be on practical exercises - team mini-hackathons, in which, in addition to writing code, you will need to make decisions, deal with controversial issues and engage in long-term planning. Students from Yandex will help students - each team individually. Read more about the upcoming school here . We will finish accepting applications on May 6 at 23:59 Moscow time, and while there is still time to complete the tasks, we decided to make out last year’s version. You will find out what mistakes are often made by novice developers and what you should pay attention to when writing the code of your first application.



Traditionally, the task is designed so that we can pay attention to various aspects of development. These include application architecture, stability, performance, layout, usability. All components are equally important: even perfectly combed and layered code with a high probability will not pass the selection if there are problems in the interface or crashes during the execution of basic user scripts. There is no universal recipe for making the perfect application, which is guaranteed to pass the selection. There are many development approaches and various options for building an architecture, but one of the components of success is a positive user experience. The product should give the impression of completeness, no matter how much useful functionality, screens or elements are in it.

Content



Repository and coding style


The theater begins with a hanger, and the project begins with a repository.

Before starting development, add the appropriate .gitignore file so that extra files do not exactly get into your repository. So, it is not recommended to store the settings file of your IDE, files with keys for signing the application, and any logins and passwords in the repository.

During development, follow the golden mean - you should not commit half a project at a time, but many small commits along one or two lines should be avoided. Write clear messages for each commit. For individual features it is worth using branchesin order to develop features independently and always have the current (and working) version of the application in the master branch. First, try to implement the minimum necessary features or only then all the "prettiness" and improvements. If you need to switch to another branch and you do not want to commit incomplete code (but do not want to lose it), it is convenient to use stash .

The code is written once and read many times, so it is important that the coding style and variable names are adequate. Leave meaningful comments if you need them, do not add magic numbers and do not mix code from various libraries that do the same thing. If the code is written by one developer, then maintaining a common style and approach is easy, but in a large team you can use various analyzers or create your own style and commit it to a version control system so that it is picked up by each developer.

Here are some other things to consider:

  • hardcode, useless code, commented out code,
  • super calls,
  • extra annotations
  • Missing annotations
  • getting into Release code necessary only in Debug-assembly
  • missing final modifier ,
  • long methods
  • the same methods
  • unused imports,
  • new Something () without assignment,
  • numbers magic ,
  • copy paste, using code you don’t understand
  • and also - use lint .

SDK and libraries


Using the Android SDK and libraries often raises questions among novice developers. Below we list some common problems and their solutions.

Incorrect work with lists
Using LinearLayout + ScrollView to display many of the same type of view means making a mistake. This method creates a load on the memory, all view are in it at the same time. Scroll starts to slow down. In this case, it is correct to use at least a ListView, or better, a more advanced RecyclerView . These containers provide reuse of the view (using the adapter mechanism). The views that are currently not visible to the user are filled with fresh data.

But here you can make a mistake. RecyclerView, unlike ListView, forces the developer to use the ViewHolder pattern. As its name implies, this pattern consists in creating a class whose objects store links to already found in the view hierarchy. You should not call findViewById every time you need to put down some value. You need to do this once, during the creation of the holder.

Saving the state
Another big problem for beginners is saving the state of the application when changing the configuration (for example, when the orientation, language, screen size , etc. , change) .) It often happens that beginners do not test such cases. This leads to user dissatisfaction or even crashes. Sometimes developers stop at fixing the orientation - which, of course, does not save. There are several ways to support an out-of-box configuration change. We will not delve into this, you can always read the documentation . The main thing to remember is that such a problem exists, to test, to pay attention to the dialogs (it is better to use DialogFragment , rather than AlertDialog), since they must also restore state. It must be remembered that storing the state of the screen in a persistent store (for example, SharedPreferences) is not recommended. As a result, a “clean” launch can lead to an existing state, and this is not particularly idiomatic.

Data loading and caching
It's no secret that going to the network on a UI stream in Android is prohibited . There are many libraries for the network, for example OkHttp , if you have REST, add Retrofit , and to upload pictures - Glide orPicasso Thus, for a long time it is no longer necessary to use the HttpUrlConnection inside AsyncTask (especially for downloading pictures - which is actually not easy). But many beginners do not think that, for example, reading and writing to the database is also an I / O operation that can affect the UX due to friezes of the main stream. The same applies to reading files, access to ContentProviders. All such operations must occur in a separate thread. What to use for this - everyone decides for himself, to describe the whole variety of solutions in this format is not possible.

Changing the system behavior of the Back button
Often there is a temptation to hang a reaction that is non-standard for the system, including completely swallowing this event (to process it, but do nothing at the same time). So - it’s better not to. The user of this OS has habits, and Back should lead to the previous screen or lead to the exit from the application.

Architecture


Android app architecture is often a sore spot even for experienced developers. In this section, we give some general tips. Whether you follow them is up to you.

Architecture and decomposition
Lack of decomposition is a sign of poor architecture. The platform does not give clear instructions on how to write applications correctly, so there is a great temptation to write everything into code in Activity or Fragment. As a result, the code becomes untestable, it is difficult to make any changes. To avoid this situation, you can apply modern architectural practices and design patterns. Read about MVP, MVVM, MVI. Write the first three classes and cover them with tests. You will probably notice that writing tests is difficult and you need to think about architecture.

Search with abstractions
GitHub repositories often show a “canonical” implementation of a particular architecture. Blind copying of other people's architectural solutions can not only not bring benefits, but also complicate your work and degrade application performance. Try not to write meaningless code just because it’s “more correct” or “cleaner”, and in each case, correlate the benefits of the solution and the complexity of its support and understanding.

Implicit relationships between components, impure functions
Quite often, the value of a global variable is statically stored in Application and changes from different parts of the application. Since the execution of the seemingly non-directly related parts of the code is affected by a global variable, conditions may arise in the application that the developer did not expect to receive at execution. The worst thing is that such bugs are very difficult to catch and reproduce. Try to write “clean” methods and explicitly specify dependencies in class constructors or in method arguments.

UI (resources, graphics, layout)


The appearance and usability of the application are very important. It is difficult to make a convenient design, but we recommend paying attention to at least the points outlined below.

Layout quality


Performance in Android pretty much depends on the layout quality. Complex container hierarchies take longer to compute. In addition, with a large nesting, drops during calculation and rendering are possible (due to stack overflow). With the advent of ConstraintLayout (and especially with the installation of its xml root element when creating from a wizard), the hierarchies became much more complicated for beginners. Nested Relative / ConstraintLayout is most often used, which is fundamentally wrong. ConstraintLayout is designed just to make the hierarchy flat. We recommend that you read at least the introduction to the documentation and try to apply this class correctly. Also avoid over-nesting the ViewGroup.

Design


Not all developers have design skills. Often there is an inconsistent color palette in the application that the developer likes, but most users don’t. It happens that not only people with disabilities (for example, color blind) cannot read labels, but also other users. Standard check: if the labels are visible in grayscale, most likely, most users will also see them. To select a color palette and general design principles, you can see two links: material.io and www.materialpalette.com .

What else is worth doing


  • Work through aspects that give a sense of completeness of the application: name, icon, about screen.
  • Adapt the appearance of the application to a different density of dots on the screen.
  • Remove unused string, graphic, and other resources.
  • Localize to other languages.
  • If the previous paragraph is completed, be sure to localize everything you need.

Error processing


The army of users of your application, as a good tester, will always find a way to break something for you. Be sure to check the application in unusual situations. It is important to develop the mindset of the tester and anticipate all kinds of non-standard usage scenarios (starting without the Internet, adding an entry to the history two times, a sudden change in device settings, etc.). After finishing work on any functionality, it is important to test this functionality and run several scripts to make sure that everything works. The problem is that testing your code is difficult, as there is an internal feeling and hope that everything works correctly. Nobody wants to write idle code. Do not follow your feelings. It is better to check out several scenarios with normal and boundary conditions.

When processing errors, you can go to two extremes - to process everything and everything without thinking or not to process anything. The second option, obviously, pops up pretty quickly, but the first is more insidious and leads to unpredictable consequences.

Let us dwell in more detail on some examples of such errors.

try..catch ... everywhere


Such processing, as the name implies, consists in enclosing any suspicious (from the author’s point of view) code in a try..catch block. The distribution includes NPE and IndexOutOfBoundsException , IllegalArgumentException, and even OutOfMemoryError , that is, exceptions that usually indicate logical errors in the application, about the states of the application from which it cannot be adequately restored. Of course, the correct solution would be to correct logical errors. In addition, when writing code in Java, you can use static analysis and put at least annotations @NonNull and @Nullable wherever you need. This will help catch NPE.

Weakreference


Often, newcomers, learning about memory leaks, begin to fear them like fire. With a lack of experience, this can turn into a wrapping of any objects in WeakReference . Typically, this technique indicates a poor understanding of the life cycle of objects and the relationships between them. Here is an example:

public class MyAdapter extends RecyclerView.Adapter {
    private final WeakReference contextRef;
    public MyAdapter (@NonNull Context context) {
        this.contextRef = new WeakReference < > (context);
    }
    @Override
    public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        final Context context = contextRef.get();
        return context == null ? null : LayoutInflater.from(context).inflate(R.layout.item, parent, false);
    }
    ...
} 

Here in the adapter constructor comes a link to Context. But the adapter is part of the UI, which is shown in a certain Activity. Therefore, he should not live longer than Context, and from this it follows that WeakReference is not needed here.

Another example of using WeakReference:

public class MyAsyncTask extends AsyncTask {
    private final WeakReference contextRef;
    public MyAsyncTask(@NonNull Context context) {
        this.contextRef = new WeakReference<>(context);
    }
    ...
    @Override
    protected void onPostExecute(@Nullable String result) {
        final Context context = contextRef.get();
        if (context != null && result != null) {
            Toast.makeText(context, result, Toast.LENGTH_SHORT).show();
        }
    }
} 

Nothing has changed in appearance, but AsyncTask is able to survive the Activity in which it was created. Therefore, the use of WeakReference is justified here.

To avoid memory leaks, you should first understand what the life cycle of the objects you are using is. You can use Memory Profiler and LeakCanary to search for memory leaks in an existing large project .

No request cancellation


Sometimes the cause of errors lies in the lack of cancellation of asynchronous operations, network requests or timers. Consider the durability and frequency of these operations, pay attention to the paired API methods like subscribe / unsubscribe, create / destroy, start / cancel.

Tests


Lack of unit tests


We love unit tests because when creating a product they help to write better and more maintainable code, and also serve as a kind of documentation that does not become obsolete over time. Some developers neglect unit tests - they say that it is difficult and long to write, their launch takes a lot of time.

Each of these points can be challenged. Let's start with the item about time costs. Perhaps at the very beginning writing unit tests will be time-consuming, but with experience speed will increase. In addition, unit tests are an investment in the future, which in the future will allow you to write code faster. The problem with startup speed is solved even easier: just configure Continuous Integration onceand run the tests automatically. If unit tests are difficult to write, then you are not writing the best quality code. Good architecture makes the task easier.

Useless tests


Useless tests not only make it difficult to understand the source code, but also affect the build time of the application, so it is even worse than useless code. There are several cases where tests are useless:

Testing someone else's code
It is assumed that someone else's code already comes with tests, so such testing only takes time.

Tests depend on each other or on external factors.
Depending on each other, tests are difficult to maintain, since changing one test affects many others. In addition, testing becomes unpredictable because tests can run in a different order.

Testing Implementation Details
Tests should perceive the tested classes as a black box, otherwise, when changing the implementation, the test itself will have to be changed. This is inconvenient and often results in deleting or ignoring the test.

Conclusion


Last year, we analyzed several hundred test tasks and once again made sure that the main causes of problems in the application are quite simple:

  • Last minute corrections without performance checks.
  • Missing or incorrect error handling in the code.
  • Incorrect interpretation of the API method description, omission of any operating features or limitations of platform components.
  • Lack of care when organizing relationships between components, when working with links and when taking into account the life cycle of objects.
  • Use of new untested technologies.

This year we recommend to everyone who enters school this year and those who are developing their first application:

  • Be more attentive and check the operability of the application even after making harmless-looking changes.
  • Learn the subtleties of the language: the principles of working with objects in memory, types of links.
  • Use a static code analyzer and additional tools like LeakCanary.
  • To study the limitations and features of the used platform components and libraries.
  • Use with caution new technologies that have not had time to prove themselves on well-known projects. The API or code may be unstable, the network may not contain documentation or analysis of typical errors, you will have to look for answers to the questions yourself.

useful links


It is useful for a beginner developer who has not written a single line for Android to start his career with courses from Google to Udacity (in English with Russian subtitles). If you don’t drop the course after the first few videos and complete it to the end, you will receive not only good theoretical knowledge, but also the first application that you made yourself!

The Android Developers YouTube channel is simply a storehouse of short instructional videos and news from the world of Android development. We recommend that you look at Android Performance Patterns , because performance is important, isn't it?

In order to keep up with the trends and always know where to develop further, you can learn a little new every week using the Android Weekly newsletter(in English) or listen to podcasts like Fragmented and AndroidDevPodcast on your way home .

Getting to know Kotlin is best started with Kotlin koans - you can get an idea of ​​the basic syntax of the language. A video course from Computer Science Center is worth going through to learn the language in more detail, learn about its pros and cons, and understand why it is implemented that way. In the case of Java, there is also a fundamental course from the Computer Science Center - theoretical knowledge is supported by a large number of practical tasks.

You can look at the implementation of common architectures in the Android Architecture Blueprints repository .

To study architectural patterns, books are often recommended that are difficult for a beginner to master. SourceMaking is ideally suited for the first acquaintance - the material is well-structured, and the majority of patterns have clear metaphors and examples of implementation in Java.

After completing the Computer Science Center's Algorithms Online Course , you will become familiar with the basic algorithmic methods and beat your hand when implementing classic algorithms. Also in the course you can find a good list of references. If the practice is not enough, you can always practice at www.hackerrank.com . Just do not waste time on tasks with Easy difficulty level - they are too simple. Learngitbranching.js.org

allows you to learn the basics of git, and more advanced features can be mastered in the Enki mobile app.

For those who decide to use git exclusively from the console, it is better to learn the basic Vim commands, for example, using the game vim-adventures.com .

Also popular now: