Android studio

HTTP-запрос (GET и POST) в Kotlin для Android

Добавьте зависимости

В файле build.gradle (Module: app) добавьте следующие зависимости:

dependencies {

    implementation 'com.squareup.retrofit2:retrofit:2.9.0'

    implementation 'com.squareup.retrofit2:converter-gson:2.9.0' // Для работы с JSON

    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4'

    implementation 'com.google.code.gson:gson:2.10.1'

}

Создайте модель данных

package com.coop.myapplication1.retrofit

data class Product(

    val id: Int,

    val device_id: String,

    val name: String,

    val last_seen: String,

    val is_active: Boolean

)

Определите API-интерфейс

Создайте интерфейс для описания запросов:

package com.coop.myapplication1.retrofit

import retrofit2.http.GET

import retrofit2.http.Path

interface ProductApi {

    @GET("device/{id}")

    suspend fun getProductById(@Path("id") id: Int): Product

}

В Main добавим следующий код:

val retrofit = Retrofit.Builder().baseUrl("http://192.168.50.70:8000/api/")

            .addConverterFactory(GsonConverterFactory.create()).build()

        val productApi = retrofit.create(ProductApi::class.java)

        val btn = findViewById<Button>(R.id.button2)

        var result = findViewById<TextView>(R.id.textView2)

        btn.setOnClickListener {

            CoroutineScope(Dispatchers.IO).launch {

                try {

                    val product = productApi.getProductById(1)

                    runOnUiThread {

                        result.text = product?.name ?: "Product not found"

                    }

                } catch (e: Exception) {

                    runOnUiThread {

                        result.text = "Error: ${e.message}"

                    }

                }

            }

        }

Для современных версий Android (API 23+) при использовании HTTP (а не HTTPS) может потребоваться дополнительная настройка для разрешения небезопасных соединений, так как ваш код использует http://192.168.50.70:8000/api/. Для этого нужно:

Создать файл res/xml/network_security_config.xml:

<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
    <domain-config cleartextTrafficPermitted="true">
        <domain includeSubdomains="true">192.168.50.70</domain>
    </domain-config>
</network-security-config>

Итоговый манифест с этой настройкой будет выглядеть так:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">
    <uses-permission android:name="android.permission.INTERNET" />
    <application
        android:allowBackup="true"
        android:dataExtractionRules="@xml/data_extraction_rules"
        android:fullBackupContent="@xml/backup_rules"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.MyApplication1"
        android:networkSecurityConfig="@xml/network_security_config"
        tools:targetApi="31">
        <activity
            android:name=".MainActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
</manifest>

View Binding


Как подключить View Binding
В файле build.gradle (уровень модуля) добавьте:
groovy

android {
    ...
    buildFeatures {
        viewBinding = true
    }
}

В MainActivity вы подключаете View Binding так:
kotlin

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import com.example.myapp.databinding.ActivityMainBinding

class MainActivity : AppCompatActivity() {
    private lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        // Доступ к элементам
        binding.textView.text = "View Binding работает!"
        binding.button.setOnClickListener {
            binding.textView.text = "Кнопка нажата!"
        }
    }
}

SharedPreferences


import android.content.Context
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle

class MainActivity : AppCompatActivity() {
    private val PREFS_NAME = "MyPrefs"
    private val KEY_USERNAME = "username"

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // Получение SharedPreferences
        val prefs = getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
        val editor = prefs.edit()

        // Сохранение данных
        editor.putString(KEY_USERNAME, "Иван")
        editor.apply() // Асинхронно сохраняет данные

        // Чтение данных
        val username = prefs.getString(KEY_USERNAME, "Гость") // "Гость" — значение по умолчанию
        println("Имя пользователя: $username")
    }
}

При наличии конфликта классов:
configurations.all {
    exclude(group = "com.intellij", module = "annotations")
}


Room Database

gradle

plugins {
    alias(libs.plugins.android.application)
    alias(libs.plugins.kotlin.android)
    id("kotlin-kapt")
}

dependencies {
    kapt(libs.androidx.room.compiler)
    implementation(libs.androidx.room.ktx)
   }

Модель данных (Entity):


import androidx.room.Entity
import androidx.room.PrimaryKey

