使用Kotlin构建基本的Android CRUD应用

本教程详细介绍了如何使用Kotlin和Spring Boot创建一个Android CRUD应用,涵盖了从设置API到实现安全的身份验证。通过Okta,您将学习如何添加OAuth 2.0授权,确保移动应用的安全性。文中还提供了完整的代码示例,帮助读者掌握Android开发中的Kotlin编程技巧和最佳实践。
摘要由CSDN通过智能技术生成

“我喜欢编写身份验证和授权代码。” 〜从来没有Java开发人员。 厌倦了一次又一次地建立相同的登录屏幕? 尝试使用Okta API进行托管身份验证,授权和多因素身份验证。

Google最近为Kotlin授予了Android官方支持状态,但对于许多开发人员而言,它仍然很难理解。 最好的开始方法是自己创建一个完整的应用程序,这将在本教程中进行。 在本教程中,您将使用Spring Boot作为支持Android(+ Kotlin)移动应用程序的API。 Spring Boot是用最少的代码创建健壮的REST API的好方法。

我将假设您具有Java经验,并且至少在创建Android应用方面有过经验。 如果您没有任何Android经验,则应该可以继续学习,但您可能需要在Google那里到那里做一些事情。

如果您想直接到最后,这里是完整的代码

在开始之前,让我们先谈谈Kotlin。

Kotlin与Java

Kotlin对新来者看起来很奇怪。 它类似于您可能已经见过的其他语言,但是有些事情看起来不对劲,通常是因为它是如此的简洁!

不要惊慌-因为它是如此的可扩展,所以有很多方法可以编写相同的代码,并且许多其他语言都没有捷径。 例如,通常您会看到大括号用作函数参数:

dialogBuilder.setPositiveButton("Delete", { dialog, whichButton ->
    deleteMovie(movie)
})

这实际上是在创建一个匿名函数( lambda )并将其传递。此函数接受两个在此推断出的参数。 看一看等效的(JRE 8之前的)Java代码:

dialogBuilder.setPositiveButton("Delete",
    new DialogInterface.OnClickListener() {
        public void onClick(DialogInterface dialog, int which) {
            deleteMovie(movie);
        }
    }
);

(当然,现在Java 8也具有lambdas )。

这是我们稍后将使用的一些代码的另一个示例:

class MovieViewHolder(val view: View) : RecyclerView.ViewHolder(view)

为了理解这一点,您必须了解几件事:

用括号声明一个类(即(view: View) )意味着您正在声明该类的主要构造函数 (是的,也有次要构造函数 )。 冒号:implementsextends相似,但实际上是关于接口的 。 在主构造函数中声明的所有内容都会自动声明为属性(成员变量)。

为了清楚起见,这是等效的Java:

public static class MovieViewHolder extends RecyclerView.ViewHolder {
    public final View view;
    public MovieViewHolder(View v) {
        super(v);
        view = v;
   }
}

作为最后一个示例,请看以下bean:

package demo

data class Movie( val id: Int, val name: String )

那是完整的文件。 它声明一个带有构造函数的类,两个只读属性(成员变量),并在构造函数中分配这些属性。 然后, data会为我们的所有成员变量以及equals()toString() 和其他变量创建getter和setter方法( 请参阅此处 ,是否要在完整的Java荣耀中看到它)。

现在您已经有了一些背景,让我们开始吧!

为您的Android + Kotlin项目创建Spring Boot API

官方的Spring Boot教程建议您使用Initializr网站创建启动框架,但是我发现从头开始构建项目会更容易。

首先,使用Gradle初始化一个空目录(确保已安装Gradle并在命令行上可用)。

C:\Users\Karl\Kotlin-Spring>gradle init

BUILD SUCCESSFUL in 3s
2 actionable tasks: 2 executed
C:\Users\Karl\Kotlin-Spring>

您应该有两个文件夹和四个文件。

.
+-- build.gradle
+-- gradle
¦   L-- wrapper
¦       +-- gradle-wrapper.jar
¦       L-- gradle-wrapper.properties
+-- gradlew
+-- gradlew.bat
L-- settings.gradle

2 directories, 6 files

现在将build.gradle更改为以下内容:

buildscript {
    ext.kotlin_version = '1.2.61' // Required for Kotlin integration
    ext.spring_boot_version = '2.0.2.RELEASE'
    repositories {
        jcenter()
    }
    dependencies {
        // Required for Kotlin integration
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
        // See https://kotlinlang.org/docs/reference/compiler-plugins.html#kotlin-spring-compiler-plugin         
        classpath "org.jetbrains.kotlin:kotlin-allopen:$kotlin_version"
        classpath("org.jetbrains.kotlin:kotlin-noarg:$kotlin_version")
        classpath "org.springframework.boot:spring-boot-gradle-plugin:$spring_boot_version"
    }
}

