Slide 1

Slide 1 text

Давайте обращать внимание на детали? Алексей Быков 10.11.18

Slide 2

Slide 2 text

๏ Android Developer at KasperskyLab ๏ Mosdroid Speaker ๏ Android Academy MSK Co-orgaznizer ๏ Passionate about mobile UX About 2

Slide 3

Slide 3 text

!3

Slide 4

Slide 4 text

!4

Slide 5

Slide 5 text

!5 Don’t do that

Slide 6

Slide 6 text

Старт приложения 6

Slide 7

Slide 7 text

Старт приложения 7 Cold start Hot start Warm start

Slide 8

Slide 8 text

Cold start 8

Slide 9

Slide 9 text

Cold start 9 Первый запуск приложения Создание процесса приложения Пользователь видит белое окно

Slide 10

Slide 10 text

Hot start 10

Slide 11

Slide 11 text

Hot start 11 Приложение уже запускалось Пересоздание процесса (Возможно) Пользователь всё ещё видит белое окно Task с Activities жив (Возможно)

Slide 12

Slide 12 text

Warm start 12

Slide 13

Slide 13 text

Warm start 13 Приложение уже запускалось Процесс приложения жив Пользователь не видит белое окно

Slide 14

Slide 14 text

Проблема: Белое окно 14

Slide 15

Slide 15 text

Белое окно: А точно ли проблема? 15

Slide 16

Slide 16 text

Material Design 16

Slide 17

Slide 17 text

Material Design 17 Placeholder Branded

Slide 18

Slide 18 text

Placeholder 18 Привязан к пользовательскому интерфейсу Минимализм Не надоедает пользователю Грузится быстрее Нет «вау» эффекта

Slide 19

Slide 19 text

Branded 19 Грузится дольше Подходит для крупных компаний Больше возможности с анимациями Может надоесть пользователю

Slide 20

Slide 20 text

Реализация 20

Slide 21

Slide 21 text

Manifest 21

Slide 22

Slide 22 text

22 Manifest

Slide 23

Slide 23 text

23 Style <item name="android:windowBackground">@drawable/background_splash</item> <item name="colorAccent">@color/light_background</item> <item name="windowActionBar">false</item> <item name="windowNoTitle">true</item> <item name="android:windowTranslucentStatus">true</item>

Slide 24

Slide 24 text

24 Style <item name="android:windowBackground">@drawable/background_splash</item> <item name="colorAccent">@color/light_background</item> <item name="windowActionBar">false</item> <item name="windowNoTitle">true</item> <item name="android:windowTranslucentStatus">true</item>

Slide 25

Slide 25 text

25 Style: API 26+ <item name="android:windowSplashscreenContent">@drawable/splashscreen</item>

Slide 26

Slide 26 text

26 Drawable (Branded)

Slide 27

Slide 27 text

27 Drawable (Branded)

Slide 28

Slide 28 text

28 Drawable (Placeholder)

Slide 29

Slide 29 text

29 Drawable (Placeholder)

Slide 30

Slide 30 text

Как делать не надо 30

Slide 31

Slide 31 text

Как делать не надо 31 Http-запросы в SplashScreen Использовать таймеры Использовать текст в Branded Screen

Slide 32

Slide 32 text

!32

Slide 33

Slide 33 text

Авторизация 33

Slide 34

Slide 34 text

Don’t do that

Slide 35

Slide 35 text

OnKeyboardStateChangeListener 35

Slide 36

Slide 36 text

OnKeyboardStateChangeListener 36 ¯ \ _ (ツ) _ / ¯

Slide 37

Slide 37 text

Manifest 37

Slide 38

Slide 38 text

Manifest 38

Slide 39

Slide 39 text

ViewGroup 39 fun ViewGroup.onKeyboardStateChanged(activity: Activity, listener: (Boolean) -> Unit) { viewTreeObserver.addOnGlobalLayoutListener { val screenSize = Point() activity.windowManager.defaultDisplay.getSize(screenSize) val rect = Rect() getWindowVisibleDisplayFrame(rect) val diff = screenSize.y - rect.bottom listener.invoke(diff != 0) } }