@Entity(tableName = "tasks")
data class Task(
    @PrimaryKey(autoGenerate = true) val id: Int = 0,
    val title: String,
    val isCompleted: Boolean
)

DAO (Data Access Object):

import androidx.room.Dao
import androidx.room.Insert
import androidx.room.Query

@Dao
interface TaskDao {
    @Insert
    suspend fun insert(task: Task)

    @Query("SELECT * FROM tasks")
    suspend fun getAllTasks(): List<Task>

    @Query("DELETE FROM tasks WHERE id = :taskId")
    suspend fun deleteById(taskId: Int)
}

База данных:

import androidx.room.Database
import androidx.room.RoomDatabase

@Database(entities = [Task::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
    abstract fun taskDao(): TaskDao
}

Main

 var db = Room.databaseBuilder(
        applicationContext,
        AppDatabase::class.java,
        "task_database"
    ).build()

    btn.setOnClickListener {
        CoroutineScope(Dispatchers.IO).launch {
            val taskDao = db.taskDao()

            // Добавление задачи
            taskDao.insert(Task(title = "Купить молоко", isCompleted = false))

            // Получение всех задач
            val tasks = taskDao.getAllTasks()

            // Переход на главный поток
            launch(Dispatchers.Main) {
                res.text = tasks.joinToString("\n") { it.toString() }
            }
        }
    }

Работа с JSON


implementation 'com.google.code.gson:gson:2.10.1'

Пример JSON

json

{
    "id": 1,
    "name": "Иван",
    "age": 25
}

Модель данных:


kotlin

data class User(
    val id: Int,
    val name: String,
    val age: Int
)

Парсинг:


kotlin

import com.google.gson.Gson

val json = """{"id": 1, "name": "Иван", "age": 25}"""
val gson = Gson()
val user = gson.fromJson(json, User::class.java)
println("Пользователь: ${user.name}, возраст: ${user.age}")

// Сериализация обратно в JSON
val jsonString = gson.toJson(user)
println("JSON: $jsonString")

Боковое меню:

package com.coop.mytestproject

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import kotlinx.coroutines.launch

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContent {
            AppNavigation()
        }
    }
}

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AppNavigation() {
    val navController = rememberNavController()
    val drawerState = rememberDrawerState(DrawerValue.Closed)
    val scope = rememberCoroutineScope()
    var selectedItem by remember { mutableStateOf(MenuItems[0]) }

    ModalNavigationDrawer(
        drawerState = drawerState,
        drawerContent = {
            ModalDrawerSheet {
                Column(Modifier.verticalScroll(rememberScrollState())) {
                    Spacer(Modifier.height(12.dp))
                    MenuItems.forEach { item ->
                        NavigationDrawerItem(
                            icon = { Icon(item.icon, contentDescription = null) },
                            label = { Text(item.name) },
                            selected = item == selectedItem,
                            onClick = {
                                scope.launch { drawerState.close() }
                                selectedItem = item
                                navController.navigate(item.route) {
                                    popUpTo(navController.graph.startDestinationId)
                                    launchSingleTop = true
                                }
                            },
                            modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding)
                        )
                    }
                }
            }
        },
        content = {
            Scaffold(
                topBar = {
                    TopAppBar(
                        title = { Text("My Project", style = TextStyle(fontSize = 20.sp)) },
                        navigationIcon = {
                            IconButton(onClick = { scope.launch { drawerState.open() } }) {
                                Icon(Icons.Default.Menu, contentDescription = "Menu")
                            }
                        },
                        colors = TopAppBarDefaults.topAppBarColors(
                            containerColor = Color(0xFF00BCD4),
                            titleContentColor = Color.White,
                            navigationIconContentColor = Color.White
                        )
                    )
                },
                content = { padding ->
                    NavHost(
                        navController = navController,
                        startDestination = "main",
                        modifier = Modifier.padding(padding)
                    ) {
                        composable("main") { General() }
                        composable("settings") { Settings() }
                        composable("about") { About() }
                    }
                }
            )
        }
    )
}

@Composable
fun General() {
    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(16.dp),
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center
    ) {
        Text("Main Screen", fontSize = 24.sp)
    }
}

@Composable
fun About() {
    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(16.dp),
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center
    ) {
        Text("About Screen", fontSize = 24.sp)
    }
}