// Required for Kotlin integration
apply plugin: 'kotlin'
// See https://kotlinlang.org/docs/reference/compiler-plugins.html#kotlin-spring-compiler-plugin
apply plugin: "kotlin-spring" 
apply plugin: 'kotlin-jpa'
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'

jar {
    baseName = 'kotlin-demo'
    version = '0.1.0-SNAPSHOT'
}

repositories {
    jcenter()
}

dependencies {
    // Required for Kotlin integration
    compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
    compile "org.jetbrains.kotlin:kotlin-reflect" // For reflection
    compile 'org.springframework.boot:spring-boot-starter-data-rest'
    compile 'org.springframework.boot:spring-boot-starter-data-jpa'
    compile 'com.h2database:h2'
}

这里导入了Kotlin和Spring Boot插件,声明了外部存储库,并添加了依赖库。

如果您还没有使用过Spring Boot,那么您应该知道它(或者说是Spring Framework)在运行时使用了依赖注入。 这意味着将根据您导入的库自动连接整个应用程序。 例如,在build.gradle的结尾,您将看到Data REST和Data JPA库。 当Spring Boot看到两个库时,它将自动将您的应用程序配置为REST服务器。 此外,由于包含了H2数据库库,因此Spring将使用H2数据库引擎来持久化我们传入或传出查询的任何REST数据。

拥有一个完整的REST应用程序所需要的就是定义一个带有@SpringBootApplication批注的类。 您甚至不需要指定它的路径-Spring会搜索它!

将以下内容放入src/main/kotlin/demo/Application.kt

package demo

import org.springframework.boot.SpringApplication
import org.springframework.boot.autoconfigure.SpringBootApplication

@SpringBootApplication
class Application

fun main(args: Array) {
    SpringApplication.run(Application::class.java, *args)
}

现在,如果您运行gradlew bootRun (在* nix上运行./gradlew bootRun ),则所有内容都应构建(并下载),并且您应该在巨大的日志Started Application中看到某处。 现在,在另一个窗口中运行curl以查看发生了什么。

C:\Users\Karl>curl localhost:8080
{
  "_links" : {
    "profile" : {
      "href" : "http://localhost:8080/profile"
    }
  }
}

令人惊讶的是,您已经通过Kotlin创建了一个完全兼容的REST服务器,只需编辑两个文件即可!

使用Kotlin添加对象

要创建对象,您只需要实体类和存储库。

Application.kt旁边,将以下内容放入Model.kt

package demo

import javax.persistence.*

@Entity
data class Movie(@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
                 val Id: Long,
                 val name: String)

在这里,您已使用数据惯用法为所有属性创建获取器和设置器,以及使用JPA批注指定如何为您的实体生成ID。

注意: Id字段必须以大写字母I开头。 如果不是,则服务器在执行查询时不会返回id字段。 在连接到客户端应用程序时,这会给您带来麻烦。

现在将其放入Repository.kt

package demo

import org.springframework.data.repository.CrudRepository

interface ItemRepository : CrudRepository<Movie, Long>

您已经完成了! 令人难以置信的是,我们现在可以在此服务器上执行任何CRUD操作,并且它将对数据库进行所有更改,并且可以正常运行。

C:\Users\Karl>curl -X POST -H "Content-Type:application/json" -d " {\"name\":\"The 40 Year Old Virgin\"} " localhost:8080/movies
{
  "name" : "The 40 Year Old Virgin",
  "_links" : {
    "self" : {
      "href" : "http://localhost:8080/movies/1"
    },
    "item" : {
      "href" : "http://localhost:8080/movies/1"
    }
  }
}
C:\Users\Karl>curl localhost:8080/movies/1
{
  "name" : "The 40 Year Old Virgin",
  "_links" : {
    "self" : {
      "href" : "http://localhost:8080/movies/1"
    },
    "item" : {
      "href" : "http://localhost:8080/movies/1"
    }
  }
}

在您的Kotlin应用程序中加载初始数据

最后,让我们加载一些数据。 同样,与Spring Boot一样,一切都可以轻松完成。 只需将以下内容放入src/main/resources/data.sql ,它将在启动时运行。

INSERT INTO movie (name) VALUES
  ('Skyfall'),
  ('Casino Royale'),
  ('Spectre');

要确认它是否正常运行,请重新启动服务器并运行curl localhost:8080/movies

后端就完成了。 是时候建立客户了。

使用Kotlin构建您的Android应用

这将需要几个步骤:首先,您将使用Android Studio创建一个空的Kotlin应用程序。 然后,您将使用RecyclerView创建一个列表视图(带有添加,编辑和删除按钮),并在其中填充硬编码数据。 最后,您将使用Retrofit将视图连接到刚创建的REST后端。

在Android Studio中创建一个项目。 确保您使用的版本至少为Android Studio3 。除了确保包含Kotlin支持外,每个窗口均使用默认值。 为项目命名,随心所欲–我将其命名为“ Kotlin Crud”。 最后,选择一个清空活动

