Process states in android

Процессы и потоки в Android: пишем AsyncTask правильно

Продолжаю свои повествования об Android. И в этот раз хочу поделиться ценной информацией о процессах и потоках, которая должна быть хорошо усвоена и всегда оставаться под рукой во избежании ошибок и недопонимания при написании приложений. В конце статьи приведу пример реализации AsyncTask, который загружает в ImageView картинку по нажатию на кнопку.

Прежде всего отмечу, что подробнее о данной теме можно прочесть в данном руководстве — developer.android.com/guide/topics/fundamentals/processes-and-threads.html

На заметку о процессах и потоках в Android

Когда запускается компонент приложения и приложение не имеет других запущенных компонентов, Android создает новый процесс для приложения с одним потоком исполнения. По умолчанию все компоненты одного приложения запускаются в одном процессе, в потоке называемом «главный». Если компонент приложения запускается и уже существует процесс для данного приложения(какой-то компонент из приложения существует), тогда компонент запущен в этом процессе и использует его поток выполнения. Вы можете изменить данное поведение, задав разные процессы для разных компонентов вашего приложения. Кроме того вы можете добавить потоки в любой процесс.

Задать отдельный процесс для компонента можно с помощью файла манифеста. Каждый тег компонента(activity, service, receiver и provider) поддерживает атрибут android:process. Данный атрибут позволяет задать процесс, в котором будет выполняться компонент. Также вы можете задать процесс в котором будут выполняться компоненты разных приложений. Также данный атрибут поддерживается тегом application, что позволяет задать определенный процесс для всех компонентов приложения.

Android пытается поддерживать процесс приложения как можно дольше, но когда потребуются ресурсы старые процессы будут вытеснены по иерархии важности.

Существует 5 уровней иерархии важности: (процессы первого уровня из списка будут удалены последними)

1.Процесс с которым взаимодействует пользователь(Foreground process)
К таким процессам относится например: активити с которым взаимодействует пользовать; сервис(экземпляр Service), с которым взаимодействует пользователь; сервис запущенный методом startForeground(); сервис, который выполняет один из методов своего жизненного цикла; BroadcastReceiver который выполняет метод onReceive().

2.Видимый процесс
Процесс, в котором не выполнены условия из пункта №1, но который влияет на то, что пользователь видит на экране. К примеру, вызван метод onPause() активити.

3.Сервисный процесс
Служба запущенная методом startService()

4.Фоновый процесс
Процесс выполняемый в фоновом режиме, который невиден пользователю.

Отмечу, что в компонентах приложения существует метод onLowMemory(), но полагаться на то, что данный метод будет вызван нельзя, также как нельзя на 100% полагаться на метод onDestroy(), поэтому логику сохранения данных или каких-либо настроек можно осуществить в методе onStop(), который(как уверяют) точно вызывается.

Когда запускается приложение, система создает «главный» поток выполнения для данного приложения, который также называется UI-потоком. Этот поток очень важен, так как именно в нем происходит отрисовка виджетов(кнопочек, списков), обработка событий вашего приложения. Система не создает отдельный поток для каждого экземпляра компонента. Все компоненты, которые запущенны в одном процессе будут созданы в потоке UI. Библиотека пользовательского интерфейса Android не является потоково-безопасной, поэтому необходимо соблюдать два важных правила:

1) Не блокировать поток UI
2) Не обращаться к компонентам пользовательского интерфейса не из UI-потока

Теперь, предположим, что у нас возникла задача — загрузить картину в ImageView из сети и тут же ее отобразить. Как мы поступим? По логике: мы создадим отдельный поток, который и сделает всю нашу работу, примерно так:

Выглядит правдоподобно, так как мы вынесли операцию загрузки картинки в отдельный поток. Проблема в том, что мы нарушили правило №2. Исправить эту проблему можно с помощью следующих методов:

Activity.runOnUiThread(Runnable)
View.post(Runnable)
View.postDelayed(Runnable, long)

К примеру, воспользуемся первым из них:

Теперь реализация потоково-безопасная: сетевая операция выполняется в отдельном потоке, а к ImageView обращаемся из потока UI.
К счастью, данные операции можно объединить с помощью наследования класса Handler и реализации нужной логики, но лучшее решение — наследовать класс AsyncTask.

AsyncTask позволяет выполнить асинхронную работу и делать обновления пользовательского интерфейса.
Для обновления реализуйте метод onPostExecute(), а всю фоновую работу заключите в метод doInBackground(). После того, как вы реализуете свою собственную задачу, необходимо ее запустить методом execute().

Привожу обещанный пример AsyncTask, в котором реализована задача загрузки и отображения картинки(вариант с аннотациями и отклонением от применения стандартного протокола диалогов):

А теперь рассмотрим самый правильный вариант с точки зрения работы с диалогами:

Кода стало побольше, но лучше использовать стандартный протокол работы с диалогами.
Также я убрал все аннотации, чтобы новичкам было проще попробовать данный код.

Не забудьте добавить в свою разметку кнопочки атрибут с указанным значением: android:onClick=«runButtonHandler»

И добавлю: в оффициальном документе(Тыц ) также, как и в моем случае, не используется preExecute(), но если вам понадобится выполнить какие-то действия с вашим пользовательским интерфейсом до начала выполнения задачи, то смело используйте данный метод.

Параметры передаваемые в AsyncTask:
1. Параметры(в нашем случае адрес URL).
2. Прогресс (единицы задающие ход изменения задачи). В нашем случае не используется.
3. Результат выполнения задачи(в нашем случае объект Bitmap)

Код довольно прост: всю фоновую работу мы выполняем в методе doInBackGround(), вызывая метод publishProgress(), чтобы во время загрузки картинки крутился наш ProgressDialog при вызове метода onProgressUpdate(). После выполнения фоновой работы вызывается метод onPostExecute() в который передается результат работы метода doInBackGround(), в нашем случае это объект Bitmap и тут же мы его загружаем в ImageView.
Отмечу пару важных моментов, которые нужно учитывать:

1) Метод doInBackGround() выполняется в фоновом потоке, потому доступа к потоку UI внутри данного метода нет.
2) Методы onPostExecute() и onProgressUpdate() выполняются в потоке UI, потому мы можем смело обращаться к нашим компонентам UI.

Да, я снова применил библиотеку android-annotations, так что не пугайтесь аннотациям.

Читайте также:  Самый лучший фотомонтаж для андроид

Хочу отметить важность понимания модели работы процессов в Android, чтобы не допустить ошибок при разработке приложений.

Данная статья это переработка доступной информации + личный опыт при реализации указанной задачи и работе с потоками.

Как всегда пожелания и замечания по поводу материала статьи в личку. Если вам есть что-то добавить или дополнить — смело пишите в комментарии.

UPD Статья обновленна добавлением более правильной версии в плане работы с диалогами.

Источник

State and Jetpack Compose

State in an app is any value that can change over time. This is a very broad definition and encompasses everything from a Room database to a variable on a class.

All Android apps display state to the user. A few examples of state in Android apps:

  • A Snackbar that shows when a network connection can’t be established.
  • A blog post and associated comments.
  • Ripple animations on buttons that play when a user clicks them.
  • Stickers that a user can draw on top of an image.

Jetpack Compose helps you be explicit about where and how you store and use state in an Android app. This guide focuses on the connection between state and composables, and on the APIs that Jetpack Compose offers to work with state more easily.

State and composition

Compose is declarative and as such the only way to update it is by calling the same composable with new arguments. These arguments are representations of the UI state. Any time a state is updated a recomposition takes place. As a result, things like TextField don’t automatically update like they do in imperative XML based views. A composable has to explicitly be told the new state in order for it to update accordingly.

If you run this, you’ll see that nothing happens. That’s because the TextField doesn’t update itself—it updates when its value parameter changes. This is due to how composition and recomposition work in Compose.