@Composable
fun Settings() {
    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(16.dp),
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center
    ) {
        Text("Settings Screen", fontSize = 24.sp)
    }
}

data class ItemElement(
    val name: String,
    val route: String,
    val icon: ImageVector
)

val MenuItems: List<ItemElement> = listOf(
    ItemElement("Main", "main", Icons.Default.Home),
    ItemElement("Settings", "settings", Icons.Default.Settings),
    ItemElement("About", "about", Icons.Default.Info)
)
build.gradle.kts(Project)
plugins {
      id("com.google.devtools.ksp") version "2.0.20-1.0.24" apply false
}

База данных sqlite


build.gradle.kts(Module)
plugins {
    id("com.google.devtools.ksp")
}
dependencies {
    ksp("androidx.room:room-compiler:2.7.1")
    implementation("androidx.compose.ui:ui:1.8.0")
    implementation("androidx.compose.material:material:1.8.0")
    implementation("androidx.room:room-runtime:2.7.1")
    implementation("androidx.room:room-ktx:2.7.1")    
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.10.2")
    implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.7")
    implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.8.7")
    implementation("androidx.compose.runtime:runtime-livedata:1.8.0")
}

1. Создание сущности (Entity)

import androidx.room.Entity
import androidx.room.PrimaryKey

@Entity(tableName = "record")
data class MyRecord(
    @PrimaryKey(autoGenerate = true)
    val id: Int = 0,
    val title: String,
    val content: String
)

2. DAO-интерфейс

import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.Query
import androidx.room.Update
import kotlinx.coroutines.flow.Flow

@Dao
interface RecordDao {
    @Query("SELECT * FROM record")
    fun getAll(): Flow<List<MyRecord>>

    @Query("SELECT * FROM record WHERE id = :id")
    fun findById(id: Int): MyRecord

    @Update
    suspend fun updateRecord(record: MyRecord)

    @Insert
    suspend fun insertRecord(record: MyRecord)

    @Delete
    suspend fun deleteRecord(record: MyRecord)
}

3. База данных Room

import android.content.Context
import androidx.room.Database
import androidx.room.RoomDatabase
import androidx.room.Room

@Database(entities = [MyRecord::class], version = 1)
abstract class RecordDatabase : RoomDatabase() {
    abstract fun recordDao(): RecordDao
    companion object {
        @Volatile private var INSTANCE: RecordDatabase? = null

        fun getDatabase(context: Context): RecordDatabase {
            return INSTANCE ?: synchronized(this) {
                Room.databaseBuilder(
                    context.applicationContext,
                    RecordDatabase::class.java,
                    "record_db"
                ).build().also { INSTANCE = it }
            }
        }
    }
}

4. ViewModel

TaskViewModel — это ViewModel, которая:
Получает данные из базы данных через DAO (Data Access Object).
Предоставляет данные (список задач) UI через StateFlow.
Выполняет операции добавления и удаления задач асинхронно.

import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch

class RecordViewModel (application: Application) : AndroidViewModel(application) {
    private val dao = RecordDatabase.getDatabase(application).recordDao()

    val records: StateFlow<List<MyRecord>> = dao.getAll()
        .stateIn(viewModelScope, SharingStarted.Lazily, emptyList())

    fun addRecord(title: String, content: String) {
        viewModelScope.launch {
            dao.insertRecord(MyRecord(title = title, content = content))
        }
    }

    fun deleteRecord(record: MyRecord) {
        viewModelScope.launch {
            dao.deleteRecord(record)
        }
    }

    fun updateRecord(record: MyRecord) {
        viewModelScope.launch {
            dao.updateRecord(record)
        }
    }
}

5. Compose UI