当您在顶部图标栏上按“播放”时,在运行Hello World时,您应该会看到它(您可以插入手机或在模拟器上运行。在线检查如何进行设置)。

Kotlin

如果您在使用Java之前制作了Android应用程序,您会注意到唯一的区别是主要活动:它称为MainActivity.kt ,而不是MainActivity.java ,并且代码看起来有些不同。

package demo

import android.support.v7.app.AppCompatActivity
import android.os.Bundle

class MainActivity : AppCompatActivity() {

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

区别如下:

  1. 该类未指定为public (在Kotlin中,这是默认设置)
  2. 用冒号指定类型: –类的类型为AppCompatActivity (或者,如您在Java中所说,它实现了 AppCompatActivity ),而savedInstanceState的类型为Bundle
  3. 方法只是被称为fun而不是function
  4. override不是注释
  5. 问号表示参数是可选的 (在Java中是不可能的)

最后一点是讨论Kotlin与Java重要性时讨论最多的问题之一:这是该语言确保null安全的多种方式之一。

导入其他Android库

您需要在应用程序的build.gradle文件中添加额外的库:一个用于回收站视图(将在第二个视图中使用),一个用于卡片视图,另一个用于浮动操作按钮。 将它们放在“ dependencies部分中的其他项旁边。

implementation 'com.android.support:design:27.1.1'
implementation 'com.android.support:cardview-v7:27.1.1'
implementation 'com.android.support:recyclerview-v7:27.1.1'

Android Studio应该要求您立即同步 。 单击该按钮,可以看到所有内容都构建正确。

注意:确保版本与其他支持库相同(例如appcompat-v7:27.1.1)。 另外,由于将使用内置图标(以后应避免使用),因此您还需要将以下内容放入build.gradledefaultConfig部分中。

vectorDrawables.useSupportLibrary = true

在Kotlin中添加图标

您需要一些按钮图标-一个用于添加 ,另一个用于刷新 。 转到“ 材料图标”站点,然后选择您喜欢的一个。 我正在选择添加按钮的一半。 当您单击它时,灰色和蓝色的下载部分应出现在左侧的按钮上。 单击灰色框“ 选定的图标”控件以打开下载选项。 现在应该有一个下拉列表,您可以在其中选择Android作为类型。

Kotlin

将颜色更改为白色,然后下载PNG选项。 将ZIP文件的内容提取到app/src/main (您应该看到ZIP文件中包含一个res文件夹)。

现在,您可以在布局中使用新图标。 它们被称为baseline_add_white_36 _add_white_36之类的东西。

最后,对循环图标(也是白色)执行相同的操作。

创建视图XML

每个列表项都需要一个XML视图。 将以下内容放入src/main/res/layout/list_item.xml

<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.CardView
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:card_view="http://schemas.android.com/apk/res-auto"

    android:id="@+id/card_view"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_marginBottom="3dp"
    android:layout_marginLeft="5dp"
    android:layout_marginRight="5dp"
    android:layout_marginTop="5dp"
    android:padding="3dp"
    card_view:cardElevation="2dp"
    card_view:cardMaxElevation="2dp">

    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:padding="5dp">

        <TextView
            android:id="@+id/name"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:padding="5dp"
            android:text="lastname"
            android:textSize="16dp" />

        <TextView
            android:id="@+id/btnDelete"
            android:layout_width="wrap_content"
            android:layout_height="35dp"
            android:layout_alignParentRight="true"
            android:drawableLeft="@android:drawable/ic_delete"
            android:padding="5dp" />

        <TextView
            android:id="@+id/btnEdit"
            android:layout_width="wrap_content"
            android:layout_height="35dp"
            android:layout_marginRight="2dp"
            android:layout_toLeftOf="@+id/btnDelete"
            android:drawableLeft="@android:drawable/ic_menu_edit"
            android:padding="5dp" />
    </RelativeLayout>

</android.support.v7.widget.CardView>

在这里,您使用的是Card View ,这是在Android中创建列表的常用方法。 几乎所有的XML都是布局设置,以确保正确对齐。 请注意您用于将其连接到我们的Kotlin文件的android:id值。 另外,我在编辑和删除按钮中使用了一些内置的Android图标。

注意:不建议这样做,因为这些图标可以在Android Studio版本之间进行更改–就像我们之前那样下载图标!

现在开始主要活动XML。 这是src/main/res/layout/activity_main.xml外观。

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <android.support.v7.widget.RecyclerView
        android:id="@+id/rv_item_list"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

    <android.support.design.widget.FloatingActionButton
        android:id="@+id/fab"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@drawable/baseline_add_white_36"
        android:layout_gravity="bottom|end"
        app:elevation="6dp"
        android:layout_alignParentBottom="true"
        android:layout_alignParentRight="true"
        android:layout_alignParentEnd="true"
        android:layout_margin="20dp"/>

</RelativeLayout>

非常简单。 现在,您已经在相对布局内获得了一个回收视图和一个浮动操作按钮,并已将baseline_add_white_36分配为该按钮的源。 请注意,回收站视图的ID为rv_list_item (您将很快使用它)。

将刷新添加到操作栏

要填写内容,请在操作栏上放置一个刷新按钮。 这需要在res/menu/buttons.xml新的XML:

<menu xmlns:android="http://schemas.android.com/apk/res/android"
      xmlns:app="http://schemas.android.com/apk/res-auto">