Key Term: Composition: a description of the UI built by Jetpack Compose when it executes composables.

Initial composition: creation of a Composition by running composables the first time.

Recomposition: re-running composables to update the Composition when data changes.

To learn more about initial composition and recomposition, see Thinking in Compose.

State in composables

Composable functions can store a single object in memory by using the remember composable. A value computed by remember is stored in the Composition during initial composition, and the stored value is returned during recomposition. remember can be used to store both mutable and immutable objects.

mutableStateOf creates an observable MutableState , which is an observable type integrated with the compose runtime.

Any changes to value will schedule recomposition of any composable functions that read value . In the case of ExpandingCard , whenever expanded changes, it causes ExpandingCard to be recomposed.

There are three ways to declare a MutableState object in a composable:

  • val mutableState = remember
  • var value by remember
  • val (value, setValue) = remember

These declarations are equivalent, and are provided as syntax sugar for different uses of state. You should pick the one that produces the easiest-to-read code in the composable you’re writing.

The by delegate syntax requires the following imports:

You can use the remembered value as a parameter for other composables or even as logic in statements to change which composables are displayed. For example, if you don’t want to display the greeting if the name is empty, use the state in an if statement:

While remember helps you retain state across recompositions, the state is not retained across configuration changes. For this, you must use rememberSaveable . rememberSaveable automatically saves any value that can be saved in a Bundle . For other values, you can pass in a custom saver object.

Other supported types of state

Jetpack Compose doesn’t require that you use MutableState to hold state. Jetpack Compose supports other observable types. Before reading another observable type in Jetpack Compose, you must convert it to a State so that Jetpack Compose can automatically recompose when the state changes.

Compose ships with functions to create State from common observable types used in Android apps:

You can build an extension function for Jetpack Compose to read other observable types if your app uses a custom observable class. See the implementation of the builtins for examples of how to do this. Any object that allows Jetpack Compose to subscribe to every change can be converted to State and read by a composable.

Key Point: Compose will automatically recompose from reading State objects.

If you use another observable type such as LiveData in Compose, you should convert it to State before reading it in a composable using a composable extension function like LiveData .observeAsState() .

Caution: Using mutable objects such as ArrayList or mutableListOf() as state in Compose will cause your users to see incorrect or stale data in your app.

Mutable objects that are not observable, such as ArrayList or a mutable data class, cannot be observed by Compose to trigger recomposition when they change.

Instead of using non-observable mutable objects, we recommend you use an observable data holder such as State
> and the immutable listOf() .

Stateful versus stateless

A composable that uses remember to store an object creates internal state, making the composable stateful. HelloContent is an example of a stateful composable because it holds and modifies its name state internally. This can be useful in situations where a caller doesn’t need to control the state and can use it without having to manage the state themselves. However, composables with internal state tend to be less reusable and harder to test.

A stateless composable is a composable that doesn’t hold any state. An easy way to achieve stateless is by using state hoisting.

As you develop reusable composables, you often want to expose both a stateful and a stateless version of the same composable. The stateful version is convenient for callers that don’t care about the state, and the stateless version is necessary for callers that need to control or hoist the state.

Читайте также:  Android set string to string

State hoisting

State hoisting in Compose is a pattern of moving state to a composable’s caller to make a composable stateless. The general pattern for state hoisting in Jetpack Compose is to replace the state variable with two parameters:

  • value: T : the current value to display
  • onValueChange: (T) -> Unit : an event that requests the value to change, where T is the proposed new value

However, you are not limited to onValueChange . If more specific events are appropriate for the composable you should define them using lambdas like ExpandingCard does with onExpand and onCollapse .

State that is hoisted this way has some important properties:

  • Single source of truth: By moving state instead of duplicating it, we’re ensuring there’s only one source of truth. This helps avoid bugs.
  • Encapsulated: Only stateful composables will be able to modify their state. It’s completely internal.
  • Shareable: Hoisted state can be shared with multiple composables. Say we wanted to name in a different composable, hoisting would allow us to do that.
  • Interceptable: callers to the stateless composables can decide to ignore or modify events before changing the state.
  • Decoupled: the state for the stateless ExpandingCard may be stored anywhere. For example, it’s now possible to move name into a ViewModel .