Slide 40

Slide 40 text

ViewGroup 40 fun ViewGroup.onKeyboardStateChanged(activity: Activity, listener: (Boolean) -> Unit) { viewTreeObserver.addOnGlobalLayoutListener { val screenSize = Point() activity.windowManager.defaultDisplay.getSize(screenSize) val rect = Rect() getWindowVisibleDisplayFrame(rect) val diff = screenSize.y - rect.bottom listener.invoke(diff != 0) } }

Slide 41

Slide 41 text

ViewGroup 41 fun ViewGroup.onKeyboardStateChanged(activity: Activity, listener: (Boolean) -> Unit) { viewTreeObserver.addOnGlobalLayoutListener { val screenSize = Point() activity.windowManager.defaultDisplay.getSize(screenSize) val rect = Rect() getWindowVisibleDisplayFrame(rect) val diff = screenSize.y - rect.bottom listener.invoke(diff != 0) } }

Slide 42

Slide 42 text

ViewGroup 42 fun ViewGroup.onKeyboardStateChanged(activity: Activity, listener: (Boolean) -> Unit) { viewTreeObserver.addOnGlobalLayoutListener { val screenSize = Point() activity.windowManager.defaultDisplay.getSize(screenSize) val rect = Rect() getWindowVisibleDisplayFrame(rect) val diff = screenSize.y - rect.bottom listener.invoke(diff != 0) } }

Slide 43

Slide 43 text

Fragment/Activity 43 ltRoot.onKeyboardStateChanged(activity, { isShown -> ivHeader.show(!isShown) })

Slide 44

Slide 44 text

!44

Slide 45

Slide 45 text

!45 Don’t do that

Slide 46

Slide 46 text

EditText 46

Slide 47

Slide 47 text

EditText 47

Slide 48

Slide 48 text

!48

Slide 49

Slide 49 text

Обработка ошибок 49

Slide 50

Slide 50 text

WTF? 50 fun handleError(e: Throwable) { viewState.showError("Какие-то проблемы..") }

Slide 51

Slide 51 text

Какие-то проблемы..

Slide 52

Slide 52 text

52

Slide 53

Slide 53 text

53 Don’t do that

Slide 54

Slide 54 text

54

Slide 55

Slide 55 text

55 Don’t do that

Slide 56

Slide 56 text

Try again later — no more! 56 https://bit.ly/2ALQF8D

Slide 57

Slide 57 text

Валидация 57

Slide 58

Slide 58 text

Валидация 58 Отображайте ошибки в TextInputLayout Убирайте ошибки в afterTextChanged

Slide 59

Slide 59 text

!59

Slide 60

Slide 60 text

NavigationView 60

Slide 61

Slide 61 text

Don’t do that

Slide 62

Slide 62 text

Start Activity 62 handler.postDelayed({ startActivity(Intent(this, TestActivity::class.java)) overridePendingTransition(R.anim.move_right_in_activity, R.anim.move_left_out_activity) }, 300)

Slide 63

Slide 63 text

Start Activity 63 override fun finish() { super.finish() overridePendingTransition(R.anim.move_left_in_activity, R.anim.move_right_out_activity) }

Slide 64

Slide 64 text

!64

Slide 65

Slide 65 text

!65

Slide 66

Slide 66 text

!66

Slide 67

Slide 67 text

RecyclerView 67

Slide 68

Slide 68 text

Don’t do that

Slide 69

Slide 69 text

Scrollbar 69

Slide 70

Slide 70 text

Scrollbar 70

Slide 71

Slide 71 text

