반응형

테스트 더블(Test Double)이란 저자인 xUnit Test Patterns의 제라드 메스자로스(Gerard Meszaros)가 만든 용어로 테스트를 진행하기 어려울 때 대신 테스트를 진행할 수 있도록 도와주는 객체를 말합니다.

테스트 객체는 크게 5개의 종류로 나뉩니다요.

 

Dummy

Martin Fowler에 의하면 Dummy는 전달되지만 실제로는 쓰지 않을 객체를 말합니다. 그저 init할 때 parameter를 채우기 위한 용도로만 쓰이구요. 내부 구현은 하지 않습니다.

protocol SomeProtocol {
	func doSomething()
	func logSomething() -> String
}

class DummySomeProtocol: SomeProtocol {
	func doSomething() {}
	func logSomething() -> String {
		return ""
	}
}

Stub

Martin Fowler에 의하면 Stub은 미리 정한 값(예를 들면 더미값)을 테스트 시에 그대로 반환하는 역할을 합니다.

// 예제
class RecentKeywordsStub: RecentKeywordsRepository {
	let keywords = ["a", "b", "c"]
	func getRecentKeywords() -> [String] {
		return keywords
  }
}
// 테스트
func test_getRecentKeywords() {
   XCTAssertEqual(sut.getRecentKeywords(), sut.keywords)
}

Spy

스텁과 같은 역할을 하며 더불어 메소드 호출 여부와 호출 횟수를 기록하는 역할을 합니다. 솔직히 스파이는 써보진 않았습니다. 이 행위를 검증하는 역할을 Mock을 만들어서 검증을 많이 하였습니다. 그래도 일단 아래와 같이 사용할 수 있다고는 하네요. 테스트 마지막 부분을 보면 호출이 정확히 되었는지를 확인하고 있습니다

class QuotesServiceSpy: QuotesService {
    private (set) var quoteCalls: [[String]] = []
    private let stocks: [Stock]
    private let error: Error?
    
    init(stocks: [Stock] = [], error: Error? = nil) {
        self.stocks = stocks
        self.error = error
    }
    
    func getQuotes(symbols: [String], 
                   completion: @escaping (Result<[Stock], Error>) -> Void) {
        quoteCalls.append(symbols)
        if let error = error {
            completion(.failure(error))
        } else {
            completion(.success(stocks))
        }
    }
}

class WatchlistViewModelTests: XCTestCase {    
    func test_getWatchlistWithSymbols_callGetQuotes() {
        let stockSymbols = ["AAPL", "MSFT", "GOOG"]
        let watchlistStore = WatchlistStoreStub(stockSymbols: stockSymbols)
        let quotesService = QuotesServiceSpy()

        let sut = WatchlistViewModel(store: watchlistStore, 
                                     service: quotesService)
        sut.getWatchlist()

				// 여기서 정확히 호출이 되었나를 확인해 테스트 통과/실패
        XCTAssertEqual(quotesService.quoteCalls[0], stockSymbols)
    }
}

Fake

Martin Fowler에 의하면 Fake 객체는 동작하는 구현을 실제로 가지고 있지만 실제 상품으로 내기보다는 테스트를 위해 간편하게 만든 구현을 가지고 있습니다

아래 Fake만보기의 start함수의 구현을 예로 들면 실제 걸음수를 가지고 와서 업데이트 하는 것이 아니라 1초마다 걸음 수를 올리는 것처럼 말이죠. 이렇게 하면 실제 디바이스가 아닌 시뮬레이터에서 테스트를 하더라도 걸음수가 올라가는지 테스트를 할 수 있겠죠?!

import Foundation

class SimulatorPedometer: Pedometer {
  var timer: Timer?
  var distance = 0.0

  func start()
    timer = Timer(timeInterval: 1, repeats: true) { _ in
      self.distance += 1
    }
    RunLoop.main.add(timer!, forMode: RunLoop.Mode.default)
  }
}

Mock

Martin Fowler에 의하면 Mock은 함수들이 원하는데로 호출되었는지를 검증할 때 쓴다고 합니다. 으음.. 저는 여기까지만 읽었을 때에는 Spy와 너무 비슷한 느낌이 드는데요..? 뭐가 다른건지 참.