class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContent {
            val navController = rememberNavController()
            val context = LocalContext.current
            MyNotepadAppTheme {
                Scaffold(
                    modifier = Modifier.fillMaxSize(),
                    bottomBar = { BottomBar(navController) },
                    contentColor = Color.White,
                    containerColor = Color.Black
                ) { innerPadding ->
                    NavHost(navController = navController, startDestination = "home") {
                        composable("home") {
                            Home(
                                navController = navController,
                                innerPadding = innerPadding,
                                context = context
                            )
                        }
                        composable("add") { Add(innerPadding) }
                        composable("settings") { Settings(innerPadding, context) }
                        composable(
                            "preview/{id}/{title}/{content}",
                            arguments = listOf(
                                navArgument("id") { type = NavType.IntType },
                                navArgument("title") { type = NavType.StringType },
                                navArgument("content") { type = NavType.StringType },
                            )
                        ) { backStackEntry ->
                            val id = backStackEntry.arguments?.getInt("id") ?: 0
                            val title = backStackEntry.arguments?.getString("title") ?: ""
                            val content = backStackEntry.arguments?.getString("content") ?: ""
                            MPreview(
                                id = id,
                                name = title,
                                content = content,
                                innerPadding = innerPadding
                            )
                        }
                    }
                }
            }
        }
    }
}
@Composable
fun Home(
    navController: NavHostController,
    innerPadding: PaddingValues,
    viewModel: RecordViewModel = viewModel(),
    context: Context = LocalContext.current
) {
    var sharedPreferences = context.getSharedPreferences("MyNotes", Context.MODE_PRIVATE)
    var currentPassword by rememberSaveable { mutableStateOf(sharedPreferences.getString("password", "0000").toString()) }
    var inputPassword by rememberSaveable { mutableStateOf("") }
    var rootPassword by rememberSaveable { mutableStateOf(sharedPreferences.getString("root", "true").toString()) }
    var statusPassword by rememberSaveable { mutableStateOf(false) }
    Log.d("Root", sharedPreferences.getString("root", "true").toString())

    if(sharedPreferences.getString("root", "true").toBoolean()==true) {
        statusPassword = true
    }
    if(statusPassword) {
        AlertDialog(
            onDismissRequest = { statusPassword = false },
            title = { Text(text = "Password") },
            text = {
            TextField(
                value = inputPassword,
                onValueChange = { inputPassword = it },
                modifier = Modifier.fillMaxWidth(),
                label = { Text("Password") }
            )},
            confirmButton = {
                TextButton(
                    onClick = {
                        statusPassword = false
                        if(inputPassword == currentPassword) {
                            sharedPreferences.edit { putString("root", "false") }
                            rootPassword = "true"
                        }
                    }
                ) {
                    Text(text = "Confirm")
                }
            },
            dismissButton = {
                TextButton(
                    onClick = { statusPassword = false }
                ) {
                    Text(text = "Cancel")
                }
            }
        )
    }
    val records by viewModel.records.collectAsState()

    Column(
        Modifier
            .padding(innerPadding)
            .padding(start = 10.dp, end = 10.dp)
    ) {
        LazyColumn {
            items(records, key = { it.id }) { record ->
                TaskItem(
                    navController = navController,
                    record = record,
                    onDelete = { viewModel.deleteRecord(it) }
                )
            }
        }
    }
}


@Composable
fun TaskItem(
    navController: NavHostController,
    record: MyRecord,
    onDelete: (MyRecord) -> Unit
) {
    val openDialog = remember { mutableStateOf(false) }
    Row(
        modifier = Modifier
            .fillMaxWidth()
            .padding(vertical = 4.dp)
            .clickable {
                navController.navigate(
                    "preview/${record.id}/${
                        URLEncoder.encode(record.title, "UTF-8")
                    }/${URLEncoder.encode(record.content, "UTF-8")}"
                )
            },
        horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically
    ) {
        Text(text = record.title)
        IconButton(onClick = { openDialog.value = true }) {
            Icon(imageVector = Icons.Default.Delete, contentDescription = "Удалить")
        }
    }

    if (openDialog.value) {
        AlertDialog(
            onDismissRequest = { openDialog.value = false },
            title = { Text("Подтвердите удаление") },
            text = { Text("Вы действительно хотите удалить запись?") },
            confirmButton = {
                TextButton(onClick = {
                    openDialog.value = false
                    onDelete(record)
                }) {
                    Text("Удалить")
                }
            },
            dismissButton = {
                TextButton(onClick = { openDialog.value = false }) {
                    Text("Отменить")
                }
            }
        )
    }
}

Как установить buildozer для kivy

sudo apt update sudo apt install python3 python3-pip pip3 install --user --upgrade buildozer sudo apt install -y git zip unzip openjdk-17-jd...