Разработчик собрал на KMP клиент для Ollama: два бага сериализации и стриминга
Разработчик показал, как подключить мобильное приложение на Kotlin Multiplatform к локальному серверу Ollama: общий UI на Compose, Koin для зависимостей, Ktor для сети. Сервер открывается для телефонов в сети одной переменной OLLAMA_HOST=0.0.0.0. Основное время ушло на два незаметных бага: пропадающее поле stream и ответ, который приходит целиком вместо потока. Оба решаются парой строк, если знать, где искать.
AI-обработка оригинала Habr AI; редакция Hamidun News
Запустить локальную модель на ноутбуке сегодня просто. Сложнее сделать к ней нормальный мобильный клиент, который работает и на Android, и на iPhone, и не заставляет писать UI дважды. Автор статьи на Habr прошёл этот путь на Kotlin Multiplatform и честно описал, где споткнулся. Для тех, кто собирается делать то же самое, это полезнее любой теории.
Сервер: три команды и одна переменная
Ollama ставится через brew или установочный скрипт на Ubuntu. По умолчанию сервер слушает только localhost, поэтому эмулятор и реальный телефон в одной сети до него не достучатся. Чтобы открыть доступ, сервер запускают с OLLAMA_HOST=0.0.0.0:11434. Дальше модель загружается через ollama pull, а на маках с чипами Apple автор берёт MLX-сборки, например gemma4:12b-mlx. Команда ollama ps показывает, что модель целиком сидит в памяти GPU (в выводе PROCESSOR 100% GPU).
Проверить сервер можно обычным curl на /api/generate. Если в запросе стоит stream: false, ответ придёт одним JSON.
Клиент: Compose, Koin, Ktor
Проект на KMP с общим UI: Koin отвечает за зависимости и ViewModel, Ktor за сеть, Navigation3 за навигацию. Автор выбрал не глобальный startKoin, а composable KoinApplication. Контейнер создаётся внутри функции App(), поэтому @Preview в Android Studio не конфликтует с глобальным состоянием, а Swift-код, который сгенерировала студия, трогать не нужно. Цена такого решения: часть API недоступна, в том числе by inject(), by viewModel() и androidContext(). Как правильно разбивать на модули большое приложение при таком подходе, автор сам пока не выяснил.
Xcode и macOS для сборки под iOS всё равно нужны. KMP избавляет от UI на Swift, но не от инструментов Apple.
Две ловушки, которые стоили часов
Первая ловушка связана с сериализатором. Kotlinx.Serialization по умолчанию не пишет в JSON поля со значениями по умолчанию. Если у вас stream = false, поле просто пропадает из запроса, а Ollama без него включает потоковый режим. В итоге вы ждёте один ответ, а получаете россыпь JSON-строк. Лечится настройкой Json { encodeDefaults = true }. Найти причину помог Network Inspector в Android Studio.
Вторая ловушка касается самого стриминга. Ответ приходил целиком в конце, и автор сначала подозревал буферизацию в OkHttp и даже разнёс код по платформам через expect/actual. Дело оказалось в другом: вместо post() нужно использовать preparePost() и читать тело через execute и bodyAsChannel построчно. Каждая строка это отдельный JSON-чанк с полями message, done и error. В клиенте всё это превращается в Flow событий: Token, Thinking, Done и Error. Отмена корутины закрывает соединение и останавливает генерацию. Отдельно приходят токены рассуждений (Thinking), если модель их отдаёт.
Ещё одна деталь: для /api/chat роль у каждого сообщения обязательна, а у /api/generate ролей нет, там просто prompt.
Что учесть на практике
Если сервер работает по HTTP, на Android нужен usesCleartextTraffic="true", а для iOS придётся добавить NSAllowsArbitraryLoads в Info.plist. Правда, эмулятор iPhone в эксперименте автора пропустил запросы и без этого. В продакшене HTTP без шифрования и сервер на 0.0.0.0 без авторизации использовать нельзя: в тексте про защиту нет ни слова, поэтому это домашний или внутренний стенд, а не готовая архитектура.
Проект пока сырой: интерфейс нужно доводить, истории чатов нет. Автор добавляет, что клиент для облачных моделей cloud.ru с картинками и аудио уже написан. Задача по переименованию проекта заняла у модели около пяти минут и примерно 50 рублей, но цена сильно зависит от выбранной модели.
Вывод для разработчика: если делаете мобильный клиент к Ollama, сразу включайте encodeDefaults, сразу берите preparePost для потоков и не ищите проблему в OkHttp. Исходники лежат на GitHub (app-z/OllameChatKmp), их можно взять как стартовую заготовку.
- 12 нояб. 2026 г.n8n выпустила Nod — open-source агента, который работает на вашем компьютере
- 12 нояб. 2026 г.Claude Code и Codex режут реверс-инжиниринг: OpenAI банила аккаунты по ошибке
- 12 нояб. 2026 г.QA-инженеры SpaceWeb поручили ИИ писать автотесты: рабочий скрипт за 50 минут
- 12 нояб. 2026 г.MiniMax открыла веса H3-Image: 4K-картинки за 10–16 секунд и 16 референсов
Вопросы и ответы по материалу
Что известно по теме: сервер: три команды и одна переменная?
Ollama ставится через brew или установочный скрипт на Ubuntu. По умолчанию сервер слушает только localhost, поэтому эмулятор и реальный телефон в одной сети до него не достучатся. Чтобы открыть доступ, сервер запускают с OLLAMA_HOST=0.0.0.0:11434. Дальше модель загружается через ollama pull, а на маках с чипами Apple автор берёт MLX-сборки, например gemma4:12b-mlx. Команда ollama ps показывает, что модель целиком сидит в памяти GPU (в выводе PROCESSOR 100% GPU).
Что известно по теме: клиент: Compose, Koin, Ktor?
Проект на KMP с общим UI: Koin отвечает за зависимости и ViewModel, Ktor за сеть, Navigation3 за навигацию. Автор выбрал не глобальный startKoin, а composable KoinApplication. Контейнер создаётся внутри функции App(), поэтому @Preview в Android Studio не конфликтует с глобальным состоянием, а Swift-код, который сгенерировала студия, трогать не нужно. Цена такого решения: часть API недоступна, в том числе by inject(), by viewModel() и androidContext().
Что известно по теме: две ловушки, которые стоили часов?
Первая ловушка связана с сериализатором. Kotlinx.Serialization по умолчанию не пишет в JSON поля со значениями по умолчанию. Если у вас stream = false, поле просто пропадает из запроса, а Ollama без него включает потоковый режим. В итоге вы ждёте один ответ, а получаете россыпь JSON-строк. Лечится настройкой Json { encodeDefaults = true }. Найти причину помог Network Inspector в Android Studio. Вторая ловушка касается самого стриминга.
Протестируйте новые LLM и мультимодальные модели в одном окне
Платформа Nomad AI (nomadnet.ai) даёт прямой доступ к Claude Opus 5.5, GPT-6, Gemini 3.1, DeepSeek и автономным агентам без VPN, зарубежных карт и зоопарка подписок.
Главное из мира ИИ — раз в неделю
7 ключевых событий недели, отобранных вручную. Без шума, репостов и пресс-релизов.
Готово! Проверьте почту — мы отправили подтверждение.