In the example case, you extract the name and the onValueChange out of HelloContent and move them up the tree to a HelloScreen composable that calls HelloContent .

By hoisting the state out of HelloContent , it’s easier to reason about the composable, reuse it in different situations, and test. HelloContent is decoupled from how its state is stored. Decoupling means that if you modify or replace HelloScreen , you don’t have to change how HelloContent is implemented.

The pattern where the state goes down, and events go up is called a unidirectional data flow. In this case, the state goes down from HelloScreen to HelloContent and events go up from HelloContent to HelloScreen . By following unidirectional data flow, you can decouple composables that display state in the UI from the parts of your app that store and change state.

Key Point: When hoisting state, there are three rules to help you figure out where state should go:

  1. State should be hoisted to at least the lowest common parent of all composables that use the state (read).
  2. State should be hoisted to at least the highest level it may be changed (write).
  3. If two states change in response to the same events they should be hoisted together.

You can hoist state higher than these rules require, but underhoisting state will make it difficult or impossible to follow unidirectional data flow.

Restoring state in Compose

Use rememberSaveable to restore your UI state after an activity or process is recreated. rememberSaveable retains state across recompositions. In addition, rememberSaveable also retains state across activity and process recreation.

Ways to store state

All data types that are added to the Bundle are saved automatically. If you want to save something that cannot be added to the Bundle , there are several options.

Parcelize

The simplest solution is to add the @Parcelize annotation to the object. The object becomes parcelable, and can be bundled. For example, this code makes a parcelable City data type and saves it to the state.

MapSaver

If for some reason @Parcelize is not suitable, you can use mapSaver to define your own rule for converting an object into a set of values that the system can save to the Bundle .

ListSaver

To avoid needing to define the keys for the map, you can also use listSaver and use its indices as keys:

Managing state in Compose

Simple state hoisting can be managed in the composable functions itself. However, if the amount of state to keep track of increases, or the logic to perform in composable functions arises, it’s a good practice to delegate the logic and state responsibilities to other classes: state holders.

Key Term: State holders manage logic and state of composables.

Note that in other materials, state holders are also called hoisted state objects.

This section covers how to manage state in different ways in Compose. Depending on the complexity of the composable, there are different alternatives to consider:

  • Composables for simple UI element state management.
  • State holders for complex UI element state management. They own UI elements’ state and UI logic.
  • Architecture Components ViewModels as a special type of state holder in charge of providing access to the business logic and the screen or UI state.

State holders come in a variety of sizes depending on the scope of the corresponding UI elements they manage, ranging from a single widget like a bottom app bar to a whole screen. State holders are compoundable; that is, a state holder may be integrated into another state holder, especially when aggregating states.

The following diagram shows a summary of the relationships between the entities involved in Compose state management. The rest of the section covers each entity in detail:

  • A composable can depend on 0 or more state holders (that can be plain objects, ViewModels, or both) depending on its complexity.
  • A plain state holder might depend on a ViewModel if it needs access to the business logic or screen state.
  • A ViewModel depends on the business or data layers.

Summary of the (optional) dependencies for each entity involved in Compose state management.

Types of state and logic

In an Android app, there are different types of state to consider:

UI element state is the hoisted state of UI elements. For example, ScaffoldState handles the state of the Scaffold composable.

Читайте также:  Код для проверки аккумулятора смартфона андроид

Screen or UI state is what needs to be displayed on the screen. For example, a CartUiState class that can contain the Cart items, messages to show to the user or loading flags. This state is usually connected with other layers of the hierarchy because it contains application data.

And also, there are different types of logic:

UI behavior logic or UI logic is related to how to display state changes on the screen. For example, navigation logic decides which screen to show next, or UI logic that decides how to display user messages on the screen that could be using snackbars or toasts. The UI behavior logic should always live in the Composition.

