ViewModel и onSaveInstanceState служат для сохранения данных при изменении конфигурации активности или фрагмента (например, при повороте экрана). Однако они решают эту задачу по-разному и имеют разные области применения. ViewModel используется для хранения и управления данными, связанных с UI, таким образом, чтобы они сохранялись при изменении конфигурации (например, при повороте экрана). Когда система уничтожает и пересоздаёт
Activity или Fragment, ViewModel остаётся в памяти до полного уничтожения владельца (например, выхода из Activity). class MyViewModel : ViewModel() {
var counter: Int = 0 // Переменная, которая сохраняется при повороте экрана
}Использование в
Activityclass MainActivity : AppCompatActivity() {
private lateinit var viewModel: MyViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
viewModel = ViewModelProvider(this).get(MyViewModel::class.java)
// Теперь viewModel.counter сохранится при повороте экрана
}
}до полного уничтожения
Activity или Fragment. списки, модели, API-данные
улучшая архитектуру.
например, при закрытии приложения
Метод
onSaveInstanceState() используется для сохранения данных в Bundle, который система автоматически передаёт при пересоздании Activity или Fragment. Когда
Activity уничтожается (например, при повороте экрана), система вызывает onSaveInstanceState(), в котором можно сохранить небольшие данные (строки, числа и т. д.). После пересоздания Activity эти данные можно восстановить из savedInstanceState. class MainActivity : AppCompatActivity() {
private var counter: Int = 0 // Значение, которое мы хотим сохранить
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// Восстанавливаем данные, если они есть
counter = savedInstanceState?.getInt("counter_key") ?: 0
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putInt("counter_key", counter) // Сохраняем значение перед уничтожением Activity
}
}EditText). BundleСтавь 👍 и забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
👍6
2. Кешировать результаты парсинга.
3. Разделять обработку сложных блоков (например, таблиц) от простых (текста).
4. Использовать библиотеки, оптимизированные для Android, такие как Markwon.
Ставь 👍 если знал ответ, 🔥 если нет
Забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥1
Если профайлер показывает, что рендеринг какого-либо фрейма занял 120 миллисекунд, это означает, что этот фрейм выполнялся слишком долго, что приводит к фризам и лагам в пользовательском интерфейсе.
В Android интерфейс обновляется 60 раз в секунду (частота 60 FPS). Это значит, что каждый кадр (фрейм) должен рендериться не дольше 16,67 мс (1000 мс / 60 FPS).
Если рендеринг кадра занимает 120 мс, то за это время устройство должно было бы нарисовать 7 кадров (120 / 16,67 ≈ 7). Однако оно успело обработать только один, что приводит к заметному подтормаживанию.
Тяжёлые вычисления в основном потоке (UI Thread) – например, сложные математические операции, работа с JSON, парсинг файлов.
Долгие операции с рендерингом – сложные векторные изображения, перегруженные
Canvas.draw() или анимации. Синхронные вызовы I/O (чтение файлов, базы данных, сети) – если, например, в
onDraw() идёт обращение к диску или базе данных. Неоптимальный layout – глубокая иерархия
ViewGroup, частые перерасчёты макетов (measure/layout). Coroutines, Executors, WorkManagerдля поиска "узких мест".
избегать сложных
onDraw(), использовать ViewStub, RecyclerView. убрать ненужные
ViewGroup, использовать ConstraintLayout. Ставь 👍 и забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
👍5
Благодаря ViewModel реализуется паттерн MVVM (Model-View-ViewModel). ViewModel отвечает за управление данными и бизнес-логикой, изолируя их от View, что упрощает тестирование и обеспечивает разделение ответственности между слоями.
Ставь 👍 если знал ответ, 🔥 если нет
Забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
👍4🔥2
Сохранение структуры Markdown в базу данных или на диск зависит от целей и требований приложения. Рассмотрим несколько вариантов.
Самый простой способ — хранить Markdown как обычный текст в базе данных или файле.
База данных (SQLite, Room):
@Entity
data class Note(
@PrimaryKey(autoGenerate = true) val id: Long = 0,
val title: String,
val content: String // Здесь хранится Markdown-текст
)
@Dao
interface NoteDao {
@Insert
suspend fun insert(note: Note)
@Query("SELECT * FROM Note WHERE id = :id")
suspend fun getNote(id: Long): Note?
}
Файл на диске
fun saveMarkdownToFile(context: Context, filename: String, content: String) {
val file = File(context.filesDir, filename)
file.writeText(content, Charsets.UTF_8)
}
fun readMarkdownFromFile(context: Context, filename: String): String {
val file = File(context.filesDir, filename)
return if (file.exists()) file.readText(Charsets.UTF_8) else ""
}Если нужно анализировать структуру Markdown (например, извлекать заголовки, ссылки, списки), можно парсить Markdown в JSON и сохранять в базу данных.
{
"title": "Android Markdown Guide",
"content": [
{
"type": "header",
"level": 1,
"text": "Введение"
},
{
"type": "paragraph",
"text": "Markdown — это простой язык разметки..."
},
{
"type": "list",
"items": [
"Простота",
"Гибкость",
"Совместимость"
]
}
]
}Сохранение в базу данных (Room + TypeConverter):
@Entity
data class MarkdownNote(
@PrimaryKey(autoGenerate = true) val id: Long = 0,
val title: String,
val jsonContent: String // Храним JSON-структуру
)
class MarkdownConverter {
@TypeConverter
fun fromJson(value: String): List<MarkdownBlock> {
return Gson().fromJson(value, object : TypeToken<List<MarkdownBlock>>() {}.type)
}
@TypeConverter
fun toJson(content: List<MarkdownBlock>): String {
return Gson().toJson(content)
}
}
Парсинг Markdown в JSON с помощью библиотеки (например, flexmark-java)
fun parseMarkdownToJson(markdown: String): String {
val document = Parser.builder().build().parse(markdown)
val blocks = mutableListOf<MarkdownBlock>()
document.children.forEach { node ->
when (node) {
is Heading -> blocks.add(MarkdownBlock("header", node.level, node.text.toString()))
is Paragraph -> blocks.add(MarkdownBlock("paragraph", text = node.text.toString()))
is BulletList -> {
val items = node.children.map { it.text.toString() }
blocks.add(MarkdownBlock("list", items = items))
}
}
}
return Gson().toJson(blocks)
}Если Markdown в основном нужен для отображения, можно сразу конвертировать его в HTML и хранить в базе/файлах.
Конвертация Markdown → HTML с помощью flexmark-java:
fun convertMarkdownToHtml(markdown: String): String {
val renderer = HtmlRenderer.builder().build()
val document = Parser.builder().build().parse(markdown)
return renderer.render(document)
}Сохранение в базу:
@Entity
data class HtmlNote(
@PrimaryKey(autoGenerate = true) val id: Long = 0,
val title: String,
val htmlContent: String
)
Ставь 👍 и забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
👍1
2. С помощью callback-ов: View предоставляет Presenter-у callback, который вызывается при обновлении данных.
3. Через LiveData (если используется MVVM): Presenter может обновить данные, которые View наблюдает.
Ставь 👍 если знал ответ, 🔥 если нет
Забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥1
В Kotlin оператор
== используется для структурного сравнения объектов. Оператор
== вызывает метод equals(), чтобы проверить содержимое объектов, а не их ссылки. data class User(val name: String)
fun main() {
val user1 = User("Alice")
val user2 = User("Alice")
println(user1 == user2) // true (структурное сравнение)
println(user1 === user2) // false (сравнение ссылок)
}
== проверяет, равны ли данные объектов (equals()). === проверяет, ссылаются ли объекты на один и тот же участок памяти. val str1 = "Hello"
val str2 = "Hello"
println(str1 == str2) // true (содержимое одинаковое)
println(str1 === str2) // true (Kotlin кеширует строки)
val obj1 = String("Hello".toCharArray())
val obj2 = String("Hello".toCharArray())
println(obj1 == obj2) // true (содержимое одинаковое)
println(obj1 === obj2) // false (разные объекты в памяти)
Если
equals() не переопределён, то сравниваются ссылки (как ===). class Person(val name: String)
fun main() {
val p1 = Person("Bob")
val p2 = Person("Bob")
println(p1 == p2) // false (equals() не переопределён, сравниваются ссылки)
}
Чтобы
== работал по содержимому, нужно переопределить equals()class Person(val name: String) {
override fun equals(other: Any?): Boolean {
return other is Person && other.name == this.name
}
}
fun main() {
val p1 = Person("Bob")
val p2 = Person("Bob")
println(p1 == p2) // true (equals() сравнивает содержимое)
}Kotlin предотвращает
NullPointerException при сравнении с null:val a: String? = null
val b: String? = "Hello"
println(a == b) // false (без NPE)
println(a == null) // true
Когда
a == b, на самом деле выполняется:a?.equals(b) ?: (b === null)
Ставь 👍 и забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
👍6
1. Component: Интерфейс, определяющий, какие зависимости могут быть предоставлены.
2. Module: Класс, предоставляющий зависимости через аннотированные методы
3. Scope: Аннотации для управления временем жизни объектов (например,
4. Inject: Аннотация для указания точек внедрения зависимостей.
Ставь 👍 если знал ответ, 🔥 если нет
Забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥5👍1
Термин "Extensions" (расширения) используется для обозначения функциональности, которая позволяет добавлять новые возможности к существующим классам без изменения их исходного кода. Этот концепт особенно популярен к примеру в Swift и Kotlin, где расширения используются для улучшения читаемости кода, упрощения его структуры и добавления удобных методов для работы с уже существующими типами данных.
Расширения позволяют добавить новую функциональность к классам, даже если исходный код этих классов недоступен для изменений (например, классы из стандартной библиотеки или сторонних библиотек). Это делается путём объявления функций или свойств, которые могут быть вызваны на экземплярах этих классов, как если бы они были частью оригинальных классов.
// Расширение класса String для проверки, пуста ли строка или состоит из пробелов
fun String.isNullOrBlank(): Boolean {
return this == null || this.trim().isEmpty()
}
// Использование расширения
val myString = " "
println(myString.isNullOrBlank()) // Выведет: true
Использует очень похожий подход, позволяя добавлять новые методы и вычисляемые свойства к существующим типам. Расширения могут даже добавлять новые протоколы к типам, что делает их чрезвычайно мощным инструментом в разработке под iOS и macOS.
// Расширение стандартного типа String для добавления метода инвертирования строки
extension String {
func reversedString() -> String {
return String(self.reversed())
}
}
let myString = "Hello"
print(myString.reversedString()) // Выведет: "olleH"
Расширения позволяют держать методы, связанные с определёнными типами данных, ближе к использованию этих типов.
Расширения предоставляют альтернативный способ добавления функциональности к классам без использования наследования, что помогает избежать избыточной иерархии и увеличения сложности системы.
Методы, добавленные через расширения, могут быть легко переиспользованы в различных частях приложения.
Ставь 👍 и забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
👍3💊1
- На конструкторе класса, чтобы указать, что он должен быть создан Dagger-ом.
- На полях или методах, чтобы внедрить зависимости.
2. Добавьте класс или его фабрику в Module, если требуется кастомная логика создания.
Ставь 👍 если знал ответ, 🔥 если нет
Забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥2👍1
В Android есть несколько способов навигации между экранами. Давай разберём основные.
Каждый экран – это отдельная
Activity, переход осуществляется с помощью Intent. val intent = Intent(this, SecondActivity::class.java)
startActivity(intent)
Один
Activity содержит несколько Fragment, навигация через FragmentManager. supportFragmentManager.beginTransaction()
.replace(R.id.container, SecondFragment())
.addToBackStack(null)
.commit()
Google рекомендует
Navigation Component для удобной работы с Fragment. findNavController().navigate(R.id.action_firstFragment_to_secondFragment)
Используется
BottomNavigationView или TabLayout для переключения между экранами. bottomNavigationView.setOnNavigationItemSelectedListener { item ->
when (item.itemId) {
R.id.home -> replaceFragment(HomeFragment())
R.id.profile -> replaceFragment(ProfileFragment())
}
true
}Боковое меню (
DrawerLayout) с пунктами навигации. drawerLayout.openDrawer(GravityCompat.START)
Навигация через ссылки (например,
myapp://profile/123). <intent-filter>
<action android:name="android.intent.action.VIEW"/>
<data android:scheme="myapp" android:host="profile"/>
</intent-filter>
Используется
NavHost и NavController. NavHost(navController, startDestination = "home") {
composable("home") { HomeScreen(navController) }
composable("profile") { ProfileScreen(navController) }
}Ставь 👍 и забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
👍11
2. Используйте ViewModelProvider.Factory, чтобы Dagger мог создать экземпляр ViewModel с учетом всех зависимостей.
3. Инжектируйте Factory через Dagger и используйте её для получения ViewModel.
Ставь 👍 если знал ответ, 🔥 если нет
Забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥3
В MVI (Model-View-Intent) есть проблема: события, которые не нужно хранить в
State, могут повторно отобразиться при пересоздании экрана (например, при повороте экрана или навигации назад). Показ
Toast / Snackbar Навигация (
openScreen()) Ошибки, которые нужно показать один раз
Это
LiveData, которая отправляет событие только один раз и не пересылает его новым подписчикам. class SingleLiveEvent<T> : MutableLiveData<T>() {
private val pending = AtomicBoolean(false)
override fun observe(owner: LifecycleOwner, observer: Observer<in T>) {
super.observe(owner) { value ->
if (pending.compareAndSet(true, false)) {
observer.onChanged(value)
}
}
}
override fun setValue(value: T?) {
pending.set(true)
super.setValue(value)
}
}ViewModel с
SingleLiveEvent class MyViewModel : ViewModel() {
val eventShowToast = SingleLiveEvent<String>()
fun onButtonClicked() {
eventShowToast.value = "Привет, это Toast!"
}
}Activity/Fragment подписывается
viewModel.eventShowToast.observe(viewLifecycleOwner) { message ->
Toast.makeText(requireContext(), message, Toast.LENGTH_SHORT).show()
}В
StateFlow все события хранятся в State (что не подходит для одноразовых событий). Вместо этого используем
SharedFlow с replay = 0, чтобы событие не повторялось. ViewModel с
SharedFlow class MyViewModel : ViewModel() {
private val _events = MutableSharedFlow<UiEvent>()
val events = _events.asSharedFlow()
fun onButtonClicked() {
viewModelScope.launch {
_events.emit(UiEvent.ShowToast("Привет, это Toast!"))
}
}
}
sealed class UiEvent {
data class ShowToast(val message: String) : UiEvent()
object NavigateToNextScreen : UiEvent()
}Подписка в UI:
lifecycleScope.launchWhenStarted {
viewModel.events.collect { event ->
when (event) {
is UiEvent.ShowToast -> Toast.makeText(context, event.iss.onessage, Toast.LENGTH_SHORT).show()
is UiEvent.NavigateToNextScreen -> findNavController().navigate(R.id.nextFragment)
}
}
}Если в
State всё-таки нужно хранить событие, но не показывать его повторно, можно использовать EventWrapper. class EventWrapper<out T>(private val content: T) {
private var hasBeenHandled = false
fun getContentIfNotHandled(): T? {
return if (hasBeenHandled) null else {
hasBeenHandled = true
content
}
}
}ViewModel с
StateFlow: data class UiState(val message: EventWrapper<String>? = null)
class MyViewModel : ViewModel() {
private val _state = MutableStateFlow(UiState())
val state = _state.asStateFlow()
fun onButtonClicked() {
_state.value = UiState(message = EventWrapper("Привет, это Toast!"))
}
}
UI подписывается и проверяет
EventWrapper: viewModel.state.collect { state ->
state.message?.getContentIfNotHandled()?.let { message ->
Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
}
}Ставь 👍 и забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
👍9🤔2
Ставь 👍 если знал ответ, 🔥 если нет
Забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
👍3🔥1
Forwarded from easyoffer
Привет, ребята!
1,5 года я учился на программиста, а сайт easyoffer.ru стал моим пет-проектом. Я создавал его, потому что:
а) нужно было добавить хоть какой-нибудь проект в резюме
б) подготовиться к прохождению собесов
И всё получилось! Благодаря еasyoffer я успешно прошёл собеседование и устроился Python Junior-разработчиком на удаленку с зарплатой 115 тысяч рублей.
Однако ещё во время разработки я понял, что у этого проекта есть потенциал. Казалось, что сайт может стать популярным и, возможно, превратиться в стартап.
По-этому я с самого начала заложил в проект минимальную бизнес-модель, на случай, если сайт начнёт набирать трафик. Я предложил пользователям полный доступ к сайту в обмен на подписку на Telegram-каналы. Это позволяло развивать аудиторию, а в будущем — зарабатывать на рекламе.
Результат превзошёл ожидания!
С момента запуска easyoffer посетило 400 тысяч человек. А когда доход с рекламы превысил мою зарплату программиста, я принял решение уйти с работы и полностью посвятить себя разработке новой версии сайта.
Вот так, зайдя в IT, через 4 месяца вышел через свой же пет-проект. Мне очень повезло
Уже год я работаю над easyoffer 2.0.
Это будет более масштабный и качественной новый проект:
– Появится тренажер
– Появятся задачи из собесов
– Фильтрация контента по грейдам
и еще очень много фич, о которых я расскажу позже.
Хочу, довести easyoffer до ума, чтобы сайт стал настоящим помощником для всех, кто готовится к собеседованиям.
По этому в ближайшее время я объявлю о старте краудфандинговой кампании, чтобы ускорить разработку и я готов щедро отблагодарить всех, кто поддержит проект.
А те, кто поддержат проект первыми, получат специальные лимитированные выгодные вознаграждения. Следите за этим телеграм каналом, если хотите стать первыми сапортерами.
1,5 года я учился на программиста, а сайт easyoffer.ru стал моим пет-проектом. Я создавал его, потому что:
а) нужно было добавить хоть какой-нибудь проект в резюме
б) подготовиться к прохождению собесов
И всё получилось! Благодаря еasyoffer я успешно прошёл собеседование и устроился Python Junior-разработчиком на удаленку с зарплатой 115 тысяч рублей.
Однако ещё во время разработки я понял, что у этого проекта есть потенциал. Казалось, что сайт может стать популярным и, возможно, превратиться в стартап.
По-этому я с самого начала заложил в проект минимальную бизнес-модель, на случай, если сайт начнёт набирать трафик. Я предложил пользователям полный доступ к сайту в обмен на подписку на Telegram-каналы. Это позволяло развивать аудиторию, а в будущем — зарабатывать на рекламе.
Результат превзошёл ожидания!
С момента запуска easyoffer посетило 400 тысяч человек. А когда доход с рекламы превысил мою зарплату программиста, я принял решение уйти с работы и полностью посвятить себя разработке новой версии сайта.
Вот так, зайдя в IT, через 4 месяца вышел через свой же пет-проект. Мне очень повезло
Уже год я работаю над easyoffer 2.0.
Это будет более масштабный и качественной новый проект:
– Появится тренажер
– Появятся задачи из собесов
– Фильтрация контента по грейдам
и еще очень много фич, о которых я расскажу позже.
Хочу, довести easyoffer до ума, чтобы сайт стал настоящим помощником для всех, кто готовится к собеседованиям.
По этому в ближайшее время я объявлю о старте краудфандинговой кампании, чтобы ускорить разработку и я готов щедро отблагодарить всех, кто поддержит проект.
А те, кто поддержат проект первыми, получат специальные лимитированные выгодные вознаграждения. Следите за этим телеграм каналом, если хотите стать первыми сапортерами.
👍5
В Retrofit можно изменять все запросы глобально с помощью Interceptor (перехватчика) в OkHttp. Это позволяет добавлять или изменять заголовки, параметры запроса, авторизацию, логирование и многое другое.
Перехватчик (
Interceptor) позволяет модифицировать запрос перед его отправкой. class AuthInterceptor(private val tokenProvider: TokenProvider) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request().newBuilder()
.addHeader("Authorization", "Bearer ${tokenProvider.getToken()}")
.addHeader("Accept", "application/json")
.build()
return chain.proceed(request)
}
}Добавляем перехватчик в OkHttpClient:
val okHttpClient = OkHttpClient.Builder()
.addInterceptor(AuthInterceptor(tokenProvider))
.build()
val retrofit = Retrofit.Builder()
.baseUrl("https://api.example.com/")
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create())
.build()
Иногда нужно добавлять общие GET-параметры (например, API-ключ).
class QueryInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val originalRequest = chain.request()
val originalUrl = originalRequest.url
val newUrl = originalUrl.newBuilder()
.addQueryParameter("api_key", "YOUR_API_KEY")
.build()
val newRequest = originalRequest.newBuilder()
.url(newUrl)
.build()
return chain.proceed(newRequest)
}
}Добавляем в
OkHttpClient: val okHttpClient = OkHttpClient.Builder()
.addInterceptor(QueryInterceptor())
.build()
Если сервер возвращает
401 Unauthorized, можно обновить токен и повторить запрос. class AuthenticatorInterceptor(private val tokenProvider: TokenProvider) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
var request = chain.request()
var response = chain.proceed(request)
if (response.code == 401) {
// Получаем новый токен
val newToken = tokenProvider.refreshToken()
// Делаем новый запрос с обновлённым токеном
val newRequest = request.newBuilder()
.header("Authorization", "Bearer $newToken")
.build()
response.close() // Закрываем старый ответ
response = chain.proceed(newRequest) // Повторяем запрос
}
return response
}
}Добавляем в
OkHttpClient: val okHttpClient = OkHttpClient.Builder()
.addInterceptor(AuthenticatorInterceptor(tokenProvider))
.build()
Для отладки удобно логировать все запросы и ответы.
val loggingInterceptor = HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.BODY
}
val okHttpClient = OkHttpClient.Builder()
.addInterceptor(loggingInterceptor)
.build()Если в приложении нужно менять
baseUrl, можно изменять его перед каждым запросом. class BaseUrlInterceptor(private val urlProvider: UrlProvider) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val originalRequest = chain.request()
val newUrl = urlProvider.getBaseUrl() + originalRequest.url.encodedPath
val newRequest = originalRequest.newBuilder()
.url(newUrl)
.build()
return chain.proceed(newRequest)
}
}Используем в
OkHttpClient: val okHttpClient = OkHttpClient.Builder()
.addInterceptor(BaseUrlInterceptor(urlProvider))
.build()
Ставь 👍 и забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
👍10
- В контексте Dagger AppComponent — объект существует, пока существует AppComponent.
- В приложении Android — часто привязывается к жизненному циклу приложения.
Ставь 👍 если знал ответ, 🔥 если нет
Забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥2👍1
dp означает "density-independent pixels" или "пиксели, не зависящие от плотности". Это виртуальная единица измерения, которая используется для обеспечения одинакового визуального размера элементов пользовательского интерфейса на экранах с различной плотностью пикселей. Это важно, поскольку устройства Android могут иметь различные размеры и разрешения экранов, и использование dp помогает создать консистентный пользовательский интерфейс на всех этих устройствах.
Базовая логика такова, что 1 dp равен одному пикселю на экране с плотностью 160 точек на дюйм (dpi). Это плотность соответствует "среднему" уровню, который называется "mdpi" в терминах Android. На таком экране:
1 dp = 1 px
На устройствах с более высокой или ниже плотностью экрана dp масштабируются соответственно:
На устройстве с плотностью 320 dpi (high density, hdpi), 1 dp = 2 px.
На устройстве с плотностью 240 dpi (high density, hdpi), 1 dp = 1.5 px.
И так далее.
Обеспечить, чтобы элементы интерфейса (как размеры, так и отступы) выглядели одинаково на разных устройствах, несмотря на физические различия в размерах экранов и их разрешениях. Это позволяет создавать приложения, которые хорошо выглядят и функционируют на множестве устройств без необходимости перепроектирования интерфейса под каждое конкретное устройство.
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Click me!"
android:padding="16dp" />
Ставь 👍 и забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
👍4
- Преобразует элементы из одного потока в другой поток.
- Пример: из потока URL в поток содержимого страниц.
Ставь 👍 если знал ответ, 🔥 если нет
Забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥3
Forwarded from easyoffer
Ищу работу пол года
Практически под каждым постом в этом канале я вижу комментарии от людей, которые ищут работу по полгода. Это перерастает в обсуждение того, как нужно (или не нужно) искать работу, почему процесс найма сломан и как они откликались на фейковые вакансии.
Честно говоря, искать работу полгода — это нонсенс. Очевидно, что человек делает что-то не так. Главная ошибка, которую совершают многие, — это создание иллюзии поиска работы.
То есть человек вроде бы ищет работу, но делает это неэффективно, тратя время на нецелевые действия. Например:
➖ Просматривает вакансии перед откликом.
➖ Пытается понять, подходит ли он под вакансию. Если считает, что не подходит — не откликается.
➖ Пишет сопроводительные письма (иногда даже уникальные под каждую вакансию).
➖ Заполняет анкеты, проходит тесты.
Все эти действия отнимают время, но не приводят к результату.
Почему это не работает?
HR-менеджер не может вручную отсмотреть 2000 откликов, оценить каждое резюме и прочитать сопроводительные письма. Поэтому компании используют ATS-системы (системы автоматического подбора), которые анализируют резюме и определяют процент его соответствия вакансии.
Что делать, чтобы повысить шансы?
1️⃣ Добавить ключевые навыки в резюме — и в основной текст, и в теги. Возьмите их с easyoffer.ru
2️⃣ Убрать нерелевантный опыт, оставить только подходящий.
3️⃣ Оформить опыт так, чтобы он выглядел релевантным. Если у вас его нет, укажите проекты, стажировки или другой опыт, который можно представить как работу от 1 года. Если опыт слишком большой, сузьте его до 6 лет.
4️⃣ Откликаться на все вакансии без разбору. Если вы Junior, не ищите только стажер или Junior-вакансии — пробуйте везде. Не отказывайте себе сами, пусть это решит HR
5️⃣ Сделать резюме публичным, потому что HR-менеджеры часто ищут кандидатов не только среди откликов, но и в базе резюме.
6️⃣ Используйте ИИ по минимуму – ATS-системы считывают это и помечают "сгенерировано ИИ"
‼️ Главное правило: чем больше откликов — тем выше шанс получить оффер. Делайте резюме удобным для ATS-систем, и вас заметят.
1. Посмотрите видео о том как я вывел свою резюме в Топ1 на HH
2. Посмотрите видео как я нашел первую работу
3. Прочитайте этот кейс про оптимизацию резюме
Если прям вообще тяжело.
Создайте несколько разных резюме. Создайте 2, 3 да хоть 10 резюме. Настройте авто-отлики и ждите приглашения на собесы.
Не нужно создавать иллюзию поиска работы, сделайте несколько простых и актуальных действий.
Практически под каждым постом в этом канале я вижу комментарии от людей, которые ищут работу по полгода. Это перерастает в обсуждение того, как нужно (или не нужно) искать работу, почему процесс найма сломан и как они откликались на фейковые вакансии.
Честно говоря, искать работу полгода — это нонсенс. Очевидно, что человек делает что-то не так. Главная ошибка, которую совершают многие, — это создание иллюзии поиска работы.
То есть человек вроде бы ищет работу, но делает это неэффективно, тратя время на нецелевые действия. Например:
Все эти действия отнимают время, но не приводят к результату.
Почему это не работает?
HR-менеджер не может вручную отсмотреть 2000 откликов, оценить каждое резюме и прочитать сопроводительные письма. Поэтому компании используют ATS-системы (системы автоматического подбора), которые анализируют резюме и определяют процент его соответствия вакансии.
Что делать, чтобы повысить шансы?
1️⃣ Добавить ключевые навыки в резюме — и в основной текст, и в теги. Возьмите их с easyoffer.ru
2️⃣ Убрать нерелевантный опыт, оставить только подходящий.
3️⃣ Оформить опыт так, чтобы он выглядел релевантным. Если у вас его нет, укажите проекты, стажировки или другой опыт, который можно представить как работу от 1 года. Если опыт слишком большой, сузьте его до 6 лет.
4️⃣ Откликаться на все вакансии без разбору. Если вы Junior, не ищите только стажер или Junior-вакансии — пробуйте везде. Не отказывайте себе сами, пусть это решит HR
5️⃣ Сделать резюме публичным, потому что HR-менеджеры часто ищут кандидатов не только среди откликов, но и в базе резюме.
6️⃣ Используйте ИИ по минимуму – ATS-системы считывают это и помечают "сгенерировано ИИ"
‼️ Главное правило: чем больше откликов — тем выше шанс получить оффер. Делайте резюме удобным для ATS-систем, и вас заметят.
1. Посмотрите видео о том как я вывел свою резюме в Топ1 на HH
2. Посмотрите видео как я нашел первую работу
3. Прочитайте этот кейс про оптимизацию резюме
Если прям вообще тяжело.
Создайте несколько разных резюме. Создайте 2, 3 да хоть 10 резюме. Настройте авто-отлики и ждите приглашения на собесы.
Не нужно создавать иллюзию поиска работы, сделайте несколько простых и актуальных действий.
Please open Telegram to view this post
VIEW IN TELEGRAM