Алексей Михайлов
5 декабря 2020
MObile KOtlin
DEVELOPMENT
План
2
DEVELOPMENT
Не включено
Что такое Kotlin Multiplatform Mobile?
В выступлении Кати Петровой
3
DEVELOPMENT
Немного о себе
IceRock Development
Алексей Михайлов
4
DEVELOPMENT
Проекты
5
DEVELOPMENT
Наши проекты
6
DEVELOPMENT
Наши проекты
7
DEVELOPMENT
Наши проекты
8
DEVELOPMENT
Наши проекты
9
DEVELOPMENT
Наши проекты
10
DEVELOPMENT
Наши проекты
11
DEVELOPMENT
MObile KOtlin
12
DEVELOPMENT
Упрощаем gradle
13
DEVELOPMENT
Упрощаем gradle
Для чего использовать?
Для упрощения настройки gradle mobile multiplatform проектов
и удобной интеграции CocoaPods
14
DEVELOPMENT
Упрощаем gradle
plugins {
id("com.android.library")
id("org.jetbrains.kotlin.multiplatform")
id("dev.icerock.mobile.multiplatform")
id("dev.icerock.mobile.multiplatform.ios-framework")
}
android {
compileSdkVersion(30)
defaultConfig {
minSdkVersion(24)
targetSdkVersion(30)
}
}
plugins {
id("com.android.library")
id("org.jetbrains.kotlin.multiplatform")
}
kotlin {
android()
ios()
}
android {
compileSdkVersion(30)
defaultConfig {
minSdkVersion(24)
targetSdkVersion(30)
}
sourceSets {
mapOf(
"main" to "src/androidMain",
...
).forEach { (name, root) ->
getByName(name).run {
setRoot(root)
}
}
}
}
val packForXcode by tasks.creating(Sync::class) {
group = "build"
val mode = System.getenv("CONFIGURATION") ?: "DEBUG"
val sdkName = System.getenv("SDK_NAME") ?: "iphonesimulator"
val targetName = "ios" + if (sdkName.startsWith("iphoneos")) "Arm64" else "X64"
val framework = kotlin.targets.getByName<KotlinNativeTarget>(targetName).binaries.getFramework(mode)
inputs.property("mode", mode)
dependsOn(framework.linkTask)
val targetDir = File(buildDir, "xcode-frameworks")
from({ framework.outputDirectory })
into(targetDir)
}
tasks.getByName("build").dependsOn(packForXcode)
15
DEVELOPMENT
Упрощаем gradle
16
DEVELOPMENT
Упрощаем gradle
1 - добавляем плагин ios-framework
2 - получаем gradle task’и sync***FrameworkIos***
3 - готовый фреймворк всегда по пути
build/cocoapods/framework
специально для podspec от CocoaPods
17
DEVELOPMENT
Упрощаем gradle
plugins {
id("dev.icerock.mobile.multiplatform.ios-framework")
}
// optional for export dependencies into framework header
framework {
export(project = project(":myproject"))
export(kotlinNativeExportable = MultiPlatfomLibrary(<...>))
export(kotlinNativeExportable = MultiPlatfomModule(<...>))
export(arm64Dependency = "***-iosarm64:***", x64Dependency = "***-iosx64:***")
}
18
DEVELOPMENT
Упрощаем gradle
cocoaPods {
podsProject = file("../ios-app/Pods/Pods.xcodeproj")
buildConfiguration = "dev-debug"
pod("MBProgressHUD")
pod(schema = "moko-widgets-flat", module = "mokoWidgetsFlat")
pod(schema = "moko-widgets-flat", module = "mokoWidgetsFlat", onlyLink = true)
}
19
DEVELOPMENT
Упрощаем gradle
cocoaPods {
podsProject = file("../ios-app/Pods/Pods.xcodeproj")
buildConfiguration = "dev-debug"
pod("MBProgressHUD")
pod(schema = "moko-widgets-flat", module = "mokoWidgetsFlat")
pod(schema = "moko-widgets-flat", module = "mokoWidgetsFlat", onlyLink = true)
}
20
DEVELOPMENT
Упрощаем gradle
cocoaPods {
podsProject = file("../ios-app/Pods/Pods.xcodeproj")
buildConfiguration = "dev-debug"
pod("MBProgressHUD")
pod(schema = "moko-widgets-flat", module = "mokoWidgetsFlat")
pod(schema = "moko-widgets-flat", module = "mokoWidgetsFlat", onlyLink = true)
}
21
DEVELOPMENT
Упрощаем gradle
cocoaPods {
podsProject = file("../ios-app/Pods/Pods.xcodeproj")
buildConfiguration = "dev-debug"
pod("MBProgressHUD")
pod(schema = "moko-widgets-flat", module = "mokoWidgetsFlat")
pod(schema = "moko-widgets-flat", module = "mokoWidgetsFlat", onlyLink = true)
}
задача компиляции CocoaPod
настройка линковки
интеграция cInterop в проект
22
DEVELOPMENT
Упрощаем gradle
cocoaPods {
podsProject = file("../ios-app/Pods/Pods.xcodeproj")
buildConfiguration = "dev-debug"
pod("MBProgressHUD")
pod(schema = "moko-widgets-flat", module = "mokoWidgetsFlat")
pod(schema = "moko-widgets-flat", module = "mokoWidgetsFlat", onlyLink = true)
}
задача компиляции CocoaPod
настройка линковки
интеграция cInterop в проект
23
DEVELOPMENT
Упрощаем gradle
cocoaPods {
podsProject = file("../ios-app/Pods/Pods.xcodeproj")
buildConfiguration = "dev-debug"
pod("MBProgressHUD")
pod(schema = "moko-widgets-flat", module = "mokoWidgetsFlat")
pod(schema = "moko-widgets-flat", module = "mokoWidgetsFlat", onlyLink = true)
}
задача компиляции CocoaPod
настройка линковки
интеграция cInterop в проект
24
DEVELOPMENT
Упрощаем gradle
25
DEVELOPMENT
Архитектурный подход
26
DEVELOPMENT
Архитектурный подход
Для чего использовать?
Для разработки по MVVM подходу
и легкой миграции с Android Architecture Components в common
27
DEVELOPMENT
Архитектурный подход
class MainViewModel : ViewModel() {
private val _text = MutableLiveData<String>()
val text: LiveData<String> = _text
init {
_text.value = "Hello ..."
viewModelScope.launch {
delay(1000)
_text.postValue("Hello World")
}
}
override fun onCleared() {
super.onCleared()
// do own cleanup here
}
}
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
android-arch-viewmodel
28
DEVELOPMENT
Архитектурный подход
class MainViewModel : ViewModel() {
private val _text = MutableLiveData<String>("")
val text: LiveData<String> = _text
init {
_text.value = "Hello ..."
viewModelScope.launch {
delay(1000)
_text.postValue("Hello World")
}
}
override fun onCleared() {
super.onCleared()
// do own cleanup here
}
}
import dev.icerock.moko.mvvm.livedata.LiveData
import dev.icerock.moko.mvvm.livedata.MutableLiveData
import dev.icerock.moko.mvvm.viewmodel.ViewModel
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
moko-mvvm
29
DEVELOPMENT
Архитектурный подход
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val viewModel: MainViewModel = ViewModelProviders
.of(this).get(MainViewModel::class.java)
viewModel.text.observe(this) {
findViewById<TextView>(R.id.text).text = it
}
}
}
import android.os.Bundle
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.ViewModelProviders
android-arch-viewmodel
30
DEVELOPMENT
Архитектурный подход
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val viewModel: MainViewModel = ViewModelProviders
.of(this).get(MainViewModel::class.java)
viewModel.text.ld().observe(this) {
findViewById<TextView>(R.id.text).text = it
}
}
}
import android.os.Bundle
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.ViewModelProviders
moko-mvvm - android
31
DEVELOPMENT
Архитектурный подход
class ViewController: UIViewController {
@IBOutlet var label: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
let viewModel = MainViewModel()
viewModel.text.addObserver { [weak self] data in
self?.label.text = data as String?
}
}
}
import Foundation
import UIKit
import MultiPlatformLibrary
moko-mvvm - ios
32
DEVELOPMENT
Архитектурный подход
Замена SingleLiveEvent - commonMain
class MainViewModel(
val eventsDispatcher: EventsDispatcher<EventsListener>
) : ViewModel() {
init {
viewModelScope.launch {
delay(1000)
eventsDispatcher.dispatchEvent { showErrorDialog("error!") }
}
}
interface EventsListener {
fun showErrorDialog(message: String)
}
}
33
DEVELOPMENT
Архитектурный подход
Замена SingleLiveEvent - android app
override fun onCreate(savedInstanceState: Bundle?) {
...
val viewModel: MainViewModel = getViewModel {
MainViewModel(eventsDispatcher = eventsDispatcherOnMain())
}
viewModel.eventsDispatcher.bind(this, object : MainViewModel.EventsListener {
override fun showErrorDialog(message: String) {
println("show error dialog $message")
}
})
}
34
DEVELOPMENT
Архитектурный подход
Замена SingleLiveEvent - ios app
class ViewController: UIViewController {
@IBOutlet var label: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
let viewModel = MainViewModel(eventsDispatcher: EventsDispatcher(listener: self))
}
}
extension ViewController: MainViewModelEventsListener {
func showErrorDialog(message: String) {
print("show error dialog \(message)")
}
}
35
DEVELOPMENT
Архитектурный подход
36
DEVELOPMENT
Архитектурный подход
немного о планах
Все пожелания можно оставлять на github moko-mvvm
37
DEVELOPMENT
Работаем с сетью
38
DEVELOPMENT
Работаем с сетью
Для чего использовать?
Для упрощения реализации REST API клиента на базе ktor-client
без необходимости писать все руками
39
DEVELOPMENT
Работаем с сетью
Retrofit
interface UserApi {
@GET("user")
fun getUserInfo(@Query("AppId") appId: String): Call<UserResponse>
}
40
DEVELOPMENT
Работаем с сетью
ktor-client
val httpClient = HttpClient()
viewModelScope.launch {
val response: HttpResponse = httpClient.get {
url("https://localhost/user")
parameter("appId", "123")
}
val text = response.readText()
}
41
DEVELOPMENT
Работаем с сетью
openapi: 3.0.0
info:
title: User server
version: v1
servers:
- url: http://localhost/
paths:
/user:
get:
parameters:
- name: appId
in: query
schema:
type: string
required: true
responses:
'200':
description: ok
content:
application/json:
schema:
$ref: '#/components/schemas/UserResponse'
components:
schemas:
UserResponse:
properties:
first_name:
type: string
last_name:
type: string
required:
- first_name
- last_name
type: object
42
DEVELOPMENT
Работаем с сетью
используя сгенерированный код
val defaultApi = DefaultApi(httpClient = HttpClient(), json = Json.Default)
viewModelScope.launch {
val user: UserResponse = defaultApi.userGet(appId = "123")
}
43
DEVELOPMENT
Работаем с сетью
44
DEVELOPMENT
Обращаемся к ресурсам
45
DEVELOPMENT
Обращаемся к ресурсам
Для чего использовать?
Использовать ресурсы приложения в бизнес-логике
и не дублировать ресурсы между платформами
46
DEVELOPMENT
Обращаемся к ресурсам
Пример (задача)
В зависимости от типа транспорта,
который мы получаем из данных сервера (enum),
нужно отобразить локализованную строку с названием этого транспорта
47
DEVELOPMENT
Обращаемся к ресурсам
Пример (задача)
enum class VehicleType {
BOAT,
CAR,
PLANE
}
class MainViewModel : ViewModel() {
val vehicleType: VehicleType
get() = ...
}
commonMain
48
DEVELOPMENT
Обращаемся к ресурсам
Пример (без moko-resources)
@StringRes
val text = when (viewModel.vehicleType) {
VehicleType.BOAT -> R.string.boatTitle
VehicleType.CAR -> R.string.carTitle
VehicleType.PLANE -> R.string.planeTitle
}
android-app
49
DEVELOPMENT
Обращаемся к ресурсам
Пример (без moko-resources)
let text: String
switch(viewModel.vehicleType) {
case VehicleType.boat:
text = NSLocalizedString("boatTitle", comment: "")
break
case VehicleType.car:
text = NSLocalizedString("carTitle", comment: "")
break
case VehicleType.plane:
text = NSLocalizedString("planeTitle", comment: "")
break
}
ios-app
50
DEVELOPMENT
Обращаемся к ресурсам
Пример (с moko-resources)
class MainViewModel : ViewModel() {
private val vehicleType: VehicleType get() = ...
val vehicleTypeString: StringResource
get() = when(vehicleType) {
VehicleType.BOAT -> MR.strings.boatTitle
VehicleType.CAR -> MR.strings.carTitle
VehicleType.PLANE -> MR.strings.planeTitle
}
}
commonMain
51
DEVELOPMENT
Обращаемся к ресурсам
Пример (с moko-resources)
@StringRes
val text = viewModel.vehicleTypeString.resourceId
android-app
52
DEVELOPMENT
Обращаемся к ресурсам
Пример (с moko-resources)
let resourceId = viewModel.vehicleTypeString.resourceId
let bundle = viewModel.vehicleTypeString.bundle
let text = NSLocalizedString(resourceId, bundle: bundle, comment: "")
ios-app
53
DEVELOPMENT
Обращаемся к ресурсам
Пример (с moko-resources)
<?xml version="1.0" encoding="UTF-8" ?>
<resources>
<string name="carTitle">Car</string>
<string name="boatTitle">Boat</string>
<string name="planeTitle">Plane</string>
</resources>
commonMain/resources
54
DEVELOPMENT
Обращаемся к ресурсам
Пример (с moko-resources)
expect object MR {
object strings : ResourceContainer<StringResource> {
val carTitle: StringResource
val boatTitle: StringResource
val planeTitle: StringResource
}
...
}
commonMain
55
DEVELOPMENT
Обращаемся к ресурсам
56
DEVELOPMENT
Обращаемся к ресурсам
StringDesc для разных источников
actual interface StringDesc {
fun toString(context: Context): String
}
androidMain
actual interface StringDesc {
fun localized(): String
}
iosMain
57
DEVELOPMENT
Обращаемся к ресурсам
StringDesc для разных источников
58
DEVELOPMENT
Обращаемся к ресурсам
StringDesc для разных источников
data class User(val fullName: String)
val user: User? get() { ... }
val title: StringDesc get() {
return if(user != null) StringDesc.Raw(user.fullName)
else StringDesc.Resource(MR.strings.not_authorized)
}
59
DEVELOPMENT
Обращаемся к ресурсам
60
DEVELOPMENT
Получаем разрешения
61
DEVELOPMENT
Получаем разрешения
Для чего использовать?
Получать рантайм разрешения из общего кода
62
DEVELOPMENT
Получаем разрешения
class MyViewModel(val permissionsController: PermissionsController): ViewModel() {
fun onPhotoPressed() {
viewModelScope.launch {
try {
permissionsController.providePermission(Permission.GALLERY)
// Permission has been granted successfully.
} catch(deniedAlways: DeniedAlwaysException) {
// Permission is always denied.
} catch(denied: DeniedException) {
// Permission was denied.
}
}
}
}
63
DEVELOPMENT
Получаем разрешения
class MyViewModel(val permissionsController: PermissionsController): ViewModel() {
fun onPhotoPressed() {
viewModelScope.launch {
try {
permissionsController.providePermission(Permission.GALLERY)
// Permission has been granted successfully.
} catch(deniedAlways: DeniedAlwaysException) {
// Permission is always denied.
} catch(denied: DeniedException) {
// Permission was denied.
}
}
}
}
64
DEVELOPMENT
Получаем разрешения
class MyViewModel(val permissionsController: PermissionsController): ViewModel() {
fun onPhotoPressed() {
viewModelScope.launch {
try {
permissionsController.providePermission(Permission.GALLERY)
// Permission has been granted successfully.
} catch(deniedAlways: DeniedAlwaysException) {
// Permission is always denied.
} catch(denied: DeniedException) {
// Permission was denied.
}
}
}
}
65
DEVELOPMENT
Получаем разрешения
class MyViewModel(val permissionsController: PermissionsController): ViewModel() {
fun onPhotoPressed() {
viewModelScope.launch {
try {
permissionsController.providePermission(Permission.GALLERY)
// Permission has been granted successfully.
} catch(deniedAlways: DeniedAlwaysException) {
// Permission is always denied.
} catch(denied: DeniedException) {
// Permission was denied.
}
}
}
}
66
DEVELOPMENT
Получаем разрешения
class MyViewModel(val permissionsController: PermissionsController): ViewModel() {
fun onPhotoPressed() {
viewModelScope.launch {
try {
permissionsController.providePermission(Permission.GALLERY)
// Permission has been granted successfully.
} catch(deniedAlways: DeniedAlwaysException) {
// Permission is always denied.
} catch(denied: DeniedException) {
// Permission was denied.
}
}
}
}
67
DEVELOPMENT
Получаем разрешения
class MyViewModel(val permissionsController: PermissionsController): ViewModel() {
fun onPhotoPressed() {
viewModelScope.launch {
try {
permissionsController.providePermission(Permission.GALLERY)
// Permission has been granted successfully.
} catch(deniedAlways: DeniedAlwaysException) {
// Permission is always denied.
} catch(denied: DeniedException) {
// Permission was denied.
}
}
}
}
68
DEVELOPMENT
Получаем разрешения
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val viewModel = getViewModel {
// Pass the platform implementation of the permission controller to a common code.
MyViewModel(PermissionsController())
}
// Binds the permissions controller to the activity lifecycle.
viewModel.permissionsController.bind(lifecycle, supportFragmentManager)
}
android-app
69
DEVELOPMENT
Получаем разрешения
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val viewModel = getViewModel {
// Pass the platform implementation of the permission controller to a common code.
MyViewModel(PermissionsController())
}
// Binds the permissions controller to the activity lifecycle.
viewModel.permissionsController.bind(lifecycle, supportFragmentManager)
}
android-app
70
DEVELOPMENT
Получаем разрешения
// Just pass the platform implementation of the permission controller to a common code.
let viewModel = ViewModel(permissionsController: PermissionsController())
ios-app
71
DEVELOPMENT
Получаем разрешения
72
DEVELOPMENT
Получаем медиа файлы
73
DEVELOPMENT
Получаем медиа файлы
Для чего использовать?
Получать изображения с камеры или галереи из общего кода
74
DEVELOPMENT
Получаем медиа файлы
class ImageSelectionViewModel(
val mediaPickerController: MediaPickerController
) : ViewModel() {
fun onSelectImagePressed() {
viewModelScope.launch {
try {
val image = mediaPickerController.pickImage(MediaSource.CAMERA)
// Use image
} catch (exc: Exception) {
// Some error
}
}
}
}
75
DEVELOPMENT
Получаем медиа файлы
class ImageSelectionViewModel(
val mediaPickerController: MediaPickerController
) : ViewModel() {
fun onSelectImagePressed() {
viewModelScope.launch {
try {
val image = mediaPickerController.pickImage(MediaSource.CAMERA)
// Use image
} catch (exc: Exception) {
// Some error
}
}
}
}
76
DEVELOPMENT
Получаем медиа файлы
class ImageSelectionViewModel(
val mediaPickerController: MediaPickerController
) : ViewModel() {
fun onSelectImagePressed() {
viewModelScope.launch {
try {
val image = mediaPickerController.pickImage(MediaSource.CAMERA)
// Use image
} catch (exc: Exception) {
// Some error
}
}
}
}
77
DEVELOPMENT
Получаем медиа файлы
class ImageSelectionViewModel(
val mediaPickerController: MediaPickerController
) : ViewModel() {
fun onSelectImagePressed() {
viewModelScope.launch {
try {
val image = mediaPickerController.pickImage(MediaSource.CAMERA)
// Use image
} catch (exc: Exception) {
// Some error
}
}
}
}
78
DEVELOPMENT
Получаем медиа файлы
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val viewModel = getViewModel {
val permissionsController = PermissionsController(
applicationContext = applicationContext
)
val mediaPickerController = MediaPickerController(permissionsController)
ImageSelectionViewModel(mediaPickerController)
}
viewModel.mediaPickerController.bind(lifecycle, supportFragmentManager)
}
android-app
79
DEVELOPMENT
Получаем медиа файлы
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val viewModel = getViewModel {
val permissionsController = PermissionsController(
applicationContext = applicationContext
)
val mediaPickerController = MediaPickerController(permissionsController)
ImageSelectionViewModel(mediaPickerController)
}
viewModel.mediaPickerController.bind(lifecycle, supportFragmentManager)
}
android-app
80
DEVELOPMENT
Получаем медиа файлы
let permissionsController = PermissionsController()
let mediaPickerController = MediaPickerController(
permissionsController: permissionsController,
viewController: self
)
viewModel = ImageSelectionViewModel(
mediaPickerController: mediaPickerController
)
ios-app
81
DEVELOPMENT
Получаем медиа файлы
expect class MediaPlayerController {
fun prepare(pathSource: String, listener: MediaPlayerListener)
fun start()
fun pause()
fun stop()
fun isPlaying(): Boolean
fun release()
}
82
DEVELOPMENT
Получаем медиа файлы
83
DEVELOPMENT
Получаем координаты
84
DEVELOPMENT
Получаем координаты
Для чего использовать?
Отслеживать изменения геопозиции из общего кода
85
DEVELOPMENT
Получаем координаты
class TrackerViewModel(
val locationTracker: LocationTracker
) : ViewModel() {
init {
viewModelScope.launch {
locationTracker.getLocationsFlow()
.distinctUntilChanged()
.collect { println("new location: $it") }
}
}
fun onStartPressed() {
viewModelScope.launch { locationTracker.startTracking() }
}
fun onStopPressed() {
locationTracker.stopTracking()
}
}
86
DEVELOPMENT
Получаем координаты
class TrackerViewModel(
val locationTracker: LocationTracker
) : ViewModel() {
init {
viewModelScope.launch {
locationTracker.getLocationsFlow()
.distinctUntilChanged()
.collect { println("new location: $it") }
}
}
fun onStartPressed() {
viewModelScope.launch { locationTracker.startTracking() }
}
fun onStopPressed() {
locationTracker.stopTracking()
}
}
87
DEVELOPMENT
Получаем координаты
class TrackerViewModel(
val locationTracker: LocationTracker
) : ViewModel() {
init {
viewModelScope.launch {
locationTracker.getLocationsFlow()
.distinctUntilChanged()
.collect { println("new location: $it") }
}
}
fun onStartPressed() {
viewModelScope.launch { locationTracker.startTracking() }
}
fun onStopPressed() {
locationTracker.stopTracking()
}
}
88
DEVELOPMENT
Получаем координаты
class TrackerViewModel(
val locationTracker: LocationTracker
) : ViewModel() {
init {
viewModelScope.launch {
locationTracker.getLocationsFlow()
.distinctUntilChanged()
.collect { println("new location: $it") }
}
}
fun onStartPressed() {
viewModelScope.launch { locationTracker.startTracking() }
}
fun onStopPressed() {
locationTracker.stopTracking()
}
}
89
DEVELOPMENT
Получаем координаты
class TrackerViewModel(
val locationTracker: LocationTracker
) : ViewModel() {
init {
viewModelScope.launch {
locationTracker.getLocationsFlow()
.distinctUntilChanged()
.collect { println("new location: $it") }
}
}
fun onStartPressed() {
viewModelScope.launch { locationTracker.startTracking() }
}
fun onStopPressed() {
locationTracker.stopTracking()
}
}
90
DEVELOPMENT
Получаем координаты
val viewModel = getViewModel {
val locationTracker = LocationTracker(
permissionsController = PermissionsController(applicationContext)
)
TrackerViewModel(locationTracker)
}
viewModel.locationTracker.bind(lifecycle, this, supportFragmentManager)
android-app
91
DEVELOPMENT
Получаем координаты
let viewModel = TrackerViewModel(
locationTracker: LocationTracker(
permissionsController: PermissionsController(),
accuracy: kCLLocationAccuracyBest
)
)
ios-app
92
DEVELOPMENT
Получаем координаты
93
DEVELOPMENT
Управляем картами
94
DEVELOPMENT
Управляем картами
Для чего использовать?
Управлять картой из общего кода
95
DEVELOPMENT
Управляем картами
class MarkerViewModel(
val mapsController: GoogleMapController
) : ViewModel() {
fun start() {
viewModelScope.launch {
val marker1 = mapsController.addMarker(
image = MR.images.marker,
latLng = LatLng(
latitude = 55.045853,
longitude = 82.920154
),
rotation = 0.0f
) {
println("marker 1 pressed!")
}
marker1.rotation = 90.0f
}
}
}
96
DEVELOPMENT
Управляем картами
viewModelScope.launch {
val route = mapsController.buildRoute(
points = listOf(
LatLng(55.032200,82.889360),
LatLng(55.013109,82.926480)
),
lineColor = Color(0xCCCC00FF),
markersImage = MR.images.marker
)
}
97
DEVELOPMENT
Управляем картами
interface MapController {
fun showMyLocation(zoom: Float)
fun showLocation(...)
suspend fun getMapCenterLatLng(): LatLng
suspend fun addMarker(...): Marker
suspend fun buildRoute(...): MapElement
suspend fun drawPolygon(...): MapElement
suspend fun getAddressByLatLng(...): String?
suspend fun getSimilarNearAddresses(...): List<MapAddress>
suspend fun getCurrentZoom(): Float
suspend fun setCurrentZoom(zoom: Float)
suspend fun getZoomConfig(): ZoomConfig
suspend fun setZoomConfig(config: ZoomConfig)
}
98
DEVELOPMENT
Управляем картами
99
DEVELOPMENT
Строим списки в UI
100
DEVELOPMENT
Строим списки в UI
Для чего использовать?
Для управления контентом RecyclerView/UITableView/UICollectionView из общего кода
101
DEVELOPMENT
Строим списки в UI
interface UnitFactory {
fun createHeader(text: String): TableUnitItem
fun createProfileTile(profileId: Long, username: String): TableUnitItem
}
102
DEVELOPMENT
Строим списки в UI
class ViewModel(unitFactory: UnitFactory) {
val items: List<TableUnitItem> = listOf(
unitFactory.createHeader("Programmers"),
unitFactory.createProfileTile(1, "Mikhailov"),
unitFactory.createProfileTile(2, "Babenko"),
unitFactory.createProfileTile(3, "Tchernov"),
unitFactory.createHeader("Designers"),
unitFactory.createProfileTile(4, "Eugeny")
)
}
103
DEVELOPMENT
Строим списки в UI
<androidx.recyclerview.widget.RecyclerView
android:layout_width="match_parent"
android:layout_height="match_parent"
app:bindValue="@{viewModel.items}"
app:adapter="@{`dev.icerock.moko.units.adapter.UnitsRecyclerViewAdapter`}"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"/>
android-app
104
DEVELOPMENT
Строим списки в UI
let viewModel = ViewModel(unitFactory: UnitFactoryImpl())
let dataSource = TableUnitsSourceKt.default(for: tableView)
dataSource.unitItems = viewModel.items
ios-app
105
DEVELOPMENT
Строим списки в UI
object UnitFactoryImpl: UnitFactory {
fun createHeader(text: String): TableUnitItem {
return LayoutHeader()
.setText(text)
.setItemId(text.hashCode())
}
fun createProfileTile(profileId: Long, username: String): TableUnitItem {
return LayoutProfileTile()
.setUserName(username)
.setItemId(profileId)
}
}
android-app
106
DEVELOPMENT
Строим списки в UI
class UnitFactoryImpl: NSObject, UnitFactory {
func createHeader(text: String) -> TableUnitItem {
let data = HeaderTableViewCell.CellModel(text: text)
return UITableViewCellUnit<HeaderTableViewCell>(data: data, itemId: text.hashCode(), configurator: nil)
}
func createProfileTile(profileId: Long, username: String) -> TableUnitItem {
let data = ProfileTableViewCell.CellModel(username: username)
return UITableViewCellUnit<ProfileTableViewCell>(data: data, itemId: profileId, configurator: nil)
}
}
ios-app
107
DEVELOPMENT
Строим списки в UI
108
DEVELOPMENT
Шлем события по сокетам
109
DEVELOPMENT
Шлем события по сокетам
Для чего использовать?
Для передачи данных по Socket.IO протоколу
110
DEVELOPMENT
Шлем события по сокетам
val socket = Socket(
endpoint = "https://my-super-server:8080",
config = SocketOptions(
queryParams = mapOf("token" to "MySuperToken"),
transport = SocketOptions.Transport.WEBSOCKET
)
) {
on("employee.connected") { data ->
val serializer = DeliveryCar.serializer()
val json = JSON.nonstrict
val deliveryCar: DeliveryCar = json.parse(serializer, data)
//...
emit("car.placed", deliveryCar.name)
}
}
111
DEVELOPMENT
Шлем события по сокетам
val socket = Socket(
endpoint = "https://my-super-server:8080",
config = SocketOptions(
queryParams = mapOf("token" to "MySuperToken"),
transport = SocketOptions.Transport.WEBSOCKET
)
) {
on("employee.connected") { data ->
val serializer = DeliveryCar.serializer()
val json = JSON.nonstrict
val deliveryCar: DeliveryCar = json.parse(serializer, data)
//...
emit("car.placed", deliveryCar.name)
}
}
112
DEVELOPMENT
Шлем события по сокетам
val socket = Socket(
endpoint = "https://my-super-server:8080",
config = SocketOptions(
queryParams = mapOf("token" to "MySuperToken"),
transport = SocketOptions.Transport.WEBSOCKET
)
) {
on("employee.connected") { data ->
val serializer = DeliveryCar.serializer()
val json = JSON.nonstrict
val deliveryCar: DeliveryCar = json.parse(serializer, data)
//...
emit("car.placed", deliveryCar.name)
}
}
113
DEVELOPMENT
Шлем события по сокетам
val socket = Socket(
endpoint = "https://my-super-server:8080",
config = SocketOptions(
queryParams = mapOf("token" to "MySuperToken"),
transport = SocketOptions.Transport.WEBSOCKET
)
) {
on("employee.connected") { data ->
val serializer = DeliveryCar.serializer()
val json = JSON.nonstrict
val deliveryCar: DeliveryCar = json.parse(serializer, data)
//...
emit("car.placed", deliveryCar.name)
}
}
114
DEVELOPMENT
Шлем события по сокетам
115
DEVELOPMENT
Оповещаем об ошибках
116
DEVELOPMENT
Оповещаем об ошибках
Для чего использовать?
Для удобного и простого отображения ошибок пользователю
117
DEVELOPMENT
Оповещаем об ошибках
class SimpleViewModel(val exceptionHandler: ExceptionHandler) : ViewModel() {
fun onAlertButtonClick() {
viewModelScope.launch {
exceptionHandler.handle {
serverRequest()
}.catch<CustomException> {
println("Got CustomException!")
false
}.finally {
println("complete")
}.execute()
}
}
}
118
DEVELOPMENT
Оповещаем об ошибках
class SimpleViewModel(val exceptionHandler: ExceptionHandler) : ViewModel() {
fun onAlertButtonClick() {
viewModelScope.launch {
exceptionHandler.handle {
serverRequest()
}.catch<CustomException> {
println("Got CustomException!")
false
}.finally {
println("complete")
}.execute()
}
}
}
119
DEVELOPMENT
Оповещаем об ошибках
SimpleViewModel(
exceptionHandler = ExceptionHandler(
errorPresenter = AlertErrorPresenter(
alertTitle = MR.strings.alertTitle.desc(),
positiveButtonText = MR.strings.okButton.desc()
),
exceptionMapper = ExceptionMappersStorage.throwableMapper(),
onCatch = {
println("Got exception: $it")
}
)
)
120
DEVELOPMENT
Оповещаем об ошибках
SimpleViewModel(
exceptionHandler = ExceptionHandler(
errorPresenter = AlertErrorPresenter(
alertTitle = MR.strings.alertTitle.desc(),
positiveButtonText = MR.strings.okButton.desc()
),
exceptionMapper = ExceptionMappersStorage.throwableMapper(),
onCatch = {
println("Got exception: $it")
}
)
)
121
DEVELOPMENT
Оповещаем об ошибках
SimpleViewModel(
exceptionHandler = ExceptionHandler(
errorPresenter = AlertErrorPresenter(
alertTitle = MR.strings.alertTitle.desc(),
positiveButtonText = MR.strings.okButton.desc()
),
exceptionMapper = ExceptionMappersStorage.throwableMapper(),
onCatch = {
println("Got exception: $it")
}
)
)
122
DEVELOPMENT
Оповещаем об ошибках
SimpleViewModel(
exceptionHandler = ExceptionHandler(
errorPresenter = AlertErrorPresenter(
alertTitle = MR.strings.alertTitle.desc(),
positiveButtonText = MR.strings.okButton.desc()
),
exceptionMapper = ExceptionMappersStorage.throwableMapper(),
onCatch = {
println("Got exception: $it")
}
)
)
123
DEVELOPMENT
Оповещаем об ошибках
124
DEVELOPMENT
Анализируем ошибки
125
DEVELOPMENT
Анализируем ошибки
Для чего использовать?
Для отправки fatal/non-fatal ошибок с корректным стектрейсом
126
DEVELOPMENT
Анализируем ошибки
val logger = CrashlyticsLogger()
val antilog = CrashReportingAntilog(exceptionLogger = logger)
Napier.base(antilog = antilog)
127
DEVELOPMENT
Анализируем ошибки
Napier.i(message = "This is random message")
Napier.e(message = "create order failed", throwable = exception)
128
DEVELOPMENT
Общий код для UI
129
DEVELOPMENT
Общий код для UI
Для чего использовать?
Для разработки iOS и Android приложений
полностью из общего кода
130
DEVELOPMENT
Общий код для UI
131
DEVELOPMENT
Общий код для UI
132
DEVELOPMENT
Общий код для UI
133
DEVELOPMENT
Общий код для UI
134
DEVELOPMENT
Общий код для UI
135
DEVELOPMENT
Общий код для UI
136
DEVELOPMENT
Общий код для UI
137
DEVELOPMENT
138
DEVELOPMENT
139
DEVELOPMENT
Ссылки
Спасибо за внимание
140
DEVELOPMENT
moko-units
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable name="text" type="String" />
</data>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@{text}" />
</layout>
android-app (with databinding)
141
DEVELOPMENT
moko-units
actual class BasicTableUnitItem actual constructor(
override val itemId: Long, private val title: String
) : TableUnitItem {
override val viewType: Int = R.layout.basic_table_unit
override fun bindViewHolder(viewHolder: RecyclerView.ViewHolder) {
(viewHolder as ViewHolder).title.text = title
}
override fun createViewHolder(...): RecyclerView.ViewHolder {...}
private class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
val title: TextView = view.findViewById(R.id.title)
}
}
android-app (without databinding)
142
DEVELOPMENT