Fab animation 71 class RecyclerViewWithFloatingActionButton : RecyclerView { var mFloatingActionButton: FloatingActionButton? = null constructor(context: Context) : super(context) {} constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {} constructor(context: Context, attrs: AttributeSet?, defStyle: Int) : super(context, attrs, defStyle) {} override fun onScrolled(dx: Int, dy: Int) { super.onScrolled(dx, dy) if (dy > 0) { // Scrolling up mFloatingActionButton?.hide() } else { // Scrolling down mFloatingActionButton?.show() } } }

Slide 72

Slide 72 text

Fab animation 72 class RecyclerViewWithFloatingActionButton : RecyclerView { var mFloatingActionButton: FloatingActionButton? = null constructor(context: Context) : super(context) {} constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {} constructor(context: Context, attrs: AttributeSet?, defStyle: Int) : super(context, attrs, defStyle) {} override fun onScrolled(dx: Int, dy: Int) { super.onScrolled(dx, dy) if (dy > 0) { // Scrolling up mFloatingActionButton?.hide() } else { // Scrolling down mFloatingActionButton?.show() } } }

Slide 73

Slide 73 text

Fab animation 73 class RecyclerViewWithFloatingActionButton : RecyclerView { var mFloatingActionButton: FloatingActionButton? = null constructor(context: Context) : super(context) {} constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {} constructor(context: Context, attrs: AttributeSet?, defStyle: Int) : super(context, attrs, defStyle) {} override fun onScrolled(dx: Int, dy: Int) { super.onScrolled(dx, dy) if (dy > 0) { // Scrolling up mFloatingActionButton?.hide() } else { // Scrolling down mFloatingActionButton?.show() } } }

Slide 74

Slide 74 text

!74

Slide 75

Slide 75 text

!75 Don’t do that

Slide 76

Slide 76 text

Debounce Click 76

Slide 77

Slide 77 text

Debounce Click 77

Slide 78

Slide 78 text

Debounce Click 78 abstract class DebounceOnClickListener() : View.OnClickListener { companion object { const private val CLICK_DELAY = 500L private var sIsClickEnabled = true } private val mClickLockRunnable = Runnable { sIsClickEnabled = true } override fun onClick(view: View?) { if (sIsClickEnabled) { sIsClickEnabled = false onDelayedClick(view) view?.postDelayed(mClickLockRunnable, CLICK_DELAY) } } abstract fun onDelayedClick(view: View?) }

Slide 79

Slide 79 text

Extension 79 fun View.setDebounceOnClickListener(listener: (view: View?) -> Unit) { setOnClickListener(object : DebounceClickListener() { override fun onDelayedClick(view: View?) { listener.invoke(view) } }) }

Slide 80

Slide 80 text

Adapter 80 itemView.setDebounceOnClickListener { mListener?.invoke(id) }

Slide 81

Slide 81 text

Пагинация 81

Slide 82

Slide 82 text

Пагинация 82 Делайте подгрузку с отступом Показывайте ошибку в конце списка

Slide 83

Slide 83 text

!83

Slide 84

Slide 84 text

!84

Slide 85

Slide 85 text

NoPaginate 85

Slide 86

Slide 86 text

NoPaginate 86 val noPaginate = NoPaginate.with(recyclerView) .setOnLoadMoreListener { //http or db request } .build()

Slide 87

Slide 87 text

NoPaginate 87 val noPaginate = NoPaginate.with(recyclerView) .setOnLoadMoreListener { //http or db request } .setLoadingTriggerThreshold(5) //0 by default .setCustomErrorItem(CustomErrorItem()) .setCustomLoadingItem(CustomLoadingItem()) .build()

Slide 88

Slide 88 text

NoPaginate 88 implementation 'ru.alexbykov:nopaginate:0.9.9' https://github.com/NoNews/NoPaginate

Slide 89

Slide 89 text

!89

Slide 90

Slide 90 text

Animation 90 TransitionManager.beginDelayedTransition(rootView)

Slide 91

Slide 91 text

!91

Slide 92

Slide 92 text

Давайте обращать внимание на детали? 92

Slide 93

Slide 93 text

Давайте обращать внимание на детали! 93

Slide 94

Slide 94 text

Let’s talk @NoNews @NoNews [email protected]