Android — Компоненти та View
Який життєвий цикл у активіті, фрагмента, View
Activity Lifecycle . (життєвий цикл Активіті)
onCreate():
Викликається під час створення активності. Це місце для ініціалізації основних компонентів активності, таких як користувацький інтерфейс (UI) та даних.
onStart():
Викликається, коли активність стає видимою для користувача.
onResume():
Викликається, коли активність починає взаємодіяти з користувачем. Активність перебуває на передньому плані і користувач може з нею взаємодіяти.
...
onPause():
Викликається, коли активність іде на задній план, але все ще видима. Використовується для збереження даних, зупинки анімацій або інших ресурсів, які не повинні тривати у фоновому режимі.
onStop():
Викликається, коли активність більше не видима для користувача. Це відбувається, коли активність закривається або переходить у фоновий режим.
onDestroy():
Викликається перед тим, як активність буде остаточно знищена. Це місце для звільнення всіх решти ресурсів.
onRestart():
Викликається, коли активність була зупинена і знову запускається перед onStart().
import android.os.Bundleimport androidx.appcompat.app.AppCompatActivityimport android.util.Log
class MainActivity : AppCompatActivity() {
private val TAG = "MainActivity"
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) Log.d(TAG, "onCreate called") }
override fun onStart() { super.onStart() Log.d(TAG, "onStart called") }
override fun onResume() { super.onResume() Log.d(TAG, "onResume called") }
override fun onPause() { super.onPause() Log.d(TAG, "onPause called") }
override fun onStop() { super.onStop() Log.d(TAG, "onStop called") }
override fun onDestroy() { super.onDestroy() Log.d(TAG, "onDestroy called") }
override fun onRestart() { super.onRestart() Log.d(TAG, "onRestart called") }
}Fragment Lifecycle . (життєвий цикл Фрагмента)
onAttach():
Викликається, коли фрагмент приєднується до своєї активності.
onCreate():
Викликається під час створення фрагмента. Тут можна виконувати ініціалізацію, яка не залежить від користувацького інтерфейсу.
onCreateView():
Викликається для створення користувацького інтерфейсу фрагмента. Тут потрібно інфлейтити (роздувати) макет фрагмента.
onViewCreated():
Викликається після того, як подання було створено. Тут можна виконувати остаточні ініціалізації подань.
onActivityCreated():
Викликається, коли метод onCreate активності завершено.
onStart():
Викликається, коли фрагмент стає видимим для користувача.
onResume():
Викликається, коли фрагмент стає активним і готовий до взаємодії з користувачем.
...
onPause():
Викликається, коли фрагмент іде на задній план, але все ще видимий.
onStop():
Викликається, коли фрагмент більше не видимий.
onDestroyView():
Викликається перед знищенням подання фрагмента.
onDestroy():
Викликається перед знищенням фрагмента.
onDetach():
Викликається, коли фрагмент від'єднується від своєї активності.
import android.content.Contextimport android.os.Bundleimport android.view.LayoutInflaterimport android.view.Viewimport android.view.ViewGroupimport android.util.Logimport androidx.fragment.app.Fragment
class ExampleFragment : Fragment() {
private val TAG = "ExampleFragment"
override fun onAttach(context: Context) { super.onAttach(context) Log.d(TAG, "onAttach called") }
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) Log.d(TAG, "onCreate called") }
override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { Log.d(TAG, "onCreateView called") // Inflate the layout for this fragment return inflater.inflate(R.layout.fragment_example, container, false) }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) Log.d(TAG, "onViewCreated called") }
override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) Log.d(TAG, "onActivityCreated called") }
override fun onStart() { super.onStart() Log.d(TAG, "onStart called") }
override fun onResume() { super.onResume() Log.d(TAG, "onResume called") }
override fun onPause() { super.onPause() Log.d(TAG, "onPause called") }
override fun onStop() { super.onStop() Log.d(TAG, "onStop called") }
override fun onDestroyView() { super.onDestroyView() Log.d(TAG, "onDestroyView called") }
override fun onDestroy() { super.onDestroy() Log.d(TAG, "onDestroy called") }
override fun onDetach() { super.onDetach() Log.d(TAG, "onDetach called") }
}View Lifecycle . (життєвий цикл View)
Constructor:
під час створення користувацького подання ви повинні спочатку перевизначити конструктор, який ініціалізує View, у ньому можна проводити необхідні обчислення
onAttachedToWindow():
Викликається, коли View приєднується до вікна.
onMeasure(int widthMeasureSpec, int heightMeasureSpec):
Викликається для визначення розміру View та його дочірніх елементів.
onLayout(boolean changed, int left, int top, int right, int bottom):
Викликається для встановлення розміру та положення View та його дочірніх елементів.
onDraw(Canvas canvas):
Викликається для відмальовування вмісту View.
...
onDetachedFromWindow():
Викликається, коли View від'єднується від вікна.
import android.content.Contextimport android.graphics.Canvasimport android.util.AttributeSetimport android.util.Logimport android.view.View
class CustomView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : View(context, attrs, defStyleAttr) {
private val TAG = "CustomView"
override fun onAttachedToWindow() { super.onAttachedToWindow() Log.d(TAG, "onAttachedToWindow called") }
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { super.onMeasure(widthMeasureSpec, heightMeasureSpec) Log.d(TAG, "onMeasure called") // Example: Set the size to be exactly 200x200 pixels setMeasuredDimension(200, 200) }
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) { super.onLayout(changed, left, top, right, bottom) Log.d(TAG, "onLayout called") // Layout the child views if any }
override fun onDraw(canvas: Canvas?) { super.onDraw(canvas) Log.d(TAG, "onDraw called") // Draw the custom view content canvas?.drawColor(android.graphics.Color.RED) }
override fun onDetachedFromWindow() { super.onDetachedFromWindow() Log.d(TAG, "onDetachedFromWindow called") }
}ViewModel Lifecycle . (життєвий цикл ViewModel)
onCleared():
Викликається, коли ViewModel знищується. Тут можна звільняти ресурси, такі як скасування завдань, закриття потоків тощо.
import androidx.lifecycle.LiveDataimport androidx.lifecycle.MutableLiveDataimport androidx.lifecycle.ViewModel
class MyViewModel : ViewModel() {
override fun onCleared() { super.onCleared() // Звільнення ресурсів, якщо необхідно }
}Service Lifecycle . (життєвий цикл Сервісу)
onCreate():
Викликається під час першого створення сервісу. Тут можна виконувати ініціалізацію необхідних ресурсів.
onStartCommand(Intent intent, int flags, int startId):
Викликається під час кожного запуску сервісу. Тут виконується основна робота сервісу.
onBind(Intent intent):
Викликається під час прив'язування компонента (наприклад, активності) до сервісу за допомогою bindService(). Цей метод використовується лише для прив'язаних сервісів.
...
onUnbind(Intent intent):
Викликається під час від'єднання компонента від прив'язаного сервісу.
onRebind(Intent intent):
Викликається, якщо компонент повторно прив'язується до сервісу після виклику onUnbind().
onDestroy():
Викликається перед знищенням сервісу. Тут можна звільняти ресурси.
import android.app.Serviceimport android.content.Intentimport android.os.IBinderimport android.util.Log
class MyService : Service() {
private val TAG = "MyService"
override fun onCreate() { super.onCreate() Log.d(TAG, "Service Created") }
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { Log.d(TAG, "Service Started")
// Виконання фонової роботи Thread(Runnable { try { // Симуляція тривалої роботи Thread.sleep(3000) Log.d(TAG, "Service Running") } catch (e: InterruptedException) { e.printStackTrace() } stopSelf() }).start()
// Якщо сервіс має бути перезапущений після завершення, повертаємо START_STICKY return START_STICKY }
override fun onBind(intent: Intent?): IBinder? { Log.d(TAG, "Service Bound") return null }
override fun onDestroy() { super.onDestroy() Log.d(TAG, "Service Destroyed") }
}

Які існують шари в чистій архітектурі і за що вони відповідають
Зазвичай застосунок розділяється на 3 шари, які можуть перебувати в різних модулях- Presentation, Domain, Data:
Presentation:
UI, ViewModel та інші подібні класи
Domain / Business Logic:
інтерфейси UseCase та їх реалізації
інтерфейси репозиторіїв (без реалізації)
локальні моделі даних, з якими працює застосунок
Data:
реалізації інтерфейсів репозиторіїв
мережеві моделі даних
моделі бази даних
інтерфейси Newtork API
Зазвичай залежності в Gradle налаштовуються таким чином:
Presentation:
implementation project(“:domain”)
implementation project(“:data”)
Data:
implementation project(‘:domain’)
Domain:
// no
З яких елементів складається Android застосунок
Activity:
це основний компонент користувацького інтерфейсу, який зазвичай представляє один екран застосунку. Кожна активність має свій життєвий цикл і може запускати інші активності.
Fragment:
це компоненти, які можуть бути використані всередині активностей для створення складнішого користувацького інтерфейсу. Фрагменти також мають свій життєвий цикл і можуть використовуватися для створення перевикористовуваних компонентів.
Service:
це компоненти, які виконують фонові операції в застосунку, не взаємодіючи з користувачем. Сервіси можуть працювати у фоновому режимі навіть після того, як користувач закрив застосунок.
Broadcast Receiver:
це компоненти, які дозволяють застосунку отримувати повідомлення від системи або інших застосунків, навіть коли застосунок не активний. Широкомовні приймачі можуть бути використані для реагування на системні події, такі як зміни стану мережі або заряджання батареї.
Content Provider:
це компоненти, які дозволяють застосунку зберігати та обмінюватися даними з іншими застосунками або системою. Провайдери контенту можуть використовуватися для доступу до даних, що зберігаються в базі даних або на віддаленому сервері.
AndroidManifest.xml:
це файл, який містить інформацію про компоненти застосунку та його налаштування. Маніфест застосунку також визначає дозволи, необхідні для доступу до різних функцій пристрою.
Яка різниця між MVP, MVVM, MVC, MVI
Model-View-Controller (MVC):
розділяє застосунок на три компоненти: Model, View та Controller.
Model:
Керує даними та бізнес-логікою.
View:
Відповідає за відображення даних та взаємодію з користувачем.
Controller:
Посередник між View та Model, обробляє користувацькі дії та оновлює View.
// Modeldata class User(val name: String, val age: Int)
// Controllerclass UserController(private val view: UserView) {
private val user = User("John Doe", 25)
fun loadUser() { view.showUser(user) }
}
// Viewinterface UserView { fun showUser(user: User)}
// Activity implementing Viewclass MainActivity : AppCompatActivity(), UserView {
private lateinit var controller: UserController
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) controller = UserController(this) controller.loadUser() }
override fun showUser(user: User) { // Update UI with user data findViewById<TextView>(R.id.textView).text = "${user.name}, ${user.age}" }
}Model-View-Presenter (MVP):
розділяє застосунок на три компоненти: Model, View та Presenter.
Model:
Керує даними та бізнес-логікою.
View:
Відповідає за відображення даних та взаємодію з користувачем.
Presenter:
Посередник між View та Model, обробляє користувацькі дії та оновлює View.
// Modeldata class User(val name: String, val age: Int)
// Viewinterface UserView { fun showUser(user: User)}
// Presenterclass UserPresenter(private val view: UserView) {
private val user = User("John Doe", 25)
fun loadUser() { view.showUser(user) }
}
// Activity implementing Viewclass MainActivity : AppCompatActivity(), UserView {
private lateinit var presenter: UserPresenter
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) presenter = UserPresenter(this) presenter.loadUser() }
override fun showUser(user: User) { // Update UI with user data findViewById<TextView>(R.id.textView).text = "${user.name}, ${user.age}" }
}Model-View-ViewModel (MVVM):
розділяє застосунок на три компоненти: Model, View та ViewModel.
Model:
Керує даними та бізнес-логікою.
View:
Відповідає за відображення даних та взаємодію з користувачем.
ViewModel:
Посередник між View та Model, керує даними для View та обробляє користувацькі дії.
// Modeldata class User(val name: String, val age: Int)
// ViewModelclass UserViewModel : ViewModel() {
private val _user = MutableLiveData<User>() val user: LiveData<User> get() = _user
fun loadUser() { _user.value = User("John Doe", 25) }
}
// Activityclass MainActivity : AppCompatActivity() {
private lateinit var viewModel: UserViewModel
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) viewModel = ViewModelProvider(this).get(UserViewModel::class.java) viewModel.user.observe(this, Observer { user -> // Update UI with user data findViewById<TextView>(R.id.textView).text = "${user.name}, ${user.age}" }) viewModel.loadUser() }
}Model-View-Intent (MVI):
ґрунтується на концепціях реактивного програмування та уні-directional data flow.
Model:
Керує даними та бізнес-логікою.
View:
Відповідає за відображення даних та взаємодію з користувачем.
Intent:
Визначає наміри користувача та спрямовує їх в обробку.
// Modeldata class User(val name: String, val age: Int)
// View Statedata class UserViewState(val user: User? = null)
// Intentsealed class UserIntent { object LoadUser : UserIntent()}
// ViewModelclass UserViewModel : ViewModel() {
private val _viewState = MutableLiveData<UserViewState>() val viewState: LiveData<UserViewState> get() = _viewState
fun processIntents(intent: UserIntent) { when (intent) { is UserIntent.LoadUser -> loadUser() } }
private fun loadUser() { _viewState.value = UserViewState(User("John Doe", 25)) }
}
// Activityclass MainActivity : AppCompatActivity() {
private lateinit var viewModel: UserViewModel
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) viewModel = ViewModelProvider(this).get(UserViewModel::class.java) viewModel.viewState.observe(this, Observer { viewState -> viewState.user?.let { user -> // Update UI with user data findViewById<TextView>(R.id.textView).text = "${user.name}, ${user.age}" } }) // Dispatch intent to load user viewModel.processIntents(UserIntent.LoadUser) }
}MVP:
Зручний для невеликих застосунків. Presenter напряму керує View та Model.
MVVM:
Добре підходить для застосунків з реактивним програмуванням та великою кількістю UI-логіки. ViewModel забезпечує двостороннє зв'язування даних.
MVC:
Найпростіший патерн. Контролер керує взаємодією між Model та View.
MVI:
Підходить для складних застосунків з використанням реактивного програмування та уні-directional data flow. Intent та View State роблять логіку застосунку передбачуваною та легко тестованою.
Що таке Android ViewModel
Android ViewModel:
це компонент архітектури Jetpack, який допомагає зберігати та керувати даними UI-компонентів (Activity, Fragment) під час поворотів екрана, перестворення застосунку та інших змін життєвого циклу застосунку.
ViewModel є частиною архітектурного шаблону Model-View-ViewModel (MVVM) і зазвичай використовується в поєднанні з LiveData, щоб надавати актуальні дані для користувацького інтерфейсу.
У ViewModel є лише один метод життєвого циклу- OnCleared()
Android SharedViewModel:
це компонент архітектури Jetpack, який дозволяє обмінюватися даними між двома або більше пов'язаними UI-компонентами (Activity, Fragment) з використанням одного й того самого екземпляра ViewModel.
SharedViewModel зазвичай використовується у випадках, коли два або більше UI-компонентів повинні мати доступ до одних і тих самих даних, але не є прямо пов'язаними один з одним. Наприклад, у випадку, коли у нас є список елементів в одному фрагменті, а деталі елемента відображаються в іншому фрагменті, який перебуває в іншій Activity.
Що таке Android Broadcast Receivers
Компоненти Android, які дозволяють застосунку отримувати системні та користувацькі повідомлення (broadcast) від інших застосунків, а також від самої системи.
Broadcast Receiver слухає та фільтрує повідомлення, і якщо подія відповідає заданим фільтрам, то запускає код, пов'язаний з цією подією.
Приклади таких подій можуть включати зміну стану мережі, зміну рівня заряду батареї, отримання повідомлення від іншого застосунку тощо.
Broadcast Receiver може використовуватися для виконання якихось завдань на пристрої, наприклад, для сповіщення користувача про зміни стану пристрою або для запуску завдань у фоновому режимі.
Для використання Android Broadcast Receiver необхідно створити клас, який розширюватиме клас BroadcastReceiver та перевизначати метод onReceive(), який викликатиметься під час отримання широкомовного повідомлення.
class MyBroadcastReceiver : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) { // обробка отриманого повідомлення }
}Далі необхідно зареєструвати цей Broadcast Receiver у файлі AndroidManifest.xml:
<receiver android:name=".MyBroadcastReceiver"> <intent-filter> <action android:name="android.intent.action.ACTION_NAME" /> </intent-filter></receiver>У цьому прикладі MyBroadcastReceiver оброблятиме повідомлення з дією “android.intent.action.ACTION_NAME”. Під час отримання такого повідомлення викликатиметься метод onReceive() цього Broadcast Receiver.
Також можна зареєструвати Broadcast Receiver динамічно в коді, використовуючи методи registerReceiver() та unregisterReceiver() класу Context.
Що таке Content Provider
компонент архітектури застосунку в Android, який забезпечує доступ до даних застосунку ззовні. Він може використовуватися для обміну даними між застосунками, надання доступу до даних з інших застосунків або для збереження та отримання даних з бази даних.
Ось приклад використання Content Provider для збереження та отримання даних з бази даних SQLite в Android:
Визначення контент-провайдера в маніфесті застосунку:
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.myapp"> <application> ... <provider android:name=".MyContentProvider" android:authorities="com.example.myapp.provider" android:exported="false" /> </application></manifest>Створення класу MyContentProvider:
Він успадковуватиметься від ContentProvider та визначення бази даних і URI, які використовуватимуться для доступу до даних
class MyContentProvider : ContentProvider() {
private lateinit var dbHelper: MyDatabaseHelper
override fun onCreate(): Boolean { context?.let { dbHelper = MyDatabaseHelper(it) } return true }
override fun query( uri: Uri, projection: Array<String>?, selection: String?, selectionArgs: Array<String>?, sortOrder: String? ): Cursor? { val db = dbHelper.readableDatabase val cursor = db.query( TABLE_NAME, projection, selection, selectionArgs, null, null, sortOrder ) cursor.setNotificationUri(context?.contentResolver, uri) return cursor }
override fun getType(uri: Uri): String? { return "vnd.android.cursor.dir/$AUTHORITY.$TABLE_NAME" }
override fun insert(uri: Uri, values: ContentValues?): Uri? { val db = dbHelper.writableDatabase val id = db.insert(TABLE_NAME, null, values) context?.contentResolver?.notifyChange(uri, null) return ContentUris.withAppendedId(uri, id) }
override fun delete(uri: Uri, selection: String?, selectionArgs: Array<String>?): Int { val db = dbHelper.writableDatabase val count = db.delete(TABLE_NAME, selection, selectionArgs) context?.contentResolver?.notifyChange(uri, null) return count }
override fun update( uri: Uri, values: ContentValues?, selection: String?, selectionArgs: Array<String>? ): Int { val db = dbHelper.writableDatabase val count = db.update(TABLE_NAME, values, selection, selectionArgs) context?.contentResolver?.notifyChange(uri, null) return count }
companion object { const val AUTHORITY = "com.example.myapp.provider" const val TABLE_NAME = "my_table" val CONTENT_URI: Uri = Uri.parse("content://$AUTHORITY/$TABLE_NAME") }
}Використання контент-провайдера для доступу до даних з іншого застосунку:
val cursor = contentResolver.query(MyContentProvider.CONTENT_URI, null, null, null, null)Що таке Android Context
Базовий абстрактний клас, реалізація якого забезпечується системою Android. Цей клас має методи для доступу до специфічних для конкретного застосунку ресурсів та класів і слугує для виконання операцій на рівні застосунку, таких, як запуск активностей, надсилання широкомовних повідомлень, отримання намірів та інше. Від класу Context успадковуються такі великі та важливі класи, як Application, Activity та Service, тому всі його методи доступні з цих класів
Activity Context:
Створюється під час створення активіті та знищується разом з активіті. Контекст — важкий об'єкт. Коли говорять про витік пам'яті в андроїді, мають на увазі витік контексту, тобто ситуацію, коли контекст активіті зберігається після виклику Activity.onDestroy(). Не передавайте контекст активіті в інший об'єкт, якщо не відомо як довго цей об'єкт проживе.
Application Context:
Сінглтон. Application Context створюється під час створення об'єкта Application і живе, поки живий процес застосунку. З цієї причини Application Context можна безпечно інжектити в інші сінглтони в застосунку. Не рекомендується використовувати Application Context для старту активіті, тому що необхідне створення нового завдання, і для layout inflation, тому що використовується дефолтна тема.
Отримати контекст усередині коду можна одним з таких методів:
getBaseContext:
отримати посилання на базовий контекст
getApplicationContext:
отримати посилання на об'єкт застосунку
getContext:
всередині активності або сервісу отримати посилання на цей об'єкт)
this:
те саме, що й getContext
MainActivity.this:
всередині вкладеного класу або методу отримати посилання на об'єкт MainActivity
getActivity:
всередині фрагмента отримати посилання на об'єкт батьківської активності
Також є різниця:
getContext():
повертає значення Context, що допускає значення NULL.
requireContext():
повертає ненульове значення Context або видає виняток, якщо воно недоступне.
Якщо ваш код перебуває у фазі життєвого циклу, коли ви знаєте, що ваш фрагмент прикріплений до контексту, просто використовуйте requireContext(), щоб отримати Context, а також щоб статичні аналізатори були задоволені потенційними проблемами NPE.
Якщо ваш код перебуває за межами звичайного життєвого циклу фрагмента (скажімо, асинхронного зворотного виклику), вам може бути краще використовувати getContext(), самостійно перевіряючи його значення, що повертається, і продовжуючи використовувати його лише в тому випадку, якщо воно не дорівнює нулю.
Що таке WorkManager
Бібліотека Android для планування та запуску асинхронних завдань у фоновому режимі. Вона надає простий спосіб виконання завдань навіть тоді, коли застосунок уже закритий, а також автоматично керує запуском завдань, щоб мінімізувати споживання ресурсів пристрою.
class MyWorker(appContext: Context, workerParams: WorkerParameters) : Worker(appContext, workerParams) { override fun doWork(): Result { // Тут виконується асинхронне завдання, наприклад, завантаження файлу return Result.success() }}
val myWorkRequest = OneTimeWorkRequestBuilder<MyWorker>().build()WorkManager.getInstance(context) .enqueue(myWorkRequest)
// приклад для кількохval workA = OneTimeWorkRequestBuilder<MyWorkerA>().build()val workB = OneTimeWorkRequestBuilder<MyWorkerB>().build()val workC = OneTimeWorkRequestBuilder<MyWorkerC>().build()val workD = OneTimeWorkRequestBuilder<MyWorkerD>().build()WorkManager.getInstance(context) .beginWith(workA, workB, workC) .combine() .then(workD) .enqueue()enqueue():
поміщає завдання в чергу на виконання. Воно буде виконане, щойно пристрій буде готовий до цього (наприклад, за наявності інтернет-з'єднання, достатнього заряду акумулятора тощо).
beginWith():
використовується для запуску кількох завдань у заданому порядку.
combine():
використовується для запуску кількох завдань паралельно та очікування їх завершення перед виконанням наступного завдання.
WorkManager і LiveData:
дозволяє отримувати сповіщення про стан виконання завдань у WorkManager. Наприклад, ви можете створити LiveData об'єкт і використовувати його для відображення прогресу виконання завдання.
class MyViewModel(application: Application) : AndroidViewModel(application) {
private val workManager = WorkManager.getInstance(application) private val workInfoLiveData = MutableLiveData<WorkInfo>()
fun startWork() { val workRequest = OneTimeWorkRequestBuilder<MyWorker>() .build() workManager.enqueue(workRequest) workManager.getWorkInfoByIdLiveData(workRequest.id) .observeForever { workInfo -> workInfo?.let { workInfoLiveData.postValue(workInfo) } } }
fun getWorkInfoLiveData(): LiveData<WorkInfo> { return workInfoLiveData }
}
class MyActivity : AppCompatActivity() {
private lateinit var viewModel: MyViewModel
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) viewModel = ViewModelProviders.of(this).get(MyViewModel::class.java) viewModel.getWorkInfoLiveData().observe(this, { workInfo -> if (workInfo != null && workInfo.state == WorkInfo.State.RUNNING) { // Відображаємо прогрес виконання } else if (workInfo != null && workInfo.state.isFinished) { // Завдання завершено } }) viewModel.startWork() }
}WorkManager і RxJava:
можна використовувати бібліотеку RxWorkManager, яка надає зручний спосіб створення Observable потоків з WorkRequest об'єктів. Ось приклад використання RxWorkManager для запуску OneTimeWorkRequest та отримання результату у вигляді Single:
val workRequest = OneTimeWorkRequestBuilder<MyWorker>() .build()
val workObservable = RxWorkManager .getInstance(application) .getWorkInfoByIdObservable(workRequest.id) .flatMap { workInfo -> if (workInfo.state == WorkInfo.State.SUCCEEDED) { val outputData = workInfo.outputData Single.just(outputData.getString("result")) } else { Single.error(Throwable("Work failed")) } }
workObservable.subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe({ result -> // Обробка успішного результату },{ error -> // Обробка помилки } )
WorkManager.getInstance(application).enqueue(workRequest)WorkManager і корутини:
можна використовувати бібліотеку kotlinx-coroutines-play-services, яка надає зручний спосіб запуску та відстеження завдань у WorkManager за допомогою корутин.
val workRequest = OneTimeWorkRequestBuilder<MyWorker>() .build()val workInfoDeferred = WorkManager.getInstance(context) .getWorkInfoById(workRequest.id) .asDeferred()
// Запускаємо завдання у фоновому потоціGlobalScope.launch(Dispatchers.IO) { WorkManager.getInstance(context).enqueue(workRequest)}
// Очікуємо результат виконання завдання в основному потоціval workInfo = workInfoDeferred.await()if (workInfo.state == WorkInfo.State.SUCCEEDED) { val outputData = workInfo.outputData val result = outputData.getString("result") // Обробка успішного результату} else { // Обробка помилки}Програми в Android працюють на JVM?
Ні, байт-код JVM інтерпретується в байт-код ART (Android Runtime), і потім виконується. До версії 5.0 замість ART був DRT (Dalvik)
Опишіть архітектуру платформи Android
Ядро Linux:
Серце Android, у якому реалізується безпека міжпроцесної взаємодії та низькорівнева робота з пам'яттю.
Hardware Abstraction Layer (HAL):
Інтерфейси для роботи із залізом. Драйвер для USB, Bluetooth, програмний інтерфейс OpenGL. Рівень, який дає платформо-незалежність Android.
Android Runtime та нативні бібліотеки:
Те що виконує користувацький код: компілятори, збирач сміття, інтерпретатор байткоду.
Android Framework:
Java API, через яке користувацька програма взаємодіє із системою. Забезпечення життєвого циклу системних компонентів.
Android Applications:
Безпосередньо застосунки, як користувацькі, так і системні (календар, камера тощо).
Де зберігаються Bundle
являє собою клас, що використовується для передавання даних між компонентами застосунку, такими як активності (Activity), фрагменти (Fragment) та служби (Service). Bundle діє як контейнер для пари "ключ-значення", де ключі є рядками, а значення можуть бути різного типу, включно з примітивними типами, об'єктами, що реалізують інтерфейс Serializable або Parcelable.
Основні сценарії використання Bundle:
Передавання даних між активностями
Збереження стану активності
Передавання даних між фрагментами
Передавання даних у службу
Bundle зберігаються в системному класі ActivityManagerService. Для кожної запущеної активіті створюється інстанс класу ActivityRecord. Цей клас має поле icicle типу Bundle. Саме в це поле зберігається стан після виклику onSaveInstanceState().
Під час зупинки активіті Bundle надсилається в системний процес через Bindler IPC. Далі на класі ActivityManagerService викликається метод activityStopped(), який приймає об'єкт Bundle. Цей метод знаходить ActivityRecord, що відповідає зупиненій активіті, і записує отриманий Bundle у поле icicle класу ActivityRecord.
Передавання даних між активностями:
import android.content.Intentimport android.os.Bundleimport androidx.appcompat.app.AppCompatActivityimport kotlinx.android.synthetic.main.activity_main.*
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) button.setOnClickListener { val intent = Intent(this, SecondActivity::class.java) val bundle = Bundle().apply { putString("KEY_MESSAGE", "Hello from MainActivity") putInt("KEY_NUMBER", 123) } intent.putExtras(bundle) startActivity(intent) } }
}import android.os.Bundleimport androidx.appcompat.app.AppCompatActivityimport kotlinx.android.synthetic.main.activity_second.*
class SecondActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_second) val bundle = intent.extras val message = bundle?.getString("KEY_MESSAGE") val number = bundle?.getInt("KEY_NUMBER") textView.text = "$message, Number: $number" }
}Збереження стану активності:
import android.os.Bundleimport androidx.appcompat.app.AppCompatActivity
class MainActivity : AppCompatActivity() {
private var counter: Int = 0
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) // Відновлення стану savedInstanceState?.let { counter = it.getInt("KEY_COUNTER", 0) } }
override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) // Збереження стану outState.putInt("KEY_COUNTER", counter) }
}Передавання даних між фрагментами:
import android.os.Bundleimport androidx.fragment.app.Fragmentimport android.view.LayoutInflaterimport android.view.Viewimport android.view.ViewGroupimport kotlinx.android.synthetic.main.fragment_first.*
class FirstFragment : Fragment() {
override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { return inflater.inflate(R.layout.fragment_first, container, false) }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) button.setOnClickListener { val bundle = Bundle().apply { putString("KEY_MESSAGE", "Hello from FirstFragment") } val secondFragment = SecondFragment().apply { arguments = bundle } fragmentManager?.beginTransaction() ?.replace(R.id.fragment_container, secondFragment) ?.addToBackStack(null) ?.commit() } }
}import android.os.Bundleimport androidx.fragment.app.Fragmentimport android.view.LayoutInflaterimport android.view.Viewimport android.view.ViewGroupimport kotlinx.android.synthetic.main.fragment_second.*
class SecondFragment : Fragment() {
override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { return inflater.inflate(R.layout.fragment_second, container, false) }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) val message = arguments?.getString("KEY_MESSAGE") textView.text = message }
}Опишіть структуру об'єкта Intent
Являє собою механізм для запуску дій усередині застосунку або між різними застосунками. Intent використовується для різних завдань, таких як запуск активності, надсилання даних між компонентами, запуск служб, широкомовна розсилка повідомлень тощо.
Основні види Intent:
Explicit Intent (Явні наміри):
Використовуються для запуску певного компонента всередині вашого застосунку. Ви явно вказуєте клас компонента, який має бути запущений.
class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) val button = findViewById<Button>(R.id.button) button.setOnClickListener { val intent = Intent(this, SecondActivity::class.java) startActivity(intent) } }}
// SecondActivity.ktclass SecondActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_second) }}Передавання даних:
val intent = Intent(this, SecondActivity::class.java).apply { putExtra("EXTRA_MESSAGE", "Hello, SecondActivity!")}startActivity(intent)
// SecondActivity.ktclass SecondActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_second) val message = intent.getStringExtra("EXTRA_MESSAGE") findViewById<TextView>(R.id.textView).text = message }}Implicit Intent (Неявні наміри):
Використовуються для виконання дії без вказання конкретного компонента. Замість цього ви вказуєте бажану дію та систему, щоб знайти відповідний компонент для виконання цієї дії.
Запуск веб-браузера для відкриття URL:
val intent = Intent(Intent.ACTION_VIEW)intent.data = Uri.parse("https://www.example.com")startActivity(intent)Надсилання електронної пошти:
val intent = Intent(Intent.ACTION_SENDTO).apply { data = Uri.parse("mailto:") // Лише для email застосунків putExtra(Intent.EXTRA_SUBJECT, "Subject of Email")}if (intent.resolveActivity(packageManager) != null) { startActivity(intent)}Основні атрибути:
Action (Дія):
Рядок, що визначає дію, яка має бути виконана, наприклад, ACTION_VIEW, ACTION_SEND, ACTION_MAIN тощо.
Data (Дані):
URI, що вказує дані, на яких має бути виконана дія.
Category (Категорія):
Додаткові характеристики для фільтрації компонентів, які можуть обробляти наміри.
Extras (Додаткові дані):
Додаткові дані, які можуть бути передані компоненту, наприклад, putExtra("key", value).
Component (Компонент):
Ім'я класу компонента, який має бути запущений (для явних намірів).
Що таке Fragment Result API
У деяких випадках вам може знадобитися передати одноразове значення між двома фрагментами або між фрагментом та його активністю хостом. Наприклад, у вас може бути фрагмент, який зчитує QR-коди, передаючи дані назад попередньому фрагменту. Починаючи з фрагмента 1.3.0-alpha04 кожен FragmentManager реалізує FragmentResultOwner. Це означає, що a FragmentManagerможе діяти як центральне сховище результатів фрагментів. Ця зміна дозволяє компонентам взаємодіяти один з одним, встановлюючи результати фрагментів та прослуховуючи ці результати, не вимагаючи, щоб ці компоненти мали прямі посилання один на одного.
Щоб передати дані назад до фрагмента A з фрагмента B, спочатку встановіть слухач результатів на фрагмент A, який отримує результат. Виклик setFragmentResultListener() фрагмента A FragmentManager, як показано в такому прикладі:
// Надсилаємо результат в інший фрагментval someData = "123"SomeFragmentManager?.setFragmentResult( BUNDLE_KEY, bundleOf(STRING_KEY to someData))// отримуємо результат в іншому фрагментіSomeFragmentManager?.setFragmentResultListener( BUNDLE_KEY, viewLifecycleOwner) { _, bundle -> bundle.getString(STRING_KEY)?.let(viewModel::setString)Що таке Zygote в Android
В операційній системі Android процес Zygote (з англ. “зигота”) — це спеціальний процес, який запускається під час старту системи і займається попередньою ініціалізацією, компіляцією та кешуванням коду застосунків, щоб пришвидшити їх запуск.
Під час запуску нового застосунку, Zygote створює новий процес, який успадковує попередньо скомпільований код із Zygote, що також допомагає пришвидшити запуск застосунку та знизити споживання пам'яті.
Zygote також надає системні служби та ресурси, які можуть використовуватися в застосунках, такі як системні віджети, служби місцезнаходження, сертифікати безпеки тощо.
Zygote відіграє важливу роль у роботі Android, оскільки дозволяє пришвидшити запуск застосунків та забезпечує ефективніше використання ресурсів системи.
Як у Kotlin можна задати параметри в xml для Custom View
class CustomView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : LinearLayout(context, attrs, defStyleAttr) {
init { setupAttributes(attrs) }
private fun setupAttributes(attrs: AttributeSet?) { context.withStyledAttributes(attrs, R.styleable.CustomView) { getBoolean(R.styleable.CustomView_variable_name, true).let { variable -> } } }
}// в attrs.xml пишемо<declare-styleable name="CustomView"> <attr format="boolean" name="variable_name" /></declare-styleable>
// тепер у макеті фрагмента можемо додати<com._.CustomView android:id="@+id/customView" android:layout_width="0dp" android:layout_height="wrap_content" app:variable_name="true" />
// тепер у попередньому перегляді та в застосунку можна буде бачити// зміни, пов'язані з цією змінною// якщо це потрібно лише для попереднього перегляду,// використовуємо tools:variable_name="true" замість app:variable_name="true"Основні типи змінних:
<attr format="boolean" name="name"><attr format="string"name="name"><attr format="integer"name="name"><attr format="reference"name="name"><attr format="color"name="name"><attr format="reference|color"name="name"><attr format="dimension"name="name"><attr format="enum" name="gravity"> <enum name="start" value="0" /> <enum name="end" value="1" /></attr>Що таке Spannable
Інтерфейс, який розширює CharSequence і дозволяє додавати стилі та інші атрибути до частин тексту. Це потужний інструмент для роботи з текстом, який дозволяє змінювати його відображення, додавати посилання, кольори, зображення та інші ефекти.
Spannable:
Інтерфейс, що розширює CharSequence і дозволяє змінювати стилі тексту.
SpannableString:
Клас, що реалізує Spannable і використовується для застосування стилів до рядків.
SpannableStringBuilder:
Клас, який дозволяє створювати та змінювати текст з різними стилями.
Типи Spans:
StyleSpan:
Застосовує стиль (наприклад, жирний або курсив).
ForegroundColorSpan:
Змінює колір тексту.
BackgroundColorSpan:
Змінює колір фону тексту.
UnderlineSpan:
Додає підкреслення до тексту.
StrikethroughSpan:
Додає закреслений текст.
ClickableSpan:
Робить текст клікабельним і дозволяє виконувати дії під час натискання.
ImageSpan:
Вставляє зображення в текст.
URLSpan:
Створює гіперпосилання.
import android.graphics.Colorimport android.os.Bundleimport android.text.Spannableimport android.text.SpannableStringimport android.text.SpannableStringBuilderimport android.text.Spannedimport android.text.style.*import android.view.Viewimport android.widget.TextViewimport androidx.appcompat.app.AppCompatActivity
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) val textView = findViewById<TextView>(R.id.textView) val spannableString = SpannableString("Hello, World! This is a spannable string.") // Застосування жирного стилю до тексту "Hello" val boldSpan = StyleSpan(android.graphics.Typeface.BOLD) spannableString.setSpan(boldSpan, 0, 5, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) // Застосування кольору тексту до "World" val colorSpan = ForegroundColorSpan(Color.RED) spannableString.setSpan(colorSpan, 7, 12, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) // Застосування підкреслення до "This" val underlineSpan = UnderlineSpan() spannableString.setSpan(underlineSpan, 14, 18, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) // Застосування закресленого тексту до "spannable" val strikethroughSpan = StrikethroughSpan() spannableString.setSpan(strikethroughSpan, 23, 33, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) // Застосування клікабельного тексту до "string" val clickableSpan = object : ClickableSpan() { override fun onClick(widget: View) { // Дія при кліку на текст } override fun updateDrawState(ds: TextPaint) { super.updateDrawState(ds) ds.isUnderlineText = false // Прибираємо підкреслення } } spannableString.setSpan(clickableSpan, 34, 40, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) textView.text = spannableString textView.movementMethod = android.text.method.LinkMovementMethod.getInstance() }
}Використання SpannableStringBuilder:
val spannableStringBuilder = SpannableStringBuilder()spannableStringBuilder.append("Bold Text")spannableStringBuilder.setSpan(StyleSpan(android.graphics.Typeface.BOLD), 0, 9, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)spannableStringBuilder.append(" and ")spannableStringBuilder.append("Red Text")spannableStringBuilder.setSpan(ForegroundColorSpan(Color.RED), 14, 22, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)textView.text = spannableStringBuilderЧим відрізняються build type, flavor і build variant
Build Type:
використовується, щоб задати налаштування збірки, наприклад чи буде використовуватися proguard і яким сертифікатом підписується apk. Найчастіше використовувані типи збірки: debug і release. Тип збірки задається параметром buildTypes у gradle-файлі.
buildTypes { debug { applicationIdSuffix ".debug" debuggable true minifyEnabled false } release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' }}Flavor:
використовується для визначення збірок застосунку з різною функціональністю. Це можуть бути платні фічі, різні таргет API, різні ресурси. Задається параметром productFlavors у gradle-файлі.
productFlavors { free { dimension "version" applicationId "ru.itsobes.flavors.free" resValue "string", "flavored_app_name", "Free It Sobes App" } paid { dimension "version" applicationId "ru.itsobes.flavors.paid" resValue "string", "flavored_app_name", "Paid It Sobes App" }}Build Variant:
це комбінація build type і flavor. Для описаних вище build type і flavor створюються чотири build variant: freeDebug, freeRelease, paidDebug, paidRelease.
Що таке SharedPreferences
SharedPreferences:
зберігає дані у вигляді пар ключів і значень у папки застосунку. У ньому є Editor для редагування, у якому commit(), що повертає результати збереження, і apply(), що не повертає результати.
У SharedPreferences є OnSharedPreferenceChangeListener, який дозволяє відстежувати зміни за ключем
EncryptedSharedPreferences:
це зашифрована реалізація sharedPreferences
DataStore:
це заміна SharedPreferences, яка усуває більшість цих недоліків. DataStore включає в себе повністю асинхронний API, що використовує Kotlin Coroutines і Flow.
Що таке Android ViewBinding
Механізм в Android, який генерує прив'язки до XML-макетів під час компіляції. Це дозволяє вам безпечно та ефективно взаємодіяти з поданнями в коді, усуваючи необхідність використання методу findViewById і знижуючи ймовірність помилок типу NullPointerException.
Основні переваги ViewBinding:
Безпека типів:
Код згенерований ViewBinding гарантує, що ви звертаєтеся лише до наявних подань.
Уникнення NullPointerException:
ViewBinding генерує класи, які безпечні для null, що зменшує ймовірність отримання NullPointerException.
Спрощення коду:
Усуває необхідність явного виклику findViewById, що робить код чистішим і простішим.
android { viewBinding { enabled = true }}Використання ViewBinding в Activity:
<?xml version="1.0" encoding="utf-8"?><LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <TextView android:id="@+id/textView" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Hello, ViewBinding!" /> <Button android:id="@+id/button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Click me" /></LinearLayout>import android.os.Bundleimport androidx.appcompat.app.AppCompatActivityimport com.example.viewbindingexample.databinding.ActivityMainBinding
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ActivityMainBinding.inflate(layoutInflater) setContentView(binding.root) binding.textView.text = "Updated with ViewBinding!" binding.button.setOnClickListener { binding.textView.text = "Button Clicked!" } }
}Використання ViewBinding у фрагменті:
<?xml version="1.0" encoding="utf-8"?><FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <TextView android:id="@+id/textView" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Hello, ViewBinding in Fragment!" /></FrameLayout>import android.os.Bundleimport android.view.LayoutInflaterimport android.view.Viewimport android.view.ViewGroupimport androidx.fragment.app.Fragmentimport com.example.viewbindingexample.databinding.FragmentExampleBinding
class ExampleFragment : Fragment() {
private var _binding: FragmentExampleBinding? = null private val binding get() = _binding!!
override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { _binding = FragmentExampleBinding.inflate(inflater, container, false) return binding.root }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) binding.textView.text = "Updated with ViewBinding in Fragment!" }
override fun onDestroyView() { super.onDestroyView() _binding = null }
}Увімкнення ViewBinding:
У build.gradle вмикається підтримка ViewBinding, додаючи блок viewBinding { enabled = true }.
Створення екземпляра класу прив'язки:
У MainActivity та ExampleFragment створюється екземпляр класу прив'язки, згенерованого на основі імені XML-макета (наприклад, ActivityMainBinding для activity_main.xml).
Ініціалізація прив'язки:
У onCreate для активності та onCreateView для фрагмента ініціалізується об'єкт прив'язки, викликаючи метод inflate.
Доступ до подань:
Замість використання findViewById, елементи інтерфейсу доступні через об'єкт прив'язки, наприклад, binding.textView і binding.button.
Керування життєвим циклом у фрагментах:
У onDestroyView фрагмента обнуляється об'єкт прив'язки _binding для запобігання витокам пам'яті.
Як працює RecyclerView
Це інструмент для відображення списків великих даних в Android. Він покращує продуктивність та оптимізує використання пам'яті, повторно використовуючи (рециклюючи) види елементів списку, коли вони виходять за межі екрана.
Як працює RecyclerView:
Повторне використання видів:
Коли елемент списку виходить за межі екрана, його подання поміщається в пул для повторного використання. Це покращує продуктивність і знижує навантаження на пам'ять.
Менеджер компонування (LayoutManager):
Визначає, як елементи будуть розміщуватися всередині RecyclerView. У прикладі використовується LinearLayoutManager, але також доступні GridLayoutManager та StaggeredGridLayoutManager.
Адаптер:
Адаптер забезпечує зв'язок даних і подань. Він створює нові види в міру необхідності та заповнює їх даними.
Основні компоненти RecyclerView:
Adapter:
надає дані для елементів списку та створює нові види елементів (ViewHolder).
ViewHolder:
утримує подання елементів списку та забезпечує їх повторне використання.
LayoutManager:
відповідає за вимірювання та позиціювання елементів усередині RecyclerView.
Основні методи RecyclerView.Adapter
onCreateViewHolder:
Створює нові види елементів списку (ViewHolder).
onBindViewHolder:
Заповнює вид даними з набору даних.
getItemCount:
Повертає загальну кількість елементів у наборі даних.
dependencies { implementation 'androidx.recyclerview:recyclerview:1.2.1'}<?xml version="1.0" encoding="utf-8"?><LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" android:padding="16dp"> <TextView android:id="@+id/textView" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Item" android:textSize="18sp" /></LinearLayout><?xml version="1.0" encoding="utf-8"?><RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <androidx.recyclerview.widget.RecyclerView android:id="@+id/recyclerView" android:layout_width="match_parent" android:layout_height="match_parent" /></RelativeLayout>import android.view.LayoutInflaterimport android.view.Viewimport android.view.ViewGroupimport android.widget.TextViewimport androidx.recyclerview.widget.RecyclerView
class MyAdapter(private val itemList: List<String>) : RecyclerView.Adapter<MyAdapter.MyViewHolder>() {
class MyViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { val textView: TextView = itemView.findViewById(R.id.textView) }
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder { val itemView = LayoutInflater.from(parent.context) .inflate(R.layout.item_view, parent, false) return MyViewHolder(itemView) }
override fun onBindViewHolder(holder: MyViewHolder, position: Int) { val currentItem = itemList[position] holder.textView.text = currentItem }
override fun getItemCount() = itemList.size
}import android.os.Bundleimport androidx.appcompat.app.AppCompatActivityimport androidx.recyclerview.widget.LinearLayoutManagerimport androidx.recyclerview.widget.RecyclerView
class MainActivity : AppCompatActivity() {
private lateinit var recyclerView: RecyclerView private lateinit var adapter: MyAdapter private lateinit var itemList: List<String>
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) recyclerView = findViewById(R.id.recyclerView) // Ініціалізація даних itemList = listOf("Item 1", "Item 2", "Item 3", "Item 4", "Item 5") // Налаштування адаптера та layout manager adapter = MyAdapter(itemList) recyclerView.adapter = adapter recyclerView.layoutManager = LinearLayoutManager(this) }
}Яка різниця між Custom View і Custom Drawing
Custom View:
це користувацький віджет, який створюється шляхом розширення наявного класу View або його підкласу. Під час створення користувацького виду, ми можемо визначити нові атрибути, методи та обробники подій, а також змінювати відображення виду. Custom View може використовуватися повторно в різних макетах і дає можливість створювати власні компоненти інтерфейсу.
Custom Drawing:
це процес малювання на полотні за допомогою методів класу Canvas. Цей підхід дозволяє малювати на екрані різні елементи, наприклад, лінії, прямокутники, кола, текст, зображення тощо. Custom Drawing може використовуватися, коли необхідно створити динамічні елементи інтерфейсу, які не можуть бути досягнуті за допомогою стандартних елементів користувацького інтерфейсу.
Що таке Deep Link і App Link
Deep Link:
це посилання на конкретний контент усередині мобільного застосунку. Це дозволяє користувачам переходити напряму до потрібного екрана застосунку, оминаючи додаткові кроки, такі як пошук у застосунку. Застосунок, який підтримує Deep Link, може обробляти певні URL-адреси та запускати відповідні екрани застосунку, якщо вони доступні на пристрої користувача.
App Link:
це просунутіша версія Deep Link, яка дозволяє застосунку зареєструватися для обробки певних URL-адрес як своїх власних. Це означає, що користувач може перейти до контенту застосунку через вебсайт, і система автоматично відкриє відповідний застосунок, якщо він встановлений на пристрої користувача. Застосунок також може надавати інформацію про себе для вебсайту, таку як іконки та заголовки, що робить процес інтуїтивнішим і привабливішим для користувачів.
Що таке Navigation Architecture Component, для чого потрібен і як працює
NavGraph:
список фрагментів, який ми будемо створювати та наповнювати. NavController зможе показувати фрагменти лише з цього списку
NavHostFragment:
контейнер. Усередині нього NavController буде відображати фрагменти.
NavController:
об'єкт, що керує навігацією застосунку в NavHost. Він координує зміну контенту пунктів призначення в NavHost у процесі переміщення користувача застосунком.
<?xml version="1.0" encoding="utf-8"?><navigation xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/nav_main" app:startDestination="@id/mainFragment">
<fragment android:id="@+id/mainFragment" android:name="com._.Main" android:label="mainFragment" tools:layout="@layout/fragment_main"> <action android:id="@+id/action_mainFragment_to_settingsFragment" app:destination="@id/settingsFragment" app:popUpTo="@id/settingsFragment" app:popUpToInclusive="true"/> <action android:id="@+id/action_mainFragment_to_settingsBottomSheetFragment" app:destination="@id/settingsBottomSheetFragment" /> </fragment>
<fragment android:id="@+id/settingsFragment" android:name="com._.settingsFragment" android:label="settingsFragment" tools:layout="@layout/fragment_settings"> </fragment>
<dialog android:id="@+id/settingsBottomSheetFragment" android:name="com._.SettingsBottomSheetFragment" android:label="settingsBottomSheetFragment" tools:layout="@layout/bottom_sheet_settings" />
</navigation>Що таке Content Uri
URI (Uniform Resource Identifier) для доступу до даних усередині застосунків Android. Він використовується для отримання доступу до різних типів даних, таких як зображення, відео, звук, контакти, календарі тощо.
Content Uri зазвичай мають формат “content://authority/path/id”, де “authority” — це ідентифікатор постачальника контенту, “path” — це шлях до конкретного ресурсу, а “id” — це унікальний ідентифікатор ресурсу.
Наприклад, для отримання доступу до фотографій, що зберігаються в галереї пристрою, можна використовувати такий Content Uri: “content://media/external/images/media”. Для отримання доступу до певного зображення можна додати ідентифікатор зображення в кінці Uri: “content://media/external/images/media/123”.
Content Uri використовується для обміну даними між різними застосунками та компонентами Android, а також для виконання операцій читання та запису даних, таких як додавання, оновлення та видалення записів.
Як зробити анімацію переходів для навігаційного компонента
У папці res створюється папка anim з ресурсами анімації
У файлі res/navigation/nav_graph.xml на вкладці дизайну виділяємо стрілку переходу між екранами і в панелі атрибутів праворуч відкриваємо розділ Animations.
У переходу є чотири властивості, які можна анімувати:
enterAnim: вхід у пункт призначення
exitAnim: вихід із пункту призначення
popEnterAnim: вхід у пункт призначення за допомогою pop action
popExitAnim: вихід із пункту призначення за допомогою pop action
View можна анімувати за допомогою Transition Framework
Які переваги та недоліки у png і svg і коли той чи інший краще використовувати
PNG- растровий формат, при сильному збільшенні будуть видні пікселі, SVG- векторний і не залежить від роздільної здатності екрана
Назвати всі випадки, коли викличеться onSaveInstanceState і onRestoreInstanceState
onSaveInstanceState():
викликається перед знищенням активності, наприклад, під час повороту екрана, згортання застосунку або зміни конфігурації пристрою. Цей метод використовується для збереження стану активності, який може загубитися під час знищення та відновлення активності. Також викликається перед тим, як активність буде знищена системою у разі нестачі пам'яті. У цьому випадку метод використовується для збереження стану активності, який може бути відновлений за необхідності.
onRestoreInstanceState():
викликається після того, як активність була відновлена зі стану, збереженого в onSaveInstanceState(). Цей метод використовується для відновлення стану активності, збереженого в onSaveInstanceState(). Може бути викликаний не лише після повороту екрана або зміни конфігурації пристрою, але також в інших випадках, коли активність була знищена та перестворена системою, наприклад, під час відновлення після аварійної зупинки застосунку.
Що означають dpi, dp, sp, dip, px, mm, in і pt
dpi:
щільність екрана, кількість пікселів на дюйм.
dp, sp:
абстрактні незалежні від щільності пікселі для розмірів елементів і тексту. Раніше ми писали про співвідношення між dp і px.
dip:
не згадуваний у документації, але зрозумілий компілятору синонім для dp.
px:
розмір, виражений у фізичній кількості пікселів екрана.
mm, in:
фізичний розмір на екрані, у міліметрах і дюймах (inches) відповідно.
pt:
типографський пункт (point), також фізичний розмір, що дорівнює 1/72 дюйма.
Які планувальники завдань існують в Android
Handler:
дозволяє виконувати відкладені в часі завдання. Не є планувальником завдань у чистому вигляді, оскільки працює на рівні процесу застосунку, а не операційної системи. Якщо застосунок зупинений, код, зашедульований через Handler не буде виконаний. Handler дозволяє надсилати повідомлення в інші потоки із затримкою або без, а також обробляти отримані повідомлення. Handler завжди пов'язаний з Looper, який своєю чергою пов'язаний з якимось потоком. Під час створення Handler у конструктор можна передати об'єкт Looper. Якщо використовується дефолтний конструктор, то Handler створюється на поточному потоці. Якщо з потоком не пов'язаний Looper, то під час створення Handler кидається RuntimeException.
AlarmManager:
запускає заплановані операції навіть якщо застосунок зупинений. Як операції використовуються PendingIntents. AlarmManager доступний з API v1, але за замовчуванням не працює в Doze Mode. Для роботи в Doze Mode використовується метод setAndAllowWhileIdle(). Цей метод доступний з API v23.
JobScheduler:
дозволяє працювати в Doze Mode і доступний з API v21.
Система групує завдання, заплановані через JobScheduler. Коли з'являється вікно в Doze Mode, виконується одразу кілька завдань. Цей підхід береже батарейку пристрою.
WorkManager:
бібліотека з Android Jetpack. WorkManager працює починаючи з API v14. Google рекомендує використовувати WorkManager замість рішень, розроблених раніше. Під капотом WorkManager використовує JobScheduler на пристроях з API v23+. На версіях API 14–22 використовується GCMNetworkManager або AlarmManager.
Які основні Layout в Android і яка у них швидкодія
LinearLayout:
це найпростіший тип Layout, який орієнтується на горизонтальне або вертикальне розташування дочірніх елементів. Швидко працює на всіх версіях Android.
RelativeLayout:
це тип Layout, який дозволяє розміщувати дочірні елементи відносно один одного або відносно батьківського елемента. Складні ієрархії RelativeLayout можуть призвести до погіршення продуктивності, тому краще уникати використання цього типу Layout, якщо це необхідно.
GridLayout:
це тип Layout, який дозволяє створювати таблиці та розташовувати елементи на перетині рядків і стовпців. GridLayout може бути дуже ефективним, якщо використовується в правильному контексті.
ConstraintLayout:
це тип Layout, який дозволяє створювати складні ієрархії View з використанням обмежень. ConstraintLayout може бути ефективним, якщо він налаштований правильно, і може навіть перевершувати продуктивність RelativeLayout.
FrameLayout:
це простий тип Layout, який може містити лише один дочірній елемент. Він швидко працює, але не підходить для розміщення кількох елементів.
TableLayout:
призначений для розміщення елементів у вигляді таблиці. Він може бути зручним для екранів з великою кількістю даних або для відображення списку.
GridLayout:
дозволяє розміщувати елементи у вигляді сітки. Він особливо корисний для екранів з великою кількістю даних, таких як екрани списку або таблиць.
ScrollView:
використовується для створення прокручуваного контенту. Цей тип layout-а може бути корисним для екранів з великою кількістю тексту або зображень.
Чи може AndroidManifest містити кілька main activity
Під main activity розуміється активіті, у якої intent-filter містить ACTION_MAIN і CATEGORY_LAUNCHER.
В AndroidManifest можна додати кілька main activity.
Для кожної активіті з'явиться іконка в меню застосунків.
За замовчуванням буде використовуватися іконка та ім'я застосунку, задані в атрибутах icon і label елемента application у маніфесті. Ці атрибути можна перевизначити в елементі activity.
Цей механізм може бути корисним для бібліотек, які використовуються в debug/testing білдах. Так бібліотека leakcanary додає іконку для запуску своєї активіті з меню застосунків.
Що таке мульти декларації
Згенеровані для класів даних компонентні функції дозволяють використовувати їх у мульти-деклараціях.
val jane = User("Jane", 35)val (name, age) = janeprintln("$name, $age years of age")// виводить "Jane, 35 years of age"Які в Android бувають типи сервісів
Foreground Service:
сервіс, який виконується на першому плані та надає користувачеві помітну функціональність. Фоновий сервіс може бути перетворений на foreground сервіс за допомогою виклику методу startForeground().
Background Service:
сервіс, який виконується у фоні та не надає користувачеві помітної функціональності. На відміну від foreground сервісу, background сервіс може бути знищений системою в будь-який момент, коли системі знадобиться звільнити пам'ять.
Bound Service:
сервіс, який забезпечує зв'язок між застосунком та іншою частиною системи, такою як інший застосунок або служба. Bound сервіс може взаємодіяти з клієнтом, надаючи йому об'єкт IBinder через метод onBind(). Клієнти можуть використовувати цей об'єкт для виклику методів, які визначені в сервісі. Коли всі клієнти від'єднуються від сервісу, він знищується.
IntentService:
це підклас Service, який використовується для виконання завдань у фоновому режимі.
IntentService автоматично створює новий потік для обробки кожного інтента, надісланого в службу. Коли інтент оброблений, служба автоматично завершується. Якщо під час виконання служби приходить новий інтент, то він додається в чергу та обробляється по черзі.
IntentService також забезпечує синхронізацію, блокуючи нові інтенти доти, доки не буде завершена обробка попередніх.
Основне використання IntentService — це виконання тривалих операцій, таких як завантаження даних з мережі, обробка зображень, збереження даних у базу даних тощо. Усі ці операції можна виконувати у фоновому режимі, щоб не блокувати основний потік користувацького інтерфейсу.
JobIntentService:
це застарілий клас в Android, який призначений для виконання фонових завдань, які можна виконати у фоновому режимі за допомогою служби. Він був представлений в Android Support Library для забезпечення зворотної сумісності. JobIntentService працює подібно до IntentService, але має додаткову функціональність для обробки завдань, які необхідно виконати у фоновому режимі, і для обробки цих завдань у фоновому режимі навіть після того, як застосунок був зупинений. На відміну від звичайного IntentService, JobIntentService автоматично керує життєвим циклом служби та гарантує, що служба не буде зупинена, доки всі завдання не будуть виконані.
Починаючи з Android 12, рекомендується використовувати WorkManager для запуску фонових завдань замість JobIntentService.
Приклад використання сервісу:
Створіть клас для сервісу, що успадковується від Service. Наприклад, створіть файл MyService.kt з таким вмістом:
class MyService : Service() {
private val binder = MyServiceBinder()
override fun onCreate() { // Виконується при створенні сервісу }
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { // Виконується при запуску сервісу return START_STICKY }
override fun onDestroy() { // Виконується при завершенні сервісу }
override fun onBind(intent: Intent?): IBinder? { // метод, який має бути перевизначений у класі Service, // щоб надати клієнту (зазвичай активності) можливість зв'язатися з сервісом через інтерфейс IBinder. // Цей метод повинен повернути об'єкт IBinder, // який клієнт може використовувати для взаємодії з сервісом. // Якщо сервіс не надає можливості зв'язку з клієнтом, // цей метод може просто повертати null. return binder }
inner class MyServiceBinder : Binder() { fun getService(): MyService { return this@MyService } }}Додайте сервіс у файл маніфесту застосунку, вказавши ім'я класу сервісу та дозволи, якщо це необхідно. Наприклад, додайте такий код у файл AndroidManifest.xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.myapplication">
<application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:theme="@style/AppTheme">
<service android:name=".MyService" android:enabled="true" android:exported="false" />
<activity android:name=".MainActivity" android:label="@string/app_name" android:theme="@style/AppTheme.NoActionBar"> <intent-filter> <action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application>
</manifest>Запуск і зупинка
val serviceIntent = Intent(this, MyService::class.java)startService(serviceIntent)
val serviceIntent = Intent(this, MyService::class.java)stopService(serviceIntent)Які класи слугують для доступу до сенсорів
SensorManager:
точка входу для роботи з сенсорами та listener-ами їх подій. Системний сервіс, який отримується за іменем Context.SENSOR_SERVICE;
Sensor:
представляє окремо взятий сенсор. Дає різну метаінформацію (енергоспоживання, точність, виробника тощо);
SensorEventListener:
інтерфейс для реалізації обробників подій, що приходять із сенсорів. У ньому реалізується логіка обробки вхідних даних;
SensorEvent:
окрема подія із сенсора: дані та точність їх вимірювання.
Які основні анотації та методи в Room
@Entity(tableName = “users”):
data class
@Entity(primaryKeys = [“firstName”, “lastName”]):
використовується composite primary key
@Entity(ignoredColumns = [“picture”]):
ігнорувати
@PrimaryKey(autoGenerate = true):
первинний ключ у data class
@ColumnInfo(name = “first_name”):
назва колонки, якщо вона повинна відрізнятися від назви змінної
@Ignore:
не використовувати це поле або цей конструктор
@Dao:
абстрактний клас або інтерфейс для роботи з таблицею
@Transaction:
виконувати як одну транзакцію, атомарно
@Database(entities = [User::class], version = 1):
абстрактний клас, який екстендить RoomDatabase
@Database(entities = [User::class], version = 1):abstract class AppDatabase : RoomDatabase() { abstract fun userDao(): UserDao}val db = Room.databaseBuilder(applicationContext, AppDatabase::class.java, "database-name").build()@TypeConverters(DateConverter.class):
class Converters { @TypeConverter fun fromTimestamp(value: Long?): Date? { return value?.let { Date(it) } }@TypeConverter fun dateToTimestamp(date: Date?): Long? { return date?.time?.toLong() }}@Database(entities = [User::class], version = 1)@TypeConverters(Converters::class)abstract class AppDatabase : RoomDatabase() { abstract fun userDao(): UserDao}val db = Room.databaseBuilder(...) .addTypeConverter(exampleConverterInstance) .build()@Query(“SELECT * FROM Users”):
sql запит
@Insert(onConflict = OnConflictStrategy.REPLACE):
вставка в таблицю
@Delete:
видалення з таблиці
@Update:
оновлення
@DatabaseView:
інкапсулює запит у клас
@Fts3 або @Fts4:
у Room 2.1.0 використовувати повнотекстовий пошук FTS
@AutoValue:
https://github.com/google/auto/blob/master/value/userguide/index.md
@MapInfo(keyColumn = “userName”, valueColumn = “bookName”):
повертає зіставлення між таблицями
@Embedded(prefix = “address”):
взяти поля з позначеного класу та вважати полями таблиці, prefix потрібен якщо збігаються імена
@Relation(parentColumn = “userId”, entityColumn = “userOwnerId”):
буде шукати де parentColumn дорівнює entityColumn, також може мати властивість associateBy = Junction(PlaylistSongCrossRef::class) де
@Entity(primaryKeys = ["playlistId", "songId"])data class PlaylistSongCrossRef( val playlistId: Long, val songId: Long)Залежність один до одного:
@Entitydata class User( @PrimaryKey val userId: Long, val name: String, val age: Int)@Entitydata class Library( @PrimaryKey val libraryId: Long, val userOwnerId: Long)data class UserAndLibrary( @Embedded val user: User, @Relation(parentColumn = "userId", entityColumn = "userOwnerId") val library: Library)@Transaction@Query("SELECT * FROM User")fun getUsersAndLibraries(): List<UserAndLibrary>Залежність один до багатьох:
@Entitydata class User( @PrimaryKey val userId: Long, val name: String, val age: Int)@Entitydata class Playlist( @PrimaryKey val playlistId: Long, val userCreatorId: Long, val playlistName: String)data class UserWithPlaylists( @Embedded val user: User, @Relation(parentColumn = "userId", entityColumn = "userCreatorId") val playlists: List<Playlist>)@Transaction@Query("SELECT * FROM User")fun getUsersWithPlaylists(): List<UserWithPlaylists>Залежність багато до багатьох:
@Entitydata class Playlist( @PrimaryKey val playlistId: Long, val playlistName: String)@Entitydata class Song( @PrimaryKey val songId: Long, val songName: String, val artist: String)@Entity(primaryKeys = ["playlistId", "songId"])data class PlaylistSongCrossRef( val playlistId: Long, val songId: Long)data class PlaylistWithSongs( @Embedded val playlist: Playlist, @Relation(parentColumn = "playlistId", entityColumn = "songId", associateBy = Junction(PlaylistSongCrossRef::class)) val songs: List<Song>)data class SongWithPlaylists( @Embedded val song: Song, @Relation(parentColumn = "songId", entityColumn = "playlistId", associateBy = Junction(PlaylistSongCrossRef::class)) val playlists: List<Playlist>)@Transaction@Query("SELECT * FROM Playlist")fun getPlaylistsWithSongs(): List<PlaylistWithSongs>@Transaction@Query("SELECT * FROM Song")fun getSongsWithPlaylists(): List<SongWithPlaylists>За допомогою Join можна можна об'єднувати таблиці в Room способом, альтернативним Embedded і Relation, запити при цьому складніше писати за SQL синтаксисом, але працює набагато швидше, ніж автоматичне об'єднання через Embedded і Relation
Асинхронні запити:
coroutines: suspend fun loadUserById(id: Int): User
RX: public Single loadUserById(int id);
LiveData: public ListenableFuture loadUserById(int id);
Підписка:
coroutines: fun loadUserById(id: Int): Flow
RX: public Flowable loadUserById(int id);
LiveData: public LiveData loadUserById(int id);
Попереднє заповнення:
Room.databaseBuilder(appContext, AppDatabase.class, "Sample.db") .createFromAsset("database/myapp.db") .build()// абоRoom.databaseBuilder(appContext, AppDatabase.class, "Sample.db") .createFromFile(File("mypath")) .build()Міграції:
Room.databaseBuilder(appContext, AppDatabase.class, "Sample.db") .createFromAsset("database/myapp.db") .fallbackToDestructiveMigration() .build()// абоRoom.databaseBuilder(appContext, AppDatabase.class, "Sample.db") .createFromAsset("database/myapp.db") .addMigrations(MIGRATION_1_2) .build() val MIGRATION_1_2 = object : Migration(1, 2) { override fun migrate(database: SupportSQLiteDatabase) { database.execSQL("CREATE TABLE `Fruit` (`id` INTEGER, `name` TEXT, " + "PRIMARY KEY(`id`))") } }// або@Database(version = 2, entities = [User::class], autoMigrations = [AutoMigration (from = 1, to = 2)])