다음에 네트워크를 Mocking하는 것을 한 번 정리해보려고합니다. 맨 밑에 이미지에 각 테스트 더블들의 범위(?)를 나타낸 사진이 있는데 솔직히 저는 Mock은 행위 검증이든 상태검증이든 다 할 수 있는 것 같은 느낌이 들더라구요.

아무튼 테스트 더블의 종류들의 경계가 살짝 모호한 것 같고 칼로 딱 나눠자를 수 없다는 생각이 들었는데 저는 아래 그림처럼 이해를 해보니까 조금 더 도움이 되더랍니다.ㅎㅎㅎㅎ.. 누군가에게 도움이 되었기를

*https://docs.microsoft.com/en-us/archive/msdn-magazine/2007/september/unit-testing-exploring-the-continuum-of-test-doubles*

Although these types seem distinct in theory, the differences become more blurred in practice. For that reason, I think it makes sense to think of test doubles as inhabiting a continuum, as illustrated in Figure 2. - MSDN Magazine -

 

참고 및 출처

https://medium.com/@pena.fernan/test-doubles-by-example-in-swift-558e9f47de52

https://hudi.blog/test-double/

https://mokacoding.com/blog/swift-test-doubles/

https://www.martinfowler.com/bliki/TestDouble.html

https://codinghack.tistory.com/92

https://sujinnaljin.medium.com/swift-mock-을-이용한-network-unit-test-하기-a69570defb41

https://www.swiftbysundell.com/articles/mocking-in-swift/

반응형
반응형

안녕하세요 keibi입니다.

처음으로 인사를 하고 글을 쓰려고 하는데 소개부터 한참 시간이 걸리네요

 

아무튼...

오늘부터 SwiftUI 관련 공부하며 정리를 시작합니다.

개인프로젝트를 하려고 하는데 최신 프로그래밍 언어의 흐름도 공부할 겸 SwiftUI와 Combine로 하기로 했거든요.

(SwiftUI로 preview그릴 때 맥북이 힘겨워하는 게 느껴지네요 5년 썼으니 바꿀 때가 된 듯합니다.)

 

 

맨 처음 짚고 넘어갈 것은 왜 SwiftUI인가?! 에 대해서 기존 UIKit과 비교해서 정리해보려고 합니다.

새로 나온 기술은 기존 기술과 다른 점이 있고 그 다른 점이 필요해서 나온 경우가 많으니까요

 

선언형과 명령형??? What과 How???

자료를 모으고 조사하면서 가장 먼저 정리해야겠다 싶은 것은 이 두 개념인 것 같아요.

 

먼저 선언형 프로그래밍은 명령형 프로그래밍과 대비되는 개념으로

프로그램이 수행할 동작을 순차적으로 나열하는 것이 아니라

결과적으로 구현하고자 하는 것을 '명세'하는 형태로 작성하는 프로그래밍 방법입니다.

 

WHAT(구현하고자 하는 것을 명세) vs HOW(수행할 동작을 순차적)

이게 무슨 말이죠??

 

 

예시를 들어볼게요

아내가 저에게 저녁밥을 차려야 하니 계란 10구짜리 하나 사 오라고 합니다

(아내 왈, 여보 간단하게 계란 장조림만 다시 해서 밥 묵자)

 

 

명령형 : 집으로 오는 길에 왼쪽을 확인한다

              OO마트가 문을 열었는지 확인하고 들어간다

              들어가서 왼쪽 신선식품 쪽으로 걸어간다

              신선식품 코너 옆에 있는 10구짜리 계란을 집는다

              결제할 곳으로 가지고 간다

              카드로 결제한다

              비닐에 계란을 담아서 집에 들고 간다

선언형 : 신선한 계란 10구 하나 부탁해~

 

 

차이가 보이시나요??

선언형은 무엇에 해당하는 '계란'에만 관심이 있고

명령형은 어떻게 계란을 살 수 있는지를 다 적는다는 차이가 있죠

 

저는 여기서 답답한 마음에 질문이 하나 생기더라고요.

 

아니 어쨌든 계란은 어딘가에 들려서 카드든 현금이든 내고

사 와야 하는데 필요한 다른 정보는 어디에 있는 거지??

 

답변부터 하자면 SwiftUI가 필요한 정보를 알아서 해결해 준다!입니다

선언형 방식이 동작하기 위해서는 '어떻게'에 해당하는 (마트 위치나, 결제 수단 같은 것들) 것들이 추상화되어 있어야 해요

 