    <item
        android:id="@+id/refresh"
        android:icon="@drawable/baseline_loop_white_48"
        android:title="@string/refresh"
        app:showAsAction="ifRoom"/>

</menu>

请注意,它具有一个名为refresh的ID。 另外,我使用了Android Icons网站(白色变体)中的循环图标-您必须像以前一样下载它。 另外,我正在使用资源中的字符串,因此您必须更改res/values/strings.xml

<resources>
    <string name="app_name">Kotlin Crud</string>
    <string name="refresh">Refresh</string>
</resources>

在Kotlin的显示列表

现在使用我们的视图显示项目列表。 做到这一点的规范方法是相对较新的RecyclerView ,它取代了原来的ListViewRecyclerView的基本思想是仅创建足以在屏幕上显示的视图–如果屏幕可以容纳五个项目,则仅创建五个。 在列表中滚动时,这些视图将被重新使用(回收),并用适当的(新)值替换其内容。

您如何开始呢? 您需要的第一件事是一个bean。 我们称它为Movie.kt

package demo

data class Movie( val id: Int, val name: String )

注意:对于以下所有类,请确保packageMainActivity.ktpackage匹配。

那不容易吗? 接下来,您需要一个Adapter 。 这是一个具有三种方法的类:一种用于返回总共显示多少个项目( getItemCount() ),一种用于为特定项目创建Android View控件( onCreateViewHolder() ),以及一种使用以下方法填充现有视图的方法:您的数据实例( onBindViewHolder() )。

将其放入MovieAdapter.kt

class MovieAdapter : RecyclerView.Adapter() {

    var movies: ArrayList = ArrayList()

    init { refreshMovies() }

    class MovieViewHolder(val view: View) : RecyclerView.ViewHolder(view)

    override fun onCreateViewHolder(parent: ViewGroup,
                                    viewType: Int): MovieAdapter.MovieViewHolder {

        val view = LayoutInflater.from(parent.context)
                .inflate(R.layout.list_item, parent, false)

        return MovieViewHolder(view)
    }

    override fun onBindViewHolder(holder: MovieViewHolder, position: Int) {
        holder.view.name.text = movies[position].name
    }

    override fun getItemCount() = movies.size

    fun refreshMovies() {
        movies.clear()

        movies.add(Movie(0, "Guardians of the Galaxy"))
        movies.add(Movie(1, "Avengers: Infinity War"))
        movies.add(Movie(2,"Thor: Ragnorok"))

        notifyDataSetChanged()
    }
}

将其粘贴到Android Studio中时,它将以红色突出显示某些内容。 您需要多次按ALT-ENTER(在Mac上为Option + Enter)才能导入所需的导入。 最终,这是您应具有的进口清单:

import android.support.v7.widget.RecyclerView
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import kotlinx.android.synthetic.main.list_item.view.*

MovieAdapter.kt发生了很多事情。 除了刚刚实施来实现所需的三种方法RecylcerView.Adapter ,你创建了一个名为的属性movies中,一个列表,并初始化它init{}构造函数。 此外,您还声明了一个内部类,名为MovieViewHolder 。 这就是为每个需要显示的视图实例化的内容(在所讨论的示例中为五个视图)。 如您所见, onCreateViewHolder实际上返回此类型的对象。 该类非常简单–将一个View (现在也是一个属性)放入其构造函数中,并返回Holder类型的对象。 在使用onBindViewHolder填写数据时,将使用此对象-在我们的示例中,是设置显示的文本。

起初这似乎很复杂。 观察以下所有方面的好方法:它如何连接到您的主代码类(即MainActivity.kt ),以及如何连接到您在XML中定义的视图?

对于第一部分,主要活动现在应该是这样的:

class MainActivity : AppCompatActivity() {

    lateinit var adapter:MovieAdapter

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

        adapter = MovieAdapter()    
    
        rv_item_list.layoutManager = LinearLayoutManager(this)
        rv_item_list.adapter = adapter
    }
}

因此,在这里,您已将adapter定义为lateinit变量– lateinit告诉Kotlin您要在创建后的某个阶段初始化它,而不是Kotlin类中的默认值–通常,您必须立即初始化。

在构造函数中,您将适配器的实例分配给该属性(请注意,您无需在Kotlin中使用new ),并为rv_item_list分配两件事– LayoutManager (用于定位)和Adapter(我们将ve刚刚创建)。

