На Инфостарте есть несколько публикаций на тему создания приложений для Android и его связке с 1С через Web-сервис. Но на дворе сейчас конец 2017 года, и пришла пора освежить свои навыки.
Во-первых, с выходом Android Studio 3.0.0 в корпорации Google уже окончательно определились с будущим основным языком программирования, и это будет Kotlin. В данной публикации будем использовать именно этот язык. Он совместим с Java и с переходом у вас никаких проблем не будет, получите только позитивные эмоции.
Во-вторых, не стоит вкладывать свои силы в разработку Web-сервисов. Этому есть несколько причин:
- На платформе 1С 8.3 им на смену пришли HTTP-сервисы
- Протокол SOAP, используемый веб-сервисами дорого обходится и для сервера, и для клиента
- Для платформы Android нет хорошей библиотеки для работы с SOAP (по крайней мере бесплатных). Библиотека ksoap2 хороша ровно до того момента, когда вы от простых примеров перейдёте к реальным задачам и на сложных структурах данных поймаете Double ID Exception, для лечения которого надо обладать исключительными знаниями протокола, схемы XML-разметок, рыться в исходниках библиотеки. Оно вам надо? Парсинг полученных данных в ksoap - это отдельный ад программиста
- Для создания клиента HTTP-сервисов существуют замечательные библиотеки, в том числе для платформы Android
- В конце концов, при помощи HTTP-сервиса в 1С вы можете сами написать собственную реализацию протокола SOAP
В данной публикации я не буду подробно рассматривать создание HTTP-сервиса, т.к. на эту тему уже есть публикации, например эти: HTTP-сервисы для тех, кто ничего не понимает в WEB и HTTP-сервисы в 1С Предприятие 8.3 . В качестве упражнения, руководствуясь этими публикациями, создайте самостоятельно HTTP-сервис с базовым URL "wms" и шаблоном /tables/{ИмяТаблицы}, возвращающий список складов в формате JSON. Формат JSON лучше XML хотя бы тем, что в нём меньше букв. Код модуля сервиса должен у вас получиться примерно такой:
Функция ТаблицыПолучить(Запрос)
ИмяТаблицы = Запрос.ПараметрыURL["ИмяТаблицы"];
Данные = Новый ЗаписьJSON;
Данные.ПроверятьСтруктуру = Ложь;
Данные.УстановитьСтроку(Новый ПараметрыЗаписиJSON(,Символы.Таб));
Данные.ЗаписатьНачалоОбъекта();
Данные.ЗаписатьИмяСвойства(ИмяТаблицы);
Данные.ЗаписатьНачалоМассива();
Запрос = Новый Запрос;
Запрос.Текст = "ВЫБРАТЬ
| Склады.Код,
| Склады.Наименование КАК Наименование
|ИЗ
| Справочник.Склады КАК Склады
|ГДЕ
| Склады.ПометкаУдаления = ЛОЖЬ
| И Склады.ЭтоГруппа = ЛОЖЬ";
Выборка = Запрос.Выполнить().Выбрать();
Пока Выборка.Следующий() Цикл
Данные.ЗаписатьНачалоОбъекта();
Данные.ЗаписатьИмяСвойства("id");
Данные.ЗаписатьЗначение(Выборка.Код);
Данные.ЗаписатьИмяСвойства("name");
Данные.ЗаписатьЗначение(Выборка.Наименование);
Данные.ЗаписатьИмяСвойства("isFifo");
Данные.ЗаписатьЗначение(Ложь);
Данные.ЗаписатьКонецОбъекта();
КонецЦикла;
Данные.ЗаписатьКонецМассива();
Данные.ЗаписатьКонецОбъекта();
Тело = Данные.Закрыть();
Ответ = Новый HTTPСервисОтвет(200);
Ответ.УстановитьТелоИзСтроки(Тело, КодировкаТекста.UTF8);
Возврат Ответ;
КонецФункции
Точно так же как и для Web-сервисов, я не рекомендую вам писать бизнес-логику в модуле HTTP-сервиса, так как в нём отсутствует проверка кода на ошибки. Я вам привел плохой пример исключительно для простоты изложения. Вместо этого старайтесь максимально переносить свой код в общие модули.
В браузере вы должны получить от сервиса такую структуру данных в формате JSON, где "stores" - это параметр, передаваемый в адресной строке вместо {ИмяТаблицы}, сохраните её в блокноте, пригодится нам в дальнейшем:
{
"stores": [
{
"id": "000000008",
"name": "Изолятор брака",
"isFifo": false
},
{
"id": "000000007",
"name": "Производственный склад",
"isFifo": false
},
{
"id": "000000002",
"name": "Склад готовой продукции",
"isFifo": false
} ]
}
А теперь переходим к клиенту: в Android Studio 3 создайте новый проект, включите поддержку языка Kotlin. Укажите минимальный SDK 25 уровня. В файле build.gradle вашего проекта добавьте следующие зависимости:
dependencies {
// эти зависимости уже могут быть в вашем проекте, их не трогайте:
implementation fileTree(include: ["*.jar"], dir: "libs")
implementation "com.android.support:appcompat-v7:26.+"
implementation "com.android.support:design:26.+"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version"
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
// а эти зависимости вы добавляете сами:
implementation "com.squareup.retrofit2:retrofit:2.+"
implementation "com.squareup.retrofit2:converter-gson:2.+"
implementation "org.jetbrains.anko:anko-sdk25:$anko_version"
implementation "org.jetbrains.anko:anko-sdk25-listeners:$anko_version"
implementation "org.jetbrains.anko:anko-commons:$anko_version"
}
Последними строками в файле конфигурации вы добавляете библиотеку Retrofit и парсер GSON, который нам понадобиться в проекте, а также очень полезные расширения языка Kotlin для Android под названием Anko - с ним ваш код будет еще более кратким и понятным.
Google рекомендует бизнес-логику приложения выносить в класс, наследуемый от Service, поэтому создайте простейший сервис примерно такого содержания:
package test.App
import android.app.Service
import android.content.Intent
import android.os.Binder
import android.os.IBinder
import okhttp3.OkHttpClient
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
class BLService : Service() {
private val mUrl = "http://server/Database1c/hs/wms" // Базовый URL в 1С указан как wms
lateinit private var rf: Retrofit // Объект, который содержит все настройки нашего
// соединения с сервером и выполняет всю работу
lateinit private var wms: WmsApi // Это API нашего HTTP-сервиса, напишем его позже
private var binder = BLBinder() // Нужен для доступа к сервису из любой Activity
inner class BLBinder : Binder() {
fun getService(): BLService? {
return this@BLService
}
}
override fun onCreate() {
super.onCreate()
val okHttpClient = OkHttpClient.Builder()
.addInterceptor(BasicAuthInterceptor("Иванов", "СуперПароль"))
// напишем этот Interceptor позже, нужен для авторизации в 1С
.build()
rf = Retrofit.Builder()
.baseUrl(mUrl)
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create())
.build()
wms = rf.create(WmsApi::class.java)
}
// обязательные переопределяемые методы:
override fun onBind(intent: Intent?): IBinder {
return binder
}
override fun onUnbind(intent: Intent?): Boolean {
return true
}
}
Не могу удержаться и покажу, как теперь стало просто запускать сервис из главной Activity при помощи расширения языка Anko:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// здесь может быть еще код
// запуск сервиса с помощью расширения языка Anko:
startService<BLService>()
}
По умолчанию Retrofit не использует авторизацию на HTTP-сервере, но к счастью его легко добавить. Для этого нам необходимо написать класс для авторизации на сервере 1С методом BASIC, так называемый интерцептор, вот его полный код:
package test.App
import okhttp3.Credentials
import okhttp3.Interceptor
import okhttp3.Response
class BasicAuthInterceptor(user: String, password: String) : Interceptor {
private val credentials : String = Credentials.basic(user, password)
override fun intercept(p0: Interceptor.Chain?): Response {
val request = p0!!.request()
val authenticatedRequest = request
.newBuilder()
.header("Authorization", credentials)
.build()
return p0.proceed(authenticatedRequest)
}
}
Не забывайте, что методом BASIC пароли пользователей 1С передаются через сеть открытым текстом, поэтому при боевом развертывании приложения всегда настраивайте веб-сервер на использование только шифрованного протокола HTTPS.
Теперь необходимо написать интерфейс API нашего HTTP-сервиса. Для начала возьмем блокнот и посмотрим на структуру данных, полученную ранее. Этот файл поможет нам создать классы Java, в которые будут завёрнуты наши данные. В случае использования ksoap2 вы бы на этом пункте хорошенько вспотели. Но ничего не бойтесь, с нами Retrofit, поэтому идем на сайт www.jsonschema2pojo.org/ и в левой его части вставляем содержание вашего JSON-пакета. В правой части заполняем как на рисунке:
На основе введеных данных этот сайт бесплатно сгенерирует нам два класса на языке Java в 2 файлах:
-----------------------------------test.App.Store.java-----------------------------------
package test.App;
import com.google.gson.annotations.Expose;
import com.google.gson.annotations.SerializedName;
public class Store {
@SerializedName("id")
@Expose
private String id;
@SerializedName("name")
@Expose
private String name;
@SerializedName("isFifo")
@Expose
private Boolean isFifo;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Boolean getIsFifo() {
return isFifo;
}
public void setIsFifo(Boolean isFifo) {
this.isFifo = isFifo;
}
}
-----------------------------------test.App.Stores.java-----------------------------------
package test.App;
import java.util.List;
import com.google.gson.annotations.Expose;
import com.google.gson.annotations.SerializedName;
public class Stores {
@SerializedName("stores")
@Expose
private List<Store> stores = null;
public List<Store> getStores() {
return stores;
}
public void setStores(List<Store> stores) {
this.stores = stores;
}
}
Добавьте эти 2 файла в свой проект, и через меню Code -> Convert Java file to Kotlin file переведите на язык Kotlin, в результате получатся совсем простые классы, избавленные от геттеров и сеттеров, которые в языке Kotlin вдобавок еще можно объединить в одном файле:
package test.App
import com.google.gson.annotations.Expose
import com.google.gson.annotations.SerializedName
class Store {
@SerializedName("id")
@Expose
var id: String? = null
@SerializedName("name")
@Expose
var name: String? = null
@SerializedName("isFifo")
@Expose
var isFifo: Boolean? = null
}
class Stores {
@SerializedName("stores")
@Expose
var stores: List<Store>? = null
}
Принцип построения этих классов-обёрток думаю теперь вам стал понятен и немного попрактиковавшись вы сможете писать их на языке Kotlin без всяких помощников типа сайта, приведенного выше.
Добавим в наш сервисный класс BLService переменную для хранения массива полученных из 1С данных:
var dbStores : Stores? = null
А теперь переходим непосредственно к написанию интерфейса API. Звучит угрожающе, но на самом деле для нашего HTTP-сервиса это будет такой простой код;
package test.App
import retrofit2.Call
import retrofit2.http.GET
interface WmsApi {
@GET("table/stores")
fun getStores() : Call<Stores>
}
Полный URL в браузере, соответствующий функции getStore() выглядел бы с учётом вышенаписанного кода так: http://server/Database1c/hs/wms/table/stores. Функции в интерфейсе всегда должны возвращать тип Call с указанием получаемого от HTTP-сервиса класса-обертки в угловых скобках. Параметры в строке @GET можно точно так же как в 1С заключать в фигурные скобки и указывать их в параметре функции, например вот так:
interface GitHubService {
@GET("users/{user}/repos")
fun listRepos(@Path("user") user: String) : Call<List<Repo>>
}
Интерфейс API инициализируется в коде нашего класса BLService такой строкой:
wms = rf.create(WmsApi::class.java)
Вы наверное удивитесь, но на этом всё. Теперь вы можете дергать 1С при помощи HTTP-сервиса. Для простоты уберу обработчики исключений и асинхронные штучки, оставив только самую суть:
dbStores = wms.getStores().execute().body()
До скорых встреч!