아래와 같은 화면을 UIKit과 SwiftUI로 구현해서 차이점을 알아볼게요. 배경에 이미지와 글자만 있는 화면입니다.

먼저 SwiftUI

import SwiftUI

struct ContentView: View {
    var body: some View {
        ZStack {
            Color.purple
                .ignoresSafeArea()
            VStack {
                Image(systemName: "globe")
                    .imageScale(.large)
                    .foregroundColor(.black)
                Text("Hello, world!")
                    .font(.largeTitle)
            }
        }
    }
}

그리고 UIKit

import UIKit

class ViewController: UIViewController {
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
        
        view.backgroundColor = .purple
        
        let imageView = UIImageView(image: UIImage(systemName: "globe"))
        imageView.tintColor = .white
        imageView.scalesLargeContentImage = true
        imageView.translatesAutoresizingMaskIntoConstraints = false
        
        let textLabel = UILabel()
        textLabel.translatesAutoresizingMaskIntoConstraints = false
        textLabel.text = "Hello, World!"
        textLabel.font = .preferredFont(forTextStyle: .largeTitle)
        textLabel.textColor = .white
        
        view.addSubview(imageView)
        view.addSubview(textLabel)
        
        NSLayoutConstraint.activate([
            imageView.centerXAnchor.constraint(equalTo: self.view.centerXAnchor),
            imageView.centerYAnchor.constraint(equalTo: self.view.centerYAnchor, constant: -20)
        ])
        NSLayoutConstraint.activate([
            textLabel.centerXAnchor.constraint(equalTo: self.view.centerXAnchor),
            textLabel.centerYAnchor.constraint(equalTo: self.view.centerYAnchor, constant: 20)
        ])
    }
}

확실히 기존 UIKit은 어떻게까지 일일이 개발자가 쓰다 보니 코드의 양이 좀 더 많은 것을 볼 수 있고

SwiftUI는 많은 것이 미리 정의가 되어 있으니 보다 짧은 코드로 쓸 수 있는 것을 알 수 있죠

 

여기서 몇 가지 더 쓸 거 없나 끄적끄적거리다가 VStack을 뷰의 상단에 붙여보고 싶어졌어요.

그런데 VStack이 제공하는 옵션은 딱 2개뿐이더라고요 alignment, spacing

@frozen public struct VStack<Content> : View where Content : View {

    /// Creates an instance with the given spacing and horizontal alignment.
    ///
    /// - Parameters:
    ///   - alignment: The guide for aligning the subviews in this stack. This
    ///     guide has the same vertical screen coordinate for every subview.
    ///   - spacing: The distance between adjacent subviews, or `nil` if you
    ///     want the stack to choose a default distance for each pair of
    ///     subviews.
    ///   - content: A view builder that creates the content of this stack.

확실히 많은 부분이 추상화된 만큼 직접 건드릴 수 있는 부분은 적어진 것 같아요. 그럼 이럴 땐 어떻게 하냐?!

VStack {
    Image(systemName: "globe")
        .imageScale(.large)
        .foregroundColor(.white)
    Text("Hello, world!")
        .font(.largeTitle)
        .foregroundColor(.white)
    Spacer()
}

요렇게 밑에 길이가 남은 공간에 따라 줄어드는 Spacer()를 활용하면 요렇게 위에 붙일 수가 있습니다.(익숙해지려면 약간의 노하우(?)같은 지식이 필요하겠네요ㅎㅎ)

 

 

읽어주셔서 감사합니다!

언제든 잘못된 정보는 꼭 답글로 달아주시면 감사하겠습니다 ( _ _ )

반응형
반응형

 

iOS 앱을 만들 때 모든 것을 다 Swift로 짜면 좋으련만 프로젝트를 진행하다보면 그렇지 못할 경우가 간혹 생깁니다.

Web RTC를 쓴다던지 게임 엔진을 같이 사용해야 한다던지 등등..

 

회사에서 cocos2dx 게임엔진이 메탈을 지원하기 때문에 cocos2dx를 swift와 같이 사용할 일이 생겼습니다. 

이 때 처음으로 C++ 소스를 Swift와 같이 사용하게 되며 공부를 하였는데 어떻게 사용할 수 있는지 그 방법을 다시 정리해보았습니다.

 

결론을 말씀드리면 C++는 Objective C++로 Wrapping해서 Swift에서도 쓸 수 있습니다.

 

Photo credit: Cecilia Humlelu

Swift는 Objective C(++)과 C는 직접 상호동작이 가능하지만 C++은 그렇지 못한 걸 알 수 있습니다.

 

 

아래 순서대로 진행할 생각입니다.

1. Xcode 프로젝트 Swift로 생성

2. C++ 파일 작성

3. Objective C++ Wrapper만들기

4. 브릿지 헤더에 만든 Objective C++의 헤더 추가하기

5. Swift에서 호출하기

 

1. Xcode 프로젝트 Swift로 생성

편한 이름으로 언어는 Swift를 선택하시고 iOS 앱 프로젝트를 생성해주세요. 저는 CppTest로 하였습니다.

 

 

2. C++ 파일 작성

[File] -> [New] -> [File]을 눌러(혹은 ⌘N) 새로운 C++ source file을 만들어주세요. 

생성 시 Also create a header file을 체크하고 만들어주세요. 저는 MyCpp로 하였습니다.

그러면 Bridge header를 만들 것인지 묻습니다. 만들어주세요

브릿지 헤더를 만듭니다

다 만들고 나면 아래와 같이 파일들이 생성될 것입니다.

C++ 파일 생성

 

*.hpp 파일을 열고 아래와 같이 입력해주세요.

// MyCpp.hpp

#include <string>
#include <iostream>

class MyCpp {
public:
    MyCpp();							// 생성자
    MyCpp(const std::string &text);				// 변수를 입력 받는 생성자
    ~MyCpp();							// 소멸자(Destructor)

public:
    void sayHello();						// Hello world를 출력하는 함수