我们应该谈论rv_item_list 。 这只是activity_main.xml内部控件的ID,特别是recyclerview。 通常,您需要使用findViewById (这对Android开发人员来说是一个痛苦),但是对于Kotlin,您只需指定其名称即可。 当Android Studio抱怨导入并且您ALT-ENTER(或与您的平台等效)时,它将自动导入kotlinx.android.synthetic.main.activity_main.* ,将所有ID引入命名空间。

最后,将以下两个功能添加到MainActivity

override fun onCreateOptionsMenu(menu: Menu): Boolean {
    val inflater = menuInflater
    inflater.inflate(R.menu.buttons, menu)
    return true
}

override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
    R.id.refresh -> {
        adapter.refreshMovies()
        Toast.makeText(this.baseContext, "Refreshed", Toast.LENGTH_LONG).show())
        true
    }
    else -> {
        super.onOptionsItemSelected(item)
    }
}

这将使您定义的菜单xml膨胀,并将按钮与适配器的刷新功能联系在一起(并方便举杯说它可以工作)。

就是这样! 运行我们的代码,您应该看到以下内容。

Kotlin

连线您的Android + Kotlin应用程式

接下来,您需要用来自API服务器的数据替换硬编码的值,并将不同的按钮连接到各自的API调用。 为此,您将使用Square的Retrofit库。

首先将以下内容添加到build.gradle依赖项中:

implementation 'com.squareup.retrofit2:retrofit:2.3.0'
implementation 'com.squareup.retrofit2:converter-gson:2.3.0'
implementation 'com.squareup.retrofit2:adapter-rxjava2:2.3.0'
implementation 'io.reactivex.rxjava2:rxandroid:2.0.1'
implementation 'com.squareup.okhttp3:logging-interceptor:3.9.1'

现在看一下在呼叫服务器以获取电影列表时发生的情况:

C:\Users\Karl>curl http://localhost:8080/movies
{
  "_embedded" : {
    "movies" : [ {
      "name" : "Skyfall",
      "id" : 1,
      "_links" : {
        "self" : {
          "href" : "http://localhost:8080/movies/1"
        },
        "movie" : {
          "href" : "http://localhost:8080/movies/1"
        }
      }
    }

我只显示了一个,因为它很长(Spring遵循一个称为HATEOAS的东西,它添加了指向json响应的链接)。 正如你所看到的,应对被包裹在一个_embedded对象,你的电影来为列表中的movies 。 您需要在Kotlin模型中表示这一点,以便Retrofit知道会发生什么。 将Movie.kt更改为此:

import com.google.gson.annotations.SerializedName

data class Movie( val id: Int, val name: String )
data class MovieList (
    @SerializedName("movies" )
    val movies: List
)
data class MovieEmbedded (
    @SerializedName("_embedded" )
    val list: MovieList
)

现在,您需要创建一个新类来设置Retrofit。 我们称它为MovieApiClient.kt

import io.reactivex.Completable
import io.reactivex.Observable
import retrofit2.Retrofit
import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory
import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.http.*

interface MovieApiClient {

    @GET("movies") fun getMovies(): Observable
    @POST("movies") fun addMovie(@Body movie: Movie): Completable
    @DELETE("movies/{id}") fun deleteMovie(@Path("id") id: Int) : Completable
    @PUT("movies/{id}") fun updateMovie(@Path("id")id: Int, @Body movie: Movie) : Completable

    companion object {

        fun create(): MovieApiClient {

            val retrofit = Retrofit.Builder()
                    .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
                    .addConverterFactory(GsonConverterFactory.create())
                    .baseUrl("http://10.0.2.2:8080/")
                    .build()

            return retrofit.create(MovieApiClient::class.java)
        }
    }
}

在这里,您可以使用批注及其预期的返回类型来定义所有端点( Completable ,是RxJava的一部分,仅表示未返回任何内容)。 您还声明了一个伴随对象 (类似于静态类),该对象使用API​​的详细信息实例化Retrofit构建器。 请注意,基本URL使用IP 10.0.2.2 ,该IP 10.0.2.2允许仿真器连接到localhost。

现在,在MovieAdapter更改标头以包括context属性(以便您可以为我们的API结果附加敬酒),以及添加使用先前的create()方法初始化的惰性客户端属性。

class MovieAdapter(val context: Context) :  RecyclerView.Adapter() {

    val client by lazy { MovieApiClient.create() }
    var movies: ArrayList = ArrayList()

懒惰接受一个函数(请注意大括号)并说“当某人首次尝试使用此属性时,请运行该函数并进行分配”。

要初始化上下文,请更改adapter initialize语句以包含主要活动上下文:

adapter = MovieAdapter(this.baseContext)

现在,将适配器中的refreshMovies()更改为以下内容:

fun refreshMovies() {
    client.getMovies()
        .subscribeOn(Schedulers.io())
        .observeOn(AndroidSchedulers.mainThread())
        .subscribe({ result ->
                movies.clear()
                movies.addAll(result.list.movies)
                notifyDataSetChanged()
        },{ error ->
                Toast.makeText(context, "Refresh error: ${error.message}", Toast.LENGTH_LONG).show()
                Log.e("ERRORS", error.message)
        })
}

因此,您正在使用客户端的getMovies()函数,该函数在MovieApiClient.kt的顶部MovieApiClient.kt 。 要了解这里发生的事情,需要自己进行整个讨论。 基本上,它使用的是反应式编程 ,这是一种将异步内容(例如,调用外部API)连接在一起的新方法。

对于其余的访问方法,将以下内容放在refreshMovies()

fun updateMovie(movie: Movie) {
    client.updateMovie(movie.id, movie)
        .subscribeOn(Schedulers.io())
        .observeOn(AndroidSchedulers.mainThread())
        .subscribe({ refreshMovies() }, { throwable ->
                Toast.makeText(context, "Update error: ${throwable.message}", Toast.LENGTH_LONG).show()
        })
}

fun addMovie(movie: Movie) {
    client.addMovie(movie)
        .subscribeOn(Schedulers.io())
        .observeOn(AndroidSchedulers.mainThread())
        .subscribe({ refreshMovies() }, { throwable ->
                Toast.makeText(context, "Add error: ${throwable.message}", Toast.LENGTH_LONG).show()
        })
}

fun deleteMovie(movie: Movie) {
    client.deleteMovie(movie.id)
        .subscribeOn(Schedulers.io())
        .observeOn(AndroidSchedulers.mainThread())
        .subscribe({ refreshMovies() }, { throwable ->
                Toast.makeText(context, "Delete error: ${throwable.message}", Toast.LENGTH_LONG).show()
        })
    }

在这里,您可以像以前一样使用Retrofit反应式调用,但是,当事情成功返回时(如果不这样做,请refreshMovies() ,而只需调用refreshMovies() )即可。

在Kotlin中显示对话框

您需要做的最后一件事是显示各种输入情况的对话框:删除,编辑和添加。 在这里,您将手动执行此操作,因此无需创建任何新的XML。

MainActivity.kt添加以下功能:

fun showNewDialog() {
    val dialogBuilder = AlertDialog.Builder(this)

    val input = EditText(this@MainActivity)
    val lp = LinearLayout.LayoutParams(
            LinearLayout.LayoutParams.MATCH_PARENT,
            LinearLayout.LayoutParams.MATCH_PARENT)
    input.layoutParams = lp

    dialogBuilder.setView(input)

    dialogBuilder.setTitle("New Movie")
    dialogBuilder.setMessage("Enter Name Below")
    dialogBuilder.setPositiveButton("Save", { dialog, whichButton ->
        adapter.addMovie(Movie(0,input.text.toString()))
    })
    dialogBuilder.setNegativeButton("Cancel", { dialog, whichButton ->
        //pass
    })
    val b = dialogBuilder.create()
    b.show()
}

此处, 对话框生成器用于显示标准弹出窗口。 您还手动添加了EditText控件,以便用户可以输入新名称。 侦听器使用“正”和“负”按钮(请参阅上一链接的“添加按钮”部分),并在发生“正”按钮(对话框确认)时调用适配器的addMovie函数。

要确保单击操作按钮时弹出此对话框,请将以下内容置于MainActivity.onCreate()函数的底部:

fab.setOnClickListener{ showNewDialog() }

这是其他对话框的代码,我们将它们放入MovieAdapter

fun showUpdateDialog(holder: MovieViewHolder, movie: Movie) {
    val dialogBuilder = AlertDialog.Builder(holder.view.context)

    val input = EditText(holder.view.context)
    val lp = LinearLayout.LayoutParams(
             LinearLayout.LayoutParams.MATCH_PARENT,
             LinearLayout.LayoutParams.MATCH_PARENT)
    input.layoutParams = lp
    input.setText(movie.name)

    dialogBuilder.setView(input)

    dialogBuilder.setTitle("Update Movie")
    dialogBuilder.setPositiveButton("Update", { dialog, whichButton ->
        updateMovie(Movie(movie.id,input.text.toString()))
    })
    dialogBuilder.setNegativeButton("Cancel", { dialog, whichButton ->
        dialog.cancel()
    })
    val b = dialogBuilder.create()
    b.show()
}

fun showDeleteDialog(holder: MovieViewHolder, movie: Movie) {
    val dialogBuilder = AlertDialog.Builder(holder.view.context)
    dialogBuilder.setTitle("Delete")
    dialogBuilder.setMessage("Confirm delete?")
    dialogBuilder.setPositiveButton("Delete", { dialog, whichButton ->
        deleteMovie(movie)
    })
    dialogBuilder.setNegativeButton("Cancel", { dialog, whichButton ->
        dialog.cancel()
    })
    val b = dialogBuilder.create()
    b.show()
}

要将它们连接起来,请将以下内容添加到onBindViewHolder方法中:

holder.view.btnDelete.setOnClickListener { showDeleteDialog(holder, movies[position]) }
holder.view.btnEdit.setOnClickListener { showUpdateDialog(holder, movies[position]) }

你几乎已经完成。 您只需要授予您的应用访问外部服务器(互联网)的权限。 将以下内容添加到AndroidManifest.xml<application>节点上方。

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".LoginActivity">

    <LinearLayout
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        android:orientation="vertical"
        android:gravity="center">

        <TextView
            android:id="@+id/auth_message"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:paddingBottom="8pt"
            android:text="Initializing authorization"
            style="@style/Base.TextAppearance.AppCompat.Medium"/>

        <ProgressBar
            android:id="@+id/progress_bar"
            style="@style/Widget.AppCompat.ProgressBar.Horizontal"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:indeterminate="true"/>

        <Button
            android:id="@+id/auth_button"
            style="@style/Widget.AppCompat.Button.Colored"
            android:text="Login"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:visibility="gone" />

    </LinearLayout>
</android.support.constraint.ConstraintLayout>

现在运行您的应用程序。 您应该能够添加,编辑和删除所有持久化到后端的内容,并且每个操作都带有对话框以确认或提供详细信息。 您刚刚制作了完整的CRUD客户端应用程序解决方案!

Kotlin

使用Android和Kotlin为安全的移动应用程序添加身份验证

大多数现代应用程序都需要一定级别的安全性,因此有必要知道如何快速轻松地添加身份验证。 为此,您将使用OktaAppAuth包装器库。

为什么选择Okta?

Okta的目标是使身份管理比您以往更加轻松,安全和可扩展。 Okta是一项云服务,允许开发人员创建,编辑和安全地存储用户帐户和用户帐户数据,并将它们与一个或多个应用程序连接。 我们的API使您能够:

注册一个永久免费的开发人员帐户 ,完成后,创建一个新的本机应用程序,并记下客户端ID和重定向URI。

添加OAuth 2.0授权服务器

首先,您需要将服务器转换为OAuth资源(由Okta管理)。 将以下内容添加到build.gradle

compile 'com.okta.spring:okta-spring-boot-starter:0.5.0'
compile 'org.springframework.boot:spring-boot-starter-security'
compile 'org.springframework.security.oauth:spring-security-oauth2:2.2.0.RELEASE'
compile 'org.springframework.security.oauth.boot:spring-security-oauth2-autoconfigure:2.0.1.RELEASE'

现在,将以下导入添加到您的Application.kt

import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer

还可以使用@EnableResourceServer注释该类。 然后创建一个名为src/main/resource/application.properties

okta.oauth2.issuer=https://{yourOktaDomain}/oauth2/default
okta.oauth2.clientId={clientId}

登录时,可以在Okta管理网站的“应用程序”选项卡中获取客户端ID值。

现在,当您重新启动Spring Boot并尝试匿名访问您的API时,您将收到授权错误。

C:\Users\Karl>curl localhost:8080/movies
{"error":"unauthorized","error_description":"Full authentication is required to access this resource"}

添加Android AppAuth插件

要使我们的应用使用Okta,您需要使用Android的AppAuth插件。 首先创建一个新的活动来容纳登录过程。

转到新建 -> 活动 -> 空活动,并将其LoginActivity 。 在创建的activity_login.xml放置以下内容:

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".LoginActivity">

    <LinearLayout
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        android:orientation="vertical"
        android:gravity="center">

        <TextView
            android:id="@+id/auth_message"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:paddingBottom="8pt"
            android:text="Initializing authorization"
            style="@style/Base.TextAppearance.AppCompat.Medium"/>

        <ProgressBar
            android:id="@+id/progress_bar"
            style="@style/Widget.AppCompat.ProgressBar.Horizontal"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:indeterminate="true"/>

        <Button
            android:id="@+id/auth_button"
            style="@style/Widget.AppCompat.Button.Colored"
            android:text="Login"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:visibility="gone" />

    </LinearLayout>
</android.support.constraint.ConstraintLayout>

现在,在AndroidManifest.xml内部交换登录活动和主要活动的名称,以便在启动时启动登录。

Kotlin

您已经添加了一个进度条,该进度条将一直保持到与Okta的授权连接建立。 完成后,将其隐藏,然后显示一个登录按钮。 为此,您需要更改LoginActivity.kt 。 但首先,将以下内容添加到build.gradle

implementation 'com.okta.android:appauth-android:0.1.0'

这将拉入Android的Okta AppAuth库。 您还需要将support lib版本更改为25.3.1 ,以使其与此库兼容,因此请将对其他版本(例如27.1.1 )的任何引用更改为该版本。 另外,将minSdkVersion更改为16并将目标SDK更改为25 。 最后,将以下内容添加到defaultConfig

android.defaultConfig.manifestPlaceholders = [
    // match the protocol of your "Login redirect URI"
    "appAuthRedirectScheme": "com.oktapreview.dev-628819"
]

现在一切都应该建立良好。

接下来,将LoginActivity.kt的内容更改为以下内容:

class LoginActivity : AppCompatActivity() {

    private var mOktaAuth: OktaAppAuth? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        mOktaAuth = OktaAppAuth.getInstance(this)

        setContentView(R.layout.activity_login)

        mOktaAuth!!.init(
                this,
                object : OktaAppAuth.OktaAuthListener {
                    override fun onSuccess() {
                        auth_button.visibility = View.VISIBLE
                        auth_message.visibility = View.GONE
                        progress_bar.visibility = View.GONE
                    }

                    override fun onTokenFailure(ex: AuthorizationException) {
                        auth_message.text = ex.toString()
                        progress_bar.visibility = View.GONE
                        auth_button.visibility = View.GONE
                    }
                }
        )

        val button = findViewById(R.id.auth_button) as Button
        button.setOnClickListener { v ->
                val completionIntent = Intent(v.context, MainActivity::class.java)
                val cancelIntent = Intent(v.context, LoginActivity::class.java)
    
                cancelIntent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP
    
                mOktaAuth!!.login(
                        v.context,
                        PendingIntent.getActivity(v.context, 0, completionIntent, 0),
                        PendingIntent.getActivity(v.context, 0, cancelIntent, 0)
                )
        }
    }
}

这将尝试初始化Okta身份验证,并在完成后隐藏进度栏和消息,并显示登录按钮。 现在,如果您尝试运行它,将会看到一个错误。 我们需要在项目中添加Okta应用程序详细信息

配置您的Okta应用

创建app/src/main/res/raw/okta_app_auth_config.json并将以下内容放入其中:

{
  "client_id": "{clientId}",
  "redirect_uri": "{redirectUriValue}",
  "scopes": ["openid", "profile", "offline_access"],
  "issuer_uri": "https://{yourOktaDomain}/oauth2/default"
}

您应该从Okta管理控制台中的应用程序获取clientIdredirectUriValues

现在,当您重新启动应用程序时,应该会看到一个登录按钮。

Kotlin

按下它将带您到预建的Okta登录屏幕。 如果您使用Okta帐户上的凭据登录,则将进入我们的主要活动,但该列表将为空-并会出现401错误的Toast弹出窗口(未经授权的访问)。

将授权添加到改造中

您需要从AppAuth库中获取访问令牌,并在进行API调用时将其传递给Retrofit。 在MainActivity.kt内部添加以下功能:

fun readAuthState(): AuthState {
    val authPrefs = getSharedPreferences("OktaAppAuthState", Context.MODE_PRIVATE)
    val stateJson = authPrefs.getString("state", "")
    return if (!stateJson!!.isEmpty()) {
        try {
            AuthState.jsonDeserialize(stateJson)
        } catch (exp: org.json.JSONException) {
            Log.e("ERROR",exp.message)
            AuthState()
        }

    } else {
        AuthState()
    }
}

它使用共享的首选项来提取Okta存储的授权数据。

现在,更改适配器的标头,以使其接受访问令牌作为字符串。

class MovieAdapter(val context: Context, val token: String?)

然后,当您在MainActivity实例化它时,请从auth状态对象传递令牌。

adapter = MovieAdapter(this.baseContext, readAuthState().accessToken)

现在,您需要在MovieApiClient.kt更改电影的get调用,以包括授权标头。

@GET("movies") fun getMovies(@Header("Authorization") token:String): Observable

返回适配器,将refreshMovies()更改为使用此新标头-带有新令牌和Bearer前缀:

fun refreshMovies() {
    client.getMovies("Bearer $token")
}

您需要对其他方法(添加,删除,更新)进行相同的更改,以使这些功能可以与经过身份验证的后端一起使用。

应该是这样。 重新部署该应用程序,您应该像以前一样进入列表–这次已通过验证!

恭喜你! 您刚刚制作了一个完整的客户端-服务器解决方案,它具有健壮且兼容的REST后端,以及使用最新技术(全部采用中央行业标准身份验证)在前端使用Android应用程序。 您可以在GitHub上找到本教程中创建的示例。

了解有关Android,Java和安全身份验证的更多信息

我们还编写了其他一些很棒的Spring Boot和Android教程,如果您有兴趣的话,请查看它们。

最后,如果您想了解更多关于Kotlin一个伟大的地方看看,一旦你与它的工作对一个位是Kotlin成语页面

如有任何疑问,请随时在下面发表评论,或在我们的Okta开发者论坛上向我们提问。 如果您想查看更多类似的教程,请在Twitter @oktadev上关注我们!

“我喜欢编写身份验证和授权代码。” 〜从来没有Java开发人员。 厌倦了一次又一次地建立相同的登录屏幕? 尝试使用Okta API进行托管身份验证,授权和多因素身份验证。

最初于2018年9月11日在Okta开发者博客上发布了``使用Kotlin在Android中构建基本的CRUD应用程序''

翻译自: https://www.javacodegeeks.com/2018/09/kotlin-build-basic-android-crud-app.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值