Business logic is what to do with state changes. For example making a payment or storing user preferences. This logic is usually placed in the business or data layers, never in the UI layer.

Composables as source of truth

Having UI logic and UI elements state in composables is a good approach if the state and logic is simple. For example, here’s the MyApp composable handling ScaffoldState and a CoroutineScope :

Because ScaffoldState contains mutable properties, all interactions with it should happen in the MyApp composable. Otherwise, if we pass it to other composables, they could mutate its state, which doesn’t comply with the single source of truth principle and makes tracking down bugs more difficult.

State holders as source of truth

When a composable contains complex UI logic that involves multiple UI elements’ state, it should delegate that responsibility to state holders. This makes this logic more testable in isolation, and reduces the composable’s complexity. This approach favors the separation of concerns principle: the composable is in charge of emitting UI elements, and the state holder contains the UI logic and UI elements’ state.

State holders are plain classes that are created and remembered in the Composition. Because they follow the composable’s lifecycle, they can take Compose dependencies.

If our MyApp composable from the Composables as source of truth section grows in responsibilities, we can create a MyAppState state holder to manage its complexity:

Because MyAppState takes dependencies, it’s a good practice to provide a method that remembers an instance of MyAppState in the Composition. In this case, the rememberMyAppState function.

Now, MyApp is focused on emitting UI elements, and delegates all the UI logic and UI elements’ state to MyAppState :

As you can see, incrementing a composable’s responsibilities increases the need for a state holder. The responsibilities could be in UI logic, or just in the amount of state to keep track of.

ViewModels as source of truth

If plain state holders classes are in charge of the UI logic and UI elements’ state, a ViewModel is a special type of state holder that is in charge of:

  • providing access to the business logic of the application that is usually placed in other layers of the hierarchy such as the business and data layers, and
  • preparing the application data for presentation in a particular screen, which becomes the screen or UI state.

ViewModels have a longer lifetime than the Composition because they survive configuration changes. They can follow the lifecycle of the host of Compose content–that is, activities or fragments–or the lifecycle of a destination or the Navigation graph if you’re using the Navigation library. Because of their longer lifetime, ViewModels should not hold long-lived references to state bound to the lifetime of the Composition. If they do, it could cause memory leaks.

We recommend screen-level composables to use ViewModels for providing access to business logic and being the source of truth for their UI state. Check the ViewModel and state holders section to see why ViewModels are a good fit for this.

The following is an example of a ViewModel used in a screen-level composable:

ViewModel and state holders

The benefits of ViewModels in Android development make them suitable for providing access to the business logic and preparing the application data for presentation on the screen. Namely, the benefits are:

  • Operations triggered by ViewModels survive configuration changes.
  • Integration with Navigation:
    • Navigation caches ViewModels while the screen is on the back stack. This is important to have your previously loaded data instantly available when you return to your destination. This is something more difficult to do with a state holder that follows the lifecycle of the composable screen.
    • The ViewModel is also cleared when the destination is popped off the back stack, ensuring that your state is automatically cleaned up. This is different from listening for the composable disposal that can happen for multiple reasons such as going to a new screen, due to a configuration change, etc.
  • Integration with other Jetpack libraries such as Hilt.

Note: If ViewModel benefits don’t apply to your use case or you do things in a different way, you can move ViewModel’s responsibilities into state holders.

Because state holders are compoundable and ViewModels and plain state holders have different responsibilities, it’s possible for a screen-level composable to have both a ViewModel that provides access to business logic AND a state holder that manages its UI logic and UI elements’ state. Since ViewModels have a longer lifespan than state holders, state holders can take ViewModels as a dependency if needed.

The following code shows a ViewModel and plain state holder working together on an ExampleScreen :

Learn more

To learn more about state and Jetpack Compose, consult the following additional resources.

Codelabs

Videos

Content and code samples on this page are subject to the licenses described in the Content License. Java is a registered trademark of Oracle and/or its affiliates.

Источник

Оцените статью