    void setText(const std::string &text);			// m_text의 getter & setter
    const std::string &getText();
    
    void setNumber(const int number);				// m_number의 getter & setter
    int getNumber();

private:
    std::string m_text;						// 문자열 변수
    int m_number;						// 정수값 변수
};

 

 

 

*.cpp 파일을 열고 아래와 같이 입력해주세요.

//  MyCpp.cpp


#include "MyCpp.hpp"

MyCpp::MyCpp():m_text() {}
MyCpp::MyCpp(const std::string &text): m_text(text) {}
MyCpp::~MyCpp() {}

void MyCpp::sayHello()
{
    std::cout << "Hello world!" << std::endl;
    
}

void MyCpp::setText(const std::string &text)
{
    m_text = text;
}
const std::string &MyCpp::getText()
{
    return m_text;
}

void MyCpp::setNumber(const int number)
{
    m_number = number;
}

int MyCpp::getNumber()
{
    return m_number;
}

 

설명이 필요없는 아주 기본적인 각각 문자열과 정수값을 설정하고 반환하는 함수들입니다.

 

3. Objective C++ Wrapper만들기

새로운 objective-c 파일을 만들어주세요. 그리고 확장자를 .mm으로 바꿉니다. 저는 이름을 CWrapper.mm로 하였습니다.

방금 만든 파일의 헤더(header)도 만들어주세요. 제 파일은 CWrapper.h입니다.

Objective-C++을 생성하고 나서의 모습입니다. 아래쪽에 *.mm과 *.h가 하나씩 추가되었습니다.

 

*.h를 작성해 주세요

//  CWrapper.h

#import <Foundation/Foundation.h>

@interface CWrapper : NSObject
- (instancetype)initWithText:(NSString*)text;
- (void)helloWorld;

- (void)setText:(NSString*)text;
- (NSString*)getText;

- (void)setNumber:(int)number;
- (int)getNumber;
@end

 

C++에 있던 것과 마찬가지로 문자열을 입력받는 상속자와 문자열, 정수값을 설정하고 반환하는 함수들을 선언하였습니다.

 

이제 *.mm을 작성해 주세요.

만약 복붙이 아니라 직접 입력을 하신다면 헤더 파일을 만들어서 자동완성에 함수가 뜨는 것을 볼 수 있습니다.

#import "CWrapper.h"
#include "MyCpp.hpp"

@interface CWrapper()
@property MyCpp *cppItem;
@end
@implementation CWrapper
- (instancetype)init
{
    self = [super init];
    self.cppItem = new MyCpp();
    return self;
}
- (instancetype)initWithText:(NSString*)text
{
    self = [super init];
    self.cppItem = new MyCpp(std::string([text cStringUsingEncoding:NSUTF8StringEncoding]));
    
    return self;
}

- (void)helloWorld
{
    printf("Hello world");
}

- (void)setText:(NSString *)text
{
    self.cppItem->setText(std::string([text cStringUsingEncoding:NSUTF8StringEncoding]));
}

- (NSString *)getText
{
    return [NSString stringWithUTF8String:self.cppItem->getText().c_str()];
}

- (void)setNumber:(int)number
{
    self.cppItem->setNumber(number);
}

- (int)getNumber
{
    return self.cppItem->getNumber();
}
@end

 

 

4. 브릿지 헤더에 만든 Objective C++의 헤더 추가하기

아래와 같이 방금 만든 Objective C++의 헤더를 추가해주세요. 이제 준비는 끝났습니다.

[project name]-Bridging-Header.h

// CppTest-Bridging-Header.h

#import "CWrapper.h"

 

5. Swift에서 호출하기

 

ViewController.swift

//  ViewController.swift

import UIKit

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        
        let cppItem = CWrapper();

        cppItem.helloWorld()
        print(cppItem.getText())
        cppItem.setText("This is test string")
        print(cppItem.getText())

        
        let cppItem2 = CWrapper(text: "Hi my name is cpp");
        print(cppItem2?.getText())
        
        cppItem2?.setNumber(33)
        print(cppItem2?.getNumber())
    }
}

 

실행하면 콘솔에 다음과 같은 로그를 볼 수 있습니다.

(std::endl은 콘솔에서는 동작하지 않네요ㅠ ㅎㅎ)

 

[참조]

(첫 번째 사진)

https://medium.com/@cecilia.humlelu/using-c-c-and-objective-c-frameworks-in-swift-apps-6a60e5f71c36

 

Using C, C++ and Objective-C frameworks in Swift apps

I did a talk in try!Swift Tokyo this year about using C, C++ and Objective-C frameworks in Swift apps. The presentation was quite concise…

medium.com

https://developer.apple.com/documentation/swift/imported_c_and_objective-c_apis/importing_swift_into_objective-c

 

Importing Swift into Objective-C | Apple Developer Documentation

Article Importing Swift into Objective-C Access Swift types and declarations from within your Objective-C codebase. OverviewYou can work with types declared in Swift from within the Objective-C code in your project by importing an Xcode-generated header fi

developer.apple.com

https://www.ekreative.com/blog/using-c-code-and-libraries-in-applications-written-in-swift/

 

Using C ++ code and libraries in applications written in Swift

Swift is a cool high-level programming language, but it’s still quite young and doesn’t have as many libraries and components as Objective-C.

www.ekreative.com

 

반응형
반응형

이 글은 Youtube Flutter강의를 번역하였습니다.

기본적으로 영상의 내용과 같습니다.

다만 2년 전 영상이라 그런지 그대로 따라할 경우 Multidex와 관련한 오류가 발생해 빌드가 되지 않는데 그 부분만 추가하였습니다.

언젠가 다시 Firestore를 활용해 앱을 만들 때 영상을 다시 볼 수 없으니 해야할 것들만 딱 정리하도록 하겠습니다. 

 

[참고 내용]

Multidex에서 dex는 Dalvik Executable의 약어입니다. android 앱은 기본적으로 Simple하고 Small하게 만들어지도록 되어있어서 65,536개의 Method들만 사용할 수 있도록 되어있다고 합니다. Method들의 수가 이 숫자를 넘어갈 경우 Multidex 오류가 발생한다고 합니다. 이 경우 앱 수준의 build.gradle에서 multiDexEnabled true를 바꿔주지 않으면 iOS앱은 잘 되는데 Android앱은 빌드가 안되는 오류가 발생합니다.

 

 

1, Firestore 저장소 만들기

firebase에 접속하여 프로젝트를 추가합니다

 

 

2. Android studio에서 flutter 앱 만들기

이름은 jk2b_test로 만들었습니다

패키지 이름은 com.jk2b.jk2btest로 만들었습니다. 추후에 Firebase에서 앱을 등록할 때 사용합니다.

따로 적지 않아도 [app]->[src]->[main] 밑의 AndroidManifest.xml에서 확인할 수 있습니다

 

3. Firebase에 iOS와 Android 앱 추가하기

저는 jk2b-test로 만들었습니다

앱은 여러개를 추가할 수 있습니다. 먼저 iOS를 추가하겠습니다

앱 등록 후 구성파일을 다운로드 합니다. 그리고 xcode를 엽니다(Runner.xcworkspace)

방금 다운 받은 plist파일을 넣어줍니다.

 

다음 Android를 추가합니다

똑같이 구성파일을 다운로드 받고 몇가지 코드를 넣어줍니다. 프로젝트 수준의 build.gradle에 dependencies를 추가해주고

classpath 'com.google.gms:google-services:4.3.3'

앱 수준의 build.gradle에 apply 구문도 추가해줍니다.

apply plugin: 'com.google.gms.google-services'

defaulfConfig 부분에 multiDexEnabled 설정도 넣어줍니다

defaultConfig {
  // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
  applicationId "com.jk2b.jk2btest"
  minSdkVersion 16
  targetSdkVersion 28
  versionCode flutterVersionCode.toInteger()
  versionName flutterVersionName
  testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
  multiDexEnabled true
}

 

 

4. Firebase에 Database 만들고 규칙 수정

보통 테스트 모드로 만드는데 저는 별 이유 없이 프로덕션 모드에서 시작하였습니다. 그래서 규칙을 수정해주어야 나중에 접근이 가능합니다. 데이터베이스 지역은 일본에 있는 동아시아로 하였습니다.(아무래도 거리가 가까우니 미국보단 빠를 것 같아서)

5. 데이터 추가

지금 만들앱에서는 문서이름은 필요가 없고 컬렉션 이름은 앱에서 알고 있어야합니다.

컬렉션은 Test로 만들고 문서 이름은 랜덤하게 생성해주는 기능을 사용했습니다.

필드에 name과 votes를 추가해줍니다

6. 코드 작성

이제 widget을 만들고 Firebase를 가져오는 코드를 만들어보겠습니다

 

main.dart를 엽니다.

cloud_firestore.dart를 import하고 firestore instance를 가져옵니다.

import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/material.dart';

void main() => runApp(MyApp());


class MyApp extends StatefulWidget {
  @override
  _MyAppState createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  Firestore firestore = Firestore.instance;
  @override

  Widget build(BuildContext context) {
    return MaterialApp(
      title: "Band Name Test",
      theme: ThemeData.dark(),
      home: const MyHomePage(title: 'Band names'),
    );
  }
}

 

화면에 보여줄 위젯을 생성합니다

class MyHomePage extends StatelessWidget {
  const MyHomePage({Key key, this.title}) : super(key: key);

  final String title;

  @override
  Widget build(BuildContext context) {
    // TODO: implement build
    return Scaffold(
      appBar: AppBar(
        title: Text(title),
      ),
      body: StreamBuilder(
          stream: Firestore.instance.collection('Test').snapshots(),
          builder: (context, snapshot) {
            if (!snapshot.hasData) return const Text('Loading...');
            return ListView.builder(
              itemExtent: 80.0,
              itemCount: snapshot.data.documents.length,
              itemBuilder: (context, index) =>
                  _buildListItem(context, snapshot.data.documents[index]),
            );
          }
      ),
    );
  }
}

 

_buildListItem 메소드를 만들면 끝!

  Widget _buildListItem(BuildContext context, DocumentSnapshot document) {
    return ListTile(
      title: Row(
        children: <Widget>[
          Expanded(
            child: Text(
              document['name'],
              style: Theme.of(context).textTheme.headline,
            ),
          ),
          Container(
            decoration: const BoxDecoration(
              color: Color(0xffddddff),
            ),
            padding: const EdgeInsets.all(10.0),
            child: Text(
              document['votes'].toString(),
              style: Theme.of(context).textTheme.display1,
            ),
          )
        ],
      ),
      onTap: () {
        print("should increase votes here.");
        document.reference.updateData({
          'votes': document['votes'] + 1
        });
      },
    );
  }

 

 

[관련 링크]

https://www.youtube.com/watch?v=DqJ_KjFzL9I&t=597s

Multidex관련

https://www.linkedin.com/pulse/android-multidex-how-deal-chris-sullivan

반응형

+ Recent posts